22 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).
All findings derived from live packet capture. No vendor documentation was used.
Certainty Ratings: ✅ CONFIRMED | 🔶 INFERRED | ❓ SPECULATIVE
Changelog
| Date | Section | Change |
|---|---|---|
| 2026-02-26 | Initial | Document created from first hex dump analysis |
| 2026-02-26 | §2 Frame Structure | CORRECTED: Frame uses DLE-STX (0x10 0x02) and DLE-ETX (0x10 0x03), not bare 0x02/0x03. 0x41 confirmed as ACK not STX. DLE stuffing rule added. |
| 2026-02-26 | §8 Timestamp | UPDATED: Year 0x07CB = 1995 confirmed as MiniMate hardware default date when RTC battery is disconnected. Not an encoding error. Confidence upgraded from ❓ to 🔶. |
| 2026-02-26 | §10 DLE Stuffing | UPGRADED: Section upgraded from ❓ SPECULATIVE to ✅ CONFIRMED. Full stuffing rules and parser state machine documented. |
| 2026-02-26 | §11 Checksum | UPDATED: Frame builder and parser rewritten to handle DLE framing and byte stuffing correctly. |
| 2026-02-26 | §14 Open Questions | DLE question removed (resolved). Timestamp year question removed (resolved). |
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 0x10. Device address or bus ID. Also stuffed on wire. |
🔶 INFERRED |
| 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: 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 |
CHANNEL CONFIG READ | Requests channel configuration block (0x24 bytes). | ✅ CONFIRMED |
1C |
TRIGGER CONFIG READ | Requests trigger settings block (0x2C bytes). | ✅ CONFIRMED |
1E |
EVENT HEADER READ | Reads event header by index. Contains timestamp and sample rate. | ✅ CONFIRMED |
0A |
WAVEFORM HEADER READ | Reads waveform header keyed by timestamp (0x30 bytes/page). | ✅ CONFIRMED |
0C |
FULL WAVEFORM RECORD | Downloads complete waveform record (0xD2 bytes/page, 2 pages). Project strings, PPV floats, channel labels. | ✅ CONFIRMED |
5A |
BULK WAVEFORM STREAM | Initiates bulk download of raw ADC sample data, keyed by timestamp. Large multi-page transfer. | ✅ CONFIRMED |
24 |
WAVEFORM PAGE A? | Paged waveform read, possibly channel group A. | 🔶 INFERRED |
25 |
WAVEFORM PAGE B? | Paged waveform read, possibly channel group B. | 🔶 INFERRED |
1F |
EVENT ADVANCE / CLOSE | Sent after waveform download completes. Likely advances internal record pointer. | 🔶 INFERRED |
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.
| Request SUB | Response SUB | Certainty |
|---|---|---|
5B |
A4 |
✅ CONFIRMED |
15 |
EA |
✅ CONFIRMED |
01 |
FE |
✅ CONFIRMED |
08 |
F7 |
✅ CONFIRMED |
06 |
F9 |
✅ CONFIRMED |
1C |
E3 |
✅ CONFIRMED |
1E |
E1 |
✅ CONFIRMED |
0A |
F5 |
✅ CONFIRMED |
0C |
F3 |
✅ CONFIRMED |
5A |
A5 |
✅ CONFIRMED |
1F |
E0 |
🔶 INFERRED |
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. [Poll loop repeats 3–5× during initialization]
8. BW → SUB 06 → channel config read
9. BW → SUB 15 → serial number
10. BW → SUB 01 → full config block
11. BW → SUB 08 → event index
12. BW → SUB 1E → first event header
13. BW → SUB 0A → waveform header (timestamp-keyed)
14. BW → SUB 0C → full waveform record download (2 pages)
15. BW → SUB 1F → advance / close event
16. [Repeat steps 12–15 for each stored event]
17. BW → SUB 5A → bulk raw waveform stream
18. Poll loop resumes (SUB 5B keepalive every ~80ms)
7. Known Data Payloads
7.1 Poll Response (SUB A4) — Device Identity Block
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
79 11 20 — 3 trailing bytes (HW revision? calibration ID?) ❓
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 38 2E 31 37 00 |
"S338.17\x00" — Firmware version |
✅ CONFIRMED |
| 0x3C | 31 30 2E 37 32 00 |
"10.72\x00" — DSP / secondary firmware version |
✅ CONFIRMED |
| 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
Offset 0x00: 00 58 09 — Total index size or record count ❓
Offset 0x03: 00 00 00 01 — Possibly stored event count = 1 ❓
Offset 0x07: 01 07 CB 00 06 1E — Timestamp of event 1 (see §8)
Offset 0x0D: 01 07 CB 00 14 00 — Timestamp of event 2 (see §8)
Offset 0x13: 00 00 00 17 3B — Unknown ❓
Offset 0x50: 10 02 FF DC — Sub-block pointer or data segment header ❓
7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages
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
Peak values as IEEE 754 big-endian floats — event 1:
Tran: 3D BB 45 7A = 0.0916 (in/s — unit config dependent)
Vert: 3D B9 56 E1 = 0.0907
Long: 3D 75 C2 7C = 0.0605
MicL: 39 BE 18 B8 = 0.000145 (PSI or dB linear — ❓ units unconfirmed)
Peak values — event 2:
Tran: 3D 56 CB B9 = 0.0521
Vert: 3C F5 C2 7C = 0.0300
Long: 3C F5 C2 7C = 0.0300
MicL: 39 64 1D AA = 0.0000875
7.6 Bulk Waveform Stream (SUB A5) — Raw ADC Sample Records
Each repeating record (🔶 INFERRED structure):
[CH_ID] [S0_HI] [S0_LO] [S1_HI] [S1_LO] ... [S8_HI] [S8_LO] [00 00] [01] [PEAK × 3 bytes]
01 00 0A 00 0B 43 xx xx
CH_ID— Channel identifier.01consistently observed. Full mapping unknown. 🔶 INFERRED- 9× signed 16-bit big-endian ADC samples. Noise floor ≈
0x000A–0x000B 00 00— separator / padding01— unknown flag byte- 3-byte partial IEEE 754 float — peak value for this sample window.
0x43prefix = range 130–260
❓ SPECULATIVE: At 1024 sps, 9 samples ≈ 8.8ms per record. Sample rate unconfirmed from captured data alone.
8. Timestamp Format
🔶 Updated 2026-02-26 — Year field resolved. Confidence upgraded.
Timestamps are 6-byte sequences appearing in event headers and waveform keys.
Observed example:
01 07 CB 00 06 1E
Decoded:
| 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 — possibly hours, minutes, or padding | ❓ SPECULATIVE |
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.
❓ Still unknown: The
00byte at offset 3. Likely encodes time-of-day (hours or minutes). Needs a capture with a precisely known event time to decode.
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-02-26 — Rewritten for correct DLE framing and byte stuffing.
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(payload: bytes) -> int:
"""
8-bit sum of de-stuffed payload bytes, modulo 256.
Pass the original (pre-stuff) payload — not the wire bytes.
"""
return sum(payload) & 0xFF
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 | S338.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. Open Questions / Still Needs Cracking
| Question | Priority | Added |
|---|---|---|
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 |
Meaning of 79 11 20 trailing bytes in serial number response |
MEDIUM | 2026-02-26 |
Full channel ID mapping in SUB 5A stream (01/02/03/04 → which sensor?) |
MEDIUM | 2026-02-26 |
| Write / set commands for device configuration | MEDIUM | 2026-02-26 |
Full trigger configuration field mapping (SUB 1C response) |
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 |
LOW | 2026-02-26 |
| MicL channel units — PSI, dB linear, or dB(L)? | LOW | 2026-02-26 |
All findings reverse-engineered from live RS-232 bridge captures. No Instantel proprietary documentation was referenced or used.
This is a living document — append changelog entries and timestamps as new findings are confirmed or corrected.