gpt4 book ai didi

python - 绘制实时传感器数据时,PyQtGraph 停止更新并卡住

转载 作者:行者123 更新时间:2023-12-04 13:11:50 24 4
gpt4 key购买 nike

我正在使用 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, 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_())

最佳答案

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

问题

原始问题中的图表被卡住了,因为 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, 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_())

关于python - 绘制实时传感器数据时,PyQtGraph 停止更新并卡住,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64307813/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com