Console debug information and GUI are not in synchronized


#1

I have written small GUI program with console debug information. Thread are used reading from serial port.

This program gets “messages” from serial port and show right green light on GUI and debug information on the console.

First test: Tested with old and new PC
I send messages from terminal program like Bray++ using excelent scripting possibility.
Script first light on lamp then pause 100ms and after that shut down lamp and immediately light on the next lamp and so one. (send right messages)
Lamps “running smooth” and console data is in syncrone (as good as I can read).
Test period was long.

Second test: old PC
Arduino gets messages from “black box” and route messages to my program (in old PC). Everything is OK.
Test period was quite short.

Third test: New PC
Arduino gets messages from “black box” and route messages to my program (in new PC). Quite often one lamp stay light on. Console debug test says shut down this lamp.
I am embarrassed.
If after that “black box” sends message shut down this lamp it shut down.
My program crash relatively often
Test period was quite short.

I know the black box bounce a lot. Debouncing is another story and this time it is not possible.

My question is:
Is there something wrong with my code?
Is it possible that runnig enviroment old / new PC and Python and PyQt versions influence to the result?
How can I fix the problem :slight_smile:

Old PC:
Intel i5-3450CPU 64-bit and win10 is 64-bit
Python 3.6.1 32 bit (Intel) on win32

New PC:
Intel i7-6500U 64-bit and win10
Python 3.7 32bit

BR: Timo

Attachmen is screen capture when error is active


from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

import time
import serial
import sys 
import testUI

class Worker(QRunnable):
    '''
    Worker thread
    '''
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    @pyqtSlot()    
    
    def run(self):
        '''
        Execute the runner function with passed self.args,
        self.kwargs.
        '''
        print("Start ", self.fn)
        self.fn()
          

class MainWindow(QtWidgets.QMainWindow, testUI.Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
            
        self.setupUi(self)  # This is defined in design.py file automatically
    
        self.threadpool = QThreadPool()
            
        self.counter = 0 #GUI is alive if counter run
              
        #self.timer = QTimer()
        #self.timer.setInterval(1000)
        #self.timer.timeout.connect(self.recurring_timer)
        #self.timer.start()
            
        self.openSerial("com9") # --- USED COM PORT ------

        #Initial values
        print("Up OFF")
        self.pixmap = QPixmap('off.jpg')
        self.l_up.setPixmap(self.pixmap)
      
        print("Right OFF")
        pixmap = QPixmap('off.jpg')
        self.l_right.setPixmap(pixmap)
    
        print("Down OFF")
        self.pixmap = QPixmap('off.jpg')
        self.l_down.setPixmap(self.pixmap)
    
        print("Left OFF")
        self.pixmap = QPixmap('off.jpg')
        self.l_left.setPixmap(self.pixmap)
              
    def openSerial(self, port):
        self.port = port
        try:
            self.ser = serial.Serial(self.port, baudrate=19200, timeout=1,
            writeTimeout=0,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS    
            )
            print(self.ser.name)
            worker = Worker(self.serialsub)
            self.threadpool.start(worker) #Start serial read thread
        except:
            print("NO COM PORT") 
    
    def serialsub(self):
        while True:
            self.reading = self.ser.readline().decode('latin-1')
            print(self.reading)
            #remove /r /n
            self.reading = self.reading[:-2]
         
            #UP ON
            if (self.reading == ("123-0x0d")):
                print("Up ON ",end='', flush=True) #debug print to console
                pixmap = QPixmap('on.jpg')
                self.l_up.setPixmap(pixmap)                
            #UP OFF
            if (self.reading == ("123-0x0e")):
                print("Up OFF")
                self.pixmap = QPixmap('off.jpg')
                self.l_up.setPixmap(self.pixmap)
        
            #Right ON    
            if (self.reading == ("123-0x13")):
                print("Right ON ",end='', flush=True)
                self.pixmap = QPixmap('on.jpg')
                self.l_right.setPixmap(self.pixmap)
            #Right OFF
            if (self.reading == ("123-0x14")):
                print("Right OFF")
                self.pixmap = QPixmap('off.jpg')
                self.l_right.setPixmap(self.pixmap)
        
            #Down ON    
            if (self.reading == ("123-0x19")):
                print("Down ON ",end='', flush=True)
                self.pixmap = QPixmap('on.jpg')
                self.l_down.setPixmap(self.pixmap)
            #Down OFF
            if (self.reading == ("123-0x1a")):
                print("Down OFF")
                self.pixmap = QPixmap('off.jpg')
                self.l_down.setPixmap(self.pixmap)
        
            #Left ON
            if (self.reading == ("123-0x21")):
                print("Left ON ",end='', flush=True)
                self.pixmap = QPixmap('on.jpg')
                self.l_left.setPixmap(self.pixmap)
            #Left OFF
            if (self.reading == ("123-0x22")):
                print("Left OFF")
                self.pixmap = QPixmap('off.jpg')
                self.l_left.setPixmap(self.pixmap) 
              
    def recurring_timer(self):
        """show whether the GUI is alive"""
        self.counter += 1
        #self.l_time.setText("Time: %d" % self.counter)
        self.l_time.setText(": %d" % self.counter)
    
def main():
    app = QtWidgets.QApplication(sys.argv)  # A new instance of QApplication
    form = MainWindow()  # We set the form to be our ExampleApp (design)
    form.show()  # Show the form
    app.exec_()  # and execute the app

if __name__ == '__main__':  # if we're running file directly and not importing it
    main()  # run the main function

#2

Hi timo! Welcome to the forum :slight_smile: That’s a nice interesting first question.

So the first thing I noticed was that the codes in the terminal image shown don’t match exactly what you’re looking for see below (look at the codes for Right) —

35505da59bd8ad93867c544d7a342b8dd0dc8279_1_353x500

You have —

Right ON    123-0x14
Right OFF  123-0x13
Right ON    123-0x14
Right OFF  **123-0x19**

According to your code 123-0x19 should be DOWN ON. However the subsequent line uses 123-0x1a for DOWN ON, which is actually DOWN OFF.

I think this is just a case of sending the wrong codes :slight_smile: …the curse of live demos!

Some other tips for the code —

  1. Stripping off the \r and \n can be done with .rstrip().
  2. You might want to store these codes in a dict and do a look up, rather than having the multiple if blocks, e.g

Create a method

def down_on(self):
    print("Down ON ",end='', flush=True)
    self.pixmap = QPixmap('on.jpg')
    self.l_down.setPixmap(self.pixmap)

…then a dictionary to store the mapping from codes to methods…

actions = {
    '123-0x19': self.down_on
}

…then you can call them as follows:

action = actions.get(self.reading, None)  # return None if the code doesn't match any known action
if action:  # check it's not None
   action()   # call the method

This helps separate what you want to happen from the logic of when it should happen.

Hope that helps a bit :slight_smile:


#3

Hi

Thank you for answering.
I appreciate your tip to clean my code.
Yes, I send wrong code, sorry for that.

But I have still the same problem. Script crash.

The original setup was
-Black box sends message using Control Area Network
-Arduino compatible CAN to SPI device converts messages to Arduino compatible
-Arduino send messages to PC

I think “Too complicated” and possibles where error can be.

I ordered small device which can convert CAN messages directly to USB-port.
http://canable.io
I changed python script so that it reads directly the CAN messages

New setup
-Blackbox send CAN messages directly to PC
-Added CAN bus to the another PC that running Wireshark.

Short test period
-Messages coming continuously
-GUI running smooth and debug information to the terminal is OK
-Wireshark shows messages OK
-Suddenly GUI crash and disappeared
-Terminal prompt blinking for ready to new operation
-Wireshark shows still messages

Question is
Is there any (easy) way to debug why python script crashing?
I believe running the script step by step to debug do not work because GUI and threading.

Next I must try
-Removing all GUI related code
-Removing threading
-Only terminal print
-This is sad because I want learning how GUI script works

BR: Timo


#4

Hi

This code do not crash

from canard import can
from canard.hw import cantact

import sys 
              
def readCAN(port):
    actions = {
        '0x123-0xd': up_on, 
        '0x123-0xe': up_off, 
        '0x123-0x13': right_on, 
        '0x123-0x14': right_off, 
        '0x123-0x19': down_on, 
        '0x123-0x1a': down_off, 
        "0x123-0x21": left_on, 
        "0x123-0x22": left_off
    }
    
    dev = cantact.CantactDev(port)
    dev.set_bitrate(250000) # Set the bitrate
    dev.start()            
    
    while True:
        frame = dev.recv() # Receive a CAN frame
        command = hex(frame.id) # CAN ID
        command = command +'-' # Only separator
    
        '''frame.data is payload
           dlc is CAN frame payload length'''
        command = command + hex(frame.data[frame.dlc-1]) 
    
        action = actions.get(command,  None)
        if action:
            action()
         
def up_on():
    print("UP ON ",end='', flush=True)

def up_off():
    print("UP OFF ")
    
def right_on():
    print("Right ON ",end='', flush=True)

def right_off():
    print("Right OFF ")

def down_on():
    print("Down ON ",end='', flush=True)

def down_off():
    print("Down OFF ")
            
def left_on():
    print("Left ON ",end='', flush=True)
 
def left_off():
    print("Left OFF ")

    
def main():
    readCAN(sys.argv[1]) # --- USED COM PORT ------

if __name__ == '__main__':  # if we're running file directly and not importing it
    main()  # run the main function

Next I must add very simple GUI.
Perhaps only window title. Crash it again?
Then I add more GUI parts and try when crash happens again.

I think threading is problem in my script.

BR: Timo

Ps: I trying paste my code between Preformatted text </> but still
it not show so good

Code tags?


#5

I’ve fixed up your code — you just need to indent by 4 spaces, and then it turns into a code block.

Re: the crashing, the most common reason for Qt GUIs to crash when you’re using threads is trying to update GUI elements from outside the GUI (main) thread. Qt GUI components aren’t thread-safe, so you need to pass your data/update out of your data/processing thread back to the GUI before updating.

If there is an error in your Python then that will likely cause a crash too, but in that case you’ll get an error message.

Can you post your GUI code so I can take a look?


#6

Thanks again

Code is not beautiful.
Example
self.actions = {
‘0x123-0xd’: self.up_on,
do not belong in the “GUI-loop”
When code running and do not crash I must cleaning it.

from PyQt5 import  QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

from canard import can
from canard.hw import cantact

import sys 
import testUI #QCreator make this UI file

class Worker(QRunnable):
    '''
    Worker thread
    '''
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
    
    @pyqtSlot()    
        
    def run(self):
        self.fn()
        
class MainWindow(QtWidgets.QMainWindow, testUI.Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
                
        self.setupUi(self)  # This is defined in testUI.py file automatically
                
        self.threadpool = QThreadPool()
                
        self.counter = 0 #GUI is alive if counter run
                  
        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()
                
        self.openSerial((sys.argv[1])) # --- USED COM PORT ------
    
        self.actions = {
            '0x123-0xd': self.up_on, 
            '0x123-0xe': self.up_off, 
            '0x123-0x13': self.right_on, 
            '0x123-0x14': self.right_off, 
            '0x123-0x19': self.down_on, 
            '0x123-0x1a': self.down_off, 
            "0x123-0x21": self.left_on, 
            "0x123-0x22": self.left_off
        }
  
                  
    def openSerial(self, port):
        self.port = port
        
        try:
            self.dev = cantact.CantactDev(self.port) # setup for CAN
            print(self.dev)
            self.dev.set_bitrate(250000) # Set the bitrate
            print("Port: ", self.port)
            self.dev.start()    # start CAN        
            
            worker = Worker(self.serialsub)
            self.threadpool.start(worker) #Start serial read thread
        except:
            print("NO COM PORT") 
        
    def serialsub(self):
        while True:
            self.frame = self.dev.recv() # Receive a CAN frame

            self.command = hex(self.frame.id) # frame.id is CAN id
            self.command = self.command +'-'            
            #frame.data is payload
            #dlc is CAN frame payload length                        
            self.command = self.command + hex(self.frame.data[self.frame.dlc-1])
                        
            print(self.command)                               
            
            action = self.actions.get(self.command,  None)
            if action:
                action()
             
    def up_on(self):
        print("UP ON ",end='', flush=True)
        self.pixmap = QPixmap('on.jpg')
        self.l_up.setPixmap(self.pixmap)
 
    def up_off(self):
        print("UP OFF ",end='', flush=True)
        self.pixmap = QPixmap('off.jpg')
        self.l_up.setPixmap(self.pixmap)

    def right_on(self):
        print("Right ON ",end='', flush=True)
        self.pixmap = QPixmap('on.jpg')
        self.l_right.setPixmap(self.pixmap)
 
    def right_off(self):
        print("Right OFF ",end='', flush=True)
        self.pixmap = QPixmap('off.jpg')
        self.l_right.setPixmap(self.pixmap) 

    def down_on(self):
        print("Down ON ",end='', flush=True)
        self.pixmap = QPixmap('on.jpg')
        self.l_down.setPixmap(self.pixmap)
 
    def down_off(self):
        print("Down OFF ",end='', flush=True)
        self.pixmap = QPixmap('off.jpg')
        self.l_down.setPixmap(self.pixmap)  
                
    def left_on(self):
        print("Left ON ",end='', flush=True)
        self.pixmap = QPixmap('on.jpg')
        self.l_left.setPixmap(self.pixmap)
 
    def left_off(self):
        print("Left OFF ",end='', flush=True)
        self.pixmap = QPixmap('off.jpg')
        self.l_left.setPixmap(self.pixmap)  

    def recurring_timer(self):
        """show whether the GUI is alive"""
        self.counter += 1
        #self.l_time.setText("Time: %d" % self.counter)
        self.l_time.setText(": %d" % self.counter)
        
def main():
    app = QtWidgets.QApplication(sys.argv)  # A new instance of QApplication
    form = MainWindow()  # We set the form to be our ExampleApp (design)
    form.show()  # Show the form
    app.exec_()  # and execute the app

if __name__ == '__main__':  # if we're running file directly and not importing it
    main()  # run the main function

#7

Hey great, much easier to see the issue with the full code — the problem is likely this here in your serialsub block —

def serialsub(self):
    while True:
        self.frame = self.dev.recv() # Receive a CAN frame

        self.command = hex(self.frame.id) # frame.id is CAN id
        self.command = self.command +'-'            
        #frame.data is payload
        #dlc is CAN frame payload length                        
        self.command = self.command + hex(self.frame.data[self.frame.dlc-1])
                    
        print(self.command)                               
        
        action = self.actions.get(self.command,  None)
        if action:
            action()

At the bottom you get the action, which is fine, but then you call the action directly. Each of these actions call self.l_left.setPixmap(self.pixmap) (or similar) which are GUI methods. So, you end up call GUI methods from within your worker thread. This will crash.

The solution is to use Qt signals to send data back to the GUI thread from your worker, and then in the GUI thread update the GUI. There is an example of this in my book, but the the below should get you there —

So first you need to define some signals. Since your run method is on your MainWindow (and so can access self there, we can just define them on that object.

class MainWindow(QtWidgets.QMainWindow, testUI.Ui_MainWindow):

    event = pyqtSignal(str)  # A signal that emits a string.

The block in serialsub method needs to change to emit the command string (rather than call the action directly). Note that I’ve also removed all the self as they aren’t necessary — they’re local vars here.

def serialsub(self):
    while True:
        frame = self.dev.recv() # Receive a CAN frame

        command = hex(frame.id) + '-' # frame.id is CAN id
        #frame.data is payload
        #dlc is CAN frame payload length                        
        command = command + hex(frame.data[frame.dlc-1])
                    
        print(command)                               
        
        if command in self.actions:
            self.event.emit(command)

Finally we need something to catch this event and act on it. We can create a custom method —

def handle_event(self, command):
    action = self.actions.get(command,  None)
    if action:
        action()

We can then connect this up, in your init block, with e.g.

self.event.connect(self.handle_event)

With this connected handle_event will be called every time your thread emits an event, and the changes to the GUI are always happening within the GUI thread.