PyQT slot calls seem to jump to the front of the event loop

53 Views Asked by At

I'm writing a GUI in PyQT to control some hardware. I've had a lot of problems with one particular instrument that I'm accessing through a company provided dll. It seems like something in the dll impacts the way Qt processes, but I don't understand how it works well.

In this case, I'm trying make a simple troubleshooting program that is periodically asking the equipment for its status based on a QTimer. I also have a button that runs an example "long process". I move these operations to a separate thread to not block the GUI. Something about this dll makes it such that I cannot connect to the equipment from multiple threads. I create this "Eqpt_Thread" and connect it to the main window. I find that the function connected to the timer can outpace itself occasionally. In the "NKT_System" class, I wrote an "is_busy" flag to try to block simultaneously operations from multiple threads (I also tried this using QMutex and has the same issues). I print out the status of the is_busy attribute going in and out of each method call within "NKT_System", and I find that the "long process" function is being called in the middle of the "get_eqpt_status" function execution (or vice versa). This causes a never-ending loop with my is_busy check since "long_process" is acting in the middle of "get_eqpt_status".

My issue is, "long_process" should be waiting for the currently running function "get_eqpt_status" to finish. This is operating on a single thread, as confirmed by the printed "QThread.currentThread()" lines. How can "long_process" be interrupting execution of "get_eqpt_status" on a single thread?

Note, I created a dummy program that replaces "get_eqpt_status" with print(1); print(2) and "long_process" with print('a'); time.sleep(4); print('b'), and I get the expected output 1, 2, a, b, 1, 2. When I'm using this equipment connection code, it's like I'm getting 1, 2, a, 1 - then everything crashes because of this unexpected behavior.

I've also tried not using Qt.QueuedConnection, though I figured that would help ensure I'm not overriding the EventLoop Queue when I click the button. Also I tried moving the QTimer that triggers get_eqpt_status into the Eqpt_Controller class, but that didn't give different results.

Also, there is some C++ example code provided with the DLL that shows use of the Qt class they use which gives me errors when multithreading. I see in the C++ example that they are creating local QEventLoop's. I wonder if that could be related, but I don't understand Qt or what is being provided in the DLL enough to know.

Code:

import sys
import time

from PyQt5.QtCore import (QObject, Qt, QTimer, pyqtSignal, pyqtSlot, QThread)
from PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow,
                             QPushButton, QGridLayout, QWidget)

from catalight.equipment.light_sources import nkt_system


class MainWindow(QMainWindow):
    """Subclass QMainWindow to customize your application's main window."""
    def __init__(self):
        super().__init__()
        
        print('Main window on:\t', QThread.currentThread())
        self.layout = QGridLayout()
        self.widget = QWidget()  # Main widget

        # Create some widgets in GUI
        self.statusLabel1 = QLabel("Status Line 1:")
        self.statusLabel2 = QLabel("Status Line 2:")
        self.statusLabel3 = QLabel("Current Time:")
        self.statusLine1 = QLabel("-")
        self.statusLine2 = QLabel("-")
        self.statusLine3 = QLabel("-")
        self.manualCtrlBut = QPushButton("Start Long Process")
        
        # Place widgets in a layout
        self.layout.addWidget(self.statusLabel1, 0, 0)
        self.layout.addWidget(self.statusLabel2, 1, 0)
        self.layout.addWidget(self.statusLabel3, 2, 0)
        self.layout.addWidget(self.statusLine1, 0, 1)
        self.layout.addWidget(self.statusLine2, 1, 1)
        self.layout.addWidget(self.statusLine3, 2, 1)
        self.layout.addWidget(self.manualCtrlBut, 3, 0, 1, 2)

        # Add layout to window
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

        # Create dedicated equipment thread
        self.eqpt_thread = QThread()
        self.eqpt_controller = Eqpt_Controller()
        self.eqpt_controller.moveToThread(self.eqpt_thread)
        # Initialize Eqpt once new thread opens
        self.eqpt_thread.started.connect(self.eqpt_controller.connect_to_hardware)
        # Connect signals/slots
        self.eqpt_controller.eqpt_status_signal.connect(self.display_eqpt_status)
        self.manualCtrlBut.clicked.connect(self.eqpt_controller.long_process, Qt.QueuedConnection)
        # Start new thread
        self.eqpt_thread.start()
        
        # Create a timer and start it once hardware is initialized
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.display_time)
        self.timer.timeout.connect(self.eqpt_controller.get_eqpt_status, Qt.QueuedConnection)
        self.eqpt_controller.eqpt_connection_finished.connect(lambda: self.timer.start(1000))

        self.show()

    @pyqtSlot("PyQt_PyObject")
    def display_eqpt_status(self, eqpt_status):
        self.statusLine1.setText(str(eqpt_status['short setpoint']))
        self.statusLine2.setText(str(eqpt_status['long setpoint']))

    def display_time(self):
        # Get the current time in seconds since the epoch
        current_time_seconds = time.time()

        # Convert the time to a struct_time
        time_struct = time.localtime(current_time_seconds)

        # Format the time as HH:MM:SS AM/PM
        formatted_time = time.strftime("%I:%M:%S %p", time_struct)

        # Print formatted time to GUI
        self.statusLine3.setText(formatted_time)


class Eqpt_Controller(QObject):
    eqpt_status_signal = pyqtSignal(dict)
    eqpt_connection_finished = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__()
        print('Controller Init on:\t', QThread.currentThread())
       
    @pyqtSlot()
    def connect_to_hardware(self):
        print('run method is being called')
        print('Hardware Connection on:\t', QThread.currentThread())
        
        self.laser = nkt_system.NKT_System() # initilize hardware
        self.eqpt_connection_finished.emit() # notify GUI
        return

    @pyqtSlot()    
    def get_eqpt_status(self):
        print('Eqpt read on:\t', QThread.currentThread())
        
        # Read equipment status (These will print status)
        center = self.laser.central_wavelength
        bandwidth = self.laser.bandwidth
        
        # Compute values and save to dict, emit values to GUI
        short_setpoint = center - bandwidth/2
        long_setpoint = center + bandwidth/2
        eqpt_status = {'short setpoint': short_setpoint,
                       'long setpoint': long_setpoint}
        self.eqpt_status_signal.emit(eqpt_status)
        return
            
    @pyqtSlot()
    def long_process(self):
        
        print('Long process on:\t', QThread.currentThread())
        print('starting long process...')
        # This will print status for debugging
        self.laser.print_output()
        print('finished')
        return
        

if __name__ == "__main__":
    # Main
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

Output of GUI after pressing button:

Loading x64 DLL from: C:\src\repos\nkt_tools\nkt_tools\NKTPDLL\x64\NKTPDLL.dll
Main window on:  <PyQt5.QtCore.QThread object at 0x000002355C460820>
Controller Init on:      <PyQt5.QtCore.QThread object at 0x000002355C460E50>
run method is being called
Hardware Connection on:  <PyQt5.QtCore.QThread object at 0x000002355C460D30>
Searching for connected NKT Laser...
NKT Extreme/Fianium Found:
Comport:  COM8 Device type:  0x60 at address: 15
System Type =  SuperK Extreme
Inlet Temperature = 22.9 C
Searching for connected NKT Varia...
NKT Varia Found:
Comport:  COM8 Device type:  0x68 at address: 17
Last laser calibration was:
2023-12-20
Eqpt read on:    <PyQt5.QtCore.QThread object at 0x000002355C460D30>
going into central wavelength    is_busy =  False
leaving central wavelength       is_busy =  False
going into bandwidth     is_busy =  False
leaving bandwidth        is_busy =  False
Long process on:         <PyQt5.QtCore.QThread object at 0x000002355C460D30>
starting long process...
going into print output  is_busy =  False
Eqpt read on:    <PyQt5.QtCore.QThread object at 0x000002355C460D30>
going into central wavelength    is_busy =  True

Notice how the last "going into central wavelength" line prints BEFORE the expected "finish" line from "long_process". This is causing my crashes

0

There are 0 best solutions below