PyQt readyRead: set text from serial to multiple labels

Question:

In PyQt5, I want to read my serial port after writing (requesting a value) to it. I’ve got it working using readyRead.connect(self.readingReady), but then I’m limited to outputting to only one text field.

The code for requesting parameters sends a string to the serial port. After that, I’m reading the serial port using the readingReady function and printing the result to a plainTextEdit form.

def read_configuration(self):
    if self.serial.isOpen():
        self.serial.write(f"?request1n".encode())
        self.label_massGainOutput.setText(f"{self.serial.readAll().data().decode()}"[:-2])
        self.serial.write(f"?request2n".encode())
        self.serial.readyRead.connect(self.readingReady)
        self.serial.write(f"?request3n".encode())
        self.serial.readyRead.connect(self.readingReady)

def readingReady(self):
    data = self.serial.readAll()
    if len(data) > 0:
        self.plainTextEdit_commandOutput.appendPlainText(f"{data.data().decode()}"[:-2])
    else: self.serial.flush()

The problem I have, is that I want every answer from the serial port to go to a different plainTextEdit form. The only solution I see now is to write a separate readingReady function for every request (and I have a lot! Only three are shown now). This must be possible in a better way. Maybe using arguments in the readingReady function? Or returning a value from the function that I can redirect to the correct form?

Without using the readyRead signal, all my values are one behind. So the first request prints nothing, the second prints the first etc. and the last is not printed out.

Does someone have a better way to implement this functionality?

Asked By: Radiofreak1041

||

Answers:

QSerialPort has asyncronous (readyRead) and syncronous API (waitForReadyRead), if you only read configuration once on start and ui freezing during this process is not critical to you, you can use syncronous API.

serial.write(f"?request1n".encode())
serial.waitForReadyRead()
res = serial.read(10)
serial.write(f"?request2n".encode())
serial.waitForReadyRead()
res = serial.read(10)

This simplification assumes that responces comes in one chunk and message size is below or equal 10 bytes which is not guaranteed. Actual code should be something like this:

def isCompleteMessage(res):
    # your code here

serial.write(f"?request2n".encode())
res = b''
while not isCompleteMessage(res):
    serial.waitForReadyRead()
    res += serial.read(10)

Alternatively you can create worker or thread, open port and query requests in it syncronously and deliver responces to application using signals – no freezes, clear code, slightly more complicated system.

Answered By: mugiseyebrows

If reqests are independent of each other you can use request queue, provide callback for each request and switch this callbacks inside readyRead slot.

class RequestQueue:
    def __init__(self, serial):
        self._queue = []
        self._started = False
        self._handler = self._defaultHandler
        self._serial = serial
        serial.readyRead.connect(self._onReadyRead)
        self._message = b''
        
    def _isCompleteMessage(self, data):
        return True

    def _onReadyRead(self):
        data = self._serial.readAll()
        self._message += data.data()
        if not self._isCompleteMessage(self._message):
            return
        self._handler(self._message)
        self._message = b''
        self._handler = self._defaultHandler
        self._sendNextRequest()

    def _defaultHandler(self, data):
        pass

    def _sendNextRequest(self):
        if len(self._queue) < 1:
            self._started = False
            self._handler = self._defaultHandler
            return
        self._started = True
        (data, handler) = self._queue[0]
        self._queue = self._queue[1:]
        self._handler = handler
        self._serial.write(data)

    def enqueue(self, data, handler):
        self._queue.append((data, handler))
        if not self._started:
            self._sendNextRequest()

if __name__ == "__main__":
    ...
    queue = RequestQueue(serial)
    queue.enqueue(f"?request1n".encode(), lambda data: ui.label1.setText(data.decode()))
    queue.enqueue(f"?request2n".encode(), lambda data: ui.label2.setText(data.decode()))
    queue.enqueue(f"?request3n".encode(), lambda data: ui.label3.setText(data.decode()))

Notice that this queue lives and operates in the same thread, since it uses asyncronous api.

Answered By: mugiseyebrows