jmri_core.hal
Hardware Abstraction Layer — connection ABC and in-memory loopback.
Concrete drivers (LocoNet, NCE, DCC-EX, etc.) live in their own packages and
subclass jmri_core.hal.connection.SystemConnection. They register
themselves via the jmri.connection entry point so load_connections()
in jmri_core.plugins can discover them.
1"""Hardware Abstraction Layer — connection ABC and in-memory loopback. 2 3Concrete drivers (LocoNet, NCE, DCC-EX, etc.) live in their own packages and 4subclass :class:`jmri_core.hal.connection.SystemConnection`. They register 5themselves via the ``jmri.connection`` entry point so :func:`load_connections` 6in :mod:`jmri_core.plugins` can discover them. 7""" 8from jmri_core.hal.connection import ConnectionSettings, SystemConnection 9from jmri_core.hal.internal import InternalConnection 10 11__all__ = ["ConnectionSettings", "InternalConnection", "SystemConnection"]
19class InternalConnection(SystemConnection): 20 """Pure in-memory system connection. No I/O. No background tasks.""" 21 22 name: ClassVar[str] = "Internal" 23 prefix: ClassVar[str] = "I" 24 25 async def open(self, settings: ConnectionSettings | None = None) -> None: 26 await super().open(settings) 27 28 async def close(self) -> None: 29 await super().close()
Pure in-memory system connection. No I/O. No background tasks.
25 async def open(self, settings: ConnectionSettings | None = None) -> None: 26 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.
36class SystemConnection(ABC): 37 """One hardware connection to a layout. 38 39 Subclasses set ``name`` (vendor-friendly label) and ``prefix`` (the JMRI 40 system-name prefix). After :meth:`open` returns, all the bean managers 41 must be populated; closed connections deregister themselves. 42 """ 43 44 name: ClassVar[str] = "" 45 prefix: ClassVar[str] = "" 46 47 def __init__(self) -> None: 48 if not self.name: 49 raise TypeError(f"{type(self).__name__}.name must be set") 50 if not self.prefix: 51 raise TypeError(f"{type(self).__name__}.prefix must be set") 52 53 # Build the default managers. Drivers override these in open() with 54 # connection-specific subclasses that talk to hardware. 55 self.turnouts: TurnoutManager = self._make_turnout_manager() 56 self.sensors: SensorManager = self._make_sensor_manager() 57 self.signal_heads: SignalHeadManager = self._make_signal_head_manager() 58 self.signal_masts: SignalMastManager = self._make_signal_mast_manager() 59 self.blocks: BlockManager = self._make_block_manager() 60 self.memories: MemoryManager = self._make_memory_manager() 61 self.reporters: ReporterManager = self._make_reporter_manager() 62 self.lights: LightManager = self._make_light_manager() 63 self.routes: RouteManager = self._make_route_manager() 64 self.throttles: ThrottleManager = self._make_throttle_manager() 65 self.programmers: ProgrammerManager = self._make_programmer_manager() 66 self.consists: ConsistManager = self._make_consist_manager() 67 # The four beans added in the "missing JSON topics" pass. These 68 # are software-only for every driver — no wire ops attached to 69 # the audio state, no reporter-side write to IdTag, no 70 # occupancy logic on layout blocks or oblocks. Managers are 71 # here so scripts and the JSON layer can register beans on any 72 # connection uniformly; real hardware integration lands with 73 # Panel Editor (Tier 1 #3) and any future audio subsystem. 74 self.audios: AudioManager = self._make_audio_manager() 75 self.id_tags: IdTagManager = self._make_id_tag_manager() 76 self.layout_blocks: LayoutBlockManager = self._make_layout_block_manager() 77 self.oblocks: OBlockManager = self._make_oblock_manager() 78 self.power: PowerManager = PowerManager() 79 self._opened: bool = False 80 # Command-station reachability. Optimistic default: drivers 81 # that don't implement heartbeat detection stay at True while 82 # the connection is open, so the base behaviour matches 83 # ``is_open``. Drivers with real detection call 84 # :meth:`_set_cs_reachable` from their traffic controller to 85 # flip this in response to actual bus activity. 86 self._cs_reachable: bool = True 87 self.command_station_power_changed: Signal[bool] = Signal() 88 89 # ---- lifecycle --------------------------------------------------------- 90 91 @abstractmethod 92 async def open(self, settings: ConnectionSettings | None = None) -> None: 93 """Connect to hardware and start any IO loops. 94 95 Concrete drivers acquire serial/TCP ports here, install protocol 96 handlers, and start any required background tasks. The base 97 implementation should be called via ``await super().open(settings)`` 98 to mark the connection opened. 99 """ 100 self._opened = True 101 102 @abstractmethod 103 async def close(self) -> None: 104 """Disconnect from hardware. Idempotent. Base impl flips the flag.""" 105 self._opened = False 106 107 @property 108 def is_open(self) -> bool: 109 return self._opened 110 111 @property 112 def is_command_station_on(self) -> bool: 113 """True when the command station itself has power. 114 115 Distinct from :attr:`PowerManager.state` (which is the DCC 116 *track* power — the CS driving DCC on the rails or not). 117 118 Value is ``is_open AND _cs_reachable`` — so a closed 119 connection is always Off, and a driver with real detection 120 (NCE, LocoNet, DCC-EX) can drop the reachable flag when it 121 loses contact with the CS without touching ``is_open``. 122 Drivers that don't implement detection leave ``_cs_reachable`` 123 at its optimistic True default, so their behaviour matches 124 ``is_open`` exactly — no regression. 125 """ 126 return self._opened and self._cs_reachable 127 128 async def _set_cs_reachable(self, reachable: bool) -> None: 129 """Driver hook — call from your traffic controller when you 130 detect a CS-on ↔ CS-off transition. 131 132 Debounces: no-op when the state hasn't changed. Emits 133 :attr:`command_station_power_changed` with the new 134 :attr:`is_command_station_on` value (which folds in the 135 ``is_open`` gate) so subscribers always see the effective 136 power state. 137 """ 138 if self._cs_reachable == reachable: 139 return 140 self._cs_reachable = reachable 141 await self.command_station_power_changed.emit(self.is_command_station_on) 142 143 # ---- manager construction hooks --------------------------------------- 144 # Drivers override these to return their hardware-aware subclasses. 145 146 def _make_turnout_manager(self) -> TurnoutManager: 147 return TurnoutManager(self.prefix) 148 149 def _make_sensor_manager(self) -> SensorManager: 150 return SensorManager(self.prefix) 151 152 def _make_signal_head_manager(self) -> SignalHeadManager: 153 return SignalHeadManager(self.prefix) 154 155 def _make_signal_mast_manager(self) -> SignalMastManager: 156 return SignalMastManager(self.prefix) 157 158 def _make_block_manager(self) -> BlockManager: 159 return BlockManager(self.prefix) 160 161 def _make_memory_manager(self) -> MemoryManager: 162 return MemoryManager(self.prefix) 163 164 def _make_reporter_manager(self) -> ReporterManager: 165 return ReporterManager(self.prefix) 166 167 def _make_light_manager(self) -> LightManager: 168 return LightManager(self.prefix) 169 170 def _make_route_manager(self) -> RouteManager: 171 return RouteManager(self.prefix) 172 173 def _make_throttle_manager(self) -> ThrottleManager: 174 return ThrottleManager(self.prefix) 175 176 def _make_programmer_manager(self) -> ProgrammerManager: 177 return ProgrammerManager() 178 179 def _make_audio_manager(self) -> AudioManager: 180 return AudioManager(self.prefix) 181 182 def _make_id_tag_manager(self) -> IdTagManager: 183 return IdTagManager(self.prefix) 184 185 def _make_layout_block_manager(self) -> LayoutBlockManager: 186 # JMRI Java uses a fixed "IL" prefix for layout blocks 187 # regardless of the driver. We follow that so system-name 188 # tables round-trip against real JMRI XML. 189 return LayoutBlockManager("IL") 190 191 def _make_oblock_manager(self) -> OBlockManager: 192 # JMRI Java uses "O" as the prefix for OBlocks (system names 193 # like "OB1"). Fixed regardless of driver. 194 return OBlockManager("O") 195 196 def _make_consist_manager(self) -> ConsistManager: 197 # Default is the software-fan-out impl. Drivers override to 198 # install a hardware-consist subclass (LnConsistManager, 199 # NceConsistManager, DccexConsistManager) that talks to the 200 # CS's group memory / opcodes. The programmer manager is 201 # passed so ``advanced`` (CV19) consists can auto-write on 202 # add/remove; drivers that replace ``self.programmers`` in 203 # ``open`` must call ``self.consists.rebind_programmers`` too. 204 return ConsistManager(self.throttles, self.prefix, self.programmers) 205 206 # ---- diagnostics ------------------------------------------------------ 207 208 def __repr__(self) -> str: 209 state = "open" if self._opened else "closed" 210 return f"<{type(self).__name__} prefix={self.prefix!r} {state}>"
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.
91 @abstractmethod 92 async def open(self, settings: ConnectionSettings | None = None) -> None: 93 """Connect to hardware and start any IO loops. 94 95 Concrete drivers acquire serial/TCP ports here, install protocol 96 handlers, and start any required background tasks. The base 97 implementation should be called via ``await super().open(settings)`` 98 to mark the connection opened. 99 """ 100 self._opened = True
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.
102 @abstractmethod 103 async def close(self) -> None: 104 """Disconnect from hardware. Idempotent. Base impl flips the flag.""" 105 self._opened = False
Disconnect from hardware. Idempotent. Base impl flips the flag.
111 @property 112 def is_command_station_on(self) -> bool: 113 """True when the command station itself has power. 114 115 Distinct from :attr:`PowerManager.state` (which is the DCC 116 *track* power — the CS driving DCC on the rails or not). 117 118 Value is ``is_open AND _cs_reachable`` — so a closed 119 connection is always Off, and a driver with real detection 120 (NCE, LocoNet, DCC-EX) can drop the reachable flag when it 121 loses contact with the CS without touching ``is_open``. 122 Drivers that don't implement detection leave ``_cs_reachable`` 123 at its optimistic True default, so their behaviour matches 124 ``is_open`` exactly — no regression. 125 """ 126 return self._opened and self._cs_reachable
True when the command station itself has power.
Distinct from PowerManager.state (which is the DCC
track power — the CS driving DCC on the rails or not).
Value is is_open AND _cs_reachable — so a closed
connection is always Off, and a driver with real detection
(NCE, LocoNet, DCC-EX) can drop the reachable flag when it
loses contact with the CS without touching is_open.
Drivers that don't implement detection leave _cs_reachable
at its optimistic True default, so their behaviour matches
is_open exactly — no regression.