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>
_decode_event_count: read uint16 BE at offset 10 (confirmed 2026-04-10 from
live BE11529 event index — data[10:12]=0x0006=6, matches device LCD).
Previous uint32 at offset 3 always returned 1 regardless of event count.
ach_server.py: use device_info.event_count (already fetched during connect())
instead of calling count_events() separately. This saves 2*N round-trips and
avoids the 1F linked-list walk which was overcounting on some devices.
count_events() kept as fallback when connect() is skipped (--events-only).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all Unicode arrows/checkmarks (-> [OK] [FAIL]) in ach_server.py
and client.py log calls — Windows cp1252 console can't encode them
- Fix DeviceInfo attribute: serial_number -> serial
- Fix _device_info_to_dict key: serial_number -> serial
- Demote count_events 1E/1F per-key log lines from WARNING to DEBUG
(they were flooding the console on devices with many stored events)
- FileHandler now opens with encoding='utf-8' so session log files
can hold any characters without codec errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ach_server.py:
- Add ach_state.json per-unit state tracking (keyed by serial number)
- count_events() before any download; skip session if no new events since last call-home
- Download only events beyond the previous high-water mark (all_events[last_count:])
- --max-events N safety cap for first-run units with many stored events
- state_path and max_events wired through AchSession constructor and serve()
client.py (_decode_monitor_status):
- Revert monitoring flag to section[1] == 0x10 (was incorrectly changed to section[6])
- Fix battery/memory offsets to section[-10:-8], [-8:-4], [-4:] (no trailing checksum byte)
- Both confirmed by full byte diff of all 144 0xE3 data frames in 4-8-26/2ndtry capture
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
minimateplus/transport.py:
- Add SocketTransport(TcpTransport) — wraps an already-accepted inbound
socket; connect() is a no-op; everything else inherited from TcpTransport.
Enables the ACH server to reuse all existing protocol/client code without
any changes.
bridges/ach_server.py:
- Minimal inbound ACH server — listens on port 12345, accepts call-home
connections from MiniMate Plus units, runs the full BW protocol:
startup handshake → get_device_info → get_events(full_waveform=True)
- Saves device_info.json + events.json + raw_rx_<ts>.bin + session log
per connection to bridges/captures/ach_inbound_<ts>/
- raw_rx.bin is byte-compatible with existing Analyzer tooling
- Taps transport.read() to capture raw S3 bytes alongside parsed output
- Each connection runs in its own daemon thread
- Clearly distinguishes push vs pull protocol in the startup log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
section[6] is the monitoring flag (was wrongly section[1] — section[1] is always
0x00 in both states). Battery and memory fields use relative-from-end offsets
(section[-11:-9], section[-9:-5], section[-5:-1]) instead of absolute positions,
which broke when the payload grew by 3 bytes in monitoring mode.
Confirmed from full byte diff of 142 0xE3 frames in 4-8-26/2ndtry capture.
SFM start_monitoring now polls /device/monitor/status every 5s for up to 60s
instead of a fixed 25s delay (unit runs ~40s on-device sensor check before
confirming monitoring state).
Also corrects stale 1C→6E response anomaly claim in protocol reference — no
exceptions to the 0xFF−SUB rule are known.
- 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.
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>