"""Shared live-keyboard protocol constants and packing helpers. Two layers share this module: * The legacy BLE live client (ble_live.py) — inner frame layout only. * The ESP-NOW mesh transport (mesh_link.py / mesh_manager.py) — inner frame layout PLUS the plaintext mesh transport header and the binary USB-CDC hub framing. Inner (encrypted) frame layout — byte-for-byte the historic BLE live protocol, so device-side dispatch is identical on both transports: byte 0: msg_type bytes 1..8: session_id (uint64 LE) bytes 9..16: seq (uint64 LE) bytes 17..: body per msg_type Mesh transport header (plaintext, precedes the encrypted envelope): off 0 u8 magic 0xE5 off 1 u8 type (MESH_T_*) off 2 u8 flags bit0 = retransmission off 3 u8 rsvd off 4 u32LE seq reliable seq (DATA) / move seq (DATA_U) / 0 off 8 u8[6] dest FF:FF:FF:FF:FF:FF = all nodes, else one STA MAC off 14 ... payload Hub CDC framing (host <-> the USB-attached hub device): 0xC8 0x35 | htype u8 | len u16LE | payload | crc16-ccitt u16LE (CRC over htype, len bytes, payload — must match serial_protocol.h) """ from __future__ import annotations import struct # ---- Inner message types (host -> device unless noted) ---- MSG_START = 0x01 MSG_KEYS = 0x02 MSG_STOP = 0x03 MSG_IDENTIFY = 0x04 MSG_MOUSE = 0x05 MSG_LABEL = 0x06 MSG_PAUSE = 0x07 # mesh-only: mute one node (it keeps ACKing the stream) MSG_RESUME = 0x08 # mesh-only: unmute MSG_ACK = 0x10 # device -> host (BLE only; mesh ACKs at transport level) MSG_ERROR = 0x11 # device -> host MSG_HELLO = 0x12 # device -> host (BLE only) ACTION_DOWN = 0 ACTION_UP = 1 ERR_LABELS = { 1: "BUFFER_FULL", 2: "NOT_LIVE_MODE", 3: "HID_FAILURE", 4: "BAD_MSG", } # ---- Mesh transport ---- MESH_MAGIC = 0xE5 MESH_HDR_LEN = 14 MESH_MAX_FRAME = 250 # ESP-NOW v1 payload cap; keeps node buffers small MESH_T_DATA = 0x01 # hub -> all (broadcast, reliable lane) MESH_T_DATA_U = 0x02 # hub -> all (latest-wins mouse-move lane) MESH_T_JOIN = 0x03 # hub -> node (per-device key) MESH_T_POLL = 0x04 # hub -> all (keepalive / discovery) MESH_T_BEACON = 0x81 # node -> hub (plaintext identity) MESH_T_ACK = 0x82 # node -> hub (consumed by hub; host sees acktab) MESH_T_JOIN_ACK = 0x83 # node -> hub (per-device key, forwarded to host) MESH_T_NACK = 0x84 # node -> hub (consumed by hub) MESH_T_ERR = 0x85 # node -> hub (forwarded to host) MESH_F_RETX = 0x01 BCAST_MAC = b"\xFF\xFF\xFF\xFF\xFF\xFF" # ---- Hub CDC framing ---- HUB_MAGIC0 = 0xC8 HUB_MAGIC1 = 0x35 HUB_H2D_SEND = 0x01 HUB_D2H_RX = 0x81 HUB_D2H_ACKTAB = 0x82 HUB_MAX_FRAME = 1500 def crc16_ccitt(data: bytes, crc: int = 0xFFFF) -> int: """CRC16-CCITT, poly 0x1021, init 0xFFFF — matches hubCrc16() in firmware/MacroPad/serial_protocol.h.""" for b in data: crc ^= b << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 \ else (crc << 1) & 0xFFFF return crc def mac_to_bytes(mac: str) -> bytes: return bytes(int(p, 16) for p in mac.split(":")) def mac_to_str(b: bytes) -> str: return ":".join(f"{x:02X}" for x in b[:6]) def pack_header(msg_type: int, session_id: int, seq: int) -> bytes: """Inner 17-byte header (identical to ble_live._pack_header).""" return struct.pack(" bytes: """``batch`` is a list of (action, hid, t_ms) tuples (max 16).""" return bytes([len(batch)]) + b"".join( struct.pack(" bytes: w = max(-127, min(127, int(wheel))) return struct.pack(" bytes: return struct.pack(" bytes: """Wrap a payload in the binary CDC framing for the hub.""" hdr = bytes([htype, len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) crc = crc16_ccitt(payload, crc16_ccitt(hdr)) return (bytes([HUB_MAGIC0, HUB_MAGIC1]) + hdr + payload + bytes([crc & 0xFF, (crc >> 8) & 0xFF]))