jmri_nce.programmer
NCE CV programmer — ops-mode (main track) and direct-mode (programming track).
Two methods of CV access:
- Ops mode (
write_cv_ops/read_cv_ops) talks to a specific loco on the main track. Reads require RailCom-capable hardware downstream (NCE BD20, etc.); without it the read times out. - Direct mode (
write_cv_direct/read_cv_direct) is the programming-track service mode. The CS pauses normal layout traffic while the operation is in progress.
The programmer exposes a "programming in progress" event so the
~jmri_nce.monitors.NceTurnoutMonitor can pause its memory polls
during direct-mode sessions (otherwise the CS may queue reads behind the
programming wait).
This module also adapts the NCE-specific programmer to the generic
jmri_core.beans.programmer.Programmer ABC via
NceServiceProgrammer and NceOpsProgrammer, which is
what the ProgrammerManager hands out for DecoderPro use.
1"""NCE CV programmer — ops-mode (main track) and direct-mode (programming track). 2 3Two methods of CV access: 4 5* **Ops mode** (``write_cv_ops`` / ``read_cv_ops``) talks to a specific loco 6 on the main track. Reads require RailCom-capable hardware downstream 7 (NCE BD20, etc.); without it the read times out. 8* **Direct mode** (``write_cv_direct`` / ``read_cv_direct``) is the 9 programming-track service mode. The CS pauses normal layout traffic 10 while the operation is in progress. 11 12The programmer exposes a "programming in progress" event so the 13:class:`~jmri_nce.monitors.NceTurnoutMonitor` can pause its memory polls 14during direct-mode sessions (otherwise the CS may queue reads behind the 15programming wait). 16 17This module also adapts the NCE-specific programmer to the generic 18:class:`jmri_core.beans.programmer.Programmer` ABC via 19:class:`NceServiceProgrammer` and :class:`NceOpsProgrammer`, which is 20what the :class:`ProgrammerManager` hands out for DecoderPro use. 21""" 22from __future__ import annotations 23 24import asyncio 25import logging 26 27from jmri_core.beans.programmer import ( 28 Programmer, 29 ProgrammerManager, 30 ProgrammingMode, 31) 32 33from jmri_nce.messages import ( 34 NceCommandError, 35 direct_mode_read_cv, 36 direct_mode_write_cv, 37 enter_programming_mode, 38 exit_programming_mode, 39 ops_mode_write_cv, 40) 41from jmri_nce.traffic import NceTrafficController 42 43_log = logging.getLogger(__name__) 44 45 46class NceProgrammer: 47 """CV access against an NCE command station.""" 48 49 def __init__(self, tc: NceTrafficController) -> None: 50 self._tc = tc 51 self._programming_in_progress = asyncio.Event() 52 53 @property 54 def is_programming(self) -> bool: 55 """True while a direct-mode session is in flight.""" 56 return self._programming_in_progress.is_set() 57 58 async def wait_idle(self, timeout: float = 30.0) -> None: 59 """Wait for any in-progress direct-mode session to finish.""" 60 if not self._programming_in_progress.is_set(): 61 return 62 # ``wait`` returns True when set; we want to wait until it's clear. 63 # asyncio.Event has no direct "wait clear" — poll briefly. 64 deadline = asyncio.get_event_loop().time() + timeout 65 while self._programming_in_progress.is_set(): 66 if asyncio.get_event_loop().time() > deadline: 67 raise TimeoutError("programming did not complete in time") 68 await asyncio.sleep(0.05) 69 70 # ----- ops mode (Programming On the Main) ---------------------------- 71 # 72 # Rebuilt on the 2026-07-14 audit path: raw NMRA DCC packet wrapped 73 # in NCE ``QUEUEn_TMP_CMD`` (opcodes 0x93..0x96). See 74 # :func:`jmri_nce.messages.ops_mode_write_cv` for the packet layout. 75 # NCE PowerPro has no ops-mode CV read (RailCom is out of scope), so 76 # only ``write_cv_ops`` exists — reads must go via direct mode. 77 78 async def write_cv_ops( 79 self, loco_address: int, cv: int, value: int, 80 *, long_address: bool = False, 81 ) -> None: 82 """Ops-mode CV write (POM). Fires a single DCC packet on the main. 83 84 Unlike ``write_cv_direct``, this does *not* pause layout traffic: 85 the CS just queues a service-mode packet in-between throttle 86 packets. Result reaches the loco during normal operation. 87 88 There's no wire-level "did the decoder accept it?" — POM is 89 inherently unverified without RailCom. The returned ACK confirms 90 the CS queued the packet, not that the decoder wrote the CV. 91 """ 92 await self._tc.send_command( 93 ops_mode_write_cv( 94 loco_address, cv, value, long_address=long_address, 95 ), 96 ) 97 98 # ----- direct mode (programming track) ------------------------------- 99 # 100 # NCE PowerPro's direct-mode CV opcodes (``0xA8`` / ``0xA9``) require 101 # the CS to be in *programming mode* — a persistent state entered 102 # via ``ENTER_PROG_CMD`` (0x9E) and exited via ``EXIT_PROG_CMD`` 103 # (0x9F). Outside programming mode the CS immediately NoAcks the 104 # command with ``'0'`` (0x30) *without* running any DCC packets on 105 # the track. That looks like an "instant NoAck" on the wire (~ms, 106 # not the 2–6 s a real programming attempt takes). 107 # 108 # Java JMRI's ``NceProgrammer`` wraps every direct-mode read/write 109 # with enter/exit. We mirror that here in :meth:`_prog_op` — the 110 # write and read helpers both go through it so exit always happens 111 # even when the operation raises. 112 113 async def _prog_op( 114 self, op, *, consume_trailer: bool = False, 115 ) -> "NceReply": # noqa: F821 116 """Run *op* (an NceCommand) wrapped in ENTER_PROG / EXIT_PROG. 117 118 The enter and exit commands are short (single-byte requests, 119 1-byte ACK reply) so the wrapper adds ~10 ms of latency, dwarfed 120 by the 2–6 s of any real direct-mode CV op. Best-effort exit: 121 if the wrapped op raises, we still send EXIT_PROG so the CS 122 returns to normal operation and layout traffic resumes. The 123 exit's own reply is swallowed on failure — we don't want it 124 masking the original error. 125 126 *consume_trailer*: for RD_CV_DIRECT (and any future command 127 where NCE PowerPro appends a trailing ACK byte after the 128 payload), set True so the trailer is eaten before EXIT_PROG 129 runs. Without this, the trailer can land inside EXIT_PROG's 130 expected-1-byte reply window and cascade into stream 131 desynchronization. 132 """ 133 await self._tc.send_command(enter_programming_mode()) 134 try: 135 reply = await self._tc.send_command(op) 136 if consume_trailer: 137 await self._tc.consume_trailer(max_wait_s=0.1) 138 return reply 139 finally: 140 try: 141 await self._tc.send_command(exit_programming_mode()) 142 except Exception as e: # noqa: BLE001 143 _log.warning( 144 "NCE: EXIT_PROG failed after %s: %s", op.name, e, 145 ) 146 147 async def write_cv_direct(self, cv: int, value: int) -> None: 148 """Service-mode write. Pauses layout traffic for ~2–6 s. 149 150 Wraps the write with :meth:`_prog_op` so the CS is in 151 programming mode when the ``WRITE_DIR_CV_CMD`` (0xA8) opcode 152 arrives — a hard requirement of NCE PowerPro firmware. 153 """ 154 self._programming_in_progress.set() 155 try: 156 await self._prog_op(direct_mode_write_cv(cv, value)) 157 finally: 158 self._programming_in_progress.clear() 159 160 async def read_cv_direct(self, cv: int) -> int: 161 """Service-mode read. Same caveats as :meth:`write_cv_direct`. 162 163 Wrapped in ENTER_PROG / EXIT_PROG for the same reason as writes. 164 Raises :class:`NceCommandError` when the decoder doesn't 165 acknowledge — NCE signals that by sending the single byte 166 ``0x30`` (ASCII ``'0'``). JMRI Java's ``NceProgrammer`` treats 167 byte ``'0'`` as ``NoAck`` regardless of whether the actual CV 168 happens to hold value 48; we mirror that here since the 169 alternative (a silent "value 48 always") is worse than the rare 170 false negative when a CV genuinely equals 48. 171 172 Firmware quirk on successful reads: NCE PowerPro sends *two* 173 bytes back — the CV value plus a trailing ``!`` ACK. Our 174 command spec asks for ``reply_len=1`` so the trailer lands in 175 the serial buffer. If the next command's drain catches it, 176 fine; if it doesn't (race), the trailer contaminates the next 177 reply. We proactively consume it here with a short blocking 178 wait, closing that race window. 179 """ 180 self._programming_in_progress.set() 181 try: 182 reply = await self._prog_op( 183 direct_mode_read_cv(cv), consume_trailer=True, 184 ) 185 byte = reply.data[0] 186 if byte == 0x30: # '0' = NCE NoAck sentinel 187 # NoAck is a single byte on the wire; the trailer eater 188 # in ``_prog_op`` times out silently in that case, 189 # which is fine. 190 raise NceCommandError( 191 reply.command, b"0", 192 b"decoder did not ack read (no decoder on prog track?)", 193 ) 194 return byte 195 except NceCommandError: 196 raise 197 finally: 198 self._programming_in_progress.clear() 199 200 201# ---- adapters to the jmri-core Programmer ABC ---------------------------- 202 203 204class NceServiceProgrammer(Programmer): 205 """Service-mode adapter — programming-track CV access.""" 206 207 def __init__(self, inner: NceProgrammer) -> None: 208 self._inner = inner 209 210 @property 211 def mode(self) -> ProgrammingMode: 212 return ProgrammingMode.SERVICE_DIRECT 213 214 async def read_cv(self, cv: int) -> int: 215 return await self._inner.read_cv_direct(cv) 216 217 async def write_cv(self, cv: int, value: int) -> None: 218 await self._inner.write_cv_direct(cv, value) 219 220 221class NceOpsProgrammer(Programmer): 222 """Ops-mode adapter — Programming On the Main for one specific loco. 223 224 Constructed via :meth:`NceProgrammerManager.ops_programmer`; carries 225 the target DCC address so callers can use the same ``read_cv`` / 226 ``write_cv`` surface as service mode. 227 228 ``read_cv`` raises :class:`NotImplementedError` — NCE PowerPro has 229 no ops-mode CV read opcode (no RailCom). Callers that need a verified 230 read must switch to service mode on the programming track. 231 """ 232 233 def __init__( 234 self, inner: NceProgrammer, address: int, *, long_address: bool = False, 235 ) -> None: 236 self._inner = inner 237 self._address = address 238 self._long_address = long_address 239 240 @property 241 def mode(self) -> ProgrammingMode: 242 return ProgrammingMode.OPS_MODE 243 244 @property 245 def address(self) -> int: 246 return self._address 247 248 @property 249 def long_address(self) -> bool: 250 return self._long_address 251 252 async def read_cv(self, cv: int) -> int: 253 raise NotImplementedError( 254 "NCE has no ops-mode CV read (RailCom is not decoded by the " 255 "PowerPro command station). Use service mode via " 256 "NceServiceProgrammer for a verified read.", 257 ) 258 259 async def write_cv(self, cv: int, value: int) -> None: 260 await self._inner.write_cv_ops( 261 self._address, cv, value, long_address=self._long_address, 262 ) 263 264 265class NceProgrammerManager(ProgrammerManager): 266 """Hands out NCE service and ops programmers backed by one underlying 267 :class:`NceProgrammer`.""" 268 269 def __init__(self, inner: NceProgrammer) -> None: 270 self._inner = inner 271 self._service_singleton: NceServiceProgrammer | None = None 272 273 def global_programmer(self) -> NceServiceProgrammer: 274 if self._service_singleton is None: 275 self._service_singleton = NceServiceProgrammer(self._inner) 276 return self._service_singleton 277 278 def ops_programmer( 279 self, address: int, *, long_address: bool = False, 280 ) -> NceOpsProgrammer: 281 """Return an ops-mode programmer bound to *address*. 282 283 Fresh instance per call — the caller usually owns one per loco 284 currently in the batch queue. Cheap: no wire traffic until the 285 first ``write_cv``. 286 """ 287 return NceOpsProgrammer( 288 self._inner, address, long_address=long_address, 289 ) 290 291 @property 292 def inner(self) -> NceProgrammer: 293 """Direct access to the underlying NCE-specific programmer.""" 294 return self._inner
47class NceProgrammer: 48 """CV access against an NCE command station.""" 49 50 def __init__(self, tc: NceTrafficController) -> None: 51 self._tc = tc 52 self._programming_in_progress = asyncio.Event() 53 54 @property 55 def is_programming(self) -> bool: 56 """True while a direct-mode session is in flight.""" 57 return self._programming_in_progress.is_set() 58 59 async def wait_idle(self, timeout: float = 30.0) -> None: 60 """Wait for any in-progress direct-mode session to finish.""" 61 if not self._programming_in_progress.is_set(): 62 return 63 # ``wait`` returns True when set; we want to wait until it's clear. 64 # asyncio.Event has no direct "wait clear" — poll briefly. 65 deadline = asyncio.get_event_loop().time() + timeout 66 while self._programming_in_progress.is_set(): 67 if asyncio.get_event_loop().time() > deadline: 68 raise TimeoutError("programming did not complete in time") 69 await asyncio.sleep(0.05) 70 71 # ----- ops mode (Programming On the Main) ---------------------------- 72 # 73 # Rebuilt on the 2026-07-14 audit path: raw NMRA DCC packet wrapped 74 # in NCE ``QUEUEn_TMP_CMD`` (opcodes 0x93..0x96). See 75 # :func:`jmri_nce.messages.ops_mode_write_cv` for the packet layout. 76 # NCE PowerPro has no ops-mode CV read (RailCom is out of scope), so 77 # only ``write_cv_ops`` exists — reads must go via direct mode. 78 79 async def write_cv_ops( 80 self, loco_address: int, cv: int, value: int, 81 *, long_address: bool = False, 82 ) -> None: 83 """Ops-mode CV write (POM). Fires a single DCC packet on the main. 84 85 Unlike ``write_cv_direct``, this does *not* pause layout traffic: 86 the CS just queues a service-mode packet in-between throttle 87 packets. Result reaches the loco during normal operation. 88 89 There's no wire-level "did the decoder accept it?" — POM is 90 inherently unverified without RailCom. The returned ACK confirms 91 the CS queued the packet, not that the decoder wrote the CV. 92 """ 93 await self._tc.send_command( 94 ops_mode_write_cv( 95 loco_address, cv, value, long_address=long_address, 96 ), 97 ) 98 99 # ----- direct mode (programming track) ------------------------------- 100 # 101 # NCE PowerPro's direct-mode CV opcodes (``0xA8`` / ``0xA9``) require 102 # the CS to be in *programming mode* — a persistent state entered 103 # via ``ENTER_PROG_CMD`` (0x9E) and exited via ``EXIT_PROG_CMD`` 104 # (0x9F). Outside programming mode the CS immediately NoAcks the 105 # command with ``'0'`` (0x30) *without* running any DCC packets on 106 # the track. That looks like an "instant NoAck" on the wire (~ms, 107 # not the 2–6 s a real programming attempt takes). 108 # 109 # Java JMRI's ``NceProgrammer`` wraps every direct-mode read/write 110 # with enter/exit. We mirror that here in :meth:`_prog_op` — the 111 # write and read helpers both go through it so exit always happens 112 # even when the operation raises. 113 114 async def _prog_op( 115 self, op, *, consume_trailer: bool = False, 116 ) -> "NceReply": # noqa: F821 117 """Run *op* (an NceCommand) wrapped in ENTER_PROG / EXIT_PROG. 118 119 The enter and exit commands are short (single-byte requests, 120 1-byte ACK reply) so the wrapper adds ~10 ms of latency, dwarfed 121 by the 2–6 s of any real direct-mode CV op. Best-effort exit: 122 if the wrapped op raises, we still send EXIT_PROG so the CS 123 returns to normal operation and layout traffic resumes. The 124 exit's own reply is swallowed on failure — we don't want it 125 masking the original error. 126 127 *consume_trailer*: for RD_CV_DIRECT (and any future command 128 where NCE PowerPro appends a trailing ACK byte after the 129 payload), set True so the trailer is eaten before EXIT_PROG 130 runs. Without this, the trailer can land inside EXIT_PROG's 131 expected-1-byte reply window and cascade into stream 132 desynchronization. 133 """ 134 await self._tc.send_command(enter_programming_mode()) 135 try: 136 reply = await self._tc.send_command(op) 137 if consume_trailer: 138 await self._tc.consume_trailer(max_wait_s=0.1) 139 return reply 140 finally: 141 try: 142 await self._tc.send_command(exit_programming_mode()) 143 except Exception as e: # noqa: BLE001 144 _log.warning( 145 "NCE: EXIT_PROG failed after %s: %s", op.name, e, 146 ) 147 148 async def write_cv_direct(self, cv: int, value: int) -> None: 149 """Service-mode write. Pauses layout traffic for ~2–6 s. 150 151 Wraps the write with :meth:`_prog_op` so the CS is in 152 programming mode when the ``WRITE_DIR_CV_CMD`` (0xA8) opcode 153 arrives — a hard requirement of NCE PowerPro firmware. 154 """ 155 self._programming_in_progress.set() 156 try: 157 await self._prog_op(direct_mode_write_cv(cv, value)) 158 finally: 159 self._programming_in_progress.clear() 160 161 async def read_cv_direct(self, cv: int) -> int: 162 """Service-mode read. Same caveats as :meth:`write_cv_direct`. 163 164 Wrapped in ENTER_PROG / EXIT_PROG for the same reason as writes. 165 Raises :class:`NceCommandError` when the decoder doesn't 166 acknowledge — NCE signals that by sending the single byte 167 ``0x30`` (ASCII ``'0'``). JMRI Java's ``NceProgrammer`` treats 168 byte ``'0'`` as ``NoAck`` regardless of whether the actual CV 169 happens to hold value 48; we mirror that here since the 170 alternative (a silent "value 48 always") is worse than the rare 171 false negative when a CV genuinely equals 48. 172 173 Firmware quirk on successful reads: NCE PowerPro sends *two* 174 bytes back — the CV value plus a trailing ``!`` ACK. Our 175 command spec asks for ``reply_len=1`` so the trailer lands in 176 the serial buffer. If the next command's drain catches it, 177 fine; if it doesn't (race), the trailer contaminates the next 178 reply. We proactively consume it here with a short blocking 179 wait, closing that race window. 180 """ 181 self._programming_in_progress.set() 182 try: 183 reply = await self._prog_op( 184 direct_mode_read_cv(cv), consume_trailer=True, 185 ) 186 byte = reply.data[0] 187 if byte == 0x30: # '0' = NCE NoAck sentinel 188 # NoAck is a single byte on the wire; the trailer eater 189 # in ``_prog_op`` times out silently in that case, 190 # which is fine. 191 raise NceCommandError( 192 reply.command, b"0", 193 b"decoder did not ack read (no decoder on prog track?)", 194 ) 195 return byte 196 except NceCommandError: 197 raise 198 finally: 199 self._programming_in_progress.clear()
CV access against an NCE command station.
54 @property 55 def is_programming(self) -> bool: 56 """True while a direct-mode session is in flight.""" 57 return self._programming_in_progress.is_set()
True while a direct-mode session is in flight.
59 async def wait_idle(self, timeout: float = 30.0) -> None: 60 """Wait for any in-progress direct-mode session to finish.""" 61 if not self._programming_in_progress.is_set(): 62 return 63 # ``wait`` returns True when set; we want to wait until it's clear. 64 # asyncio.Event has no direct "wait clear" — poll briefly. 65 deadline = asyncio.get_event_loop().time() + timeout 66 while self._programming_in_progress.is_set(): 67 if asyncio.get_event_loop().time() > deadline: 68 raise TimeoutError("programming did not complete in time") 69 await asyncio.sleep(0.05)
Wait for any in-progress direct-mode session to finish.
79 async def write_cv_ops( 80 self, loco_address: int, cv: int, value: int, 81 *, long_address: bool = False, 82 ) -> None: 83 """Ops-mode CV write (POM). Fires a single DCC packet on the main. 84 85 Unlike ``write_cv_direct``, this does *not* pause layout traffic: 86 the CS just queues a service-mode packet in-between throttle 87 packets. Result reaches the loco during normal operation. 88 89 There's no wire-level "did the decoder accept it?" — POM is 90 inherently unverified without RailCom. The returned ACK confirms 91 the CS queued the packet, not that the decoder wrote the CV. 92 """ 93 await self._tc.send_command( 94 ops_mode_write_cv( 95 loco_address, cv, value, long_address=long_address, 96 ), 97 )
Ops-mode CV write (POM). Fires a single DCC packet on the main.
Unlike write_cv_direct, this does not pause layout traffic:
the CS just queues a service-mode packet in-between throttle
packets. Result reaches the loco during normal operation.
There's no wire-level "did the decoder accept it?" — POM is inherently unverified without RailCom. The returned ACK confirms the CS queued the packet, not that the decoder wrote the CV.
148 async def write_cv_direct(self, cv: int, value: int) -> None: 149 """Service-mode write. Pauses layout traffic for ~2–6 s. 150 151 Wraps the write with :meth:`_prog_op` so the CS is in 152 programming mode when the ``WRITE_DIR_CV_CMD`` (0xA8) opcode 153 arrives — a hard requirement of NCE PowerPro firmware. 154 """ 155 self._programming_in_progress.set() 156 try: 157 await self._prog_op(direct_mode_write_cv(cv, value)) 158 finally: 159 self._programming_in_progress.clear()
Service-mode write. Pauses layout traffic for ~2–6 s.
Wraps the write with _prog_op() so the CS is in
programming mode when the WRITE_DIR_CV_CMD (0xA8) opcode
arrives — a hard requirement of NCE PowerPro firmware.
161 async def read_cv_direct(self, cv: int) -> int: 162 """Service-mode read. Same caveats as :meth:`write_cv_direct`. 163 164 Wrapped in ENTER_PROG / EXIT_PROG for the same reason as writes. 165 Raises :class:`NceCommandError` when the decoder doesn't 166 acknowledge — NCE signals that by sending the single byte 167 ``0x30`` (ASCII ``'0'``). JMRI Java's ``NceProgrammer`` treats 168 byte ``'0'`` as ``NoAck`` regardless of whether the actual CV 169 happens to hold value 48; we mirror that here since the 170 alternative (a silent "value 48 always") is worse than the rare 171 false negative when a CV genuinely equals 48. 172 173 Firmware quirk on successful reads: NCE PowerPro sends *two* 174 bytes back — the CV value plus a trailing ``!`` ACK. Our 175 command spec asks for ``reply_len=1`` so the trailer lands in 176 the serial buffer. If the next command's drain catches it, 177 fine; if it doesn't (race), the trailer contaminates the next 178 reply. We proactively consume it here with a short blocking 179 wait, closing that race window. 180 """ 181 self._programming_in_progress.set() 182 try: 183 reply = await self._prog_op( 184 direct_mode_read_cv(cv), consume_trailer=True, 185 ) 186 byte = reply.data[0] 187 if byte == 0x30: # '0' = NCE NoAck sentinel 188 # NoAck is a single byte on the wire; the trailer eater 189 # in ``_prog_op`` times out silently in that case, 190 # which is fine. 191 raise NceCommandError( 192 reply.command, b"0", 193 b"decoder did not ack read (no decoder on prog track?)", 194 ) 195 return byte 196 except NceCommandError: 197 raise 198 finally: 199 self._programming_in_progress.clear()
Service-mode read. Same caveats as write_cv_direct().
Wrapped in ENTER_PROG / EXIT_PROG for the same reason as writes.
Raises NceCommandError when the decoder doesn't
acknowledge — NCE signals that by sending the single byte
0x30 (ASCII '0'). JMRI Java's NceProgrammer treats
byte '0' as NoAck regardless of whether the actual CV
happens to hold value 48; we mirror that here since the
alternative (a silent "value 48 always") is worse than the rare
false negative when a CV genuinely equals 48.
Firmware quirk on successful reads: NCE PowerPro sends two
bytes back — the CV value plus a trailing ! ACK. Our
command spec asks for reply_len=1 so the trailer lands in
the serial buffer. If the next command's drain catches it,
fine; if it doesn't (race), the trailer contaminates the next
reply. We proactively consume it here with a short blocking
wait, closing that race window.
205class NceServiceProgrammer(Programmer): 206 """Service-mode adapter — programming-track CV access.""" 207 208 def __init__(self, inner: NceProgrammer) -> None: 209 self._inner = inner 210 211 @property 212 def mode(self) -> ProgrammingMode: 213 return ProgrammingMode.SERVICE_DIRECT 214 215 async def read_cv(self, cv: int) -> int: 216 return await self._inner.read_cv_direct(cv) 217 218 async def write_cv(self, cv: int, value: int) -> None: 219 await self._inner.write_cv_direct(cv, value)
Service-mode adapter — programming-track CV access.
222class NceOpsProgrammer(Programmer): 223 """Ops-mode adapter — Programming On the Main for one specific loco. 224 225 Constructed via :meth:`NceProgrammerManager.ops_programmer`; carries 226 the target DCC address so callers can use the same ``read_cv`` / 227 ``write_cv`` surface as service mode. 228 229 ``read_cv`` raises :class:`NotImplementedError` — NCE PowerPro has 230 no ops-mode CV read opcode (no RailCom). Callers that need a verified 231 read must switch to service mode on the programming track. 232 """ 233 234 def __init__( 235 self, inner: NceProgrammer, address: int, *, long_address: bool = False, 236 ) -> None: 237 self._inner = inner 238 self._address = address 239 self._long_address = long_address 240 241 @property 242 def mode(self) -> ProgrammingMode: 243 return ProgrammingMode.OPS_MODE 244 245 @property 246 def address(self) -> int: 247 return self._address 248 249 @property 250 def long_address(self) -> bool: 251 return self._long_address 252 253 async def read_cv(self, cv: int) -> int: 254 raise NotImplementedError( 255 "NCE has no ops-mode CV read (RailCom is not decoded by the " 256 "PowerPro command station). Use service mode via " 257 "NceServiceProgrammer for a verified read.", 258 ) 259 260 async def write_cv(self, cv: int, value: int) -> None: 261 await self._inner.write_cv_ops( 262 self._address, cv, value, long_address=self._long_address, 263 )
Ops-mode adapter — Programming On the Main for one specific loco.
Constructed via NceProgrammerManager.ops_programmer(); carries
the target DCC address so callers can use the same read_cv /
write_cv surface as service mode.
read_cv raises NotImplementedError — NCE PowerPro has
no ops-mode CV read opcode (no RailCom). Callers that need a verified
read must switch to service mode on the programming track.
253 async def read_cv(self, cv: int) -> int: 254 raise NotImplementedError( 255 "NCE has no ops-mode CV read (RailCom is not decoded by the " 256 "PowerPro command station). Use service mode via " 257 "NceServiceProgrammer for a verified read.", 258 )
Read CV cv, returns 0..255.
266class NceProgrammerManager(ProgrammerManager): 267 """Hands out NCE service and ops programmers backed by one underlying 268 :class:`NceProgrammer`.""" 269 270 def __init__(self, inner: NceProgrammer) -> None: 271 self._inner = inner 272 self._service_singleton: NceServiceProgrammer | None = None 273 274 def global_programmer(self) -> NceServiceProgrammer: 275 if self._service_singleton is None: 276 self._service_singleton = NceServiceProgrammer(self._inner) 277 return self._service_singleton 278 279 def ops_programmer( 280 self, address: int, *, long_address: bool = False, 281 ) -> NceOpsProgrammer: 282 """Return an ops-mode programmer bound to *address*. 283 284 Fresh instance per call — the caller usually owns one per loco 285 currently in the batch queue. Cheap: no wire traffic until the 286 first ``write_cv``. 287 """ 288 return NceOpsProgrammer( 289 self._inner, address, long_address=long_address, 290 ) 291 292 @property 293 def inner(self) -> NceProgrammer: 294 """Direct access to the underlying NCE-specific programmer.""" 295 return self._inner
Hands out NCE service and ops programmers backed by one underlying
NceProgrammer.
279 def ops_programmer( 280 self, address: int, *, long_address: bool = False, 281 ) -> NceOpsProgrammer: 282 """Return an ops-mode programmer bound to *address*. 283 284 Fresh instance per call — the caller usually owns one per loco 285 currently in the batch queue. Cheap: no wire traffic until the 286 first ``write_cv``. 287 """ 288 return NceOpsProgrammer( 289 self._inner, address, long_address=long_address, 290 )
Return an ops-mode programmer bound to address.
Fresh instance per call — the caller usually owns one per loco
currently in the batch queue. Cheap: no wire traffic until the
first write_cv.
292 @property 293 def inner(self) -> NceProgrammer: 294 """Direct access to the underlying NCE-specific programmer.""" 295 return self._inner
Direct access to the underlying NCE-specific programmer.