jmri_nce.beans
NCE-aware bean classes + managers + power manager.
1"""NCE-aware bean classes + managers + power manager.""" 2from __future__ import annotations 3 4from jmri_core.beans.power import PowerManager 5from jmri_core.beans.sensor import Sensor 6from jmri_core.beans.turnout import Turnout 7from jmri_core.manager import Manager 8from jmri_core.state import PowerState, TurnoutState 9 10from jmri_nce.messages import accessory_decoder_normal_ops, power_off, power_on 11from jmri_nce.traffic import NceTrafficController 12 13# ----- Turnout ------------------------------------------------------------- 14 15 16class NceTurnout(Turnout): 17 """Sends an accessory-decoder normal-ops packet on commanded-state change. 18 19 Known state arrives via :class:`~jmri_nce.monitors.NceTurnoutMonitor` 20 polling, not in-line, so :meth:`set_commanded_state` does **not** mirror 21 commanded into known. The monitor sets known state on the next poll 22 cycle (default 1.5s). 23 """ 24 25 __slots__ = ("_address", "_tc") 26 27 def __init__(self, system_name: str, address: int, tc: NceTrafficController) -> None: 28 super().__init__(system_name) 29 self._address = address 30 self._tc = tc 31 32 @property 33 def address(self) -> int: 34 return self._address 35 36 async def set_commanded_state(self, state: TurnoutState) -> None: 37 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 38 await self._tc.send_command( 39 accessory_decoder_normal_ops(self._address, on=(state == TurnoutState.CLOSED)) 40 ) 41 prev = self._commanded_state 42 if prev == state: 43 return 44 self._commanded_state = state 45 # Mirror commanded → known for immediate UI feedback. JMRI Java 46 # does the same. The next monitor poll will correct this back if 47 # the real CS state disagrees (e.g., motor stall, no decoder). 48 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 49 await self._set_known_state(state) 50 51 52class NceTurnoutManager(Manager[NceTurnout]): 53 """Constructs and indexes NCE turnouts by accessory address.""" 54 55 type_letter = "T" 56 57 def __init__(self, system_prefix: str, *, tc: NceTrafficController) -> None: 58 super().__init__(system_prefix) 59 self._tc = tc 60 self._by_address: dict[int, NceTurnout] = {} 61 62 def _create(self, system_name: str) -> NceTurnout: 63 address = _parse_address(system_name, self.full_prefix) 64 bean = NceTurnout(system_name, address, self._tc) 65 self._by_address[address] = bean 66 return bean 67 68 async def deregister(self, bean: NceTurnout) -> None: 69 self._by_address.pop(bean.address, None) 70 await super().deregister(bean) 71 72 def get_by_address(self, address: int) -> NceTurnout | None: 73 return self._by_address.get(address) 74 75 76# ----- Sensor -------------------------------------------------------------- 77 78 79class NceSensor(Sensor): 80 """Passive — its state is set by the AIU poller. 81 82 Sensor system names embed the AIU address + input number: ``NS<n>`` 83 where *n* = (aiu_addr-1) * 14 + input_bit + 1. The poller does the 84 decoding; the bean is just storage. 85 """ 86 87 __slots__ = ("_address",) 88 89 def __init__(self, system_name: str, address: int) -> None: 90 super().__init__(system_name) 91 self._address = address 92 93 @property 94 def address(self) -> int: 95 return self._address 96 97 98class NceSensorManager(Manager[NceSensor]): 99 type_letter = "S" 100 101 def __init__(self, system_prefix: str) -> None: 102 super().__init__(system_prefix) 103 self._by_address: dict[int, NceSensor] = {} 104 105 def _create(self, system_name: str) -> NceSensor: 106 address = _parse_address(system_name, self.full_prefix) 107 bean = NceSensor(system_name, address) 108 self._by_address[address] = bean 109 return bean 110 111 def get_by_address(self, address: int) -> NceSensor | None: 112 return self._by_address.get(address) 113 114 115# ----- Power --------------------------------------------------------------- 116 117 118class NcePowerManager(PowerManager): 119 """Send OP_POWER_ON / OP_POWER_OFF; state is set on successful reply.""" 120 121 def __init__(self, tc: NceTrafficController) -> None: 122 super().__init__() 123 self._tc = tc 124 125 async def set_power(self, state: PowerState) -> None: 126 if state == PowerState.ON: 127 await self._tc.send_command(power_on()) 128 elif state == PowerState.OFF: 129 await self._tc.send_command(power_off()) 130 # NCE has no IDLE wire command; ignore other targets here. 131 await self._set_state(state) 132 133 134# ----- helpers -------------------------------------------------------------- 135 136 137def _parse_address(system_name: str, full_prefix: str) -> int: 138 rest = system_name.removeprefix(full_prefix) 139 try: 140 return int(rest) 141 except ValueError as e: 142 raise ValueError( 143 f"NCE system name {system_name!r} must end in a numeric address" 144 ) from e
17class NceTurnout(Turnout): 18 """Sends an accessory-decoder normal-ops packet on commanded-state change. 19 20 Known state arrives via :class:`~jmri_nce.monitors.NceTurnoutMonitor` 21 polling, not in-line, so :meth:`set_commanded_state` does **not** mirror 22 commanded into known. The monitor sets known state on the next poll 23 cycle (default 1.5s). 24 """ 25 26 __slots__ = ("_address", "_tc") 27 28 def __init__(self, system_name: str, address: int, tc: NceTrafficController) -> None: 29 super().__init__(system_name) 30 self._address = address 31 self._tc = tc 32 33 @property 34 def address(self) -> int: 35 return self._address 36 37 async def set_commanded_state(self, state: TurnoutState) -> None: 38 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 39 await self._tc.send_command( 40 accessory_decoder_normal_ops(self._address, on=(state == TurnoutState.CLOSED)) 41 ) 42 prev = self._commanded_state 43 if prev == state: 44 return 45 self._commanded_state = state 46 # Mirror commanded → known for immediate UI feedback. JMRI Java 47 # does the same. The next monitor poll will correct this back if 48 # the real CS state disagrees (e.g., motor stall, no decoder). 49 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 50 await self._set_known_state(state)
Sends an accessory-decoder normal-ops packet on commanded-state change.
Known state arrives via ~jmri_nce.monitors.NceTurnoutMonitor
polling, not in-line, so set_commanded_state() does not mirror
commanded into known. The monitor sets known state on the next poll
cycle (default 1.5s).
37 async def set_commanded_state(self, state: TurnoutState) -> None: 38 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 39 await self._tc.send_command( 40 accessory_decoder_normal_ops(self._address, on=(state == TurnoutState.CLOSED)) 41 ) 42 prev = self._commanded_state 43 if prev == state: 44 return 45 self._commanded_state = state 46 # Mirror commanded → known for immediate UI feedback. JMRI Java 47 # does the same. The next monitor poll will correct this back if 48 # the real CS state disagrees (e.g., motor stall, no decoder). 49 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 50 await self._set_known_state(state)
Ask the layout to throw or close the switch.
The base implementation only updates the in-memory commanded state
and emits the signal. Driver subclasses override this to push a
hardware message and then call super().set_commanded_state().
53class NceTurnoutManager(Manager[NceTurnout]): 54 """Constructs and indexes NCE turnouts by accessory address.""" 55 56 type_letter = "T" 57 58 def __init__(self, system_prefix: str, *, tc: NceTrafficController) -> None: 59 super().__init__(system_prefix) 60 self._tc = tc 61 self._by_address: dict[int, NceTurnout] = {} 62 63 def _create(self, system_name: str) -> NceTurnout: 64 address = _parse_address(system_name, self.full_prefix) 65 bean = NceTurnout(system_name, address, self._tc) 66 self._by_address[address] = bean 67 return bean 68 69 async def deregister(self, bean: NceTurnout) -> None: 70 self._by_address.pop(bean.address, None) 71 await super().deregister(bean) 72 73 def get_by_address(self, address: int) -> NceTurnout | None: 74 return self._by_address.get(address)
Constructs and indexes NCE turnouts by accessory address.
69 async def deregister(self, bean: NceTurnout) -> None: 70 self._by_address.pop(bean.address, None) 71 await super().deregister(bean)
Remove bean from the registry. No-op if not present.
80class NceSensor(Sensor): 81 """Passive — its state is set by the AIU poller. 82 83 Sensor system names embed the AIU address + input number: ``NS<n>`` 84 where *n* = (aiu_addr-1) * 14 + input_bit + 1. The poller does the 85 decoding; the bean is just storage. 86 """ 87 88 __slots__ = ("_address",) 89 90 def __init__(self, system_name: str, address: int) -> None: 91 super().__init__(system_name) 92 self._address = address 93 94 @property 95 def address(self) -> int: 96 return self._address
Passive — its state is set by the AIU poller.
Sensor system names embed the AIU address + input number: NS<n>
where n = (aiu_addr-1) * 14 + input_bit + 1. The poller does the
decoding; the bean is just storage.
99class NceSensorManager(Manager[NceSensor]): 100 type_letter = "S" 101 102 def __init__(self, system_prefix: str) -> None: 103 super().__init__(system_prefix) 104 self._by_address: dict[int, NceSensor] = {} 105 106 def _create(self, system_name: str) -> NceSensor: 107 address = _parse_address(system_name, self.full_prefix) 108 bean = NceSensor(system_name, address) 109 self._by_address[address] = bean 110 return bean 111 112 def get_by_address(self, address: int) -> NceSensor | None: 113 return self._by_address.get(address)
Per-type bean registry. Subclass and override _create().
Attributes set on the class describe the bean type:
type_letter— single uppercase letter the bean class owns (e.g."T"for Turnout). Must be set by subclass.system_prefix— connection prefix (e.g."L"). Set per-instance because one bean class can have many connection-specific managers.
119class NcePowerManager(PowerManager): 120 """Send OP_POWER_ON / OP_POWER_OFF; state is set on successful reply.""" 121 122 def __init__(self, tc: NceTrafficController) -> None: 123 super().__init__() 124 self._tc = tc 125 126 async def set_power(self, state: PowerState) -> None: 127 if state == PowerState.ON: 128 await self._tc.send_command(power_on()) 129 elif state == PowerState.OFF: 130 await self._tc.send_command(power_off()) 131 # NCE has no IDLE wire command; ignore other targets here. 132 await self._set_state(state)
Send OP_POWER_ON / OP_POWER_OFF; state is set on successful reply.
126 async def set_power(self, state: PowerState) -> None: 127 if state == PowerState.ON: 128 await self._tc.send_command(power_on()) 129 elif state == PowerState.OFF: 130 await self._tc.send_command(power_off()) 131 # NCE has no IDLE wire command; ignore other targets here. 132 await self._set_state(state)
Command the layout to state. Base impl just updates in-memory.