jmri_ncemonitor.transport.serial_port

Real RS-485 adapter via pyserial-asyncio.

Two paths for the 9-bit address marker:

  1. 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; open with no extra kwargs works.

  2. 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" or parity="S" and a matching bytesize / stopbits config, 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 set mark_parity_is_address=True so the SerialPort knows 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
DEFAULT_BAUD = 9600
class SerialPort(jmri_ncemonitor.transport.port.BytePort):
 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.

SerialPort( reader: asyncio.streams.StreamReader, writer: asyncio.streams.StreamWriter)
40    def __init__(self, reader: StreamReader, writer: StreamWriter) -> None:
41        self._reader = reader
42        self._writer = writer
43        self._open = True
@classmethod
async def open( cls, port: str, baud: int = 9600, *, parity: str = 'N', stopbits: float = 1.0, bytesize: int = 8) -> SerialPort:
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.

async def write(self, data: bytes) -> None:
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        )
async def read_byte(self) -> int:
86    async def read_byte(self) -> int:
87        data = await self._reader.readexactly(1)
88        return data[0]
async def aclose(self) -> None:
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()
is_open: bool
 98    @property
 99    def is_open(self) -> bool:
100        return self._open