I am using PyQt5 and pyqtgraph to plot live sensor data. This graph is part of a bigger PyQt5 application which is used to interact with various hardware and also visualize the sensor data.
Background: Below code is a very simplified example for the code that is responsible for querying the sensor for data and then plotting a graph of the instantaneous position and its moving average. The sensor is queried in a separate thread at every x ms intervals.
Problem: The graphing and sensor reading works as expected. However, after running the application for a few seconds, the pyqtgraph stops updating and freezes. Once the graph freezes, the only time I see the graph refresh/update is if I try to resize the window or give focus to another window and refocus back to the graph window. In such cases the graph will be updated only once and it doesn't continue to refresh.
I have read other user having similar issue in links like below. However the proposed solution was not to update the GUI from a separate thread. In my case, I am NOT updating the graph from a separate thread. I am only using the separate thread to collect sensor data then emit a signal which with the new data. The updating of the graph occurs in the main thread.
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_())