Reputation: 204468
I'm learning python and asyncio and after having success with asyncio for a TCP client/server I took my first stab at creating a serial client/server using pyserial-asyncio running in bash on a Raspberry Pi 5 using Python 3.8 (I cannot change version).
Here is the server:
import asyncio
import serial_asyncio
class UARTProtocol(asyncio.Protocol):
def __init__(self):
self.transport = None
def connection_made(self, transport):
self.transport = transport
print('Port opened', transport)
def data_received(self, data):
print('Data received:', data.decode())
# Echo received data back (example)
self.transport.write(data)
# Close the connection if 'exit' is received
if data == b"exit\r":
self.transport.close()
def connection_lost(self, exc):
print('Port closed')
self.transport = None
def pause_writing(self):
print('pause writing')
print(self.transport.get_write_buffer_size())
def resume_writing(self):
print(self.transport.get_write_buffer_size())
print('resume writing')
async def run_uart_server():
loop = asyncio.get_running_loop()
try:
transport, protocol = await serial_asyncio.create_serial_connection(loop, UARTProtocol, '/dev/ttyAMA2', baudrate=9600)
print("UART server started.")
await asyncio.Future() # Run forever
except serial.serialutil.SerialException as e:
print(f"Error: Could not open serial port: {e}")
finally:
if transport:
transport.close()
if __name__ == "__main__":
asyncio.run(run_uart_server())
and the client:
import asyncio
import serial_asyncio
async def uart_client(port, baudrate):
try:
reader, writer = await serial_asyncio.open_serial_connection(url=port, baudrate=baudrate)
print(f"Connected to {port} at {baudrate} bps")
async def receive_data():
while True:
try:
data = await reader.readline()
if data:
print(f"Received: {data.decode().strip()}")
except Exception as e:
print(f"Error reading data: {e}")
break
async def send_data():
while True:
message = input("Enter message to send (or 'exit' to quit): ")
if message.lower() == 'exit':
break
writer.write((message + '\n').encode())
# writer.write_eof()
await writer.drain()
print(f"Sent: {message}")
await asyncio.gather(receive_data(), send_data())
except serial.SerialException as e:
print(f"Error opening serial port: {e}")
finally:
if 'writer' in locals():
writer.close()
await writer.wait_closed()
print("Connection closed.")
if __name__ == "__main__":
asyncio.run(uart_client('/dev/ttyAMA1', 9600))
I want the client to prompt me for some text which is immediately sent to the server and printed there. I can get the client to prompt me for text, but the server doesn't display any of it until after I type exit
in the client to close the connection and then it prints all of the text I typed in the client loop.
Among many other things, I've tried adding writer.write_eof()
in the client (see commented out line in the client code below) and that succeeds in the server immediately displaying the preceding text from the client but then the client never prompts me for input again.
If I run the server and just do echo foo > /dev/ttyAMA1
from bash the server prints foo
immediately so I suspect the client is the problem.
What am I doing wrong?
Upvotes: 1
Views: 33
Reputation: 10969
The problem here is that input
is a blocking call. We all know that input
doesn't return until the user types some text and hits the enter key. We also know that it's not an async function, therefore it doesn't use the event loop. So it can't run other Tasks while it's waiting for the user to type something. All the asyncio Tasks are frozen until the user does something.
All functions that aren't async have this same quality. Most functions do not wait for the user, and most of them return fairly quickly. It's not normally a big deal. But to use input
in an asyncio program requires a little work.
The solution is to issue the call to input
in another thread. That thread will be blocked, but the main thread will keep going. All the other async Tasks will keep running, except for the one that's waiting for the user. The convenient way to do this is with the loop.run_in_executor()
, which was available before Python3.8.
Here is a little script to demonstrate its use. I tested this with 3.13 but I avoided using any feature introduced after 3.8. Or at least I hope so.
import sys
import asyncio
async def ticks():
while True:
await asyncio.sleep(1.0)
sys.stdout.write(".")
sys.stdout.flush()
async def ask():
def inp():
return input("Command?")
while True:
loop = asyncio.get_event_loop()
x = await loop.run_in_executor(None, inp)
print(x)
if x == 'stop':
break
async def main():
tix = asyncio.create_task(ticks())
await ask()
tix.cancel()
if __name__ == "__main__":
asyncio.run(main())
Upvotes: 1