jmri_loconet.dcc
NMRA DCC function-group packet encoding for OPC_IMM_PACKET.
LocoNet's OPC_IMM_PACKET (0xED) wraps a raw NMRA DCC packet for
the command station to send to the layout. We use this for function
groups F9..F28 because the slot-based OPC_LOCO_DIRF/OPC_LOCO_SND
opcodes only cover F0..F8.
This module duplicates the NMRA encoding from jmri_nce.dcc so the
two drivers stay decoupled. Both produce identical wire bytes (NMRA is
the standard); only the carrier opcode differs.
1"""NMRA DCC function-group packet encoding for OPC_IMM_PACKET. 2 3LocoNet's ``OPC_IMM_PACKET`` (``0xED``) wraps a raw NMRA DCC packet for 4the command station to send to the layout. We use this for function 5groups F9..F28 because the slot-based ``OPC_LOCO_DIRF``/``OPC_LOCO_SND`` 6opcodes only cover F0..F8. 7 8This module duplicates the NMRA encoding from :mod:`jmri_nce.dcc` so the 9two drivers stay decoupled. Both produce identical wire bytes (NMRA is 10the standard); only the carrier opcode differs. 11""" 12from __future__ import annotations 13 14 15def _address_bytes(address: int, long_address: bool) -> bytes: 16 if long_address: 17 if not 1 <= address <= 10239: 18 raise ValueError(f"long address out of range: {address}") 19 return bytes([0xC0 | ((address >> 8) & 0x3F), address & 0xFF]) 20 if not 1 <= address <= 127: 21 raise ValueError(f"short address out of range: {address}") 22 return bytes([address]) 23 24 25def _xor(packet: bytes) -> int: 26 out = 0 27 for b in packet: 28 out ^= b 29 return out 30 31 32def function_high_packet_dcc( 33 *, 34 address: int, 35 long_address: bool, 36 functions: list[bool], 37 group: int, 38) -> bytes: 39 """NMRA function-group 3 (F13..F20) or 4 (F21..F28) instruction. 40 41 Wire form: ``[addr…] [0xDE | 0xDF] [8-bit mask] [xor]``. 42 """ 43 addr = _address_bytes(address, long_address) 44 if group == 3: 45 opcode = 0xDE 46 bits = functions[13:21] 47 elif group == 4: 48 opcode = 0xDF 49 bits = functions[21:29] 50 else: 51 raise ValueError(f"function group must be 3 or 4, got {group}") 52 mask = 0 53 for i, on in enumerate(bits): 54 if on: 55 mask |= 1 << i 56 body = addr + bytes([opcode, mask]) 57 return body + bytes([_xor(body)]) 58 59 60def wrap_in_imm_packet(dcc: bytes, *, repeat: int = 2) -> bytes: 61 """Wrap a complete NMRA DCC packet in an ``OPC_IMM_PACKET`` frame. 62 63 Layout: ``0xED LEN 0x7F repeat_byte D[0]…D[n] checksum``. 64 65 The ``IM1`` byte (``0x7F``) and ``IM2`` (repeat encoding) follow the 66 Digitrax IMM packet spec. ``repeat`` is sent into ``IM2``'s low bits 67 (1–4 typical for function packets). The checksum is the standard 68 LocoNet XOR over the body — but this helper returns only the body 69 bytes; the traffic controller adds the checksum via its frame writer. 70 71 *dcc* must be a complete NMRA packet **including** the trailing XOR 72 error byte — we'll embed it as-is and strip the high bit per LocoNet 73 spec (DCC bytes go on the wire with high bits packed into IM1/IM2). 74 75 For Stage 9.1 we ship a simplified encoding: each DCC byte's high bit 76 is OR'd into a dedicated high-bit byte, and the low 7 bits go on the 77 wire directly. Sufficient for function packets where the DCC bytes 78 are typically 0x80–0xFF (high bit always set). 79 """ 80 if not 3 <= len(dcc) <= 5: 81 raise ValueError(f"IMM packet wraps 3..5 DCC bytes, got {len(dcc)}") 82 # Length byte: total frame length (including OPC + LEN + body + cs). 83 # IM1: bit 4 = number of repeats (low 3 bits), bits 0-3 = high bits of 84 # data bytes 0-3. JMRI's standard layout for accessory/function packets 85 # uses repeat=0x71 to mean "send twice". 86 body = bytearray([0xED, 0x0B, 0x7F]) 87 # repeat encoding: bits 4-6 = repeat count, bits 0-3 = high bits of 88 # data bytes (high bit of DCC byte i goes into bit i of IM2). 89 im2 = (repeat & 0x07) << 4 90 for i, b in enumerate(dcc): 91 if b & 0x80: 92 im2 |= (1 << i) & 0x0F 93 body.append(im2) 94 # Add the 7-bit data bytes (high bit cleared). 95 for b in dcc: 96 body.append(b & 0x7F) 97 # Pad to fixed 5 data bytes (LocoNet expects 11-byte fixed IMM packet body). 98 while len(body) < 10: 99 body.append(0x00) 100 return bytes(body)
33def function_high_packet_dcc( 34 *, 35 address: int, 36 long_address: bool, 37 functions: list[bool], 38 group: int, 39) -> bytes: 40 """NMRA function-group 3 (F13..F20) or 4 (F21..F28) instruction. 41 42 Wire form: ``[addr…] [0xDE | 0xDF] [8-bit mask] [xor]``. 43 """ 44 addr = _address_bytes(address, long_address) 45 if group == 3: 46 opcode = 0xDE 47 bits = functions[13:21] 48 elif group == 4: 49 opcode = 0xDF 50 bits = functions[21:29] 51 else: 52 raise ValueError(f"function group must be 3 or 4, got {group}") 53 mask = 0 54 for i, on in enumerate(bits): 55 if on: 56 mask |= 1 << i 57 body = addr + bytes([opcode, mask]) 58 return body + bytes([_xor(body)])
NMRA function-group 3 (F13..F20) or 4 (F21..F28) instruction.
Wire form: [addr…] [0xDE | 0xDF] [8-bit mask] [xor].
61def wrap_in_imm_packet(dcc: bytes, *, repeat: int = 2) -> bytes: 62 """Wrap a complete NMRA DCC packet in an ``OPC_IMM_PACKET`` frame. 63 64 Layout: ``0xED LEN 0x7F repeat_byte D[0]…D[n] checksum``. 65 66 The ``IM1`` byte (``0x7F``) and ``IM2`` (repeat encoding) follow the 67 Digitrax IMM packet spec. ``repeat`` is sent into ``IM2``'s low bits 68 (1–4 typical for function packets). The checksum is the standard 69 LocoNet XOR over the body — but this helper returns only the body 70 bytes; the traffic controller adds the checksum via its frame writer. 71 72 *dcc* must be a complete NMRA packet **including** the trailing XOR 73 error byte — we'll embed it as-is and strip the high bit per LocoNet 74 spec (DCC bytes go on the wire with high bits packed into IM1/IM2). 75 76 For Stage 9.1 we ship a simplified encoding: each DCC byte's high bit 77 is OR'd into a dedicated high-bit byte, and the low 7 bits go on the 78 wire directly. Sufficient for function packets where the DCC bytes 79 are typically 0x80–0xFF (high bit always set). 80 """ 81 if not 3 <= len(dcc) <= 5: 82 raise ValueError(f"IMM packet wraps 3..5 DCC bytes, got {len(dcc)}") 83 # Length byte: total frame length (including OPC + LEN + body + cs). 84 # IM1: bit 4 = number of repeats (low 3 bits), bits 0-3 = high bits of 85 # data bytes 0-3. JMRI's standard layout for accessory/function packets 86 # uses repeat=0x71 to mean "send twice". 87 body = bytearray([0xED, 0x0B, 0x7F]) 88 # repeat encoding: bits 4-6 = repeat count, bits 0-3 = high bits of 89 # data bytes (high bit of DCC byte i goes into bit i of IM2). 90 im2 = (repeat & 0x07) << 4 91 for i, b in enumerate(dcc): 92 if b & 0x80: 93 im2 |= (1 << i) & 0x0F 94 body.append(im2) 95 # Add the 7-bit data bytes (high bit cleared). 96 for b in dcc: 97 body.append(b & 0x7F) 98 # Pad to fixed 5 data bytes (LocoNet expects 11-byte fixed IMM packet body). 99 while len(body) < 10: 100 body.append(0x00) 101 return bytes(body)
Wrap a complete NMRA DCC packet in an OPC_IMM_PACKET frame.
Layout: 0xED LEN 0x7F repeat_byte D[0]…D[n] checksum.
The IM1 byte (0x7F) and IM2 (repeat encoding) follow the
Digitrax IMM packet spec. repeat is sent into IM2's low bits
(1–4 typical for function packets). The checksum is the standard
LocoNet XOR over the body — but this helper returns only the body
bytes; the traffic controller adds the checksum via its frame writer.
dcc must be a complete NMRA packet including the trailing XOR error byte — we'll embed it as-is and strip the high bit per LocoNet spec (DCC bytes go on the wire with high bits packed into IM1/IM2).
For Stage 9.1 we ship a simplified encoding: each DCC byte's high bit is OR'd into a dedicated high-bit byte, and the low 7 bits go on the wire directly. Sufficient for function packets where the DCC bytes are typically 0x80–0xFF (high bit always set).