Files
seismo-relay/docs/instantel_protocol_reference.md
T
claude b9a8e50b3c docs: update protocol reference with v0.9.0 erase-all protocol
Changelog section:
- 5 new entries (2026-04-11): erase-all confirmation, SUB 0x06 purpose
  resolved, §7.11 added, §14.6 ACH session lifecycle marked IMPLEMENTED

§5.1 Request Commands:
- SUB 0x06 description updated: "EVENT STORAGE RANGE READ" (not "CHANNEL
  CONFIG READ"), token=0xFE, last 8 bytes = first/last stored event keys
- SUB 0xA3 added: ERASE ALL BEGIN — standard build_bw_frame, token=0xFE, ack 0x5C
- SUB 0xA2 added: ERASE ALL CONFIRM — standard build_bw_frame, token=0xFE, ack 0x5D

§5.2 Response SUBs:
- 0x06→0xF9 marked CONFIRMED 2026-04-11
- 0xA3→0x5C and 0xA2→0x5D added with CONFIRMED status

§7.11 (new section): Erase-All Protocol
- Full wire sequence (6 request/response pairs)
- SUB 0x06 storage range payload layout (36 bytes, last 8 = first/last key)
- Post-erase key counter reset: device restarts from 0x01110000
- Implementation notes pointing to client.py and ach_server.py

§14.6 ACH Session Lifecycle:
- Removed "Future" label — fully implemented in bridges/ach_server.py
- Added step 6 (optional erase), step 8 (DCD/DTR auto-resume)
- Documents ach_server.py flags and ach_state.json schema
- Notes RV55 DCD/DTR issue as known open problem

Open Questions table:
- SUB 0x06 purpose RESOLVED
- Erase-all sequence RESOLVED
- ACH server RESOLVED
- Sensor Check byte: still open, added as formal question
- RV55 DCD/DTR: added as new open question

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 01:20:43 -04:00

134 KiB
Raw Blame History

Instantel MiniMate Plus — Blastware RS-232 Protocol Reference

"The Rosetta Stone"

Reverse-engineered via RS-232 serial bridge sniffing between Blastware software and an Instantel MiniMate Plus seismograph (S/N: BE18189).
Cross-referenced against Instantel MiniMate Plus Operator Manual (716U0101 Rev 15) from v0.18 onward.
Certainty Ratings: CONFIRMED | 🔶 INFERRED | SPECULATIVE Certainty ratings apply only to protocol semantics, not to capture tooling behavior.


Changelog

Date Section Change
2026-02-26 Initial Document created from first hex dump analysis
2026-02-26 §2 Frame Structure CORRECTED: Frame uses DLE-STX (0x10 0x02) and DLE-ETX (0x10 0x03), not bare 0x02/0x03. 0x41 confirmed as ACK not STX. DLE stuffing rule added.
2026-02-26 §8 Timestamp UPDATED: Year 0x07CB = 1995 confirmed as MiniMate hardware default date when RTC battery is disconnected. Not an encoding error. Confidence upgraded from to 🔶.
2026-02-26 §10 DLE Stuffing UPGRADED: Section upgraded from SPECULATIVE to CONFIRMED. Full stuffing rules and parser state machine documented.
2026-02-26 §11 Checksum UPDATED: Frame builder and parser rewritten to handle DLE framing and byte stuffing correctly.
2026-02-26 §14 Open Questions DLE question removed (resolved). Timestamp year question removed (resolved).
2026-02-26 §7.2 Serial Number Response CORRECTED: Trailing bytes are 0x79 0x11 only (2 bytes, not 3). 0x20 was misidentified as a trailing byte — it is the frame checksum.
2026-02-26 §7.2 Serial Number Response UPDATED: Two-unit comparison confirms 0x11 = firmware minor version (S337.170x11 = 17). Byte 0 is unit-specific, derivation unknown.
2026-02-26 §15 Binary Log Format NEW: .bin logger format strips DLE from ETX (0x10 0x030x03). Not raw wire bytes.
2026-02-26 §5.1 Request Commands ADDED: Three new read commands confirmed: SUB 09, 1A, 2E.
2026-02-26 §5.3 Write Commands NEW SECTION: Full write command family documented from compliance setup capture. SUBs 6883.
2026-02-26 §7.5 Full Waveform Record UPDATED: Project strings field layout fully mapped from write payload diff. Client field confirmed at byte +230 in SUB 71 frame.
2026-02-26 §14 Open Questions Write commands question resolved. Session 152427 had swapped port labels — superseded by sessions 184518 and 185019.
2026-02-26 Global CORRECTED: Firmware version S338.17S337.17 everywhere, including hex encoding at config offset 0x34.
2026-02-26 §3 Payload Structure DOWNGRADED: ADDR field certainty 🔶 INFERRED❓ SPECULATIVE. Added note that bytes 12 purpose is unconfirmed.
2026-02-26 §5.2 Response SUBs STRENGTHENED: 0xFF - SUB rule wording clarified — high confidence, no counterexample, not yet formally proven.
2026-02-26 §15 → Appendix A RENAMED: Binary log format section moved to Appendix A with explicit note that it describes tooling behavior, not protocol.
2026-02-26 Header ADDED: Certainty legend clarification — ratings apply to protocol semantics only, not tooling behavior.
2026-02-26 §7.6 Channel Config Float Layout NEW SECTION: Trigger level confirmed as IEEE 754 BE float in in/s. Alarm level identified as adjacent float = 1.0 in/s. Unit string "in./s" embedded inline. 0x082A removed as trigger level candidate.
2026-03-01 §7.6 Channel Config Float Layout UPGRADED: Alarm level offset fully confirmed via controlled capture (alarm 1.0→2.0, trigger 0.5→0.6). Complete per-channel layout documented. Three-channel repetition confirmed (Tran, Vert, Long). Certainty upgraded to CONFIRMED.
2026-03-01 §7.7 .set File Format NEW SECTION: Blastware save-to-disk format decoded. Little-endian binary struct matching wire protocol payload. Full per-channel block layout mapped. Record time confirmed as uint32 at +16. MicL unit string confirmed as "psi\0". 0x082A mystery noted — not obviously record time, needs one more capture to resolve.
2026-03-02 §7.4 Event Index Block CONFIRMED: Backlight and power save offsets independently confirmed via device-set capture (backlight=100=0x64 at +75, power-save=30=0x1E at +83). On-device change visible in S3→BW read response — no Blastware write involved. Offsets are CONFIRMED.
2026-03-02 §7.4 Event Index Block NEW: Monitoring LCD Cycle identified at offsets +84/+85 as uint16 BE. Default value = 65500 (0xFFDC) = effectively disabled / maximum. Confirmed from operator manual §3.13.1g.
2026-03-02 §7.4 Event Index Block UPDATED: Backlight confirmed as uint8 range 0255 seconds per operator manual §3.13.1e ("adjustable timer, 0 to 255 seconds"). Power save unit confirmed as minutes per operator manual §3.13.1f.
2026-03-02 Global NEW SOURCE: Operator manual (716U0101 Rev 15) added as reference. Cross-referencing settings definitions, ranges, and units. Header updated.
2026-03-02 §14 Open Questions Float 6.2061 in/s mystery: manual confirms only two geo ranges (1.25 in/s and 10.0 in/s). 6.2061 is NOT a user-selectable range → likely internal ADC full-scale calibration constant or hardware range ceiling. Downgraded to LOW priority.
2026-03-02 §14 Open Questions 0x082A hypothesis refined: 2090 decimal. At 1024 sps, 2 sec record = 2048 samples. Possible that 0x082A = total samples including 0.25s pre-trigger (256 samples) at some adjusted rate. Needs capture with different record time.
2026-03-02 §14 Open Questions NEW items added: Trigger sample width (default=2), Auto Window (1-9 sec), Aux Trigger (enabled/disabled) — all confirmed settings from operator manual not yet mapped in protocol.
2026-03-02 §14 Open Questions Monitoring LCD Cycle resolved — removed from open questions.
2026-03-02 Appendix A CORRECTED: Previous entry stated logger strips DLE from ETX. This was wrong — it applied to an older logger version. s3_bridge v0.5.0 confirmed to preserve raw wire bytes including 0x10 0x03 intact. HxD inspection of new capture confirmed 10 03 present in S3→BW record payloads.
2026-03-02 Appendix A UPDATED: New capture architecture: two flat raw wire dumps per session (raw_s3.bin, raw_bw.bin), one per direction, no record wrapper. Replaces structured .bin format for parser input.
2026-03-02 Appendix A PARSER: Deterministic DLE state machine implemented (s3_parser.py). Three states: IDLE → IN_FRAME → AFTER_DLE. Replaces heuristic global scanning. Properly handles DLE stuffing (10 10 → literal 10). Only complete STX→ETX pairs counted as frames.
2026-03-02 Appendix A VALIDATED: raw_bw.bin yields 7 complete frames via state machine. raw_s3.bin contains large structured responses (first frame payload ~3922 bytes). Both files confirmed lossless. BW bare 0x02 pattern confirmed as asymmetric framing (BW sends bare STX, S3 sends DLE+STX).
2026-03-09 §7.6, §Appendix B CONFIRMED: Record time located in SUB E5 data page2 at payload offset +0x28 as float32 BE. Confirmed via two controlled captures: 7 sec = 40 E0 00 00, 13 sec = 41 50 00 00. Geo range (only 1.25 or 10.0 in/s) eliminates ambiguity — 7 and 13 are not valid geo range values.
2026-03-09 §7.5, §14 CORRECTED: The byte 0x0A appearing after the "Extended Notes" null-padded label in the E5 payload is NOT record time. It is an unknown field that equals 10 and does not change when record time changes. False lead closed.
2026-03-09 §14 RESOLVED: 0x082A mystery closed — confirmed as fixed-size E5 payload length (2090 bytes), not a record-time-derived sample count. Value is constant regardless of record time or other settings.
2026-03-09 §7.8, §14, Appendix B NEW — Trigger Sample Width confirmed: Located in BW→S3 write frame SUB 0x82, destuffed payload offset [22], uint8. Confirmed via BW-side capture (raw_bw.bin) diffing two sessions: Width=4 → 0x04, Width=3 → 0x03. Setting is transmitted only on BW→S3 write (SUB 0x82), invisible in S3-side compliance dumps.
2026-03-09 §14, Appendix B CONFIRMED — Mode gating is a real protocol behavior: Several settings are only transmitted (and possibly only interpreted by the device) when the required mode is active. Trigger Sample Width is only sent when in Compliance/Single-Shot/Fixed Record Time mode. Auto Window is only relevant when Record Stop Mode = Auto — attempting to capture it in Fixed mode produced no change on the wire (F7 and D1 blocks identical before/after). This is an architectural property, not a gap in the capture methodology. Future capture attempts for mode-gated settings must first activate the appropriate mode.
2026-03-09 §14 UPDATED — Auto Window: Capture attempted (Auto Window 3→9) in Fixed record time mode. No change observed in any S3-side frame (F7, D1, E5 all identical). Confirmed mode-gated behind Record Stop Mode = Auto. Not capturable without switching modes — deferred.
2026-03-11 §14, Appendix B CONFIRMED — Aux Trigger read location: SUB FE (FULL_CONFIG_RESPONSE), destuffed payload offset 0x0109, uint8. 0x00 = disabled, 0x01 = enabled. Confirmed via controlled capture: changed Aux Trigger in Blastware, sent to unit, re-read config. FE diff showed clean isolated flip at 0x0109 with only 3 other bytes changing (likely counters/checksums at 0x0033, 0x00C0, 0x04ED).
2026-03-11 §14, Appendix B PARTIAL — Aux Trigger write path: Write command not yet isolated. The BW→S3 write appears to occur inside the A4 (POLL_RESPONSE) stream via inner frame handshaking — multiple WRITE_CONFIRM_RESPONSE inner frames (SUBs 7C, 7D, 8B, 8C, 8D, 8E, 96, 97) appeared in A4 after the write, and the TRIGGER_CONFIG_RESPONSE (SUB E3) inner frames were removed. Write command itself not yet captured in a clean session — likely SUB 15 or embedded in the partial session 0. Write path deferred for a future clean capture.
2026-03-11 §4, §14 NEW — SUB A4 is a composite container frame: A4 (POLL_RESPONSE) payload contains multiple embedded inner frames using the same DLE framing (10 02 start, 10 03 end, 10 10 stuffing). Phase-shift diffing issue resolved in s3_analyzer.py by adding _extract_a4_inner_frames() and _diff_a4_payloads() — diff count reduced from 2300 → 17 meaningful entries.
2026-03-11 §14 NEW — SUB 6E response anomaly: BW sends SUB 1C (TRIGGER_CONFIG_READ) and S3 responds with SUB 6E — does NOT follow the 0xFF - SUB rule (0xFF - 0x1C = 0xE3). Only known exception to the response pairing rule observed to date. SUB 6E payload starts with ASCII string "Long2". CORRECTION 2026-04-08: This "exception" was a misidentification. The 1C in that capture was BW→S3 (a monitor status poll), and the 6E response was from an inner A4 sub-frame misread as a top-level S3 frame. Confirmed from 4-8-26/2ndtry capture (338 BW TX frames): SUB 0x1C always receives response SUB 0xE3 (= 0xFF 0x1C). No exceptions to the response pairing rule are known.
2026-03-12 §11 CONFIRMED — BW→S3 large-frame checksum algorithm: SUBs 68, 69, 71, 82, and 1A (with data) use: chk = (sum(b for b in payload[2:-1] if b != 0x10) + 0x10) % 256 — SUM8 of payload bytes [2:-1] skipping all 0x10 bytes, plus 0x10 as a constant, mod 256. Validated across 20 frames from two independent captures with differing string content (checksums differ between sessions, both validate correctly). Small frames (POLL, read commands) continue to use plain SUM8 of payload[0:-1]. The two formulas are consistent: small frames have exactly one 0x10 (CMD at [0]), which the large-frame formula's [2:] start and +0x10 constant account for.
2026-03-12 §11 RESOLVED — BAD CHK false positives on BW POLL frames: Parser bug — BW frame terminator (03 41, ETX+ACK) was being included in the de-stuffed payload instead of being stripped as framing. BW frames end with bare 0x03 (not 10 03). Fix: strip trailing 03 41 from BW payloads before checksum computation.
2026-03-30 §3, §5.1 CONFIRMED — BW→S3 two-step read offset is at payload[5], NOT payload[3:4]. All BW read-command frames have payload[3] = 0x00 and payload[4] = 0x00 unconditionally. The two-step offset byte lives at payload[5]: 0x00 for the length-probe step, DATA_LEN for the data-fetch step. Validated against all captured frames in bridges/captures/3-11-26/raw_bw_*.bin — every frame is an exact bit-for-bit match when built with offset at [5]. The page_hi/page_lo framing in the docstring was a misattribution from the S3-side response layout (where [3]/[4] ARE page bytes).
2026-03-30 §4, §5.2 CONFIRMED — S3 probe response page_key is always 0x0000. The S3 response to a length-probe step does NOT carry the data length back in page_hi/page_lo. Both bytes are 0x00 in every observed probe response. Data lengths for each SUB are fixed constants (see §5.1 table). The minimateplus library now uses a hardcoded DATA_LENGTHS dict rather than trying to read the length from the probe response.
2026-03-31 §12 TCP Transport NEW SECTION — TCP/modem transport confirmed transparent from Blastware Operator Manual (714U0301 Rev 22). Key facts confirmed: (1) Protocol bytes over TCP are bit-for-bit identical to RS-232 — no handshake framing. (2) No ENQ byte on TCP connect (Enable ENQ on TCP Connect: 0-Disable in Raven ACEmanager). (3) Raven modem Data Forwarding Timeout = 1 second — modem buffers serial bytes up to 1s before forwarding over TCP; TcpTransport.read_until_idle uses idle_gap=1.5s to compensate. (4) TCP port is user-configurable (12335 in manual example; user's install uses 12345). (5) Baud rate over serial link to modem is 38400,8N1 regardless of TCP path. (6) ACH (Auto Call Home) = INBOUND to server (unit calls home); "call up" = OUTBOUND from client (Blastware/SFM connects to modem IP). TcpTransport implements outbound (call-up) mode.
2026-03-31 §14.3 NEW — Sierra Wireless RV50/RV55 Quiet Mode requirement confirmed. Quiet Mode (ATQ) must be enabled on the serial port. When disabled (+ Verbose mode on), the modem injects RING\r\nCONNECT\r\n onto the RS-232 serial line at connection time — MiniMate receives unexpected bytes, loses protocol sync, and never responds to POLL (unit beeps but returns no S3 frame). Working RV50 field config: Quiet Mode enabled, Data Forwarding Timeout=1, TCP Connect Response Delay=0. Misconfigured RV55 had all three wrong.
2026-03-31 §14.2 CORRECTED — Sierra Wireless RV50/RV55 sends RING/CONNECT over TCP to caller even with Quiet Mode enabled. Quiet Mode suppresses these only on the serial port (protecting the MiniMate). TCP client still receives \r\nRING\r\n\r\nCONNECT\r\n prefixed before the first S3 frame bytes. Parser handles correctly by scanning for DLE+STX (0x10 0x02) and discarding prefix bytes. Previous note "no CONNECT string" described Raven X ENQ-disable behaviour; RV50/RV55 differ.
2026-03-31 §7.3 NEW — Calibration date field confirmed at Full Config (SUB FE) destuffed payload offsets 0x530x57. Two-unit comparison: BE18189 (calibrated 2023) has 07 E7 at 0x560x57; BE11529 (calibrated 2025) has 07 E9. Bytes 0x560x57 = uint16 BE calibration year CONFIRMED. Adjacent bytes at 0x530x55 likely encode month/day (both units show 0x10 at offset 0x54 = BCD October; 0x53 and 0x55 differ between units). Full date layout 🔶 INFERRED — pending third-unit capture or recalibration diff. Resolves open question.
2026-03-31 §9 CONFIRMED via Console cold-start capture"Operating System" (16 B: 4f 70 65 72 61 74 69 6e 67 20 53 79 73 74 65 6d) arrives as first TCP bytes on cold-connect before unit enters DLE-framed mode. TcpTransport + retry logic handles gracefully: first attempt times out waiting for SUB A4; second connect (after unit fully booted) succeeds.
2026-04-01 §7.7.5, §8 CONFIRMED — Full waveform record (0C) timestamp layout cross-referenced against Blastware event report for BE11529 thump event ("00:28:12 April 1, 2026"). 9-byte format at bytes[08]: [day][sub_code][month][year:2 BE][unknown][hour][min][sec]. All fields verified. Sub_code 0x10 = Waveform (continuous/single-shot). Previous 7-byte format doc was wrong — replaced with confirmed 9-byte layout.
2026-04-01 §7.7.5 CONFIRMED — Record type encoded in byte[1] (sub_code), not as ASCII string. 0x10 = Waveform . Histogram sub_code not yet captured. ASCII string search approach removed.
2026-04-01 §7.7.5, §14 CONFIRMED — Per-channel PPV at label+6 ( all four channels), cross-referenced vs Blastware: Tran=0.420, Vert=3.870, Long=0.495 in/s. CONFIRMED — Peak Vector Sum at fixed offset 87 = 3.906 in/s matches Blastware "Peak Vector Sum". Is √(Tran²+Vert²+Long²) at max instantaneous vector moment, not vector sum of per-channel peaks. Open question "offset 87 purpose" closed.
2026-04-01 §8 RESOLVED — §8 unknown byte at offset 3. Field is confirmed absent in the 9-byte waveform record format (no such field). The 6-byte event-index format has a separator byte at [3] whose purpose remains but is no longer actively blocking anything.
2026-04-01 §7.6.2 (NEW) NEW — SUB 1A multi-frame read protocol documented. SUB 1A (compliance config read) requires a 4-frame sequence, not a simple 2-step probe+fetch. Reverse-engineered from raw_bw_20260311_155355.bin (Blastware TX capture, ACK+STX/ETX format, BW CMD=0x10). Sequence: (A) probe offset=0x0000, params=...00 64 00 00; (B) data request offset=0x0400, params=...00 64 00 00; (C) data request offset=0x0400, params=...04 00 00 64 00 00; (D) data request offset=0x002A, params=...08 00 00 64 00 00. E5 response to each frame carries a page_key field (bytes data[3:5]): page 0x0000 = 44-byte header chunk, page 0x0010 = main config data (~1027+1055 bytes across C and D).
2026-04-01 §7.6.2 NEW — BE11529 duplicate-page behaviour. BE11529 (firmware S338.17) sometimes responds to frame D with page 0x0000 (44 bytes) instead of page 0x0010 (~1055 bytes). When this happens, frame D is an exact duplicate of frame B and must be dropped to prevent cfg mis-alignment. Detection: track (page_key, chunk_len) pairs and skip repeats. When frame D delivers page 0x0010 correctly, total cfg = 44 + 1027 + 1055 = 2126 bytes; with duplicate D, cfg = 44 + 1027 = 1071 bytes. Both are valid inputs to the decoder.
2026-04-01 §7.6.1 CORRECTED — Record time offset. Previous doc (+0x28 from E5 data page2 start) was correct for single-frame reads but unreliable for BE11529 due to a 1-byte DLE jitter (see §7.6.3). The minimateplus library now uses an anchor-based approach: search for \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 in cfg[40:100]; record time float32 BE is at anchor+10. Validated at 3.0, 5.0, and 8.0 seconds.
2026-04-01 §7.6.3 (NEW) NEW — Sample rate confirmed and documented. Sample rate (Normal=1024 / Fast=2048 / Faster=4096 Sa/s) is stored as uint16 BE at anchor2, where anchor is the 10-byte sequence \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00. DLE jitter root cause: 4096 = 0x1000, so in the raw S3 frame the sample-rate bytes are sent as 10 10 00 (DLE-escaped 10); after DLE unstuffing → 10 00 (2 bytes instead of 3 for 1024/2048), making frame C 1 byte shorter and shifting all subsequent offsets by 1. Anchor search is immune to this shift. All three modes confirmed on BE11529 firmware S338.17.
2026-04-01 §5.1 CONFIRMED — _pending_frames buffer and reset_parser=False parameter. MiniMateProtocol._recv_one() now supports reset_parser=False to preserve parser state between consecutive reads within a multi-frame sequence. A _pending_frames: list[S3Frame] buffer stores extra frames parsed from a single TCP chunk when multiple E5 responses arrive together. Required for reliable SUB 1A frame B/C/D sequence on BE11529.
2026-04-02 §7.8 (NEW) CONFIRMED — SUB 5A frame format. offset_hi byte (0x10) must be sent raw, not DLE-stuffed — standard build_bw_frame incorrectly stuffs it to 10 10 on the wire; device ignores the frame. BW sends it as bare 10. Checksum is DLE-aware: when walking the byte sequence, 10 XX pairs contribute only XX to the sum; lone bytes contribute normally. build_5a_frame() reproduces BW's exact wire format.
2026-04-02 §7.8 CONFIRMED — SUB 5A params are 11 bytes (not 10) for chunk frames. Extra trailing 0x00 confirmed from 1-2-26 BW wire capture. Probe frame and termination frame differ — see bulk_waveform_params() in framing.py.
2026-04-02 §7.7.5 CONFIRMED — Event-time metadata source. Client:, User Name:, and Seis Loc: strings are present in A5 frame 7 of the SUB 5A bulk waveform stream — they are NOT in the 210-byte SUB 0C waveform record. They reflect the compliance setup active when the event was stored on the device (not the current setup). get_events() now issues SUB 5A after each 0C download. Sequence: 1E → 0A → 0C → 5A → 1F.
2026-04-02 §7.6.2 FIXED — Compliance config orphaned send bug. An extra self._send(SUB_COMPLIANCE / 0x2A / DATA_PARAMS) before the B/C/D receive loop had no corresponding recv_one(). Every receive in the loop was consuming the previous send's response, leaving frame D's channel block unread. Bug removed. Total config bytes now ~2126 (was ~1071 due to truncation). trigger_level_geo, alarm_level_geo, max_range_geo are now correctly populated.
2026-04-02 §7.6.1 CORRECTED — Anchor search range. Previous doc stated anchor search range cfg[40:100]. With the orphaned-send bug fixed, the 44-byte header padding is gone and the anchor now appears at cfg[11]. Corrected to cfg[0:150].
2026-04-03 §7.6 CONFIRMED — Blast waveform format (4-2-26 capture). Blast/waveform-mode SUB 5A stream uses 4-channel interleaved signed int16 LE, 8 bytes per sample-set [T,V,L,M]. NOT the 32-byte block format (which is noise/histogram mode only). Frame sizes are NOT multiples of 8 — cross-frame alignment correction required (track global byte offset mod 8; skip (8-align)%8 bytes at each frame start). A5[0] STRT record confirmed: 21 bytes at db[7:]+11; waveform starts at strt_pos+27 (after 2-byte null pad + 4-byte 0xFF sentinel). Frame index 7 = metadata only, no ADC data. Full §7.6 rewritten.
2026-04-03 §7.6 CONFIRMED — Noise block format details. 32-byte blocks: LE uint16 type + LE uint16 ctr + 9×int16 LE samples + 10B metadata. Samples are little-endian (previous doc said big-endian — WRONG). Type: 0x0016=sync (appears at start of each A5 frame), 0x0000=data. Noise floor ≈ 911 counts. Metadata fixed pattern 00 01 43 [2B var] 00 [pretrig] [rectime] 00 00 confirmed.
2026-04-03 client.py NEW — _decode_a5_waveform() and download_waveform() implemented. _decode_a5_waveform(frames_data, event) decodes full A5 waveform stream into event.raw_samples = {"Tran":[…], "Vert":[…], "Long":[…], "Mic":[…]}. Populates event.total_samples, event.pretrig_samples, event.rectime_seconds from STRT record. Handles cross-frame alignment. MiniMateClient.download_waveform(event) calls read_bulk_waveform_stream(stop_after_metadata=False) then invokes the decoder. Waveform key stored on Event as _waveform_key during get_events().
2026-04-03 §7.7.5 CONFIRMED — sub_code=0x03 (Waveform continuous) uses 10-byte timestamp header — one byte wider than sub_code=0x10 layout. Cross-referenced against Blastware event report for BE11529 (15:20:17 Apr 3 2026). Raw header: 10 03 10 04 07 ea 00 0f 14 11 = [unknown_a][day][unknown_b][month][year:2 BE][unknown][hour][min][sec]. Peak Vector Sum is at tran_label 12 (label-relative, NOT fixed offset 87 — fixed offset only incidentally correct for sub_code=0x10).
2026-04-05 §7.8 CONFIRMED — 5A "Project:" string is session-start config, NOT per-event. The "Project:" value in A5 frame 7 reflects the compliance setup active when the monitoring session started, not when the individual event was recorded. _decode_a5_metadata_into() only sets project from 5A when 0C did not already supply one. "Client:", "User Name:", "Seis Loc:", and "Extended Notes" are NOT in the 0C record and are set unconditionally from 5A.
2026-04-06 §5.1 CORRECTED — SUB 1F token position is params[7], NOT params[6]. Both 3-31-26 and 4-3-26 BW TX captures confirm: raw params 00 00 00 00 00 00 00 FE 00 00, token byte at index 7. Previous doc (params[6]) was wrong — with wrong position the device ignores the token and 1F returns null immediately.
2026-04-06 §6.1 NEW — Full event download sequence documented (§6.1). Sequence confirmed from 4-2-26 and 4-3-26 BW TX captures: 1E(all-zero) → [per event: 0A → 1E(arm/0xFE) → 0C → 1F(arm/0xFE) → POLL×3 → 5A → 1F(browse)]. Each step documented with confirmed requirements.
2026-04-06 §6.1 CONFIRMED — 1E(token=0xFE) arm step required. Device silently ignores all 5A probe frames unless a second 1E with token=0xFE is sent between 0A and 0C. Present in every download cycle in the 4-2-26 and 4-3-26 captures.
2026-04-06 §6.1 CONFIRMED — SUB 1F(token=0xFE) must precede POLL×3 before 5A. BW always sends 1F(0xFE) before the 3 POLL cycles before 5A. 5A still uses the pre-advance key (set by 0A+1E-arm+0C); 1F only arms the device's 5A state machine.
2026-04-06 §6.1 CONFIRMED — browse 1F must be conditional. Calling 1F(browse=True/all-zero) after a FAILED 5A disrupts device state and causes the next event's 5A probe to time out with 0 bytes received. Browse 1F is only called after a SUCCESSFUL 5A. Failure fallback: use the key returned by the prior 1F(arm/0xFE) call.
2026-04-06 §7.8 ADDED — bytes_fed diagnostic counter on S3FrameParser. Counts raw bytes fed to the parser since last reset(). Logged at WARNING when 5A probe times out — distinguishes "device sent no bytes at all" from "device responded but frame was malformed or had wrong SUB".
2026-04-06 §7.8.2 CORRECTED — SUB 5A chunk counter is monotonic for ALL chunks. Previous doc hard-coded chunk 1 counter as 0x1004 (from 4-2-26 BW TX capture). This was a Blastware artifact. Empirically confirmed: counter = chunk_num * 0x0400 works (device responds immediately); counter=0x1004 for chunk 1 causes 120 s timeout. BW's true internal formula appears to be key4[2:4] + n * 0x0400 — for event 1 (key 01110000) this equals n * 0x0400. The device does not strictly validate counter values.
2026-04-06 §7.8.4 NEW — 5A end-of-stream signalling confirmed. After streaming all waveform chunks, the device sends exactly 1 raw byte in response to the next chunk request, then goes silent for the full recv timeout. This byte is NOT a complete DLE-framed A5 response — the frame parser accumulates it as bytes_fed=1 and never assembles a frame. This is the device's natural end-of-stream signal. Handling: on TimeoutError, if bytes_fed > 0 AND prior chunks were received, treat as graceful end and proceed to the termination frame. A bytes_fed=0 timeout with no prior chunks is a genuine transport failure and must still raise.
2026-04-06 §7.8.4 NEW — 5A chunk timing and count (empirical, BE11529 at 1024 sps). Each chunk response arrives within ~1 second over TCP/cellular. A 9,306-sample event (≈9.1 s at 1024 sps) produces 35 chunks before end-of-stream. Chunks 116 have varying data lengths (10361123 bytes); chunks 1735 are uniformly 1036 bytes each (post-event silence, all-zero ADC samples). Safe recv timeout for chunk loop: 10 s (10× typical response time). Default transport timeout (120 s) results in a ~2-minute stall per event at end-of-stream.
2026-04-06 §7.8.3 KNOWN ISSUE — _decode_a5_waveform hardcoded fi==9 skip. The decoder contains elif fi == 9: continue which was written for the 9-frame original blast capture where frame 9 was a device terminator. For streams with >9 frames (current device produces 35+), frame index 9 is live waveform data — this skip discards ~1,070 bytes (~133 sample-sets) per event. The terminator is now detected via page_key == 0x0000, not by frame index. The fi==9 skip should be removed.
2026-04-06 §7.8 CONFIRMED — ADC count-to-physical-unit conversion. Raw waveform samples are signed 16-bit integers (counts). Conversion: value = counts × (range / 32767). For geo channels: range = 10.000 in/s (from the device's compliance config geo range field). For the mic channel: range is in psi (device-specific). Near-full-scale counts (≈32,700) on all four channels simultaneously indicate ADC saturation (clipping) from a high-amplitude event.
2026-04-08 §5.1, §7.10, §12 NEW — Monitoring commands confirmed. SUB 0x1C (monitor status), 0x96 (start monitoring), 0x97 (stop monitoring) all confirmed from 4-8-26/2ndtry capture. SESSION_RESET (41 03) required before POLL to wake a monitoring unit.
2026-04-09 §7.10 CORRECTED — monitoring flag and battery/memory offsets. section[1] == 0x10 is the monitoring flag (100% accurate across 144 data frames in 2ndtry capture). Previous note claiming section[6] was wrong — section[6] has device-specific non-binary values (0xea/0x07). Battery/memory offsets corrected: section[-10:-8] (battery×100), section[-8:-4] (memory_total), section[-4:] (memory_free). NOTE: frame.data has checksum stripped by parser — earlier offsets of [-11:-9]/[-9:-5]/[-5:-1] were wrong because they assumed a trailing checksum byte that isn't there.
2026-04-08 §7.10 NEW — SUBs 0x0E (channel sensor data) and 0x98 (trigger test) observed in 4-8-26/sensor-check capture (Blastware "Unit Channel Test" comms check). SUB 0x0E: 2-step read with channel selector in params[6:8], data length 0x0A per channel, RSP SUB = 0xF1. SUB 0x98: single probe frame with params[0] = 0xFF, RSP SUB = 0x67; sent twice per test cycle. Not yet implemented in SFM.
2026-04-08 §7.10 NEW — SUBs 0x15 and 0x01 observed in sensor-check capture. SUB 0x15 (serial number short form, data length 0x0A, RSP 0xEA) and SUB 0x01 (device info block, data length 0x98 = 152 bytes, RSP 0xFE) seen in Blastware's "Unit Channel Test" init sequence. Note: SUB 0x01 response SUB 0xFE collides with the existing SUB 0xFE → RSP 0x01 naming convention — they are inverse commands.
2026-04-08 §12 CONFIRMED — Unit partially reachable during on-device sensor check. 4-8-26/sensor-check capture shows: POLL responds normally throughout; SUB 0x0E channel reads partially served (channels 04 responded), then ~40s silent gap while sensor check ran, then channels 57 responded. On-device sensor check duration ≈ 40 s. SFM _pollMonitorConfirm() polls status every 5 s for up to 60 s after start_monitoring.
2026-04-08 §7.9 (NEW) NEW — Compliance config field inventory captured from Blastware UI. See §7.9 for full field list (Recording Setup, Notes, Special Setups tabs). Most fields NOT yet mapped to raw byte offsets. Confirmed decoded: sample_rate, record_time, trigger_level_geo, alarm_level_geo, max_range_geo, backlight_on_time, power_saving_timeout, monitoring_lcd_cycle, project/client/operator/sensor_location/notes. Sensor Check dropdown (Before monitoring / After each event / Disabled) NOT YET LOCATED in raw config bytes.
2026-04-11 §5.1, §5.2 NEW — Erase-all command sequence confirmed from MITM capture. SUB 0xA3 (begin erase, token=0xFE → ack 0x5C) + SUB 0xA2 (confirm erase, token=0xFE → ack 0x5D). Standard build_bw_frame format (not write-format). Required intermediate steps: 0x1C probe+data (monitor status read) + 0x06 probe+data (event storage range). All response SUBs follow the standard 0xFFSUB formula with no exceptions.
2026-04-11 §5.1 CONFIRMED — SUB 0x06 (CHANNEL CONFIG READ) now confirmed as event storage range. Two-step read, data offset = 0x24 (36 bytes). Token=0xFE at params[7]. Last 8 bytes of response: first stored event key (bytes 8:4) and last stored event key (bytes 4:). Both equal 01110000 when device memory is empty. Used by Blastware to verify erase completion.
2026-04-11 §7.11 (NEW) NEW — §7.11 Erase-All Protocol added. Full wire sequence, SUB 0x06 storage range payload layout, post-erase key counter reset (resets to 0x01110000). Confirmed from 4-11-26 MITM capture of live Blastware ACH session.
2026-04-11 §14.6 RESOLVED — ACH Session Lifecycle is no longer "Future". bridges/ach_server.py fully implements inbound ACH: POLL handshake, device info, event download. State tracked via ach_state.json (key-based, with max_downloaded_key for post-erase detection). --clear-after-download flag added for the standard delete-after-upload workflow.

1. Physical Layer

Parameter Value Certainty
Interface RS-232 serial CONFIRMED
Baud rate 38400 CONFIRMED (from bridge log header)
Data bits 8 CONFIRMED (standard for this baud/era)
Parity None 🔶 INFERRED (no parity errors observed)
Stop bits 1 🔶 INFERRED (standard assumption)
Flow control None (no RTS/CTS activity) 🔶 INFERRED

2. Frame Structure

⚠️ 2026-02-26 — CORRECTED: Previous version incorrectly identified 0x41 as STX and 0x02/0x03 as bare frame delimiters. The protocol uses proper DLE framing. See below.

Every message follows this structure:

[ACK]  [DLE+STX]  [PAYLOAD...]  [CHECKSUM]  [DLE+ETX]
 0x41  0x10 0x02    N bytes        1 byte    0x10 0x03

Special Byte Definitions

Token Raw Bytes Meaning Certainty
ACK 0x41 (ASCII 'A') Acknowledgment / ready token. Standalone single byte. Sent before every frame by both sides. CONFIRMED
DLE 0x10 Data Link Escape. Prefixes the next byte to give it special meaning. CONFIRMED — 2026-02-26
STX 0x10 0x02 DLE+STX = Start of frame (two-byte sequence) CONFIRMED — 2026-02-26
ETX 0x10 0x03 DLE+ETX = End of frame (two-byte sequence) CONFIRMED — 2026-02-26
CHECKSUM 1 byte 8-bit sum of de-stuffed payload bytes, modulo 256. Sits between payload and DLE+ETX. CONFIRMED

DLE Byte Stuffing Rule

CONFIRMED — 2026-02-26

Any 0x10 byte appearing naturally in the payload data is escaped by doubling it: 0x100x10 0x10. This prevents the parser from confusing real data with frame control sequences.

  • Transmit: Replace every 0x10 in payload with 0x10 0x10
  • Receive: Replace every 0x10 0x10 in the frame body with a single 0x10
Sequence on wire Meaning
0x10 0x02 Frame START — only valid at beginning
0x10 0x03 Frame END
0x10 0x10 Escaped literal 0x10 byte in payload data
Any other 0x10 0xXX Protocol error / undefined

Frame Parser Notes

  • The 0x41 ACK always arrives in a separate read() call before the frame body due to RS-232 inter-byte timing at 38400 baud. This is normal.
  • Your parser must be stateful and buffered — read byte by byte, accumulate between DLE+STX and DLE+ETX. Never assume one read() = one frame.
  • Checksum is computed on the de-stuffed payload, not the raw wire bytes.
  • The ACK and DLE+STX are not included in the checksum.

Checksum Verification Example

Raw frame on wire (with ACK and DLE framing):

41  10 02  |  10 10 00 5B 00 00 00 00 00 00 00 00 00 00 00 00 00  |  6B  |  10 03
^ACK^^STX^    ^---------- stuffed payload (0x10→0x10 0x10) ------^  ^chk^  ^ETX^

After de-stuffing (0x10 0x100x10):

De-stuffed: 10 00 5B 00 00 00 00 00 00 00 00 00 00 00 00 00
Checksum:   10+00+5B+00+... = 0x6B ✅

3. Payload Structure

The payload (bytes between DLE+STX and CHECKSUM, after de-stuffing) has consistent internal structure:

[CMD]  [DLE]  [ADDR]  [FLAGS]  [SUB_CMD]  [OFFSET_HI]  [OFFSET_LO]  [PARAMS × N]
 xx    0x10   0x10     0x00      xx          xx            xx
Field Position Notes Certainty
CMD byte 0 Command or response code CONFIRMED
DLE byte 1 Always 0x10. Part of address/routing scheme. On wire this is stuffed as 0x10 0x10. CONFIRMED — 2026-02-26
ADDR byte 2 Always observed as 0x10. Also stuffed on wire. Purpose unknown — may not be an address. SPECULATIVE
FLAGS byte 3 Usually 0x00. Non-zero values seen in event-keyed requests. 🔶 INFERRED
SUB_CMD byte 4 The actual operation being requested. CONFIRMED
OFFSET_HI byte 5 High byte of data offset for paged reads. CONFIRMED
OFFSET_LO byte 6 Low byte of data offset. CONFIRMED

NOTE on bytes 12: After de-stuffing, bytes 1 and 2 are both 0x10 in every observed frame across all captured sessions and both units. Their semantic meaning is not yet confirmed. No capture has shown either field vary across units, commands, or directions. They may represent routing, bus ID, or fixed header constants — or the field boundaries assumed here may be wrong entirely.

🔶 NOTE: Because bytes 1 and 2 are both 0x10, they appear on the wire as four consecutive 0x10 bytes (0x10 0x10 0x10 0x10). This is normal — both are stuffed. Do not mistake them for DLE+STX or DLE+ETX.


4. Communication Pattern

4.1 ACK Handshake (Every Transaction)

Side A  →  0x41                                    (ACK: "ready / received")
Side A  →  10 02 [payload] [chk] 10 03             (frame)
Side B  →  0x41                                    (ACK)
Side B  →  10 02 [payload] [chk] 10 03             (response frame)

4.2 Two-Step Paged Read Pattern

All data reads use a two-step length-prefixed pattern. It is not optional.

Step 1 — Request with offset=0 ("how much data is there?"):
  BW  →  0x41
  BW  →  10 02 [CMD] 10 10 00 [SUB] 00 00 [00 00 ...] [chk] 10 03

Step 2 — Device replies with total data length:
  S3  →  0x41
  S3  →  10 02 [RSP] 00 10 10 [SUB] 00 00 00 00 00 00 [LEN_HI] [LEN_LO] [chk] 10 03

Step 3 — Re-request using LEN as offset ("now send the data"):
  BW  →  0x41
  BW  →  10 02 [CMD] 10 10 00 [SUB] 00 00 [LEN_HI] [LEN_LO] [00 ...] [chk] 10 03

Step 4 — Device sends actual data payload:
  S3  →  0x41
  S3  →  10 02 [RSP] 00 10 10 [SUB] 00 00 [LEN_HI] [LEN_LO] [DATA...] [chk] 10 03

5. Command Reference Table

5.1 Request Commands (Blastware → S3)

SUB Byte Name Description Certainty
5B POLL / KEEPALIVE Sent continuously (~every 80ms). Requests device identity/status. CONFIRMED
15 SERIAL NUMBER REQUEST Requests device serial number. CONFIRMED
01 FULL CONFIG READ Requests complete device configuration block (~0x98 bytes). Firmware, model, serial, channel config, scaling factors. CONFIRMED
08 EVENT INDEX READ Requests the event record index (0x58 bytes). Event count and record pointers. CONFIRMED
06 EVENT STORAGE RANGE READ Requests event storage range block (0x24 = 36 bytes). Token=0xFE at params[7]. Last 8 bytes of response: first stored event key ([-8:-4]) and last stored event key ([-4:]). Both equal 01110000 when device is empty. Used by Blastware as part of the erase-all verification step. Previously labelled "CHANNEL CONFIG READ" — function now confirmed from 4-11-26 MITM capture. CONFIRMED 2026-04-11
1C TRIGGER CONFIG READ Requests trigger settings block (0x2C bytes). CONFIRMED
1E EVENT HEADER READ Gets first waveform key. Token byte at params[7] (0x00=browse, 0xFE=download-arm). Key at data[11:15]; trailing offset at data[15:19] (0 = only one event). Two uses: (1) all-zero to get key0; (2) token=0xFE after 0A, before 0C — REQUIRED to arm device for SUB 5A. CONFIRMED 2026-04-06
0A WAVEFORM HEADER READ Checks record type for a given waveform key. Variable DATA_LENGTH: 0x30=full bin, 0x26=partial bin. Key at params[4..7]. Required before every 1F call to establish device waveform context. CONFIRMED 2026-03-31
0C FULL WAVEFORM RECORD Downloads 210-byte waveform/histogram record. Sub_code at byte[1]: 0x10=Waveform (9-byte timestamp hdr), 0x03=Waveform-continuous (10-byte hdr, 1-byte shift). PPV floats at label+6 (search "Tran"/"Vert"/"Long"/"MicL"). Peak Vector Sum at tran_label12 (NOT fixed offset). Key at params[4..7], DATA_LENGTH=0xD2. CONFIRMED 2026-04-03
1F EVENT ADVANCE Advances to next waveform key. Token byte at params[7] (⚠️ NOT params[6]): 0x00=browse (all-zero params), 0xFE=download (arm 5A state machine). Returns next key at data[11:15]; null sentinel when data[15:19]=0x00000000. Requires preceding 0A to establish context. Browse 1F must ONLY be called after successful 5A — calling it after a failed 5A disrupts device state for the next event's 5A probe. CONFIRMED 2026-04-06
5A BULK WAVEFORM STREAM Bulk download of raw ADC sample data. Non-standard frame format: offset_hi=0x10 sent raw (not DLE-stuffed), DLE-aware checksum. Requires 1E-arm + 0C + 1F(0xFE) + POLL×3 before first probe. A5[7] contains event-time metadata (Project:/Client:/User Name:/Seis Loc:). 9+ A5 frames for full waveform; stop_after_metadata=True exits after A5[7]. CONFIRMED 2026-04-06
24 WAVEFORM PAGE A? Paged waveform read, possibly channel group A. 🔶 INFERRED
25 WAVEFORM PAGE B? Paged waveform read, possibly channel group B. 🔶 INFERRED
09 UNKNOWN READ A Read command, response (F6) returns 0xCA (202) bytes. Purpose unknown. 🔶 INFERRED
1A COMPLIANCE CONFIG READ Multi-step sequence (A+B+C+D frames). Response (E5) carries sample_rate (uint16 BE at anchor2), record_time (float32 BE at anchor+10), trigger/alarm/max_range floats, and project strings. Anchor: \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00, search cfg[0:150]. Total ~2126 cfg bytes. CONFIRMED 2026-04-02
2E UNKNOWN READ B Read command, response (D1) returns 0x1A (26) bytes. Purpose unknown. 🔶 INFERRED
0E CHANNEL SENSOR DATA Real-time sensor reading for one channel. Two-step read, data length 0x0A (10 bytes). Channel selector in params[6:8] (0x00000x0007 for 8 channels). Response (F1) carries amplitude, frequency, overswing data for that channel. Used by Blastware "Unit Channel Test" comms check. CONFIRMED 2026-04-08
98 TRIGGER TEST Trigger-test command. Single probe frame; params[0] = 0xFF. Response (0x67) is all-zero data. Sent twice per Blastware comms-check cycle. Not a full POLL, no monitor state change. CONFIRMED 2026-04-08
1C MONITOR STATUS READ Two-step read, data offset 0x2C (44 bytes). section[1] == 0x10 → monitoring; 0x00 → idle (CONFIRMED 2026-04-09, 100% accuracy on 144 frames). Payload length: 4647 bytes IDLE, 4849 bytes MONITORING. frame.data has checksum stripped — no trailing byte to skip. Battery/memory at end: section[-10:-8] = battery×100 (uint16 BE), section[-8:-4] = memory_total (uint32 BE), section[-4:] = memory_free (uint32 BE). CONFIRMED 2026-04-09
96 START MONITORING Single write frame, no data payload. Transitions unit from idle to monitoring mode (after optional on-device sensor check ~40 s). CONFIRMED 2026-04-08
97 STOP MONITORING Single write frame, no data payload. Stops monitoring, unit returns to idle. CONFIRMED 2026-04-08
A3 ERASE ALL BEGIN Single frame, token=0xFE at params[7]. Initiates device memory erase. Must be followed by 0x1C probe+data + 0x06 probe+data + 0xA2 to complete. Standard build_bw_frame (not write-format). Response ack SUB = 0x5C. CONFIRMED 2026-04-11
A2 ERASE ALL CONFIRM Single frame, token=0xFE at params[7]. Commits the erase initiated by 0xA3. After this ack (SUB 0x5D), device memory is cleared and the event counter resets to 0x01110000. CONFIRMED 2026-04-11

All requests use CMD byte 0x02. All responses use CMD byte 0x10 0x02 (which, after de-stuffing, is just the DLE+CMD combination — see §3).

5.2 Response SUB Bytes (S3 → Blastware)

🔶 INFERRED pattern: Response SUB = 0xFF - Request SUB. Verified on all observed pairs to date — no counterexample has been observed across read commands, write commands, or either unit. Confidence is high but not formally proven across the full command space.

Request SUB Response SUB Certainty
5B A4 CONFIRMED
15 EA CONFIRMED
01 FE CONFIRMED
08 F7 CONFIRMED
06 F9 CONFIRMED 2026-04-11
1C E3 CONFIRMED 2026-04-08
1E E1 CONFIRMED
0A F5 CONFIRMED
0C F3 CONFIRMED
5A A5 CONFIRMED
1F E0 CONFIRMED 2026-03-31
09 F6 CONFIRMED
1A E5 CONFIRMED
2E D1 CONFIRMED
0E F1 CONFIRMED 2026-04-08
98 67 CONFIRMED 2026-04-08
96 69 CONFIRMED 2026-04-08
97 68 CONFIRMED 2026-04-08
A3 5C CONFIRMED 2026-04-11
A2 5D CONFIRMED 2026-04-11

5.3 Write Commands (Blastware → Device)

CONFIRMED — 2026-02-26 from compliance setup capture (session 185019).

Write commands are initiated by Blastware (BW->S3) and use SUB bytes in the 0x600x83 range. The device acknowledges each write with a short response frame containing no data payload.

Pattern: Write SUB = Read SUB + 0x60 (e.g. 0x08 EVENT INDEX READ → 0x68 EVENT INDEX WRITE).

SUB Name Description Response SUB Certainty
68 EVENT INDEX WRITE Writes event index block (mirrors SUB 08 read). Contains event count and timestamps. 97 CONFIRMED
69 WAVEFORM DATA WRITE Writes large waveform/channel data block (0xCA bytes, mirrors SUB 09). 96 CONFIRMED
71 COMPLIANCE / PROJECT STRINGS WRITE Writes compliance config and all project string fields. Contains setup name, project, client, operator, sensor location, and extended notes. Also contains channel scaling floats and 0x082A threshold value. 8E CONFIRMED
72 WRITE CONFIRM A Short frame, no data. Likely commit/confirm step after 71. 8D CONFIRMED
73 WRITE CONFIRM B Short frame, no data. 8C CONFIRMED
74 WRITE CONFIRM C Short frame, no data. 8B CONFIRMED
82 TRIGGER CONFIG WRITE Writes trigger config block (0x1C bytes, mirrors SUB 1C read). 7D CONFIRMED
83 TRIGGER WRITE CONFIRM Short frame, no data. Likely commit step after 82. 7C CONFIRMED

Write response SUB pairs follow the same 0xFF - request rule:

Write SUB Response SUB
68 97
69 96
71 8E
72 8D
73 8C
74 8B
82 7D
83 7C

6. Session Startup Sequence

1.  Device powers on / resets
2.  S3  →  "Operating System"  (raw ASCII, no DLE framing — UART boot string)
3.  BW  →  0x41 + POLL frame (SUB 5B)
4.  S3  →  0x41 + POLL RESPONSE (SUB A4, reports data length = 0x30)
5.  BW  →  0x41 + POLL frame (SUB 5B, offset = 0x30)
6.  S3  →  0x41 + POLL RESPONSE with data: "Instantel" + "MiniMate Plus"
7.  BW  →  SUB 06  → channel config read
8.  BW  →  SUB 15  → serial number
9.  BW  →  SUB 01  → full config block
10. BW  →  SUB 1A  → compliance config (4-frame sequence: A+B+C+D)
11. BW  →  SUB 08  → event index

6.1 Event Download Sequence (per-event, confirmed from 4-2-26 + 4-3-26 BW captures)

# Once per session:
BW  →  SUB 1E (all-zero params)    → key0, trailing0

# Per event (repeat until null sentinel data[15:19]=0x00000000):
BW  →  SUB 0A (key)                ← REQUIRED before every 1F; establishes device context
BW  →  SUB 1E (token=0xFE)         ← REQUIRED arm step; must be BETWEEN 0A and 0C
BW  →  SUB 0C (key)                ← read waveform record (peaks, timestamp, project)
BW  →  SUB 1F (token=0xFE)         ← arm 5A state machine; do NOT use returned key
BW  →  SUB 5B × 3                  ← REQUIRED: 3 full POLL probe+data cycles before 5A
BW  →  SUB 5A (key)                ← bulk waveform stream (A5 frames)
# IF 5A succeeded:
BW  →  SUB 1F (all-zero)           → next_key  ← use this for loop iteration
# IF 5A failed/timed out:
# Do NOT send 1F(browse) — use cached key from 1F(token=0xFE) above instead

Null sentinel: data[15:19] == b"\x00\x00\x00\x00" in both 1E and 1F responses. Do NOT use data[11:15] (key) as sentinel — event 0 has key=0x00000000.


7. Known Data Payloads

7.1 Poll Response (SUB A4) — Device Identity Block / Composite Container

⚠️ SUB A4 is a composite container frame. The large A4 payload (~3600+ bytes) contains multiple embedded inner sub-frames using the same DLE framing as the outer protocol (10 02 start, 10 03 end, 10 10 stuffing). Inner frames carry WRITE_CONFIRM_RESPONSE and TRIGGER_CONFIG_RESPONSE sub-frames among others. Flat byte-by-byte diffing of A4 is unreliable due to phase shifting — use inner-frame-aware diffing (_diff_a4_payloads() in s3_analyzer.py). Confirmed 2026-03-11.

Two-step read. Data payload = 0x30 bytes.

Offset 0x00:  0x08            — string length prefix
Offset 0x01:  "Instantel"     — manufacturer (null-padded to ~20 bytes)
Offset 0x15:  "MiniMate Plus" — model name (null-padded to ~20 bytes)

Raw payload (after de-stuffing):

00 00 00 08 49 6E 73 74 61 6E 74 65 6C 00 00 00 00 00 00 00 00 00 00 00 00 00
4D 69 6E 69 4D 61 74 65 20 50 6C 75 73 00 00 00 00 00 00 00 00 00

7.2 Serial Number Response (SUB EA)

Data payload = 0x0A bytes:

"BE18189\x00"  — 7 ASCII bytes + null terminator (8 bytes)
79 11          — 2 trailing bytes
Trailing Byte Value (Unit 1) Value (Unit 2) Meaning Certainty
trail[0] 0x79 0x70 Unit-specific — factory calibration ID or HW stamp? SPECULATIVE
trail[1] 0x11 0x11 Firmware minor version — 0x11 = 17 = S337.**17** CONFIRMED — 2026-02-26

Two-unit comparison data:

Unit 1: serial="BE18189"  trail=79 11   firmware=S337.17
Unit 2: serial="BE11529"  trail=70 11   firmware=S337.17

2026-02-26 — CORRECTED: Previously documented as 79 11 20 (3 bytes). 0x20 is the frame checksum, not payload data. Actual data block is exactly 10 bytes (0x0A).

2026-02-26 — CONFIRMED: trail[1] = firmware minor version. Both units share firmware S337.17 → minor = 17 = 0x11. Will change if firmware differs between units.

Still unknown: trail[0] is unit-specific. Does not derive from serial string via sum, XOR, or modulo. Possibly written at factory calibration. Needs a third unit or write-command capture to determine.

7.3 Full Config Response (SUB FE) — 0x98 bytes

Offset Raw Decoded Certainty
0x00 42 45 31 38 31 38 39 00 "BE18189\x00" — Serial number CONFIRMED
0x08 79 11 Unknown — possibly HW revision or calibration stamp SPECULATIVE
0x0A 00 01 Unknown flags SPECULATIVE
0x14 3F 80 00 00 IEEE 754 float = 1.0 (Tran scale factor) 🔶 INFERRED
0x18 41 00 00 00 IEEE 754 float = 8.0 (unknown — MicL range?) 🔶 INFERRED
0x1C 3F 80 00 00 ×6 IEEE 754 float = 1.0 ×6 (remaining channel scales) 🔶 INFERRED
0x34 53 33 33 37 2E 31 37 00 "S337.17\x00" — Firmware version CONFIRMED
0x3C 31 30 2E 37 32 00 "10.72\x00" — DSP / secondary firmware version CONFIRMED
0x53 varies Likely calibration day or time field — 0x15 (BE18189), 0x1D (BE11529) 🔶 INFERRED
0x54 10 Calibration month — BCD 0x10 = October (both units) 🔶 INFERRED
0x55 varies Calibration day — 0x02 (BE18189), 0x04 (BE11529) 🔶 INFERRED
0x560x57 07 E7 / 07 E9 Calibration year — uint16 BE. 0x07E7=2023, 0x07E9=2025 CONFIRMED — 2026-03-31
0x44 49 6E 73 74 61 6E 74 65 6C... "Instantel" — Manufacturer (repeated) CONFIRMED
0x6D 4D 69 6E 69 4D 61 74 65 20 50 6C 75 73 "MiniMate Plus" — Model name CONFIRMED

7.4 Event Index Response (SUB F7) — 0x58 bytes

2026-03-02 — CONFIRMED: Backlight and power save offsets confirmed via two independent captures with device-set values. Offsets are from the start of the data section (after the 16-byte protocol header).

Layout (offsets relative to data section start):

Offset +00:  00 58 09            — Total index size or record count ❓
Offset +03:  00 00 00 01         — Possibly stored event count = 1 ❓
Offset +07:  01 07 CB 00 06 1E  — Timestamp of event 1 (see §8)
Offset +0D:  01 07 CB 00 14 00  — Timestamp of event 2 (see §8)
Offset +13:  00 00 00 17 3B     — Unknown ❓
Offset +4B:  [backlight]         — BACKLIGHT ON TIME ✅ CONFIRMED
Offset +4C:  00                  — padding (backlight is uint8, not uint16)
Offset +53:  [power_save]        — POWER SAVING TIMEOUT ✅ CONFIRMED
Offset +54:  [lcd_hi] [lcd_lo]  — MONITORING LCD CYCLE (uint16 BE) ✅ CONFIRMED
Offset Size Type Known values Meaning Certainty
+4B 1 uint8 250, 100 BACKLIGHT ON TIME (0255 seconds per manual) CONFIRMED
+4C 1 0x00 Padding / high byte of potential uint16 🔶 INFERRED
+53 1 uint8 10, 30 POWER SAVING TIMEOUT (minutes) CONFIRMED
+54..+55 2 uint16 BE 0xFFDC = 65500 MONITORING LCD CYCLE (seconds; 65500 ≈ disabled/max) CONFIRMED

Confirmation captures:

Capture Backlight (+4B) Power Save (+53) LCD Cycle (+54/55)
20260301_160702 (BW-written) 0xFA = 250 0x0A = 10 min 0xFF 0xDC = 65500
20260302_144606 (device-set) 0x64 = 100 0x1E = 30 min 0xFF 0xDC = 65500

📖 Manual cross-reference (716U0101 Rev 15, §3.13.1):

  • Backlight On Time: "adjustable timer, from 0 to 255 seconds" (§3.13.1e)
  • Power Saving Timeout: "automatically turns the Minimate Plus off" — stored in minutes (§3.13.1f)
  • Monitoring LCD Cycle: "cycles off for the time period... set to zero to turn off" — 65500 = effectively disabled (§3.13.1g)

7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages

2026-02-26 — UPDATED: Project strings field layout confirmed by diffing compliance setup write payload (SUB 71). Client field change "Hello Claude""Claude test2" isolated exact byte position.

Project strings field layout (confirmed from SUB 71 write frame, offset +230 from frame start):

Offset  Field label (null-padded, ~16 bytes)   Field value (null-padded, ~32 bytes)
------  ------------------------------------   ------------------------------------
+0x00   "Standard Recording Setup"             ← setup name (no label)
+0x28   "Project:"                             project description string
+0x50   "Client:"                              client name string          ← confirmed at +230
+0x78   "User Name:"                           operator name string
+0xA0   "Seis Loc:"                            sensor location string
+0xC8   "Extended Notes"                       notes string

🔶 Offsets are approximate — exact byte boundaries need one more targeted capture with a known-length string change to pin down padding rules.

Confirmed ASCII strings extracted from payload:

"Project:"
"I-70 at SR 51-75978 - Loc 1 - 4256 SR51 "   ← project description
"BE18189"                                       ← serial number
"Histogram"                                     ← record type
"Standard Recording Setup"                      ← setup name
"Client:"
"Golden Triangle"                               ← client name
"User Name:"
"Terra-Mechanics Inc. - B. Harrison"            ← operator
"Seis Loc:"
"Location #1 - 4256 SR 51 - Intec"             ← sensor location
"Extended Notes"
"Tran"                                          ← Transverse channel
"Vert"                                          ← Vertical channel
"Long"                                          ← Longitudinal channel
"MicL"                                          ← Microphone / air overpressure

7.6 Channel Config Float Layout (SUB E5 / SUB 71)

CONFIRMED — 2026-03-01 from controlled captures (sessions 193237 and 151147). Trigger changed 0.500 → 0.200, then 0.200 → 0.600. Alarm changed 1.0 → 2.0. All positions confirmed.

The SUB 1A read response (E5) and SUB 71 write block contain per-channel threshold and scaling values packed as IEEE 754 big-endian floats, with inline unit strings. This layout repeats once per geophone channel (Tran, Vert, Long — 3×):

[00 00]  [max_range float]  [00 00]  [trigger float]  ["in.\0"]  [alarm float]  ["/s\0\0"]  [00 01]  [chan_label...]
          40 C6 97 FD                 3F 19 99 9A       69 6E 2E   40 00 00 00    2F 73 00 00
          = 6.206                     = 0.600 in/s      "in."      = 2.000 in/s   "/s"
Field Example bytes Decoded Certainty
[00 00] 00 00 Separator / padding 🔶 INFERRED
Max range float 40 C6 97 FD 6.206 — full-scale range in in/s 🔶 INFERRED
[00 00] 00 00 Separator / padding 🔶 INFERRED
Trigger level 3F 19 99 9A 0.600 in/s — IEEE 754 BE float CONFIRMED
Unit string 69 6E 2E 00 "in.\0" CONFIRMED
Alarm level 40 00 00 00 2.000 in/s — IEEE 754 BE float CONFIRMED
Unit string 2F 73 00 00 "/s\0\0" CONFIRMED
[00 01] 00 01 Unknown flag / separator 🔶 INFERRED
Channel label e.g. 56 65 72 74 "Vert" — identifies which channel CONFIRMED

State transitions observed across captures:

Capture Trigger Alarm Notes
193237 (read) 3F000000 = 0.500 3F800000 = 1.000 Device state before any change
193237 (write 1) 3E4CCCCD = 0.200 3F800000 = 1.000 Trigger changed only
151147 (write 1) 3E4CCCCD = 0.200 40000000 = 2.000 Alarm changed, trigger carried over
151147 (write 2) 3F19999A = 0.600 40000000 = 2.000 Trigger changed, alarm carried over

Values are stored natively in imperial units (in/s) — unit strings "in." and "/s" embedded inline confirm this regardless of display locale.

7.6.1 Record Time

CONFIRMED — 2026-04-01 (BE11529 / firmware S338.17). Updated from 2026-03-09 offset-based confirmation; anchor approach supersedes the +0x28 absolute offset.

Record time is stored as a 32-bit IEEE 754 float, big-endian, located via an anchor pattern (see §7.6.3 below).

Anchor-relative location: search for the 10-byte sequence \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 in cfg[0:150]. Record time float is at anchor + 10.

2026-04-02 — CORRECTED: Search range was cfg[40:100]. With the compliance-config orphaned-send bug fixed (§7.6.2), the 44-byte accidental header padding is gone and the anchor now appears at cfg[11]. Search range widened to cfg[0:150].

Record Time float32 BE bytes Decoded
3 seconds 40 40 00 00 3.0
5 seconds 40 A0 00 00 5.0
7 seconds 40 E0 00 00 7.0
8 seconds 41 00 00 00 8.0
10 seconds 41 20 00 00 10.0
13 seconds 41 50 00 00 13.0

⚠️ Do NOT use absolute offset +0x28 from page2 start for BE11529. The total cfg length varies by ±1 byte depending on sample rate setting (see §7.6.3 DLE jitter note). Absolute offsets are unreliable; anchor search is the correct approach.

0x0A after "Extended Notes" label: invariant across record time changes — not record time.

0x082A (= 2090) — RESOLVED: Fixed payload length of the E5 response block. Constant regardless of any setting.


7.6.2 SUB 1A Multi-Frame Read Protocol

CONFIRMED — 2026-04-01 (BE11529 / firmware S338.17). Reverse-engineered from raw_bw_20260311_155355.bin (Blastware TX capture).

SUB 1A (compliance config read) requires four frames, not the standard 2-step probe+fetch used by other SUBs. The Blastware TX frame format for these is ACK (0x41) + STX (0x02) + [DLE-stuffed body] + ETX (0x03) with CMD=0x10.

Frame sequence:

Step Name BW offset param Notes
A Probe 0x0000 Triggers E5 response with page_key=0x0000 (44-byte header)
B Data B 0x0400 params: 00 00 00 00 00 00 00 64 00 00; E5 page_key=0x0000 (44 bytes)
C Data C 0x0400 params: 00 00 04 00 00 00 00 64 00 00; E5 page_key=0x0010 (~1027 bytes)
D Data D 0x002A params: 00 00 08 00 00 00 00 64 00 00; E5 page_key=0x0010 (~1055 bytes, or 0x0000/44 bytes when BE11529 sends a duplicate)

E5 response page_key field: bytes data[3:5] of the E5 payload. 0x0000 = header/short chunk, 0x0010 = main config data.

Assembling cfg: concatenate non-duplicate chunks in order (B+C+D). Track (page_key, chunk_len) pairs; drop any repeat to avoid mis-alignment.

Expected cfg lengths:

  • Frame D delivers page 0x0010 correctly: 44 + 1027 + 1055 = 2126 bytes
  • Frame D duplicates page 0x0000 (BE11529 occasional): 44 + 1027 = 1071 bytes (still fully decodable)

7.6.3 Sample Rate and DLE Jitter

CONFIRMED — 2026-04-01 (BE11529 / firmware S338.17). Validated across Normal (1024), Fast (2048), and Faster (4096) modes.

Location: uint16 BE at anchor 2, where anchor = \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 in cfg[40:100].

Device Mode uint16 BE value Sa/s
Normal 04 00 1024
Fast 08 00 2048
Faster 10 00 4096

DLE jitter (critical — explains the ±1 byte cfg length variation):

The sample rate bytes sit immediately before a 0x10 (DLE) prefix byte in the raw S3 frame. For the "Faster" mode (4096 = 0x1000), the high byte 0x10 is itself a DLE character and must be escaped in the S3 frame as 10 10. After DLE unstuffing: 10 10 0010 00 (2 bytes). For Normal/Fast modes (high byte = 0x04/0x08), no escaping needed: payload stays 3 bytes. Result: "Faster" mode produces a cfg that is 1 byte shorter than Normal/Fast, shifting all subsequent absolute offsets by 1.

Why anchor search is required: any decoder that uses fixed absolute offsets for record_time or sample_rate will produce garbage values when the device is set to "Faster" mode. The 10-byte anchor \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 (0x012C = 300 s max-record-length constant, followed by two alarm-level floats \xbe\x80\x00\x00\x00\x00) straddles the boundary and is unaffected by this shift.


7.7 Blastware .set File Format

🔶 INFERRED — 2026-03-01 from Standard_Recording_Setup.set cross-referenced against known wire payloads.

Blastware's "save setup to disk" feature produces a binary .set file that is structurally identical to the wire protocol payload, but with all multi-byte values in little-endian byte order (Windows-native) rather than the big-endian order used on the wire. No DLE framing, no checksums — raw struct dump.

File layout (2522 bytes observed):

0x0000  Header / metadata block (~40 bytes) — partially decoded
0x002A  "Standard Recording Setup.set\0" — setup filename, null-padded
0x0078  Project strings block — same layout as SUB 71 wire payload
          "Project:\0" + value, "Client:\0" + value, "User Name:\0" + value,
          "Seis Loc:\0" + value, "Extended Notes\0" + value
0x06A0  Channel records block — one record per channel (geo×3 + mic×1 + duplicates)
0x0820  Device info block — serial number, firmware, model strings
0x08C0  Event index / timestamp block
0x0910  Histogram / reporting config
0x09D0  Trailer (10 bytes)

Per-channel record layout (little-endian, ~46 bytes per channel):

offset  size  type      value (Tran example)   meaning
+00     2     uint16    0x0001                 channel type (1=geophone, 0=mic)
+02     4     char[4]   "Tran"                 channel label
+06     2     uint16    0x0000                 padding
+08     2     uint16    0x0001                 unknown
+0A     2     uint16    0x0050 = 80            unknown (sensitivity? gain?)
+0C     2     uint16    0x000F = 15            unknown
+0E     2     uint16    0x0028 = 40            unknown
+10     2     uint16    0x0015 = 21            unknown
+12     4     bytes     03 02 04 01            flags (recording mode etc.)
+16     4     uint32    0x00000003             record time in seconds ✅ CONFIRMED
+1A     4     float32   6.2061                 max range (in/s for geo, psi for mic)
+1E     2               00 00                  padding
+20     4     float32   0.6000                 trigger level ✅ CONFIRMED
+24     4     char[4]   "in.\0" / "psi\0"      unit string (geo vs mic)
+28     4     float32   2.0000                 alarm level ✅ CONFIRMED
+2C     4     char[4]   "/s\0\0" / varies      unit string 2

MicL channel differences:

  • channel_type = 0 (vs 1 for geophones)
  • trigger = 0.009, alarm = 0.021 (in psi)
  • unit string = "psi\0" instead of "in.\0"confirms MicL units are psi

Endianness summary:

Context Byte order Example (0.6 in/s trigger)
.set file Little-endian 9A 99 19 3F
Wire protocol (SUB 71 / E5) Big-endian 3F 19 99 9A

0x082A — still unidentified. Record time in the .set file = 0x00000003 (3 sec), which would be 00 00 00 03 on wire — not 0x082A. The original sessions had record time = 2, which would be 00 00 00 02. 0x082A = 2090 doesn't match any obvious record time encoding. May correspond to one of the unknown uint16 fields at +0A through +10. A capture changing sample rate or histogram interval would help isolate it.


7.8 Trigger / Advanced Config Write Frame (BW→S3 SUB 0x82)

CONFIRMED — 2026-03-09 from controlled BW-side capture diff (Trigger Sample Width 4→3).

SUB 0x82 is the BW→S3 write command for the advanced trigger configuration block. It is the write counterpart to the S3→BW read response SUB 0xD1 (0xFF 0x82 = 0x7D is a separate sub; the D1/2E read pair is distinct). The 0x82 write frame is only visible in raw_bw.bin — it does not appear in S3-side compliance dumps.

Destuffed BW write frame layout (47 raw bytes → 46 destuffed):

offset  value   meaning
[00]    0x10    addr (literal 0x10 after destuffing)
[01]    0x00    unknown
[02]    0x82    SUB: advanced config write
[03]    0x00    unknown
[04]    0x00    unknown
[05]    0x1C    length = 28 bytes (payload size)
[06..10] 00..  header/padding
[11..16] 00..  header/padding
[17]    0x1A    unknown (constant 26 = 0x1A)
[18]    0xD5    unknown (constant)
[19]    0x00    unknown
[20]    0x00    unknown
[21]    0x10    literal 0x10 (stuffed in raw frame as 10 10)
[22]    0x04/0x03  Trigger Sample Width ✅ CONFIRMED (uint8, samples)
[23]    0x0A    unknown (constant 10; NOT Auto Window)
[24..43] 0xFF.. padding
[44]    0x00    unknown
[45]    checksum

Confirmed Trigger Sample Width values:

Width setting Byte [22]
4 samples 0x04
3 samples 0x03
2 samples (default) 0x02 (expected — not yet captured)

Known constants in this frame: [17]=0x1A, [18]=0xD5, [23]=0x0A. These do not change with Trigger Sample Width changes. Byte [23] = 10 was initially a candidate for Auto Window (range 19) but cannot be Auto Window because 10 is outside the valid range.

Mode gating: This write frame is only transmitted when Blastware performs a Send To Unit operation in Compliance / Single-Shot / Fixed Record Time mode. The frame is absent from other session types.


7.9 Mode Gating — Protocol Architecture Note

CONFIRMED — 2026-03-09 from controlled captures and null-change experiments.

Several settings are mode-gated: the device only transmits (reads) or accepts (writes) certain fields when the appropriate operating mode is active. This is an architectural property of the protocol, not a gap in capture methodology.

Observed mode gating:

Setting Gate Condition Evidence
Trigger Sample Width Compliance / Single-Shot / Fixed Record Time mode Not visible in S3-side reads; only in BW write frame (SUB 0x82) when mode is active
Auto Window Record Stop Mode = Auto Capture of 3→9 change in Fixed mode produced zero wire change in all frames (F7, D1, E5 all identical)

Implication for captures: To map a mode-gated setting, you must first activate the gating mode on the device, then perform the compliance dump or write capture. Changing the setting value while in the wrong mode will produce no observable wire change.

Suspected mode-gated settings not yet captured:

  • Auto Window (requires Record Stop Mode = Auto)
  • Auxiliary Trigger (unknown gate condition)

7.5 Full Waveform Record (SUB F3) — 0xD2 bytes (210 bytes)

Updated 2026-03-31 — Full layout confirmed. See §7.7.5 for the complete record structure including timestamp, record type, PPV float positions, and project strings.

Peak values are found by searching for channel label strings "Tran", "Vert", "Long", "MicL" and reading float32 BE at label_offset + 6. The floats are not 4-byte aligned — confirmed from 3-31-26 capture.

Example peak values (event 1 from 3-31-26):

Tran:  3D BB 45 7A  =  0.0916 in/s
Vert:  3D B9 56 E1  =  0.0907 in/s
Long:  3D 75 C2 7C  =  0.0605 in/s
MicL:  39 BE 18 B8  =  0.000145 psi  ✅ units confirmed

Example peak values (event 2 from earlier capture):

Tran:  3D 56 CB B9  =  0.0521 in/s
Vert:  3C F5 C2 7C  =  0.0300 in/s
Long:  3C F5 C2 7C  =  0.0300 in/s
MicL:  39 64 1D AA  =  0.0000875 psi

⚠️ The record is delivered as data_rsp.data[11:11+0xD2] — the outer data section header (LENGTH_ECHO, KEY_ECHO) occupies data[0..10]. Callers of read_waveform_record() receive the 210-byte record directly.

7.6 Bulk Waveform Stream (SUB A5) — Raw ADC Sample Records

Two distinct formats exist depending on recording mode. Both confirmed from captures.


7.6.1 Blast / Waveform mode — CONFIRMED (4-2-26 capture)

4-channel interleaved signed 16-bit little-endian, 8 bytes per sample-set:

[T_lo T_hi  V_lo V_hi  L_lo L_hi  M_lo M_hi]  × N sample-sets
  • T = Transverse (Tran), V = Vertical (Vert), L = Longitudinal (Long), M = Microphone
  • Channel order follows the Blastware convention: Tran is always first (ch[0]).
  • Encoding: signed int16 little-endian. Full scale = ±32768 counts.
  • Sample rate: set by compliance config (typical: 1024 Hz for blast monitoring).
  • Each A5 frame chunk carries a different number of waveform bytes. Frame sizes are NOT multiples of 8, so naive concatenation scrambles channel assignments at frame boundaries. Always track cumulative byte offset mod 8 to correct alignment.

A5[0] frame layout:

db[7:]:   [11-byte header]  [21-byte STRT record]  [6-byte preamble]  [waveform ...]
STRT:     offset 11 in db[7:]
           +0..3  b'STRT'     magic
           +8..9  uint16 BE   total_samples  (full-record expected sample-set count)
          +16..17 uint16 BE   pretrig_samples (pre-trigger window, in sample-sets)
          +18     uint8       rectime_seconds
preamble: +19..20 0x00 0x00   null padding
          +21..24 0xFF × 4    synchronisation sentinel
Waveform: starts at strt_pos + 27 within db[7:]

A5[1..N] frame layout (non-metadata frames):

db[7:]:   [8-byte per-frame header]  [waveform ...]
Header:   [counter LE uint16, 0x00 × 6]  — frame sequence counter (0, 8, 12, 16, 20, …×0x400)
Waveform: starts at byte 8 of db[7:]

Special frames:

Frame index Contents
A5[0] Probe response: STRT record + first waveform chunk
A5[7] Event-time metadata strings only (no waveform data)
A5[9] Terminator frame (page_key=0x0000) — ignored
A5[1..6,8] Waveform chunks

Confirmed from 4-2-26 blast capture (total_samples=9306, pretrig=298, rate=1024 Hz):

Frame  Waveform bytes  Cumulative  Align(mod 8)
A5[0]       933B           933B        0
A5[1]       963B          1896B        5
A5[2]       946B          2842B        0
A5[3]       960B          3802B        2
A5[4]       952B          4754B        2
A5[5]       946B          5700B        2
A5[6]       941B          6641B        4
A5[8]       992B          7633B        1
Total:     7633B  → 954 naive sample-sets, 948 alignment-corrected

Only 948 of 9306 sample-sets captured (10%) — stop_after_metadata=True terminated download after A5[7] was received.

Channel identification note: The 4-2-26 blast saturated all four geophone channels to near-maximum ADC output (~3200032617 counts). Channel ordering [Tran, Vert, Long, Mic] = [ch0, ch1, ch2, ch3] is the Blastware convention and is consistent with per-channel PPV values (Tran=0.420, Vert=3.870, Long=0.495 in/s from 0C record), but cannot be independently confirmed from a fully-saturating event alone.


7.6.2 Noise monitoring / Histogram mode — CONFIRMED (3-31-26 capture)

32-byte blocks with the following layout:

Offset  Size  Type     Description
 0       2    uint16 LE  block type: 0x0016=sync, 0x0000=data
 2       2    uint16 LE  block counter (ctr)
 4      18    int16 LE × 9  ADC samples
22      10    bytes      metadata: [00 01 43 VAR VAR 00 pretrig rectime 00 00]
  • Sync blocks (type=0x0016) appear at the start of each A5 frame; ctr=0 in sync blocks.
  • Data blocks (type=0x0000) carry actual sample data. First data block ctr=288 (empirical, not yet decoded — likely related to a pre-trigger sample offset).
  • Metadata fixed bytes: 00 01 43 then 2 variable bytes, then 00 [pretrig] [rectime] 00 00. Pretrig byte = 0x1E (30) and rectime byte = 0x0A (10) for the 3-31-26 capture.
  • 9 samples per block (int16 LE, NOT big-endian). Noise floor ≈ 911 counts.
  • This is a different recording mode from waveform/blast — the device firmware uses 32-byte blocks for histogram/noise monitoring and 4-channel continuous for waveform events.

Open: The 9-sample-per-block structure does not divide evenly into 4 channels. Whether these represent a single channel, all channels in rotation, or downsampled aggregates is not yet determined. The first data block ctr=288 vs pretrig=30 is also unexplained — possibly counting in units other than sample-sets.



7.7 Event Download Protocol — Confirmed from 3-31-26 Capture

Added 2026-03-31. All findings confirmed from live bridge capture bridges/captures/3-31-26/raw_bw_20260331_200245.bin + raw_s3_20260331_200245.bin (148 BW frames / 147 S3 frames). Analysis scripts: parsers/analyze_3_31_26.py.

Overview

Event download uses four SUBs in a key-driven iterator loop. The "waveform key" is a 4-byte opaque record address that uniquely identifies one histogram bin or waveform record on the device's internal storage.

Step BW SUB S3 Response Purpose
1 (once) 1E — EVENT_HEADER E1 Get the first waveform key
2 0A — WAVEFORM_HEADER F5 Check record type / confirm full bin
3 0C — WAVEFORM_RECORD F3 Download 210-byte record (peaks, project, timestamp)
4 1F — EVENT_ADVANCE E0 Advance iterator, get next key
↑ repeat steps 24 until key == 00 00 00 00

Blastware optimisation (confirmed): Step 2 (0A) is only called for the first key. Subsequent keys come from 1F with token 0xFE (download mode), which guarantees they are full records — so Blastware skips 0A and jumps directly to 0C. Our implementation follows the same pattern.


7.7.1 Waveform Key

The waveform key is a 4-byte opaque record address (uint32, likely a flash sector offset or circular-buffer pointer internal to the S3 DSP).

  • First key: returned by 1E at data[11:15]
  • Subsequent keys: returned by 1F at data[11:15]
  • Terminator: 00 00 00 00 signals no more events

Example keys from 3-31-26 capture (one Blastware "event" / 4 histogram bins):

01 11 00 16   ← first bin  (full, 0x30 length)
01 11 11 B6   ← second bin (partial, 0x26 length — skipped by 1F/0xFE)
01 11 11 F6   ← third bin  (partial, 0x26 length — skipped)
01 11 12 36   ← fourth bin (full, 0x30 length — returned by 1F/0xFE)
00 00 00 00   ← terminator

7.7.2 Token Byte (SUB 1E / 1F)

A token byte at payload[12] (= params[6] in build_bw_frame) controls the 1F advance behaviour:

Token Mode Behaviour
0x00 Browse Advance one record, including partial histogram bins
0xFE Download Skip partial bins, advance to the next full record

We always use 0xFE — it minimises round trips and avoids needing to handle partial-bin 0C calls.


7.7.3 Variable DATA_LENGTH for SUB 0A (WAVEFORM_HEADER)

Unlike all other SUBs, 0A does NOT have a fixed data length. The length is returned in the probe response at data[4]:

Length Meaning
0x30 Full histogram bin — has a waveform record to download
0x26 Partial histogram bin — no waveform record

Both the probe and data-request frames carry the same key in params[4..7]. The read_waveform_header() method in protocol.py reads probe.data[4] and uses that value as the data-request offset.


7.7.4 Response Data Section Layout

All S3 event download responses share this data section prefix:

data[0]    LENGTH_ECHO  — echoes the request DATA_LENGTH byte
data[1..4] 00 00 00 00  — four zero bytes
data[5..8] KEY_ECHO     — echoes the 4-byte waveform key from the request
data[9..10] 00 00       — two zero bytes
data[11..] ACTUAL_DATA  — real payload starts here

Actual data lengths:

  • 1E response (E1): data[11:19] — 8 bytes (data[11:15] = key4)
  • 0A probe response (F5): data[4] = variable length (0x30 or 0x26)
  • 0A data response (F5): data[11:11+length] — waveform header bytes
  • 0C data response (F3): data[11:11+0xD2] — 210-byte waveform record
  • 1F response (E0): data[11:15] = next key4; data[8] = token echo

7.7.5 Waveform Record Layout (210 bytes, SUB F3 → response F3)

Updated 2026-04-01 — Full timestamp layout confirmed against Blastware event report (BE11529 thump event, "00:28:12 April 1, 2026"). Record type encoding corrected (byte[1], not ASCII string search). Peak Vector Sum field confirmed at fixed offset 87.

The 210-byte record (data_rsp.data[11:11+0xD2]) contains:

Header / Timestamp (9 bytes at offsets 08, CONFIRMED 2026-04-01):

byte[0]:    day               (uint8)
byte[1]:    sub_code          0x10 = Waveform (continuous/single-shot) ✅
                              histogram code not yet captured ❓
byte[2]:    month             (uint8)
bytes[34]: year              (uint16 big-endian)
byte[5]:    unknown           (0x00 in all observed samples ❓)
byte[6]:    hour              (uint8)
byte[7]:    minute            (uint8)
byte[8]:    second            (uint8)

Thump event raw bytes (2026-04-01 00:28:12):

01  10  04  07 ea  00  00  1c  0c
↑   ↑   ↑   ↑──↑  ↑   ↑   ↑   ↑
d=1 sub m=4 y=2026 ?  h=0 m=28 s=12

Cross-referenced against the .MLG file for the same event, which stores an 8-byte timestamp at two offsets (trigger time and end time):

MLG format: [day:1][month:1][year:2 LE][?:1][hour:1][min:1][sec:1]
01 04 ea 07 00 00 1c 0c   → trigger at April 1, 2026 00:28:12
01 04 ea 07 00 00 1c 0f   → end time  April 1, 2026 00:28:15  (3.0 s record time ✅)

Record type — encoded in byte[1] (sub_code), NOT as an ASCII string:

  • 0x10"Waveform" (continuous / single-shot mode)
  • histogram sub_code: not yet confirmed — capture a histogram event with debug=true

Peak particle velocity floats ( CONFIRMED 2026-03-31, re-confirmed 2026-04-01):

Channel labels "Tran", "Vert", "Long", "MicL" are embedded as ASCII strings at variable offsets within the record. The PPV float for each channel is at label_offset + 6 (IEEE 754 big-endian float32).

The floats are NOT 4-byte aligned — Tran, Long, and MicL all fall at non-aligned offsets. The previous heuristic step-4 scanner missed all three.

Confirmed offsets from thump event (2026-04-01, cross-referenced vs Blastware):

"Tran" at offset  99  →  float at 105  =  0x3ED70A2D  =  0.420 in/s  ✅ Blastware: 0.420
"Vert" at offset 114  →  float at 120  =  0x4077AE01  =  3.870 in/s  ✅ Blastware: 3.870
"Long" at offset 129  →  float at 135  =  0x3EFD7090  =  0.495 in/s  ✅ Blastware: 0.495
"MicL" at offset 144  →  float at 150  =  0x3985114E  =  0.000254 psi

Channel labels are separated by inner-frame bytes 10 03 (DLE ETX), preserved as literal data by S3FrameParser.

Peak Vector Sum ( CONFIRMED 2026-04-01):

Offset 87:  IEEE 754 big-endian float32
            = √(Tran² + Vert² + Long²) at the sample instant of maximum
              combined geo motion
            NOT the vector sum of the three per-channel peaks (those may
            occur at different sample times)

Thump event:  0x4079F6C5  =  3.906 in/s  ✅ matches Blastware "Peak Vector Sum: 3.906 in/s"
Near-ambient: 0x3C75C28F  =  0.015 in/s  (histogram event, near-zero ambient)

Project strings — ASCII label-value pairs (search for label, read null-terminated value):

"Project:"      → project description   (in 0C record ✅)
"Client:"       → client name           (in SUB 5A / A5 frame 7 ✅  — NOT in 0C)
"User Name:"    → operator / user       (in SUB 5A / A5 frame 7 ✅  — NOT in 0C)
"Seis Loc:"     → sensor location       (in SUB 5A / A5 frame 7 ✅  — NOT in 0C)
"Extended Notes"→ notes field           (in SUB 5A / A5 frame 7 ✅)

2026-04-02 — CONFIRMED: Client:, User Name:, and Seis Loc: are sourced from SUB 5A (bulk waveform stream), specifically A5 frame 7 of the multi-frame response. They are NOT present in the 210-byte SUB 0C waveform record. The strings reflect the compliance setup that was active when the event was recorded on the device — making SUB 5A the authoritative source for true event-time metadata. The get_events() client method now issues a SUB 5A request after each 0C download (stop_after_metadata=True) and overwrites event.project_info with the decoded fields.


7.7.6 Complete Download Loop (Python pseudocode)

key4, _ = proto.read_event_first()            # SUB 1E
if key4 == b'\x00\x00\x00\x00':
    return []  # no events

events = []
is_first = True

while key4 != b'\x00\x00\x00\x00':
    if is_first:
        _header, rec_len = proto.read_waveform_header(key4)  # SUB 0A
        is_first = False
        if rec_len < 0x30:
            key4 = proto.advance_event()   # skip partial first bin
            continue

    record = proto.read_waveform_record(key4)   # SUB 0C (0xD2 bytes)
    events.append(decode(record))

    key4 = proto.advance_event()                # SUB 1F (token=0xFE)

return events

7.7.7 Updated Download Loop with SUB 5A Metadata

Added 2026-04-02. Confirmed working on BE11529 over TCP/cellular.

key4, _ = proto.read_event_first()                       # SUB 1E
if key4 == b'\x00\x00\x00\x00':
    return []

events = []
is_first = True

while key4 != b'\x00\x00\x00\x00':
    if is_first:
        _header, rec_len = proto.read_waveform_header(key4)  # SUB 0A
        is_first = False
        if rec_len < 0x30:
            key4 = proto.advance_event()
            continue

    record = proto.read_waveform_record(key4)            # SUB 0C (0xD2 bytes)
    event  = decode(record)

    a5_data = proto.read_bulk_waveform_stream(            # SUB 5A → A5 frames
        key4, stop_after_metadata=True)
    client._decode_a5_metadata_into(a5_data, event)       # overwrites project_info

    events.append(event)
    key4 = proto.advance_event()                          # SUB 1F (token=0xFE)

return events

7.8 SUB 5A — Bulk Waveform Stream (event-time metadata)

Added 2026-04-02. Frame format confirmed by reproducing Blastware wire bytes byte-for-byte from the 1-2-26 BW capture.

SUB 5A initiates a bulk transfer of the raw sample data for a stored event. The response is a sequence of A5 frames. Frame 7 (0-indexed) contains the full compliance setup as it existed when the event was recorded — including Client:, User Name:, Seis Loc:, and Extended Notes ASCII label-value pairs.

7.8.1 Frame Format

SUB 5A uses a non-standard frame layout that differs from all other BW→S3 write commands.

[ACK][STX][10][10][00][5A][00][offset_hi][offset_lo][params...][chk][ETX]
  41  02  10  10  00  5A  00     ^^raw^^    ^^raw^^  ^^stuffed^^

Two critical differences from build_bw_frame:

  1. offset_hi is sent raw, not DLE-stuffed. When offset_hi = 0x10, the wire carries a bare 0x10 — NOT the stuffed 10 10 that build_bw_frame would produce. The device ignores frames where this byte is incorrectly stuffed.

  2. DLE-aware checksum. Walking the full frame byte sequence: when a 10 XX pair is seen, only XX is added to the running sum; lone bytes are added normally.

7.8.2 Request Sequence

Frame offset_word counter params Purpose
Probe 0x1004 0x0000 10 bytes (bulk_waveform_params(0)) Initiate transfer
Chunk 1 0x1004 0x0400 11 bytes First data chunk
Chunk 2 0x1004 0x0800 11 bytes Second chunk
Chunk N 0x1004 N * 0x0400 11 bytes Nth chunk
Termination 0x005A last + 0x0400 10 bytes End transfer

⚠️ 2026-04-06 CORRECTED — chunk counter is monotonic for ALL chunks. The 4-2-26 BW TX capture showed counter=0x1004 for chunk 1, which was hardcoded as a special case. This was a Blastware artifact. Empirically confirmed: counter=0x0400 for chunk 1 works correctly; counter=0x1004 causes the device to time out. The device does NOT strictly validate the counter value — it streams data for any valid 5A request for the given key. Use chunk_num * 0x0400 (monotonic) for all chunks. BW's true internal formula is key4[2:4] + n * 0x0400. For event 1 (key 01110000) this equals n * 0x0400 since key4[2:4] = 0x0000. The monotonic formula is correct for all keys encountered on this device.

The stop_after_metadata=True flag causes the loop to stop as soon as b"Project:" is found in the accumulated A5 frame data, typically after 79 chunks. A termination frame is always sent before returning.

7.8.3 A5 Frame Layout

Each A5 response frame contains a chunk of raw bulk data. Frame 7 of the stream carries the compliance text block with all project-info label-value pairs. The client layer searches for ASCII labels with a null-terminated value read:

"Project:"       → null-terminated project name
"Client:"        → null-terminated client name
"User Name:"     → null-terminated operator name
"Seis Loc:"      → null-terminated sensor location
"Extended Notes" → null-terminated notes

All five fields reflect the setup at event-record time, not the current device config.

7.8.4 End-of-Stream Behaviour and Chunk Timing

Confirmed 2026-04-06 — empirical observation on BE11529 (S338.17) over TCP/cellular.

End-of-stream signal: After sending all waveform chunks, the device sends exactly 1 raw byte in response to the next chunk request, then goes silent. This byte is not a complete DLE-framed A5 response — S3FrameParser.bytes_fed reports 1 and no frame is ever assembled. This is the device's natural end-of-stream indicator.

Handling logic in read_bulk_waveform_stream:

TimeoutError caught:
  if bytes_fed > 0 AND frames already collected:
      → graceful end-of-stream; break loop; proceed to termination frame
  else (bytes_fed == 0, no prior frames):
      → genuine transport failure; re-raise

Chunk timing (BE11529, 1024 sps, TCP/cellular):

Metric Observed value
Chunk response time ~1 s per chunk
Chunks for a 9,306-sample event 35 chunks
Data per chunk (active signal) 1,0361,123 bytes
Data per chunk (post-event silence) 1,036 bytes (uniform)
Safe recv timeout per chunk 10 s (10× typical)
Default transport timeout 120 s → ~2-min stall at end-of-stream

Chunks with uniform 1,036-byte payload (chunks 1735 in the observed event) contain all-zero ADC samples — the device continues recording silence until the configured record time expires before terminating the stream.

ADC count-to-physical conversion:

Raw samples are signed 16-bit integers (32,768 to +32,767). To convert to physical units:

value_in_s (in/s) = counts × (geo_range / 32767)

where geo_range is from the compliance config (typically 10.000 in/s). Mic channel uses psi units with its own range. Near-full-scale values on all channels simultaneously indicate ADC saturation (clipping).

Known decoder issue — fi==9 hardcoded skip:

_decode_a5_waveform() contains elif fi == 9: continue from an earlier assumption that frame index 9 is always the device terminator. For streams with more than 9 frames, frame 9 is live waveform data. The skip discards ~1,070 bytes (~133 sample-sets) per event. Terminator detection should use page_key == 0x0000, not frame index. This skip should be removed.


7.9 Compliance Config Field Inventory (Blastware UI, 2026-04-08)

Fields visible in the Blastware "Compliance Setup" dialog. = byte offset confirmed in code. = not yet located in raw bytes.

Recording Setup tab

Field Values / Type Status
Recording Mode Continuous / Single Shot / Histogram
Record Stop Mode Fixed Record Time / Auto / Manual Stop
Sample Rate Standard 1024 / Fast 2048 / Faster 4096 sps sample_rate (anchor2)
Record Time float, seconds (3, 5, 8, 10, 13…) record_time (anchor+10)
Histogram Interval 5 / 15 / 30 / 60 min (mode-gated behind Histogram mode)
Storage Mode Save All Data / Save Triggered
Geophone Type Standard Triaxial / 4.5 Hz Geophone
Geophone — Enable all bool
Geophone — Trigger Source bool
Chan 1-3 Trigger Level float, in/s trigger_level_geo
Chan 1-3 Maximum Range Normal 10.000 / 1.25 in/s max_range_geo
Microphone — Enable all bool
Microphone — Trigger Source bool
Chan 4 Trigger Level float, dB or psi

Notes tab

Field Values / Type Status
Enable User Notes bool
Project ASCII string (sourced from A5 frame 7 via SUB 5A)
Client ASCII string (sourced from A5 frame 7)
User Name ASCII string (sourced from A5 frame 7)
Seis Loc ASCII string (sourced from A5 frame 7)
Enable Extended Notes bool
Extended Notes ASCII text
Extended Notes Title ASCII string
Enable Job Number bool
Job Number int
Enable Scaled Distance bool
Distance from Blast float
Charge Weight float

Special Setups tab

Field Values / Type Status
Unit Timer Mode Off / On
Start Date/Time date+time
Stop Date/Time date+time
Self Check Mode Off / On
Self Check Time HH:MM
Sensor Check Before monitoring / After each event / Disabled not yet located
Measurement Units Imperial / Metric
Show Mic units in dB bool
Time Format 24 Hour / 12 Hour (AM/PM)
Backlight on Time int, seconds (0255) event index block +75
Power Saving Timeout int, minutes event index block +83
Monitoring LCD Cycle int event index block +84:86 (uint16 BE)
Set unit time with setup bool

Note on Sensor Check: The dropdown has three states — "Before monitoring", "After each event", "Disabled". The user's unit always runs "Before monitoring", so no enable/disable diff capture has been done yet. Finding this byte requires a capture with Disabled vs Before-monitoring with all other settings held constant.


7.10 SUB 0x0E — Channel Sensor Data & SUB 0x98 — Trigger Test 2026-04-08

Both confirmed from 4-8-26/sensor-check capture (Blastware "Unit Channel Test" comms check).

SUB 0x0E — Channel Sensor Data

Standard two-step read. Data length: 0x0A (10 bytes) per channel.

Request params: Channel selector in params[6:8] (bytes 6 and 7 of the 10-byte params field).

params[6:8] Channel
00 00 Channel 0 (Transverse)
00 01 Channel 1 (Vertical)
10 02 Channel 2 (Longitudinal) — note 0x10 in params[6]
00 03 Channel 3
10 04 Channel 4 — note 0x10 in params[6]
00 05 Channel 5
00 06 Channel 6
00 07 Channel 7

Response SUB = 0xFF 0x0E = 0xF1.

Response data (10 bytes at section = data[11:]):

The data response payload for each channel is 10 bytes. From the sensor-check S3 capture, channels 0 and 1 have non-zero data; channels 57 are all-zero (no sensors connected to those channels). Full byte-level field mapping NOT YET done — data includes overswing ratio, frequency, and amplitude per the Blastware "Unit Channel Test" dialog columns.

Example (channel 0, first pass):

data[11:] = 00 01 00 00 4A 00 25 10 02 0A  (17 bytes including probe + data frames combined)

Probe frame response confirms data length in page bytes; data frame carries the 10-byte channel reading.

Blastware "Unit Channel Test" sequence (confirmed 4-8-26/sensor-check):

SESSION_RESET + POLL × 3 (startup — SESSION_RESET required for monitoring unit)
SUB 0x08 (event index read)
POLL × 8 (Blastware polls while waiting; unit may be in sensor check during this window)
SUB 0x15 (serial number, data length 0x0A)
SUB 0x01 (device info, data length 0x98)
SUB 0x08 (event index again)
SUB 0x01 (device info again)
SUB 0x0E × 8 channels (pass 1 — channels 07, probe+data each)
  [~40 second gap if unit is performing on-device sensor check]
SUB 0x0E × 8 channels (pass 2 — same channels, fresh ADC readings)
SUB 0x98 × 2 (trigger test)

SUB 0x98 — Trigger Test

Single probe frame only. No data step.

Request format: params[0] = 0xFF (all other param bytes = 0x00). Sent as a single frame (probe step only, no data step).

Response SUB = 0xFF 0x98 = 0x67. Response is a standard 17-byte zero-data S3 frame.

Wire bytes for request (confirmed frame 52 of 4-8-26 BW capture):

41 02 10 10 00 98 FF 00 00 00 00 00 00 00 00 00 00 00 00 A7 03

Blastware sends SUB 0x98 twice per comms-check cycle.

SUB 0x01 — Device Info Block

Observed in Blastware comms-check init sequence (before 0x0E channel reads). Standard two-step read with data length 0x98 (152 bytes).

Response SUB = 0xFE (= 0xFF 0x01 = 0xFE).

⚠️ Naming collision: The existing SUB 0xFE (Full Config Read, RSP 0x01) and this SUB 0x01 (RSP 0xFE) are inverse commands. They are NOT the same command. SUB 0xFE is a full 166-byte config block used for firmware version, calibration date, etc. SUB 0x01 returns 152 bytes and appears to contain a subset of device identification data.

Observed payload (data[11:], first ~40 bytes of 161 total):

00 00 00 00 00 42 45 31 31 35 32 39 00 ...  "BE11529\0"

Contains serial number, firmware bytes, and floating-point calibration fields. Full field map NOT YET done.


7.11 Erase-All Protocol (SUBs 0xA3 / 0xA2 / 0x06) 2026-04-11

Confirmed 2026-04-11 from MITM capture of a live Blastware ACH session (bridges/captures/mitm/ach_mitm_20260411_001912/).

Blastware uses a 4-step sequence to erase all stored events from device memory. All frames use standard build_bw_frame format (NOT write-format).

7.11.1 Wire Sequence

BW → device:  SUB 0xA3  offset=0x0000  params=00 00 00 00 00 00 00 FE 00 00
device → BW:  SUB 0x5C  (begin-erase ack)

BW → device:  SUB 0x1C  offset=0x0000  params=00 00 00 00 00 00 00 FE 00 00  (probe)
device → BW:  SUB 0xE3  (probe ack)
BW → device:  SUB 0x1C  offset=0x002C  params=(same)                          (data)
device → BW:  SUB 0xE3  (44-byte monitor status response)

BW → device:  SUB 0x06  offset=0x0000  params=00 00 00 00 00 00 00 FE 00 00  (probe)
device → BW:  SUB 0xF9  (probe ack)
BW → device:  SUB 0x06  offset=0x0024  params=(same)                          (data)
device → BW:  SUB 0xF9  (36-byte storage range response)

BW → device:  SUB 0xA2  offset=0x0000  params=00 00 00 00 00 00 00 FE 00 00
device → BW:  SUB 0x5D  (confirm-erase ack — device memory is now cleared)

All response SUBs follow the standard formula 0xFF request_SUB. No exceptions. The token=0xFE at params[7] is required for 0xA3, 0x06, and 0xA2.

7.11.2 SUB 0x06 Storage Range Response (36 bytes)

The 36-byte response from the data step ends with two 4-byte event keys:

Offset (from response end) Field Notes
[-8:-4] First stored event key e.g. 0111ea60 before erase
[-4:] Last stored event key e.g. 0111eaa6 before erase

After a successful erase:

  • Both keys read 01110000 (device-empty sentinel)
  • The device's internal event counter has reset

Example pre-erase: ... 0111ea60 0111eaa6 Example post-erase: ... 01110000 01110000

7.11.3 Post-Erase Key Counter Reset

After a successful erase the device resets its event counter. New events start from key 0x01110000 — the same key as the very first event ever recorded on the device. This means:

  • Any system using event keys for deduplication must clear its "seen keys" state after an erase, or risk treating fresh events as already downloaded.
  • Detection heuristic: if max(device_keys) < historical_max_key, the counter was reset. All device keys should be treated as new regardless of prior state.

The ach_server.py implementation stores max_downloaded_key in ach_state.json and applies this heuristic on every call-home.

7.11.4 Implementation Notes

  • MiniMateClient.delete_all_events() in client.py orchestrates the full sequence.
  • MiniMateProtocol exposes begin_erase_all(), confirm_erase_all(), and read_event_storage_range() as separate methods.
  • The ACH server --clear-after-download flag calls delete_all_events() after a successful event download and resets ach_state.json state for the unit.

8. Timestamp Format

Two timestamp wire formats are used:

8.1 6-byte format (event index / 1E header)

🔶 Updated 2026-02-26 — Year field resolved. Confidence upgraded.

Appears in event index blocks. Time-of-day fields (hour/min/sec) are absent.

Observed example:

01 07 CB 00 06 1E
Byte(s) Value Meaning Certainty
01 1 Record validity / type flag 🔶 INFERRED
07 CB 1995 Year — 16-bit big-endian integer CONFIRMED — 2026-02-26
00 0 Unknown separator
06 6 Month (June) CONFIRMED
1E 30 Day (0x1E = 30 decimal) CONFIRMED

2026-02-26 — CONFIRMED: The year 1995 is the MiniMate Plus factory default RTC date, which the device reverts to whenever the internal battery is disconnected or the real-time clock loses power. Any event timestamped around 1995 means the clock was not set. This is known device behavior, not an encoding anomaly.

8.2 9-byte format (Full Waveform Record / SUB 0C, bytes 08)

CONFIRMED 2026-04-01 — Cross-referenced against Blastware event report for BE11529 thump event: "00:28:12 April 1, 2026".

Full date + time, including a sub_code byte that encodes the recording mode.

Observed example (thump event, 2026-04-01):

01  10  04  07 ea  00  00  1c  0c
Byte(s) Value Meaning Certainty
01 1 Day
10 0x10 Sub_code: 0x10 = Waveform (continuous mode) / histogram code
04 4 Month (April)
07 ea 2026 Year — 16-bit big-endian integer
00 0 Unknown separator
00 0 Hour
1c 28 Minute
0c 12 Second

The .MLG file for the same event stores the timestamp in a different binary representation (little-endian year, no sub_code byte), confirming the waveform record and the saved file use distinct serialisation formats.


9. Out-of-Band / Non-Frame Messages

Message Direction Trigger Certainty
"Operating System" S3 → BW Device boot / UART init / RTC reset CONFIRMED

The device prints this boot string directly to the UART before switching to DLE-framed binary protocol mode. Your implementation should discard any non-0x41/non-0x10 0x02 bytes during the connection phase. Wait for the first valid framed poll response before proceeding.


10. DLE Byte Stuffing

CONFIRMED — 2026-02-26 (previously SPECULATIVE)

This protocol uses standard DLE (Data Link Escape) byte stuffing, a classical technique used in protocols like IBM BISYNC dating to the 1970s.

Parser State Machine

IDLE:
  receive 0x41       → emit ACK event, stay IDLE
  receive 0x10       → goto WAIT_STX

WAIT_STX:
  receive 0x02       → frame started, goto IN_FRAME
  receive anything   → error, goto IDLE

IN_FRAME:
  receive 0x10       → goto ESCAPE
  receive any byte   → append to buffer, stay IN_FRAME

ESCAPE:
  receive 0x03       → frame complete — validate checksum, process buffer, goto IDLE
  receive 0x10       → append single 0x10 to buffer, goto IN_FRAME  (stuffed literal)
  receive 0x02       → error (nested STX), goto IDLE
  receive anything   → error, goto IDLE

11. Checksum Reference Implementation

⚠️ Updated 2026-03-12 — BW→S3 large-frame checksum algorithm confirmed. Two distinct formulas apply depending on frame direction and size.

Checksum Overview

Direction Frame type Formula Coverage
S3→BW All frames sum(payload) & 0xFF All de-stuffed payload bytes [0:-1]
BW→S3 Small frames (POLL, read cmds) sum(payload) & 0xFF All de-stuffed payload bytes [0:-1]
BW→S3 Large write frames (SUB 68,69,71,82,1A+data) See formula below De-stuffed payload bytes [2:-1], skipping 0x10 bytes, plus constant

BW→S3 Large-Frame Checksum Formula

def calc_checksum_bw_large(payload: bytes) -> int:
    """
    Checksum for large BW→S3 write frames (SUB 68, 69, 71, 82, 1A with data).
    
    Formula: sum all bytes in payload[2:-1], skipping 0x10 bytes, add 0x10, mod 256.
    Confirmed across 20 frames from two independent captures (2026-03-12).
    """
    return (sum(b for b in payload[2:-1] if b != 0x10) + 0x10) & 0xFF

Why this formula: The CMD byte at payload[0] is always 0x10 (DLE). The byte at payload[1] is always 0x00. Starting from payload[2] skips both. All 0x10 bytes in the data section are excluded from the sum, then 0x10 is added back as a constant — effectively treating DLE as a transparent/invisible byte in the checksum. This is consistent with 0x10 being a framing/control character in the protocol.

Consistency check: For small frames, payload[0] = 0x10 and there are no other 0x10 bytes in the payload. The large-frame formula applied to a small frame would give (sum(payload[2:-1]) + 0x10) = sum(payload[0:-1]) — identical to the plain SUM8. The two formulas converge for frames without embedded 0x10 data bytes.

DLE = 0x10
STX = 0x02
ETX = 0x03
ACK = 0x41


def stuff(data: bytes) -> bytes:
    """Escape all 0x10 bytes in payload as 0x10 0x10 for transmission."""
    out = []
    for b in data:
        out.append(b)
        if b == DLE:
            out.append(DLE)  # double it
    return bytes(out)


def destuff(data: bytes) -> bytes:
    """Remove DLE stuffing from received payload bytes."""
    out = []
    i = 0
    while i < len(data):
        if data[i] == DLE and i + 1 < len(data) and data[i + 1] == DLE:
            out.append(DLE)
            i += 2
        else:
            out.append(data[i])
            i += 1
    return bytes(out)


def calc_checksum_s3(payload: bytes) -> int:
    """
    Standard SUM8: used for all S3→BW frames and small BW→S3 frames.
    Sum of all payload bytes (excluding the checksum byte itself), mod 256.
    """
    return sum(payload) & 0xFF


def calc_checksum_bw_large(payload: bytes) -> int:
    """
    Large BW→S3 write frame checksum (SUB 68, 69, 71, 82, 1A with data).
    Sum payload[2:-1] skipping 0x10 bytes, add 0x10, mod 256.
    """
    return (sum(b for b in payload[2:-1] if b != 0x10) + 0x10) & 0xFF


# Backwards-compatible alias
def calc_checksum(payload: bytes) -> int:
    return calc_checksum_s3(payload)


def build_frame(payload: bytes) -> bytes:
    """
    Build a complete on-wire frame from a raw payload.
    Output: ACK + DLE+STX + stuffed_payload + checksum + DLE+ETX
    """
    chk = calc_checksum(payload)
    stuffed = stuff(payload)
    return bytes([ACK, DLE, STX]) + stuffed + bytes([chk, DLE, ETX])


def parse_frame(raw: bytes) -> bytes | None:
    """
    Parse and validate a raw on-wire frame.
    Accepts input starting with ACK (0x41) or DLE+STX (0x10 0x02).
    Returns de-stuffed payload bytes on success, None on any error.
    """
    # Strip optional leading ACK
    if raw and raw[0] == ACK:
        raw = raw[1:]

    # Validate frame delimiters
    if len(raw) < 5:
        return None
    if raw[0] != DLE or raw[1] != STX:
        return None
    if raw[-2] != DLE or raw[-1] != ETX:
        return None

    # Extract: everything between DLE+STX and DLE+ETX
    inner = raw[2:-2]
    chk_received = inner[-1]
    stuffed_payload = inner[:-1]

    # De-stuff and validate checksum
    payload = destuff(stuffed_payload)
    if calc_checksum(payload) != chk_received:
        return None

    return payload


# ── Example: build a POLL request (SUB 5B) ────────────────────────────────────
poll_payload = bytes([
    0x02,                    # CMD
    0x10, 0x10,              # DLE, ADDR (each stuffed to 0x10 0x10 on wire)
    0x00,                    # FLAGS
    0x5B,                    # SUB: POLL
    0x00, 0x00,              # OFFSET_HI, OFFSET_LO
    0x00, 0x00, 0x00, 0x00,  # padding
    0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
])
frame = build_frame(poll_payload)
# Wire output: 41 10 02 02 10 10 10 10 00 5B 00 00 00 00 00 00 00 00 00 00 6B 10 03

Build in this order — each step is independently testable:

  1. DLE frame parser — stateful byte-by-byte reader implementing the §10 state machine. Handles ACK, stuffing, de-stuffing, checksum validation.
  2. connect(port, baud=38400) — open port, flush buffer, discard ASCII boot strings, send first POLL frame.
  3. identify() — SUB 5B two-step read → returns {"manufacturer": "Instantel", "model": "MiniMate Plus"}.
  4. get_serial() — SUB 15 two-step read → returns serial number string.
  5. get_config() — SUB 01 two-step read → returns full config dict (firmware, channel scales, etc.).
  6. get_event_count() — SUB 08 two-step read → returns number of stored events.
  7. get_event_header(index) — SUB 1E read → returns timestamp dict.
  8. get_event_record(timestamp) — SUB 0C paginated read → returns PPV dict per channel.
  9. download_waveform(timestamp) — SUB 5A bulk stream → returns raw ADC arrays per channel.
  10. set_*() write commands — not yet captured, requires additional sniffing sessions.

13. Device Under Test

Field Value
Manufacturer Instantel
Model MiniMate Plus
Serial Number BE18189
Firmware S337.17
DSP / Secondary FW 10.72
Channels Tran, Vert, Long, MicL (4 channels)
Sample Rate ~1024 sps (🔶 INFERRED)
Bridge Config COM5 (Blastware) ↔ COM4 (Device), 38400 baud
Capture Tool s3_bridge v0.4.0

14. TCP / Modem Transport

CONFIRMED — 2026-03-31 from Blastware Operator Manual 714U0301 Rev 22 §4.4 and ACEmanager Raven modem configuration screenshots.

The MiniMate Plus protocol is fully transport-agnostic at the byte level. The same DLE-framed S3/BW frame stream that flows over RS-232 is transmitted unmodified over a TCP socket. No additional framing, handshake bytes, or session tokens are added at the application layer.


14.1 Two Usage Modes

"Call Up" (Outbound TCP — SFM connects to modem)

Blastware or SFM opens a TCP connection to the modem's static IP address on its device port. The modem bridges the TCP socket to its RS-232 serial port, which is wired directly to the MiniMate Plus. From the protocol perspective this is identical to a direct serial connection.

SFM ──TCP──► Raven modem ──RS-232──► MiniMate Plus
             (static IP, port N)      (38400,8N1)

This is the mode implemented by TcpTransport(host, port). Typical call:

GET /device/info?host=203.0.113.5&tcp_port=12345

"Call Home" / ACH (Inbound TCP — unit calls the server)

The MiniMate Plus is configured with an IP address and port. On an event trigger or scheduled time it powers up its modem, which establishes a TCP connection outbound to the server. Blastware (or a future SFM ACH listener) accepts the incoming connection. After the unit connects, the PC has a configurable "Wait for Connection" window to send the first command before the unit times out and hangs up.

MiniMate Plus ──RS-232──► Raven modem ──TCP──► ACH server (listening)
                                                (static office IP, port N)

TcpTransport is a client (outbound connect only). A separate AchServer listener component is needed for this mode — not yet implemented.


14.2 No Application-Layer Handshake on TCP Connect

Confirmed from ACEmanager configuration screenshot:

Enable ENQ on TCP Connect: 0-Disable

When a TCP connection is established (in either direction), no ENQ byte or other handshake marker is sent by the modem before the protocol stream starts. The first byte from either side is a raw protocol byte — for SFM-initiated call-up, SFM sends POLL_PROBE immediately after connect().

Sierra Wireless RV50/RV55 note: Even with Quiet Mode enabled, these modems send \r\nRING\r\n\r\nCONNECT\r\n over the TCP connection to the calling client at connect time. Quiet Mode only suppresses these strings on the serial port (protecting the MiniMate Plus). The TCP client must tolerate these prefix bytes — scan for DLE+STX (0x10 0x02) and discard everything before it. This is the same approach used for the "Operating System" boot string (§9).

The Raven X (deprecated) did not exhibit this behaviour. The note below about "no CONNECT string" describes Raven X with ENQ-disable; it does not apply to RV50/RV55.

No ENQ byte or other application-layer handshake is added. The Raven modem's TCP dialog is configured with:

ACEmanager Setting Value Meaning
TCP Auto Answer 2 — Telnet Server TCP mode (transparent pass-through, not actually Telnet)
Telnet Echo Mode 0 — No Echo No echo of received bytes
Enable ENQ on TCP Connect 0 — Disable No ENQ byte on connect
TCP Connect Response Delay 0 No delay before first byte
TCP Idle Timeout 0 No modem-level idle disconnect

14.3 Modem Serial Port Configuration

Hardware note: The Raven X modem shown in the Blastware manual is 3G-only and no longer operational (3G network shutdown). The current field hardware is the Sierra Wireless RV55 (and newer RX55). Both run ALEOS firmware and have an identical ACEmanager web UI — the settings below apply to all three generations.

The modem's RS-232 port (wired to the MiniMate Plus) must be configured as:

ACEmanager Setting Value
Configure Serial Port 38400,8N1
Flow Control None
DB9 Serial Echo OFF
Data Forwarding Timeout 1 second (S50=1)
Data Forwarding Character 0 (disabled)

The Data Forwarding Timeout is the most protocol-critical setting. The modem accumulates bytes from the RS-232 port for up to 1 second before forwarding them as a TCP segment. This means:

  • A large S3 response frame may arrive as multiple TCP segments with up to 1-second gaps between them.
  • A read_until_idle implementation with idle_gap < 1.0 s will incorrectly declare the frame complete mid-stream.
  • TcpTransport.read_until_idle overrides the default idle_gap=0.05 s to idle_gap=1.5 s to compensate.

If connecting to a unit via a direct Ethernet connection (no serial modem in the path), the 1.5 s idle gap will still work but will feel slower. In that case you can pass idle_gap=0.1 explicitly.

Sierra Wireless RV50 / RV55 Required Settings

CONFIRMED — 2026-03-31 from working RV50 field config vs misconfigured RV55.

The following ACEmanager Serial settings are required for correct transparent operation. A single wrong setting is enough to break the protocol exchange (unit beeps on connect but never returns an S3 frame):

ACEmanager Setting Required Value Why
Quiet Mode Enable Disabling it causes the modem to inject RING\r\nCONNECT\r\n onto the RS-232 serial line at connection time, corrupting the S3 handshake.
Configure Serial Port 38400,8N1 Must match MiniMate baud rate.
Flow Control None Hardware flow control (CTS/RTS) will block unit's serial TX if pins are not wired.
Data Forwarding Timeout 1 (= 0.1 s) Controls RS-232→TCP forwarding latency. 5 (0.5 s) works but is sluggish; 1 matches working field units.
TCP Connect Response Delay 0 Any non-zero value causes the modem to silently discard our POLL frame during the delay window.
TCP Idle Timeout 2 (minutes) Prevents premature disconnect. Too low and the modem drops the session before the unit responds.
DB9 Serial Echo Disable Echo would corrupt the S3 stream.

14.4 Connection Timeouts on the Unit Side

The MiniMate Plus firmware has two relevant timeouts configurable via Blastware's Call Home Setup dialog:

Timeout Description Impact
Wait for Connection Seconds after TCP connect during which the unit waits for the first BW frame. If nothing arrives, unit terminates the session. SFM must send POLL_PROBE within this window after connect(). Default appears short (≈1530 s).
Serial Idle Time Seconds of inactivity after which the unit terminates the connection. SFM must complete its work and disconnect cleanly — or send periodic keep-alive frames — within this window.

For our TcpTransport + MiniMateProtocol stack, both timeouts are satisfied automatically because connect() is immediately followed by protocol.poll() which sends POLL_PROBE, and the full session (POLL + read + disconnect) typically completes in < 30 seconds.


14.5 Port Numbers

The TCP port is user-configurable in both Blastware and the modem. There is no universally fixed port.

Setting location Value in manual example Value in user's install
Blastware TCP Communication dialog 12335 12345
Raven ACEmanager Destination Port 12349 (UDP example) varies

TcpTransport defaults to DEFAULT_TCP_PORT = 12345 which matches the user's install. This can be overridden by the port argument or the tcp_port query parameter in the SFM server.


14.6 ACH Session Lifecycle (Call Home Mode) IMPLEMENTED 2026-04-11

When the unit calls home under ACH, the session lifecycle from the unit's perspective is:

  1. Unit triggers (event or scheduled time)
  2. Unit powers up modem, dials / connects TCP to server IP:port
  3. Unit waits for "Wait for Connection" window for first BW frame from server
  4. Server sends POLL_PROBE → unit responds with POLL_RESPONSE (same as serial)
  5. Server reads serial number, full config, events as needed
  6. (Optional) Server erases device memory: SUB 0xA3 → 0x1C → 0x06 → 0xA2
  7. Server disconnects (or unit disconnects on Serial Idle Time expiry)
  8. Unit detects DCD/DTR going low (modem signals line drop), returns to monitor mode automatically

Step 4 onward is identical to the serial/call-up protocol. The only difference from our perspective is that we are the listener rather than the connector.

Implementation: bridges/ach_server.py — run with python bridges/ach_server.py. Key flags:

  • --clear-after-download — erase device memory after a successful event download
  • --allow-ip IP — restrict to specific unit IPs
  • --max-events N — cap events per session for safety

State persistence: ach_state.json — tracks downloaded_keys (set of event key hex strings) and max_downloaded_key (high-water mark) per unit serial number. Post-erase key reuse (0x01110000 recycled) is detected via the high-water mark.

Note on DCD/DTR: The MiniMate Plus monitors the RS-232 DCD line. When the TCP connection closes, the Sierra Wireless modem drops DCD, which the unit interprets as "serial connection ended" and automatically resumes monitoring. No start_monitoring() (SUB 0x96) command is needed from the server. ⚠️ Newer RV55 firmware may not assert DCD by default — known issue, not yet resolved.


Appendix A — s3_bridge Capture Format

CONFIRMED — 2026-02-26

⚠️ This behavior is not part of the Instantel protocol. It is an artifact of the bridge logger implementation.

The .bin files produced by s3_bridge are not raw wire bytes. The logger makes one modification:

Wire sequence In .bin file Notes
0x10 0x03 (DLE+ETX) 0x03 DLE stripped from end-of-frame marker
All other bytes Unchanged ACK, DLE+STX, stuffed payload, checksum all preserved verbatim

Practical impact for parsing .bin files:

  • Frame end: scan for bare 0x03 (not 0x10 0x03)
  • Checksum: the byte immediately before the bare 0x03 is the checksum
  • Everything else (ACK detection, DLE+STX, payload de-stuffing) works as documented in §10

⚠️ This means checksums cannot be verified on frames where the stuffed payload ends in 0x10 — that trailing 0x10 would normally be the DLE prefix of ETX, but the logger strips it, making the frame boundary ambiguous in that edge case. In practice this has not been observed in captured data.


14. Open Questions / Still Needs Cracking

Question Priority Added Notes
Timestamp 6-byte format byte[3] — purpose of the separator 0x00 byte LOW 2026-02-26 Not blocking; 9-byte waveform record format (§8.2) fully confirmed without this byte.
trail[0] in serial number response — unit-specific byte, derivation unknown. trail[1] resolved as firmware minor version. MEDIUM 2026-02-26
Full channel ID mapping in SUB 5A stream (01/02/03/04 → which sensor?) MEDIUM 2026-02-26
Exact byte boundaries of project string fields in SUB 71 write frame — padding rules unconfirmed MEDIUM 2026-02-26
Purpose of SUB 09 / response F6 — 202-byte read block MEDIUM 2026-02-26
Purpose of SUB 2E / response D1 — 26-byte read block MEDIUM 2026-02-26
Full field mapping of SUB 1A / response E5 — channel scaling / compliance config block. SUBSTANTIALLY RESOLVED 2026-04-01: Multi-frame sequence (§7.6.2), page_key chunking, duplicate-page detection, record_time (§7.6.1), sample_rate (§7.6.3), setup_name, project strings all confirmed. Trigger/alarm level floats in the channel block (§7.6) also confirmed. Remaining unknowns: trigger level (geo), alarm level (geo), max range (in CFG header not yet decoded). MEDIUM 2026-02-26 Substantially resolved 2026-04-01
0x082A in channel config block — not trigger, alarm, or record time directly. RESOLVED: fixed E5 payload length (2090 bytes). Constant regardless of all settings. RESOLVED 2026-03-01 Resolved 2026-03-09
Record time in wire protocol — float32 BE, anchor-relative in cfg (see §7.6.1/§7.6.3). RESOLVED. Previous +0x28 offset was unreliable due to DLE jitter — superseded by anchor search. Confirmed at 3, 5, 7, 8, 10, 13 seconds. RESOLVED 2026-03-09 Confirmed 2026-03-09; anchor method confirmed 2026-04-01
Sample rate — uint16 BE at anchor2 in cfg. RESOLVED. Normal=1024, Fast=2048, Faster=4096. Anchor required to handle DLE jitter. See §7.6.3. RESOLVED 2026-04-01 Confirmed 2026-04-01
Unknown uint16 fields at channel block +0A (=80), +0C (=15), +0E (=40), +10 (=21) — manual describes "Sensitive (Gain=8) / Normal (Gain=1)" per-channel range; 80/15/40/21 might encode gain, sensitivity, or ADC config. LOW 2026-03-01
Full trigger configuration field mapping (SUB 1C / write 82) LOW 2026-02-26
Whether SUB 24/25 are distinct from SUB 5A or redundant LOW 2026-02-26
Meaning of 0x07 E7 field in config block — RESOLVED: Calibration year. uint16 BE at destuffed payload offset 0x560x57. Confirmed via two-unit comparison: BE18189 (calibrated 2023) = 07 E7; BE11529 (calibrated 2025) = 07 E9. Adjacent bytes at 0x530x55 encode remaining calibration date (month confirmed as BCD October for both units; full layout 🔶 INFERRED). RESOLVED 2026-02-26 Resolved 2026-03-31
Trigger Sample WidthRESOLVED: BW→S3 write frame SUB 0x82, destuffed payload offset [22], uint8. Width=4 → 0x04, Width=3 → 0x03. Confirmed via BW-side capture diff. Only visible in raw_bw.bin write traffic, not in S3-side compliance reads. RESOLVED 2026-03-02 Confirmed 2026-03-09
Auto Window — "1 to 9 seconds" per manual (§3.13.1b). Mode-gated: only transmitted/active when Record Stop Mode = Auto. Capture attempted in Fixed mode (3→9 change) — no wire change observed in any frame. Deferred pending mode switch. LOW 2026-03-02 Updated 2026-03-09
Auxiliary Trigger read locationRESOLVED: SUB FE offset 0x0109, uint8, 0x00=disabled, 0x01=enabled. Confirmed 2026-03-11 via controlled toggle capture. RESOLVED 2026-03-02 Resolved 2026-03-11
Auxiliary Trigger write path — Write command not yet captured in a clean session. Inner frame handshake visible in A4 (multiple WRITE_CONFIRM_RESPONSE SUBs appear, TRIGGER_CONFIG_RESPONSE removed), but the BW→S3 write command itself was in a partial session. Likely SUB 15 or similar. Deferred for clean capture. LOW 2026-03-11 NEW
SUB 6E response to SUB 1CRESOLVED 2026-04-08: This was a misidentification. The 1C → 6E "exception" was misread — likely an inner A4 sub-frame. Confirmed from 4-8-26 capture (338 frames): SUB 0x1C always → 0xE3. No exceptions to the 0xFF SUB rule are known. RESOLVED 2026-04-08 CLOSED
Max Geo Range float 6.2061 in/s — NOT a user-selectable range (manual only shows 1.25 and 10.0 in/s). Likely internal ADC full-scale constant or hardware range ceiling. Not worth capturing. LOW 2026-02-26 Downgraded 2026-03-02
MicL channel units — RESOLVED: psi, confirmed from .set file unit string "psi\0" RESOLVED 2026-03-01
Backlight offset — RESOLVED: +4B in event index data, uint8, seconds RESOLVED 2026-03-02
Power save offset — RESOLVED: +53 in event index data, uint8, minutes RESOLVED 2026-03-02
Monitoring LCD Cycle — RESOLVED: +54/+55 in event index data, uint16 BE, seconds (65500 = disabled) RESOLVED 2026-03-02
SUB 0x06 purpose — RESOLVED: event storage range. Previously labeled "CHANNEL CONFIG READ". 4-11-26 MITM capture confirms it returns first/last stored event keys in the final 8 bytes of the 36-byte response. Used by Blastware as part of the erase-all verification step. RESOLVED 2026-04-11
Erase-all command sequence — RESOLVED. SUB 0xA3 (begin) + 0x1C (monitor status) + 0x06 (storage range) + 0xA2 (confirm). Confirmed from 4-11-26 MITM capture. All frames standard build_bw_frame, token=0xFE. RESOLVED 2026-04-11
ACH inbound server — RESOLVED. bridges/ach_server.py implements full inbound ACH pipeline. --clear-after-download flag for delete-after-upload workflow. Post-erase key-reuse detection via max_downloaded_key high-water mark. RESOLVED 2026-04-11
Sensor Check dropdown byte location — byte offset in 1A compliance config payload for the "Sensor Check: Before monitoring / After each event / Disabled" setting is NOT YET LOCATED. Confirmed: unit always runs with "Before monitoring" set. Need a capture with "Disabled" to diff. MEDIUM 2026-04-08 Still open
RV55 DCD/DTR default — newer Sierra Wireless RV55 firmware does not assert DCD/DTR by default, so the MiniMate Plus never detects TCP disconnect and stays idle instead of resuming monitoring. Root cause: RV55 ACEmanager DCD Control setting. Workaround not yet found. MEDIUM 2026-04-11 Still open


Appendix B — Operator Manual Cross-Reference (716U0101 Rev 15)

Added 2026-03-02. Cross-referencing confirms setting names, ranges, units, and behavior for fields found in protocol captures. The manual does NOT describe the wire protocol — it describes the user-facing device interface. Use to infer data types, ranges, and semantics of protocol fields.

Setting Name (Manual) Manual Location Protocol Location Type Range / Notes
Backlight On Time §3.13.1e Event Index +4B uint8 0255 seconds
Power Saving Timeout §3.13.1f Event Index +53 uint8 minutes (user sets 160+)
Monitoring LCD Cycle §3.13.1g Event Index +54/55 uint16 BE seconds; 0=off; 65500≈disabled
Trigger Level (Geo) §3.8.6 Channel block, float float32 BE 0.00510.000 in/s
Alarm Level (Geo) §3.9.9 Channel block, float float32 BE higher than trigger level
Trigger Level (Mic) §3.8.6 Channel block, float float32 BE 100148 dB in 1 dB steps
Alarm Level (Mic) §3.9.10 Channel block, float float32 BE higher than mic trigger
Record Time §3.8.9 cfg anchor+10, float32 BE (wire); .set +16, uint32 LE (file) float32 BE (wire) 1105 s; confirmed 3→40400000, 5→40A00000, 8→41000000, 13→41500000. Use anchor §7.6.1/§7.6.3 — NOT fixed offset.
Max Geo Range §3.8.4 Channel block, float float32 BE 1.25 or 10.0 in/s (user); 6.2061 in protocol = internal constant
Microphone Units §3.9.7 Inline unit string char[4] "psi\0", "pa.\0", "dB\0\0"
Sample Rate §3.8.2 cfg anchor2, uint16 BE — anchor=\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00 in cfg[40:100] uint16 BE Normal=1024, Fast=2048, Faster=4096 CONFIRMED 2026-04-01 (BE11529 S338.17). Anchor required — see §7.6.3 DLE jitter.
Record Mode §3.8.1 Unknown Single Shot, Continuous, Manual, Histogram, Histogram Combo
Trigger Sample Width §3.13.1h BW→S3 SUB 0x82 write frame, destuffed [22], uint8 uint8 Default=2; confirmed 4=0x04, 3=0x03. BW-side write only — not visible in S3 compliance reads. Mode-gated: only sent in Compliance/Single-Shot/Fixed mode.
Auto Window §3.13.1b Mode-gated — NOT YET MAPPED uint8? 19 seconds; only active when Record Stop Mode = Auto. Capture in Fixed mode produced no wire change.
Auxiliary Trigger §3.13.1d SUB FE (FULL_CONFIG_RESPONSE) offset 0x0109 (read); write path not yet isolated uint8 (bool) 0x00=disabled, 0x01=enabled; confirmed 2026-03-11
Password §3.13.1c Unknown 4-key sequence
Serial Connection §3.9.11 Unknown Direct / Via Modem
Baud Rate §3.9.12 Unknown 38400 for direct

Appendix C — Logger & Parser Validation (2026-03-02)

Documents the logger integrity verification and parser refactor completed 2026-03-02. Tooling behavior only — not protocol semantics.

C.1 Logger Validation

Concern: Earlier sessions noted that the s3_bridge logger may have been stripping 0x10 from DLE ETX sequences, producing bare 0x03 terminators in the capture file.

Resolution: HxD inspection of a new capture produced by s3_bridge v0.5.0 confirmed that 10 03 sequences are present intact inside S3→BW record payloads. The forward_loop function writes raw bytes to the .bin before any sniffer or framing logic runs — there is no ETX stripping in v0.5.0.

The earlier stripping behavior applied to a previous logger version. v0.5.0 is confirmed lossless with respect to wire bytes.

Confirmed wire framing:

  • Frame start: 0x10 0x02 (DLE STX)
  • Frame end: 0x10 0x03 (DLE ETX)
  • DLE stuffing: 0x10 0x10 in payload = literal 0x10

C.2 Capture Architecture (Current)

As of 2026-03-02 the capture pipeline produces two flat raw wire dump files per session:

File Contents
raw_s3.bin All bytes transmitted by S3 (device → Blastware), in order
raw_bw.bin All bytes transmitted by BW (Blastware → device), in order

No record headers, no timestamps, no framing logic applied by the dumper. Files are flat concatenations of serial.read() chunks. Frame boundaries must be recovered by the parser.

C.3 Parser Design — DLE State Machine

A deterministic state machine replaces all prior heuristic scanning.

States:

STATE_IDLE       — scanning for frame start
STATE_IN_FRAME   — consuming payload bytes
STATE_AFTER_DLE  — last byte was 0x10, awaiting qualifier

Transitions:

Current State Byte Action Next State
IDLE 10 02 Begin new frame IN_FRAME
IDLE any Discard IDLE
IN_FRAME != 10 Append to payload IN_FRAME
IN_FRAME 10 AFTER_DLE
AFTER_DLE 10 Append literal 0x10 IN_FRAME
AFTER_DLE 03 Frame complete, emit IDLE
AFTER_DLE other Treat as payload (recovery) IN_FRAME

Properties:

  • Does not scan globally for 10 02
  • Only complete STX→ETX pairs are emitted as frames
  • Incomplete trailing frames at EOF are discarded (expected at capture boundaries)
  • DLE stuffing handled correctly

C.4 Observed Traffic (Validation Captures)

raw_bw.bin (Blastware → S3):

  • 7 complete frames via state machine
  • Mostly small command/control frames, several zero-length payloads
  • Bare 0x02 used as STX (asymmetric — BW does not use DLE STX)
  • Contains project metadata strings: "Standard Recording Setup.set", "Claude test2", "Location #1 - Brians House"

raw_s3.bin (S3 → Blastware):

  • First frame payload ~3922 bytes (large structured response)
  • Repeated "Instantel" / "MiniMate Plus" / "BE18189" strings throughout
  • Multiple medium-length structured frames
  • DLE+ETX confirmed intact

C.5 Key Lessons

  1. Global byte counting ≠ frame counting. 0x10 0x02 appears inside payloads. Only state machine transitions produce valid frame boundaries.
  2. STX count ≠ frame count. Only STX→ETX pairs within proper state transitions count.
  3. EOF mid-frame is normal. Capture termination during active traffic produces an incomplete trailing frame. Not an error.
  4. Layer separation. The parser extracts frames only. Decoding block IDs, validating checksums, and interpreting semantics are responsibilities of a separate protocol decoder layer above it.

C.6 Parser Layer Architecture

raw_s3.bin / raw_bw.bin
        ↓
DLE Frame Parser (s3_parser.py)   <- framing only
        ↓
Protocol Decoder (future)         <- SUB IDs, block layout, checksums
        ↓
Semantic Interpretation           <- settings, events, responses

All findings reverse-engineered from live RS-232 bridge captures.
Cross-referenced from 2026-03-02 with Instantel MiniMate Plus Operator Manual (716U0101 Rev 15).
This is a living document — append changelog entries and timestamps as new findings are confirmed or corrected.