#!/usr/bin/env python3 """Зеркало бинарного протокола из src/link_protocol.h (единственный источник истины на стороне Python). Кадрирование поверх потока, CRC16-CCITT, упаковка и разбор CONTROL/IDENT/HELLO/LOG, инкрементальный парсер с ресинхронизацией. При правке src/link_protocol.h меняй ЭТОТ файл и сверяйся: python3 tools/link_proto.py --selftest """ import struct import sys SYNC0 = 0xA5 SYNC1 = 0x5A VERSION = 1 MAX_PAYLOAD = 64 # типы кадров HELLO = 0x03 IDENT = 0x02 CONTROL = 0x01 LOG = 0x04 # роли ROLE_TX = ord("T") ROLE_RX = ord("R") # флаги CONTROL CTRL_FLAG_FAILSAFE = 0x01 CONTROL_LEN = 4 + 4 + 1 + 2 * 16 # 41 IDENT_LEN = 1 + 1 + 2 + 12 # 16 SBUS_MID = 1024 # тайминги (мс), зеркало link_protocol.h FRAME_PERIOD_MS = 50 RX_USB_TIMEOUT_MS = 400 def crc16(data: bytes) -> int: """CRC16-CCITT, poly 0x1021, init 0xFFFF — как linkCrc16 в прошивке.""" crc = 0xFFFF for byte in data: crc ^= byte << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else (crc << 1) & 0xFFFF return crc def build_frame(frame_type: int, payload: bytes) -> bytes: """Собрать кадр: SYNC + VER + TYPE + LEN + PAYLOAD + CRC16(LE).""" if len(payload) > MAX_PAYLOAD: raise ValueError("payload too long") body = bytes((VERSION, frame_type, len(payload))) + payload crc = crc16(body) return bytes((SYNC0, SYNC1)) + body + struct.pack(" bytes: """Полезная нагрузка CONTROL. ch — 16 значений 0..2047.""" if len(ch) != 16: raise ValueError("need 16 channels") return struct.pack(" dict(seq, t_ms, flags, ch[16]).""" if len(payload) != CONTROL_LEN: raise ValueError("bad CONTROL length") seq, t_ms, flags = struct.unpack_from(" dict(role, proto_ver, fw_ver, uid).""" if len(payload) != IDENT_LEN: raise ValueError("bad IDENT length") role, proto_ver, fw_ver = struct.unpack_from(" str: return {ROLE_TX: "tx", ROLE_RX: "rx"}.get(role, "?") def neutral_control(seq: int = 0, t_ms: int = 0, failsafe: bool = True) -> bytes: """Нейтральный CONTROL (все каналы 1024) — для сетевого failsafe на RX.""" flags = CTRL_FLAG_FAILSAFE if failsafe else 0 return pack_control(seq, t_ms, flags, [SBUS_MID] * 16) class FrameParser: """Инкрементальный разбор потока. Скорми feed() любые байты — вернёт список (type, payload) полных валидных кадров. Устойчив к мусору и потере байт: при сбое CRC/версии сдвигается на байт и ищет следующий SYNC.""" def __init__(self): self._buf = bytearray() def feed(self, data: bytes): self._buf.extend(data) out = [] while True: frame = self._try_one() if frame is None: break out.append(frame) return out def _try_one(self): buf = self._buf while True: # найти синхрослово while len(buf) >= 2 and not (buf[0] == SYNC0 and buf[1] == SYNC1): del buf[0] if len(buf) < 5: # SYNC(2)+VER+TYPE+LEN return None ver, ftype, length = buf[2], buf[3], buf[4] if ver != VERSION or length > MAX_PAYLOAD: del buf[0] # ложный синк — сдвиг и поиск заново в этом же вызове continue total = 5 + length + 2 if len(buf) < total: return None body = bytes(buf[2:5 + length]) want = struct.unpack_from(" валидный извлекается сразу (ресинхрон # не прерывает разбор остатка буфера) p3 = FrameParser() got = p3.feed(bytes(bad) + frame) assert len(got) == 1 and got[0][1] == payload, got # известный CRC-вектор для сверки с прошивкой assert crc16(b"123456789") == 0x29B1, hex(crc16(b"123456789")) print("link_proto selftest OK") if __name__ == "__main__": if "--selftest" in sys.argv: _selftest() else: print(__doc__)