user9170
user9170

Reputation: 1000

How to write a Python async serial async reader?

I am porting some C++ code to Python and I am having a heck of a time figuring out how to have an onReceive handler for serial bytes. I am using

import serial_asyncio
class Arduino: #comms is with an Arduino in serial mode)
    async def connect(self, serial_port):
        (self.serial_reader, self.serial_writer) = await 
        serial_asyncio.open_serial_connection(url=serial_port, baudrate=115200)
        print("serial opened:", serial_port)
        self.buffer = b""

    async def recv(self, data):
        self.buffer += data
        self.process_buffer(self.buffer)

if __name__ == '__main__':
     ardunio = Arduino("/dev/ttyS0")         
     loop = asyncio.get_event_loop()
     loop.run_until_complete(ardunio.connect())
     loop.run_forever()

However I cannot figure out how to patch the recv handler in to the read. I Qt, I could:

connect(&QAbstractSocket::readyRead, &Arduino::handleBytes);

In Node:

arduino.on('data', line => console.log(line))

In Python, there doesn't seem to be any obvious answer? How do I get the serial port bytes that arrive to be passed into Arduino.receive(self, data)?

Upvotes: 0

Views: 2887

Answers (1)

user4815162342
user4815162342

Reputation: 155216

However I cannot figure out how to patch the recv handler in to the read.

open_serial_connection is not a callback-based interface, it returns a pair of streams which expose the contents through coroutines. This allows you to communicate with the serial port as if you were writing blocking code, i.e. without the use of callbacks and buffers that build up data. For example (untested):

async def main():
    reader, writer = await serial_asyncio.connect(url="/dev/ttyS0", baudrate=115200)
    # instead of: arduino.on('data', line => console.log(line))
    # ...we can just read some data from the serial port
    data = await reader.read(1024)
    # ...and print it right here
    print(repr(data))

asyncio.run(main())

Coroutines like StreamReader.read will appear to block waiting for data, but they will really just suspend the current coroutine and let the event loop do other things. This allows you to easily express timeouts or do other processing while waiting for the data to arrive from the serial port.

If you still need callbacks, for example because you need to communicate with a C API, you have two options:

  • use the lower-level create_serial_connection function. It accepts a type that inherits asyncio.Protocol where you can define hooks like data_received (as a callback, not a coroutine), which is close to how you modeled your Arduino class.

  • keep using the coroutine API, but use add_done_callback to register a callback to run when the coroutine is ready.

An example of the latter would be:

async def main():
    reader, writer = await serial_asyncio.connect(url="/dev/ttyS0", baudrate=115200)
    # Python equivalent of arduino.on('data', x):
    # 1. call reader.read() but don't await - instead, create a "future"
    # which works like a JavaScript Promise
    future = asyncio.ensure_future(reader.read(1024))
    # register a done callback when the result is available
    future.add_done_callback(future, lambda _: print(repr(future.result())))
    # go do something else - here we wait for an event just so main()
    # doesn't exit immediately and terminate our program
    await asyncio.Event().wait()

asyncio.run(main())

But unless you are communicating with C, I see no advantage in using this style over the normal async/await.

Upvotes: 2

Related Questions