frame.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. """frame.py -- the USB-CDC wire protocol shared with OpenController/deck_input.cpp.
  2. frame = [0xA5][0x5A][type][len][payload(len)][sum8(type,len,payload...)]
  3. The reader resynchronizes on the 2-byte sync word, so any stray bytes (a boot banner, a serial
  4. monitor) are skipped harmlessly. Keep this file in lock-step with deck_input.{h,cpp}.
  5. """
  6. import struct
  7. SYNC0 = 0xA5
  8. SYNC1 = 0x5A
  9. # Deck -> nRF
  10. T_INPUT = 0x01
  11. T_CONTROL = 0x02
  12. T_REQ_STATUS = 0x03
  13. # nRF -> Deck
  14. T_STATUS = 0x10
  15. T_TEXT = 0x1F
  16. # INPUT payload: buttons u32, lx ly rx ry s16, lt rt u8, lpx lpy rpx rpy s16, ax ay az gx gy gz s16.
  17. # 4 + 8 + 2 + 8 + 12 = 34 bytes, little-endian. Matches applyInput() in deck_input.cpp.
  18. _INPUT = struct.Struct("<I4hBB4h6h")
  19. INPUT_LEN = _INPUT.size # 34
  20. def _sum8(type_, payload):
  21. s = (type_ + len(payload)) & 0xFF
  22. for b in payload:
  23. s = (s + b) & 0xFF
  24. return s
  25. def build(type_, payload=b""):
  26. """Serialize one frame."""
  27. payload = bytes(payload)
  28. return bytes([SYNC0, SYNC1, type_, len(payload)]) + payload + bytes([_sum8(type_, payload)])
  29. def build_input(st):
  30. """st: an object/dict with Triton fields -> an INPUT frame."""
  31. g = st.get if isinstance(st, dict) else (lambda k, d=0: getattr(st, k, d))
  32. payload = _INPUT.pack(
  33. g("buttons", 0) & 0xFFFFFFFF,
  34. g("lx", 0), g("ly", 0), g("rx", 0), g("ry", 0),
  35. g("lt", 0) & 0xFF, g("rt", 0) & 0xFF,
  36. g("lpx", 0), g("lpy", 0), g("rpx", 0), g("rpy", 0),
  37. g("ax", 0), g("ay", 0), g("az", 0), g("gx", 0), g("gy", 0), g("gz", 0),
  38. )
  39. return build(T_INPUT, payload)
  40. def build_set_forwarding(on):
  41. return build(T_CONTROL, bytes([0x01, 1 if on else 0]))
  42. def build_req_status():
  43. return build(T_REQ_STATUS)
  44. def parse_status(payload):
  45. """Decode a T_STATUS payload into a dict. Mirrors deckStatusTask() in deck_input.cpp."""
  46. if len(payload) < 4:
  47. return None
  48. flags, link_slot, sess_ch, nbond = payload[0], payload[1], payload[2], payload[3]
  49. bonds = []
  50. off = 4
  51. for _ in range(nbond):
  52. if off + 26 > len(payload):
  53. break
  54. used = payload[off]
  55. alive = payload[off + 1]
  56. puuid = struct.unpack_from("<I", payload, off + 2)[0]
  57. iuuid = struct.unpack_from("<I", payload, off + 6)[0]
  58. serial = payload[off + 10:off + 26].split(b"\x00")[0].decode("latin1", "replace")
  59. bonds.append({"used": bool(used), "alive": bool(alive),
  60. "puuid": puuid, "iuuid": iuuid, "serial": serial})
  61. off += 26
  62. return {
  63. "forwarding": bool(flags & 1),
  64. "link_up": bool(flags & 2),
  65. "link_slot": None if link_slot == 0xFF else link_slot,
  66. "sess_ch": sess_ch,
  67. "bonds": bonds,
  68. }
  69. class Reader:
  70. """Feed raw bytes; yields (type, payload) tuples for each complete, checksum-valid frame.
  71. Bytes that fall *between* frames are the firmware's plain-text `#` diagnostic lines (Serial.printf,
  72. which the framed protocol resyncs past). We capture those too and surface them as (T_TEXT, line) so
  73. the app can show the firmware log without a serial monitor.
  74. """
  75. def __init__(self):
  76. self._st = 0
  77. self._type = 0
  78. self._len = 0
  79. self._buf = bytearray()
  80. self._sum = 0
  81. self._text = bytearray()
  82. def _flush_text(self, out):
  83. line = self._text.rstrip(b"\r\n")
  84. self._text = bytearray()
  85. if line and all(9 <= b < 127 or b in (10, 13) for b in line):
  86. out.append((T_TEXT, bytes(line)))
  87. def feed(self, data):
  88. out = []
  89. for c in data:
  90. st = self._st
  91. if st == 0:
  92. if c == SYNC0:
  93. self._st = 1
  94. else:
  95. # inter-frame byte: accumulate the firmware's plain-text diagnostic lines
  96. self._text.append(c)
  97. if c == 0x0A or len(self._text) > 240:
  98. self._flush_text(out)
  99. elif st == 1:
  100. self._st = 2 if c == SYNC1 else (1 if c == SYNC0 else 0)
  101. elif st == 2:
  102. self._type = c
  103. self._sum = c
  104. self._st = 3
  105. elif st == 3:
  106. self._len = c
  107. self._sum = (self._sum + c) & 0xFF
  108. self._buf = bytearray()
  109. self._st = 5 if c == 0 else 4
  110. elif st == 4:
  111. self._buf.append(c)
  112. self._sum = (self._sum + c) & 0xFF
  113. if len(self._buf) >= self._len:
  114. self._st = 5
  115. elif st == 5:
  116. if c == self._sum:
  117. out.append((self._type, bytes(self._buf)))
  118. self._st = 0
  119. return out