Files
seismo-relay/docs/instantel_protocol_reference.md
T

192 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-05-08 §7.6.1 (RETRACTION) RETRACTED — "raw int16 LE 8 bytes/sample-set" body codec was never validated. The original 4-2-26 confirmation was based on misreading broken-decoder output (full-scale ±32K noise) as evidence the signal had saturated. BW's own 0C peaks for that capture (Tran=0.420 / Vert=3.870 / Long=0.495 in/s) prove the signal was NOT saturated — none of those exceed 13K ADC counts. No event in the project's archive has ever come close to saturation, yet the decoder consistently produces ±32K noise on every event. Conclusion: the body codec is not raw int16 LE; the actual encoding is open. Body byte distribution is heavily skewed (24% 0x00, 10.5% 0x10, lots of 10 XX pairs) — likely a delta encoding with 0x10 as escape, but unverified. Retraction box added at top of §7.6.1; "fully-saturating event" claim removed from channel-identification note. The histogram codec in §7.6.2 IS verified and decoded correctly (different recording mode, 32-byte blocks); use it as a structural hint when reverse-engineering the waveform codec.
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 → originally speculated as internal ADC full-scale constant, but was NOT confirmed at this time. Using it directly as the range produces ~9× PPV overread. Meaning unknown. Downgraded to LOW 2026-03-02, re-escalated to HIGH 2026-04-16. RESOLVED 2026-04-17 — see §7.6.2 and changelog entry.
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 ⚠ PARTIALLY INVALIDATED — ADC count-to-physical-unit conversion. Raw waveform samples are signed 16-bit integers (counts). Conversion formula value = counts × (range / 32767) is believed correct, but the range value was UNKNOWN at time of writing. UPDATED 2026-04-17: max_range_geo = 6.206053 is confirmed as the ADC-to-velocity scale factor (inverse sensitivity, (in/s)/V). The correct conversion is therefore: PPV (in/s) = counts × (1.61133 / 32767) × 6.206053 = counts × 4.982e-5 in/s per count. The earlier ~9× overread from using 6.206053 directly as the range was because the range IS 1.61133 × 6.206053 = 10.000 in/s, not 6.206053. See §7.6.2 for the confirmed field layout.
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.
2026-04-17 §7.6.2, §14 RESOLVED — Float 6.206053 at channel_label+28 is the ADC-to-velocity scale factor. Confirmed from Series III Interface Handbook §4.5 formula: Range (×1) = 1.61133 V / Sensitivity (V/unit). For the standard Instantel geophone at Normal range (10.000 in/s): Sensitivity = 1.61133 / 10 = 0.161133 V/(in/s). The stored value is the inverse sensitivity = 1/0.161133 = 6.206053 (in/s)/V. Cross-check: 1.61133 V × 6.206053 = 10.000 in/s . The firmware uses it as: PPV (in/s) = ADC_voltage (V) × 6.206053. Value is identical on all Instantel standard geophones — it is a hardware/firmware constant, NOT a user-configurable setting. Do NOT write this field. Open question §14 item "Max Geo Range float 6.2061" is now RESOLVED.
2026-04-20 §7.6.4 (NEW), §7.9, Appendix B CONFIRMED — Recording Mode byte location. Three targeted captures (4-20-26) confirmed recording_mode at anchor8 in both the E5 read payload and the BW write payload (6-byte anchor \xbe\x80\x00\x00\x00\x00). BW write payload and E5 read payload are byte-identical around the anchor region — Blastware round-trips the wire-encoded E5 bytes verbatim with only the target field modified. Anchor position varies by ±1 depending on whether recording_mode = 0x03 (Histogram), because E5 wire-encodes 0x03 as the inner DLE+ETX pair \x10\x03 (2 bytes), which S3FrameParser preserves as two literal bytes in compliance_raw. Enum: 0x00=Single Shot, 0x01=Continuous, 0x03=Histogram, 0x04=Histogram+Continuous. 0x02 value not yet observed. The byte at anchor9 is 0x00 for Single Shot / Continuous, and 0x10 for Histogram (DLE prefix from E5 encoding) and Histogram+Continuous (actual config byte). See §7.6.4 for full details.
2026-04-21 Appendix D (NEW) NEW — Blastware .N00 and .MLG file formats fully decoded. minimateplus/blastware_file.py implements write_n00() and write_mlg(). N00 file format confirmed: 22B header + 21B STRT record + variable body + 26B footer. Body reconstructed from A5 bulk waveform stream frames with per-frame skip amounts (probe=7+strt_pos+21, A5[1]=13, A5[2+]=12, terminator=11) and DLE strip rule (strip 0x10 before {0x02,0x03,0x04}, keep following byte). Footer extracted verbatim from terminator frame's last 26 bytes. Split-pair edge case: when frame.data[-1]==0x10 and chk_byte∈{0x02,0x03,0x04}, reunite both bytes before stripping and always remove trailing chk_byte (stripped[:-1]) — chk_byte is checksum, not payload. STRT record must be copied verbatim from A5[0]; bytes [10:20] are device-specific and cannot be reconstructed from Event fields. write_n00 verified byte-perfect against M529LIY6.N00 from 4-3-26-multi_event capture. MLG format: 308B header + N×292B records; CRC algorithm unknown (write as 0x0000).
2026-04-21 Appendix D §D.5 (NEW) NEW — Blastware filename encoding fully decoded. Serial prefix: chr(ord('B') + floor(serial/1000)) + last 3 digits zero-padded. Stem: 4-char base-36 of floor(total_seconds/1296). Extension: AB0 for manual/direct downloads (3 chars), AB0W or AB0H for ACH/call-home downloads (4 chars), where AB = 2-char base-36 of total_seconds % 1296 and W/H = waveform/histogram. Epoch = 1985-01-01 00:00:00 device local time. Confirmed against 3,248 files from 10-year production archive with zero errors. 3-day cycle property: same daily recording time cycles through 3 extensions (864s/day shift, period=3 days). blastware_filename(event, serial, ach=False) implements full formula.
2026-04-21 §7.6.2, §5.3 CORRECTED — compliance_raw contains wire-encoded bytes, NOT logical bytes. S3FrameParser appends DLE+ETX inner-frame pairs as two literal bytes to the frame body. Any 0x03 values in the compliance config appear in compliance_raw as \x10\x03 (two bytes), not as a single 0x03. The previous claim "S3FrameParser handles this transparently so compliance_raw contains logical (destuffed) bytes" was wrong. Consequence: compliance_raw is the wire-encoded E5 payload; anchor-relative reads work correctly because the anchor position automatically accounts for any DLE-encoded bytes before it. For write-back, round-tripping compliance_raw verbatim sends the correct wire bytes to the device. DLE ETX escaping in write frames: Blastware escapes 0x03 bytes in write frame data as \x10\x03 on wire; our build_bw_write_frame does not (writes data raw). Device is confirmed to accept raw writes for all tested modes — likely uses the offset/length field for write frame framing, not ETX scanning.
2026-04-20 §7.6.2, §7.9, Appendix B CONFIRMED — Geophone maximum range / sensitivity selector byte location. Two targeted captures (4-20-26, geo sensitivity folder): one at Normal 10.000 in/s, one at Sensitive 1.250 in/s. E5 read payload diff: exactly 3 bytes differ at channel_label+33 for Tran/Vert/Long. Values: 0x00=Normal 10.000 in/s, 0x01=Sensitive 1.250 in/s. Same offset applies to the SUB 71 write payload (which is the same 2126-byte E5-format buffer round-tripped verbatim). channel_label+20 reads 0x01 in ALL captures regardless of range setting — it is NOT this field. Previous hypothesis (uint8 at Tran+20, 0x01=Normal) was WRONG. Stored as geo_range in ComplianceConfig. Encoded to all three geo channel blocks (Tran/Vert/Long) at label+33.
2026-04-20 §5.1, §5.3, §7.12 (NEW) NEW — Auto Call Home config protocol confirmed from 4-20-26 call home settings captures. SUB 0x2C (Call Home Config READ, response 0xD3, data offset 0x7C=124) and SUB 0x7E/0x7F (WRITE + CONFIRM, response 0x81/0x80) confirmed. Write payload = read payload (125 bytes) + \x00\x00 (127 bytes total). DLE-escaped ETX at raw[117:119]: the device returns logical value 0x03 (num_retries=3) as \x10\x03 on the wire — S3FrameParser preserves both bytes as two literals, causing a +1 byte shift for all subsequent fields. Write frame sends these bytes verbatim (device interprets \x10\x03 as literal value 3). Field map confirmed from 10-frame BW TX diff. See §7.12 for full layout.
2026-05-01 §7.8.2, §7.8.5 (NEW), §7.8.6 (NEW), §7.8.7 (NEW) REWRITTEN — SUB 5A bulk waveform stream protocol. Five BW MITM captures (4-27-26 "open 2sec waveform" + "copy event to disk", 5-1-26 BW 3-sec + 2nd-event + Download All) prove that the previous chunk-counter formula max(key4[2:4], 0x0400) + (chunk_num-1) * 0x0400 over-reads 5× past the actual event end. BW reads ~12-16 chunks per event at 0x0200 increments (NOT 0x0400), bounded by end_offset extracted from the STRT record at data[23:27] of the first A5 response. TERM frame formula corrected: offset_word = end_offset - next_boundary, params[2:4] = next_boundary BE where next_boundary = last_chunk_counter + 0x0200. Verified across 3 events (offsets 0x1ABE, 0x21F2, 0x417E). Metadata pages 0x1002 / 0x1004 are global, fixed-address device pages containing Project/Client/User Name/Seis Loc/Extended Notes — read ONCE per Blastware session (not per event). Event-1 vs event-N split: events at start_key[2:4]=0 use probe@0x0000 + metadata pages + sample chunks at 0x0600 onward; continuation events skip metadata and start at start_key+0x0046. WAVEHDR length 0x46 vs 0x2C disambiguates real events from boundary markers — the "Download All" pattern walks 1E/0A/1F to map all event keys+lengths upfront, then downloads each 0x46-keyed event in turn. Old stop_after_metadata=True knob is a workaround for the missing end_offset bound and becomes obsolete under the new walk. See new §7.8.5 / §7.8.6 / §7.8.7 for full details.
2026-05-04 §7.8.5, §7.8.8 CORRECTED — Event-N probe counter is just start_offset, NOT start_offset + 0x0046. The +0x46 formula in the original §7.8.5 was based on calling the off=0x2C boundary key the "start_key", but in the iteration walk cur_key passed into read_bulk_waveform_stream is always the off=0x46 WAVEHDR record key from 1F (the partial-record skip path in get_events re-runs 1F to advance past 0x2C boundary records). Adding +0x46 placed the probe one WAVEHDR past the actual event start; the response no longer contained STRT at byte 17, parse_strt_end_offset returned None, and the chunk loop fell back to the max_chunks=128 cap, walking ~110 chunks of post-event circular-buffer garbage. Confirmed against both the 5-1-26 "copy 2nd address" capture (probe at counter=0x2238 with key=01112238) and the 5-4-26 BW 2-sec event capture. Fixed in protocol.py read_bulk_waveform_stream v0.14.1.
2026-05-05 §7.8.1 (rule #3 added) CONFIRMED — Partial DLE stuffing of 0x10 bytes in 5A params region. The device's de-stuffing rule for the SUB 5A params region is: 10 1010, 10 02/03/04 → kept literal (inner-frame markers), 10 X for any other X → de-stuffs to just X (drops the 0x10). Therefore any 0x10 byte in the logical params followed by a byte NOT in {0x02, 0x03, 0x04, 0x10} MUST be doubled on the wire. This affects counters with 0x10 in the high byte — most importantly counter=0x1000, where logical params bytes ... 10 00 ... were being sent raw and the device de-stuffed 10 00 to just 00, returning the response for counter=0x0000 (= the file header + STRT). That STRT block then ended up embedded in the assembled file body at file offset 0x1016 and Blastware refused to open the file. This was the root cause of the long-standing ">1-sec event 0 won't open in BW" pattern (1-sec events worked because their end_offset < 0x1000, so no chunk request ever needed counter 0x10__). All 17 5A request frames in the 5-1-26 bwcap3sec capture (probe + 2 meta + 13 samples + TERM) now match BW byte-for-byte after the fix. Fixed in framing.py build_5a_frame v0.14.3.
2026-05-05 §7.8 / Blastware file format CONFIRMED — File body assembly is contiguous concatenation, no de-duplication. The "duplicate header+STRT strip" hack from v0.13.x was actively destroying valid waveform data — sample chunks at counter 0x1000 and beyond often coincidentally contain the byte sequence 00 12 03 00 STRT in their delta-encoded ADC stream, and the strip was zeroing 25 bytes per match. Removed in v0.14.2. The correct file body is: probe contribution + meta@0x1002 + meta@0x1004 + sample contributions in stream order + TERM contribution. Verified byte-perfect against BW reference M529LKIQ.G10 (8708 bytes, 0 differences) when fed the same A5 frames as the BW capture.

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, partial DLE stuffing of 0x10 in params (10 X where X∉{02,03,04,10} must be doubled to 10 10 X — see §7.8). Requires 1E-arm + 0C + 1F(0xFE) + POLL×3 before first probe. Walk: probe at counter=start_offset (event 1: 0x0000) → metadata pages 0x1002 + 0x1004 (event 1 only) → sample chunks at 0x0600, 0x0800, …, step 0x0200, bounded by end_offset parsed from STRT@data[17] of probe response → TERM frame at residual offset_word. Project:/Client:/User Name:/Seis Loc: live in the metadata pages, NOT in the sample-chunk stream. CONFIRMED 2026-05-05 (BYTE-PERFECT vs BW capture)
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 recording_mode (uint8 at anchor4 in E5 sf1), 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. See §7.6.4 for recording_mode enum. CONFIRMED 2026-04-02; recording_mode added 2026-04-20
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
2C CALL HOME CONFIG READ Two-step read, data offset 0x7C (124 bytes + 1-byte DLE artefact = 125 raw bytes). Returns Auto Call Home configuration: enable flag, dial string, scheduled call times, retry settings, modem timing. Response SUB = 0xD3. DLE note: logical value 0x03 (num_retries) is returned as \x10\x03 on the wire, which S3FrameParser preserves as two literal bytes — this shifts all subsequent field positions by +1. See §7.12 for full field map. CONFIRMED 2026-04-20
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
2C D3 CONFIRMED 2026-04-20
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
7E CALL HOME CONFIG WRITE Writes Auto Call Home configuration (127 bytes: 125-byte read payload + \x00\x00). Offset = data[1]+2 = 0x7E. Write format (DLE-aware checksum, only BW_CMD 0x10 doubled on wire). Response SUB = 0x81. Must be followed by SUB 0x7F confirm. 81 CONFIRMED 2026-04-20
7F CALL HOME WRITE CONFIRM Short frame, no data. Commits call home config write from SUB 0x7E. Response SUB = 0x80. 80 CONFIRMED 2026-04-20
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
7E 81
7F 80
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
ADC scale factor 40 C6 97 FD 6.206053 (in/s)/V — CONFIRMED 2026-04-17. This is the inverse sensitivity of the standard Instantel geophone = 1/0.161133. Interface Handbook §4.5: Range = 1.61133 V × 6.206053 = 10.000 in/s. Used by firmware: PPV (in/s) = ADC_voltage × 6.206053. Hardware constant — do NOT write. CONFIRMED
[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.6.4 Recording Mode

CONFIRMED — 2026-04-20 (BE11529 / firmware S338.17). Three targeted captures in a single Blastware session (4-20-26 directory), changing Recording Mode only between each write.

Recording mode is stored as a uint8 with different anchor-relative positions depending on whether you are reading from a device response or constructing a write payload.

In the SUB 71 write payload (3-chunk compliance write, cfg[5]):

Enum Mode
0x00 Single Shot
0x01 Continuous
0x02 Unknown (not yet observed)
0x03 Histogram
0x04 Histogram + Continuous (combined mode)

Anchor-relative position: anchor 3 (3 bytes before the 10-byte anchor in the write payload). The write payload layout in the region around the anchor:

cfg[anchor - 3] = recording_mode  (uint8)
cfg[anchor - 2] = sample_rate_hi  (uint8, MSB of uint16 BE)
cfg[anchor - 1] = sample_rate_lo  (uint8, LSB of uint16 BE)
cfg[anchor:anchor+10] = \x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00  ← anchor
cfg[anchor + 10:anchor + 14] = record_time  (float32 BE)

In the E5 read response (sub-frame 1, page=0x0010, data[17]):

The anchor appears at data[21] in this sub-frame. Recording mode is at data[17] = anchor 4 (one position earlier than in the write payload). This is because an extra 0x10 byte is present at data[18] in the read format (between recording_mode and sample_rate), which is NOT present in the write payload. The read-format layout:

data[17] = recording_mode  (uint8)
data[18] = 0x10            ← extra byte present in E5 read only; absent in SUB 71 write
data[19] = sample_rate_hi  (uint8, MSB of uint16 BE)
data[20] = sample_rate_lo  (uint8, LSB of uint16 BE)
data[21:31] = anchor (\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00)
data[31:35] = record_time  (float32 BE)

Chunk checksum at cfg[1024]: The first of the three SUB 71 write chunks (1027 bytes) contains a running checksum byte at cfg[1024] whose delta exactly equals the delta of cfg[5] (recording_mode). This byte reflects the cumulative change from recording_mode through to its position and should not be mistaken for a second copy of the recording_mode field.

Decode path (_decode_compliance_config_into): use data[anchor_pos - 4] where anchor_pos is the index of the first byte of the anchor in the assembled E5 cfg bytes.

Encode path (_encode_compliance_config): use cfg[anchor_pos - 3] = recording_mode value (write-payload offset; no extra 0x10 byte).


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.206053               ✅ CONFIRMED 2026-04-17 — ADC-to-velocity scale factor (= 1/sensitivity = (in/s)/V). Interface Handbook §4.5: Range = 1.61133 V × 6.206053 = 10.000 in/s (Normal range). Firmware uses: PPV (in/s) = ADC_voltage × 6.206053. Hardware constant — identical on all tested units. Do NOT write.
+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

§7.6 below describes the deprecated 0x0400-step walk and is RETAINED FOR HISTORICAL CONTEXT ONLY. The "A5[7] is metadata", "A5[9] is terminator", and chunk-counter frame-index claims in this section are all artifacts of the broken walk that was overrunning past event end by ~5×.

For the corrected protocol (v0.14.0+), use:

  • §7.8.5 — chunk addressing (probe at start_offset, samples step 0x0200, bounded by STRT end_offset)
  • §7.8.6 — TERM frame formula
  • §7.8.7 — fixed metadata pages 0x1002 / 0x1004 (this is where Project / Client / User Name / Seis Loc strings actually live — NOT in any sample-chunk frame)
  • §7.8.8 — multi-event "Download All" sequence

The waveform sample encoding described in §7.6.1 below (4-channel interleaved s16 LE, 8 bytes per sample-set) is NOT actually verified — see the retraction note at the top of §7.6.1. The frame-indexing claims and metadata-source claims in §7.6 are also wrong; use §7.8.5–§7.8.8.

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


7.6.1 Blast / Waveform mode — NOT VERIFIED (retracted 2026-05-08)

⚠️ RETRACTION (2026-05-08)

The "4-channel interleaved s16 LE, 8 bytes per sample-set" claim below was never actually validated. It got into this document because the decoder built around that assumption produced full-scale ±32K counts on every channel of the 4-2-26 capture, and the ±32K-shaped output was misread as "the signal must have saturated."

Cross-checking the BW-reported peaks proves the opposite:

Channel BW PPV (in/s) Expected ADC counts at 10 in/s FS
Tran 0.420 1,376
Vert 3.870 12,686
Long 0.495 1,622

None of these are anywhere near ±32K saturation. No event in the project's archive (across all captures from 1-2-26 onward) has ever come close to saturation either. Yet the decoder has consistently produced ±32K-shaped noise on every event. The right conclusion is that the byte-to-sample interpretation has been wrong the whole time, NOT that every event happened to saturate.

What's actually known about the body bytes:

  • The byte distribution is heavily skewed (24% 0x00, 10.5% 0x10, plus high frequencies of 0x01 / 0x04 / 0x0F / 0xF0 / 0xF1). Lots of 10 XX pairs. Reading them as LE int16 produces uniform ±32K noise — the signature of mis-aligned or encoded data.
  • The CHANGELOG note for v0.14.2 calls the body a "delta-encoded ADC stream" — that hint plus the byte distribution points toward a delta encoding with 0x10 as an escape marker, but no decoder has been worked out yet.
  • The histogram-mode codec in §7.6.2 IS verified and decoded correctly (different format: 32-byte blocks with 9× int16 LE samples + metadata). The same firmware emits both formats, so §7.6.2 may share encoding primitives with the waveform codec and is worth using as a structural hint when reverse-engineering.

Treat the spec below as a starting hypothesis to disprove, not ground truth. The frame-layout pieces (STRT location, preamble, chunk header) appear correct; the per-byte sample interpretation is the open question.

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: Channel ordering [Tran, Vert, Long, Mic] = [ch0, ch1, ch2, ch3] is the Blastware convention. This ordering has not been independently verified end-to-end, since no decoder yet produces samples that match BW's own rendering of the same event (see the retraction at the top of §7.6.1). Once the body codec is decoded, the per-channel PPV values from the 0C record (Tran=0.420, Vert=3.870, Long=0.495 in/s for the 4-2-26 capture) provide the cross-check that pins down channel order.

Historical note: earlier revisions of this section claimed the 4-2-26 blast had "saturated all four channels to ~3200032617 counts," citing that as evidence the s16 LE interpretation was correct. That claim was wrong — the ±32K values were the broken decoder's output, not the actual signal amplitude (which the 0C peaks above show was nowhere near saturation). Retracted 2026-05-08.


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 ✅, also mirrored in metadata pages)
"Client:"       → client name           (in SUB 5A metadata pages ✅  — NOT in 0C)
"User Name:"    → operator / user       (in SUB 5A metadata pages ✅  — NOT in 0C)
"Seis Loc:"     → sensor location       (in SUB 5A metadata pages ✅  — NOT in 0C)
"Extended Notes"→ notes field           (in SUB 5A metadata pages ✅)

UPDATED 2026-05-05: Client:, User Name:, and Seis Loc: come from the dedicated SUB 5A metadata pages at counter 0x1002 and 0x1004 — see §7.8.7. They are NOT present in the 210-byte SUB 0C waveform record.

An earlier draft of this doc claimed they came from "A5 frame 7" of the bulk waveform stream — that was an artifact of the deprecated 0x0400-step walk where the broken chunk counter formula happened to land sample-chunk fi=7 on top of the 0x1002 metadata page. Under the corrected v0.14.0+ walk (§7.8.5), sample chunks at 0x1000 / 0x1200 contain ordinary waveform data, and the metadata pages are read separately.

The strings reflect the compliance setup that was active when the monitoring session first started (not per-event). get_events() reads the metadata pages once at the start of the SFM session and the decoded values are stamped onto every event in that session.


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

The loop in this subsection is DEPRECATED — it uses the broken stop_after_metadata=True hack and the wrong sequence ordering. See §7.8.5–§7.8.8 for the corrected protocol. The pseudocode below is preserved as historical record only.

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)

§7.8.1 (frame format) — added 2026-04-02; v0.14.3 partial DLE stuffing finalized 2026-05-05. Frame format confirmed by reproducing Blastware wire bytes byte-for-byte across the 1-2-26 capture (10 frames) and the 5-1-26 bwcap3sec capture (17 frames, all match including the DLE-stuffed 10 10 00 for counter=0x1000).

SUB 5A initiates a bulk transfer of the raw sample data for a stored event. The response is a sequence of A5 frames. Project-info ASCII strings (Project:, Client:, User Name:, Seis Loc:, Extended Notes) live in the dedicated metadata pages at counter 0x1002 and 0x1004 (see §7.8.7), not in the sample-chunk stream.

For the corrected protocol read in order:

  • §7.8.1 — frame format (raw offset_hi, DLE-aware checksum, partial DLE stuffing of params)
  • §7.8.5 — chunk addressing (probe → metadata pages → samples → TERM, all bounded by end_offset)
  • §7.8.6 — TERM frame formula
  • §7.8.7 — fixed metadata pages 0x1002 / 0x1004
  • §7.8.8 — multi-event "Download All" sequence

§7.8.2–§7.8.4 are retained as historical record of earlier (incorrect) understandings — do not implement against them.

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^^

Three 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.

  3. Partial DLE stuffing of 0x10 bytes in the params region (CONFIRMED 2026-05-05). The device's de-stuffing rule for the params region is:

    • 10 10 → de-stuffs to 10
    • 10 02 / 03 / 04 → kept literal (these are inner-frame markers)
    • 10 X for other X → de-stuffs to just X (drops the leading 0x10)

    Therefore any 0x10 byte in the logical params that is followed by a byte NOT in {0x02, 0x03, 0x04, 0x10} MUST be doubled on the wire (10 X10 10 X) so the device's de-stuffer reproduces the original 10 X pair. This applies most commonly to counters with 0x10 in the high byte (e.g. counter=0x1000 produces logical params bytes ... 10 00 ..., which BW encodes on the wire as ... 10 10 00 ...). Without this stuffing the device interprets counter=0x1000 as 0x0000 and returns the probe response (= a copy of the file header + STRT record); that STRT block then ends up embedded in the assembled file body and Blastware refuses to open the file.

    0x10 bytes in offset_hi are still written RAW per (1) above — only the params region has this stuffing requirement. Metadata-page params for counter 0x1002 / 0x1004 survive without stuffing because 10 02 / 10 04 fall in the "kept literal" carve-out.

    Verified against BW 5-1-26 bwcap3sec frame 20: params logical bytes 00 01 11 10 00 00 00 00 00 00 00 (counter=0x1000) are encoded on the wire as 00 01 11 10 10 00 00 00 00 00 00 00 (12 wire bytes for 11 logical bytes).

7.8.2 Request Sequence — DEPRECATED 2026-05-01 (see §7.8.5–§7.8.7 for the corrected protocol)

The 0x0400-step / max(key4[2:4], 0x0400) formula in this section is WRONG. Five new BW MITM captures (4-27-26 + 5-1-26) prove the actual chunk increment is 0x0200, the chunk loop is bounded by end_offset from the STRT record (not by chunk count or by a device-side timeout), and the TERM frame's offset_word=0x005A magic is incorrect — the real TERM offset_word is computed from end_offset and the last chunk address. Under the deprecated formula SFM over-reads roughly 5× past the actual event end into post-event circular-buffer garbage, corrupting reconstructed Blastware files for any waveform ≥ 2 sec.

The whole "stop_after_metadata + one extra chunk + 0e 08 footer" workaround in this section was compensating for the missing end_offset bound. It is obsoleted by the STRT-bounded walk in §7.8.5.

Read this section for historical context only. For the correct protocol, jump to:

  • §7.8.5 — chunk addressing and the STRT end_offset
  • §7.8.6 — TERM frame formula
  • §7.8.7 — fixed metadata pages 0x1002 and 0x1004
Frame offset_word counter params Purpose
Probe 0x1004 0x0000 10 bytes (bulk_waveform_params(0)) Initiate transfer
Chunk 1 0x1004 max(key4[2:4], 0x0400) 11 bytes First data chunk
Chunk 2 0x1004 max(key4[2:4], 0x0400) + 0x0400 11 bytes Second chunk
Chunk N 0x1004 max(key4[2:4], 0x0400) + (N-1) * 0x0400 11 bytes Nth chunk
Termination 0x005A max(key4[2:4], 0x0400) + N * 0x0400 10 bytes End transfer

Historical correction notes (left in place to deter re-derivation of the same wrong formula): the table above was the result of three iterative "corrections" between 2026-04-06 and 2026-04-26 that progressively narrowed in on the wrong answer because every test was on events with key4[2:4]=0 and the device responds to whatever counter you ask for. The 5-1-26 captures with a non-zero start_key event (01112238) finally exposed the bug.

The stop_after_metadata=True flag (deprecated as a primary loop-exit) scanned for b"Project:" in the chunk stream because the metadata strings happened to be reachable when the broken 0x0400-step walk passed the global metadata pages at 0x1002/0x1004. Under the corrected walk, those strings come from explicit reads at counter=0x1002 and 0x1004, not from the sample-chunk stream — see §7.8.7.

7.8.3 A5 Frame Layout — DEPRECATED 2026-05-01

The "Frame 7 carries the compliance text block" claim below is WRONG. It was an artifact of the deprecated 0x0400-step walk where the broken counter formula happened to land sample-chunk fi=7 on top of the 0x1002 metadata page in flash. Under the corrected v0.14.0+ walk (§7.8.5), Frame 7 of the sample-chunk sequence is just sample-chunk #5 (counter=0x1000), and contains either ordinary waveform data or — critically when DLE-stuffing of params is wrong (§7.8.1.3) — a duplicate file header + STRT block when the device misinterprets counter=0x1000 as 0x0000. See §7.8.7 for the actual source of these strings.

Historical claim (NOT TO BE IMPLEMENTED): 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:

"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 do reflect the setup at event-record time, not the current device config. But the source is the metadata pages (§7.8.7), not "Frame 7" of the sample stream.

7.8.4 End-of-Stream Behaviour and Chunk Timing — REINTERPRETED 2026-05-01

The "1 raw byte then silence" pattern documented below was originally interpreted as "the device's natural end-of-event signal." The 5-1-26 captures show this is actually the device's response when the requester has walked past the addressable buffer region (i.e. ~5× past the actual event end under the deprecated 0x0400-step walk). Under the corrected STRT-bounded walk (§7.8.5), the stream ends cleanly with the TERM frame's response — no timeout, no 1-byte teaser. The fallback below remains useful as defensive handling for malformed events but should not be the primary loop-exit.

Defensive fallback handling in read_bulk_waveform_stream:

TimeoutError caught (rare under corrected walk):
  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 2-sec event (corrected walk) 14 (12 sample chunks + 2 metadata pages) + TERM
Chunks for a 3-sec event (corrected walk) 18 (16 sample chunks + 2 metadata pages) + TERM
Chunks for a continuation event (corrected walk) ~15 sample chunks + TERM (no metadata reread)
Chunks under deprecated walk for 2-3 sec event 37 (over-reads ~5×)
Data per chunk (corrected, 0x0200 size) ~540575 bytes wire (= 0x0200 payload + framing)
Data per chunk (deprecated 0x0400 step) 1,0361,123 bytes wire (= 0x0400 payload + framing)
Safe recv timeout per chunk 10 s (10× typical)
Default transport timeout 120 s → ~2-min stall at end-of-stream

ADC count-to-physical conversion — CONFIRMED 2026-04-17:

Raw samples are signed 16-bit integers (32,768 to +32,767). Source: Interface Handbook §4.5.

CONFIRMED 2026-04-17 — The max_range_geo field (float32 = 6.206053, bytes 40 C6 97 FD) is the ADC-to-velocity scale factor (inverse sensitivity, (in/s)/V) for the standard Instantel geophone, confirmed from Interface Handbook §4.5. The correct conversion formula is:

PPV (in/s) = ADC_voltage (V) × 6.206053
           = counts × (1.61133 / 32767) × 6.206053
           = counts × 4.982e-5  (in/s per count at full scale)

where geo_range = 1.61133 V × 6.206053 = 10.000 in/s is the Normal (Gain=1) full-scale range. The earlier ~9× overread was caused by mistakenly using 6.206053 as the range directly — it is actually the scale factor, and the range itself is ADC_fullscale × scale_factor = 1.61133 × 6.206053 = 10.000 in/s. Mic channel uses psi units with its own range (still unresolved).

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.8.5 Chunk addressing and the STRT end_offset (NEW 2026-05-01)

Confirmed across 3 events (4-27-26 + 5-1-26 captures).

params[0] is always 0x00. params[1:5] is a 4-byte absolute device flash-buffer address — equivalently, "the key of the page being requested." The device returns 0x0200 (= 512) bytes starting at that address. Increments between consecutive sample chunks are 0x0200, NOT 0x0400 (the previous 0x0400 figure was a Blastware-side artifact / our implementation's bug — see §7.8.2).

STRT record (data layout in the first A5 response)

The first A5 response (the probe response, or the first chunk for continuation events) contains a STRT record at byte offset 17 of data:

data[ 0:14]   echoes request: [chunk_size_hi=0x02 / 0x04 ...] [00] [01 11] [counter_hi counter_lo] [00 × 8] [00 12]
data[14:17]   10 03 00         ← inner DLE+ETX frame separator (preserved literally)
data[17:21]   "STRT"           ← magic
data[21:23]   ff fe            ← sentinel
data[23:27]   end_key          ← 4-byte key of where this event ENDS
data[27:31]   start_key        ← 4-byte key of where this event STARTS
data[31:33]   uint16 BE        ← ?? sample count or byte count, varies (not yet decoded)
data[33:35]   uint16 BE        ← ??
data[35]      0x46             ← record type marker (waveform full record)
data[36:]     additional pointers / first sample bytes — content varies by event

end_offset = (end_key[2] << 8) | end_key[3] is the authoritative event-end pointer. Use it to bound the chunk loop and to compute the TERM frame.

Chunk pattern by event location in buffer

Event 1 / start_key[2:4] = 0x0000 (first event after erase or wrap):

1.  Probe at counter = 0x0000          (params[1:5] = full key)
2.  Read fixed metadata pages          counter = 0x1002, then 0x1004
3.  Walk sample chunks                 counter = 0x0600, 0x0800, …, by 0x0200,
                                        up to but not including end_offset & 0xFE00
4.  TERM                               (see §7.8.6)

The range [0x0046, 0x0600) is skipped — likely some pre-event firmware-reserved area for the first slot in a freshly-erased buffer. Harmless to skip; BW does the same.

Event 2+ / start_key[2:4] != 0x0000 (continuation events in a populated buffer):

1.  First chunk at counter = start_key[2:4]              ← acts as both probe and first
                                                          sample chunk; response carries STRT at byte 17
2.  Walk sample chunks                                   counter += 0x0200 each
3.  TERM

start_key here is the off=0x46 WAVEHDR record key returned by 1F (e.g. 01112238), NOT the off=0x2C boundary key that immediately precedes it. An earlier draft of this spec described event-N as "probe at start + 0x46" — that formula was correct only if "start" meant the boundary key (0x21F2 in the 5-1-26 event 2 case). In the iteration walk used by SFM and BW, cur_key passed into the 5A flow is always the off=0x46 key, so the probe counter equals cur_key[2:4] with no extra offset. Adding +0x46 places the probe one WAVEHDR past the actual event start, the response no longer contains STRT at byte 17, and the chunk loop falls back to the max_chunks cap.

Confirmed:

  • 5-1-26 "copy 2nd address" BW capture: probe counter=0x2238 with key=01112238; A5[0] has STRT@17 with end_offset=0x417E.
  • 5-4-26 BW 2-sec event capture: same probe counter=0x2238, same end_offset=0x417E.

No metadata-page reads. Pages 0x1002/0x1004 are session-global and were already read during event 1 in the same Blastware session. In SFM, treat metadata pages as a once- per-MiniMateClient.connect() (or once-per-call-home) read, not per-event.

Verified end_offset values
Capture start_key end_key end_offset event size sample-chunk start
4-27-26 "open 2sec" / "copy event to disk" 01110000 01111ABE 0x1ABE 6,846 B 0x0600 (event-1 case)
5-1-26 "copy 3sec" / Download All event 1 01110000 011121F2 0x21F2 8,690 B 0x0600 (event-1 case)
5-1-26 "copy 2nd address" / DA event 2 01112238 (= 1F result) 0111417E 0x417E, span 0x1F8C = 8,076 B 0x2238 (= cur_key[2:4])
5-4-26 BW 2-sec event 01112238 0111417E 0x417E 0x2238 (= cur_key[2:4])

7.8.6 TERM Frame Formula (NEW 2026-05-01)

Confirmed across 3 events. Replaces the deprecated offset_word=0x005A / params[2] = key4[2] formula in §7.8.2.

The TERM frame fetches the partial last chunk and the file footer. Its response payload contains the bytes between the last full 0x0200-aligned chunk and end_offset — typically 20520 B — and is required for reconstructing the Blastware waveform file. Append the TERM response data to the chunk stream like any other A5 frame.

last_chunk_counter = address of last full 0x0200-byte chunk read
next_boundary      = last_chunk_counter + 0x0200
TERM offset_word   = end_offset - next_boundary
TERM params[0]     = key[0]                            (= 0x01 on every observed device)
TERM params[1]     = key[1]                            (= 0x11)
TERM params[2]     = (next_boundary >> 8) & 0xFF
TERM params[3]     = next_boundary & 0xFF
TERM params[4:10]  = zeros                              ← 10-byte params (not 11)

Frame = build_5a_frame(offset_word, params)

The device receives requested_address = (params[2] << 8) | offset_word (where offset_word contains both offset_hi and offset_lo of the 5A frame, with the high bit of offset_hi being effectively bit 0 of (end_offset >> 8)). It reconstructs end_offset and replies with (end_offset - next_boundary) bytes of waveform tail starting at next_boundary.

Verification
Event end_offset last chunk next_boundary TERM offset_word TERM params[2:4] TERM response size
2-sec 0x1ABE 0x1800 0x1A00 0x00BE 1A 00 208 B
3-sec 0x21F2 0x1E00 0x2000 0x01F2 20 00 520 B
Event-2 0x417E 0x3E38 0x4038 0x0146 40 38 (not measured directly; same pattern)

Equivalent way to write the formula:

  • offset_word = end_offset & 0x01FF — low 9 bits of end_offset
  • params[2:4] = (end_offset & 0xFE00) BE — high 7 bits of end_offset, low byte zeroed

(The two forms are arithmetically identical to end_offset - next_boundary and next_boundary because next_boundary = end_offset & 0xFE00 whenever the chunk loop stopped at the last full 0x0200 boundary below end_offset.)

7.8.7 Fixed Metadata Pages 0x1002 / 0x1004 (NEW 2026-05-01) 🔶

🔶 Inferred — observed in BW captures but page contents not yet byte-decoded.

Two chunk addresses are GLOBAL device/session metadata, not event-specific:

  • counter = 0x1002 — first metadata page
  • counter = 0x1004 — second metadata page

These are at fixed absolute addresses in the device's flash buffer. They contain the session-start compliance-setup ASCII strings — Project, Client, User Name, Seis Loc, Extended Notes — that under the deprecated 0x0400-step walk used to be discoverable in the sample-chunk stream as "A5 frame 7" content. Under the corrected 0x0200-step walk these strings come exclusively from the dedicated metadata-page reads, not from sample chunks.

Caching strategy

BW reads them ONCE per Blastware session, during event 1's download, and caches them. For SFM:

  • Read once per MiniMateClient.connect() / once per call-home session.
  • Subsequent events in the same session don't need to re-fetch them.
  • Their content does not change while iterating events. They DO change when the user applies a new compliance setup (SUB 71 write) — invalidate the cache then.
TODO — content layout

The byte-for-byte layout of pages 0x1002 and 0x1004 has not been decoded. First task on the implementation side: dump both pages from a fresh capture and verify they include all the strings currently extracted from the deprecated A5 frame 7 of the chunk stream. Compare to the existing _decode_a5_metadata_into parser — same string-search anchors (b"Project:", b"Client:", b"User Name:", b"Seis Loc:", b"Extended Notes") likely apply directly.

7.8.8 "Download All" Sequence (NEW 2026-05-01)

Confirmed from 5-1-26 "Download All" capture (raw_*_171216_download_all_2events.bin).

Before any 5A traffic, BW's "Download All" pre-walks the entire event chain to map keys and event boundaries:

SERIAL × 2  →  CHCFG  →  EVT_KEY (1E, all-zero) → key0
                      →  WAVEHDR (0A, key0) → off=0x46 (real event start)
                      →  EVT_NEXT (1F, all-zero) → key1
                      →  WAVEHDR (0A, key1) → off=0x2C (boundary)
                      →  EVT_NEXT → key2
                      →  WAVEHDR (0A, key2) → off=0x46 (real event start)
                      →  EVT_NEXT → key3
                      →  WAVEHDR (0A, key3) → off=0x2C (boundary)
                      →  EVT_NEXT → null sentinel

The DATA_LENGTH at data_rsp.data[5] (echoed BW offset for the data fetch step) disambiguates real events from boundary markers:

WAVEHDR offset Meaning
0x46 (= 70) Real event start key — this key has event data behind it
0x2C (= 44) Boundary marker — this key is the END of the previous event AND the start of the empty/header gap before the next event

Pairs: each real event spans [real_key, next_real_key) in the buffer. In the example above: event 1 = [01110000, 011121F2), event 2 = [01112238, 0111417E). Note that the "end of event 1" key (011121F2) is also the "boundary key" that comes BEFORE event 2's real start key (01112238) — they differ by exactly 0x46 bytes (the event header size).

After the pre-walk completes, BW downloads each 0x46-keyed event in turn using the 5A bulk stream protocol from §7.8.5. Use the 0x46 keys, not the 0x2C keys, as input to read_bulk_waveform_stream.


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 Single Shot (0x00) / Continuous (0x01) / Histogram (0x03) / Histogram+Continuous (0x04) recording_mode — write: cfg[anchor3]; read E5 sf1: data[anchor4] — confirmed 2026-04-20
Record Stop Mode Fixed Record Time / Auto / Manual Stop Hint: data[40] in E5 sf1 changed 01 7F00 00 alongside Continuous → Single Shot; may be related but unconfirmed independently
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 (range selector) Normal 10.000 / 1.25 in/s geo_range uint8 — CONFIRMED 2026-04-20. Offset = Tran+33 (same in E5 read and SUB 71 write — 2126-byte buffer is round-tripped verbatim). 0x00=Normal 10 in/s, 0x01=Sensitive 1.25 in/s. Applied to Tran/Vert/Long. Tran+20 is NOT this field (constant 0x01 on all captures).
Chan 1-3 ADC Scale Factor 6.206053 (in/s)/V geo_adc_scale float32 — CONFIRMED 2026-04-17. Offset = Tran+28 (same in E5 read and SUB 71 write). Inverse sensitivity = 1/0.161133. Interface Handbook §4.5: 1.61133 V × 6.206053 = 10.000 in/s. Hardware constant — do NOT write.
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 SUB 5A metadata pages at counter 0x1002 / 0x1004 — see §7.8.7)
Client ASCII string (sourced from SUB 5A metadata pages — see §7.8.7)
User Name ASCII string (sourced from SUB 5A metadata pages — see §7.8.7)
Seis Loc ASCII string (sourced from SUB 5A metadata pages — see §7.8.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.

7.12 Auto Call Home Config (SUB 0x2C / 0x7E / 0x7F) — CONFIRMED 2026-04-20

Confirmed from bridges/captures/4-20-26/call home settings/ — 10 BW TX write frames diffed against the S3 read payload. Accessible in Blastware via Remote Access → Setup Unit.

7.12.1 Read Protocol — SUB 0x2C → Response 0xD3

Standard two-step read:

Step Offset Purpose
Probe 0x0000 Get ack (no data returned)
Data 0x007C (124) Receive 125-byte raw payload

DATA_LENGTHS[SUB_CALL_HOME] = 0x7C

The raw payload is accessed as data_rsp.data[11:] — this is 125 bytes (not 124) because the device returns logical value 0x03 (num_retries=3) as the two-byte wire sequence \x10\x03. S3FrameParser is in STATE_IN_FRAME when it sees 0x10, transitions to STATE_AFTER_DLE, and then on 0x03 (ETX qualifier) it would normally end the frame — but in the _IN_FRAME_DLE state it instead appends both the 0x10 and the 0x03 literally to the payload. The result: raw[117] = 0x10, raw[118] = 0x03, and all subsequent fields are shifted +1 from their logical positions.

7.12.2 Raw Payload Field Map (125 bytes, from data_rsp.data[11:])

All offsets are into the 125-byte raw array. Offsets ≥ 119 are shifted +1 from logical due to the DLE-escaped 0x03 at raw[117:119].

Raw Offset Field Type Notes
[5] auto_call_home_enabled uint8 0x00 = disabled, 0x01 = enabled
[6:46] dial_string ASCII 40-byte null-padded, e.g. "12345" or phone number
[87] after_event_recorded uint8 0x00 = off, 0x01 = on
[91] at_specified_times uint8 0x00 = off, 0x01 = on
[93] time1_enabled uint8 0x00 = off, 0x01 = on
[101] time1_hour uint8 023
[102] time1_min uint8 059
[95] time2_enabled uint8 0x00 = off, 0x01 = on
[105] time2_hour uint8 023
[106] time2_min uint8 059
[117] DLE prefix 0x10 Part of \x10\x03 wire encoding for num_retries value 3
[118] num_retries (value = 3) uint8 Logical value 0x03; check raw[117] == 0x10 to detect DLE prefix
[120] time_between_retries_sec uint8 Shift +1 from logical 119
[122] wait_for_connection_sec uint8 Shift +1 from logical 121
[124] warm_up_time_sec uint8 Shift +1 from logical 123

Unconfirmed fields (offsets not yet mapped from captures):

  • Time slots 3 and 4 (if they exist — Blastware UI only shows 2 time slots in observed sessions)
  • modem_power_relay_enabled (bool)
  • storage_mode (call home trigger on all events vs. triggered only?)

7.12.3 DLE-Escaped 0x03 — Critical Detail

The \x10\x03 sequence at raw[117:119] is not a DLE stuffing artifact in the usual sense. Standard DLE stuffing escapes \x10\x10\x10. But here the device is encoding the integer value 3 in a position where the byte \x03 would be indistinguishable from the frame ETX terminator. The device therefore sends \x10\x03 (DLE + ETX = "inner-frame terminator" in S3 inner-frame syntax). S3FrameParser correctly handles this: in STATE_AFTER_DLE, seeing \x03 (ETX) while inside an outer frame causes it to append both \x10 and \x03 as literal bytes rather than ending the frame. The outer frame only terminates on a bare \x03 (without the DLE prefix).

The write frame sends these bytes verbatim — the device accepts \x10\x03 in the write payload and interprets it as the value 3. No transformation is needed in _encode_call_home_config().

Limitation: Any field that needs to encode the value 3 (0x03) requires this DLE prefix. The current encoder raises ValueError if any hour or minute field equals 3, since the encoder does not yet implement DLE-prefixed writes for arbitrary field positions. In practice, 3:00 AM / 3 minutes past are unlikely scheduled call times.

7.12.4 Write Protocol — SUB 0x7E → 0x7F

Write format (same as other write commands — only BW_CMD 0x10 doubled on wire; all other bytes written raw; DLE-aware checksum):

Step SUB Payload Offset Response
Data write 0x7E 127 bytes (125-byte read payload + \x00\x00) data[1]+2 = 0x7E (126) 0x81
Confirm 0x7F empty 0x00 0x80

Write payload construction:

write_payload = bytearray(raw_125_bytes)
write_payload.append(0x00)
write_payload.append(0x00)
# patch fields in-place, then pass bytes(write_payload) to build_bw_write_frame

Offset formula: write_payload[1] = 0x7C (same as DATA_LENGTH). offset = write_payload[1] + 2 = 0x7C + 2 = 0x7E = 126. This follows the identical pattern as SUB 0x68 (event index write) and SUB 0x69 (waveform write).

No preceding 0x2C read required — Blastware sends SUB 0x7E directly using cached state. The seismo-relay implementation always reads first (get_call_home_config()) before writing for safety.

7.12.5 Implementation Notes

  • MiniMateProtocol.read_call_home_config() — standard two-step read; returns data_rsp.data[11:] (125 bytes raw)
  • MiniMateProtocol.write_call_home_config(data) — sends SUB 0x7E (127-byte payload) then SUB 0x7F confirm
  • MiniMateClient.get_call_home_config()CallHomeConfig dataclass
  • MiniMateClient.set_call_home_config(...) — reads current config, patches via _encode_call_home_config(), writes back
  • _decode_call_home_config(raw) — handles DLE prefix detection at raw[117]
  • _encode_call_home_config(raw, ...) — patches in-place, appends 2 trailing zeros; raises ValueError if any hour/min == 3
  • REST API: GET /device/call_home and POST /device/call_home in sfm/server.py
  • Web UI: "Call Home" tab in sfm/sfm_webapp.html

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.2061RESOLVED 2026-04-17. Confirmed as the ADC-to-velocity scale factor = inverse sensitivity = 1/0.161133 = 6.206053 (in/s)/V. Source: Interface Handbook §4.5 formula Range = 1.61133 V / Sensitivity. For standard Instantel geo at Normal (Gain=1) range: Sensitivity = 1.61133/10 = 0.161133 V/(in/s), scale = 6.206053. Firmware: PPV (in/s) = ADC_voltage × 6.206053. The earlier ~9× overread was from using 6.206053 directly as range instead of as scale factor (range = 1.61133 V × 6.206053 = 10.000 in/s). Hardware constant — do NOT write. RESOLVED 2026-02-26 Resolved 2026-04-17
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.
ADC Scale Factor (geo_adc_scale) §3.8.4 / Interface Handbook §4.5 Channel block, Tran+28 (same in E5 read and SUB 71 write), float32 BE float32 BE = 6.206053 CONFIRMED 2026-04-17 — inverse sensitivity (in/s)/V. Range = 1.61133 V × 6.206053 = 10.000 in/s. Firmware: PPV (in/s) = ADC_voltage × 6.206053. Hardware constant, identical on all units. Do NOT write.
Max Geo Range (geo_range) §3.8.4 Channel block, Tran+33 (same in E5 read and SUB 71 write), uint8; applied to Tran/Vert/Long uint8 CONFIRMED 2026-04-20 — 0x00=Normal 10.000 in/s, 0x01=Sensitive 1.250 in/s. NOTE: Tran+20 reads 0x01 on ALL captures regardless of range — it is NOT this field.
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 Write: cfg[anchor3], uint8. Read (E5 sf1): data[anchor4], uint8. Note: extra 0x10 byte at read data[anchor3] shifts offset by 1 vs write. uint8 0x00=Single Shot, 0x01=Continuous, 0x02=unknown, 0x03=Histogram, 0x04=Histogram+Continuous. CONFIRMED 2026-04-20
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


Appendix D — Blastware Binary File Formats (.N00 / .MLG / others)

CONFIRMED 2026-04-21 — all fields verified by binary diff of reconstructed vs reference files from the 4-3-26-multi_event capture (M529LIY6.N00, BE11529.MLG).

⚠️ EXTENSION MAPPING REFUTED 2026-04-21 — earlier assumption that extension encodes recording mode is FALSE. A continuous-mode event produced .EI0, not .9T0. Extension encoding algorithm is unknown. Do not use extension to infer recording mode.

D.1 Common File Header (22 bytes)

All Blastware files (regardless of type) share an 18-byte prefix followed by a 4-byte type tag.

Offset Length Value Description
0x00 6 10 00 01 80 00 00 Fixed prefix
0x06 10 Instantel\x00 ASCII string
0x10 2 07 2c Fixed suffix
0x12 4 varies File type tag (see below)

Total header: 22 bytes.

Type tags:

Extension Type tag Description
.N00 00 12 03 00 Waveform event (confirmed)
.9T0 00 12 03 00 Waveform event — same type tag as .N00 (assumed; not independently confirmed)
.EI0 00 12 03 00 Waveform event — same type tag (assumed; continuous-mode event observed 2026-04-21)
.MLG 22 01 0e a0 Monitor log

Extension encoding — new firmware (V10.72+) FULLY DECODED (confirmed 2026-04-22):

The extension differs depending on how the file was saved:

Download method Extension format Example
Manual / direct (Blastware connected to unit) AB0 (3 chars) .CE0
Call-home / ACH AB0W or AB0H (4 chars) .CE0H

Where:

  • AB = 2-char base-36 of total_seconds % 1296; A = value // 36, B = value % 36
  • total_seconds = (event_local_time 1985-01-01T00:00:00_local) in seconds
  • 0 = always literal digit zero
  • W = Full Waveform, H = Full Histogram (ACH only)

Base-36 alphabet: 09 = 09, AZ = 1035.

The 10-year production archive contains only ACH files (all end in W or H). Manual Blastware downloads produce the same AB0 prefix but without the trailing type character.

3-day cycle property (confirmed 2026-04-22): A unit recording at a fixed daily time cycles through exactly 3 different extensions with a 3-day period. Each calendar day shifts total_seconds % 1296 by 864 (since 86400 % 1296 = 864). The cycle repeats every 3 days because gcd(1296, 864) = 432. Confirmed from archive: top 3 extensions CE0H (95), 0E0H (93), OE0H (91) are the 3-day cycle of a 06:00:14 daily call-in (seconds-in-window = 446, 14, 878).

B character invariance: 864 = 24 × 36, so adding one day never changes value % 36 — the second extension character is invariant for a fixed daily recording time. Only the first character cycles through 3 values.

Old firmware (S338): 3-char extensions observed (.N00, .EI0, etc.) — may simply be manual downloads under the same AB0 scheme, or a different encoding. Not yet confirmed.

Micromate Series 4 uses a different extension format (observed: IDFH, IDFW). This formula does NOT apply to Micromate units.

All waveform files share the same 00 12 03 00 type tag regardless of extension. Blastware identifies file type by extension, not by type tag alone.

D.2 Timestamp Encoding (Blastware files)

All timestamps in N00 and MLG files use an 8-byte big-endian format:

Byte Field
0 day (uint8)
1 month (uint8)
23 year (uint16 BE)
4 0x00 (reserved)
5 hour (uint8)
6 minute (uint8)
7 second (uint8)

Example: 01 04 07 ea 00 00 1c 08 → April 1, 2026, 00:28:08.

Note: this differs from the 8-byte protocol timestamp ([day][sub_code][month][year_HI][year_LO][0x00][hour][min][sec] = 9 bytes) used in the device's on-wire 0C waveform records. The file format uses a compact 8-byte layout without the sub_code byte.

D.3 N00 File Format — Single-Shot Waveform Event

File layout: [22B header] [21B STRT record] [body bytes] [26B footer]

D.3.1 STRT Record (21 bytes)

The STRT record immediately follows the 22-byte header.

Offset Length Field Notes
0 4 STRT ASCII literal
4 2 ff fe Fixed
6 4 event key (key4) 4-byte waveform key
10 4 device-specific NOT a repeat of key4 — device-internal field
14 6 device-specific NOT zero-padded — device-internal fields
20 1 rectime uint8 seconds

Critical: The STRT record must be copied verbatim from A5[0].data[7+strt_pos:] — bytes [10:20] contain device-specific values that cannot be reconstructed from protocol-level Event fields alone.

D.3.2 Body Bytes (variable)

The body is reconstructed from the raw A5 bulk waveform stream frames by stripping DLE framing markers and taking the appropriate slice of each frame's data section.

Per-frame contribution (from frame.data):

Frame Skip amount Notes
A5[0] (probe) 7 + strt_pos_in_w0 + 21 Skip frame.data prefix + STRT record
A5[1] 13 7-byte prefix + 6-byte first-chunk header
A5[2..N] 12 7-byte prefix + 5-byte chunk header
Terminator (page_key=0x0000) 11 7-byte prefix + 4-byte terminator header

DLE strip rule: For each frame's contribution (frame.data[skip:]), strip any 0x10 byte immediately followed by 0x02, 0x03, or 0x04. Only the 0x10 is stripped; the following byte is kept as payload.

Split-pair edge case: When frame.data[-1] == 0x10 AND frame.chk_byte ∈ {0x02, 0x03, 0x04}, the S3FrameParser split a DLE+XX pair at the payload/checksum boundary. Reunite the bytes before stripping (relevant + bytes([chk_byte])), then always remove the trailing chk_byte from the result (stripped[:-1]) — chk_byte is the wire checksum, never payload.

Body/footer split: Accumulate all frame contributions (data frames + terminator) into all_bytes. Then:

  • body = all_bytes[:-26] (variable length)
  • footer = all_bytes[-26:] (always 26 bytes — extracted from terminator content)

The footer terminates the N00 file. Its bytes come directly from the terminator A5 frame's inner content — do NOT reconstruct from event metadata.

Offset Length Field Notes
0 2 0e 08 Fixed marker
2 8 ts1 Start timestamp (8B big-endian)
10 8 ts2 Stop timestamp (8B big-endian)
18 6 00 01 00 02 00 00 Fixed
24 2 CRC 2-byte CRC — algorithm unconfirmed

CRC: The 2-byte CRC at footer[24:26] has an unconfirmed algorithm. In M529LIY6.N00 it reads fe da. Attempts to match CRC-16/CCITT, CRC-16/IBM, CRC-32 (truncated), and 40+ polynomial/init combinations all failed. The writer copies it verbatim from the terminator frame.

D.4 MLG File Format — Monitor Log

File layout: [308B header] [N × 292B records]

D.4.1 MLG Header (308 bytes)

Offset Length Field Notes
0x00 22 common header prefix + 22 01 0e a0 type tag
0x16 16 unknown observed as zeros in BE11529.MLG
0x2A 8 serial number null-padded ASCII (e.g. "BE11529")
0x32 remainder zero pad pads to 308 bytes total

D.4.2 MLG Record (292 bytes each)

Offset Length Field Notes
0 2 CRC 2-byte CRC — algorithm unconfirmed; write as 00 00
2 4 22 01 0e 80 Record marker
6 8 ts1 Start timestamp (8B big-endian)
14 8 ts2 Stop timestamp (8B big-endian); zeros if no stop
22 4 flags Record type flags (see below)
26 10 serial Null-padded ASCII serial number
36 variable text Type-dependent content
remainder zero pad pads to 292 bytes total

Record flags:

Value Meaning
ff ff 00 00 Monitoring start with no stop recorded
01 00 02 00 Triggered event (has ts1 + ts2)
02 00 00 00 Monitoring interval (has ts1 + ts2)

Text content for triggered events (flags = 01 00 02 00):

Byte Field
0 0x08
18 ts1 copy (8B big-endian)
9+ "Geo: X.XXX in/s\x00" ASCII geo threshold

D.4.3 MLG CRC

The 2-byte CRC at record[0:2] uses an unconfirmed algorithm. Tested against CRC-16/CCITT, CRC-16/IBM, CRC-32 (truncated), word sums, XOR variants, and 40+ polynomial/init combinations — none matched. The writer emits 00 00. Blastware may reject files with incorrect CRCs (impact on import unknown — TODO: test).

D.5 Filename Encoding PARTIALLY CONFIRMED 2026-04-22

Blastware assigns waveform filenames of the form <prefix_letter><serial3><stem><ext>, where:

D.5.1 Serial Prefix CONFIRMED 2026-04-22

The first 4 characters of the filename encode the full device serial number:

prefix_letter = chr(ord('B') + floor(serial_numeric / 1000))
serial3       = f"{serial_numeric % 1000:03d}"   (last 3 digits, zero-padded)

Where serial_numeric is the integer after the "BE" device-type prefix.

Examples (all confirmed from archive):

Serial serial_numeric / 1000 prefix_letter serial3 Filename prefix
BE6907 6 H 907 H907
BE7145 7 I 145 I145
BE11529 11 M 529 M529
BE14036 14 P 036 P036
BE17353 17 S 353 S353
BE18003 18 T 003 T003
BE18191 18 T 191 T191
BE18676 18 T 676 T676

Interpretation: The prefix letter encodes the production generation (batch of 1000 units). B=generation 0 (serials 0999), C=generation 1 (10001999), etc. No units with prefix A have been observed — the earliest known units start around serial 2000+ (prefix D).

Note: The "BE" device-type prefix is implicit. The filename only encodes the numeric part of the serial. Other Instantel device types (Micromate, Blastmate) may use a different scheme.

D.5.2 Stem + Extension — full timestamp encoding FULLY CONFIRMED 2026-04-22

The stem (4 chars) and AB extension (2 chars) together form a 6-digit base-36 number encoding a complete second-resolution timestamp:

total_seconds = stem_int * 1296 + ab_int
event_local_time = datetime(1985, 1, 1) + timedelta(seconds=total_seconds)
  • Epoch: 1985-01-01 00:00:00 device local time CONFIRMED — verified against 3,248 files from a 10-year production archive; zero errors (only 2 mismatches were Micromate IDFH/IDFW files which use a completely different naming scheme)
  • Unit: 1296 seconds = 36² ≈ 21.6 minutes per stem increment
  • Alphabet: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" (digits then uppercase letters)
  • Collision: Events within the same 21.6-minute window share a stem; extension distinguishes them

Decoding example — P036L318.C80H (BE14036, Full Histogram):

stem  L318 = 21×36³ + 3×36² + 1×36 + 8 = 983,708
AB    C8   = 12×36  + 8               =     440
total_sec  = 983,708 × 1296 + 440     = 1,274,886,008
event_time = 1985-01-01 + 1,274,886,008s = 2025-05-26 15:00:08 local

Note on local time: The device's onboard clock is set to the local timezone of the deployment site. The epoch and all timestamps are in that same local time — there is no UTC conversion. Files moved between timezones will decode to the original deployment timezone.

D.5.3 Extension taxonomy

Third character of extension is always '0'. File type is identified by extension, not by the type tag in the header (all waveform extensions share type tag 00 12 03 00).

Extension Recording mode Sample rate Status
.N00 Single Shot (0x00) 1024 sps CONFIRMED
.9T0 Continuous (0x01) 1024 sps CONFIRMED
.490 ? ? observed from M529LJ8V.490
.5K0 ? ? observed from M529LJDY.5K0
.980 ? ? observed from M529LJDY.980
.ML0 ? ? observed from M529LJDY.ML0 (167s duration; possibly Histogram)

Why 5 extensions for "Continuous"? Binary analysis of all 6 example files shows that .9T0, .490, .5K0, .980, .ML0 are byte-for-byte identical in all metadata regions (compliance anchor block, channel descriptor blocks Tran/Vert/Long/MicL). The A5 frame 7 body reflects the session-start compliance config, not the per-event capture config. All 5 files show recording_mode=0x01 and sample_rate=1024 in the body. The extension must therefore encode the capture-time compliance state — likely a combination of recording mode, sample rate, and possibly mic units or other options. This cannot be determined from file body alone without capture-time compliance data from the 0C record sub_code and the actual waveform sample count.

DLE-shift offset note for reading recording_mode from N00/9T0 body:

The compliance block in the file body has been through _strip_inner_frame_dles. The 0x10 constant at logical anchor7 (between recording_mode and sample_rate_HI) gets stripped when sample_rate_HI = 0x04 (1024 sps), because 0x10 precedes 0x04 ∈ {0x02,0x03,0x04}. After stripping, the anchor shifts left by 1, so:

1024 sps (strip occurs) 2048 or 4096 sps (no strip)
file[anc7] = recording_mode file[anc8] = recording_mode
file[anc6:anc4] = sample_rate file[anc6:anc4] = sample_rate

For 1024 sps files, the expected file bytes around the anchor are:

file[anc9]: mode_prefix (0x00 for Single Shot/Continuous; 0x10 for Histogram)
file[anc8]: 0x00 (was recording_mode, but shifted away — now reads 0x00 for mode_prefix)
file[anc7]: recording_mode (0x00=Single Shot, 0x01=Continuous, etc.)
file[anc6]: 0x04  (sample_rate_HI for 1024 sps)
file[anc5]: 0x00  (sample_rate_LO)
file[anc4]: histogram_interval_HI
file[anc3]: histogram_interval_LO

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.