fix: update event handling in MiniMateClient and protocol to ensure correct sequence for waveform downloads
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user