抓取实时传感器数据时,PyQtGraph停止更新并冻结

如何解决抓取实时传感器数据时,PyQtGraph停止更新并冻结

我正在使用PyQt5和pyqtgraph绘制实时传感器数据。该图是更大的PyQt5应用程序的一部分,该应用程序用于与各种硬件进行交互并可视化传感器数据。

背景:以下代码是该代码的非常简化的示例,该代码负责查询传感器的数据,然后绘制瞬时位置及其移动平均值的图表。每隔x ms间隔在单独的线程中查询一次传感器。

问题:图形和传感器读数按预期工作。但是,在运行应用程序几秒钟后,pyqtgraph停止更新并冻结。一旦图表冻结,我唯一看到图表刷新/更新的地方是我是否尝试调整窗口大小或将焦点移到另一个窗口,然后重新聚焦回图表窗口。在这种情况下,图形将仅更新一次,并且不会继续刷新。

我在下面的链接中阅读了其他有类似问题的用户。但是,提出的解决方案不是从单独的线程更新GUI。就我而言,我不是从单独的线程更新图形。我仅使用单独的线程来收集传感器数据,然后发出带有新数据的信号。图的更新发生在主线程中。

enter image description here

import time 
import numpy as np
from threading import Thread
import pyqtgraph as pg
import bottleneck as bn
import PyQt5

class MySensor():
    def get_position(self,mean=0.0,standard_dev=0.1):
        # Random sensor data 
        return np.random.normal(mean,standard_dev,1)[0]

class SignalCommunicate(PyQt5.QtCore.QObject):
    # https://stackoverflow.com/a/45620056
    got_new_sensor_data = PyQt5.QtCore.pyqtSignal(float,float)
    position_updated = PyQt5.QtCore.pyqtSignal(float)

class LiveSensorViewer():

    def __init__(self,sensor_update_interval=25):
        # super().__init__()
        
        # How frequently to get sensor data and update graph 
        self.sensor_update_interval = sensor_update_interval

        # Init sensor object which gives live data 
        self.my_sensor = MySensor()
        
        # Init with default values
        self.current_position = self.my_sensor.get_position(mean=0.0,standard_dev=0.1)
        self.current_position_timestamp = time.time()

        # Init array which stores sensor data 
        self.log_time = [self.current_position_timestamp] 
        self.log_position_raw = [self.current_position] 
        self.moving_avg = 5

        # Define the array size on max amount of data to store in the list 
        self.log_size = 1 * 60 * 1000/self.sensor_update_interval  

        # Setup the graphs which will display sensor data 
        self.plot_widget = pg.GraphicsLayoutWidget(show=True)
        self.my_graph = self.plot_widget.addPlot(axisItems = {'bottom': pg.DateAxisItem()})
        self.my_graph.showGrid(x=True,y=True,alpha=0.25)
        self.my_graph.addLegend()

        # Curves to be drawn on the graph 
        self.curve_position_raw = self.my_graph.plot(self.log_time,self.log_position_raw,name='Position raw (mm)',pen=pg.mkPen(color='#525252'))
        self.curve_position_moving_avg = self.my_graph.plot(self.log_time,name='Position avg. 5 periods (mm)',pen=pg.mkPen(color='#FFF'))

        # A dialog box which displays the sensor value only. No graph. 
        self.my_dialog = PyQt5.QtWidgets.QWidget()
        self.verticalLayout = PyQt5.QtWidgets.QVBoxLayout(self.my_dialog)
    
        self.my_label = PyQt5.QtWidgets.QLabel()
        self.verticalLayout.addWidget(self.my_label)
        self.my_label.setText('Current sensor position:')

        self.my_sensor_value = PyQt5.QtWidgets.QDoubleSpinBox()
        self.verticalLayout.addWidget(self.my_sensor_value)
        self.my_sensor_value.setDecimals(6)

        self.my_dialog.show()

        # Signals that can be emitted 
        self.signalComm = SignalCommunicate()
         # Connect the signal 'position_updated' to the QDoubleSpinBox 
        self.signalComm.position_updated.connect(self.my_sensor_value.setValue)
        
        # Setup thread which will continuously query the sensor for data 
        self.position_update_thread = Thread(target=self.read_position,args=(self.my_sensor,self.sensor_update_interval))
        self.position_update_thread.daemon = True
        self.position_update_thread.start() # Start the thread to query sensor data 

    def read_position(self,sensor_obj,update_interval ):
        # This function continuously runs in a seprate thread to continuously query the sensor for data 

        sc = SignalCommunicate()
        sc.got_new_sensor_data.connect(self.handle_sensor_data)

        while True:
            # Get data and timestamp from sensor 
            new_pos = sensor_obj.get_position(mean=0.0,standard_dev=0.1)
            new_pos_time = time.time()

            # Emit signal with sensor data and  timestamp 
            sc.got_new_sensor_data.emit(new_pos,new_pos_time)

            # Wait before querying the sensor again 
            time.sleep(update_interval/1000)

    def handle_sensor_data(self,new_pos,new_pos_time ):

        # Get the sensor position/timestamp emitted from the separate thread 
        self.current_position_timestamp = new_pos_time
        self.current_position = new_pos

        # Emit a singal with new position info 
        self.signalComm.position_updated.emit(self.current_position)

        # Add data to log array 
        self.log_time.append(self.current_position_timestamp)
        if len(self.log_time) > self.log_size:
            # Append new data to the log and remove old data to maintain desired log size 
            self.log_time.pop(0)

        self.log_position_raw.append(self.current_position)
        if len(self.log_position_raw) > self.log_size:
            # Append new data to the log and remove old data to maintain desired log size 
            self.log_position_raw.pop(0)

        if len(self.log_time) <= self.moving_avg:
            # Skip calculating moving avg if only 10 data points collected from sensor to prevent errors 
            return 
        else:
            self.calculate_moving_avg()
        
        # Request a graph update 
        self.update_graph()

    def calculate_moving_avg(self):
        # Get moving average of the position 
        self.log_position_moving_avg = bn.move_mean(self.log_position_raw,window=self.moving_avg,min_count=1)

    def update_graph(self):
        self.curve_position_raw.setData(self.log_time,self.log_position_raw)
        self.curve_position_moving_avg.setData(self.log_time,self.log_position_moving_avg)

if __name__ == '__main__':
    import sys
    from PyQt5 import QtWidgets

    app = QtWidgets.QApplication(sys.argv)

    z = LiveSensorViewer()

    app.exec_()
    sys.exit(app.exec_())   

解决方法

我能够找到原始问题的解决方案。我在下面发布了解决方案,并详细说明了原始问题发生的原因。

问题

原始问题中的图形被冻结,因为PyQtGraph是从单独的线程而不是主线程更新的。可以通过打印输出

来确定函数从哪个线程执行。
threading.currentThread().getName()

在原始问题中,对update_graph()的调用是通过运行在单独线程中的handle_sensor_data()进行的。 handle_sensor_data()在单独的线程中运行的原因是,信号got_new_sensor_data的实例已连接到handle_sensor_data()内的插槽read_position(),该插槽也位于在单独的线程中运行。

解决方案

解决方案是通过发出一个信号来触发对update_graph()的调用,例如self.signalComm.request_graph_update.emit()内部的handle_sensor_data()。该信号request_graph_update必须从主线程即update_graph()内部连接到插槽__init__()

下面是解决方案的完整代码。

import time 
import numpy as np
import threading
from threading import Thread
import pyqtgraph as pg
import bottleneck as bn
import PyQt5


class MySensor():
    def get_position(self,mean=0.0,standard_dev=0.1):
        # Random sensor data 
        return np.random.normal(mean,standard_dev,1)[0]

class SignalCommunicate(PyQt5.QtCore.QObject):
    # https://stackoverflow.com/a/45620056
    got_new_sensor_data = PyQt5.QtCore.pyqtSignal(float,float)
    position_updated = PyQt5.QtCore.pyqtSignal(float)

    request_graph_update = PyQt5.QtCore.pyqtSignal()

class LiveSensorViewer():

    def __init__(self,sensor_update_interval=25):
        # super().__init__()
        
        # How frequently to get sensor data and update graph 
        self.sensor_update_interval = sensor_update_interval

        # Init sensor object which gives live data 
        self.my_sensor = MySensor()
        
        # Init with default values
        self.current_position = self.my_sensor.get_position(mean=0.0,standard_dev=0.1)
        self.current_position_timestamp = time.time()

        # Init array which stores sensor data 
        self.log_time = [self.current_position_timestamp] 
        self.log_position_raw = [self.current_position] 
        self.moving_avg = 5

        # Define the array size on max amount of data to store in the list 
        self.log_size = 1 * 60 * 1000/self.sensor_update_interval  

        # Setup the graphs which will display sensor data 
        self.plot_widget = pg.GraphicsLayoutWidget(show=True)
        self.my_graph = self.plot_widget.addPlot(axisItems = {'bottom': pg.DateAxisItem()})
        self.my_graph.showGrid(x=True,y=True,alpha=0.25)
        self.my_graph.addLegend()

        # Curves to be drawn on the graph 
        self.curve_position_raw = self.my_graph.plot(self.log_time,self.log_position_raw,name='Position raw (mm)',pen=pg.mkPen(color='#525252'))
        self.curve_position_moving_avg = self.my_graph.plot(self.log_time,name='Position avg. 5 periods (mm)',pen=pg.mkPen(color='#FFF'))

        # A dialog box which displays the sensor value only. No graph. 
        self.my_dialog = PyQt5.QtWidgets.QWidget()
        self.verticalLayout = PyQt5.QtWidgets.QVBoxLayout(self.my_dialog)
    
        self.my_label = PyQt5.QtWidgets.QLabel()
        self.verticalLayout.addWidget(self.my_label)
        self.my_label.setText('Current sensor position:')

        self.my_sensor_value = PyQt5.QtWidgets.QDoubleSpinBox()
        self.verticalLayout.addWidget(self.my_sensor_value)
        self.my_sensor_value.setDecimals(6)

        self.my_dialog.show()

        # Signals that can be emitted 
        self.signalComm = SignalCommunicate()
         # Connect the signal 'position_updated' to the QDoubleSpinBox 
        self.signalComm.position_updated.connect(self.my_sensor_value.setValue)
        # Update graph whenever the 'request_graph_update' signal is emitted 
        self.signalComm.request_graph_update.connect(self.update_graph)
        
        # Setup thread which will continuously query the sensor for data 
        self.position_update_thread = Thread(target=self.read_position,args=(self.my_sensor,self.sensor_update_interval))
        self.position_update_thread.daemon = True
        self.position_update_thread.start() # Start the thread to query sensor data 

    def read_position(self,sensor_obj,update_interval ):
        # print('Thread ={}          Function = read_position()'.format(threading.currentThread().getName()))

        # This function continuously runs in a seprate thread to continuously query the sensor for data 

        sc = SignalCommunicate() 
        sc.got_new_sensor_data.connect(self.handle_sensor_data)

        while True:
            # Get data and timestamp from sensor 
            new_pos = sensor_obj.get_position(mean=0.0,standard_dev=0.1)
            new_pos_time = time.time()

            # Emit signal with sensor data and  timestamp 
            sc.got_new_sensor_data.emit(new_pos,new_pos_time)

            # Wait before querying the sensor again 
            time.sleep(update_interval/1000)

    def handle_sensor_data(self,new_pos,new_pos_time ):
        print('Thread ={}          Function = handle_sensor_data()'.format(threading.currentThread().getName()))
        # Get the sensor position/timestamp emitted from the separate thread 
        self.current_position_timestamp = new_pos_time
        self.current_position = new_pos

        # Emit a singal with new position info 
        self.signalComm.position_updated.emit(self.current_position)

        # Add data to log array 
        self.log_time.append(self.current_position_timestamp)
        if len(self.log_time) > self.log_size:
            # Append new data to the log and remove old data to maintain desired log size 
            self.log_time.pop(0)

        self.log_position_raw.append(self.current_position)
        if len(self.log_position_raw) > self.log_size:
            # Append new data to the log and remove old data to maintain desired log size 
            self.log_position_raw.pop(0)

        if len(self.log_time) <= self.moving_avg:
            # Skip calculating moving avg if only 10 data points collected from sensor to prevent errors 
            return 
        else:
            self.calculate_moving_avg()
        
        # Request a graph update 
        # self.update_graph()                     # Uncomment this if you want update_graph() to run in the same thread as handle_sensor_data() function 

        # Emitting this signal ensures update_graph() will run in the main thread since the signal was connected in the __init__ function (main thread)
        self.signalComm.request_graph_update.emit()     

    def calculate_moving_avg(self):
        print('Thread ={}          Function = calculate_moving_avg()'.format(threading.currentThread().getName()))
        # Get moving average of the position 
        self.log_position_moving_avg = bn.move_mean(self.log_position_raw,window=self.moving_avg,min_count=1)

    def update_graph(self):
        print('Thread ={}          Function = update_graph()'.format(threading.currentThread().getName()))
        self.curve_position_raw.setData(self.log_time,self.log_position_raw)
        self.curve_position_moving_avg.setData(self.log_time,self.log_position_moving_avg)

if __name__ == '__main__':
    import sys
    from PyQt5 import QtWidgets

    app = QtWidgets.QApplication(sys.argv)

    z = LiveSensorViewer()

    app.exec_()
    sys.exit(app.exec_())   

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-