Socket-transport cmd space (off_14 ids) from live capture: 1101/1307/1705/1707 + host_probe3

This commit is contained in:
kfj001 committed 2026-10-16 00:14:42 +00:00
1 parent 2cdc274fe3
commit 213a0518d6
7 files changed
+401 -60

No files matched your search

+107 -40
View File
@@ -1,12 +1,23 @@
"""Frame builders for the two byte transports.
* **BLE** — ``communication_center/bluetooth/bluetooth_frame.dart`` (see
``docs/RE_DART_FINDINGS.md`` §9). Frame layout and magics are confirmed;
the *exact* on-air order of the magic relative to the frame and the MTU
continuation scheme still need a live capture.
* **Local TCP socket** — ``communication_center/socket`` uses ``int16ToBytes``
/ ``bytesToInt16`` (little-endian) framing (see §8). A length-prefixed
frame is the working candidate; adjust once a capture is available.
``docs/RE_DART_FINDINGS.md`` §9). On-air layout, settled from
``createBluetoothFrame``::
magic[2] ++ crc[2] ++ int16LE(totalLen) ++ int16LE(sn) ++ content
* ``totalLen = 6 + len(content)`` (2 crc-slot + 2 len + 2 sn bytes).
* CRC-16/MODBUS over the buffer with ``00 00`` in the crc slots, rendered
as the app's swapped-lowercase 4-hex string; each hex *pair* is then run
through ``int.parse(pair, radix: 32)`` (literal ``0x20`` — quirky but
explicit) and written into slots 2..3.
* Magics: ``5A A5`` data (default), ``4A A4`` ack, ``2A A2`` MTU announce.
* The encrypted envelope (same bytes as the socket path) is split into
MTU-sized slices when longer than the negotiated MTU; every slice goes
out as its own frame with its own ``_increaseBluetoothSN()`` value.
The receiver dedups by sn, accumulates ``content`` parts and runs the
same ``decryptData`` over the concatenation.
* **Local TCP socket** — raw encrypted bytes, no framing (see §8).
"""
from __future__ import annotations
@@ -20,12 +31,79 @@ from .crc import crc16_hex_string, crc16_modbus, int16_to_le_bytes, le_bytes_to_
MAGIC_ACK = b"\x4a\xa4"
#: First two bytes of a **data** frame (receiving one triggers the app's ack).
MAGIC_DATA = b"\x5a\xa5"
#: Magic of the MTU-announce frame (``_sendMtu`` sends ``mtuToData()``).
MAGIC_MTU = b"\x2a\xa2"
MAGIC_NAMES = {
MAGIC_ACK[0:2]: "ack",
MAGIC_DATA[0:2]: "data",
MAGIC_MTU[0:2]: "mtu",
}
#: On-air header length: magic(2) + crc(2) + totalLen(2) + sn(2).
BLE_HEADER_LEN = 8
def _ble_crc_bytes(body_with_zero_slots: bytes) -> bytes:
"""CRC slot bytes the way ``createBluetoothFrame`` derives them.
CRC-16/MODBUS over the frame with ``00 00`` slots, swapped-lowercase
4-hex string, then ``int.parse(pair, radix: 32)`` per pair (the
disassembly materialises ``0x20`` for the radix). Values can exceed
one byte in theory; the GATT write path must narrow them, so mask
here (assumption — confirm against a capture).
"""
hexstr = crc16_hex_string(body_with_zero_slots)
return bytes(int(hexstr[i:i + 2], 32) & 0xFF for i in (0, 2))
def create_ble_frame(
content: bytes,
sn: int,
magic: bytes = MAGIC_DATA,
) -> bytes:
"""``createBluetoothFrame(content, magic, sn)`` — full on-air frame.
Layout::
magic ++ crc[2] ++ int16LE(6 + len(content)) ++ int16LE(sn) ++ content
"""
total = 6 + len(content)
buf = bytearray(
bytes(magic[:2])
+ b"\x00\x00"
+ int16_to_le_bytes(total)
+ int16_to_le_bytes(sn)
+ bytes(content)
)
buf[2:4] = _ble_crc_bytes(bytes(buf))
return bytes(buf)
def build_ble_data_frame(content: bytes, sn: int) -> bytes:
"""Full on-air data frame (magic ``0x5A 0xA5``)."""
return create_ble_frame(content, sn, MAGIC_DATA)
def build_ble_ack_frame(content: bytes, sn: int) -> bytes:
"""Full on-air ACK frame (magic ``0x4A 0xA4``)."""
return create_ble_frame(content, sn, MAGIC_ACK)
def magic_in_data(data: bytes) -> Optional[str]:
"""Return ``"ack"`` / ``"data"`` / ``"mtu"`` if known; else ``None``."""
return MAGIC_NAMES.get(bytes(data[:2]))
def ble_frame_sn(data: bytes) -> int:
"""``sn`` field of a frame (int16LE at offset 6)."""
return le_bytes_to_int16(data[6:8])
def ble_frame_total_len(data: bytes) -> int:
"""``totalLen`` field of a frame (int16LE at offset 4)."""
return le_bytes_to_int16(data[4:6])
# In createBluetoothFrame the CRC placeholder sits at byte offsets 2..5 of the
# frame body (content ++ [00 00 00 00] ++ int16LE(totalLen) ++ int16LE(value) ++ sn).
_CRC_SLOTS = slice(2, 4)
@@ -77,51 +155,40 @@ def magic_in_data(data: bytes) -> Optional[str]:
return MAGIC_NAMES.get(bytes(data[:2]))
def sn_in_data(data: bytes, sn: str) -> bool:
"""True if the frame (after any magic) ends with the device SN."""
sn_bytes = sn.encode("utf-8")
return data.endswith(sn_bytes)
def sn_in_data(data: bytes, sn: int) -> bool:
"""True if the frame's ``sn`` field equals *sn*."""
if len(data) < BLE_HEADER_LEN:
return False
return ble_frame_sn(data) == int(sn)
def content_in_data(data: bytes, sn: str) -> Optional[bytes]:
"""Extract the ``content`` bytes of a data/ack frame.
Layout after the 2-byte magic is fixed-size except for ``content``, so::
content = frame[2 : len(frame) - len(sn) - FRAME_TAIL_SIZE]
"""
body = data[2:] if len(data) >= 2 else data
sn_bytes = sn.encode("utf-8")
if not body.endswith(sn_bytes):
def content_in_data(data: bytes) -> Optional[bytes]:
"""Frame ``content`` (everything after the 8-byte header)."""
if len(data) < BLE_HEADER_LEN:
return None
end = len(body) - len(sn_bytes) - _FRAME_TAIL_SIZE
if end < 2: # need at least the 4-byte crc slots
return None
return body[2:end]
return bytes(data[BLE_HEADER_LEN:])
def verify_frame_crc(data: bytes) -> bool:
"""Recompute the CRC-16 over the frame body and compare slots 2..3."""
body = data[2:] if len(data) >= 2 else data
if len(body) < 4:
"""Recompute the CRC-16 over the frame (zeroed slots) and compare."""
if len(data) < BLE_HEADER_LEN:
return False
candidate = body[:]
candidate[2:4] = b"\x00\x00\x00\x00"
return bytes.fromhex(crc16_hex_string(bytes(candidate))) == body[2:4]
candidate = bytearray(data)
candidate[2:4] = b"\x00\x00"
return _ble_crc_bytes(bytes(candidate)) == bytes(data[2:4])
def chunk_for_mtu(frame: bytes, mtu: int = 20) -> List[bytes]:
"""Split a frame for GATT writes.
def chunk_for_mtu(encrypted: bytes, mtu: int = 20) -> List[bytes]:
"""Split the encrypted envelope into MTU-sized slices.
First chunk carries the 2-byte magic; the rest are raw continuation
chunks. The app's ``mtuToData`` does something similar — the exact
continuation framing (length headers? re-magic?) needs confirmation.
Mirrors the ``sendObject`` over-length branch (``ceil(len/mtu)`` slices
via ``sublist``): every slice is sent as its own BLE frame with its own
``_increaseBluetoothSN()`` value (see :func:`create_ble_frame`).
"""
if mtu <= 2:
raise ValueError("mtu must exceed the 2-byte magic")
if len(frame) <= mtu:
return [frame]
return [frame[i:i + mtu] for i in range(0, len(frame), mtu)]
if mtu <= 0:
raise ValueError("mtu must be positive")
return [bytes(encrypted[i:i + mtu])
for i in range(0, len(encrypted), mtu)] or [b""]
def magicEquals(a: bytes, b: bytes) -> bool: