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?
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.
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.
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?
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.
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.