我有一个 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'
你安装了winrt吗?
pip install winrt
检查虚拟环境是否正确且活跃!