pyqtgraph(PyQt5)实时绘制温湿度数据(threading.Thread线程实现)

假设某一硬件设备通过串口定时向主机发送数据,数据格式为:

Temperature: 30.20
Humidity: 26.40

那么在PC端可以用python结合pyqtgraph创建GUI程序来绘图,程序需要解决的几个问题:

  1. 开启子线程用于串口数据接收处理,然后通过qt的信号将数据发回给主线程
  2. 串口连接要设置timeout参数,用来接收完整的数据帧
  3. 数据处理前要对数据格式进行判断,比如单片机刚重启的时候会答应其他信息,那些是解析不了的,先进行数据过滤
  4. 串口接收数据的线程设置为守护线程,主要主程序关闭后该线程会自动销毁

python程序如下:

# -*- coding: utf-8 -*-
"""
Created on Mon Jun 28 23:36:50 2021

@author: LX
"""
import sys
import ctypes
import time
import pyqtgraph as pg
import threading
import serial
from collections import deque
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import datetime
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
from pyqtgraph import DateAxisItem

__version__ = '1.0'

class MainWindow(QMainWindow):
    newdata = pyqtSignal(object) # 创建信号
    def __init__(self, filename=None, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle('温湿度数据采集')
        self.setWindowIcon(QIcon(r"D:\Github\bouncescope\smile.ico"))

        self.t = []
        self.temp = []
        self.hum = []
        self.history = 3600# 历史保存数据的数量

        self.connected = False
        self.port = 'COM9'
        self.baud = 115200

        # 启动线程
        # QTimer.singleShot(0, self.startThread)
        self.btn = QPushButton('点击运行!')
        font = QFont()
        font.setPointSize(16)
        self.label = QLabel("实时获取温湿度数据")
        self.label.setFont(font)
        self.label.setAlignment(Qt.AlignCenter)
        self.data_label = QLabel("Data")
        # self.data_label.setAlignment(Qt.AlignCenter)
        
        self.pw = pg.PlotWidget(
            # axisItems={'bottom': TimeAxisItem(orientation='bottom')}
            )
        self.pw_hum = pg.PlotWidget()
        # setup pyqtgraph
        self.init_pg_temp()  # 温度
        self.init_pg_hum()  # 湿度
        
        # 设置布局
        vb = QVBoxLayout()
        hb = QHBoxLayout()

        vb.addWidget(self.label)
        vb.addWidget(self.btn)

        hb.addWidget(self.pw)
        hb.addWidget(self.pw_hum)

        vb.addLayout(hb)
        vb.addWidget(self.data_label)

        self.cwidget = QWidget()
        self.cwidget.setLayout(vb)
        self.setCentralWidget(self.cwidget)
        
        self.btn.clicked.connect(self.startThread)
        self.newdata.connect(self.updatePlot)

    def init_pg_temp(self):
        # 设置图表标题
        self.pw.setTitle("温度变化趋势",
                         color='008080',
                         size='12pt')
        # 设置上下左右的label
        self.pw.setLabel("left","气温(摄氏度)")
        self.pw.setLabel("bottom","时间")
        # 设置Y轴 刻度 范围
        # self.pw.setYRange(min=10,max=50)  # 最大值
        # 显示表格线
        self.pw.showGrid(x=True, y=True)
        # 背景色改为白色
        self.pw.setBackground('w')
        # 居中显示 PlotWidget
        # self.setCentralWidget(self.pw)
        axis = DateAxisItem() # 设置时间轴,主要此时x的数据为时间戳time.time()
        self.pw.setAxisItems({'bottom':axis})
        self.curve_temp = self.pw.getPlotItem().plot(
            pen=pg.mkPen('r', width=2)
        )
    def init_pg_hum(self):
        
        # 设置图表标题
        self.pw_hum.setTitle("湿度度变化趋势",
                         color='008080',
                         size='12pt')
        # 设置上下左右的label
        self.pw_hum.setLabel("left","湿度")
        self.pw_hum.setLabel("bottom","时间")
        # 设置Y轴 刻度 范围
        # self.pw_hum.setYRange(min=10, max=100)  # 最大值
        # 显示表格线
        self.pw_hum.showGrid(x=True, y=True)
        # 背景色改为白色
        self.pw_hum.setBackground('w')
        # 居中显示 PlotWidget
        # self.setCentralWidget(self.pw_hum)
        # 实时显示应该获取 plotItem, 调用setData,
        # 这样只重新plot该曲线,性能更高
        axis = DateAxisItem()
        self.pw_hum.setAxisItems({'bottom':axis})
        self.curve_hum = self.pw_hum.getPlotItem().plot(
            pen=pg.mkPen('b', width=2)
        )

    def startThread(self):
        '''
        这里使用python的threading.Thread构造线程,并将线程设置为守护线程,这样
        主线程退出后守护线程也会跟着销毁
        '''
        self.btn.setEnabled(False)
        print('Start lisnening to the COM-port')
        # timeout参数很重要!可以结合波特率和传输的数据量计算出数据发送所需的时间
        serial_port = serial.Serial(self.port, self.baud, timeout=0.1)
        thread = threading.Thread(target=self.read_from_port, args=(serial_port,))
        thread.setDaemon(True) # 守护线程
        thread.start() # 启动线程

    def updatePlot(self, signal):   
        '''更新绘图'''
        self.curve_temp.getViewBox().enableAutoRange()
        self.curve_temp.getViewBox().setAutoVisible()
        self.curve_temp.setData(signal[0], signal[1][0])

        self.curve_hum.getViewBox().enableAutoRange()
        self.curve_hum.getViewBox().setAutoVisible()
        self.curve_hum.setData(signal[0], signal[1][1])

    def process_data(self, data:str):
        ''''处理数据,注意原来通过串口发送的数据格式'''

        if len(self.t) >= self.history: # 保证存储数量为设定的历史长度数量
            self.t.pop(0)
            self.temp.pop(0)
            self.hum.pop(0)

        if data.startswith('Temp'):
            try:
                # ['Temperature:28.00\r', 'Humidity:28.00']
                data = data.strip().replace(' ', '').replace('\r','').split('\n')
                print(data)
                self.data_label.setText('Time:'+str(datetime.datetime.now())+', '+
                data[0]+', '+data[1])
                self.t.append(time.time())
                self.temp.append(float(data[0].split(':')[1].strip()))
                self.hum.append(float(data[1].split(':')[1].strip()))
            except :
                print('No valid data')

            signal = (self.t,(self.temp, self.hum))
            self.newdata.emit(signal)
        else:
            print('数据格式错误,接收到的数据为:',data)

    def read_all(self, port, chunk_size=200):
        """Read all characters on the serial port and return them.
        参考:https://stackoverflow.com/questions/19161768/pyserial-inwaiting-returns-incorrect-number-of-bytes
        """
        if not port.timeout:
            raise TypeError('Port needs to have a timeout set!')

        read_buffer = b''
        while True:
            # Read in chunks. Each chunk will wait as long as specified by
            # timeout. Increase chunk_size to fail quicker
            byte_chunk = port.read(size=chunk_size)
            read_buffer += byte_chunk
            if not len(byte_chunk) == chunk_size:
                break

        return read_buffer

    # 从串口读取数据
    def read_from_port(self,ser):
        while True:
            bytedata = self.read_all(ser)
            if bytedata:
                self.process_data(bytedata.decode()) # 处理数据

    def stopThread(self):
        print ('Stop the thread...')

    def closeEvent(self, event):
        if self.okToContinue():
             event.accept()
             self.stopThread()
        else:
            event.ignore()

    def okToContinue(self):
        return True

if __name__ == '__main__':

    app = QApplication(sys.argv)
    win = MainWindow()

    win.show()
    app.exec_()

效果如下:
在这里插入图片描述
已知问题:如果鼠标控制了绘图的缩放,后面会无法自动缩放x轴和y轴,这个应该是pyqtgraph的问题,待解决
解决办法:每次绘图的时候启用自动缩放

实现多线程同时接收数据绘制曲线,可以考虑使用PyQtGraphThreadedPlotItem类。这个类是专门为多线程数据绘制而设计的,可以在不阻塞GUI的情况下实时绘制数据。 首先,定义一个数据处理类,负责处理数据,并向主线程发送绘图信号。这个类需要继承QObject,并包含一个处理数据的槽函数,以及一个发送绘图信号的信号。 ```python from PyQt5.QtCore import QObject, pyqtSignal class DataHandler(QObject): plot_signal = pyqtSignal(list, list) def handle_data(self): # 处理数据 x, y = [], [] # 发送绘图信号 self.plot_signal.emit(x, y) ``` 然后,在主线程中创建ThreadedPlotItem对象,并将数据处理类的绘图信号连接到ThreadedPlotItem的addData()槽函数上。 ```python import pyqtgraph as pg from PyQt5.QtCore import QThread class MainWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.plot_widget = pg.PlotWidget() self.setCentralWidget(self.plot_widget) self.plot_item = pg.ThreadedPlotItem() self.plot_widget.addItem(self.plot_item) self.data_handler = DataHandler() self.data_handler.plot_signal.connect(self.plot_item.addData) self.thread = QThread() self.data_handler.moveToThread(self.thread) self.thread.started.connect(self.data_handler.handle_data) self.thread.start() ``` 最后,启动线程并显示界面即可。 需要注意的是,数据处理类的handle_data()函数需要在子线程中执行,因此需要将数据处理类移动到一个新的线程中。在这里我们使用了QThread类来实现多线程。 同时,需要注意在处理数据时,要保证线程安全,避免出现数据竞争等问题。可以使用线程锁或者信号槽机制来解决这个问题。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值