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)
def function_high_packet_dcc( *, address: int, long_address: bool, functions: list[bool], group: int) -> bytes:
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].

def wrap_in_imm_packet(dcc: bytes, *, repeat: int = 2) -> bytes:
 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).