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

PyQtGraph串口实时绘图类封装及Qt Designer集成问题求助

故障排查结论

封装后失效90%以上是以下几个低级错误导致,和pyqtgraph本身无关:

  • 子类构造时未调用pg.GraphicsWindow(或嵌入用的pg.GraphicsLayoutWidget)的父类__init__方法,或调用时未传入parent参数,Qt对象树断裂,组件生命周期不受Qt管控,嵌入UI后会出现不刷新、闪退、无响应问题
  • 全局代码中定义的QTimer、串口实例、数据缓存数组、曲线对象,封装后写成了构造函数内的局部变量,出了构造作用域就被Python垃圾回收,串口读数据、定时刷新绘图的逻辑直接失效
  • 原全局代码中绑定的定时器timeout槽函数、串口数据解析函数,封装后未正确绑定为类实例方法,调用时作用域不匹配,触发AttributeError但未被捕获
  • 直接使用顶层pg.GraphicsWindow做嵌入组件:该类自带独立窗口框架,嵌入Qt Designer生成的布局时会出现窗口嵌套、焦点冲突、大小无法自适应的问题,嵌入场景应替换为pg.GraphicsLayoutWidget作为父类。
可直接复用的正确实现

组件代码

import serial
import pyqtgraph as pg
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QWidget
import numpy as np

# 注意:嵌入Qt Designer请继承pg.GraphicsLayoutWidget,不要用pg.GraphicsWindow
class CustomPressurePlotWidget(pg.GraphicsLayoutWidget):
    def __init__(self, parent=None):
        # 必须调用父类构造,传入parent
        super().__init__(parent=parent)
        self.setBackground('w')
        # 固定配置参数
        self.PORT = '/dev/ttyUSB0'
        self.BAUD = 230400
        self.PACKET_LEN = 22
        self.SCROLL_WINDOW = 200  # 滚动窗口显示最近200个采样点
        self.CURVE_NUM_PER_SIDE = 5  # 每侧5条压力曲线

        # 所有资源必须绑定为self属性,避免被GC回收
        # 初始化滚动数据缓存
        self.left_data = np.zeros((self.CURVE_NUM_PER_SIDE, self.SCROLL_WINDOW))
        self.right_data = np.zeros((self.CURVE_NUM_PER_SIDE, self.SCROLL_WINDOW))
        self.plot_x = np.arange(self.SCROLL_WINDOW)

        # 初始化左足绘图区
        self.left_plot = self.addPlot(title="左鞋垫压力")
        self.left_plot.setYRange(0, 4096)  # 按实际传感器量程调整
        self.left_plot.setLabel('bottom', '采样点')
        self.left_plot.setLabel('left', '压力值')
        self.left_curves = []
        for i in range(self.CURVE_NUM_PER_SIDE):
            curve = self.left_plot.plot(
                self.plot_x, self.left_data[i], 
                pen=pg.mkPen(color=pg.intColor(i, self.CURVE_NUM_PER_SIDE), width=2)
            )
            self.left_curves.append(curve)

        # 换行添加右足绘图区
        self.nextRow()
        self.right_plot = self.addPlot(title="右鞋垫压力")
        self.right_plot.setYRange(0, 4096)
        self.right_plot.setLabel('bottom', '采样点')
        self.right_plot.setLabel('left', '压力值')
        self.right_curves = []
        for i in range(self.CURVE_NUM_PER_SIDE):
            curve = self.right_plot.plot(
                self.plot_x, self.right_data[i],
                pen=pg.mkPen(color=pg.intColor(i, self.CURVE_NUM_PER_SIDE), width=2)
            )
            self.right_curves.append(curve)

        # 初始化串口
        self.ser = None
        self.init_serial()

        # 初始化刷新定时器,必须绑定为self属性
        self.refresh_timer = QTimer()
        self.refresh_timer.timeout.connect(self.read_and_update)
        self.refresh_timer.start(10)  # 10ms刷新一次,可按实际采样率调整

    def init_serial(self):
        try:
            self.ser = serial.Serial(
                port=self.PORT,
                baudrate=self.BAUD,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_1,
                bytesize=serial.EIGHTBITS,
                timeout=0.01
            )
        except Exception as e:
            print(f"串口初始化失败: {str(e)}")

    def parse_packet(self, raw_bytes):
        """22字节数据包解析,可按实际传感器协议调整偏移量
        通用协议结构:帧头(1B) + 左足5个uint16(10B) + 右足5个uint16(10B) + 校验(1B)
        """
        if len(raw_bytes) != self.PACKET_LEN:
            return None, None
        # 跳过帧头,从第1位开始读10字节转5个uint16(小端序)
        left_vals = np.frombuffer(raw_bytes[1:11], dtype=np.uint16)
        right_vals = np.frombuffer(raw_bytes[11:21], dtype=np.uint16)
        return left_vals, right_vals

    def read_and_update(self):
        if not self.ser or not self.ser.is_open:
            return
        # 非阻塞读,凑够完整数据包就解析
        while self.ser.in_waiting >= self.PACKET_LEN:
            # 匹配帧头,默认帧头为0xAA,可按实际协议修改
            head_byte = self.ser.read(1)
            if head_byte != b'\xaa':
                continue
            raw_packet = head_byte + self.ser.read(self.PACKET_LEN - 1)
            left_vals, right_vals = self.parse_packet(raw_packet)
            if left_vals is None:
                continue
            # 滚动更新缓存
            self.left_data = np.roll(self.left_data, -1, axis=1)
            self.left_data[:, -1] = left_vals
            self.right_data = np.roll(self.right_data, -1, axis=1)
            self.right_data[:, -1] = right_vals
            # 批量更新曲线
            for i in range(self.CURVE_NUM_PER_SIDE):
                self.left_curves[i].setData(self.plot_x, self.left_data[i])
                self.right_curves[i].setData(self.plot_x, self.right_data[i])

Qt Designer集成步骤

  • 打开Qt Designer,在目标界面位置拖入一个普通QWidget控件,调整好大小并加入布局自适应
  • 右键点击该QWidget,选择「提升为...」
  • 在弹出窗口中,「提升的类名称」填CustomPressurePlotWidget,「头文件」填上述组件代码保存的py文件名(比如代码存为pressure_plot.py就填pressure_plot,不需要加.py后缀)
  • 点击「添加」后选中该提升类,点击「提升」即可
  • 编译生成ui对应的py文件后,确保组件代码文件和ui文件在同一目录,运行时会自动加载绘图组件,不需要额外手动调用addWidget

注意:不要在组件内部写while True死循环读串口,会阻塞Qt主线程导致UI无响应;所有串口读取、数据解析逻辑放在定时器槽函数中,超时时间设为10ms以内即可保证实时性。

内容的提问来源于stack exchange,提问作者Aloi Leseigneur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:15:47