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

PyQt5下高速UDP数据绘图丢包与界面冻结问题排查

解决PyQt5+pyqtgraph高速UDP数据接收时界面冻结与丢包问题

问题根源分析

  1. UDP接收线程人为延迟:UdpThread.run中的self.msleep(10)强制线程休眠10ms,完全无法匹配72Mbit/s的高速数据接收需求,直接导致大量数据包积压丢失。
  2. 高频UI更新阻塞主线程:每个UDP包解析后立即触发两次UI更新,且每次更新都直接调用setData刷新曲线,高频的UI操作会占用主线程大量资源,引发界面冻结。
  3. 无限制的数据缓冲区:data_buffers使用无长度限制的collections.deque,随着数据积累,内存占用持续增加,数据处理和绘图的耗时也会越来越长。
  4. 未定义的UI控件引用:update_plot_view中调用的x_offset_slider和x_div_slider未在MainWindow中初始化,会引发运行时异常,中断程序流程。

针对性修复方案

1. 移除UDP线程的人为延迟

删除UdpThread.run中的self.msleep(10),让线程全力处理UDP数据包接收。同时增大socket接收缓冲区,提升系统层面的抗丢包能力。

2. 批量更新UI,降低刷新频率

使用定时器定期批量推送解析后的数据,统一更新绘图,避免高频UI操作阻塞主线程;同时限制数据缓冲区的最大长度,防止内存溢出。

3. 修复未定义控件问题

补充初始化x_offset_slider和x_div_slider控件,保证update_plot_view函数正常运行。

4. 优化数据处理逻辑

临时缓存解析后的数据,批量发送给UI线程,减少线程间信号传递的开销。

修改后的完整代码

import sys
import collections
import pyqtgraph as pg
from PyQt5.QtCore import QThread, pyqtSignal, QTimer, QCoreApplication, QObject
from PyQt5.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QWidget, QSlider, QHBoxLayout, QLabel
import socket

def convert_hex_to_bin(hex_string):
    return bin(int(hex_string, 16))[2:].zfill(len(hex_string) * 4)

class UdpThread(QThread):
    raw_data_received = pyqtSignal(bytes)

    def __init__(self, parent=None, port=8080):
        super().__init__(parent)
        self.port = port
        self.running = True

    def run(self):
        UDP_IP = "0.0.0.0"
        MAX_UDP_PAYLOAD = 1035
        try:
            udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            # 增大socket接收缓冲区至8MB,提升高速数据处理能力
            udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024*8)
            udp_socket.bind((UDP_IP, self.port))
            while self.running:
                data, _ = udp_socket.recvfrom(MAX_UDP_PAYLOAD)
                if data:
                    self.raw_data_received.emit(data)
        except socket.error as e:
            print(f"Socket error: {e}")
        finally:
            if 'udp_socket' in locals():
                udp_socket.close()

    def stop(self):
        self.running = False
        try:
            dummy_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            dummy_socket.sendto(b'stop', ('127.0.0.1', self.port))
            dummy_socket.close()
        except socket.error:
            pass
        self.wait()

class DataProcessor(QObject):
    data_parsed = pyqtSignal(dict)  # 修改为批量发送多通道数据

    def __init__(self):
        super().__init__()
        self.temp_data = {i: [] for i in range(1, 11)}  # 临时存储批量数据

    def process_data(self, datagram_data):
        try:
            data_pack = datagram_data.hex()
            data_online = data_pack[16:-6]
            
            # --- Process Block 1 ---
            block1 = data_online[:1024]
            metadata_data1 = block1[:14]
            metadata_bin1 = convert_hex_to_bin(metadata_data1)
            channel1 = int(metadata_bin1[-7:], 2)
            if 1 <= channel1 <=10:
                block1_data = block1[14:-2]
                result_list1 = [int(block1_data[j:j+6], 16) for j in range(0, len(block1_data), 6)]
                self.temp_data[channel1].extend(result_list1)
            
            # --- Process Block 2 ---
            block2 = data_online[1024:]
            metadata_data2 = block2[:14]
            metadata_bin2 = convert_hex_to_bin(metadata_data2)
            channel2 = int(metadata_bin2[-7:], 2)
            if 1 <= channel2 <=10:
                block2_data = block2[14:-2]
                result_list2 = [int(block2_data[j:j+6], 16) for j in range(0, len(block2_data), 6)]
                self.temp_data[channel2].extend(result_list2)

        except (ValueError, IndexError) as e:
            print(f"Error parsing data: {e}")

    def flush_data(self):
        # 批量发送解析后的数据
        send_data = {k: v.copy() for k, v in self.temp_data.items() if v}
        if send_data:
            self.data_parsed.emit(send_data)
        # 清空临时数据
        for k in self.temp_data:
            self.temp_data[k].clear()

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Data Logger")
        self.setGeometry(100, 100, 1200, 800) 
        
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # 添加滑块控件,修复原代码未定义问题
        slider_layout = QHBoxLayout()
        self.x_offset_slider = QSlider()
        self.x_offset_slider.setRange(0, 100)
        self.x_offset_slider.setValue(50)
        slider_layout.addWidget(QLabel("X Offset:"))
        slider_layout.addWidget(self.x_offset_slider)

        self.x_div_slider = QSlider()
        self.x_div_slider.setRange(0, 100)
        self.x_div_slider.setValue(50)
        slider_layout.addWidget(QLabel("X Div:"))
        slider_layout.addWidget(self.x_div_slider)
        main_layout.addLayout(slider_layout)

        self.plot_widget = pg.PlotWidget()
        main_layout.addWidget(self.plot_widget)
        self.plot_widget.addLegend()
        self.plot_widget.showGrid(x=True, y=True, alpha=0.5)

        self.num_channels = 10 
        self.channel_colors = ['#00BFFF', '#FF4500', '#32CD32', '#FFD700', '#9370DB', '#808080', '#00AAFF', '#FF8000', '#32AA32', '#FFA700']
        
        # 设置缓冲区最大长度,避免内存溢出
        self.buffer_maxlen = 2000
        self.data_buffers = {i: collections.deque(maxlen=self.buffer_maxlen) for i in range(1, self.num_channels + 1)} 
        self.curves = {i: self.plot_widget.plot(pen=pg.mkPen(self.channel_colors[i-1], width=2, style = pg.QtCore.Qt.DashLine), name=f"Channel {i}")
                       for i in range(1, self.num_channels + 1)}

        self.udp_thread = UdpThread()
        self.processor_thread = QThread()
        self.data_processor = DataProcessor()
        
        self.data_processor.moveToThread(self.processor_thread)
        self.processor_thread.start()
        
        self.udp_thread.raw_data_received.connect(self.data_processor.process_data)
        self.data_processor.data_parsed.connect(self.update_plot)
        self.udp_thread.start()

        # 定时器批量更新UI,30ms刷新一次(约30fps)
        self.update_timer = QTimer(self)
        self.update_timer.timeout.connect(self.data_processor.flush_data)
        self.update_timer.start(30)

    def update_plot_view(self):
        max_len = max(len(buf) for buf in self.data_buffers.values())
        if max_len == 0:
            max_len = self.buffer_maxlen 

        view_window_size = 1000 

        x_offset_val = (self.x_offset_slider.value() - 50) / 100.0 * view_window_size 
        x_div_val = (100 - self.x_div_slider.value()) / 100.0 * view_window_size + 100 

        x_max = max_len
        x_min = max(0, x_max - x_div_val)
        
        self.plot_widget.setXRange(x_min + x_offset_val, x_max + x_offset_val, padding=0)

    def update_plot(self, batch_data):
        for channel, data in batch_data.items():
            if channel in self.data_buffers: 
                self.data_buffers[channel].extend(data) 
                self.curves[channel].setData(list(self.data_buffers[channel]))
        # 统一更新视图,避免多次调用
        self.update_plot_view()
    
    def closeEvent(self, event):
        print("Stopping UDP listener thread...")
        self.update_timer.stop()
        self.udp_thread.stop()
        self.processor_thread.quit()
        self.processor_thread.wait()
        super().closeEvent(event)
        
if __name__ == '__main__':
    app = QCoreApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    
    main_win = MainWindow()
    main_win.show()
    
    sys.exit(app.exec_()) 

高速数据场景进阶优化建议

  1. 二进制化数据解析:使用struct模块直接解析字节数据,替代十六进制字符串转换,大幅提升解析速度。
  2. 进程级隔离:将UDP接收和数据处理放在独立进程中,使用multiprocessing.Queue传递数据,避免线程间通信的性能瓶颈。
  3. 绘图效率优化:使用numpy数组替代Python列表传递给setData,pyqtgraph对numpy数组的处理效率更高。
  4. 动态调整刷新频率:根据数据接收速率动态调整UI刷新定时器的间隔,平衡流畅度和性能开销。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 12:14:52