Files
seismo-relay/docs/instantel_protocol_reference.md

22 KiB
Raw Blame History

Instantel MiniMate Plus — Blastware RS-232 Protocol Reference

"The Rosetta Stone"

Reverse-engineered via RS-232 serial bridge sniffing between Blastware software and an Instantel MiniMate Plus seismograph (S/N: BE18189).
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 0x41 as STX and 0x02/0x03 as bare frame delimiters. The protocol uses proper DLE framing. See below.

Every message follows this structure:

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

Special Byte Definitions

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

DLE Byte Stuffing Rule

CONFIRMED — 2026-02-26

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

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

Frame Parser Notes

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

Checksum Verification Example

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

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

After de-stuffing (0x10 0x100x10):

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

3. Payload Structure

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

[CMD]  [DLE]  [ADDR]  [FLAGS]  [SUB_CMD]  [OFFSET_HI]  [OFFSET_LO]  [PARAMS × N]
 xx    0x10   0x10     0x00      xx          xx            xx
Field Position Notes Certainty
CMD byte 0 Command or response code CONFIRMED
DLE byte 1 Always 0x10. Part of address/routing scheme. On wire this is stuffed as 0x10 0x10. CONFIRMED — 2026-02-26
ADDR byte 2 Always 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 consecutive 0x10 bytes (0x10 0x10 0x10 0x10). This is normal — both are stuffed. Do not mistake them for DLE+STX or DLE+ETX.


4. Communication Pattern

4.1 ACK Handshake (Every Transaction)

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

4.2 Two-Step Paged Read Pattern

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

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

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

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

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

5. Command Reference Table

5.1 Request Commands (Blastware → S3)

SUB Byte Name Description Certainty
5B POLL / KEEPALIVE Sent continuously (~every 80ms). Requests device identity/status. CONFIRMED
15 SERIAL NUMBER REQUEST Requests device serial number. CONFIRMED
01 FULL CONFIG READ Requests complete device configuration block (~0x98 bytes). Firmware, model, serial, channel config, scaling factors. CONFIRMED
08 EVENT INDEX READ Requests the event record index (0x58 bytes). Event count and record pointers. CONFIRMED
06 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 35× 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 1215 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. 01 consistently observed. Full mapping unknown. 🔶 INFERRED
  • 9× signed 16-bit big-endian ADC samples. Noise floor ≈ 0x000A0x000B
  • 00 00 — separator / padding
  • 01 — unknown flag byte
  • 3-byte partial IEEE 754 float — peak value for this sample window. 0x43 prefix = range 130260

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 00 byte 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 0x02 bytes during the connection phase. Wait for the first valid framed poll response before proceeding.


10. DLE Byte Stuffing

CONFIRMED — 2026-02-26 (previously SPECULATIVE)

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

Parser State Machine

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

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

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

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

11. Checksum Reference Implementation

⚠️ Updated 2026-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

Build in this order — each step is independently testable:

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

13. Device Under Test

Field Value
Manufacturer Instantel
Model MiniMate Plus
Serial Number BE18189
Firmware 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.