首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >PyQtGraph在抓取活传感器数据时停止更新和冻结

PyQtGraph在抓取活传感器数据时停止更新和冻结
EN

Stack Overflow用户
提问于 2020-10-11 18:47:27
回答 1查看 1.7K关注 0票数 3

我正在使用PyQt5和比例尺来绘制实时传感器数据。这个图形是一个更大的PyQt5应用程序的一部分,它用于与各种硬件交互并可视化传感器数据。

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

问题:图形和传感器读取工作如预期的那样。但是,在运行应用程序几秒钟之后,pyqtgraph停止更新并冻结。一旦图形冻结,我看到图刷新/更新的唯一时间是尝试调整窗口大小或将焦点分配到另一个窗口并将焦点重新聚焦到图形窗口。在这种情况下,图形将只更新一次,不会继续刷新。

我已经阅读了其他用户有类似的问题,在下面的链接。但是,建议的解决方案不是从单独的线程更新GUI。在我的例子中,我不是从一个单独的线程更新图表。我只使用单独的线程收集传感器数据,然后发出一个信号,与新的数据。图的更新发生在主线程中。

代码语言:javascript
运行
复制
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, self.log_position_raw, 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_())   
EN

回答 1

Stack Overflow用户

发布于 2020-10-25 03:20:40

我找到了解决我原来问题的办法。我在下面公布了解决办法,并详细阐述了最初问题发生的原因。

The issue

原始问题中的图形是冻结的,因为PyQtGraph是从一个单独的线程而不是主线程更新的。的输出,可以确定函数从哪个线程执行。

代码语言:javascript
运行
复制
threading.currentThread().getName()

在最初的问题中,对update_graph()的调用是从运行在单独线程中的handle_sensor_data()发出的。handle_sensor_data()在一个单独的线程中运行的原因是,信号got_new_sensor_data的实例连接到read_position()中的插槽handle_sensor_data(),该槽也在一个单独的线程中运行。

解决方案

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

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

代码语言:javascript
运行
复制
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, self.log_position_raw, 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_())   
票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/64307813

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档