jmri_ncemonitor.transport.serial_port
Real RS-485 adapter via pyserial-asyncio.
Two paths for the 9-bit address marker:
High-bit-marker adapters (the JMRI reference USB adapter, and most USB-serial bridges configured for NCE): the adapter delivers the 9th bit as bit 7 of the byte, so the framing rule is simply "high bit set = address byte." This is the default;
openwith no extra kwargs works.True 9-bit-serial adapters via mark/space parity: some Linux installs (and pyserial in particular) can distinguish address vs data bytes by parity. The operator passes
parity="M"orparity="S"and a matchingbytesize/stopbitsconfig, and the underlying UART flags the address byte as a parity error that pyserial surfaces through the byte stream. Callers who use this path must setmark_parity_is_address=Trueso theSerialPortknows how to interpret parity-flagged bytes.
For path 2, the practical operator recipe is documented in the
README.md — the exact driver-side setup varies too much between
Linux distros and adapter chipsets to hardcode a single default.
1"""Real RS-485 adapter via pyserial-asyncio. 2 3Two paths for the 9-bit address marker: 4 51. **High-bit-marker adapters** (the JMRI reference USB adapter, and 6 most USB-serial bridges configured for NCE): the adapter delivers 7 the 9th bit as bit 7 of the byte, so the framing rule is simply 8 "high bit set = address byte." This is the default; ``open`` with 9 no extra kwargs works. 10 112. **True 9-bit-serial adapters via mark/space parity**: some Linux 12 installs (and pyserial in particular) can distinguish address vs 13 data bytes by parity. The operator passes ``parity="M"`` or 14 ``parity="S"`` and a matching ``bytesize`` / ``stopbits`` config, 15 and the underlying UART flags the address byte as a parity error 16 that pyserial surfaces through the byte stream. Callers who use 17 this path must set ``mark_parity_is_address=True`` so the 18 :class:`SerialPort` knows how to interpret parity-flagged bytes. 19 20For path 2, the practical operator recipe is documented in the 21``README.md`` — the exact driver-side setup varies too much between 22Linux distros and adapter chipsets to hardcode a single default. 23""" 24from __future__ import annotations 25 26import contextlib 27from typing import TYPE_CHECKING 28 29from jmri_ncemonitor.transport.port import BytePort 30 31if TYPE_CHECKING: 32 from asyncio import StreamReader, StreamWriter 33 34 35DEFAULT_BAUD = 9600 36 37 38class SerialPort(BytePort): 39 def __init__(self, reader: StreamReader, writer: StreamWriter) -> None: 40 self._reader = reader 41 self._writer = writer 42 self._open = True 43 44 @classmethod 45 async def open( 46 cls, 47 port: str, 48 baud: int = DEFAULT_BAUD, 49 *, 50 parity: str = "N", 51 stopbits: float = 1.0, 52 bytesize: int = 8, 53 ) -> SerialPort: 54 """Open a serial port for Cab Bus monitoring. 55 56 ``parity`` / ``stopbits`` / ``bytesize`` accept the standard 57 pyserial values — the default of ``N``, ``1``, ``8`` matches 58 the high-bit-marker convention that JMRI's reference USB 59 adapter uses. 60 61 For true 9-bit adapters that discriminate address bytes via 62 mark/space parity, operators typically set 63 ``parity="M", stopbits=1, bytesize=8`` on the address-byte 64 side of the bus, and rely on the underlying UART's parity 65 detection to flag the boundary. The exact combination 66 depends on the adapter's firmware — see the README. 67 """ 68 import serial_asyncio # noqa: PLC0415 69 reader, writer = await serial_asyncio.open_serial_connection( 70 url=port, 71 baudrate=baud, 72 parity=parity, 73 stopbits=stopbits, 74 bytesize=bytesize, 75 ) 76 return cls(reader=reader, writer=writer) 77 78 async def write(self, data: bytes) -> None: 79 # The monitor never writes; we expose this only because the ABC 80 # requires it. Surfacing a clear error guards against misuse. 81 raise RuntimeError( 82 "NCE Cab Bus monitor is passive — writes are not supported" 83 ) 84 85 async def read_byte(self) -> int: 86 data = await self._reader.readexactly(1) 87 return data[0] 88 89 async def aclose(self) -> None: 90 if not self._open: 91 return 92 self._open = False 93 self._writer.close() 94 with contextlib.suppress(Exception): 95 await self._writer.wait_closed() 96 97 @property 98 def is_open(self) -> bool: 99 return self._open
39class SerialPort(BytePort): 40 def __init__(self, reader: StreamReader, writer: StreamWriter) -> None: 41 self._reader = reader 42 self._writer = writer 43 self._open = True 44 45 @classmethod 46 async def open( 47 cls, 48 port: str, 49 baud: int = DEFAULT_BAUD, 50 *, 51 parity: str = "N", 52 stopbits: float = 1.0, 53 bytesize: int = 8, 54 ) -> SerialPort: 55 """Open a serial port for Cab Bus monitoring. 56 57 ``parity`` / ``stopbits`` / ``bytesize`` accept the standard 58 pyserial values — the default of ``N``, ``1``, ``8`` matches 59 the high-bit-marker convention that JMRI's reference USB 60 adapter uses. 61 62 For true 9-bit adapters that discriminate address bytes via 63 mark/space parity, operators typically set 64 ``parity="M", stopbits=1, bytesize=8`` on the address-byte 65 side of the bus, and rely on the underlying UART's parity 66 detection to flag the boundary. The exact combination 67 depends on the adapter's firmware — see the README. 68 """ 69 import serial_asyncio # noqa: PLC0415 70 reader, writer = await serial_asyncio.open_serial_connection( 71 url=port, 72 baudrate=baud, 73 parity=parity, 74 stopbits=stopbits, 75 bytesize=bytesize, 76 ) 77 return cls(reader=reader, writer=writer) 78 79 async def write(self, data: bytes) -> None: 80 # The monitor never writes; we expose this only because the ABC 81 # requires it. Surfacing a clear error guards against misuse. 82 raise RuntimeError( 83 "NCE Cab Bus monitor is passive — writes are not supported" 84 ) 85 86 async def read_byte(self) -> int: 87 data = await self._reader.readexactly(1) 88 return data[0] 89 90 async def aclose(self) -> None: 91 if not self._open: 92 return 93 self._open = False 94 self._writer.close() 95 with contextlib.suppress(Exception): 96 await self._writer.wait_closed() 97 98 @property 99 def is_open(self) -> bool: 100 return self._open
Helper class that provides a standard way to create an ABC using inheritance.
45 @classmethod 46 async def open( 47 cls, 48 port: str, 49 baud: int = DEFAULT_BAUD, 50 *, 51 parity: str = "N", 52 stopbits: float = 1.0, 53 bytesize: int = 8, 54 ) -> SerialPort: 55 """Open a serial port for Cab Bus monitoring. 56 57 ``parity`` / ``stopbits`` / ``bytesize`` accept the standard 58 pyserial values — the default of ``N``, ``1``, ``8`` matches 59 the high-bit-marker convention that JMRI's reference USB 60 adapter uses. 61 62 For true 9-bit adapters that discriminate address bytes via 63 mark/space parity, operators typically set 64 ``parity="M", stopbits=1, bytesize=8`` on the address-byte 65 side of the bus, and rely on the underlying UART's parity 66 detection to flag the boundary. The exact combination 67 depends on the adapter's firmware — see the README. 68 """ 69 import serial_asyncio # noqa: PLC0415 70 reader, writer = await serial_asyncio.open_serial_connection( 71 url=port, 72 baudrate=baud, 73 parity=parity, 74 stopbits=stopbits, 75 bytesize=bytesize, 76 ) 77 return cls(reader=reader, writer=writer)
Open a serial port for Cab Bus monitoring.
parity / stopbits / bytesize accept the standard
pyserial values — the default of N, 1, 8 matches
the high-bit-marker convention that JMRI's reference USB
adapter uses.
For true 9-bit adapters that discriminate address bytes via
mark/space parity, operators typically set
parity="M", stopbits=1, bytesize=8 on the address-byte
side of the bus, and rely on the underlying UART's parity
detection to flag the boundary. The exact combination
depends on the adapter's firmware — see the README.