C.SH.K
C.SH.K

Reputation: 61

How to solve the self-crash problem of running multiple pyqt or pyqtgraph drawing components?

enter image description here

I designed the interface, and the custom component tried to output multiple drawing components, and the program crashed after a while.

The program probably consists of the following:ble. Py reads the bluetooth values temporarily holding the EMG array. main_plot.py instantiates the Show_EMG plotting class and outputs the Show_EMG plotting class reading the Bluetooth values of ble.PY

The program crashed itself without reporting any errors, I tried to output errors at different terminals.

ERROR MESSAGE: enter image description here

enter image description here

CMD: enter image description here

pyqtgraph Component Code(Show_EMG.py):

import ble
from pyqtgraph import PlotWidget
import pyqtgraph as pg
import numpy as np
from PyQt5 import QtCore

class Plot_Show(object):
    '''
    Form,y,x,data,length = 1800, width = 250, high = 120, text = "sEMG Voltage"
    '''
    def __init__(self,Form,y,x,data,text=""):
        # length = 1800, width = 250, high = 120, text = "sEMG Voltage"
        self.Form=Form
        self.y=y
        self.x=x
        self.data=ble.EMG[data]
        self.length=1800
        if(text==""):
            self.test="sEMG Voltage"
        else:
            self.text = text
        self.graphicsView = PlotWidget(self.Form)
        self.initUI()


    def initUI(self):

        self.graphicsView.setGeometry(QtCore.QRect(self.y, self.x, 250, 120))
        self.graphicsView.hideButtons()
        self.graphicsView.setObjectName(self.text)


        self.graphicsView.setLabel(axis="left",text=self.text)
        self.graphicsView.setLabel(axis='bottom',text='Time')
        self.graphicsView.setMouseEnabled(x=False,y=False)
        self.graphicsView.setAntialiasing(True)
        self.graphicsView.setMenuEnabled(False)
        self.graphicsView.hideButtons()
        self.data1 = np.zeros(self.length)
        self.curve1 = self.graphicsView.plot(self.data1)
        self.ptr1 = 0

        def update1():
            global data1, ptr1
            self.graphicsView.setRange(xRange=[self.ptr1,self.ptr1+self.length],yRange=[5,550],padding=0)
            self.data1[:-1] = self.data1[1:]  # shift data in the array one sample left

            self.data1[-1] = self.data

            self.ptr1 += 1
            self.curve1.setData(self.data1)
            self.curve1.setPos(self.ptr1, 0)

        self.timer = pg.QtCore.QTimer()
        self.timer.timeout.connect(update1)
        self.timer.start(10)

main_plot.py Code:

import ble
import sys

import Show_EMG
from PyQt5 import QtCore, QtWidgets
import threading

class Ui_Form(object):
    def __init__(self):
        super().__init__()

    def setupUi(self, Form,**kwargs):
        Form.resize(820, 454)
        Form.setObjectName("Form")

                Show_EMG.Plot_Show(Form=Form, y=10, x=10, data=0, text="sEMG2 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=10, x=140, data=1, text="sEMG2 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=10, x=270, data=2, text="sEMG3 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=280, x=10, data=3, text="sEMG4 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=280, x=140, data=4, text="sEMG5 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=280, x=270, data=5, text="sEMG6 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=550, x=10, data=0, text="sEMG7 Voltage")
        Show_EMG.Plot_Show(Form=Form, y=550, x=140, data=0, text="sEMG8 Voltage")

        self.gridLayoutWidget = QtWidgets.QWidget(Form)
        self.gridLayoutWidget.setGeometry(QtCore.QRect(550, 270, 261, 121))
        self.gridLayoutWidget.setObjectName("gridLayoutWidget")
        self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setGeometry(QtCore.QRect(370, 410, 75, 23))
        self.pushButton.setObjectName("pushButton")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        self.pushButton.setText(_translate("Form", "开始记录"))
        Form.setWindowTitle(_translate("Form", "Form"))

def main():
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    thread_main=threading.Thread(target=main)
    thread_main.start()
    thread_ble=threading.Thread(target=ble.ble)
    thread_ble.start()

Ble.EMG array default temporarily to:[200. 0. 0. 0. 0. 0.]

More Ble Code detail: https://gist.github.com/allrobot/1547447f313942f278118cb2e569f59f

I tried to add threads in main_plot.py, but the program still crashes itself...

Perhaps QTimer should be the cause of the problem?

How can I change the code to solve the self-crash problem?I need to fix the custom component classes, but I am novice to PyQT, you have any suggestions?

Upvotes: 1

Views: 556

Answers (2)

furas
furas

Reputation: 142909

As you already know (all) GUIs shouldn't run in separated thread.

But main problem which I had with code is that some elements in PyQt don't work if they are not assigned to global or class variables.

And when Plot is not assigned to variable then its QTimer doesn't work for me.

So I put all Plot on list and now all QTimers (without special threads) works for me.

        self.plots = [
            PlotShow(...),
            PlotShow(...),
            PlotShow(...),
            # ...
        ]

Full working code.

I used class ble to simulate module ble.py and to have all code in one file (for tests).

I made also few small changes: PEP 8 -- Style Guide for Python Code

#import ble
from PyQt5 import QtCore, QtWidgets
import pyqtgraph as pg   # pg.PlotWidget
import numpy as np
import threading


class ble:
    ''' Simulate module `ble.py` '''
    
    EMG = [0,0,0,0,0,0,0,0,0,0,0,0,0]
    
    def ble():
        import math
        import random
        import time

        counter = 0
        
        while True:
        
            for i in range(10):
            
                if i in (0, 4, 8):
                    ble.EMG[i] = random.randint(0, 550)
                elif i in (1, 5, 6):
                    ble.EMG[i] = random.randint(150, 250)
                else:
                    ble.EMG[i] = 200 + math.sin(math.radians(counter//i)) * 200
            
            counter += 1
            time.sleep(0.1)
    
class PlotShow():  # PE8: `CamelNames` for classes
    '''
    form, y, x, data, length=1800, width=250, high=120, text="sEMG Voltage"
    '''
    def __init__(self, form, y, x, data_number, text=""):   # PEP8: spaces after commans `,`
        self.form = form   # PE8: `lower_case_names` for variables
        self.y = y
        self.x = x
        self.data_number = data_number
        self.length = 1800

        self.data = np.zeros(self.length)
        self.ptr  = 0
        
        if not text:
            self.test="sEMG Voltage"
        else:
            self.text = text
        
        self.initUI()

        #print('start:', self.text)
        self.timer = pg.QtCore.QTimer()
        self.timer.timeout.connect(self.update_plot)
        self.timer.start(10)


    def initUI(self):
        self.graphicsView = pg.PlotWidget(self.form)

        self.graphicsView.setGeometry(QtCore.QRect(self.y, self.x, 250, 120))
        self.graphicsView.hideButtons()
        self.graphicsView.setObjectName(self.text)

        self.graphicsView.setLabel(axis="left",text=self.text)
        self.graphicsView.setLabel(axis='bottom',text='Time')
        self.graphicsView.setMouseEnabled(x=False,y=False)
        self.graphicsView.setAntialiasing(True)
        self.graphicsView.setMenuEnabled(False)
        self.graphicsView.hideButtons()
        
        self.curve = self.graphicsView.plot(self.data)

    def update_plot(self):
        #print('update:', self.text)
        
        self.data[:-1] = self.data[1:]  # shift data in the array one sample left
        self.data[-1]  = ble.EMG[self.data_number]

        self.ptr += 1

        self.curve.setData(self.data)
        self.curve.setPos(self.ptr, 0)
        self.graphicsView.setRange(xRange=[self.ptr, self.ptr+self.length], yRange=[5, 550], padding=0)  # PEP8: spaces after commans `,`

        
class UIForm():  # PE8: `CamelNames` for classes

    def setupUI(self, form, **kwargs):
        form.resize(820, 454)
        form.setObjectName("Form")

        self.plots = [
            PlotShow(form=form, y=10,  x=10,  data_number=0, text="sEMG1 Voltage"),
            PlotShow(form=form, y=10,  x=140, data_number=1, text="sEMG2 Voltage"),
            PlotShow(form=form, y=10,  x=270, data_number=2, text="sEMG3 Voltage"),
            PlotShow(form=form, y=280, x=10,  data_number=3, text="sEMG4 Voltage"),
            PlotShow(form=form, y=280, x=140, data_number=4, text="sEMG5 Voltage"),
            PlotShow(form=form, y=280, x=270, data_number=5, text="sEMG6 Voltage"),
            PlotShow(form=form, y=550, x=10,  data_number=6, text="sEMG7 Voltage"),
            PlotShow(form=form, y=550, x=140, data_number=7, text="sEMG8 Voltage"),
            PlotShow(form=form, y=550, x=270, data_number=8, text="sEMG9 Voltage"),
        ]
        
        self.gridLayoutWidget = QtWidgets.QWidget(form)
        self.gridLayoutWidget.setGeometry(QtCore.QRect(550, 270, 261, 121))
        self.gridLayoutWidget.setObjectName("gridLayoutWidget")
        
        self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        
        self.pushButton = QtWidgets.QPushButton(form)
        self.pushButton.setGeometry(QtCore.QRect(370, 410, 75, 23))
        self.pushButton.setObjectName("pushButton")

        self.retranslateUi(form)
        
        QtCore.QMetaObject.connectSlotsByName(form)

    def retranslateUi(self, form):
        _translate = QtCore.QCoreApplication.translate
        self.pushButton.setText(_translate("Form", "开始记录"))
        form.setWindowTitle(_translate("Form", "Form"))

def main():
    app = QtWidgets.QApplication([])
    form = QtWidgets.QWidget()  # PE8: `lower_case_names` for variables
    ui = UIForm()
    ui.setupUI(form)
    form.show()
    app.exec()

if __name__ == "__main__":
    # better start before GUI to create all needed variables and values
    thread_ble = threading.Thread(target=ble.ble)
    thread_ble.start()
    
    #thread_main = threading.Thread(target=main)
    #thread_main.start()
    #input() # keep running program when GUI runs in thread
    
    # GUI rather shouldn't run in separated thread
    main()

enter image description here

Frankly, if all plots get data from the same source and in the same time then you could use one QTimer to run all update1 in all plots - but this timer should be in UIForm instead of PlotShow


EDIT:

Version which use only one QTimer in UIForm to execute update_plot() for all plots on list self.plots.

#import ble
from PyQt5 import QtCore, QtWidgets
import pyqtgraph as pg   # pg.PlotWidget
import numpy as np
import threading


class ble:
    ''' Simulate module `ble.py` '''
    
    EMG = [0,0,0,0,0,0,0,0,0,0,0,0,0]
    
    def ble():
        import math
        import random
        import time

        counter = 0
        
        while True:
        
            for i in range(10):
            
                if i in (0, 4, 8):
                    ble.EMG[i] = random.randint(0, 550)
                elif i in (1, 5, 6):
                    ble.EMG[i] = random.randint(150, 250)
                else:
                    ble.EMG[i] = 200 + math.sin(math.radians(counter//i)) * 200
            
            counter += 1
            time.sleep(0.1)
    
class PlotShow():  # PE8: `CamelNames` for classes
    '''
    form, y, x, data, length=1800, width=250, high=120, text="sEMG Voltage"
    '''
    def __init__(self, form, y, x, data_number, text=""):   # PEP8: spaces after commans `,`
        self.form = form   # PE8: `lower_case_names` for variables
        self.y = y
        self.x = x
        self.data_number = data_number
        self.length = 1800

        self.data = np.zeros(self.length)
        self.ptr  = 0
        
        if not text:
            self.test="sEMG Voltage"
        else:
            self.text = text
        
        self.initUI()

        #print('start:', self.text)
        #self.timer = pg.QtCore.QTimer()
        #self.timer.timeout.connect(self.update_plot)
        #self.timer.start(10)


    def initUI(self):
        self.graphicsView = pg.PlotWidget(self.form)

        self.graphicsView.setGeometry(QtCore.QRect(self.y, self.x, 250, 120))
        self.graphicsView.hideButtons()
        self.graphicsView.setObjectName(self.text)

        self.graphicsView.setLabel(axis="left",text=self.text)
        self.graphicsView.setLabel(axis='bottom',text='Time')
        self.graphicsView.setMouseEnabled(x=False,y=False)
        self.graphicsView.setAntialiasing(True)
        self.graphicsView.setMenuEnabled(False)
        self.graphicsView.hideButtons()
        
        self.curve = self.graphicsView.plot(self.data)

    def update_plot(self):
        #print('update:', self.text)
        
        self.data[:-1] = self.data[1:]  # shift data in the array one sample left
        self.data[-1]  = ble.EMG[self.data_number]

        self.ptr += 1

        self.curve.setData(self.data)
        self.curve.setPos(self.ptr, 0)
        self.graphicsView.setRange(xRange=[self.ptr, self.ptr+self.length], yRange=[5, 550], padding=0)  # PEP8: spaces after commans `,`

        
class UIForm():  # PE8: `CamelNames` for classes

    def setupUI(self, form, **kwargs):
        form.resize(820, 454)
        form.setObjectName("Form")

        self.plots = [
            PlotShow(form=form, y=10,  x=10,  data_number=0, text="sEMG1 Voltage"),
            PlotShow(form=form, y=10,  x=140, data_number=1, text="sEMG2 Voltage"),
            PlotShow(form=form, y=10,  x=270, data_number=2, text="sEMG3 Voltage"),
            PlotShow(form=form, y=280, x=10,  data_number=3, text="sEMG4 Voltage"),
            PlotShow(form=form, y=280, x=140, data_number=4, text="sEMG5 Voltage"),
            PlotShow(form=form, y=280, x=270, data_number=5, text="sEMG6 Voltage"),
            PlotShow(form=form, y=550, x=10,  data_number=6, text="sEMG7 Voltage"),
            PlotShow(form=form, y=550, x=140, data_number=7, text="sEMG8 Voltage"),
            PlotShow(form=form, y=550, x=270, data_number=8, text="sEMG9 Voltage"),
        ]
        
        self.gridLayoutWidget = QtWidgets.QWidget(form)
        self.gridLayoutWidget.setGeometry(QtCore.QRect(550, 270, 261, 121))
        self.gridLayoutWidget.setObjectName("gridLayoutWidget")
        
        self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.gridLayout.setObjectName("gridLayout")
        
        self.pushButton = QtWidgets.QPushButton(form)
        self.pushButton.setGeometry(QtCore.QRect(370, 410, 75, 23))
        self.pushButton.setObjectName("pushButton")

        self.retranslateUi(form)
        
        QtCore.QMetaObject.connectSlotsByName(form)

        self.timer = pg.QtCore.QTimer()
        self.timer.timeout.connect(self.update_all_plots)
        self.timer.start(10)
        
    def update_all_plots(self):
        for plot in self.plots:
            plot.update_plot()
        
    def retranslateUi(self, form):
        _translate = QtCore.QCoreApplication.translate
        self.pushButton.setText(_translate("Form", "开始记录"))
        form.setWindowTitle(_translate("Form", "Form"))

def main():
    app = QtWidgets.QApplication([])
    form = QtWidgets.QWidget()  # PE8: `lower_case_names` for variables
    ui = UIForm()
    ui.setupUI(form)
    form.show()
    app.exec()

if __name__ == "__main__":
    # better start before GUI to create all needed variables and values
    thread_ble = threading.Thread(target=ble.ble)
    thread_ble.start()
    
    #thread_main = threading.Thread(target=main)
    #thread_main.start()
    #input() # keep running program when GUI runs in thread
    
    # GUI rather shouldn't run in separated thread
    main()

Upvotes: 1

C.SH.K
C.SH.K

Reputation: 61

Thank @furas and @musicamante suggests, the problem is solved.

The bug is surprisingly simple.

I set the refresh rate too fast. Because I setting self.timer.start(10) in the custom component class, So the program crashes itself.🤣It only need setting self.timer.start(100), the program can working...

Upvotes: 0

Related Questions