jmri_loconet.opcodes

LocoNet opcode constants and helpers.

LocoNet message bytes have the high bit set on opcodes (0x80–0xFF) and clear on data bytes (0x00–0x7F). Bits 5–6 of the opcode encode the message length class:

0b00 → 2 bytes (opcode + checksum)
0b01 → 4 bytes
0b10 → 6 bytes
0b11 → variable: the byte *after* the opcode is the total length

Checksum = 0xFF XOR of every preceding byte. This module exposes the opcodes JMRI's LnConstants declares, plus length_class_of() and expected_length().

 1"""LocoNet opcode constants and helpers.
 2
 3LocoNet message bytes have the high bit set on opcodes (``0x80–0xFF``) and
 4clear on data bytes (``0x00–0x7F``). Bits 5–6 of the opcode encode the
 5message length class:
 6
 7    0b00 → 2 bytes (opcode + checksum)
 8    0b01 → 4 bytes
 9    0b10 → 6 bytes
10    0b11 → variable: the byte *after* the opcode is the total length
11
12Checksum = ``0xFF XOR`` of every preceding byte. This module exposes the
13opcodes JMRI's ``LnConstants`` declares, plus :func:`length_class_of` and
14:func:`expected_length`.
15"""
16from __future__ import annotations
17
18# 2-byte messages -----------------------------------------------------------
19OPC_BUSY       = 0x81  # master is busy
20OPC_GPOFF      = 0x82  # global power off
21OPC_GPON       = 0x83  # global power on
22OPC_IDLE       = 0x85  # force idle
23
24# 4-byte messages -----------------------------------------------------------
25OPC_LOCO_SPD   = 0xA0  # set loco speed
26OPC_LOCO_DIRF  = 0xA1  # set loco direction + F0..F4
27OPC_LOCO_SND   = 0xA2  # set loco F5..F8
28OPC_SW_REQ     = 0xB0  # switch request (turnout)
29OPC_SW_REP     = 0xB1  # switch report (turnout feedback)
30OPC_INPUT_REP  = 0xB2  # input report (sensor)
31OPC_LONG_ACK   = 0xB4  # long-form acknowledgment
32OPC_SLOT_STAT1 = 0xB5  # write slot status
33OPC_CONSIST_FUNC = 0xB6
34OPC_UNLINK_SLOTS = 0xB8
35OPC_LINK_SLOTS   = 0xB9
36OPC_MOVE_SLOTS   = 0xBA  # move slots (used to dispatch / release)
37OPC_RQ_SL_DATA   = 0xBB  # request slot data
38OPC_SW_STATE     = 0xBC
39OPC_SW_ACK       = 0xBD
40OPC_LOCO_ADR     = 0xBF  # request slot for a DCC address
41
42# Variable-length messages --------------------------------------------------
43OPC_PEER_XFER  = 0xE5  # peer transfer (programmer / utility)
44OPC_SL_RD_DATA = 0xE7  # slot read data (14-byte response to OPC_LOCO_ADR)
45OPC_IMM_PACKET = 0xED  # immediate DCC packet
46OPC_WR_SL_DATA = 0xEF  # write slot data (14-byte)
47
48
49# Slot status constants (high nibble of slot stat byte) ---------------------
50LOCO_FREE      = 0x00
51LOCO_COMMON    = 0x10
52LOCO_IDLE      = 0x20
53LOCO_IN_USE    = 0x30
54LOCO_STAT_MASK = 0x30
55
56DEC_MODE_128         = 0x07  # 128 step decoder
57DEC_MODE_28          = 0x02  # 28 step decoder
58DEC_MODE_14          = 0x01  # 14 step decoder
59DEC_MODE_128A        = 0x06  # 128 step + advanced consist
60DEC_MODE_28A         = 0x03  # 28 step + advanced consist
61
62
63# Length helpers -----------------------------------------------------------
64
65
66def length_class_of(opcode: int) -> int:
67    """Return 0, 1, 2, or 3 for the length-class bits of *opcode*."""
68    return (opcode >> 5) & 0b11
69
70
71def expected_length(opcode: int) -> int | None:
72    """Bytes including the opcode and checksum for a fixed-length frame.
73
74    Returns ``None`` for variable-length opcodes (length class 3) where the
75    length lives in the byte after the opcode.
76    """
77    cls = length_class_of(opcode)
78    if cls == 0b00:
79        return 2
80    if cls == 0b01:
81        return 4
82    if cls == 0b10:
83        return 6
84    return None
85
86
87def is_valid_opcode_byte(b: int) -> bool:
88    """True if *b* could be the first byte of a LocoNet message."""
89    return 0x80 <= b <= 0xFF
OPC_BUSY = 129
OPC_GPOFF = 130
OPC_GPON = 131
OPC_IDLE = 133
OPC_LOCO_SPD = 160
OPC_LOCO_DIRF = 161
OPC_LOCO_SND = 162
OPC_SW_REQ = 176
OPC_SW_REP = 177
OPC_INPUT_REP = 178
OPC_LONG_ACK = 180
OPC_SLOT_STAT1 = 181
OPC_CONSIST_FUNC = 182
OPC_MOVE_SLOTS = 186
OPC_RQ_SL_DATA = 187
OPC_SW_STATE = 188
OPC_SW_ACK = 189
OPC_LOCO_ADR = 191
OPC_PEER_XFER = 229
OPC_SL_RD_DATA = 231
OPC_IMM_PACKET = 237
OPC_WR_SL_DATA = 239
LOCO_FREE = 0
LOCO_COMMON = 16
LOCO_IDLE = 32
LOCO_IN_USE = 48
LOCO_STAT_MASK = 48
DEC_MODE_128 = 7
DEC_MODE_28 = 2
DEC_MODE_14 = 1
DEC_MODE_128A = 6
DEC_MODE_28A = 3
def length_class_of(opcode: int) -> int:
67def length_class_of(opcode: int) -> int:
68    """Return 0, 1, 2, or 3 for the length-class bits of *opcode*."""
69    return (opcode >> 5) & 0b11

Return 0, 1, 2, or 3 for the length-class bits of opcode.

def expected_length(opcode: int) -> int | None:
72def expected_length(opcode: int) -> int | None:
73    """Bytes including the opcode and checksum for a fixed-length frame.
74
75    Returns ``None`` for variable-length opcodes (length class 3) where the
76    length lives in the byte after the opcode.
77    """
78    cls = length_class_of(opcode)
79    if cls == 0b00:
80        return 2
81    if cls == 0b01:
82        return 4
83    if cls == 0b10:
84        return 6
85    return None

Bytes including the opcode and checksum for a fixed-length frame.

Returns None for variable-length opcodes (length class 3) where the length lives in the byte after the opcode.

def is_valid_opcode_byte(b: int) -> bool:
88def is_valid_opcode_byte(b: int) -> bool:
89    """True if *b* could be the first byte of a LocoNet message."""
90    return 0x80 <= b <= 0xFF

True if b could be the first byte of a LocoNet message.