Correct wire format to disassembly: raw S1||crypt(S2), radix 32, int snno/seconds, decryptData response parser, --probe

- envelope: 7 fixed keys + conditional dataArea, no data key; int
  type/snno, seconds timestamp, data setter auto len/crc (CRC32 upper);
  SequenceNumber 1..9999 (increaseSN)
- crypto: encrypt is plaintext-header-first a||transform(b), key radix 32
  at both tryParse sites; decrypt_data ports decryptData (dual-0x58
  markers, s+'}' equipNo probe, s+','+T return); decrypt_request kept
  for the device-side known-header split
- socket: raw send (no length prefix), accumulate-and-try receive
- battery: queryAPowerData shape (data={'update':2}), radix default 32,
  --probe prints exact TX/RX bytes per variant; device_local.send_probe
- docs: RE_DART_FINDINGS ss3/5/8 + open items, BATTERY_POC host recipe
- tests: 19 new protocol tests, suite 44/44 green
This commit is contained in:
kfj001 committed 2026-10-15 23:01:58 +00:00
1 parent 2438a04ed5
commit 9e69f07636
10 files changed
+744 -182

No files matched your search

+38 -16
View File
@@ -11,6 +11,7 @@
from __future__ import annotations
import json
from typing import List, Optional, Tuple
from .crc import crc16_hex_string, crc16_modbus, int16_to_le_bytes, le_bytes_to_int16
@@ -130,30 +131,51 @@ def magicEquals(a: bytes, b: bytes) -> bool:
# --------------------------------------------------------------------- socket
def frame_socket_payload(payload: bytes) -> bytes:
"""Length-prefixed local-socket frame (candidate — §8).
"""Local-socket frame: the app sends the encrypted envelope **raw**.
``int16LE(len(payload)) || payload`` — the app's socket layer frames with
the same little-endian int16 helpers.
``ClientSocket.send`` forwards its argument straight to
``dart:io _Socket.add`` with no length prefix, markers, or handshake
(see §8); ``SocketManager._sendData`` only clears the receive buffer
afterwards. This helper is therefore the identity (kept so call sites
read like the app).
"""
if len(payload) > 0xFFFF:
raise ValueError("socket payload exceeds the int16 length field")
return int16_to_le_bytes(len(payload)) + bytes(payload)
return bytes(payload)
class SocketFrameDecoder:
"""Reassembles length-prefixed socket frames from a byte stream."""
"""Accumulating socket receiver (``SocketManager._onSocketReceiveData``).
Each incoming chunk is appended to a growable buffer and ``decrypt_data``
is attempted over the whole buffer (inside try/catch in the app); a
non-empty decoded string that parses as JSON is one complete reply, at
which point the buffer is cleared. Use :meth:`feed` for raw chunks and
:meth:`feed_json` when you want parsed envelopes back.
"""
def __init__(self) -> None:
self._buf = bytearray()
def feed(self, data: bytes) -> List[bytes]:
"""Append *data*; return a list of complete payloads (may be empty)."""
def feed(self, data: bytes) -> List[str]:
"""Append *data*; return a list of complete decoded JSON strings."""
from .crypto import decrypt_data # local import: cheap, no cycles
self._buf.extend(data)
out: List[bytes] = []
while len(self._buf) >= 2:
length = le_bytes_to_int16(bytes(self._buf[:2]))
if len(self._buf) < 2 + length:
break
out.append(bytes(self._buf[2:2 + length]))
del self._buf[2 + length]
decoded = decrypt_data(bytes(self._buf))
if not decoded:
return []
try:
json.loads(decoded)
except ValueError:
return []
del self._buf[:]
return [decoded]
# Backwards-compatible alias for the old length-prefixed probing API.
def feed_frames(self, data: bytes) -> List[bytes]:
"""Append *data*; return complete **raw wire** buffers (legacy)."""
self._buf.extend(data)
if not self._buf:
return []
out = [bytes(self._buf)]
del self._buf[:]
return out