ModuleNotFoundError:没有名为“winrt.windows.foundation.collections”的模块

问题描述 投票:0回答:1

我有一个 Tkinter 程序,它使用多个库(matplotlib、numpy、pygame 等)以及 bleak BLE 库。我使用 PyInstaller 在 Windows 上创建了一个 exe,大多数程序都可以正常工作,除非我尝试使用 bleak 库。我收到异常和回溯提及

ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
我不知道如何解决这个问题,希望得到一些帮助!

这是我收到的消息的示例:

2024-10-23 14:09:21,946 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...0026A917BBC30>, <winrt._winrt...0026A8DFC4610>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...0026A917BBC30>, <winrt._winrt...0026A8DFC4610>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run*emphasized text*
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'

__________________ 更新 _________________

这是一个超过 3500 行的大型程序,因此我提取了一个简单的 ble 模块来重现错误。

from __future__ import annotations
import asyncio
import threading
import tkinter as tk
from bleak import BleakScanner, BleakClient
from bleak.backends.device import BLEDevice
from bleak.backends.service import BleakGATTService, BleakGATTServiceCollection
import bleak.exc
import logging
import copy
import seq, dmutil, dme

# from concurrent.futures import ThreadPoolExecutor
# from bleak.exc import BleakError
# https://stackoverflow.com/questions/62162898/run-bleak-python-library-in-background-with-asyncio
# https://bleak.readthedocs.io/en/latest/troubleshooting.html#:~:text=Calling%20asyncio.run()%20more%20than%20once%20Bleak
# https://stackoverflow.com/questions/63858511/using-threads-in-combination-with-asyncio


class Ble:
    DM_DATA_SERVICE_UUID = "36794f20-3a88-418c-8df8-7394c5c80200"
    DM_COMMAND_CHAR_UUID = "36794f20-3a88-418c-8df8-7394c5c80201"
    DM_BRIGHT_CHAR_UUID = "36794f20-3a88-418c-8df8-7394c5c80202"

    def __init__(self, app) -> None:
        def run_asyncio_loop(loop):
            """Run an asyncio loop forever"""
            asyncio.set_event_loop(loop)
            loop.run_forever()

        # Create and start a thread for the ble asynchronous loop
        self.ble_loop = asyncio.new_event_loop()
        asyncio_thread = threading.Thread(
            target=run_asyncio_loop, args=(self.ble_loop,), daemon=True
        )
        asyncio_thread.start()

        self.app: dme.App = app
        self.found_dm = False
        self.device: BLEDevice
        self.client: BleakClient

    async def aio_scan_for_dm(self) -> bool:
        """Scan BLE devices looking for DM. Store device/client info if found"""
        logging.info("Scanning Bluetooth Low Energy for devices ...")
        self.found_dm = False
        devices = await BleakScanner.discover()
        for device in devices:
            logging.info(f"Found device {device.name} at {device.address} ")
            if device.name and (device.name.startswith("DM_")):
                self.found_dm = True
                self.device = device
                self.client = BleakClient(self.device, timeout=5.0)
        return self.found_dm

    async def aio_connect(self) -> bool:
        """Connect to the found DM"""
        logging.info(f"Connecting to {self.name} ...")
        if not self.found_dm:
            await self.aio_scan_for_dm()
        if self.found_dm:
            try:
                await self.client.connect()
                logging.info("Connected ...")
            except asyncio.exceptions.TimeoutError as e:
                logging.error(f"Can't connect to device {self.device.address} {e}.")
                return False
        else:
            return False
        return True

    async def aio_disconnect(self) -> None:
        """Disconnect from the DM"""
        logging.info(f"Disconnecting from {self.name} ...")
        if self.found_dm and self.client.is_connected:
            await self.client.disconnect()

    async def aio_send_brightness(self, level: int):
        """Set DM brightness level"""
        if not (0 <= level <= 100):
            logging.error(
                "Brightness should be set between 0% and 100%. Setting it to 50%"
            )
            level = 50
        logging.info(f"Changing brithness to {level}%")
        try:
            await self.client.write_gatt_char(
                Ble.DM_BRIGHT_CHAR_UUID, bytes([level]), response=True
            )
        except bleak.exc.BleakError as e:
            logging.error(f"Can't write brightness to {self.device.name} : {e}")

    async def aio_stop_seq(self):
        """Stop the sequence"""
        logging.info(f"Stop display LED")
        try:
            await self.client.write_gatt_char(
                Ble.DM_COMMAND_CHAR_UUID, bytes([0xFF]), response=True
            )
        except bleak.exc.BleakError as e:
            logging.error(f"Can't stop sequence of {self.device.name} : {e}")

    @staticmethod
    def encode_led(leds: list[str]) -> int:
        """Encode the led in an int"""
        led = 0
        if "A1" in leds:
            led += 1
        if "A2" in leds:
            led += 2
        if "A3" in leds:
            led += 4
        if "A4" in leds:
            led += 8
        if "A5" in leds:
            led += 16
        if "B1" in leds:
            led += 32
        if "B2" in leds:
            led += 64
        if "B3" in leds:
            led += 128
        if "B4" in leds:
            led += 256
        if "B5" in leds:
            led += 512
        return led

    @staticmethod
    def encode_oscillator(
        cmd_index: int, osc_index: int, duration: int, oscillator: seq.Oscillator
    ) -> bytearray:
        """Encode one step"""
        data = bytearray()
        data.append(cmd_index)
        data.append(osc_index)
        data.extend(duration.to_bytes(2, byteorder="big"))
        data.extend(Ble.encode_led(oscillator.leds).to_bytes(2, byteorder="big"))
        data.extend(int(oscillator.f_start * 10).to_bytes(2, byteorder="big"))
        data.extend(int(oscillator.f_end * 10).to_bytes(2, byteorder="big"))
        data.append(int(oscillator.d_start))
        data.append(int(oscillator.d_end))
        data.extend(int(oscillator.b_start * 10).to_bytes(2, byteorder="big"))
        data.extend(int(oscillator.b_end * 10).to_bytes(2, byteorder="big"))
        # s = [" ".join(format(x, "02x") for x in data)]
        # logging.info(s)
        return data

    async def aio_send_step(self, step: seq.Step):
        """Send one Step to the DM"""
        logging.info(
            f"Sending the step with index={step.index} osc {len(step.oscillators)} to {self.name}"
        )
        duration = step.t_end - step.t_start
        data: list[bytearray] = []
        data.append(bytearray([1]))  # start command list
        # data.append(bytearray.fromhex("00000001000707d007d0646400320005"))
        # data.append(bytearray.fromhex("01000001006007d007d0646400320005"))
        # data.append(bytearray.fromhex("02000001001807d007d0646400320005"))
        # data.append(bytearray.fromhex("03000001018007d007d0646400320005"))
        # data.append(bytearray.fromhex("04000001000107d007d0646400320005"))

        # append all the oscillators for this step
        for osc_idx, osc in enumerate(step.oscillators):
            data.append(Ble.encode_oscillator(0, osc_idx, duration, osc))

        data.append(bytearray([2]))  # start playing step

        try:
            for cmd in data:
                logging.info(f"{cmd.hex(" ", 2)}")
                await self.client.write_gatt_char(Ble.DM_COMMAND_CHAR_UUID, cmd)
        except bleak.exc.BleakError as e:
            logging.info(f"Can't send data to {self.name}: {e}")

    async def aio_send_seq(self, seq: seq.Sequence):
        """Send one Step to the DM"""
        logging.info(
            f"Sending sequence {seq.name} to {self.name} with {len(seq.steps)} steps length={seq.duration}"
        )
        data: list[bytearray] = []
        data.append(bytearray([1]))  # start command code
        # append all steps
        for step_idx, step in enumerate(seq.steps):
            duration = step.t_end - step.t_start
            # append all the oscillators for this step
            for osc_idx, osc in enumerate(step.oscillators):
                if osc.leds == []:  # if no led used skip
                    continue
                data.append(Ble.encode_oscillator(step_idx, osc_idx, duration, osc))
        data.append(bytearray([2]))  # stop cmd code: start playing step
        # send the sequence to the DM
        try:
            for cmd in data:
                logging.info(f"--- {cmd.hex(" ", 2)}")
                await self.client.write_gatt_char(Ble.DM_COMMAND_CHAR_UUID, cmd)
        except bleak.exc.BleakError as e:
            logging.info(f"Can't send data to {self.name}: {e}")

    async def aio_show_ble_info(self):
        """Show Services, Characteritic, and Descriptor of a connected DM (for debug)"""
        for service in self.client.services:
            logging.info("Services:")
            for service in self.client.services:
                logging.info(f"- Service Description: {service.description}")
                logging.info(f"  UUID: {service.uuid}")
                logging.info(f"  Handle: {service.handle}")
                for char in service.characteristics:
                    value = None
                    if "read" in char.properties:
                        try:
                            value = bytes(await self.client.read_gatt_char(char))
                        except Exception as error:
                            value = error
                    logging.info(f"  - Characteristic Description: {char.description}")
                    logging.info(f"    UUID: {char.uuid}")
                    logging.info(f"    Handle: {char.handle}")
                    logging.info(f"    Properties: {', '.join(char.properties)}")
                    logging.info(f"    Value: {value}")
                    for descriptor in char.descriptors:
                        desc_value = None
                        try:
                            desc_value = bytes(
                                await self.client.read_gatt_descriptor(char.handle)
                            )
                        except Exception as error:
                            desc_value = error
                        logging.info(
                            f"    - Descriptor Description: {descriptor.description}"
                        )
                        logging.info(f"      UUID: {descriptor.uuid}")
                        logging.info(f"      Handle: {descriptor.handle}")
                        logging.info(f"      Value: {desc_value}")

    @property
    def name(self) -> str | None:
        if self.found_dm:
            return self.device.name
        return ""

    @property
    def address(self) -> str | None:
        if self.found_dm:
            return self.device.address
        return ""

    @property
    def is_connected(self) -> bool:
        if self.found_dm:
            return self.client.is_connected
        else:
            return False

    async def aio_play_seq(self, seq) -> None:
        logging.info("Play thread started")
        await self.aio_send_brightness(self.app.param_frame.brightness_var.get())
        await self.aio_send_seq(seq)
        await asyncio.sleep(seq.duration)

    def play_dmled(self, position: float):
        if position == 0.0:  # send original sequence
            asyncio.run_coroutine_threadsafe(
                self.aio_play_seq(self.app.sequence), self.ble_loop
            )
        else:  # we create a new seq that start from position
            cur_step, pos, osc_values = dmutil.step_pos_values_at_time(
                position, self.app.sequence
            )
            actual_step = self.app.sequence.steps[cur_step]
            oscillators = []
            for osc_idx, osc in enumerate(actual_step.oscillators):
                osc.f_start = osc_values[osc_idx][0]
                osc.f_end = actual_step.oscillators[osc_idx].f_end
                osc.b_start = osc_values[osc_idx][1]
                osc.b_end = actual_step.oscillators[osc_idx].b_end
                osc.d_start = osc_values[osc_idx][2]
                osc.d_end = actual_step.oscillators[osc_idx].d_end
                osc.leds = actual_step.oscillators[osc_idx].leds
                oscillators.append(osc)
            duration = actual_step.t_end - actual_step.t_start - pos
            # logging.info(f"{cur_step=} {pos=} {duration=}")
            new_step = seq.Step(actual_step.index, 0, duration, oscillators)
            # logging.info(new_step)
            # create a sequence starting with new_step
            new_seq = seq.Sequence("temp_seq", None, 0, [new_step])
            # now add the following steps
            last_time = duration
            if cur_step != len(self.app.sequence.steps) - 1:
                for step_idx, step in enumerate(
                    self.app.sequence.steps[cur_step + 1 :]
                ):
                    new_seq.steps.insert(step_idx + 1, copy.deepcopy(step))
                    duration = step.t_end - step.t_start
                    new_seq.steps[-1].t_start = last_time
                    new_seq.steps[-1].t_end = last_time + duration
                    new_seq.steps[-1].index = step.index
                    new_seq.steps[-1].oscillators = step.oscillators
                    last_time = new_seq.steps[-1].t_end
                new_seq.duration = new_seq.steps[-1].t_end
            # logging.info(new_seq)
            asyncio.run_coroutine_threadsafe(self.aio_play_seq(new_seq), self.ble_loop)

    def stop_dmled(self):
        asyncio.run_coroutine_threadsafe(self.aio_stop_seq(), self.ble_loop)


####################### BELOW TEST ###############################
####################### BELOW TEST ###############################
####################### BELOW TEST ###############################


class App(tk.Tk):
    """Test program: scan for dm, connect/disconnect, send sequence"""

    def __init__(self) -> None:
        super().__init__()
        self.ble = Ble(self)
        self.loop = self.ble.ble_loop

        self.title("Test BLE")
        self.protocol("WM_DELETE_WINDOW", self._quit)
        logging.basicConfig(
            level=20,
            format="{asctime} - {module} {levelname} - {message}",
            style="{",
            # datefmt="%Y-%m-%d %H:%M:%S",
        )
        tk.Button(self, text="Scan DM", width=15, command=self.scan_for_dm).grid(
            row=0, column=0, padx=5, sticky="w"
        )
        self.tb_dm_name = tk.Label(
            self,
            width=30,
            justify="left",
            bg="silver",
            text="Click scan ...",
            anchor="w",
        )
        self.tb_dm_name.grid(row=0, column=1, sticky="w")
        self.bt_connect = tk.Button(
            self, text="Connect", width=15, command=self.connect_disconnect
        )
        self.bt_connect.grid(row=1, column=0, padx=5, sticky="w")
        self.tb_dm_status = tk.Label(
            self, width=30, text="Disconnected", justify="left", anchor="w", bg="silver"
        )
        self.tb_dm_status.grid(row=1, column=1, sticky="w")
        scan_button = tk.Button(self, text="Test DM", width=15, command=self.start_test)
        scan_button.grid(row=2, column=0, columnspan=2, padx=5, sticky="w")

    def _quit(self) -> None:
        self.quit()
        self.destroy()

    def scan_for_dm(self):
        # Submit the scan task to the asyncio loop and set up a callback
        future = asyncio.run_coroutine_threadsafe(self.ble.aio_scan_for_dm(), self.loop)
        future.add_done_callback(lambda f: self.update_dm_name())

    def connect_disconnect(self):
        if not self.ble.is_connected:
            self.tb_dm_status.configure(text=f"Connecting...")
            future = asyncio.run_coroutine_threadsafe(self.ble.aio_connect(), self.loop)
        else:
            self.tb_dm_status.configure(text=f"Disonnecting...")
            future = asyncio.run_coroutine_threadsafe(
                self.ble.aio_disconnect(), self.loop
            )
        future.add_done_callback(lambda f: self.update_dm_status())
        future.add_done_callback(lambda f: self.update_dm_name())

    def start_test(self):
        logging.info("calling test_dm in a thread")
        asyncio.run_coroutine_threadsafe(self.test_dm(), self.loop)

    def update_dm_name(self):
        # Update the text with detected devices
        if self.ble.found_dm:
            self.tb_dm_name.configure(
                text=f"{self.ble.name} ({self.ble.address})",
                justify="left",
            )
        else:
            self.tb_dm_name.configure(text="No DM found", justify="left")

    def update_dm_status(self):
        # Update the text with connection status
        if self.ble.is_connected:
            self.tb_dm_status.configure(text=f"Connected")
            self.bt_connect.configure(text="Disconnect")
        else:
            self.tb_dm_status.configure(text=f"Disconnected")
            self.bt_connect.configure(text="Connect", justify="left")

    async def test_dm(self) -> None:
        osc1 = seq.Oscillator(["A1", "A4", "B1"], 9, 10, 50, 50, 80, 10)
        osc2 = seq.Oscillator(["A1", "A5", "B2"], 8, 9, 30, 30, 10, 80)
        step1 = seq.Step(0, 0, 10, [osc1, osc2])
        step2 = seq.Step(0, 0, 5, [osc2, osc1])
        if not self.ble.is_connected:
            await self.ble.aio_connect()
        await self.ble.aio_send_brightness(50)
        await self.ble.aio_send_step(step1)
        await asyncio.sleep(5)
        logging.info("changing brightness at middle of step")
        await self.ble.aio_send_brightness(20)
        await asyncio.sleep(5)
        logging.info("second step")
        await self.ble.aio_send_brightness(50)
        await self.ble.aio_send_step(step2)
        # create seq
        logging.info("Creating and sending sequence")
        new_seq = seq.Sequence("Test", None, 15, [step1, step2])
        await self.ble.aio_send_seq(new_seq)


if __name__ == "__main__":
    app = App()  # build initial display
    app.mainloop()  # process tkinter events

重现错误使用

pyinstaller ble.py
运行生成的程序

这是运行时产生的日志文件的开头

2024-10-25 14:10:31,659 - ble INFO - Scanning Bluetooth Low Energy for devices ...
2024-10-25 14:10:32,744 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13570>, <winrt._winrt...001EF94A13430>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13570>, <winrt._winrt...001EF94A13430>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:32,746 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13DB0>, <winrt._winrt...001EF94A127D0>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13DB0>, <winrt._winrt...001EF94A127D0>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:33,026 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13D90>, <winrt._winrt...001EF94A12750>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13D90>, <winrt._winrt...001EF94A12750>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:33,030 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13E30>, <winrt._winrt...001EF94A13EB0>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13E30>, <winrt._winrt...001EF94A13EB0>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:33,990 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13D10>, <winrt._winrt...001EF94A13DD0>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13D10>, <winrt._winrt...001EF94A13DD0>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:33,993 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13C90>, <winrt._winrt...001EF94A13E70>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13C90>, <winrt._winrt...001EF94A13E70>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
2024-10-25 14:10:34,307 - base_events ERROR - Exception in callback BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13C10>, <winrt._winrt...001EF94A13CF0>)
handle: <Handle BleakScannerWinRT._received_handler(<winrt._winrt...001EF94A13C10>, <winrt._winrt...001EF94A13CF0>)>
Traceback (most recent call last):
  File "asyncio\events.py", line 88, in _run
  File "bleak\backends\winrt\scanner.py", line 147, in _received_handler
ModuleNotFoundError: No module named 'winrt.windows.foundation.collections'
python pyinstaller python-packaging python-bleak
1个回答
0
投票

你安装了winrt吗?

pip install winrt

检查虚拟环境是否正确且活跃!

© www.soinside.com 2019 - 2024. All rights reserved.