Initial public release
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""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("<BQQ", msg_type, session_id & 0xFFFFFFFFFFFFFFFF,
|
||||
seq & 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
|
||||
def pack_keys_body(batch) -> bytes:
|
||||
"""``batch`` is a list of (action, hid, t_ms) tuples (max 16)."""
|
||||
return bytes([len(batch)]) + b"".join(
|
||||
struct.pack("<BBI", a & 0xFF, h & 0xFF, t & 0xFFFFFFFF)
|
||||
for (a, h, t) in batch
|
||||
)
|
||||
|
||||
|
||||
def pack_mouse_body(buttons: int, x: int, y: int, wheel: int) -> bytes:
|
||||
w = max(-127, min(127, int(wheel)))
|
||||
return struct.pack("<BHHb", buttons & 0xFF, x & 0xFFFF, y & 0xFFFF, w)
|
||||
|
||||
|
||||
def pack_mesh_header(mtype: int, flags: int, seq: int, dest: bytes) -> bytes:
|
||||
return struct.pack("<BBBBI", MESH_MAGIC, mtype & 0xFF, flags & 0xFF, 0,
|
||||
seq & 0xFFFFFFFF) + dest[:6]
|
||||
|
||||
|
||||
def parse_mesh_header(frame: bytes):
|
||||
"""Return (type, flags, seq, dest_bytes, payload) or None."""
|
||||
if len(frame) < MESH_HDR_LEN or frame[0] != MESH_MAGIC:
|
||||
return None
|
||||
magic, mtype, flags, _rsvd, seq = struct.unpack("<BBBBI", frame[:8])
|
||||
return mtype, flags, seq, frame[8:14], frame[MESH_HDR_LEN:]
|
||||
|
||||
|
||||
def frame_hub_message(htype: int, payload: bytes) -> 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]))
|
||||
Reference in New Issue
Block a user