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

使用Multiprocessing+FuncAnimation实现蓝牙实时绘图问题排查

问题原因

单进程场景下数据重复追加

  • 本质是两个问题叠加:一是单线程同时跑阻塞式蓝牙IO和matplotlib动画刷新,事件调度时序混乱;二是蓝牙RFCOMM是流协议,没有帧边界,直接把recv()拿到的所有字节解码转整数时,若发送端速率和接收端读取速率不匹配,接收缓冲区会堆积多帧数据,比如连续发两次8就可能被一次性读成b'88',转成整数就出现重复值,和FuncAnimation速率不匹配只是表象,核心是没做流的分包粘包处理。

多进程场景下读不到列表数据

  • Python多进程的内存空间是完全隔离的,在主进程定义的x、y普通列表,子进程启动时只会复制一份初始副本,接收数据的进程修改的是自己进程内存里的列表,绘图进程访问的是自己持有的初始空列表,两边数据完全不互通,不管是声明全局变量还是普通函数传参都解决不了这个问题。另外原代码还存在两个隐性bug:蓝牙套接字sock是主进程创建的,直接在子进程调用会出现跨进程套接字异常;sock.close()写在if __name__ == "__main__"判断块外,子进程启动导入模块时会触发无效套接字操作报错。
修复方案
  • 跨进程数据传递改用multiprocessing.Queue,这是进程安全的FIFO通道,接收进程拿到解析后的有效数据就放到队列里,绘图进程每次刷新时从队列拉取所有待处理数据,追加到本地维护的列表后再绘图,不需要共享全局列表。
  • 蓝牙接收端增加粘包处理逻辑:约定ESP32每发送一个角度数值就追加一个换行符\n作为帧尾,接收端维护缓冲区拼接收到的字节流,按换行符切分完整数据帧,避免读到拼接的错误值。
  • 给FuncAnimation设置固定刷新间隔(建议50~200ms,根据数据发送速率调整),避免无限制刷新占用资源。
  • 所有跨进程使用的对象(蓝牙套接字、队列、缓冲区大小)通过函数参数显式传递给子进程,不要依赖全局变量继承。

修正后的可运行代码如下:

from bluetooth import *
from itertools import count
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from multiprocessing import Process, Queue
import sys

plt.style.use('fivethirtyeight')

def rx_and_echo(sock, data_queue, buf_size):
    recv_buffer = ""
    while True:
        try:
            data = sock.recv(buf_size)
            if not data:
                data_queue.put(0)
                continue
            # 拼接到缓冲区,处理粘包半包
            recv_buffer += data.decode('utf-8')
            # 按换行分割完整帧
            while '\n' in recv_buffer:
                frame, recv_buffer = recv_buffer.split('\n', 1)
                frame = frame.strip()
                if not frame:
                    continue
                data_queue.put(int(frame))
        except Exception as e:
            print(f"蓝牙接收异常: {e}")
            data_queue.put(0)

def plot(i, ax, x_list, y_list, data_queue):
    # 一次性把队列里所有待处理数据读出来
    while not data_queue.empty():
        try:
            val = data_queue.get_nowait()
            x_list.append(next(plot.index))
            y_list.append(val)
        except:
            break
    # 限制最多显示最近100个点,避免绘图卡顿
    if len(x_list) > 100:
        x_list[:] = x_list[-100:]
        y_list[:] = y_list[-100:]
    ax.cla()
    ax.plot(x_list, y_list)
    ax.set_ylabel("角度值")
    ax.set_xlabel("采样序号")

def animation(data_queue):
    fig, ax = plt.subplots()
    x_list = []
    y_list = []
    plot.index = count()
    # 间隔100ms刷新,可根据需求调整
    ani = FuncAnimation(fig, plot, fargs=(ax, x_list, y_list, data_queue), interval=100)
    plt.tight_layout()
    plt.show()

if __name__ == "__main__":
    # ESP32的MAC地址
    addr = "78:21:84:88:A9:BE"
    service_matches = find_service(address=addr)

    buf_size = 1024
    if len(service_matches) == 0:
        print("couldn't find the SampleServer service =(")
        sys.exit(0)

    first_match = service_matches[0]
    name = first_match["name"]
    host = first_match["host"]

    port = 1
    print("connecting to \"%s\" on %s, port %s" % (name, host, port))

    # 创建蓝牙套接字
    sock = BluetoothSocket(RFCOMM)
    sock.connect((host, port))
    print("connected")

    # 初始化进程间数据队列
    data_queue = Queue()

    p1 = Process(target=rx_and_echo, args=(sock, data_queue, buf_size))
    p2 = Process(target=animation, args=(data_queue,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

    sock.close()

注意:需要同步修改ESP32端代码,每次发送角度数值后追加\n,例如Arduino环境下使用Serial.println(angle)即可自动追加换行符。

内容的提问来源于stack exchange,提问作者Bryan Medina

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 10:12:27