Python中Bleak与Matplotlib集成时的连接阻塞问题排查
问题
我有一个通过低功耗蓝牙(BLE)传输加速度数据的嵌入式设备,支持Nordic UART Service(NUS)实现BLE海量数据交互。使用Python的bleak库在PC端采集数据时,单独测试bleak的uart_service示例代码完全正常。
我通过blitting优化实现了Matplotlib超过200帧/秒的绘图效率,但将Matplotlib绘图与bleak代码集成后出现阻塞问题:BLE能成功连接设备(终端输出可确认),但代码卡在async with BleakClient(device) as client:这一行,注释Matplotlib相关代码后程序即可正常运行。
尝试过用pyqtgraph结合bleak,但pyqtgraph不支持asyncio,且参考的旧Qt代码接口已变更无法编译,自行编写的代码也无法正常运行。
附上相关代码:
import matplotlib.pyplot as plt import numpy as np import asyncio from bleak import BleakClient, BleakScanner address = 'E6:B0:76:20:OE:41' UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" NAME = "NUS" def init_matplot(): global ax, fig, ln0, ln1, ln2, bg y = np.zeros(200) x = np.linspace(0, 1, 200) fig, ax = plt.subplots() (ln0,) = ax.plot(x, y, animated=True) (ln1,) = ax.plot(x, y, animated=True) (ln2,) = ax.plot(x, y, animated=True) plt.show(block=False) plt.ion() bg = fig.canvas.copy_from_bbox(fig.bbox) ax.draw_artist(ln0) ax.draw_artist(ln1) ax.draw_artist(ln2) def callback(characteristic, data): trim_str = data.decode('utf-8').split(",") val_x = float(trim_str[0]) val_y = float(trim_str[1]) val_z = float(trim_str[2]) print(f"{val_x, val_y, val_z}") global ax, fig, ln0, ln1, ln2, bg fig.canvas.restore_region(bg) ln0.set_ydata(val_x) ln1.set_ydata(val_y) ln2.set_ydata(val_z) ax.draw_artist(ln0) ax.draw_artist(ln1) ax.draw_artist(ln2) # copy the image to the GUI state, but screen might not be changed yet fig.canvas.blit(fig.bbox) # flush any pending GUI events, re-painting the screen if needed fig.canvas.flush_events() async def start_app(): device = await BleakScanner.find_device_by_address(address) if device is None: print("Address could not be found") device = await BleakScanner.find_device_by_name(NAME) if device is None: print("Name could not be found") exit(-1) else: print("Device matched on name.") else: print("Device matched on address.") print("Connecting...") async with BleakClient(device) as client: print("Connected") await client.start_notify(UART_TX_CHAR_UUID, callback) await asyncio.sleep(380.0) await client.stop_notify(UART_TX_CHAR_UUID) if __name__ == "__main__": init_matplot() asyncio.run(start_app())
解决方案
核心原因:GUI事件循环与asyncio事件循环冲突
Matplotlib默认GUI后端(如TkAgg)会启动独立事件循环,而asyncio.run()也会启动自身的事件循环,两者无法共存,导致BLE连接步骤被阻塞。
具体修复步骤
切换到支持asyncio的Matplotlib后端
使用QtAgg后端(需安装PyQt5:pip install pyqt5),在代码开头添加:import matplotlib matplotlib.use('QtAgg')修正绘图逻辑错误
原代码中ln0.set_ydata(val_x)是错误的,set_ydata需要传入数组而非单个数值。需维护固定长度的数组,每次收到新数据后滚动更新:# 全局维护数据数组 x_data = np.zeros(200) y_data = np.zeros(200) z_data = np.zeros(200) def callback(characteristic, data): global x_data, y_data, z_data trim_str = data.decode('utf-8').split(",") val_x = float(trim_str[0]) val_y = float(trim_str[1]) val_z = float(trim_str[2]) # 滚动更新数据 x_data = np.roll(x_data, -1) x_data[-1] = val_x y_data = np.roll(y_data, -1) y_data[-1] = val_y z_data = np.roll(z_data, -1) z_data[-1] = val_z # 后续绘图操作...整合事件循环
替换asyncio.run()为显式事件循环调用,避免和Matplotlib的GUI循环冲突:if __name__ == "__main__": init_matplot() loop = asyncio.get_event_loop() try: loop.run_until_complete(start_app()) except KeyboardInterrupt: print("程序终止") finally: loop.close() plt.close('all')
完整修正代码
import matplotlib matplotlib.use('QtAgg') import matplotlib.pyplot as plt import numpy as np import asyncio from bleak import BleakClient, BleakScanner address = 'E6:B0:76:20:OE:41' UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" NAME = "NUS" # 全局数据缓存数组 x_data = np.zeros(200) y_data = np.zeros(200) z_data = np.zeros(200) def init_matplot(): global ax, fig, ln0, ln1, ln2, bg x = np.linspace(0, 1, 200) fig, ax = plt.subplots() (ln0,) = ax.plot(x, x_data, animated=True) (ln1,) = ax.plot(x, y_data, animated=True) (ln2,) = ax.plot(x, z_data, animated=True) plt.show(block=False) plt.ion() bg = fig.canvas.copy_from_bbox(fig.bbox) ax.draw_artist(ln0) ax.draw_artist(ln1) ax.draw_artist(ln2) fig.canvas.blit(fig.bbox) fig.canvas.flush_events() def callback(characteristic, data): global x_data, y_data, z_data, ax, fig, ln0, ln1, ln2, bg trim_str = data.decode('utf-8').split(",") val_x = float(trim_str[0]) val_y = float(trim_str[1]) val_z = float(trim_str[2]) print(f"{val_x, val_y, val_z}") # 滚动更新数据队列 x_data = np.roll(x_data, -1) x_data[-1] = val_x y_data = np.roll(y_data, -1) y_data[-1] = val_y z_data = np.roll(z_data, -1) z_data[-1] = val_z # 优化绘图更新 fig.canvas.restore_region(bg) ln0.set_ydata(x_data) ln1.set_ydata(y_data) ln2.set_ydata(z_data) ax.draw_artist(ln0) ax.draw_artist(ln1) ax.draw_artist(ln2) fig.canvas.blit(fig.bbox) fig.canvas.flush_events() async def start_app(): device = await BleakScanner.find_device_by_address(address) if device is None: print("Address could not be found") device = await BleakScanner.find_device_by_name(NAME) if device is None: print("Name could not be found") exit(-1) else: print("Device matched on name.") else: print("Device matched on address.") print("Connecting...") async with BleakClient(device) as client: print("Connected") await client.start_notify(UART_TX_CHAR_UUID, callback) # 保持运行直到手动中断 while True: await asyncio.sleep(0.01) await client.stop_notify(UART_TX_CHAR_UUID) if __name__ == "__main__": init_matplot() loop = asyncio.get_event_loop() try: loop.run_until_complete(start_app()) except KeyboardInterrupt: print("Stopping...") finally: loop.close() plt.close('all')
内容的提问来源于stack exchange,提问作者Joshua

