User uploaded 3 high-amplitude events (PPV 6-7 in/s — shook the geophone
hard) to decode-re/5-11-26/. These cracked the Tran codec:
- Preamble bytes [3:5] and [5:7] = Tran[0] and Tran[1] as int16 BE
in 16-count units (LSB = 0.005 in/s). Confirmed across all 7
fixtures.
- First data block carries Tran deltas from sample 2 onward:
* 10 NN block: NN/2 bytes of payload, each byte = two 4-bit signed
nibble deltas (high nibble first)
* 20 NN block: NN int8 signed deltas
Verified 22+42+46 = 110 Tran samples across SP0/SS0/SV0 with 0 errors
against BW's ASCII export.
Why the earlier 96-combination brute force failed: the quiet 5-8
events all had T[0] = T[1] ≈ 0 so the preamble's per-channel encoding
was undetectable. Loud events made the encoding obvious.
What's solved:
- minimateplus.waveform_codec.decode_tran_initial: returns first
N Tran samples in 16-count units for any body.
- Walker length formula for in-data 30 NN blocks (NN*2 instead of NN*4).
- Walker now handles bodies that start with 20 NN (in addition to 10 NN).
What's still open:
- Tran past the first data block (multi-block channel switching).
- Vert / Long / MicL channel encodings.
- Walker correctness past offset ~427 in event-b.
Tests: 36 pass. decode_waveform_v2 still returns None — the full
multi-channel decoder is not wired up. decode_tran_initial is the
new verified entry point.
Files: minimateplus/waveform_codec.py, tests/test_waveform_codec.py
(adds 5-11-26 fixtures + decode_tran_initial tests), and
docs/instantel_protocol_reference.md §7.6.1 (Tran codec spec).
196 KiB
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.17 → 0x11 = 17). Byte 0 is unit-specific, derivation unknown. |
| 2026-02-26 | §15 Binary Log Format | NEW: .bin logger format strips DLE from ETX (0x10 0x03 → 0x03). 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 68–83. |
| 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.17 → S337.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 1–2 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 0–255 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". 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 0x53–0x57. Two-unit comparison: BE18189 (calibrated 2023) has 07 E7 at 0x56–0x57; BE11529 (calibrated 2025) has 07 E9. Bytes 0x56–0x57 = uint16 BE calibration year ✅ CONFIRMED. Adjacent bytes at 0x53–0x55 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[0–8]: [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 anchor−2, 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 ≈ 9–11 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 1–16 have varying data lengths (1036–1123 bytes); chunks 17–35 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 0–4 responded), then ~40s silent gap while sensor check ran, then channels 5–7 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 0xFF−SUB 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 anchor−8 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 anchor−9 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 10 → 10, 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
0x41as STX and0x02/0x03as 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: 0x10 → 0x10 0x10. This prevents the parser from confusing real data with frame control sequences.
- Transmit: Replace every
0x10in payload with0x10 0x10 - Receive: Replace every
0x10 0x10in the frame body with a single0x10
| 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
0x41ACK always arrives in a separateread()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 0x10 → 0x10):
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 1–2: After de-stuffing, bytes 1 and 2 are both
0x10in 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 consecutive0x10bytes (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_label−12 (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 anchor−4 in E5 sf1), sample_rate (uint16 BE at anchor−2), 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] (0x0000–0x0007 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: 46–47 bytes IDLE, 48–49 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 0x60–0x83 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 02start,10 03end,10 10stuffing). 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).0x20is 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 firmwareS337.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 |
| 0x56–0x57 | 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 (0–255 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
193237and151147). Trigger changed0.500 → 0.200, then0.200 → 0.600. Alarm changed1.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
+0x28absolute 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 atcfg[11]. Search range widened tocfg[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
0x0010correctly: 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 00 → 10 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.setcross-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.setfile =0x00000003(3 sec), which would be00 00 00 03on wire — not0x082A. The original sessions had record time = 2, which would be00 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 1–9) 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 ofread_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 STRTend_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 — 🟡 STRUCTURAL FRAMING + TRAN CODEC DECODED (2026-05-11)
Status (2026-05-11): Block-level framing is solved. The Tran-channel encoding (preamble + first data block) is fully verified against the 3-event May 11 2026 high-amplitude bundle (PPV 6-7 in/s) and the 4-event May 8 bundle. Verts / Long / MicL channel encodings and multi-block Tran continuation are still open. The previous int16 LE claim remains REFUTED (see history below).
The earlier "4-channel interleaved s16 LE, 8 bytes per sample-set" claim was never validated and was wrong. No event in the project's archive ever came close to ADC saturation, yet the int16 LE decoder consistently produced full-scale ±32K noise — that was the signature of mis-aligned encoded data, not signal saturation.
Body file layout
A Blastware waveform-file body (the variable-length section between the 21-byte STRT record and the 26-byte file footer) is composed of tagged variable-length blocks, NOT raw int16 samples.
[preamble: 7 or 9 bytes]
[stream of tagged blocks]
[trailer: per-channel summary blocks]
Preamble (CONFIRMED 2026-05-11 across 3+4 events):
body[0:3] = 00 02 00 magic
body[3:5] = Tran[0] int16 BE first Tran sample (LSB = 0.005 in/s)
body[5:7] = Tran[1] int16 BE second Tran sample
The preamble is therefore 7 bytes long. Earlier observations of a
"9-byte preamble" on continuous-mode events were a misread — those
events still have a 7-byte preamble; the next 2 bytes are part of the
first 10 NN or 20 NN data block (its tag).
Verified preamble decode for all 7 fixture events — Tran[0] and Tran[1] from the preamble bytes exactly match the BW ASCII export (rounded to 0.005 in/s):
| Event | Preamble [3:7] (hex) | T[0] decoded | T[0] truth | T[1] decoded | T[1] truth |
|---|---|---|---|---|---|
| event-a (May 8) | 01 00 00 00 |
+1 | +1 (0.005) | 0 | 0 |
| event-b (May 8) | ff ff ff 00 |
-1 | -1 | -1 | -1 |
| event-c (May 8) | 00 00 00 00 |
0 | 0 | 0 | 0 |
| event-d (May 8) | 00 00 00 00 |
0 | 0 | 0 | 0 |
| SP0 (May 11) | 00 04 00 04 |
+4 | +4 (0.020) | +4 | +4 |
| SS0 (May 11) | ff a7 ff a7 |
-89 | -89 (-0.445) | -89 | -89 |
| SV0 (May 11) | fd 17 fd 06 |
-745 | -745 (-3.725) | -762 | -762 |
Block tags (CONFIRMED 2026-05-08)
Every block starts with a 2-byte tag. Five tag types are confirmed:
| Tag (hex) | Block type | On-wire length |
|---|---|---|
10 NN |
Small-delta data block | NN/2 + 2 bytes |
20 NN |
Literal data block (int8-shaped) | NN + 2 bytes |
00 NN |
2-byte marker between data blocks | 2 bytes |
30 NN |
Trailer summary block | NN × 4 bytes |
40 02 |
Segment header | 20 bytes (fixed) |
NN is always a multiple of 4. 10 NN and 20 NN data blocks
alternate with 00 NN markers — every 10/20 NN block is
followed by a 00 NN marker before the next data block.
Segments
The body is divided into ~16 SEGMENTS for a 1280-sample event (= 1
segment per ~80 sample-sets), separated by 40 02 segment headers.
A 3328-sample event has ~42 segments.
The 18-byte 40 02 payload structure (CONFIRMED across all 4
fixtures by inspecting the increment of bytes [8:12]):
| Offset | Length | Field |
|---|---|---|
| 0 | 4 | Anchor / channel state (open — see below) |
| 4 | 4 | Variable field (open) |
| 8 | 4 | uint32 LE counter — increments by 1 per segment |
| 12 | 4 | Fixed pattern 02 00 00 01 |
| 16 | 2 | Variable tail |
The counter at bytes [8:12] starts in the 0x40s for a freshly-erased device and increments cleanly — useful as a structural sanity check.
Examples from event-c (1 sec single-shot):
Segment header 1 (offset 235):
40 02 | 00 00 00 00 | 0a 4b 01 1e | 47 00 00 00 | 02 00 00 01 | 00 01
^counter=0x47
Segment header 2 (offset 523):
40 02 | ff fe ff fe | 13 f5 01 06 | 48 00 00 00 | 02 00 00 01 | 00 02
^counter=0x48 (+1)
Trailer
The trailer (after the last segment's data) is a sequence of 32-byte
30 08 blocks plus a final 30 04 / 20 04 / 40 02 summary
ending in the constant 2-byte tail 00 1A. These contain
per-channel statistics (peak times, peak values, mean offsets — bytes
in the form f3/f4/f5 near 20 10 markers strongly resemble
int8 channel-bias values around -12). Detailed decoding of the
trailer is outside the path needed for sample reconstruction.
Tran channel codec — CONFIRMED 2026-05-11
The first data block (immediately after the 7-byte preamble) carries Tran-channel deltas starting at sample 2. Two block types in alternation:
-
10 NN:NN/2bytes of payload. Each byte = two 4-bit signed nibbles (high nibble first; 0..7 → 0..+7, 8..F → -8..-1). Each nibble is one Tran delta in 16-count units. -
20 NN:NNbytes of payload. Each byte = one int8 signed delta in 16-count units.
Verified against all 3 May-11 fixture events:
| Event | First block | # T samples decoded | Matches truth |
|---|---|---|---|
| SP0 | 10 14 (10 bytes / 20 nibbles) |
22 (= 2 preamble + 20 deltas) | 22/22 ✓ |
| SS0 | 10 28 (20 bytes / 40 nibbles) |
42 | 42/42 ✓ |
| SV0 | 20 2c (44 int8 bytes) |
46 | 46/46 ✓ |
Implementation: :func:minimateplus.waveform_codec.decode_tran_initial.
What's still open
-
Tran past the first data block. After the first block, the body has more
10 NN/20 NNblocks separated by00 NNmarkers and occasionally30 NNblocks. Naive continuation (treat all subsequent10/20 NNblocks as Tran) does NOT match truth past the first block — the codec interleaves channels somehow.30 04markers appearing in SS0 between blocks 1 and 5 look like channel-switch tags, but the switching rule has not been fully decoded. -
Vert / Long / MicL channel encodings. No verified decoder exists for these yet. Hypotheses tested without success: V_init stored as int16 BE in
30 NNblock payload; V/L/M blocks encoded in order after Tran with30 NNseparators; V encoded asV - Tdifferential. None match truth. -
30 NNblock length. In the trailer,30 NNblocks are NN×4 bytes long. In the data section,30 NNblocks are NN×2 bytes long (= 8 bytes for NN=4 in SS0). The walker tries NN×2 first and falls back to NN×4 if needed. -
Walker correctness past offset ~427 in event-b. The walker bails out partway through event-b — there is at least one block whose length doesn't fit the lengths confirmed for the other events. This is a separate (now lower-priority) issue.
Recommended next step
A capture with a known external waveform (calibration tone of known
frequency and amplitude) would unlock the magnitude scaling and
disambiguate which channel a 20 NN block belongs to. Multiple
captures of the same signal at different geo_range settings
(Normal 10 in/s vs Sensitive 1.25 in/s) would also pin down whether
sample values are scaled at the codec layer or only at the BW
display layer.
Reference module
minimateplus/waveform_codec.py implements the verified block
walker (:func:walk_body, :func:split_segments,
:func:parse_segment_header). decode_waveform_v2 is a stub that
returns None until a verified per-byte sample decoder is wired
up; production code (minimateplus/client.py) continues to use
the legacy int16 LE decoder, which produces wrong samples but stable
output shape — keep the .h5 sidecars marked as
"sample-codec unverified" until the byte-to-sample mapping lands.
History (do not re-derive)
| Date | Note |
|---|---|
| 2026-05-11 | Tran channel codec cracked using a high-amplitude (PPV 6-7 in/s) event bundle. Preamble[3:7] = Tran[0]/Tran[1] as int16 BE in 16-count units (LSB = 0.005 in/s). First data block (10 NN nibble-deltas or 20 NN int8-deltas) carries Tran deltas from sample 2. Verified 22+42+46 = 110 samples across SP0/SS0/SV0 with 0 errors. Earlier 96-combination brute-force search on the quiet 5-8 bundle failed because Tran[0] = Tran[1] = 0 in those events made initial-value-from-preamble undetectable. |
| 2026-05-08 | Block tagging confirmed against the 4-event May 2026 bundle. All bodies parse cleanly through walk_body for events a/c/d. Event-b walks partway and stops at offset 427 (open issue). |
| 2026-05-08 | Earlier "4-channel interleaved s16 LE" claim formally retracted — never validated, produced full-scale ±32K noise on every event because the bytes are encoded, not raw samples. |
| 2026-04-02 | "Frame 7 metadata", "Frame 9 terminator", and 0x0400-step chunk-counter claims documented as-was; later proved to be artifacts of an over-reading 5A walk (now superseded by §7.8.5–7.8.7). |
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 43then 2 variable bytes, then00 [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 ≈ 9–11 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 2–4 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
1Eatdata[11:15] - Subsequent keys: returned by
1Fatdata[11:15] - Terminator:
00 00 00 00signals 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:
1Eresponse (E1):data[11:19]— 8 bytes (data[11:15]= key4)0Aprobe response (F5):data[4]= variable length (0x30 or 0x26)0Adata response (F5):data[11:11+length]— waveform header bytes0Cdata response (F3):data[11:11+0xD2]— 210-byte waveform record1Fresponse (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 0–8, ✅ 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[3–4]: 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:, andSeis Loc:come from the dedicated SUB 5A metadata pages at counter0x1002and0x1004— 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 at0x1000/0x1200contain 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=Truehack 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 00for 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:
-
offset_hiis sent raw, not DLE-stuffed. Whenoffset_hi = 0x10, the wire carries a bare0x10— NOT the stuffed10 10thatbuild_bw_framewould produce. The device ignores frames where this byte is incorrectly stuffed. -
DLE-aware checksum. Walking the full frame byte sequence: when a
10 XXpair is seen, onlyXXis added to the running sum; lone bytes are added normally. -
Partial DLE stuffing of
0x10bytes in the params region (CONFIRMED 2026-05-05). The device's de-stuffing rule for the params region is:10 10→ de-stuffs to1010 02 / 03 / 04→ kept literal (these are inner-frame markers)10 Xfor other X → de-stuffs to justX(drops the leading0x10)
Therefore any
0x10byte in the logical params that is followed by a byte NOT in{0x02, 0x03, 0x04, 0x10}MUST be doubled on the wire (10 X→10 10 X) so the device's de-stuffer reproduces the original10 Xpair. This applies most commonly to counters with0x10in the high byte (e.g. counter=0x1000produces logical params bytes... 10 00 ..., which BW encodes on the wire as... 10 10 00 ...). Without this stuffing the device interprets counter=0x1000as0x0000and 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.0x10bytes inoffset_hiare still written RAW per (1) above — only the params region has this stuffing requirement. Metadata-page params for counter0x1002/0x1004survive without stuffing because10 02/10 04fall 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 as00 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_offsetfrom the STRT record (not by chunk count or by a device-side timeout), and the TERM frame'soffset_word=0x005Amagic is incorrect — the real TERM offset_word is computed fromend_offsetand 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]=0and 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) | ~540–575 bytes wire (= 0x0200 payload + framing) |
| Data per chunk (deprecated 0x0400 step) | 1,036–1,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
20–520 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_offsetparams[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 pagecounter = 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[anchor−3]; read E5 sf1: data[anchor−4] — confirmed 2026-04-20 |
| Record Stop Mode | Fixed Record Time / Auto / Manual Stop | ❓ Hint: data[40] in E5 sf1 changed 01 7F → 00 00 alongside Continuous → Single Shot; may be related but unconfirmed independently |
| Sample Rate | Standard 1024 / Fast 2048 / Faster 4096 sps | ✅ sample_rate (anchor−2) |
| 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 (0–255) | ✅ 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 5–7 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 0–7, 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()inclient.pyorchestrates the full sequence.MiniMateProtocolexposesbegin_erase_all(),confirm_erase_all(), andread_event_storage_range()as separate methods.- The ACH server
--clear-after-downloadflag callsdelete_all_events()after a successful event download and resetsach_state.jsonstate 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 | 0–23 |
[102] |
time1_min |
uint8 | 0–59 |
[95] |
time2_enabled |
uint8 | 0x00 = off, 0x01 = on |
[105] |
time2_hour |
uint8 | 0–23 |
[106] |
time2_min |
uint8 | 0–59 |
[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; returnsdata_rsp.data[11:](125 bytes raw)MiniMateProtocol.write_call_home_config(data)— sends SUB 0x7E (127-byte payload) then SUB 0x7F confirmMiniMateClient.get_call_home_config()→CallHomeConfigdataclassMiniMateClient.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; raisesValueErrorif any hour/min == 3- REST API:
GET /device/call_homeandPOST /device/call_homeinsfm/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 0–8)
✅ 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 0x02bytes 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
12. Recommended Implementation Sequence
Build in this order — each step is independently testable:
- DLE frame parser — stateful byte-by-byte reader implementing the §10 state machine. Handles ACK, stuffing, de-stuffing, checksum validation.
connect(port, baud=38400)— open port, flush buffer, discard ASCII boot strings, send first POLL frame.identify()— SUB5Btwo-step read → returns{"manufacturer": "Instantel", "model": "MiniMate Plus"}.get_serial()— SUB15two-step read → returns serial number string.get_config()— SUB01two-step read → returns full config dict (firmware, channel scales, etc.).get_event_count()— SUB08two-step read → returns number of stored events.get_event_header(index)— SUB1Eread → returns timestamp dict.get_event_record(timestamp)— SUB0Cpaginated read → returns PPV dict per channel.download_waveform(timestamp)— SUB5Abulk stream → returns raw ADC arrays per channel.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_idleimplementation withidle_gap < 1.0 swill incorrectly declare the frame complete mid-stream. TcpTransport.read_until_idleoverrides the defaultidle_gap=0.05 stoidle_gap=1.5 sto 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 (≈15–30 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:
- Unit triggers (event or scheduled time)
- Unit powers up modem, dials / connects TCP to server IP:port
- Unit waits for "Wait for Connection" window for first BW frame from server
- Server sends POLL_PROBE → unit responds with POLL_RESPONSE (same as serial)
- Server reads serial number, full config, events as needed
- (Optional) Server erases device memory: SUB 0xA3 → 0x1C → 0x06 → 0xA2
- Server disconnects (or unit disconnects on Serial Idle Time expiry)
- 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(not0x10 0x03) - Checksum: the byte immediately before the bare
0x03is 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 trailing0x10would 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 anchor−2 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 0x56–0x57. Confirmed via two-unit comparison: BE18189 (calibrated 2023) = 07 E7; BE11529 (calibrated 2025) = 07 E9. Adjacent bytes at 0x53–0x55 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 Width — RESOLVED: 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 location — RESOLVED: 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 |
6E response to SUB 1C1C → 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 |
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 | 0–255 seconds |
| Power Saving Timeout | §3.13.1f | Event Index +53 | uint8 | minutes (user sets 1–60+) |
| 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.005–10.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 | 100–148 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) | 1–105 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 anchor−2, 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[anchor−3], uint8. Read (E5 sf1): data[anchor−4], uint8. Note: extra 0x10 byte at read data[anchor−3] 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? | 1–9 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 0x10in payload = literal0x10✅
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
0x02used 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
- Global byte counting ≠ frame counting.
0x10 0x02appears inside payloads. Only state machine transitions produce valid frame boundaries. - STX count ≠ frame count. Only STX→ETX pairs within proper state transitions count.
- EOF mid-frame is normal. Capture termination during active traffic produces an incomplete trailing frame. Not an error.
- 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 oftotal_seconds % 1296;A = value // 36,B = value % 36total_seconds = (event_local_time − 1985-01-01T00:00:00_local)in seconds0= always literal digit zeroW= Full Waveform,H= Full Histogram (ACH only)
Base-36 alphabet: 0–9 = 0–9, A–Z = 10–35.
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) |
| 2–3 | 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)
D.3.3 Footer (26 bytes)
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 |
| 1–8 | 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 0–999), C=generation 1 (1000–1999), 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:00device local time ✅ CONFIRMED — verified against 3,248 files from a 10-year production archive; zero errors (only 2 mismatches were MicromateIDFH/IDFWfiles 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 anchor−7 (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[anc−7] = recording_mode |
file[anc−8] = recording_mode |
file[anc−6:anc−4] = sample_rate |
file[anc−6:anc−4] = sample_rate |
For 1024 sps files, the expected file bytes around the anchor are:
file[anc−9]: mode_prefix (0x00 for Single Shot/Continuous; 0x10 for Histogram)
file[anc−8]: 0x00 (was recording_mode, but shifted away — now reads 0x00 for mode_prefix)
file[anc−7]: recording_mode (0x00=Single Shot, 0x01=Continuous, etc.)
file[anc−6]: 0x04 (sample_rate_HI for 1024 sps)
file[anc−5]: 0x00 (sample_rate_LO)
file[anc−4]: histogram_interval_HI
file[anc−3]: 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.