Frame index 9 was assumed to be the device terminator based on the
9-frame original blast capture. For streams with >9 frames (current
device produces 35), fi==9 is live waveform data — the skip was
dropping ~133 sample-sets per event.
Terminator detection is handled upstream via page_key==0x0000 in
read_bulk_waveform_stream, so no index-based skip is needed here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two improvements to eliminate the ~2-min-per-event wait and unnecessary
full-event-list download when only one event is requested:
1. protocol.py: pass timeout=10.0 to _recv_one in the 5A chunk loop.
Device responds within ~1s per chunk; 10s gives a safe 10x buffer.
End-of-stream detection (raw_bytes=1) now fires in 10s instead of 120s,
cutting ~110s of dead wait per event.
2. client.py: add stop_after_index parameter to get_events(). When set,
iteration stops immediately after the target event is collected — no
further 0A/1E/0C/5A/1F cycles for events the caller doesn't need.
3. server.py: pass stop_after_index=index to both /device/event/{idx}
and /device/event/{idx}/waveform endpoints so a single-event request
only downloads that one event.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the device finishes streaming waveform data, it sends a single
partial byte (raw_bytes=1) rather than a complete A5 frame, then goes
silent for the full 120s timeout.
Observed: 35 chunks succeed, chunk 36 times out with raw_bytes=1 —
identical for both events in the test run.
Fix: on TimeoutError, if bytes_fed > 0 and we already have collected
frames, treat it as natural end-of-stream and break to the termination
step rather than propagating the exception. True transport failures
(raw_bytes=0, no prior frames) still raise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps each recv_one call in a try/except TimeoutError, logging:
- On timeout: chunk_num, counter, raw bytes_fed (distinguishes "device
silent" from "device sent unparseable bytes")
- On success: chunk_num, page_key, data_len, contains_Project flag
Parser is explicitly reset before each chunk recv so bytes_fed is
accurate per-chunk rather than cumulative. Helps identify exactly which
chunk fails and whether the device is responding at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added `_decode_a5_waveform()` to parse SUB 5A frames into per-channel time-series data.
- Introduced `download_waveform(event)` method in `MiniMateClient` to fetch full waveform data.
- Updated `Event` model to include new fields: `total_samples`, `pretrig_samples`, `rectime_seconds`, and `_waveform_key`.
- Enhanced documentation in `CHANGELOG.md` and `instantel_protocol_reference.md` to reflect new features and confirmed protocol details.
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>
The channel block is in frame C data (cfg[44:1071]) not deep in a
hypothetical frame D section. The offset-1000 assumption was wrong —
searching from 44 lets us find it while unit string validation still
prevents false positives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Temporary: log tran_pos, surrounding bytes, and exact unit string check
results at WARNING level so we can see why trigger/alarm/max_range are
still null even with the full 2126-byte cfg.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old key was (page_key, chunk_len) which would incorrectly drop a second
config section that has the same length as the first (e.g. current-config
vs event-time-config when settings haven't changed).
New key is the full chunk bytes — only truly byte-identical chunks are
dropped. Different data that happens to share page_key and length now
comes through correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DeviceInfo.event_count: Optional[int] = None (new field in models.py)
- connect() now calls proto.read_event_index() after compliance config and
stores the decoded count in device_info.event_count
- _serialise_device_info() exposes event_count in /device/info and /device/events
JSON responses
event_count is decoded from uint32 BE at offset +3 of the 88-byte F7 payload
(🔶 inferred — needs live device confirmation against a multi-event device).
Any ProtocolError from the index read is caught and logged; event_count stays
None rather than failing the whole connect().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The channel block is only present in the full ~2126-byte cfg (when frame D
delivers correctly rather than duplicating frame B's page). Layout per §7.6:
[00 00][max_range f32][00 00][trigger f32]["in.\0"][alarm f32]["/s\0\0"][00 01][label]
Relative offsets from the "Tran" label position (label-24/label-18/label-10)
are validated by checking the unit strings "in.\0" at label-14 and "/s\0\0"
at label-6 before reading the floats. Guard against "Tran2" false-match.
When frame D duplicates, cfg is ~1071 bytes and tran_pos search returns a hit
without the unit string sentinels — we log the miss and leave fields None.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-step probe+fetch for SUB 08 (EVENT_INDEX), returning the raw 88-byte
(0x58) index block. SUB_EVENT_INDEX and DATA_LENGTHS[0x08]=0x58 were
already registered — this just wires the method that calls them.
Docstring notes the partially-decoded layout (event count at +3 as uint32 BE,
timestamps at +7) pending live device confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
record_time (float32_BE) and sample_rate (uint16_BE) both validated
against live device across normal / fast / faster modes and multiple
record time settings. Diagnostic scaffolding no longer needed.
Fixed a 1-byte offset jitter that produced garbage values when the
device was set to "faster" (4096 Sa/s) mode.
Root cause: 4096 = 0x1000, so the sample_rate bytes in the raw S3
frame are `10 10 00` (DLE-escaped). After DLE unstuffing → `10 00`
(2 bytes vs 3 for 1024/2048), making frame C 1 byte shorter and
shifting all subsequent field offsets by -1.
Fix: locate the stable 10-byte anchor `01 2c 00 00 be 80 00 00 00 00`
(max-record-limit constant + first two alarm-level floats) and read:
sample_rate = uint16_BE at anchor - 2
record_time = float32_BE at anchor + 10
Offline-validated against all five logged hex dumps (1071 and 1070
byte cfg, record times 3/5/8 s, sample rates 1024 and 4096):
all five: correct values with anchor approach.
Both offsets identified from _cfg_diagnostic scan on BE11529:
cfg[64] float32_BE = 3.0 → record_time in seconds
cfg[52] uint16_BE = 1024 → sample_rate in Sa/s
Values are plausible but NOT yet validated by changing device settings
and re-reading. Marked as ✅ candidate in docstring — confirm and
remove the ⚠️ note once a device-side change is observed here.
BE11529 sometimes returns frame D with page_key=0x0000 (44 bytes),
identical to the frame B response, inflating cfg to ~1115 bytes and
mis-aligning all field offsets. Track (page_key, chunk_size) pairs
and drop any repeat before appending to the running config buffer.
- setup_name was broken: _find_string_after(b"Standard Recording Setup")
returned what comes AFTER the string (i.e. "Project:"), not the name
itself. Fixed by searching for the first long (>=8 char) ASCII string
in cfg[40:250] with _find_first_string().
- record_time offset 0x28 was wrong (that location holds "(L)", a unit
label string). Disabled for now to avoid returning garbage; correct
offset will be determined from _cfg_diagnostic() output.
- Added _cfg_diagnostic(): logs all strings and all plausible float32/uint16
values across the full cfg so record_time and sample_rate offsets can be
pinpointed from a single device run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverse-engineered the full Blastware 4-frame sequence for SUB 1A:
A (probe): offset=0x0000, params[7]=0x64
B (data req 1): offset=0x0400, params[2]=0x00, params[7]=0x64 → bytes 0..1023
C (data req 2): offset=0x0400, params[2]=0x04, params[7]=0x64 → bytes 1024..2047
D (data req 3): offset=0x002A, params[2]=0x08, params[7]=0x64 → bytes 2048..2089
We were only sending A+D and getting 44 bytes (the last chunk).
Now sends B, C, D in sequence; each E5 response has 11-byte echo header
stripped, and chunks are concatenated. Devices that return all data in
one frame (BE18189 style) are handled — timeouts on B/C are skipped
gracefully and data from D still accumulates.
Total expected: 0x0400 + 0x0400 + 0x002A = 0x082A = 2090 bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BE11529 sends the compliance config (SUB 1A / E5 response) as a stream
of small frames (~44 bytes each) rather than one large frame like BE18189.
Changes:
- read_compliance_config(): loop calling _recv_one() until 0x082A bytes
accumulated or 2s inter-frame gap; first frame strips 11-byte echo header,
subsequent frames logged in full for structure analysis
- _recv_one(): add reset_parser=False option to preserve parser state and
_pending_frames buffer between consecutive reads from one device response;
also stash any extra frames parsed in a single TCP chunk so they are not lost
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>