You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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连接步骤被阻塞。

具体修复步骤

  1. 切换到支持asyncio的Matplotlib后端
    使用QtAgg后端(需安装PyQt5:pip install pyqt5),在代码开头添加:

    import matplotlib
    matplotlib.use('QtAgg')
    
  2. 修正绘图逻辑错误
    原代码中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
        # 后续绘图操作...
    
  3. 整合事件循环
    替换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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 22:13:15