Search code examples
pyqtportpyserialqmainwindowpyqt6

Using USB port data in PyQt


The aim of my code is to create a window with labels, each representing a sensor. The data comes from the USB port in a table of 0s&1s and depending on the value it colours the labels accordingly.
The goal is supposed to look like this:

this

I am unsure of how to pass the data from the port to the function in real time without recreating the window as a whole each time, as I only want it to change the drawn labels (and their colours). Therefore, I was wondering if anyone could point me in the right direction or give me a suggestion of what I can do to make this work.

The code creating my main window & labels:

class MainWindow (qt.QMainWindow):
    def __init__(self):
        super().__init__()
        self.count = 0
        self.j = 0
        self.i = 0
        self.screen()
        self.making()

    def screen(self):
        self.setWindowTitle("Bee Counter")
        self.showMaximized()

    def making(self):
        for i in values: #Iterates over the list of data which comes from the port.
            if (i == 1):
                self.label = qt.QLabel(self)
                self.label.setStyleSheet("background-color: green; border: 1px solid black;")
                self.move_label() #Creates multiple labels with the colour green.
            else: 
                self.label = qt.QLabel(self)
                self.label.setStyleSheet("background-color: red; border: 1px solid black;")
                self.move_label() #Creates multiple labels with the colour red.
            
            self.count +=1
            

    def move_label(self):
        self.label.resize(A, A)
        self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        if self.count%2==0:
            k=20
            self.label.move(X0+self.j,k)
            self.j=self.j+X_STEP
            self.label.setText(f"{self.count}")

        else:
            k=90
            self.label.move(X0+self.i,k)
            self.label.setText(f"{self.count}")
            self.i=self.i+X_STEP
        self.label.show()

if __name__ == '__main__':
    app = qt.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    app.exec()   

The code getting the data from the port:

    ser = serial.Serial(
        port='COM3',\
        baudrate=115200,\
        parity=serial.PARITY_NONE,\
        stopbits=serial.STOPBITS_ONE,\
        bytesize=serial.EIGHTBITS)

    print("<Succesfully connected to: " + ser.portstr)

    while 1: 
        if ser.inWaiting()>0:
            line = ser.readline()
            line = line.decode('utf-8')
            line = [char for char in line if char=="1" or char=="0"] #Gets data in a form of a table of 0s & 1s.
            print(line)
            time.sleep(0.01) 

    ser.close()

P.S. Excuse my perhaps very obvious question, I simply can not wrap my head around it :)


Solution

  • The concept is based on the wrong premise: making should only create the labels (and keep a reference to them), while another function should be responsible for their update.

    Since the data rate is quite fast and the display object very simple, it's probably better to use a custom widget instead of continuously set the style sheet.

    class DisplayWidget(QWidget):
        state = False
        def __init__(self, index):
            super().__init__()
            self.index = str(index)
            self.setFixedSize(32, 32)
    
        def setState(self, state):
            if self.state != state:
                self.state = state
                self.update()
    
        def paintEvent(self, event):
            qp = QPainter(self)
            qp.setBrush(Qt.green if self.state else Qt.red)
            qp.drawRect(self.rect().adjusted(0, 0, -1, -1))
            qp.drawText(self.rect(), Qt.AlignCenter, self.index)
    

    Now, the "viewer" is a custom widget that is able to create a predefined grid of display widgets and updates them when necessary.

    You can provide a default field count, or just ignore that, since the function that updates the data is also capable to update the grid whenever the field count doesn't match.

    class SerialViewer(QWidget):
        def __init__(self, fieldCount=None):
            super().__init__()
            layout = QGridLayout(self)
            layout.setAlignment(Qt.AlignCenter)
            self.widgets = []
    
            if isinstance(fieldCount, int) and fieldCount > 0:
                self.createGrid(fieldCount)
    
        def createGrid(self, fieldCount, rows=2):
            while self.widgets:
                self.widgets.pop(0).deleteLater()
            rows = max(1, rows)
            count = 0
            columns, rest = divmod(fieldCount, rows)
            if rest:
                columns += 1
            for column in range(columns):
                for row in range(rows):
                    widget = DisplayWidget(count)
                    self.layout().addWidget(widget, row, column)
                    self.widgets.append(widget)
                    count += 1
                    if count == fieldCount:
                        break
    
        def updateData(self, data):
            if len(data) != len(self.widgets):
                self.createGrid(len(data))
            for widget, state in zip(self.widgets, data):
                widget.setState(state)
    

    As you can see, instead of using resize() or move(), I'm using a layout manager that is automatically able to place (and eventually resize) the widgets. Remember, fixed geometries are almost always discouraged. Also note that widgets should not be directly added to a QMainWindow, but set for its central widget.

    The thread is implemented in a QThread subclass, using a custom signal that is emitted whenever new data is available:

    class SerialThread(QThread):
        dataReceived = pyqtSignal(object)
        def run(self):
            ser = serial.Serial(
                port='COM3', 
                baudrate=115200, 
                parity=serial.PARITY_NONE, 
                stopbits=serial.STOPBITS_ONE, 
                bytesize=serial.EIGHTBITS)
    
            self.keepRunning = True
            while self.keepRunning:
                if ser.inWaiting() > 0:
                    line = ser.readline()
                    line = line.decode('utf-8')
                    self.dataReceived.emit(
                        list(int(char) for char in line if char in '01')
                    )
    
                # note that the indentation level of sleep() is *outside*
                # of the "if" otherwise it may temporarily block the loop
                # in case there is no data available
                time.sleep(0.01) 
    
        def stop(self):
            self.keepRunning = False
            self.wait()
    

    Finally, the main window, from which we can start or stop the serial communication:

    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            central = QWidget()
            self.setCentralWidget(central)
    
            self.startButton = QPushButton('Start')
            self.stopButton = QPushButton('Stop', enabled=False)
    
            self.serialViewer = SerialViewer(32)
    
            layout = QGridLayout(central)
            layout.addWidget(self.startButton)
            layout.addWidget(self.stopButton, 0, 1)
            layout.addWidget(self.serialViewer, 1, 0, 1, 2)
    
            self.serialThread = SerialThread()
    
            self.startButton.clicked.connect(self.start)
            self.stopButton.clicked.connect(self.serialThread.stop)
            self.serialThread.dataReceived.connect(
                self.serialViewer.updateData)
            self.serialThread.finished.connect(self.stopped)
    
        def start(self):
            self.startButton.setEnabled(False)
            self.stopButton.setEnabled(True)
            self.serialThread.start()
    
        def stopped(self):
            self.startButton.setEnabled(True)
            self.stopButton.setEnabled(False)
    

    Notes:

    • Qt already provides an asynchronous class for serial communication that already supports signals, QSerialPort;
    • the above codes use the old enum syntax, for Qt6 you need the full enum names (Qt.GlobalColor.red, Qt.AlignmentFlag.AlignCenter, etc);
    • you will probably need a further check for the serial connection before starting the while loop in run();