feat: fetch event-time metadata from SUB 5A bulk waveform stream
Add read_bulk_waveform_stream() to MiniMateProtocol and wire it into get_events() so each event gets authoritative client/operator/sensor_location from the A5 frames recorded at event-time, not the current compliance config. - framing.py: bulk_waveform_params() and bulk_waveform_term_params() helpers (probe/chunk params and termination params for SUB 5A, confirmed from 1-2-26 BW TX capture) - protocol.py: read_bulk_waveform_stream(key4, stop_after_metadata=True) — probe + chunk loop (counter += 0x0400), early stop when b"Project:" found (A5[7] of 9), then sends termination at offset=0x005A - client.py: _decode_a5_metadata_into() needle-searches concatenated A5 frame data for Project/Client/User Name/Seis Loc/Extended Notes; get_events() now calls SUB 5A after each SUB 0C, overwriting project_info with event-time values Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,8 @@ from .framing import (
|
||||
build_bw_frame,
|
||||
waveform_key_params,
|
||||
token_params,
|
||||
bulk_waveform_params,
|
||||
bulk_waveform_term_params,
|
||||
POLL_PROBE,
|
||||
POLL_DATA,
|
||||
)
|
||||
@@ -88,6 +90,17 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
# NOT handled here — requires specialised read logic.
|
||||
}
|
||||
|
||||
# SUB 5A (BULK_WAVEFORM_STREAM) protocol constants.
|
||||
# Confirmed from 1-2-26 BW TX capture analysis (2026-04-02).
|
||||
_BULK_CHUNK_OFFSET = 0x1004 # offset field for probe + all regular chunk requests ✅
|
||||
_BULK_TERM_OFFSET = 0x005A # offset field for termination request ✅
|
||||
_BULK_COUNTER_STEP = 0x0400 # chunk counter increment per request ✅
|
||||
# Note: BW's second chunk used counter=0x1004 rather than the expected 0x0400.
|
||||
# This appears to be a waveform-specific pre-trigger byte offset unique to BW's
|
||||
# implementation. All subsequent chunks incremented by 0x0400 as expected.
|
||||
# 🔶 INFERRED: device echoes the counter back but may not validate it.
|
||||
# Confirm empirically on first live test.
|
||||
|
||||
# Default timeout values (seconds).
|
||||
# MiniMate Plus is a slow device — keep these generous.
|
||||
DEFAULT_RECV_TIMEOUT = 10.0
|
||||
@@ -395,6 +408,115 @@ class MiniMateProtocol:
|
||||
log.debug("read_waveform_record: received %d record bytes", len(record))
|
||||
return record
|
||||
|
||||
def read_bulk_waveform_stream(
|
||||
self,
|
||||
key4: bytes,
|
||||
*,
|
||||
stop_after_metadata: bool = True,
|
||||
max_chunks: int = 32,
|
||||
) -> list[bytes]:
|
||||
"""
|
||||
Download the SUB 5A (BULK_WAVEFORM_STREAM) A5 frames for one event.
|
||||
|
||||
The bulk waveform stream carries both raw ADC samples (large) and
|
||||
event-time metadata strings ("Project:", "Client:", "User Name:",
|
||||
"Seis Loc:", "Extended Notes") embedded in one of the middle frames
|
||||
(confirmed: A5[7] of 9 for 1-2-26 capture).
|
||||
|
||||
Protocol is request-per-chunk, NOT a continuous stream:
|
||||
1. Probe (offset=_BULK_CHUNK_OFFSET, is_probe=True, counter=0x0000)
|
||||
2. Chunks (offset=_BULK_CHUNK_OFFSET, is_probe=False, counter+=0x0400)
|
||||
3. Loop until metadata found (stop_after_metadata=True) or max_chunks
|
||||
4. Termination (offset=_BULK_TERM_OFFSET, counter=last+_BULK_COUNTER_STEP)
|
||||
Device responds with a final A5 frame (page_key=0x0000).
|
||||
|
||||
The termination frame (page_key=0x0000) is NOT included in the returned list.
|
||||
|
||||
Args:
|
||||
key4: 4-byte waveform key from EVENT_HEADER (1E).
|
||||
stop_after_metadata: If True (default), send termination as soon as
|
||||
b"Project:" is found in a frame's data — avoids
|
||||
downloading the full ADC waveform payload (several
|
||||
hundred KB). Set False to download everything.
|
||||
max_chunks: Safety cap on the number of chunk requests sent
|
||||
(default 32; a typical event uses 9 large frames).
|
||||
|
||||
Returns:
|
||||
List of raw data bytes from each A5 response frame (not including
|
||||
the terminator frame). Frame indices match the request sequence:
|
||||
index 0 = probe response, index 1 = first chunk, etc.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or unexpected SUB.
|
||||
|
||||
Confirmed from 1-2-26 BW TX/RX captures (2026-04-02):
|
||||
- probe + 8 regular chunks + 1 termination = 10 TX frames
|
||||
- 9 large A5 responses + 1 terminator A5 = 10 RX frames
|
||||
- page_key=0x0010 on large frames; page_key=0x0000 on terminator ✅
|
||||
- "Project:" metadata at A5[7].data[626] ✅
|
||||
"""
|
||||
if len(key4) != 4:
|
||||
raise ValueError(f"waveform key must be 4 bytes, got {len(key4)}")
|
||||
|
||||
rsp_sub = _expected_rsp_sub(SUB_BULK_WAVEFORM) # 0xFF - 0x5A = 0xA5
|
||||
frames_data: list[bytes] = []
|
||||
counter = 0
|
||||
|
||||
# ── Step 1: probe ────────────────────────────────────────────────────
|
||||
log.debug("5A probe key=%s", key4.hex())
|
||||
params = bulk_waveform_params(key4, 0, is_probe=True)
|
||||
self._send(build_bw_frame(SUB_BULK_WAVEFORM, _BULK_CHUNK_OFFSET, params))
|
||||
rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
frames_data.append(rsp.data)
|
||||
log.debug("5A A5[0] page_key=0x%04X %d bytes", rsp.page_key, len(rsp.data))
|
||||
|
||||
# ── Step 2: chunk loop ───────────────────────────────────────────────
|
||||
for chunk_num in range(1, max_chunks + 1):
|
||||
counter = chunk_num * _BULK_COUNTER_STEP
|
||||
params = bulk_waveform_params(key4, counter)
|
||||
log.debug("5A chunk %d counter=0x%04X", chunk_num, counter)
|
||||
self._send(build_bw_frame(SUB_BULK_WAVEFORM, _BULK_CHUNK_OFFSET, params))
|
||||
rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
if rsp.page_key == 0x0000:
|
||||
# Device unexpectedly terminated mid-stream (no termination needed).
|
||||
log.debug("5A A5[%d] page_key=0x0000 — device terminated early", chunk_num)
|
||||
return frames_data
|
||||
|
||||
frames_data.append(rsp.data)
|
||||
log.debug(
|
||||
"5A A5[%d] page_key=0x%04X %d bytes",
|
||||
chunk_num, rsp.page_key, len(rsp.data),
|
||||
)
|
||||
|
||||
if stop_after_metadata and b"Project:" in rsp.data:
|
||||
log.debug("5A A5[%d] metadata found — stopping early", chunk_num)
|
||||
break
|
||||
else:
|
||||
log.warning(
|
||||
"5A reached max_chunks=%d without end-of-stream; sending termination",
|
||||
max_chunks,
|
||||
)
|
||||
|
||||
# ── Step 3: termination ──────────────────────────────────────────────
|
||||
term_counter = counter + _BULK_COUNTER_STEP
|
||||
term_params = bulk_waveform_term_params(key4, term_counter)
|
||||
log.debug(
|
||||
"5A termination term_counter=0x%04X offset=0x%04X",
|
||||
term_counter, _BULK_TERM_OFFSET,
|
||||
)
|
||||
self._send(build_bw_frame(SUB_BULK_WAVEFORM, _BULK_TERM_OFFSET, term_params))
|
||||
try:
|
||||
term_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
log.debug(
|
||||
"5A termination response page_key=0x%04X %d bytes",
|
||||
term_rsp.page_key, len(term_rsp.data),
|
||||
)
|
||||
except TimeoutError:
|
||||
log.debug("5A no termination response — device may have already closed")
|
||||
|
||||
return frames_data
|
||||
|
||||
def advance_event(self) -> bytes:
|
||||
"""
|
||||
Send the SUB 1F (EVENT_ADVANCE) two-step read with download-mode token
|
||||
|
||||
Reference in New Issue
Block a user