RS-485¶
anyserial supports kernel-level RS-485 half-duplex operation on Linux
via the TIOCSRS485 ioctl. When enabled, the tty driver asserts RTS
around each transmitted frame so the transceiver auto-switches between
TX and RX — no user-space RTS toggling, no timing against the UART FIFO
drain, no race with partial writes.
See DESIGN §19 for the full rationale.
Platform support
Kernel RS-485 is Linux-only. macOS has no equivalent
ioctl (DarwinBackend rejects via
UnsupportedPolicy); BSD has FreeBSD-only TIOCSRS485 support in
some drivers but is out of scope.
Manual RTS toggling (see below) is the
portable fallback on both platforms.
Quick start¶
import anyio
from anyserial import RS485Config, SerialConfig, open_serial_port
async def main() -> None:
config = SerialConfig(
baudrate=115_200,
rs485=RS485Config(
enabled=True,
rts_on_send=True,
rts_after_send=False,
delay_before_send=0.001, # seconds
delay_after_send=0.002,
rx_during_tx=False,
),
)
async with await open_serial_port("/dev/ttyUSB0", config) as port:
await port.send(b"\x01\x03\x00\x00\x00\x01\x84\x0a") # Modbus-RTU read
reply = await port.receive(256)
print(reply.hex())
anyio.run(main)
RS485Config¶
| Field | Default | Meaning |
|---|---|---|
enabled |
True |
Master switch — set rs485=None on SerialConfig to skip the ioctl entirely. |
rts_on_send |
True |
Logic level of RTS while transmitting. Most half-duplex transceivers want True. |
rts_after_send |
False |
Level of RTS after transmission completes. Leave False for receive-after-send. |
delay_before_send |
0.0 |
Seconds to hold RTS before transmission starts (float; kernel converts to ms). |
delay_after_send |
0.0 |
Seconds to hold RTS after transmission ends. |
rx_during_tx |
False |
Enable receive while transmitting. Needed only on full-duplex RS-485 wiring. |
Delays round to milliseconds in the kernel struct; a driver may further
round to its hardware granularity. The config delays are seconds
(0.001 = one ms) to match anyio.sleep and the rest of the API.
Runtime enable / disable¶
configure() accepts a new SerialConfig with a different rs485
field. Switching rs485=None after RS-485 was previously enabled
restores the struct serial_rs485 state the kernel reported before
anyserial first wrote to it:
async with await open_serial_port("/dev/ttyUSB0", SerialConfig()) as port:
# Plain UART mode.
await port.send(b"AT\r\n")
await port.configure(SerialConfig(rs485=RS485Config()))
# Half-duplex RS-485 mode from here on.
await port.send(b"\x01\x03...")
await port.configure(SerialConfig())
# Back to plain UART; the kernel RS-485 settings are restored.
close() performs the same restore step so the next process to open
the device inherits the kernel default rather than anyserial's last
write.
Driver support¶
TIOCSRS485 is a kernel ioctl, so whether it works depends on the
tty driver, not on anyserial. Expect:
- Supported: genuine FTDI chips on recent kernels, most industrial
PCIe UART cards (
8250_pci,exar,lpc_sch), some PL-series adapters, many vendor-specific drivers. - Unsupported: most consumer USB-serial dongles (CP210x, CH340,
older FTDI clones). The driver returns
ENOTTYorEINVAL.
The SerialCapabilities.rs485
field is Capability.SUPPORTED on Linux because the platform has the
ioctl. The device answer is only knowable at apply time, which is why
rs485 on SerialConfig routes through
UnsupportedPolicy.
Handling unsupported drivers¶
The unsupported_policy field on SerialConfig controls what happens
when TIOCSRS485 fails:
from anyserial import SerialConfig
from anyserial._types import UnsupportedPolicy
# Default: raise UnsupportedFeatureError.
SerialConfig(rs485=RS485Config(), unsupported_policy=UnsupportedPolicy.RAISE)
# Warn via warnings.warn(RuntimeWarning) and keep going without RS-485.
SerialConfig(rs485=RS485Config(), unsupported_policy=UnsupportedPolicy.WARN)
# Silently continue without RS-485.
SerialConfig(rs485=RS485Config(), unsupported_policy=UnsupportedPolicy.IGNORE)
WARN / IGNORE are useful for drivers whose TIOCSRS485 behaviour
you cannot know at config-build time — the library will fall back to
standard UART mode and your code can toggle RTS manually if the protocol
demands it.
Manual RTS toggling¶
anyserial intentionally does not expose a "fake RS-485 via RTS in
user space" shortcut in the core API. The timing is platform-dependent
and non-equivalent to kernel RS-485: you have to wait for the UART FIFO
to drain (not just the kernel output queue) before flipping RTS back to
receive, or you'll clip the end of the frame.
If you need manual direction control, wire it yourself:
await port.set_control_lines(rts=True)
await port.send_buffer(frame)
await port.drain_exact() # waits for the FIFO, not just the kernel queue
await port.set_control_lines(rts=False)
drain_exact
calls tcdrain in a worker thread — it's the only portable way to wait
for the hardware FIFO. Kernel RS-485 avoids this round-trip entirely,
which is why it's preferred whenever the driver supports it.
Testing support on your device¶
import os
import contextlib
import anyio
from anyserial._linux.rs485 import read_rs485
def probe(path: str) -> bool:
fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
try:
try:
read_rs485(fd)
return True
except OSError:
return False
finally:
with contextlib.suppress(OSError):
os.close(fd)
print(probe("/dev/ttyUSB0"))
The private _linux.rs485 module is stable-in-practice — it wraps
kernel ABI that has not changed since Linux 4.14 — but it is not part
of the public API and may move between minor versions.