jmri_ncemonitor.messages
Typed Cab Bus observation messages.
The NCE Cab Bus has many message types; this module covers the ones a
monitor user actually wants to see decoded. Anything else surfaces as
CabBusUnknown carrying the raw bytes so callers don't lose it.
Addresses observed on the bus:
0x00..0x3F(with high bit set on the wire →0x80..0xBF) — cab IDs.0x60..0x7F— fast clock, accessory packets, master traffic.
1"""Typed Cab Bus observation messages. 2 3The NCE Cab Bus has many message types; this module covers the ones a 4monitor user actually wants to see decoded. Anything else surfaces as 5:class:`CabBusUnknown` carrying the raw bytes so callers don't lose it. 6 7Addresses observed on the bus: 8 9* ``0x00..0x3F`` (with high bit set on the wire → ``0x80..0xBF``) — cab IDs. 10* ``0x60..0x7F`` — fast clock, accessory packets, master traffic. 11""" 12from __future__ import annotations 13 14from dataclasses import dataclass 15 16from jmri_ncemonitor.codec import address_of, is_address_byte 17 18# --- base + unknown --------------------------------------------------------- 19 20 21class CabBusMessage: 22 """Marker base for every decoded message.""" 23 address: int 24 25 26@dataclass(frozen=True, slots=True) 27class CabBusUnknown(CabBusMessage): 28 """A well-formed frame whose body we don't have a typed decoder for.""" 29 address: int 30 data: bytes # raw bytes (excluding the address marker byte itself) 31 32 33# --- cab traffic ------------------------------------------------------------ 34 35 36@dataclass(frozen=True, slots=True) 37class CabPoll(CabBusMessage): 38 """Master polling cab *address* for its current state. 39 40 Data is the 1-byte poll command (typically 0x00 = "send state"). 41 """ 42 address: int 43 command: int 44 45 46@dataclass(frozen=True, slots=True) 47class CabResponse(CabBusMessage): 48 """Cab *address* responding with its keyboard / encoder state. 49 50 Fields are decoded from the standard 8-byte response payload: 51 52 * ``key`` — last key code pressed (0 if none) 53 * ``speed`` — encoder speed value 0..127 54 * ``direction`` — 0 = reverse, 1 = forward 55 * ``function_low`` — bit-packed F0..F4 56 * ``function_high`` — bit-packed F5..F8 57 """ 58 address: int 59 key: int 60 speed: int 61 direction: int 62 function_low: int 63 function_high: int 64 65 66# --- master traffic --------------------------------------------------------- 67 68 69# Master "fake address" used for non-cab messages on the bus. 70ADDR_FAST_CLOCK = 0x60 71ADDR_ACCESSORY = 0x68 72ADDR_AIU_UPDATE = 0x70 73 74 75@dataclass(frozen=True, slots=True) 76class FastClockUpdate(CabBusMessage): 77 """Fast-clock packet observed on the bus.""" 78 address: int # always ADDR_FAST_CLOCK 79 hour: int 80 minute: int 81 82 83@dataclass(frozen=True, slots=True) 84class AccessoryObserved(CabBusMessage): 85 """An accessory-decoder command seen on the bus. 86 87 The monitor captures the address that was thrown/closed and which way. 88 """ 89 address: int # always ADDR_ACCESSORY 90 accessory_address: int 91 closed: bool 92 93 94@dataclass(frozen=True, slots=True) 95class AiuUpdate(CabBusMessage): 96 """An AIU input update observed on the bus.""" 97 address: int # always ADDR_AIU_UPDATE 98 aiu_module: int 99 input_word: int # 16-bit raw input word 100 101 102# --- new observable messages (Stage 4.5) ----------------------------------- 103 104 105ADDR_CONSIST_CMD = 0x79 # consist add/remove observed on master → cab 106ADDR_MACRO_CMD = 0x7B # accessory macro triggered observed 107ADDR_MASTER_ACK = 0x7C # master acknowledgement of cab-sent command 108ADDR_THROTTLE_CMD = 0x78 # master-side throttle commands 109ADDR_PROG_RESP = 0x7A # programming response (ops + service mode) 110 111 112@dataclass(frozen=True, slots=True) 113class ThrottleObserved(CabBusMessage): 114 """A throttle command seen on the bus. 115 116 Observable form (master → cab broadcast): 117 118 * ``dcc_address`` — loco address the command targets 119 * ``speed`` — 0..127 wire form, 1 = e-stop 120 * ``forward`` — direction bit 121 * ``functions`` — 5-bit F0..F4 mask (the common short form) 122 """ 123 address: int # always ADDR_THROTTLE_CMD 124 dcc_address: int 125 speed: int 126 forward: bool 127 functions: int # F0..F4 packed 128 129 130@dataclass(frozen=True, slots=True) 131class ProgrammingResponse(CabBusMessage): 132 """A response to a programming-track read seen on the bus. 133 134 Carries the CV number being read/written and the value byte. The 135 direction of the operation (read vs write) is encoded in the 136 ``operation`` byte the CS sends along. 137 """ 138 address: int # always ADDR_PROG_RESP 139 cv: int 140 value: int 141 operation: int # 0 = read result, 1 = write ack, 2 = error 142 143 144@dataclass(frozen=True, slots=True) 145class ConsistObserved(CabBusMessage): 146 """A consist add / remove observed on the bus. 147 148 ``operation`` is 0 for add-to-consist, 1 for remove-from-consist, 149 2 for kill-consist. Address bytes carry the consist number 150 (1..127; matches the CV19 wire range). 151 """ 152 address: int # always ADDR_CONSIST_CMD 153 consist_number: int 154 loco_address: int 155 operation: int 156 157 158@dataclass(frozen=True, slots=True) 159class MacroObserved(CabBusMessage): 160 """A macro trigger observed on the bus. 161 162 Macros are pre-programmed sequences of accessory operations the 163 command station holds in memory. This message captures which 164 macro number was fired. 165 """ 166 address: int # always ADDR_MACRO_CMD 167 macro_number: int 168 169 170@dataclass(frozen=True, slots=True) 171class MasterAck(CabBusMessage): 172 """An acknowledgement byte from the master to a cab. 173 174 The master emits these one-byte ACKs after processing a cab 175 command. ``code`` is the raw ack code — non-zero values indicate 176 an error condition specific to whatever the cab last sent. 177 """ 178 address: int # always ADDR_MASTER_ACK 179 code: int 180 181 182# --- decode ----------------------------------------------------------------- 183 184 185def decode(frame: bytes) -> CabBusMessage: 186 """Decode one Cab Bus frame into a typed message. 187 188 *frame* starts with the address byte (high bit set). Everything after 189 is body. Returns :class:`CabBusUnknown` for unrecognized frames. 190 """ 191 if not frame or not is_address_byte(frame[0]): 192 raise ValueError("Cab Bus frame must start with an address byte") 193 addr = address_of(frame[0]) 194 body = frame[1:] 195 196 # Cab range 197 if addr < 0x40: 198 if len(body) == 1: 199 return CabPoll(address=addr, command=body[0]) 200 # JMRI's Cab Bus reference documents a fixed 9-byte cab 201 # response frame (5 state bytes + 3 pad + 1 checksum). 202 # Require the full length so short frames fall through to 203 # CabBusUnknown instead of silently succeeding on truncated 204 # data — the monitor operator wants to see truncation, not 205 # a plausible-looking half-decoded message. 206 if len(body) >= 9: 207 return CabResponse( 208 address=addr, 209 key=body[0], 210 speed=body[1] & 0x7F, 211 direction=(body[2] >> 0) & 0x01, 212 function_low=body[3] & 0x1F, 213 function_high=body[4] & 0x0F, 214 ) 215 216 # Master-side traffic. 217 if addr == ADDR_FAST_CLOCK and len(body) >= 2: 218 return FastClockUpdate(address=addr, hour=body[0] & 0x7F, minute=body[1] & 0x7F) 219 220 if addr == ADDR_ACCESSORY and len(body) >= 3: 221 accy_addr = ((body[0] & 0x7F) << 7) | (body[1] & 0x7F) 222 closed = bool(body[2] & 0x01) 223 return AccessoryObserved( 224 address=addr, accessory_address=accy_addr + 1, closed=closed 225 ) 226 227 if addr == ADDR_AIU_UPDATE and len(body) >= 3: 228 return AiuUpdate( 229 address=addr, 230 aiu_module=body[0] & 0x7F, 231 input_word=((body[1] & 0x7F) << 7) | (body[2] & 0x7F), 232 ) 233 234 if addr == ADDR_THROTTLE_CMD and len(body) >= 4: 235 return ThrottleObserved( 236 address=addr, 237 dcc_address=((body[0] & 0x7F) << 7) | (body[1] & 0x7F), 238 speed=body[2] & 0x7F, 239 forward=bool(body[3] & 0x40), 240 functions=body[3] & 0x1F, 241 ) 242 243 if addr == ADDR_PROG_RESP and len(body) >= 4: 244 return ProgrammingResponse( 245 address=addr, 246 cv=((body[0] & 0x7F) << 7) | (body[1] & 0x7F), 247 value=body[2] & 0xFF, 248 operation=body[3] & 0x03, 249 ) 250 251 if addr == ADDR_CONSIST_CMD and len(body) >= 4: 252 return ConsistObserved( 253 address=addr, 254 consist_number=body[0] & 0x7F, 255 loco_address=((body[1] & 0x7F) << 7) | (body[2] & 0x7F), 256 operation=body[3] & 0x03, 257 ) 258 259 if addr == ADDR_MACRO_CMD and len(body) >= 1: 260 return MacroObserved( 261 address=addr, 262 macro_number=body[0] & 0x7F, 263 ) 264 265 if addr == ADDR_MASTER_ACK and len(body) >= 1: 266 return MasterAck( 267 address=addr, 268 code=body[0] & 0x7F, 269 ) 270 271 return CabBusUnknown(address=addr, data=body) 272 273 274def encode(msg: CabBusMessage) -> bytes: 275 """Inverse of :func:`decode` — used only by tests / synthetic playback. 276 277 The real monitor never writes to the bus (it's passive); we provide 278 ``encode`` to construct test fixtures that feed the monitor through a 279 loopback transport. 280 """ 281 if isinstance(msg, CabPoll): 282 return bytes([0x80 | msg.address, msg.command]) 283 if isinstance(msg, CabResponse): 284 # 9-byte body: 5 state bytes + 3 pad + 1 checksum. The 285 # checksum is the XOR of the 5 state bytes on the wire — we 286 # emit it here so encode() → decode() round-trips against 287 # the tightened 9-byte length check in ``decode``. 288 state = ( 289 msg.key & 0x7F, 290 msg.speed & 0x7F, 291 msg.direction & 0x7F, 292 msg.function_low & 0x7F, 293 msg.function_high & 0x7F, 294 ) 295 checksum = 0 296 for b in state: 297 checksum ^= b 298 return bytes([ 299 0x80 | msg.address, 300 *state, 301 0x00, 0x00, 0x00, 302 checksum & 0x7F, 303 ]) 304 if isinstance(msg, FastClockUpdate): 305 return bytes([0x80 | ADDR_FAST_CLOCK, msg.hour & 0x7F, msg.minute & 0x7F]) 306 if isinstance(msg, AccessoryObserved): 307 a = msg.accessory_address - 1 308 return bytes([ 309 0x80 | ADDR_ACCESSORY, 310 (a >> 7) & 0x7F, 311 a & 0x7F, 312 0x01 if msg.closed else 0x00, 313 ]) 314 if isinstance(msg, AiuUpdate): 315 return bytes([ 316 0x80 | ADDR_AIU_UPDATE, 317 msg.aiu_module & 0x7F, 318 (msg.input_word >> 7) & 0x7F, 319 msg.input_word & 0x7F, 320 ]) 321 if isinstance(msg, ThrottleObserved): 322 return bytes([ 323 0x80 | ADDR_THROTTLE_CMD, 324 (msg.dcc_address >> 7) & 0x7F, 325 msg.dcc_address & 0x7F, 326 msg.speed & 0x7F, 327 (msg.functions & 0x1F) | (0x40 if msg.forward else 0), 328 ]) 329 if isinstance(msg, ProgrammingResponse): 330 return bytes([ 331 0x80 | ADDR_PROG_RESP, 332 (msg.cv >> 7) & 0x7F, 333 msg.cv & 0x7F, 334 msg.value & 0x7F, 335 msg.operation & 0x7F, 336 ]) 337 if isinstance(msg, ConsistObserved): 338 return bytes([ 339 0x80 | ADDR_CONSIST_CMD, 340 msg.consist_number & 0x7F, 341 (msg.loco_address >> 7) & 0x7F, 342 msg.loco_address & 0x7F, 343 msg.operation & 0x7F, 344 ]) 345 if isinstance(msg, MacroObserved): 346 return bytes([ 347 0x80 | ADDR_MACRO_CMD, 348 msg.macro_number & 0x7F, 349 ]) 350 if isinstance(msg, MasterAck): 351 return bytes([ 352 0x80 | ADDR_MASTER_ACK, 353 msg.code & 0x7F, 354 ]) 355 if isinstance(msg, CabBusUnknown): 356 return bytes([0x80 | msg.address]) + msg.data 357 raise NotImplementedError(f"encode not implemented for {type(msg).__name__}")
Marker base for every decoded message.
27@dataclass(frozen=True, slots=True) 28class CabBusUnknown(CabBusMessage): 29 """A well-formed frame whose body we don't have a typed decoder for.""" 30 address: int 31 data: bytes # raw bytes (excluding the address marker byte itself)
A well-formed frame whose body we don't have a typed decoder for.
37@dataclass(frozen=True, slots=True) 38class CabPoll(CabBusMessage): 39 """Master polling cab *address* for its current state. 40 41 Data is the 1-byte poll command (typically 0x00 = "send state"). 42 """ 43 address: int 44 command: int
Master polling cab address for its current state.
Data is the 1-byte poll command (typically 0x00 = "send state").
47@dataclass(frozen=True, slots=True) 48class CabResponse(CabBusMessage): 49 """Cab *address* responding with its keyboard / encoder state. 50 51 Fields are decoded from the standard 8-byte response payload: 52 53 * ``key`` — last key code pressed (0 if none) 54 * ``speed`` — encoder speed value 0..127 55 * ``direction`` — 0 = reverse, 1 = forward 56 * ``function_low`` — bit-packed F0..F4 57 * ``function_high`` — bit-packed F5..F8 58 """ 59 address: int 60 key: int 61 speed: int 62 direction: int 63 function_low: int 64 function_high: int
Cab address responding with its keyboard / encoder state.
Fields are decoded from the standard 8-byte response payload:
key— last key code pressed (0 if none)speed— encoder speed value 0..127direction— 0 = reverse, 1 = forwardfunction_low— bit-packed F0..F4function_high— bit-packed F5..F8
76@dataclass(frozen=True, slots=True) 77class FastClockUpdate(CabBusMessage): 78 """Fast-clock packet observed on the bus.""" 79 address: int # always ADDR_FAST_CLOCK 80 hour: int 81 minute: int
Fast-clock packet observed on the bus.
84@dataclass(frozen=True, slots=True) 85class AccessoryObserved(CabBusMessage): 86 """An accessory-decoder command seen on the bus. 87 88 The monitor captures the address that was thrown/closed and which way. 89 """ 90 address: int # always ADDR_ACCESSORY 91 accessory_address: int 92 closed: bool
An accessory-decoder command seen on the bus.
The monitor captures the address that was thrown/closed and which way.
95@dataclass(frozen=True, slots=True) 96class AiuUpdate(CabBusMessage): 97 """An AIU input update observed on the bus.""" 98 address: int # always ADDR_AIU_UPDATE 99 aiu_module: int 100 input_word: int # 16-bit raw input word
An AIU input update observed on the bus.
113@dataclass(frozen=True, slots=True) 114class ThrottleObserved(CabBusMessage): 115 """A throttle command seen on the bus. 116 117 Observable form (master → cab broadcast): 118 119 * ``dcc_address`` — loco address the command targets 120 * ``speed`` — 0..127 wire form, 1 = e-stop 121 * ``forward`` — direction bit 122 * ``functions`` — 5-bit F0..F4 mask (the common short form) 123 """ 124 address: int # always ADDR_THROTTLE_CMD 125 dcc_address: int 126 speed: int 127 forward: bool 128 functions: int # F0..F4 packed
A throttle command seen on the bus.
Observable form (master → cab broadcast):
dcc_address— loco address the command targetsspeed— 0..127 wire form, 1 = e-stopforward— direction bitfunctions— 5-bit F0..F4 mask (the common short form)
131@dataclass(frozen=True, slots=True) 132class ProgrammingResponse(CabBusMessage): 133 """A response to a programming-track read seen on the bus. 134 135 Carries the CV number being read/written and the value byte. The 136 direction of the operation (read vs write) is encoded in the 137 ``operation`` byte the CS sends along. 138 """ 139 address: int # always ADDR_PROG_RESP 140 cv: int 141 value: int 142 operation: int # 0 = read result, 1 = write ack, 2 = error
A response to a programming-track read seen on the bus.
Carries the CV number being read/written and the value byte. The
direction of the operation (read vs write) is encoded in the
operation byte the CS sends along.
145@dataclass(frozen=True, slots=True) 146class ConsistObserved(CabBusMessage): 147 """A consist add / remove observed on the bus. 148 149 ``operation`` is 0 for add-to-consist, 1 for remove-from-consist, 150 2 for kill-consist. Address bytes carry the consist number 151 (1..127; matches the CV19 wire range). 152 """ 153 address: int # always ADDR_CONSIST_CMD 154 consist_number: int 155 loco_address: int 156 operation: int
A consist add / remove observed on the bus.
operation is 0 for add-to-consist, 1 for remove-from-consist,
2 for kill-consist. Address bytes carry the consist number
(1..127; matches the CV19 wire range).
159@dataclass(frozen=True, slots=True) 160class MacroObserved(CabBusMessage): 161 """A macro trigger observed on the bus. 162 163 Macros are pre-programmed sequences of accessory operations the 164 command station holds in memory. This message captures which 165 macro number was fired. 166 """ 167 address: int # always ADDR_MACRO_CMD 168 macro_number: int
A macro trigger observed on the bus.
Macros are pre-programmed sequences of accessory operations the command station holds in memory. This message captures which macro number was fired.
171@dataclass(frozen=True, slots=True) 172class MasterAck(CabBusMessage): 173 """An acknowledgement byte from the master to a cab. 174 175 The master emits these one-byte ACKs after processing a cab 176 command. ``code`` is the raw ack code — non-zero values indicate 177 an error condition specific to whatever the cab last sent. 178 """ 179 address: int # always ADDR_MASTER_ACK 180 code: int
An acknowledgement byte from the master to a cab.
The master emits these one-byte ACKs after processing a cab
command. code is the raw ack code — non-zero values indicate
an error condition specific to whatever the cab last sent.
186def decode(frame: bytes) -> CabBusMessage: 187 """Decode one Cab Bus frame into a typed message. 188 189 *frame* starts with the address byte (high bit set). Everything after 190 is body. Returns :class:`CabBusUnknown` for unrecognized frames. 191 """ 192 if not frame or not is_address_byte(frame[0]): 193 raise ValueError("Cab Bus frame must start with an address byte") 194 addr = address_of(frame[0]) 195 body = frame[1:] 196 197 # Cab range 198 if addr < 0x40: 199 if len(body) == 1: 200 return CabPoll(address=addr, command=body[0]) 201 # JMRI's Cab Bus reference documents a fixed 9-byte cab 202 # response frame (5 state bytes + 3 pad + 1 checksum). 203 # Require the full length so short frames fall through to 204 # CabBusUnknown instead of silently succeeding on truncated 205 # data — the monitor operator wants to see truncation, not 206 # a plausible-looking half-decoded message. 207 if len(body) >= 9: 208 return CabResponse( 209 address=addr, 210 key=body[0], 211 speed=body[1] & 0x7F, 212 direction=(body[2] >> 0) & 0x01, 213 function_low=body[3] & 0x1F, 214 function_high=body[4] & 0x0F, 215 ) 216 217 # Master-side traffic. 218 if addr == ADDR_FAST_CLOCK and len(body) >= 2: 219 return FastClockUpdate(address=addr, hour=body[0] & 0x7F, minute=body[1] & 0x7F) 220 221 if addr == ADDR_ACCESSORY and len(body) >= 3: 222 accy_addr = ((body[0] & 0x7F) << 7) | (body[1] & 0x7F) 223 closed = bool(body[2] & 0x01) 224 return AccessoryObserved( 225 address=addr, accessory_address=accy_addr + 1, closed=closed 226 ) 227 228 if addr == ADDR_AIU_UPDATE and len(body) >= 3: 229 return AiuUpdate( 230 address=addr, 231 aiu_module=body[0] & 0x7F, 232 input_word=((body[1] & 0x7F) << 7) | (body[2] & 0x7F), 233 ) 234 235 if addr == ADDR_THROTTLE_CMD and len(body) >= 4: 236 return ThrottleObserved( 237 address=addr, 238 dcc_address=((body[0] & 0x7F) << 7) | (body[1] & 0x7F), 239 speed=body[2] & 0x7F, 240 forward=bool(body[3] & 0x40), 241 functions=body[3] & 0x1F, 242 ) 243 244 if addr == ADDR_PROG_RESP and len(body) >= 4: 245 return ProgrammingResponse( 246 address=addr, 247 cv=((body[0] & 0x7F) << 7) | (body[1] & 0x7F), 248 value=body[2] & 0xFF, 249 operation=body[3] & 0x03, 250 ) 251 252 if addr == ADDR_CONSIST_CMD and len(body) >= 4: 253 return ConsistObserved( 254 address=addr, 255 consist_number=body[0] & 0x7F, 256 loco_address=((body[1] & 0x7F) << 7) | (body[2] & 0x7F), 257 operation=body[3] & 0x03, 258 ) 259 260 if addr == ADDR_MACRO_CMD and len(body) >= 1: 261 return MacroObserved( 262 address=addr, 263 macro_number=body[0] & 0x7F, 264 ) 265 266 if addr == ADDR_MASTER_ACK and len(body) >= 1: 267 return MasterAck( 268 address=addr, 269 code=body[0] & 0x7F, 270 ) 271 272 return CabBusUnknown(address=addr, data=body)
Decode one Cab Bus frame into a typed message.
frame starts with the address byte (high bit set). Everything after
is body. Returns CabBusUnknown for unrecognized frames.
275def encode(msg: CabBusMessage) -> bytes: 276 """Inverse of :func:`decode` — used only by tests / synthetic playback. 277 278 The real monitor never writes to the bus (it's passive); we provide 279 ``encode`` to construct test fixtures that feed the monitor through a 280 loopback transport. 281 """ 282 if isinstance(msg, CabPoll): 283 return bytes([0x80 | msg.address, msg.command]) 284 if isinstance(msg, CabResponse): 285 # 9-byte body: 5 state bytes + 3 pad + 1 checksum. The 286 # checksum is the XOR of the 5 state bytes on the wire — we 287 # emit it here so encode() → decode() round-trips against 288 # the tightened 9-byte length check in ``decode``. 289 state = ( 290 msg.key & 0x7F, 291 msg.speed & 0x7F, 292 msg.direction & 0x7F, 293 msg.function_low & 0x7F, 294 msg.function_high & 0x7F, 295 ) 296 checksum = 0 297 for b in state: 298 checksum ^= b 299 return bytes([ 300 0x80 | msg.address, 301 *state, 302 0x00, 0x00, 0x00, 303 checksum & 0x7F, 304 ]) 305 if isinstance(msg, FastClockUpdate): 306 return bytes([0x80 | ADDR_FAST_CLOCK, msg.hour & 0x7F, msg.minute & 0x7F]) 307 if isinstance(msg, AccessoryObserved): 308 a = msg.accessory_address - 1 309 return bytes([ 310 0x80 | ADDR_ACCESSORY, 311 (a >> 7) & 0x7F, 312 a & 0x7F, 313 0x01 if msg.closed else 0x00, 314 ]) 315 if isinstance(msg, AiuUpdate): 316 return bytes([ 317 0x80 | ADDR_AIU_UPDATE, 318 msg.aiu_module & 0x7F, 319 (msg.input_word >> 7) & 0x7F, 320 msg.input_word & 0x7F, 321 ]) 322 if isinstance(msg, ThrottleObserved): 323 return bytes([ 324 0x80 | ADDR_THROTTLE_CMD, 325 (msg.dcc_address >> 7) & 0x7F, 326 msg.dcc_address & 0x7F, 327 msg.speed & 0x7F, 328 (msg.functions & 0x1F) | (0x40 if msg.forward else 0), 329 ]) 330 if isinstance(msg, ProgrammingResponse): 331 return bytes([ 332 0x80 | ADDR_PROG_RESP, 333 (msg.cv >> 7) & 0x7F, 334 msg.cv & 0x7F, 335 msg.value & 0x7F, 336 msg.operation & 0x7F, 337 ]) 338 if isinstance(msg, ConsistObserved): 339 return bytes([ 340 0x80 | ADDR_CONSIST_CMD, 341 msg.consist_number & 0x7F, 342 (msg.loco_address >> 7) & 0x7F, 343 msg.loco_address & 0x7F, 344 msg.operation & 0x7F, 345 ]) 346 if isinstance(msg, MacroObserved): 347 return bytes([ 348 0x80 | ADDR_MACRO_CMD, 349 msg.macro_number & 0x7F, 350 ]) 351 if isinstance(msg, MasterAck): 352 return bytes([ 353 0x80 | ADDR_MASTER_ACK, 354 msg.code & 0x7F, 355 ]) 356 if isinstance(msg, CabBusUnknown): 357 return bytes([0x80 | msg.address]) + msg.data 358 raise NotImplementedError(f"encode not implemented for {type(msg).__name__}")