fix: update event handling in MiniMateClient and protocol to ensure correct sequence for waveform downloads

This commit is contained in:
2026-04-06 13:19:51 -04:00
parent 41090a9346
commit d0d5a18d5c
3 changed files with 102 additions and 54 deletions
+18 -5
View File
@@ -194,20 +194,33 @@ there is only one event.
0A(key0) ← REQUIRED: establishes device context 0A(key0) ← REQUIRED: establishes device context
1E(token=0xFE) ← REQUIRED: arms device for 5A; CONFIRMED 4-2-26 + 4-3-26 1E(token=0xFE) ← REQUIRED: arms device for 5A; CONFIRMED 4-2-26 + 4-3-26
0C(key0) ← read waveform record 0C(key0) ← read waveform record
5A(key0) ← bulk stream (metadata or full waveform) 1F(all zeros / browse=True) → key1 ← MUST come BEFORE 5A (BW sequence, confirmed 4-2-26)
1F(all zeros / browse=True) → key1 POLL × 3 ← REQUIRED: 3 full POLL cycles before 5A (BW frames 68-73)
5A(key0) ← bulk stream; key0 used even though 1F already advanced
0A(key1) 0A(key1)
1E(token=0xFE) ← re-arm for next event's 5A 1E(token=0xFE) ← re-arm for next event's 5A
0C(key1) 0C(key1)
1F(all zeros) → key2
POLL × 3
5A(key1) 5A(key1)
1F(all zeros) → null ← done 1F(all zeros) → null ← done
``` ```
**The 1E(token=0xFE) arm step is the root cause of the 5A timeout (FIXED 2026-04-06):** **The 1E(token=0xFE) arm step is required (FIXED 2026-04-06):**
The device silently ignores all 5A probe frames unless a second SUB 1E with token=0xFE The device silently ignores all 5A probe frames unless a second SUB 1E with token=0xFE
has been issued between 0A and 0C. This step is present in EVERY download cycle in both has been issued between 0A and 0C. This step is present in EVERY download cycle in both
the 4-2-26 and 4-3-26 BW TX captures. Without it, `read_bulk_waveform_stream()` blocks the 4-2-26 and 4-3-26 BW TX captures.
for the full 120 s timeout.
**1F must come BEFORE 5A (FIXED 2026-04-06):**
BW always calls 1F (advance event) before starting the 5A bulk stream. 5A still uses the
pre-advance key — the device streams the waveform for the key that was set up with 0A+1E-arm+0C
even after 1F has moved the internal pointer to the next event.
**POLL × 3 required before 5A (FIXED 2026-04-06):**
BW sends exactly 3 complete POLL (SUB 5B) probe+data cycles between the last 1F and the
first 5A probe frame. Confirmed from 4-2-26 BW TX capture frames 68-73. Without these
POLLs the device does not respond to the 5A probe. Use `proto.poll()` (not `startup()`
`startup()` drains the boot string, which is only needed on initial connect).
`advance_event(browse=True)` sends all-zero params; `advance_event()` default (browse=False) `advance_event(browse=True)` sends all-zero params; `advance_event()` default (browse=False)
sends token=0xFE and is NOT used by any caller. sends token=0xFE and is NOT used by any caller.
+61 -49
View File
@@ -277,68 +277,85 @@ class MiniMateClient:
idx = 0 idx = 0
while data8[4:8] != b"\x00\x00\x00\x00": while data8[4:8] != b"\x00\x00\x00\x00":
log.info("get_events: record %d key=%s", idx, key4.hex()) cur_key = key4 # key for this event's 0A/1E-arm/0C/5A calls
log.info("get_events: record %d key=%s", idx, cur_key.hex())
ev = Event(index=idx) ev = Event(index=idx)
ev._waveform_key = key4 # stored so download_waveform() can re-use it ev._waveform_key = cur_key
# Always call 0A before 0C to establish device waveform context. # SUB 0A — MUST be called first to establish device waveform context.
# The device requires 0A context for both 0C and the subsequent 1F. # Required before 0C, 1E-arm, and 1F.
# (Earlier code skipped 0A for events after the first — confirmed wrong.)
proceed = True proceed = True
try: try:
_hdr, rec_len = proto.read_waveform_header(key4) _hdr, rec_len = proto.read_waveform_header(cur_key)
if rec_len < 0x30: if rec_len < 0x30:
log.warning( log.warning(
"get_events: key=%s is partial (len=0x%02X) — skipping", "get_events: key=%s is partial (len=0x%02X) — skipping",
key4.hex(), rec_len, cur_key.hex(), rec_len,
) )
proceed = False proceed = False
except ProtocolError as exc: except ProtocolError as exc:
log.warning( log.warning(
"get_events: 0A failed for key=%s: %s — skipping 0C", "get_events: 0A failed for key=%s: %s — skipping",
key4.hex(), exc, cur_key.hex(), exc,
) )
proceed = False proceed = False
if proceed: if proceed:
# SUB 1E (download-arm) — MUST be sent between 0A and 0C. # SUB 1E (download-arm) — MUST be sent between 0A and 0C.
# The device ignores 5A probe frames unless this second 1E with # Device ignores 5A probe frames without this second 1E(token=0xFE).
# token=0xFE has been issued first. Confirmed from both 4-2-26 # Confirmed from both 4-2-26 and 4-3-26 BW TX captures (2026-04-06).
# and 4-3-26 BW TX captures (2026-04-06). log.info("get_events: 1E download-arm (token=0xFE) for key=%s", cur_key.hex())
# The returned key is the same event key we already hold — ignore it.
log.info("get_events: 1E download-arm (token=0xFE) for key=%s", key4.hex())
try: try:
proto.read_event_first(token=0xFE) proto.read_event_first(token=0xFE)
log.info("get_events: 1E download-arm OK") log.info("get_events: 1E download-arm OK")
except ProtocolError as exc: except ProtocolError as exc:
log.warning( log.warning(
"get_events: 1E download-arm failed for key=%s: %s", "get_events: 1E download-arm failed for key=%s: %s",
key4.hex(), exc, cur_key.hex(), exc,
) )
# SUB 0C — full waveform record (peak values, timestamp, "Project:" string) # SUB 0C — full waveform record (peak values, timestamp, project string)
try: try:
record = proto.read_waveform_record(key4) record = proto.read_waveform_record(cur_key)
if debug: if debug:
ev._raw_record = record ev._raw_record = record
_decode_waveform_record_into(record, ev) _decode_waveform_record_into(record, ev)
except ProtocolError as exc: except ProtocolError as exc:
log.warning( log.warning(
"get_events: 0C failed for key=%s: %s", key4.hex(), exc "get_events: 0C failed for key=%s: %s", cur_key.hex(), exc
) )
# SUB 5A — bulk waveform stream. # SUB 1F — advance BEFORE 5A (matches BW sequence from 4-2-26 capture).
# By default (full_waveform=False): stop early after frame 7 ("Project:") # Save next key now; 5A will still use cur_key.
# is found — fetches only ~8 frames for event-time metadata. try:
# When full_waveform=True: fetch the complete stream (stop_after_metadata=False, key4, data8 = proto.advance_event(browse=True)
# max_chunks=128) and decode raw ADC samples into ev.raw_samples. log.info(
"get_events: 1F → key=%s trailing=%s",
key4.hex(), data8[4:8].hex(),
)
except ProtocolError as exc:
log.warning("get_events: 1F failed: %s — stopping after this event", exc)
key4, data8 = b"\x00\x00\x00\x00", b"\x00\x00\x00\x00\x00\x00\x00\x00"
# POLL × 3 — BW sends 3 full POLL cycles between 1F and 5A.
# Confirmed from 4-2-26 BW TX capture (frames 68-73 before 5A at 74).
log.info("get_events: POLL × 3 before 5A")
for _p in range(3):
try:
proto.poll()
except ProtocolError as exc:
log.warning("get_events: POLL %d failed: %s", _p, exc)
# SUB 5A — bulk waveform stream (uses cur_key, NOT the advanced key4).
# By default (full_waveform=False): stop after frame 7 for metadata only.
# When full_waveform=True: fetch all chunks and decode raw ADC samples.
try: try:
if full_waveform: if full_waveform:
log.info( log.info(
"get_events: 5A full waveform download for key=%s", key4.hex() "get_events: 5A full waveform download for key=%s", cur_key.hex()
) )
a5_frames = proto.read_bulk_waveform_stream( a5_frames = proto.read_bulk_waveform_stream(
key4, stop_after_metadata=False, max_chunks=128 cur_key, stop_after_metadata=False, max_chunks=128
) )
if a5_frames: if a5_frames:
_decode_a5_metadata_into(a5_frames, ev) _decode_a5_metadata_into(a5_frames, ev)
@@ -348,8 +365,11 @@ class MiniMateClient:
len((ev.raw_samples or {}).get("Tran", [])), len((ev.raw_samples or {}).get("Tran", [])),
) )
else: else:
log.info(
"get_events: 5A metadata-only download for key=%s", cur_key.hex()
)
a5_frames = proto.read_bulk_waveform_stream( a5_frames = proto.read_bulk_waveform_stream(
key4, stop_after_metadata=True cur_key, stop_after_metadata=True
) )
if a5_frames: if a5_frames:
_decode_a5_metadata_into(a5_frames, ev) _decode_a5_metadata_into(a5_frames, ev)
@@ -360,36 +380,28 @@ class MiniMateClient:
) )
except ProtocolError as exc: except ProtocolError as exc:
log.warning( log.warning(
"get_events: 5A failed for key=%s: %s event-time metadata unavailable", "get_events: 5A failed for key=%s: %s — metadata unavailable",
key4.hex(), exc, cur_key.hex(), exc,
) )
# Include all full records regardless of sub_code / record_type.
# Partial records (proceed=False, rec_len < 0x30 or 0A failed) are
# the only thing we skip — we have no data to decode for those.
if proceed:
events.append(ev) events.append(ev)
idx += 1 idx += 1
else:
log.info(
"get_events: key=%s — skipping partial/failed record (rec_len < 0x30)",
key4.hex(),
)
# SUB 1F — advance to the next record key. else:
# Uses browse mode (all-zero params) — empirically confirmed to work # Partial/failed record — skip 5A, just advance with 1F.
# with 2+ events stored (4-3-26 browse-mode capture). BW itself uses
# token=0xFE here in download mode, but browse mode is simpler and
# avoids complications with the download-arm state machine.
try:
key4, data8 = proto.advance_event(browse=True)
log.info( log.info(
"get_events: 1F → key=%s trailing=%s", "get_events: key=%s — skipping partial/failed record",
key4.hex(), data8[4:8].hex(), cur_key.hex(),
) )
except ProtocolError as exc: try:
log.warning("get_events: 1F failed: %s — stopping iteration", exc) key4, data8 = proto.advance_event(browse=True)
break log.info(
"get_events: 1F → key=%s trailing=%s",
key4.hex(), data8[4:8].hex(),
)
except ProtocolError as exc:
log.warning("get_events: 1F failed: %s — stopping iteration", exc)
break
log.info("get_events: downloaded %d event(s)", len(events)) log.info("get_events: downloaded %d event(s)", len(events))
return events return events
+23
View File
@@ -241,6 +241,29 @@ class MiniMateProtocol:
log.debug("read SUB=0x%02X: received %d data bytes", sub, len(data_rsp.data)) log.debug("read SUB=0x%02X: received %d data bytes", sub, len(data_rsp.data))
return data_rsp.data return data_rsp.data
def poll(self) -> S3Frame:
"""
Send a single POLL (SUB 5B) probe+data cycle and return the data response.
This is a bare POLL cycle with no boot-string drain — use during an active
session (contrast with startup(), which drains the "Operating System" boot
string first).
Confirmed from 4-2-26 BW TX capture: BW sends exactly 3 of these POLL
cycles between the last 1F and the first 5A probe frame during every
waveform download. Without them the device ignores the 5A probe.
"""
self._send(POLL_PROBE)
self._recv_one(
expected_sub=_expected_rsp_sub(SUB_POLL),
timeout=self._recv_timeout,
)
self._send(POLL_DATA)
return self._recv_one(
expected_sub=_expected_rsp_sub(SUB_POLL),
timeout=self._recv_timeout,
)
def send_keepalive(self) -> None: def send_keepalive(self) -> None:
""" """
Send a single POLL_PROBE keepalive without waiting for a response. Send a single POLL_PROBE keepalive without waiting for a response.