jmri_openlcb.connection

OlcbConnection — SystemConnection subclass for OpenLCB/LCC.

Settings: { "kind": "loopback" | "serial" | "tcp", "source_alias": 0x123, # 12-bit node alias for outbound PCERs # ...transport-specific... }

 1"""``OlcbConnection`` — SystemConnection subclass for OpenLCB/LCC.
 2
 3Settings:
 4    {
 5        "kind": "loopback" | "serial" | "tcp",
 6        "source_alias": 0x123,    # 12-bit node alias for outbound PCERs
 7        # ...transport-specific...
 8    }
 9"""
10from __future__ import annotations
11
12from typing import Any, ClassVar
13
14from jmri_core.hal.connection import ConnectionSettings, SystemConnection
15
16from jmri_openlcb.beans import OlcbSensorManager, OlcbTurnoutManager
17from jmri_openlcb.power import OlcbPowerManager
18from jmri_openlcb.traffic import OlcbTrafficController
19from jmri_openlcb.transport import BytePort, LoopbackPort, SerialPort, TcpPort
20
21
22class OlcbConnection(SystemConnection):
23    name: ClassVar[str] = "OpenLCB"
24    prefix: ClassVar[str] = "X"
25
26    def __init__(self) -> None:
27        super().__init__()
28        self._port: BytePort | None = None
29        self._tc: OlcbTrafficController | None = None
30
31    async def open(self, settings: ConnectionSettings | None = None) -> None:
32        s = settings or {}
33        self._port = await _build_port(s)
34        self._tc = OlcbTrafficController(
35            self._port, source_alias=int(s.get("source_alias", 0x123))
36        )
37        await self._tc.start()
38
39        self.turnouts = OlcbTurnoutManager(self.prefix, tc=self._tc)  # type: ignore[assignment]
40        self.sensors = OlcbSensorManager(self.prefix, tc=self._tc)    # type: ignore[assignment]
41        # Well-known event power manager — produces + consumes the
42        # reserved 01.00.00.00.00.00.FF.FE / .FF event IDs so a
43        # single set_power() call converges every OpenLCB node
44        # listening to the bus. See :mod:`jmri_openlcb.power`.
45        self.power = OlcbPowerManager(self._tc)
46
47        await super().open(settings)
48
49    async def close(self) -> None:
50        # Drop the power manager's listener before we tear down the
51        # traffic controller so the manager doesn't fire against a
52        # closed signal.
53        if isinstance(self.power, OlcbPowerManager):
54            self.power.close()
55        if self._tc is not None:
56            await self._tc.aclose()
57            self._tc = None
58        self._port = None
59        await super().close()
60
61    @property
62    def traffic(self) -> OlcbTrafficController:
63        if self._tc is None:
64            raise RuntimeError("connection not open")
65        return self._tc
66
67
68async def _build_port(settings: dict[str, Any]) -> BytePort:
69    kind = settings.get("kind", "loopback")
70    if kind == "serial":
71        port = settings.get("port")
72        if not port:
73            raise ValueError("serial transport requires 'port'")
74        baud = int(settings.get("baud", 333333))
75        return await SerialPort.open(port=port, baud=baud)
76    if kind == "tcp":
77        host = settings.get("host")
78        tcp_port = settings.get("tcp_port")
79        if not host or tcp_port is None:
80            raise ValueError("tcp transport requires 'host' and 'tcp_port'")
81        return await TcpPort.open(host=host, port=int(tcp_port))
82    if kind == "loopback":
83        lb = settings.get("loopback_port")
84        if not isinstance(lb, LoopbackPort):
85            raise ValueError("loopback transport requires 'loopback_port'")
86        return lb
87    raise ValueError(f"unknown OpenLCB transport kind: {kind!r}")
class OlcbConnection(jmri_core.hal.connection.SystemConnection):
23class OlcbConnection(SystemConnection):
24    name: ClassVar[str] = "OpenLCB"
25    prefix: ClassVar[str] = "X"
26
27    def __init__(self) -> None:
28        super().__init__()
29        self._port: BytePort | None = None
30        self._tc: OlcbTrafficController | None = None
31
32    async def open(self, settings: ConnectionSettings | None = None) -> None:
33        s = settings or {}
34        self._port = await _build_port(s)
35        self._tc = OlcbTrafficController(
36            self._port, source_alias=int(s.get("source_alias", 0x123))
37        )
38        await self._tc.start()
39
40        self.turnouts = OlcbTurnoutManager(self.prefix, tc=self._tc)  # type: ignore[assignment]
41        self.sensors = OlcbSensorManager(self.prefix, tc=self._tc)    # type: ignore[assignment]
42        # Well-known event power manager — produces + consumes the
43        # reserved 01.00.00.00.00.00.FF.FE / .FF event IDs so a
44        # single set_power() call converges every OpenLCB node
45        # listening to the bus. See :mod:`jmri_openlcb.power`.
46        self.power = OlcbPowerManager(self._tc)
47
48        await super().open(settings)
49
50    async def close(self) -> None:
51        # Drop the power manager's listener before we tear down the
52        # traffic controller so the manager doesn't fire against a
53        # closed signal.
54        if isinstance(self.power, OlcbPowerManager):
55            self.power.close()
56        if self._tc is not None:
57            await self._tc.aclose()
58            self._tc = None
59        self._port = None
60        await super().close()
61
62    @property
63    def traffic(self) -> OlcbTrafficController:
64        if self._tc is None:
65            raise RuntimeError("connection not open")
66        return self._tc

One hardware connection to a layout.

Subclasses set name (vendor-friendly label) and prefix (the JMRI system-name prefix). After open() returns, all the bean managers must be populated; closed connections deregister themselves.

name: ClassVar[str] = 'OpenLCB'
prefix: ClassVar[str] = 'X'
async def open(self, settings: dict[str, typing.Any] | None = None) -> None:
32    async def open(self, settings: ConnectionSettings | None = None) -> None:
33        s = settings or {}
34        self._port = await _build_port(s)
35        self._tc = OlcbTrafficController(
36            self._port, source_alias=int(s.get("source_alias", 0x123))
37        )
38        await self._tc.start()
39
40        self.turnouts = OlcbTurnoutManager(self.prefix, tc=self._tc)  # type: ignore[assignment]
41        self.sensors = OlcbSensorManager(self.prefix, tc=self._tc)    # type: ignore[assignment]
42        # Well-known event power manager — produces + consumes the
43        # reserved 01.00.00.00.00.00.FF.FE / .FF event IDs so a
44        # single set_power() call converges every OpenLCB node
45        # listening to the bus. See :mod:`jmri_openlcb.power`.
46        self.power = OlcbPowerManager(self._tc)
47
48        await super().open(settings)

Connect to hardware and start any IO loops.

Concrete drivers acquire serial/TCP ports here, install protocol handlers, and start any required background tasks. The base implementation should be called via await super().open(settings) to mark the connection opened.

async def close(self) -> None:
50    async def close(self) -> None:
51        # Drop the power manager's listener before we tear down the
52        # traffic controller so the manager doesn't fire against a
53        # closed signal.
54        if isinstance(self.power, OlcbPowerManager):
55            self.power.close()
56        if self._tc is not None:
57            await self._tc.aclose()
58            self._tc = None
59        self._port = None
60        await super().close()

Disconnect from hardware. Idempotent. Base impl flips the flag.

traffic: jmri_openlcb.OlcbTrafficController
62    @property
63    def traffic(self) -> OlcbTrafficController:
64        if self._tc is None:
65            raise RuntimeError("connection not open")
66        return self._tc