jmri_nce.messages
NCE commands and replies.
Each command is a fixed-shape request → fixed-shape reply pair. We model
this declaratively with NceCommand and a small set of factory
functions that build commands for the opcodes the rest of the package
needs. New commands can be added without touching the traffic controller.
Every factory in this file must trace to a specific JMRI Java
NceMessage or NceBinaryCommand factory. The wire format is
audit-checked against JMRI's NceMessage.java opcode constants and
NceMonBinaryBundle.properties. Do not add a factory without citing
the JMRI source it mirrors.
1"""NCE commands and replies. 2 3Each command is a fixed-shape request → fixed-shape reply pair. We model 4this declaratively with :class:`NceCommand` and a small set of factory 5functions that build commands for the opcodes the rest of the package 6needs. New commands can be added without touching the traffic controller. 7 8**Every factory in this file must trace to a specific JMRI Java 9``NceMessage`` or ``NceBinaryCommand`` factory.** The wire format is 10audit-checked against JMRI's ``NceMessage.java`` opcode constants and 11``NceMonBinaryBundle.properties``. Do not add a factory without citing 12the JMRI source it mirrors. 13""" 14from __future__ import annotations 15 16from dataclasses import dataclass 17 18from jmri_nce.opcodes import ( 19 CS_CLOCK_ADDR, 20 LONG_ADDRESS_OFFSET, 21 OP_ACCY_DECODER_OPS, 22 OP_AIU_BASE_QUERY, 23 OP_CLOCK_SET_1224, 24 OP_CLOCK_SET_RATIO, 25 OP_CLOCK_START, 26 OP_CLOCK_STOP, 27 OP_ENTER_PROG_MODE, 28 OP_EXIT_PROG_MODE, 29 OP_NCE_LOCO_CMD, 30 OP_POWER_OFF, 31 OP_POWER_ON, 32 OP_QUERY_SW_REV, 33 OP_QUEUE_PACKET_BASE, 34 OP_RD_CV_DIRECT_MODE, 35 OP_READ_16_MEM, 36 OP_WR_CV_DIRECT_MODE, 37 OP_WRITE_N_MEM, 38) 39 40 41@dataclass(frozen=True, slots=True) 42class NceCommand: 43 """One outbound request. 44 45 NCE has two distinct reply shapes: 46 47 * **Action commands** (power on/off, accessory throw, write CV, ...) 48 reply with a single ACK byte (``!`` on success). ``reply_len`` is 49 the number of *data* bytes that precede the ACK — usually 0. 50 * **Read commands** (read memory, read AIU, ...) reply with exactly 51 *reply_len* bytes of raw data, no trailing ACK. Set ``is_read=True`` 52 and the traffic controller will not consume an ACK byte after. 53 """ 54 55 name: str 56 request: bytes 57 reply_len: int = 0 58 timeout_s: float = 0.5 59 is_read: bool = False 60 61 62@dataclass(frozen=True, slots=True) 63class NceReply: 64 """A successful response. ``data`` excludes the ACK byte itself.""" 65 66 command: NceCommand 67 data: bytes 68 69 70class NceCommandError(Exception): 71 """The command station replied with a non-OK ACK byte.""" 72 73 def __init__(self, command: NceCommand, ack: bytes, data: bytes) -> None: 74 super().__init__( 75 f"NCE command {command.name!r} failed: ack={ack!r} data={data!r}" 76 ) 77 self.command = command 78 self.ack = ack 79 self.data = data 80 81 82# --- factory functions ------------------------------------------------------ 83 84 85def power_on() -> NceCommand: 86 """JMRI: ``NceMessage.getEnableMain`` (opcode 137).""" 87 return NceCommand(name="POWER_ON", request=bytes([OP_POWER_ON])) 88 89 90def power_off() -> NceCommand: 91 """JMRI: ``NceMessage.getKillMain`` (opcode 139).""" 92 return NceCommand(name="POWER_OFF", request=bytes([OP_POWER_OFF])) 93 94 95def read_16_memory(address: int) -> NceCommand: 96 """Read exactly 16 bytes at *address*. 97 98 JMRI: ``NceBinaryCommand.accMemoryRead`` (opcode 0x8F, READ16_CMD). 99 NCE has no "read N bytes" opcode — only fixed 16-byte or 1-byte 100 reads. For arbitrary lengths, issue multiple 16-byte reads. 101 """ 102 return NceCommand( 103 name=f"READ16({address:#06x})", 104 request=bytes([OP_READ_16_MEM, (address >> 8) & 0xFF, address & 0xFF]), 105 reply_len=16, 106 is_read=True, 107 ) 108 109 110# Backwards-compatible alias for the turnout monitor's existing call site. 111# The name emphasises that the accessory-state memory is the typical 112# caller, but the opcode itself is address-agnostic. 113def read_accessory_memory(address: int) -> NceCommand: 114 """Read 16 bytes of accessory-state memory at *address* (turnout poll). 115 116 Alias for :func:`read_16_memory` — the underlying opcode 0x8F works 117 at arbitrary addresses despite the historical "accessory" name. 118 """ 119 return read_16_memory(address) 120 121 122def write_memory(address: int, data: bytes) -> NceCommand: 123 """Write *data* (1..16 bytes) starting at the 16-bit *address*. 124 125 JMRI: ``NceBinaryCommand.accMemoryWriteN`` (opcode 0x8E, WRITE_N_CMD). 126 127 IMPORTANT: The CS expects a **fixed 20-byte request** regardless of 128 payload length — JMRI Java allocates ``new byte[20]`` and fills only 129 the first ``4 + data.length`` bytes, leaving the rest zero-padded. 130 We match that shape byte-for-byte; without the pad the CS waits 131 forever for the remaining bytes and the ACK never arrives. 132 """ 133 if not 1 <= len(data) <= 16: 134 raise ValueError(f"write length must be 1..16, got {len(data)}") 135 header = bytes([ 136 OP_WRITE_N_MEM, 137 (address >> 8) & 0xFF, address & 0xFF, 138 len(data) & 0xFF, 139 ]) 140 padding = bytes(20 - len(header) - len(data)) 141 return NceCommand( 142 name=f"WRITE_N_MEM({address:#06x},{len(data)})", 143 request=header + data + padding, 144 ) 145 146 147def accessory_decoder_normal_ops(address: int, on: bool) -> NceCommand: 148 """Operate an accessory via the NCE binary accessory-decoder command. 149 150 JMRI: ``NceBinaryCommand.accDecoder`` (opcode 0xAD, ACC_CMD). 151 152 *address* is the accessory address (1..2044). ``on=True`` selects the 153 "closed" (normal) output; ``on=False`` selects "thrown" (reverse). 154 Wire form: ``[0xAD, addr_hi, addr_lo, dir, 0x00]`` where *dir* is 155 the NMRA accessory-direction nibble (3=closed, 4=thrown). 156 """ 157 if not 1 <= address <= 2044: 158 raise ValueError(f"accessory address must be 1..2044, got {address}") 159 direction = 3 if on else 4 160 return NceCommand( 161 name=f"ACCY({address},{'closed' if on else 'thrown'})", 162 request=bytes([ 163 OP_ACCY_DECODER_OPS, 164 (address >> 8) & 0xFF, 165 address & 0xFF, 166 direction, 167 0x00, 168 ]), 169 ) 170 171 172def nce_loco_cmd( 173 loco_address: int, *, long_address: bool, sub_cmd: int, data: int 174) -> NceCommand: 175 """Throttle command via NCE binary ``nceLocoCmd``. 176 177 JMRI: ``NceBinaryCommand.nceLocoCmd`` (opcode 0xA2, LOCO_CMD). 178 179 Modern firmware (commandOptions >= 10) uses this 5-byte command for 180 speed, direction, and function changes. 181 182 [0xA2] [addr_hi] [addr_lo] [sub_cmd] [data] (5 bytes) 183 184 For long addresses, the wire address is ``loco_address + 0xC000`` 185 so the CS can disambiguate from short addresses with the same value. 186 187 *sub_cmd* values come from :mod:`jmri_nce.opcodes`: 188 189 * ``LOCO_SUB_128_FWD`` / ``LOCO_SUB_128_REV`` — 128-step speed 190 * ``LOCO_SUB_ESTOP_FWD`` / ``LOCO_SUB_ESTOP_REV`` — emergency stop 191 * ``LOCO_SUB_FUNC_GROUP_1..5`` — function bitmasks 192 """ 193 if not 0 <= sub_cmd <= 23: 194 raise ValueError(f"sub_cmd must be 0..23, got {sub_cmd}") 195 if not 0 <= data <= 255: 196 raise ValueError(f"data byte must be 0..255, got {data}") 197 wire = loco_address + LONG_ADDRESS_OFFSET if long_address else loco_address 198 return NceCommand( 199 name=f"LOCO({loco_address}{'L' if long_address else 'S'},sub={sub_cmd},data={data})", 200 request=bytes([ 201 OP_NCE_LOCO_CMD, 202 (wire >> 8) & 0xFF, 203 wire & 0xFF, 204 sub_cmd & 0xFF, 205 data & 0xFF, 206 ]), 207 ) 208 209 210def query_command_version() -> NceCommand: 211 """Read the command station firmware version. Reply is 3 bytes. 212 213 JMRI: ``NceBinaryCommand.getNceEpromRev`` (opcode 0xAA, SW_REV_CMD). 214 Safe: read-only, no side effects. 215 """ 216 return NceCommand( 217 name="QUERY_SW_REV", 218 request=bytes([OP_QUERY_SW_REV]), 219 reply_len=3, 220 is_read=True, 221 ) 222 223 224def enter_programming_mode() -> NceCommand: 225 """Put the CS into programming mode. 226 227 JMRI: ``NceMessage.getEnterProgMode`` (opcode 0x9E). Direct-mode 228 CV read/write commands (``0xA8``, ``0xA9``) require the CS to be 229 in this mode — outside it, the CS immediately NoAcks the request 230 with ``'0'`` (0x30) without running any DCC packets on the prog 231 track. Java JMRI's ``NceProgrammer`` wraps every direct-mode op 232 with this + :func:`exit_programming_mode`. 233 234 Reply is a single ACK byte (``!`` on success). 235 """ 236 return NceCommand( 237 name="ENTER_PROG", 238 request=bytes([OP_ENTER_PROG_MODE]), 239 # Fast: CS just flips a state flag. Give it a generous window 240 # for slow USB-serial adapters. 241 timeout_s=1.0, 242 ) 243 244 245def exit_programming_mode() -> NceCommand: 246 """Take the CS out of programming mode. 247 248 JMRI: ``NceMessage.getExitProgMode`` (opcode 0x9F). Send this after 249 a direct-mode CV op so the CS returns to normal operation and 250 layout traffic resumes. Reply is ``!`` on success. 251 """ 252 return NceCommand( 253 name="EXIT_PROG", 254 request=bytes([OP_EXIT_PROG_MODE]), 255 timeout_s=1.0, 256 ) 257 258 259def direct_mode_write_cv(cv: int, value: int) -> NceCommand: 260 """Write a CV on the programming track (direct mode). 261 262 JMRI: ``NceMessage.getWriteDirectCV`` (opcode 0xA8, WRITE_DIR_CV_CMD). 263 Layout: ``[0xA8, cv_hi, cv_lo, value]``. 264 """ 265 if not 1 <= cv <= 1024: 266 raise ValueError(f"CV must be 1..1024, got {cv}") 267 if not 0 <= value <= 255: 268 raise ValueError(f"value must be 0..255, got {value}") 269 return NceCommand( 270 name=f"WR_CV_DIRECT({cv},{value})", 271 request=bytes([OP_WR_CV_DIRECT_MODE, (cv >> 8) & 0xFF, cv & 0xFF, value]), 272 timeout_s=6.0, # service-mode writes take seconds 273 ) 274 275 276def direct_mode_read_cv(cv: int) -> NceCommand: 277 """Read a CV on the programming track. 278 279 JMRI: ``NceMessage.getReadDirectCV`` (opcode 0xA9, READ_DIR_CV_CMD). 280 Reply is 1 byte (the CV value). 281 """ 282 if not 1 <= cv <= 1024: 283 raise ValueError(f"CV must be 1..1024, got {cv}") 284 return NceCommand( 285 name=f"RD_CV_DIRECT({cv})", 286 request=bytes([OP_RD_CV_DIRECT_MODE, (cv >> 8) & 0xFF, cv & 0xFF]), 287 reply_len=1, 288 timeout_s=6.0, 289 is_read=True, 290 ) 291 292 293def ops_mode_write_cv( 294 loco_address: int, cv: int, value: int, *, long_address: bool = False, 295) -> NceCommand: 296 """Ops-mode CV byte-write (Programming On the Main) to a specific loco. 297 298 JMRI: ``NceMessage.sendPacketMessage`` builds the equivalent wrapped 299 packet in ``NceProgrammer.writeCV`` when ops mode is selected. 300 301 Wire form: build a raw NMRA CV Access instruction packet, then wrap 302 it in the appropriate ``QUEUEn_TMP_CMD`` envelope where ``n`` is the 303 packet's byte length (opcode ``0x93 + (packet_len - 3)`` covers 304 3..6-byte packets, per :data:`OP_QUEUE_PACKET_BASE`). NCE has no 305 native ops-mode CV *read* (RailCom is out of PowerPro's scope) — do 306 not add ``ops_mode_read_cv``; use direct mode for verified reads. 307 308 NMRA S-9.2.1 CV Access — Long Form / Byte Write: 309 310 <address bytes> 1110 CCVV VVVVVVVV DDDDDDDD <XOR> 311 312 * short address (1..127): 1 address byte ``0AAAAAAA`` 313 * long address (128..9999): 2 address bytes ``11AAAAAA AAAAAAAA`` 314 * ``CC = 11`` → byte-write (10 = verify, 01 = bit-manipulate) 315 * ``VVVVVVVVVV`` = CV number minus 1, 10-bit 316 * ``DDDDDDDD`` = value 317 * trailing XOR of all preceding bytes 318 319 Reply: single ACK byte from the CS (``!`` on success). The packet 320 goes on the wire from the CS; no decoder ack round-trip. 321 """ 322 if not 1 <= cv <= 1024: 323 raise ValueError(f"CV must be 1..1024, got {cv}") 324 if not 0 <= value <= 255: 325 raise ValueError(f"value must be 0..255, got {value}") 326 327 if long_address: 328 if not 128 <= loco_address <= 9999: 329 raise ValueError( 330 f"long address must be 128..9999, got {loco_address}" 331 ) 332 # 14-bit long address in the DCC packet: top 6 bits go into the 333 # low 6 bits of 0xC0, low 8 bits stand alone. 334 addr_bytes = [ 335 0xC0 | ((loco_address >> 8) & 0x3F), 336 loco_address & 0xFF, 337 ] 338 else: 339 if not 1 <= loco_address <= 127: 340 raise ValueError( 341 f"short address must be 1..127, got {loco_address}" 342 ) 343 addr_bytes = [loco_address & 0x7F] 344 345 # NMRA encodes CV number minus one, spread over two bytes: 2 high 346 # bits ride the instruction opcode, 8 low bits follow. 347 cv_index = cv - 1 348 inst_hi = 0xEC | ((cv_index >> 8) & 0x03) # 1110_1100 with CC=11 + VV 349 inst_lo = cv_index & 0xFF 350 351 packet = bytes(addr_bytes + [inst_hi, inst_lo, value & 0xFF]) 352 xor = 0 353 for b in packet: 354 xor ^= b 355 dcc_packet = packet + bytes([xor]) 356 357 # QUEUEn_TMP_CMD wire form (verified against JMRI Java NCE Command 358 # Monitor trace on real PowerPro hardware, 2026-07-25): 359 # 360 # [queue_opcode] [send_count] [dcc_packet_bytes...] 361 # 362 # * ``queue_opcode`` = 0x93..0x96 (packet length 3..6) 363 # * ``send_count`` = number of times the CS retransmits the 364 # packet on the wire. NMRA POM recommends 365 # 3–5 repeats for reliability; JMRI Java uses 366 # 5, which we mirror. The CS reads this byte 367 # as part of the command — earlier versions 368 # of this code omitted it, so the CS would 369 # read our first packet byte as the send 370 # count and then time out ~5 s waiting for a 371 # packet byte that never came, returning 372 # NoAck (``0``). 373 # * ``dcc_packet`` = 3..6 bytes: address + CV Access instruction 374 # + XOR checksum, per NMRA S-9.2.1. 375 packet_len = len(dcc_packet) 376 if not 3 <= packet_len <= 6: 377 # Defensive: the packet builder should never produce a length 378 # outside 3..6, but if a future edit changes the encoding this 379 # catches the mistake before the wrong opcode goes out. 380 raise ValueError( 381 f"internal error: DCC packet length {packet_len} outside 3..6", 382 ) 383 queue_opcode = OP_QUEUE_PACKET_BASE + (packet_len - 3) 384 send_count = 5 # matches JMRI Java default 385 386 return NceCommand( 387 name=f"WR_CV_OPS({loco_address}{'L' if long_address else 'S'},{cv},{value})", 388 request=bytes([queue_opcode, send_count]) + dcc_packet, 389 # Ops-mode writes are fire-and-forget on the wire; only the CS 390 # queue-accept ACK ('!') comes back. No data bytes. 391 # 392 # ``timeout_s`` is 5.0 s — matches JMRI Java's timeout for 393 # equivalent operations. NCE PowerPro's actual queue-then-ack 394 # latency depends on the CS's current DCC output load: with 395 # active throttles + fast-clock ticks + accessory refresh, we 396 # observed >2 s on real hardware even after bumping from the 397 # 0.5 s default. 5 s gives comfortable headroom; a real hang 398 # (packet dropped, CS off-line) still surfaces before the user 399 # gives up on the batch. 400 timeout_s=5.0, 401 ) 402 403 404def read_clock() -> NceCommand: 405 """Read the CS fast clock (16 bytes at ``CS_CLOCK_ADDR``). 406 407 JMRI: ``NceClockControl.issueReadOnlyRequest`` uses 408 ``NceBinaryCommand.accMemoryRead(getClockAddr())`` (opcode 0x8F). 409 Wire request: 3 bytes ``[0x8F, addr_hi, addr_lo]``; reply is 16 410 data bytes. Passive read — safe to send even when the clock isn't 411 running. 412 413 Reply byte layout mirrors ``NceClockControl.readClockPacket``: 414 415 * ``[0]`` scale — CS-internal divisor, ``ratio = 250 / scale`` 416 * ``[1]`` tick 417 * ``[2]`` seconds (binary, not BCD) 418 * ``[3]`` minutes 419 * ``[4]`` hours (1..12 in 12h mode; 0..23 in 24h) 420 * ``[5]`` AM/PM flag (ASCII ``'A'`` = 65 → AM) 421 * ``[6]`` 12/24 mode (1 = 24h, else 12h) 422 * ``[13]`` status 423 424 1.5 s timeout (vs the 0.5 s default) leaves headroom on slow USB-serial 425 adapters where a tightly-scheduled read can otherwise time out while 426 its 16 bytes are still in flight. 427 """ 428 return NceCommand( 429 name="READ_CLOCK", 430 request=bytes([ 431 OP_READ_16_MEM, 432 (CS_CLOCK_ADDR >> 8) & 0xFF, CS_CLOCK_ADDR & 0xFF, 433 ]), 434 reply_len=16, 435 is_read=True, 436 timeout_s=1.5, 437 ) 438 439 440def write_clock_time(hours: int, minutes: int, seconds: int) -> NceCommand: 441 """Write hh/mm/ss to the CS clock. 442 443 JMRI: ``NceClockControl.issueClockSetMem`` writes ``[ss, mm, hh]`` at 444 ``CS_CLOCK_ADDR + 2`` via ``accMemoryWriteN`` (opcode 0x8E, WRITE_N_CMD). 445 Delegates to :func:`write_memory` so the fixed-20-byte wire pad is 446 applied in one place. 1.5 s timeout mirrors the clock read. 447 """ 448 if not 0 <= hours <= 23: 449 raise ValueError(f"hours must be 0..23, got {hours}") 450 if not 0 <= minutes <= 59: 451 raise ValueError(f"minutes must be 0..59, got {minutes}") 452 if not 0 <= seconds <= 59: 453 raise ValueError(f"seconds must be 0..59, got {seconds}") 454 payload = bytes([seconds & 0xFF, minutes & 0xFF, hours & 0xFF]) 455 inner = write_memory(CS_CLOCK_ADDR + 2, payload) 456 # Reuse the well-formed request but with a clock-specific name + timeout. 457 return NceCommand( 458 name=f"WRITE_CLOCK_TIME({hours:02d}:{minutes:02d}:{seconds:02d})", 459 request=inner.request, 460 timeout_s=1.5, 461 ) 462 463 464def set_clock_ratio(ratio: int) -> NceCommand: 465 """Set the CS fast-clock ratio (integer, 1..15). 466 467 JMRI: ``NceBinaryCommand.accSetClockRatio`` (opcode 0x87, CLOCK_RATIO_CMD). 468 Sends ``[0x87, ratio]``. Read-back later reports ``scale = 250 / ratio``. 469 """ 470 if not 1 <= ratio <= 15: 471 raise ValueError(f"clock ratio must be 1..15, got {ratio}") 472 return NceCommand( 473 name=f"SET_CLOCK_RATIO({ratio})", 474 request=bytes([OP_CLOCK_SET_RATIO, ratio & 0xFF]), 475 timeout_s=1.5, 476 ) 477 478 479def set_clock_1224(twenty_four_hour: bool) -> NceCommand: 480 """Switch the CS clock between 12h and 24h display. 481 482 JMRI: ``NceBinaryCommand.accSetClock1224`` (opcode 0x86, CLOCK_1224_CMD). 483 """ 484 return NceCommand( 485 name=f"SET_CLOCK_1224({'24' if twenty_four_hour else '12'})", 486 request=bytes([OP_CLOCK_SET_1224, 1 if twenty_four_hour else 0]), 487 timeout_s=1.5, 488 ) 489 490 491def start_clock() -> NceCommand: 492 """Start the CS fast clock. 493 494 JMRI: ``NceBinaryCommand.accStartClock`` (opcode 0x84, START_CLOCK_CMD). 495 """ 496 return NceCommand( 497 name="START_CLOCK", 498 request=bytes([OP_CLOCK_START]), 499 timeout_s=1.5, 500 ) 501 502 503def stop_clock() -> NceCommand: 504 """Stop (hold) the CS fast clock. 505 506 JMRI: ``NceBinaryCommand.accStopClock`` (opcode 0x83, STOP_CLOCK_CMD). 507 """ 508 return NceCommand( 509 name="STOP_CLOCK", 510 request=bytes([OP_CLOCK_STOP]), 511 timeout_s=1.5, 512 ) 513 514 515def read_aiu(aiu_address: int) -> NceCommand: 516 """Read one AIU module's 16-bit input state. 517 518 JMRI: ``NceBinaryCommand.accAiu2Read`` (opcode 0x9B, READ_AUI2_CMD). 519 Returns the AIU's two-byte word: bits 0..13 are inputs 1..14. 520 """ 521 if not 1 <= aiu_address <= 63: 522 raise ValueError(f"AIU address must be 1..63, got {aiu_address}") 523 return NceCommand( 524 name=f"AIU_QUERY({aiu_address})", 525 request=bytes([OP_AIU_BASE_QUERY, aiu_address & 0xFF]), 526 reply_len=2, 527 is_read=True, 528 )
42@dataclass(frozen=True, slots=True) 43class NceCommand: 44 """One outbound request. 45 46 NCE has two distinct reply shapes: 47 48 * **Action commands** (power on/off, accessory throw, write CV, ...) 49 reply with a single ACK byte (``!`` on success). ``reply_len`` is 50 the number of *data* bytes that precede the ACK — usually 0. 51 * **Read commands** (read memory, read AIU, ...) reply with exactly 52 *reply_len* bytes of raw data, no trailing ACK. Set ``is_read=True`` 53 and the traffic controller will not consume an ACK byte after. 54 """ 55 56 name: str 57 request: bytes 58 reply_len: int = 0 59 timeout_s: float = 0.5 60 is_read: bool = False
One outbound request.
NCE has two distinct reply shapes:
- Action commands (power on/off, accessory throw, write CV, ...)
reply with a single ACK byte (
!on success).reply_lenis the number of data bytes that precede the ACK — usually 0. - Read commands (read memory, read AIU, ...) reply with exactly
reply_len bytes of raw data, no trailing ACK. Set
is_read=Trueand the traffic controller will not consume an ACK byte after.
63@dataclass(frozen=True, slots=True) 64class NceReply: 65 """A successful response. ``data`` excludes the ACK byte itself.""" 66 67 command: NceCommand 68 data: bytes
A successful response. data excludes the ACK byte itself.
71class NceCommandError(Exception): 72 """The command station replied with a non-OK ACK byte.""" 73 74 def __init__(self, command: NceCommand, ack: bytes, data: bytes) -> None: 75 super().__init__( 76 f"NCE command {command.name!r} failed: ack={ack!r} data={data!r}" 77 ) 78 self.command = command 79 self.ack = ack 80 self.data = data
The command station replied with a non-OK ACK byte.
86def power_on() -> NceCommand: 87 """JMRI: ``NceMessage.getEnableMain`` (opcode 137).""" 88 return NceCommand(name="POWER_ON", request=bytes([OP_POWER_ON]))
JMRI: NceMessage.getEnableMain (opcode 137).
91def power_off() -> NceCommand: 92 """JMRI: ``NceMessage.getKillMain`` (opcode 139).""" 93 return NceCommand(name="POWER_OFF", request=bytes([OP_POWER_OFF]))
JMRI: NceMessage.getKillMain (opcode 139).
96def read_16_memory(address: int) -> NceCommand: 97 """Read exactly 16 bytes at *address*. 98 99 JMRI: ``NceBinaryCommand.accMemoryRead`` (opcode 0x8F, READ16_CMD). 100 NCE has no "read N bytes" opcode — only fixed 16-byte or 1-byte 101 reads. For arbitrary lengths, issue multiple 16-byte reads. 102 """ 103 return NceCommand( 104 name=f"READ16({address:#06x})", 105 request=bytes([OP_READ_16_MEM, (address >> 8) & 0xFF, address & 0xFF]), 106 reply_len=16, 107 is_read=True, 108 )
Read exactly 16 bytes at address.
JMRI: NceBinaryCommand.accMemoryRead (opcode 0x8F, READ16_CMD).
NCE has no "read N bytes" opcode — only fixed 16-byte or 1-byte
reads. For arbitrary lengths, issue multiple 16-byte reads.
114def read_accessory_memory(address: int) -> NceCommand: 115 """Read 16 bytes of accessory-state memory at *address* (turnout poll). 116 117 Alias for :func:`read_16_memory` — the underlying opcode 0x8F works 118 at arbitrary addresses despite the historical "accessory" name. 119 """ 120 return read_16_memory(address)
Read 16 bytes of accessory-state memory at address (turnout poll).
Alias for read_16_memory() — the underlying opcode 0x8F works
at arbitrary addresses despite the historical "accessory" name.
123def write_memory(address: int, data: bytes) -> NceCommand: 124 """Write *data* (1..16 bytes) starting at the 16-bit *address*. 125 126 JMRI: ``NceBinaryCommand.accMemoryWriteN`` (opcode 0x8E, WRITE_N_CMD). 127 128 IMPORTANT: The CS expects a **fixed 20-byte request** regardless of 129 payload length — JMRI Java allocates ``new byte[20]`` and fills only 130 the first ``4 + data.length`` bytes, leaving the rest zero-padded. 131 We match that shape byte-for-byte; without the pad the CS waits 132 forever for the remaining bytes and the ACK never arrives. 133 """ 134 if not 1 <= len(data) <= 16: 135 raise ValueError(f"write length must be 1..16, got {len(data)}") 136 header = bytes([ 137 OP_WRITE_N_MEM, 138 (address >> 8) & 0xFF, address & 0xFF, 139 len(data) & 0xFF, 140 ]) 141 padding = bytes(20 - len(header) - len(data)) 142 return NceCommand( 143 name=f"WRITE_N_MEM({address:#06x},{len(data)})", 144 request=header + data + padding, 145 )
Write data (1..16 bytes) starting at the 16-bit address.
JMRI: NceBinaryCommand.accMemoryWriteN (opcode 0x8E, WRITE_N_CMD).
IMPORTANT: The CS expects a fixed 20-byte request regardless of
payload length — JMRI Java allocates new byte[20] and fills only
the first 4 + data.length bytes, leaving the rest zero-padded.
We match that shape byte-for-byte; without the pad the CS waits
forever for the remaining bytes and the ACK never arrives.
148def accessory_decoder_normal_ops(address: int, on: bool) -> NceCommand: 149 """Operate an accessory via the NCE binary accessory-decoder command. 150 151 JMRI: ``NceBinaryCommand.accDecoder`` (opcode 0xAD, ACC_CMD). 152 153 *address* is the accessory address (1..2044). ``on=True`` selects the 154 "closed" (normal) output; ``on=False`` selects "thrown" (reverse). 155 Wire form: ``[0xAD, addr_hi, addr_lo, dir, 0x00]`` where *dir* is 156 the NMRA accessory-direction nibble (3=closed, 4=thrown). 157 """ 158 if not 1 <= address <= 2044: 159 raise ValueError(f"accessory address must be 1..2044, got {address}") 160 direction = 3 if on else 4 161 return NceCommand( 162 name=f"ACCY({address},{'closed' if on else 'thrown'})", 163 request=bytes([ 164 OP_ACCY_DECODER_OPS, 165 (address >> 8) & 0xFF, 166 address & 0xFF, 167 direction, 168 0x00, 169 ]), 170 )
Operate an accessory via the NCE binary accessory-decoder command.
JMRI: NceBinaryCommand.accDecoder (opcode 0xAD, ACC_CMD).
address is the accessory address (1..2044). on=True selects the
"closed" (normal) output; on=False selects "thrown" (reverse).
Wire form: [0xAD, addr_hi, addr_lo, dir, 0x00] where dir is
the NMRA accessory-direction nibble (3=closed, 4=thrown).
173def nce_loco_cmd( 174 loco_address: int, *, long_address: bool, sub_cmd: int, data: int 175) -> NceCommand: 176 """Throttle command via NCE binary ``nceLocoCmd``. 177 178 JMRI: ``NceBinaryCommand.nceLocoCmd`` (opcode 0xA2, LOCO_CMD). 179 180 Modern firmware (commandOptions >= 10) uses this 5-byte command for 181 speed, direction, and function changes. 182 183 [0xA2] [addr_hi] [addr_lo] [sub_cmd] [data] (5 bytes) 184 185 For long addresses, the wire address is ``loco_address + 0xC000`` 186 so the CS can disambiguate from short addresses with the same value. 187 188 *sub_cmd* values come from :mod:`jmri_nce.opcodes`: 189 190 * ``LOCO_SUB_128_FWD`` / ``LOCO_SUB_128_REV`` — 128-step speed 191 * ``LOCO_SUB_ESTOP_FWD`` / ``LOCO_SUB_ESTOP_REV`` — emergency stop 192 * ``LOCO_SUB_FUNC_GROUP_1..5`` — function bitmasks 193 """ 194 if not 0 <= sub_cmd <= 23: 195 raise ValueError(f"sub_cmd must be 0..23, got {sub_cmd}") 196 if not 0 <= data <= 255: 197 raise ValueError(f"data byte must be 0..255, got {data}") 198 wire = loco_address + LONG_ADDRESS_OFFSET if long_address else loco_address 199 return NceCommand( 200 name=f"LOCO({loco_address}{'L' if long_address else 'S'},sub={sub_cmd},data={data})", 201 request=bytes([ 202 OP_NCE_LOCO_CMD, 203 (wire >> 8) & 0xFF, 204 wire & 0xFF, 205 sub_cmd & 0xFF, 206 data & 0xFF, 207 ]), 208 )
Throttle command via NCE binary nceLocoCmd.
JMRI: NceBinaryCommand.nceLocoCmd (opcode 0xA2, LOCO_CMD).
Modern firmware (commandOptions >= 10) uses this 5-byte command for speed, direction, and function changes.
[0xA2] [addr_hi] [addr_lo] [sub_cmd] [data] (5 bytes)
For long addresses, the wire address is loco_address + 0xC000
so the CS can disambiguate from short addresses with the same value.
sub_cmd values come from jmri_nce.opcodes:
LOCO_SUB_128_FWD/LOCO_SUB_128_REV— 128-step speedLOCO_SUB_ESTOP_FWD/LOCO_SUB_ESTOP_REV— emergency stopLOCO_SUB_FUNC_GROUP_1..5— function bitmasks
211def query_command_version() -> NceCommand: 212 """Read the command station firmware version. Reply is 3 bytes. 213 214 JMRI: ``NceBinaryCommand.getNceEpromRev`` (opcode 0xAA, SW_REV_CMD). 215 Safe: read-only, no side effects. 216 """ 217 return NceCommand( 218 name="QUERY_SW_REV", 219 request=bytes([OP_QUERY_SW_REV]), 220 reply_len=3, 221 is_read=True, 222 )
Read the command station firmware version. Reply is 3 bytes.
JMRI: NceBinaryCommand.getNceEpromRev (opcode 0xAA, SW_REV_CMD).
Safe: read-only, no side effects.
225def enter_programming_mode() -> NceCommand: 226 """Put the CS into programming mode. 227 228 JMRI: ``NceMessage.getEnterProgMode`` (opcode 0x9E). Direct-mode 229 CV read/write commands (``0xA8``, ``0xA9``) require the CS to be 230 in this mode — outside it, the CS immediately NoAcks the request 231 with ``'0'`` (0x30) without running any DCC packets on the prog 232 track. Java JMRI's ``NceProgrammer`` wraps every direct-mode op 233 with this + :func:`exit_programming_mode`. 234 235 Reply is a single ACK byte (``!`` on success). 236 """ 237 return NceCommand( 238 name="ENTER_PROG", 239 request=bytes([OP_ENTER_PROG_MODE]), 240 # Fast: CS just flips a state flag. Give it a generous window 241 # for slow USB-serial adapters. 242 timeout_s=1.0, 243 )
Put the CS into programming mode.
JMRI: NceMessage.getEnterProgMode (opcode 0x9E). Direct-mode
CV read/write commands (0xA8, 0xA9) require the CS to be
in this mode — outside it, the CS immediately NoAcks the request
with '0' (0x30) without running any DCC packets on the prog
track. Java JMRI's NceProgrammer wraps every direct-mode op
with this + exit_programming_mode().
Reply is a single ACK byte (! on success).
246def exit_programming_mode() -> NceCommand: 247 """Take the CS out of programming mode. 248 249 JMRI: ``NceMessage.getExitProgMode`` (opcode 0x9F). Send this after 250 a direct-mode CV op so the CS returns to normal operation and 251 layout traffic resumes. Reply is ``!`` on success. 252 """ 253 return NceCommand( 254 name="EXIT_PROG", 255 request=bytes([OP_EXIT_PROG_MODE]), 256 timeout_s=1.0, 257 )
Take the CS out of programming mode.
JMRI: NceMessage.getExitProgMode (opcode 0x9F). Send this after
a direct-mode CV op so the CS returns to normal operation and
layout traffic resumes. Reply is ! on success.
260def direct_mode_write_cv(cv: int, value: int) -> NceCommand: 261 """Write a CV on the programming track (direct mode). 262 263 JMRI: ``NceMessage.getWriteDirectCV`` (opcode 0xA8, WRITE_DIR_CV_CMD). 264 Layout: ``[0xA8, cv_hi, cv_lo, value]``. 265 """ 266 if not 1 <= cv <= 1024: 267 raise ValueError(f"CV must be 1..1024, got {cv}") 268 if not 0 <= value <= 255: 269 raise ValueError(f"value must be 0..255, got {value}") 270 return NceCommand( 271 name=f"WR_CV_DIRECT({cv},{value})", 272 request=bytes([OP_WR_CV_DIRECT_MODE, (cv >> 8) & 0xFF, cv & 0xFF, value]), 273 timeout_s=6.0, # service-mode writes take seconds 274 )
Write a CV on the programming track (direct mode).
JMRI: NceMessage.getWriteDirectCV (opcode 0xA8, WRITE_DIR_CV_CMD).
Layout: [0xA8, cv_hi, cv_lo, value].
277def direct_mode_read_cv(cv: int) -> NceCommand: 278 """Read a CV on the programming track. 279 280 JMRI: ``NceMessage.getReadDirectCV`` (opcode 0xA9, READ_DIR_CV_CMD). 281 Reply is 1 byte (the CV value). 282 """ 283 if not 1 <= cv <= 1024: 284 raise ValueError(f"CV must be 1..1024, got {cv}") 285 return NceCommand( 286 name=f"RD_CV_DIRECT({cv})", 287 request=bytes([OP_RD_CV_DIRECT_MODE, (cv >> 8) & 0xFF, cv & 0xFF]), 288 reply_len=1, 289 timeout_s=6.0, 290 is_read=True, 291 )
Read a CV on the programming track.
JMRI: NceMessage.getReadDirectCV (opcode 0xA9, READ_DIR_CV_CMD).
Reply is 1 byte (the CV value).
294def ops_mode_write_cv( 295 loco_address: int, cv: int, value: int, *, long_address: bool = False, 296) -> NceCommand: 297 """Ops-mode CV byte-write (Programming On the Main) to a specific loco. 298 299 JMRI: ``NceMessage.sendPacketMessage`` builds the equivalent wrapped 300 packet in ``NceProgrammer.writeCV`` when ops mode is selected. 301 302 Wire form: build a raw NMRA CV Access instruction packet, then wrap 303 it in the appropriate ``QUEUEn_TMP_CMD`` envelope where ``n`` is the 304 packet's byte length (opcode ``0x93 + (packet_len - 3)`` covers 305 3..6-byte packets, per :data:`OP_QUEUE_PACKET_BASE`). NCE has no 306 native ops-mode CV *read* (RailCom is out of PowerPro's scope) — do 307 not add ``ops_mode_read_cv``; use direct mode for verified reads. 308 309 NMRA S-9.2.1 CV Access — Long Form / Byte Write: 310 311 <address bytes> 1110 CCVV VVVVVVVV DDDDDDDD <XOR> 312 313 * short address (1..127): 1 address byte ``0AAAAAAA`` 314 * long address (128..9999): 2 address bytes ``11AAAAAA AAAAAAAA`` 315 * ``CC = 11`` → byte-write (10 = verify, 01 = bit-manipulate) 316 * ``VVVVVVVVVV`` = CV number minus 1, 10-bit 317 * ``DDDDDDDD`` = value 318 * trailing XOR of all preceding bytes 319 320 Reply: single ACK byte from the CS (``!`` on success). The packet 321 goes on the wire from the CS; no decoder ack round-trip. 322 """ 323 if not 1 <= cv <= 1024: 324 raise ValueError(f"CV must be 1..1024, got {cv}") 325 if not 0 <= value <= 255: 326 raise ValueError(f"value must be 0..255, got {value}") 327 328 if long_address: 329 if not 128 <= loco_address <= 9999: 330 raise ValueError( 331 f"long address must be 128..9999, got {loco_address}" 332 ) 333 # 14-bit long address in the DCC packet: top 6 bits go into the 334 # low 6 bits of 0xC0, low 8 bits stand alone. 335 addr_bytes = [ 336 0xC0 | ((loco_address >> 8) & 0x3F), 337 loco_address & 0xFF, 338 ] 339 else: 340 if not 1 <= loco_address <= 127: 341 raise ValueError( 342 f"short address must be 1..127, got {loco_address}" 343 ) 344 addr_bytes = [loco_address & 0x7F] 345 346 # NMRA encodes CV number minus one, spread over two bytes: 2 high 347 # bits ride the instruction opcode, 8 low bits follow. 348 cv_index = cv - 1 349 inst_hi = 0xEC | ((cv_index >> 8) & 0x03) # 1110_1100 with CC=11 + VV 350 inst_lo = cv_index & 0xFF 351 352 packet = bytes(addr_bytes + [inst_hi, inst_lo, value & 0xFF]) 353 xor = 0 354 for b in packet: 355 xor ^= b 356 dcc_packet = packet + bytes([xor]) 357 358 # QUEUEn_TMP_CMD wire form (verified against JMRI Java NCE Command 359 # Monitor trace on real PowerPro hardware, 2026-07-25): 360 # 361 # [queue_opcode] [send_count] [dcc_packet_bytes...] 362 # 363 # * ``queue_opcode`` = 0x93..0x96 (packet length 3..6) 364 # * ``send_count`` = number of times the CS retransmits the 365 # packet on the wire. NMRA POM recommends 366 # 3–5 repeats for reliability; JMRI Java uses 367 # 5, which we mirror. The CS reads this byte 368 # as part of the command — earlier versions 369 # of this code omitted it, so the CS would 370 # read our first packet byte as the send 371 # count and then time out ~5 s waiting for a 372 # packet byte that never came, returning 373 # NoAck (``0``). 374 # * ``dcc_packet`` = 3..6 bytes: address + CV Access instruction 375 # + XOR checksum, per NMRA S-9.2.1. 376 packet_len = len(dcc_packet) 377 if not 3 <= packet_len <= 6: 378 # Defensive: the packet builder should never produce a length 379 # outside 3..6, but if a future edit changes the encoding this 380 # catches the mistake before the wrong opcode goes out. 381 raise ValueError( 382 f"internal error: DCC packet length {packet_len} outside 3..6", 383 ) 384 queue_opcode = OP_QUEUE_PACKET_BASE + (packet_len - 3) 385 send_count = 5 # matches JMRI Java default 386 387 return NceCommand( 388 name=f"WR_CV_OPS({loco_address}{'L' if long_address else 'S'},{cv},{value})", 389 request=bytes([queue_opcode, send_count]) + dcc_packet, 390 # Ops-mode writes are fire-and-forget on the wire; only the CS 391 # queue-accept ACK ('!') comes back. No data bytes. 392 # 393 # ``timeout_s`` is 5.0 s — matches JMRI Java's timeout for 394 # equivalent operations. NCE PowerPro's actual queue-then-ack 395 # latency depends on the CS's current DCC output load: with 396 # active throttles + fast-clock ticks + accessory refresh, we 397 # observed >2 s on real hardware even after bumping from the 398 # 0.5 s default. 5 s gives comfortable headroom; a real hang 399 # (packet dropped, CS off-line) still surfaces before the user 400 # gives up on the batch. 401 timeout_s=5.0, 402 )
Ops-mode CV byte-write (Programming On the Main) to a specific loco.
JMRI: NceMessage.sendPacketMessage builds the equivalent wrapped
packet in NceProgrammer.writeCV when ops mode is selected.
Wire form: build a raw NMRA CV Access instruction packet, then wrap
it in the appropriate QUEUEn_TMP_CMD envelope where n is the
packet's byte length (opcode 0x93 + (packet_len - 3) covers
3..6-byte packets, per OP_QUEUE_PACKET_BASE). NCE has no
native ops-mode CV read (RailCom is out of PowerPro's scope) — do
not add ops_mode_read_cv; use direct mode for verified reads.
NMRA S-9.2.1 CV Access — Long Form / Byte Write:
<address bytes> 1110 CCVV VVVVVVVV DDDDDDDD <XOR>
- short address (1..127): 1 address byte
0AAAAAAA - long address (128..9999): 2 address bytes
11AAAAAA AAAAAAAA CC = 11→ byte-write (10 = verify, 01 = bit-manipulate)VVVVVVVVVV= CV number minus 1, 10-bitDDDDDDDD= value- trailing XOR of all preceding bytes
Reply: single ACK byte from the CS (! on success). The packet
goes on the wire from the CS; no decoder ack round-trip.
405def read_clock() -> NceCommand: 406 """Read the CS fast clock (16 bytes at ``CS_CLOCK_ADDR``). 407 408 JMRI: ``NceClockControl.issueReadOnlyRequest`` uses 409 ``NceBinaryCommand.accMemoryRead(getClockAddr())`` (opcode 0x8F). 410 Wire request: 3 bytes ``[0x8F, addr_hi, addr_lo]``; reply is 16 411 data bytes. Passive read — safe to send even when the clock isn't 412 running. 413 414 Reply byte layout mirrors ``NceClockControl.readClockPacket``: 415 416 * ``[0]`` scale — CS-internal divisor, ``ratio = 250 / scale`` 417 * ``[1]`` tick 418 * ``[2]`` seconds (binary, not BCD) 419 * ``[3]`` minutes 420 * ``[4]`` hours (1..12 in 12h mode; 0..23 in 24h) 421 * ``[5]`` AM/PM flag (ASCII ``'A'`` = 65 → AM) 422 * ``[6]`` 12/24 mode (1 = 24h, else 12h) 423 * ``[13]`` status 424 425 1.5 s timeout (vs the 0.5 s default) leaves headroom on slow USB-serial 426 adapters where a tightly-scheduled read can otherwise time out while 427 its 16 bytes are still in flight. 428 """ 429 return NceCommand( 430 name="READ_CLOCK", 431 request=bytes([ 432 OP_READ_16_MEM, 433 (CS_CLOCK_ADDR >> 8) & 0xFF, CS_CLOCK_ADDR & 0xFF, 434 ]), 435 reply_len=16, 436 is_read=True, 437 timeout_s=1.5, 438 )
Read the CS fast clock (16 bytes at CS_CLOCK_ADDR).
JMRI: NceClockControl.issueReadOnlyRequest uses
NceBinaryCommand.accMemoryRead(getClockAddr()) (opcode 0x8F).
Wire request: 3 bytes [0x8F, addr_hi, addr_lo]; reply is 16
data bytes. Passive read — safe to send even when the clock isn't
running.
Reply byte layout mirrors NceClockControl.readClockPacket:
[0]scale — CS-internal divisor,ratio = 250 / scale[1]tick[2]seconds (binary, not BCD)[3]minutes[4]hours (1..12 in 12h mode; 0..23 in 24h)[5]AM/PM flag (ASCII'A'= 65 → AM)[6]12/24 mode (1 = 24h, else 12h)[13]status
1.5 s timeout (vs the 0.5 s default) leaves headroom on slow USB-serial adapters where a tightly-scheduled read can otherwise time out while its 16 bytes are still in flight.
441def write_clock_time(hours: int, minutes: int, seconds: int) -> NceCommand: 442 """Write hh/mm/ss to the CS clock. 443 444 JMRI: ``NceClockControl.issueClockSetMem`` writes ``[ss, mm, hh]`` at 445 ``CS_CLOCK_ADDR + 2`` via ``accMemoryWriteN`` (opcode 0x8E, WRITE_N_CMD). 446 Delegates to :func:`write_memory` so the fixed-20-byte wire pad is 447 applied in one place. 1.5 s timeout mirrors the clock read. 448 """ 449 if not 0 <= hours <= 23: 450 raise ValueError(f"hours must be 0..23, got {hours}") 451 if not 0 <= minutes <= 59: 452 raise ValueError(f"minutes must be 0..59, got {minutes}") 453 if not 0 <= seconds <= 59: 454 raise ValueError(f"seconds must be 0..59, got {seconds}") 455 payload = bytes([seconds & 0xFF, minutes & 0xFF, hours & 0xFF]) 456 inner = write_memory(CS_CLOCK_ADDR + 2, payload) 457 # Reuse the well-formed request but with a clock-specific name + timeout. 458 return NceCommand( 459 name=f"WRITE_CLOCK_TIME({hours:02d}:{minutes:02d}:{seconds:02d})", 460 request=inner.request, 461 timeout_s=1.5, 462 )
Write hh/mm/ss to the CS clock.
JMRI: NceClockControl.issueClockSetMem writes [ss, mm, hh] at
CS_CLOCK_ADDR + 2 via accMemoryWriteN (opcode 0x8E, WRITE_N_CMD).
Delegates to write_memory() so the fixed-20-byte wire pad is
applied in one place. 1.5 s timeout mirrors the clock read.
465def set_clock_ratio(ratio: int) -> NceCommand: 466 """Set the CS fast-clock ratio (integer, 1..15). 467 468 JMRI: ``NceBinaryCommand.accSetClockRatio`` (opcode 0x87, CLOCK_RATIO_CMD). 469 Sends ``[0x87, ratio]``. Read-back later reports ``scale = 250 / ratio``. 470 """ 471 if not 1 <= ratio <= 15: 472 raise ValueError(f"clock ratio must be 1..15, got {ratio}") 473 return NceCommand( 474 name=f"SET_CLOCK_RATIO({ratio})", 475 request=bytes([OP_CLOCK_SET_RATIO, ratio & 0xFF]), 476 timeout_s=1.5, 477 )
Set the CS fast-clock ratio (integer, 1..15).
JMRI: NceBinaryCommand.accSetClockRatio (opcode 0x87, CLOCK_RATIO_CMD).
Sends [0x87, ratio]. Read-back later reports scale = 250 / ratio.
480def set_clock_1224(twenty_four_hour: bool) -> NceCommand: 481 """Switch the CS clock between 12h and 24h display. 482 483 JMRI: ``NceBinaryCommand.accSetClock1224`` (opcode 0x86, CLOCK_1224_CMD). 484 """ 485 return NceCommand( 486 name=f"SET_CLOCK_1224({'24' if twenty_four_hour else '12'})", 487 request=bytes([OP_CLOCK_SET_1224, 1 if twenty_four_hour else 0]), 488 timeout_s=1.5, 489 )
Switch the CS clock between 12h and 24h display.
JMRI: NceBinaryCommand.accSetClock1224 (opcode 0x86, CLOCK_1224_CMD).
492def start_clock() -> NceCommand: 493 """Start the CS fast clock. 494 495 JMRI: ``NceBinaryCommand.accStartClock`` (opcode 0x84, START_CLOCK_CMD). 496 """ 497 return NceCommand( 498 name="START_CLOCK", 499 request=bytes([OP_CLOCK_START]), 500 timeout_s=1.5, 501 )
Start the CS fast clock.
JMRI: NceBinaryCommand.accStartClock (opcode 0x84, START_CLOCK_CMD).
504def stop_clock() -> NceCommand: 505 """Stop (hold) the CS fast clock. 506 507 JMRI: ``NceBinaryCommand.accStopClock`` (opcode 0x83, STOP_CLOCK_CMD). 508 """ 509 return NceCommand( 510 name="STOP_CLOCK", 511 request=bytes([OP_CLOCK_STOP]), 512 timeout_s=1.5, 513 )
Stop (hold) the CS fast clock.
JMRI: NceBinaryCommand.accStopClock (opcode 0x83, STOP_CLOCK_CMD).
516def read_aiu(aiu_address: int) -> NceCommand: 517 """Read one AIU module's 16-bit input state. 518 519 JMRI: ``NceBinaryCommand.accAiu2Read`` (opcode 0x9B, READ_AUI2_CMD). 520 Returns the AIU's two-byte word: bits 0..13 are inputs 1..14. 521 """ 522 if not 1 <= aiu_address <= 63: 523 raise ValueError(f"AIU address must be 1..63, got {aiu_address}") 524 return NceCommand( 525 name=f"AIU_QUERY({aiu_address})", 526 request=bytes([OP_AIU_BASE_QUERY, aiu_address & 0xFF]), 527 reply_len=2, 528 is_read=True, 529 )
Read one AIU module's 16-bit input state.
JMRI: NceBinaryCommand.accAiu2Read (opcode 0x9B, READ_AUI2_CMD).
Returns the AIU's two-byte word: bits 0..13 are inputs 1..14.