Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ native asynchronous APIs for all platforms.
pip install serialx
```

For drop-in import compatibility (`serial`, `serial_asyncio`, `serial_asyncio_fast`), install:
For drop-in import compatibility (`serial`, `serial_asyncio`, `serial_asyncio_fast`) in
environments where existing code cannot be migrated:
```console
pip install serialx-compat
```
Expand All @@ -31,41 +32,64 @@ with serialx.serial_for_url("/dev/serial/by-id/port", baudrate=115200) as serial
assert pins.dtr is serialx.PinState.HIGH
```

A high-level asynchronous serial `(reader, writer)` pair:
An async equivalent of the synchronous API:

```Python
import asyncio
import contextlib

import serialx

async def main():
reader, writer = await serialx.open_serial_connection("/dev/serial/by-id/port", baudrate=115200)
async with serialx.async_serial_for_url(
"/dev/serial/by-id/port", baudrate=115200,
) as serial:
data = await serial.readexactly(5)
serial.write(b"test")
await serial.flush()

await serial.set_modem_pins(rts=True, dtr=True)
pins = await serial.get_modem_pins()
assert pins.rts is serialx.PinState.HIGH
```

A `(StreamReader, StreamWriter)` pair is also available for code already wired up to
the asyncio streams API:

```Python
import asyncio
import serialx

with contextlib.closing(writer):
data = await reader.readexactly(5)
writer.write(b"test")
await writer.drain()
async def main():
reader, writer = await serialx.open_serial_connection(
"/dev/serial/by-id/port", baudrate=115200,
)

try:
data = await reader.readexactly(5)
writer.write(b"test")
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
```

And a low-level asynchronous serial transport:
And a low-level asynchronous serial transport for protocol-style consumers:

```Python
import asyncio
import serialx

async def main():
loop = asyncio.get_running_loop()
protocol = YourProtocol()
loop = asyncio.get_running_loop()
protocol = YourProtocol()

transport, protocol = await serialx.create_serial_connection(
loop,
lambda: protocol,
url="/dev/serial/by-id/port",
baudrate=115200
)
transport, protocol = await serialx.create_serial_connection(
loop,
lambda: protocol,
url="/dev/serial/by-id/port",
baudrate=115200,
)

await transport.set_modem_pins(rts=True, dtr=True)
await transport.set_modem_pins(rts=True, dtr=True)
```

## ESPHome serial proxy
Expand Down
4 changes: 0 additions & 4 deletions docs/api/platforms/posix.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,3 @@
:members:
:member-order: bysource
```

```{eval-rst}
.. autofunction:: serialx.platforms.serial_posix.posix_list_serial_ports
```
99 changes: 99 additions & 0 deletions docs/how-to/async-serial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Async serial
Python's asyncio module introduces many similar async primitives. serialx provides APIs
for both high-level and low-level async code.

## Async-with
`async with` is the simplest pattern and is suited for scripts and self-contained code:

```python
import serialx

async with serialx.async_serial_for_url(
"/dev/serial/by-id/port", baudrate=115200,
) as serial:
serial.write(b"ping")
data = await serial.readexactly(4)
Comment thread
puddly marked this conversation as resolved.
```

Unlike the sync API, you have all of the asyncio primitives at your disposal, including
granular task cancellation, timeouts, and concurrency:

```python
import asyncio

async with serialx.async_serial_for_url(
"/dev/serial/by-id/port", baudrate=115200,
) as serial:
async with asyncio.TaskGroup() as tg:
async def ping() -> None:
while True:
serial.write(b"ping")
await serial.flush()
await asyncio.sleep(1)

tg.create_task(ping())

async with asyncio.timeout(30):
data = await serial.readexactly(4)
Comment thread
puddly marked this conversation as resolved.
Comment thread
puddly marked this conversation as resolved.
```

### Manual open and close
The instance returned by `async_serial_for_url` is unopened. Open and close
explicitly when you need to keep the connection alive across function boundaries:

```python
serial = serialx.async_serial_for_url(
"/dev/serial/by-id/port", baudrate=115200,
)

await serial.open()

try:
...
finally:
await serial.close()
```

### Reading and writing
Reads are coroutines, writes are synchronous (data is buffered and drained on
demand):

```python
data = await serial.read(64) # up to 64 bytes
chunk = await serial.readexactly(32) # exactly 32 bytes
line = await serial.readline() # through the next \n
header = await serial.readuntil(b"\r\n") # through a custom delimiter

serial.write(b"hello ")
serial.write(b"world\n")
await serial.flush() # wait until the data has been written
```

### Modem pins
Modem control pins are async, since some transports (ESPHome, RFC2217) round-trip
to the device:

```python
await serial.set_modem_pins(rts=True, dtr=True)
pins = await serial.get_modem_pins()
assert pins.rts is serialx.PinState.HIGH
```

## Async protocols and transports
While the high-level async API is useful for simple code, libraries and other
high-performance uses should use asyncio transports and protocols. These have the
benefit of allowing an `asyncio.Protocol` to immediately enqueue data in the same event
loop cycle as it is received.

```python
import asyncio
import serialx

loop = asyncio.get_running_loop()
transport, protocol = await serialx.create_serial_connection(
loop=loop,
protocol_factory=your_protocol_factory,
url="/dev/serial/by-id/port",
baudrate=115200,
)
```
12 changes: 12 additions & 0 deletions docs/how-to/pyserial-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,15 @@ if pins.cts is serialx.PinState.HIGH:
:::

`set_modem_pins` accepts individual pin kwargs or a full `ModemPins` dataclass. Pins omitted from the call are left unchanged. `get_modem_pins` returns a `ModemPins` dataclass of `PinState` enum values, call `.to_bool()` on a pin for a `bool | None`.

### Simplified async API
If you have existing sync code using `serial_for_url` and want to make it async, use `async_serial_for_url`. The method names match the sync API (e.g. `read`, `readexactly`, `readline`, `readuntil`, `write`, `flush`) so the migration is mostly adding `async`/`await`:

```diff
-with serialx.serial_for_url("/dev/ttyUSB0", baudrate=115200) as serial:
- serial.write(b"ping")
- data = serial.readexactly(4)
+async with serialx.async_serial_for_url("/dev/ttyUSB0", baudrate=115200) as serial:
+ serial.write(b"ping")
+ data = await serial.readexactly(4)
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ api
:caption: How-to
:hidden:

how-to/async-serial
how-to/esphome
how-to/pyodide
how-to/pyserial-migration
Expand Down
53 changes: 47 additions & 6 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Quickstart

Install `serialx`:

```bash
Expand All @@ -16,15 +15,57 @@ with serialx.serial_for_url("/dev/serial/by-id/port", baudrate=115200) as serial
data = serial.readexactly(4)
```

Open a serial connection asynchronously:
Open a serial port asynchronously:

```python
import serialx

reader, writer = await serialx.open_serial_connection(
"/dev/serial/by-id/port",
baudrate=115200,
)
async with serialx.async_serial_for_url(
"/dev/serial/by-id/port", baudrate=115200,
) as serial:
serial.write(b"ping")
data = await serial.readexactly(4)
Comment thread
puddly marked this conversation as resolved.
```

A `(StreamReader, StreamWriter)` pair is also available for code already wired up to
the asyncio streams API:

```Python
import asyncio
import serialx

async def main():
reader, writer = await serialx.open_serial_connection(
"/dev/serial/by-id/port", baudrate=115200,
)

try:
data = await reader.readexactly(5)
writer.write(b"test")
await writer.drain()
finally:
writer.close()
await writer.wait_closed()
```

And a low-level asynchronous serial transport for protocol-style consumers:

```Python
import asyncio
import serialx

async def main():
loop = asyncio.get_running_loop()
protocol = YourProtocol()

transport, protocol = await serialx.create_serial_connection(
loop,
lambda: protocol,
url="/dev/serial/by-id/port",
baudrate=115200,
)

await transport.set_modem_pins(rts=True, dtr=True)
```

For optional transports (ESPHome, RFC2217, socket) and compatibility notes, see
Expand Down
25 changes: 25 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,30 @@ with serialx.serial_for_url("/dev/serial/by-id/port", baudrate=115200) as serial
serial.write(b"test")
```

## Async
There are quite a few approaches in async Python. serialx supports all of the popular
asyncio primitives in addition to a simple async API. It's recommended to use the async
API over the sync API in general, as the async API allows for granular timeouts,
task cancellation, and concurrency.

### Async (simple API)
For a simple translation of sync code into async code, you can use `serialx.async_serial_for_url`:
```python
import serialx

async with serialx.async_serial_for_url("/dev/serial/by-id/port", baudrate=115200) as serial:
data = await serial.readexactly(5)
serial.write(b"test")
await serial.flush()
```
Comment thread
puddly marked this conversation as resolved.

All functions, including `open` and `close`, are async and work exactly as they do with
the sync API.

## Async (`StreamReader` and `StreamWriter`)
A `(StreamReader, StreamWriter)` pair is available for code already wired up to
the asyncio streams API:

```python
import serialx

Expand All @@ -29,6 +52,8 @@ finally:
```

## Async (transport)
For protocol-style consumers that want raw `asyncio.Protocol` callbacks:

```python
import asyncio
import serialx
Expand Down
4 changes: 4 additions & 0 deletions serialx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""serialx serial port implementation."""

from .async_serial import (
AsyncSerial,
SerialStreamWriter,
async_serial_for_url,
create_serial_connection,
open_serial_connection,
)
Expand Down Expand Up @@ -42,6 +44,8 @@
from .platforms import Serial, SerialTransport

__all__ = [
"AsyncSerial",
"async_serial_for_url",
"create_serial_connection",
"get_serial_classes",
"list_serial_ports",
Expand Down
Loading
Loading