protocol: send 3-step data requests for full compliance config (SUB 1A)

Reverse-engineered the full Blastware 4-frame sequence for SUB 1A:
  A (probe):      offset=0x0000, params[7]=0x64
  B (data req 1): offset=0x0400, params[2]=0x00, params[7]=0x64  → bytes 0..1023
  C (data req 2): offset=0x0400, params[2]=0x04, params[7]=0x64  → bytes 1024..2047
  D (data req 3): offset=0x002A, params[2]=0x08, params[7]=0x64  → bytes 2048..2089

We were only sending A+D and getting 44 bytes (the last chunk).
Now sends B, C, D in sequence; each E5 response has 11-byte echo header
stripped, and chunks are concatenated.  Devices that return all data in
one frame (BE18189 style) are handled — timeouts on B/C are skipped
gracefully and data from D still accumulates.

Total expected: 0x0400 + 0x0400 + 0x002A = 0x082A = 2090 bytes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Harrison
2026-04-01 15:08:50 -04:00
parent a5069c302d
commit eee1e36a1b

View File

@@ -436,67 +436,66 @@ class MiniMateProtocol:
log.debug("read_compliance_config: 1A data request offset=0x2A params[2]=0x08")
self._send(build_bw_frame(SUB_COMPLIANCE, 0x2A, _DATA_PARAMS))
# ── Accumulate E5 frames ──────────────────────────────────────────────
# BE18189 sends one large frame; BE11529 may chunk into many small ones.
# Loop until target bytes received or inter-frame gap expires.
_TARGET = 0x082A # 2090 bytes — known total from BW captures
_INTER_FRAME_TIMEOUT = 2.0 # seconds; give up if no new frame within this
# ── Multi-request accumulation ────────────────────────────────────────
#
# Full BW sequence (confirmed from raw_bw captures 3-11-26):
#
# Frame B: offset=0x0400 params[2]=0x00 → requests cfg bytes 0..1023
# Frame C: offset=0x0400 params[2]=0x04 → requests cfg bytes 1024..2047
# Frame D: offset=0x002A params[2]=0x08 → requests cfg bytes 2048..2089
#
# Total: 0x0400 + 0x0400 + 0x002A = 0x082A = 2090 bytes.
#
# The "offset" field in B and C encodes the chunk length (0x0400 = 1024),
# not a byte offset into the config. params[2] tracks cumulative pages
# (0x00 → 0x04 → 0x08; each page = 256 bytes → 0x04 pages = 1024 bytes).
#
# Each request gets its own E5 response with an 11-byte echo header.
# Devices that send the full block in a single frame (BE18189) may return
# the entire config from the last request alone — we handle both cases by
# trying each step and concatenating whatever arrives.
_DATA_PARAMS_B = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00])
_DATA_PARAMS_C = bytes([0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00])
# _DATA_PARAMS_D already built above as _DATA_PARAMS
_STEPS = [
("B", 0x0400, _DATA_PARAMS_B),
("C", 0x0400, _DATA_PARAMS_C),
("D", 0x002A, _DATA_PARAMS), # _DATA_PARAMS built above
]
config = bytearray()
frame_count = 0
while len(config) < _TARGET:
timeout = self._recv_timeout if frame_count == 0 else _INTER_FRAME_TIMEOUT
# reset_parser=True only for the first frame; subsequent frames must not
# discard bytes already buffered from the device's chunked response stream.
for step_name, step_offset, step_params in _STEPS:
log.debug(
"read_compliance_config: sending frame %s offset=0x%04X params=%s",
step_name, step_offset, step_params.hex(),
)
self._send(build_bw_frame(SUB_COMPLIANCE, step_offset, step_params))
try:
data_rsp = self._recv_one(
expected_sub=rsp_sub,
timeout=timeout,
reset_parser=(frame_count == 0),
)
data_rsp = self._recv_one(expected_sub=rsp_sub)
except TimeoutError:
if frame_count == 0:
raise # hard fail — device never responded at all
# Inter-frame gap expired — device finished sending
log.warning(
"read_compliance_config: inter-frame timeout after %d frames "
"(%d / %d cfg bytes received)",
frame_count, len(config), _TARGET,
"read_compliance_config: frame %s — no E5 response (timeout); "
"device may return all data on a later request",
step_name,
)
break
continue
frame_count += 1
if frame_count == 1:
# First frame: strip 11-byte echo header (length + key echo + padding)
chunk = data_rsp.data[11:]
log.warning(
"read_compliance_config: frame %d page=0x%04X "
"data=%d cfg_chunk=%d (header stripped)",
frame_count, data_rsp.page_key, len(data_rsp.data), len(chunk),
"read_compliance_config: frame %s page=0x%04X data=%d "
"cfg_chunk=%d running_total=%d",
step_name, data_rsp.page_key, len(data_rsp.data),
len(chunk), len(config) + len(chunk),
)
else:
# Subsequent frames: log raw prefix to determine structure,
# then strip the same 11-byte header (assumption — verify from logs).
raw = data_rsp.data
log.warning(
"read_compliance_config: frame %d page=0x%04X data=%d "
"raw[0:16]=%s",
frame_count, data_rsp.page_key, len(raw),
raw[:16].hex() if len(raw) >= 16 else raw.hex(),
)
# Strip header if frame is long enough; otherwise accumulate all
chunk = raw[11:] if len(raw) > 11 else raw
config.extend(chunk)
log.warning(
"read_compliance_config: running total=%d / %d",
len(config), _TARGET,
)
log.warning(
"read_compliance_config: done — %d frame(s), %d cfg bytes total",
frame_count, len(config),
"read_compliance_config: done — %d cfg bytes total",
len(config),
)
# Hex dump first 128 bytes for field mapping