## v0.13.0 — 2026-05-01
### Fixed
- **SUB 5A bulk waveform stream — over-read bug for events ≥ 2 sec.**
`read_bulk_waveform_stream` was walking the chunk counter past the actual
end of the event, picking up post-event circular-buffer garbage that
corrupted reconstructed Blastware files for any waveform > ~1 sec. The
loop now extracts the event's `end_offset` from the STRT record at
`data[23:27]` of the probe response and stops the chunk walk when the next
counter would step past it. Verified against three BW MITM captures
(4-27-26 + 5-1-26): 2-sec event drops from 37 over-read chunks to 7
bounded chunks; 3-sec drops to 9; non-zero-start "event 2" drops to 9.
### Added
- `framing.bulk_waveform_term_v2(key4, end_offset, last_chunk_counter)` —
computes the corrected SUB 5A TERM frame's `(offset_word, params)` per the
formula confirmed across all 3 BW captures. Not yet wired into
`read_bulk_waveform_stream` (the legacy TERM is still used to preserve the
existing `blastware_file.write_blastware_file` frame-structure expectations);
available for the next iteration that switches to BW's 0x0200 chunk step.
- `framing.parse_strt_end_offset(a5_data)` — extracts the event-end pointer
from the STRT record in an A5 response payload.
- Added `CallHomeConfig` model to represent the Auto Call Home settings.
- Introduced methods in `MiniMateClient` for reading (`get_call_home_config`) and writing (`set_call_home_config`) the call home configuration.
- Updated `MiniMateProtocol` with new commands for call home operations (SUB 0x2C for read, SUB 0x7E for write, and SUB 0x7F for confirm).
- Created API endpoints for retrieving and updating call home settings in the server.
- Enhanced the web interface with a new "Call Home" tab for user interaction with call home settings.
- Implemented JavaScript functions for reading and writing call home configurations from the web app.
Add full decode pipeline for 0x2C partial records from the device's event
list, representing continuous monitoring intervals where no threshold was
crossed. These records appear interleaved with full triggered events in the
browse walk and were previously ignored.
minimateplus/models.py
- Add MonitorLogEntry dataclass: key, start_time, stop_time, serial,
geo_threshold_ips, raw_header, duration_seconds property
minimateplus/protocol.py
- read_waveform_header() now returns (data_rsp.data, length) — full payload
including the record-type byte at position 0 — instead of the sliced header.
Callers that need the old slice use raw_data[11:11+length] as before.
minimateplus/client.py
- Add _decode_0a_partial_header(): auto-detects 9-byte (sub_code=0x10) vs
10-byte (sub_code=0x03) timestamp format, handles 1-byte inter-timestamp
gap, extracts serial via BE anchor and geo threshold via Geo: anchor.
- Add get_monitor_log_entries(skip_keys=None): browse walk (1E → 0A → 1F),
decodes partial records, skips full records and already-seen keys.
minimateplus/__init__.py
- Export MonitorLogEntry
bridges/ach_server.py
- After get_events(), call get_monitor_log_entries(skip_keys=seen_keys) and
save new entries to monitor_log.json in the session directory.
- Add _monitor_log_entry_to_dict() helper.
- Include monitor log keys in downloaded_keys for state persistence.
CLAUDE.md / CHANGELOG.md
- Document 0x2C partial record layout (timestamp format, ASCII metadata
region, 1-byte gap edge case) confirmed from 4-11-26 MITM capture.
- Version bump to v0.10.0; update What's next.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Introduced new SUBs for monitoring status, start, and stop commands in protocol.py.
- Implemented read_monitor_status, start_monitoring, and stop_monitoring methods in MiniMateProtocol class.
- Added new API endpoints for monitoring status retrieval and control in server.py.
- Enhanced the web application with a monitoring panel, including battery and memory status display.
- Created a new Python script to parse SUB 0x1C response frames for monitoring status.
- Documented the monitoring status response format and field locations in markdown and text files.
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>
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>
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>
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>
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.
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>
Adds full support for reading device compliance configuration (2090-byte E5
response) containing record time, trigger/alarm levels, and project strings.
protocol.py:
- Implement read_compliance_config() two-step read (SUB 1A → E5)
- Fixed length 0x082A (2090 bytes)
models.py:
- Add ComplianceConfig dataclass with fields: record_time, sample_rate,
trigger_level_geo, alarm_level_geo, max_range_geo, project strings
- Add compliance_config field to DeviceInfo
client.py:
- Implement _decode_compliance_config_into() to extract:
* Record time float at offset +0x28 ✅
* Trigger/alarm levels per-channel (heuristic parsing) 🔶
* Project/setup strings from E5 payload
* Placeholder for sample_rate (location TBD ❓)
- Update connect() to read SUB 1A after SUB 01, cache in device_info
- Add ComplianceConfig to imports
sfm/server.py:
- Add _serialise_compliance_config() JSON encoder
- Include compliance_config in /device/info response
- Updated _serialise_device_info() to output compliance config
Both record_time (at fixed offset 0x28) and project strings are ✅ CONFIRMED
from protocol reference §7.6. Trigger/alarm extraction uses heuristics
pending more detailed field mapping from captured data.
Sample rate remains undiscovered in the E5 payload — likely in the
mystery flags at offset +0x12 or requires a "fast mode" capture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>