fix: ensure proper waveform context by always calling 0A before 0C and use all-zero params for event advancement

This commit is contained in:
Brian Harrison
2026-04-03 18:48:04 -04:00
parent 6adf8b6078
commit 755050b347
2 changed files with 50 additions and 31 deletions

View File

@@ -137,14 +137,23 @@ then sends the termination frame.
### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce) ### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce)
**token_params bug:** The token byte was at `params[6]` (wrong). Both 3-31-26 and 4-3-26 **token_params bug (FIXED):** The token byte was at `params[6]` (wrong). Both 3-31-26 and
BW TX captures confirm it belongs at **`params[7]`** (raw: `00 00 00 00 00 00 00 fe 00 00`). 4-3-26 BW TX captures confirm it belongs at **`params[7]`** (raw: `00 00 00 00 00 00 00 fe 00 00`).
With the wrong position the device ignores the token and 1F returns null immediately. With the wrong position the device ignores the token and 1F returns null immediately.
**all-zero params required (empirically confirmed):** Even with the correct token position,
sending `token=0xFE` causes the device to return null from 1F in multi-event sessions.
All callers (`count_events`, `get_events`) must use `advance_event(browse=True)` which
sends all-zero params. The 3-31-26 capture that "confirmed" token=0xFE had only one event
stored — 1F always returns null at end-of-events, so we never actually observed 1F
successfully returning a second key with token=0xFE. Empirical evidence from live device
testing with 2+ events is definitive: **always use all-zero params for 1F.**
**0A context requirement:** `advance_event()` (1F) only returns a valid next-event key **0A context requirement:** `advance_event()` (1F) only returns a valid next-event key
when a preceding `read_waveform_header()` (0A) or `read_waveform_record()` (0C) call has when a preceding `read_waveform_header()` (0A) call has established device waveform
established device waveform context for the current key. Calling 1F cold (after only 1E, context for the current key. Call 0A before every event in the loop, not just the first.
with no 0A/0C) returns the null sentinel regardless of how many events are stored. Calling 1F cold (after only 1E, with no 0A) returns the null sentinel regardless of how
many events are stored.
**1F response layout:** The next event's key IS at `data_rsp.data[11:15]` (= payload[16:20]). **1F response layout:** The next event's key IS at `data_rsp.data[11:15]` (= payload[16:20]).
Confirmed from 4-3-26 browse-mode S3 captures: Confirmed from 4-3-26 browse-mode S3 captures:
@@ -162,15 +171,19 @@ echo) — in both cases, all zeros means "no more events."
= sample-count offset to the next event key (key1 = key0 + this offset). If offset == 0, = sample-count offset to the next event key (key1 = key0 + this offset). If offset == 0,
there is only one event. there is only one event.
**Correct iteration pattern (confirmed from 4-3-26 capture):** **Correct iteration pattern (confirmed empirically with live device, 2+ events):**
``` ```
1E(all zeros) → key0, trailing0 ← trailing0 non-zero if event 1 exists 1E(all zeros) → key0, trailing0 ← trailing0 non-zero if event 1 exists
0A(key0) establishes device context 0A(key0) ← REQUIRED: establishes device context
1F(token=0xFE at params[7]) → key1, trailing1 0C(key0) [+ 5A(key0) for get_events] ← read record data
0A(key1) ← establishes context for next advance 1F(all zeros / browse=True) → key1 ← use all-zero params, NOT token=0xFE
1F(token=0xFE) → null ← done 0A(key1) ← REQUIRED before each advance
0C(key1) [+ 5A(key1) for get_events]
1F(all zeros) → null ← done
``` ```
`advance_event(browse=True)` sends all-zero params; `advance_event()` default (browse=False)
sends token=0xFE and is NOT used by any caller.
`advance_event()` returns `(key4, event_data8)`. `advance_event()` returns `(key4, event_data8)`.
Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`. Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`.

View File

@@ -274,34 +274,31 @@ class MiniMateClient:
return [] return []
events: list[Event] = [] events: list[Event] = []
idx = 0 idx = 0
is_first = True
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()) log.info("get_events: record %d key=%s", idx, key4.hex())
ev = Event(index=idx) ev = Event(index=idx)
ev._waveform_key = key4 # stored so download_waveform() can re-use it ev._waveform_key = key4 # stored so download_waveform() can re-use it
# First event: call 0A to verify it's a full record (0x30 length). # Always call 0A before 0C to establish device waveform context.
# Subsequent keys come from 1F(0xFE) which guarantees full records, # The device requires 0A context for both 0C and the subsequent 1F.
# so we skip 0A for those — exactly matching Blastware behaviour. # (Earlier code skipped 0A for events after the first — confirmed wrong.)
proceed = True proceed = True
if is_first: try:
try: _hdr, rec_len = proto.read_waveform_header(key4)
_hdr, rec_len = proto.read_waveform_header(key4) if rec_len < 0x30:
if rec_len < 0x30:
log.warning(
"get_events: first key=%s is partial (len=0x%02X) — skipping",
key4.hex(), rec_len,
)
proceed = False
except ProtocolError as exc:
log.warning( log.warning(
"get_events: 0A failed for key=%s: %s — skipping 0C", "get_events: key=%s is partial (len=0x%02X) — skipping",
key4.hex(), exc, key4.hex(), rec_len,
) )
proceed = False proceed = False
is_first = False except ProtocolError as exc:
log.warning(
"get_events: 0A failed for key=%s: %s — skipping 0C",
key4.hex(), exc,
)
proceed = False
if proceed: if proceed:
# SUB 0C — full waveform record (peak values, timestamp, "Project:" string) # SUB 0C — full waveform record (peak values, timestamp, "Project:" string)
@@ -357,9 +354,18 @@ class MiniMateClient:
events.append(ev) events.append(ev)
idx += 1 idx += 1
# SUB 1F — advance to the next full waveform record key # SUB 1F — advance to the next record key.
# Uses browse mode (all-zero params) — empirically confirmed to work on
# this device. token=0xFE (download mode) returns null regardless of
# context, even after 0A+0C+5A. The 3-31-26 capture only had one event
# so we never saw 1F actually return a second key in download mode;
# the token=0xFE assumption was never validated for multi-event iteration.
try: try:
key4, data8 = proto.advance_event() key4, data8 = proto.advance_event(browse=True)
log.info(
"get_events: 1F → key=%s trailing=%s",
key4.hex(), data8[4:8].hex(),
)
except ProtocolError as exc: except ProtocolError as exc:
log.warning("get_events: 1F failed: %s — stopping iteration", exc) log.warning("get_events: 1F failed: %s — stopping iteration", exc)
break break