使用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
相关产品推荐
相关产品推荐

