feat: add full event download pipeline
This commit is contained in:
@@ -29,6 +29,8 @@ from .framing import (
|
||||
S3Frame,
|
||||
S3FrameParser,
|
||||
build_bw_frame,
|
||||
waveform_key_params,
|
||||
token_params,
|
||||
POLL_PROBE,
|
||||
POLL_DATA,
|
||||
)
|
||||
@@ -53,6 +55,7 @@ SUB_EVENT_INDEX = 0x08
|
||||
SUB_CHANNEL_CONFIG = 0x06
|
||||
SUB_TRIGGER_CONFIG = 0x1C
|
||||
SUB_EVENT_HEADER = 0x1E
|
||||
SUB_EVENT_ADVANCE = 0x1F
|
||||
SUB_WAVEFORM_HEADER = 0x0A
|
||||
SUB_WAVEFORM_RECORD = 0x0C
|
||||
SUB_BULK_WAVEFORM = 0x5A
|
||||
@@ -74,6 +77,11 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
SUB_FULL_CONFIG: 0x98, # 152-byte full config block ✅
|
||||
SUB_EVENT_INDEX: 0x58, # 88-byte event index ✅
|
||||
SUB_TRIGGER_CONFIG: 0x2C, # 44-byte trigger config 🔶
|
||||
SUB_EVENT_HEADER: 0x08, # 8-byte event header (waveform key + event data) ✅
|
||||
SUB_EVENT_ADVANCE: 0x08, # 8-byte next-key response ✅
|
||||
# SUB_WAVEFORM_HEADER (0x0A) is VARIABLE — length read from probe response
|
||||
# data[4]. Do NOT add it here; use read_waveform_header() instead. ✅
|
||||
SUB_WAVEFORM_RECORD: 0xD2, # 210-byte waveform/histogram record ✅
|
||||
SUB_UNKNOWN_2E: 0x1A, # 26 bytes, purpose TBD 🔶
|
||||
0x09: 0xCA, # 202 bytes, purpose TBD 🔶
|
||||
# SUB_COMPLIANCE (0x1A) uses a multi-step sequence with a 2090-byte total;
|
||||
@@ -227,6 +235,166 @@ class MiniMateProtocol:
|
||||
"""
|
||||
self._send(POLL_PROBE)
|
||||
|
||||
# ── Event download API ────────────────────────────────────────────────────
|
||||
|
||||
def read_event_first(self) -> tuple[bytes, bytes]:
|
||||
"""
|
||||
Send the SUB 1E (EVENT_HEADER) two-step read and return the first
|
||||
waveform key and accompanying 8-byte event data block.
|
||||
|
||||
This always uses all-zero params — the device returns the first stored
|
||||
event's waveform key unconditionally.
|
||||
|
||||
Returns:
|
||||
(key4, event_data8) where:
|
||||
key4 — 4-byte opaque waveform record address (data[11:15])
|
||||
event_data8 — full 8-byte data section (data[11:19])
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
||||
|
||||
Confirmed from 3-31-26 capture: 1E request uses all-zero params;
|
||||
response data section layout is:
|
||||
[LENGTH_ECHO:1][00×4][KEY_ECHO:4][00×2][KEY4:4][EXTRA:4] …
|
||||
Actual data starts at data[11]; first 4 bytes are the waveform key.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_EVENT_HEADER)
|
||||
length = DATA_LENGTHS[SUB_EVENT_HEADER] # 0x08
|
||||
|
||||
log.debug("read_event_first: 1E probe")
|
||||
self._send(build_bw_frame(SUB_EVENT_HEADER, 0))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("read_event_first: 1E data request offset=0x%02X", length)
|
||||
self._send(build_bw_frame(SUB_EVENT_HEADER, length))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
event_data8 = data_rsp.data[11:19]
|
||||
key4 = data_rsp.data[11:15]
|
||||
log.debug("read_event_first: key=%s", key4.hex())
|
||||
return key4, event_data8
|
||||
|
||||
def read_waveform_header(self, key4: bytes) -> tuple[bytes, int]:
|
||||
"""
|
||||
Send the SUB 0A (WAVEFORM_HEADER) two-step read for *key4*.
|
||||
|
||||
The data length for 0A is VARIABLE and must be read from the probe
|
||||
response at data[4]. Two known values:
|
||||
0x30 — full histogram bin (has a waveform record to follow)
|
||||
0x26 — partial histogram bin (no waveform record)
|
||||
|
||||
Args:
|
||||
key4: 4-byte waveform record address from 1E or 1F.
|
||||
|
||||
Returns:
|
||||
(header_bytes, record_length) where:
|
||||
header_bytes — raw data section starting at data[11]
|
||||
record_length — DATA_LENGTH read from probe (0x30 or 0x26)
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
||||
|
||||
Confirmed from 3-31-26 capture: 0A probe response data[4] carries
|
||||
the variable length; data-request uses that length as the offset byte.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_WAVEFORM_HEADER)
|
||||
params = waveform_key_params(key4)
|
||||
|
||||
log.debug("read_waveform_header: 0A probe key=%s", key4.hex())
|
||||
self._send(build_bw_frame(SUB_WAVEFORM_HEADER, 0, params))
|
||||
probe_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
# Variable length — read from probe response data[4]
|
||||
length = probe_rsp.data[4] if len(probe_rsp.data) > 4 else 0x30
|
||||
log.debug("read_waveform_header: 0A data request offset=0x%02X", length)
|
||||
|
||||
if length == 0:
|
||||
return b"", 0
|
||||
|
||||
self._send(build_bw_frame(SUB_WAVEFORM_HEADER, length, params))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
header_bytes = data_rsp.data[11:11 + length]
|
||||
log.debug(
|
||||
"read_waveform_header: key=%s length=0x%02X is_full=%s",
|
||||
key4.hex(), length, length == 0x30,
|
||||
)
|
||||
return header_bytes, length
|
||||
|
||||
def read_waveform_record(self, key4: bytes) -> bytes:
|
||||
"""
|
||||
Send the SUB 0C (WAVEFORM_RECORD / FULL_WAVEFORM_RECORD) two-step read.
|
||||
|
||||
Returns the 210-byte waveform/histogram record containing:
|
||||
- Record type string ("Histogram" or "Waveform") at a variable offset
|
||||
- Per-channel labels ("Tran", "Vert", "Long", "MicL") with PPV floats
|
||||
at label_offset + 6
|
||||
|
||||
Args:
|
||||
key4: 4-byte waveform record address.
|
||||
|
||||
Returns:
|
||||
210-byte record bytes (data[11:11+0xD2]).
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
||||
|
||||
Confirmed from 3-31-26 capture: 0C always uses offset=0xD2 (210 bytes).
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_WAVEFORM_RECORD)
|
||||
length = DATA_LENGTHS[SUB_WAVEFORM_RECORD] # 0xD2
|
||||
params = waveform_key_params(key4)
|
||||
|
||||
log.debug("read_waveform_record: 0C probe key=%s", key4.hex())
|
||||
self._send(build_bw_frame(SUB_WAVEFORM_RECORD, 0, params))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("read_waveform_record: 0C data request offset=0x%02X", length)
|
||||
self._send(build_bw_frame(SUB_WAVEFORM_RECORD, length, params))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
record = data_rsp.data[11:11 + length]
|
||||
log.debug("read_waveform_record: received %d record bytes", len(record))
|
||||
return record
|
||||
|
||||
def advance_event(self) -> bytes:
|
||||
"""
|
||||
Send the SUB 1F (EVENT_ADVANCE) two-step read with download-mode token
|
||||
(0xFE) and return the next waveform key.
|
||||
|
||||
In download mode (token=0xFE), the device skips partial histogram bins
|
||||
and returns the key of the next FULL record directly. This is the
|
||||
Blastware-observed behaviour for iterating through all stored events.
|
||||
|
||||
Returns:
|
||||
key4 — 4-byte next waveform key from data[11:15].
|
||||
Returns b'\\x00\\x00\\x00\\x00' when there are no more events.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
||||
|
||||
Confirmed from 3-31-26 capture: 1F uses token=0xFE at params[6];
|
||||
loop termination is key4 == b'\\x00\\x00\\x00\\x00'.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_EVENT_ADVANCE)
|
||||
length = DATA_LENGTHS[SUB_EVENT_ADVANCE] # 0x08
|
||||
params = token_params(0xFE)
|
||||
|
||||
log.debug("advance_event: 1F probe")
|
||||
self._send(build_bw_frame(SUB_EVENT_ADVANCE, 0, params))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("advance_event: 1F data request offset=0x%02X", length)
|
||||
self._send(build_bw_frame(SUB_EVENT_ADVANCE, length, params))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
key4 = data_rsp.data[11:15]
|
||||
log.debug(
|
||||
"advance_event: next key=%s done=%s",
|
||||
key4.hex(), key4 == b"\x00\x00\x00\x00",
|
||||
)
|
||||
return key4
|
||||
|
||||
# ── Internal helpers ──────────────────────────────────────────────────────
|
||||
|
||||
def _send(self, frame: bytes) -> None:
|
||||
|
||||
Reference in New Issue
Block a user