add: log also saved in binary. updated the reference material.
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
s3_bridge.py — S3 <-> Blastware serial bridge with frame-aware session logging
|
s3_bridge.py — S3 <-> Blastware serial bridge with frame-aware session logging
|
||||||
Version: v0.4.0
|
Version: v0.3.0
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
- Low CPU: avoids per-byte console printing
|
- Low CPU: avoids per-byte console printing
|
||||||
- Forwards bytes immediately (true bridge)
|
- Forwards bytes immediately (true bridge)
|
||||||
- Frame-aware logging: buffers per direction until ETX (0x03), then logs full frame on one line
|
- Frame-aware logging: buffers per direction until ETX (0x03), then logs full frame on one line
|
||||||
- Also logs plain ASCII bursts (e.g., "Operating System") cleanly
|
- Also logs plain ASCII bursts (e.g., "Operating System") cleanly
|
||||||
- Session log file created on start, closed on Ctrl+C
|
- Dual log output: hex text log (.log) AND raw binary log (.bin) written simultaneously
|
||||||
|
- Session log files created on start, closed on Ctrl+C
|
||||||
|
|
||||||
Usage examples:
|
Usage examples:
|
||||||
python s3_bridge.py
|
python s3_bridge.py
|
||||||
@@ -30,7 +31,7 @@ from typing import Optional
|
|||||||
import serial
|
import serial
|
||||||
|
|
||||||
|
|
||||||
VERSION = "v0.4.0"
|
VERSION = "v0.3.0"
|
||||||
|
|
||||||
|
|
||||||
def now_ts() -> str:
|
def now_ts() -> str:
|
||||||
@@ -57,21 +58,30 @@ def looks_like_text(b: bytes) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
class SessionLogger:
|
class SessionLogger:
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str, bin_path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
|
self.bin_path = bin_path
|
||||||
self._fh = open(path, "a", buffering=1, encoding="utf-8", errors="replace")
|
self._fh = open(path, "a", buffering=1, encoding="utf-8", errors="replace")
|
||||||
|
self._bin_fh = open(bin_path, "ab", buffering=0)
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
def log_line(self, line: str) -> None:
|
def log_line(self, line: str) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._fh.write(line + "\n")
|
self._fh.write(line + "\n")
|
||||||
|
|
||||||
|
def log_raw(self, data: bytes) -> None:
|
||||||
|
"""Write raw bytes directly to the binary log."""
|
||||||
|
with self._lock:
|
||||||
|
self._bin_fh.write(data)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
try:
|
try:
|
||||||
self._fh.flush()
|
self._fh.flush()
|
||||||
|
self._bin_fh.flush()
|
||||||
finally:
|
finally:
|
||||||
self._fh.close()
|
self._fh.close()
|
||||||
|
self._bin_fh.close()
|
||||||
|
|
||||||
|
|
||||||
class FrameAssembler:
|
class FrameAssembler:
|
||||||
@@ -168,6 +178,7 @@ def forward_loop(
|
|||||||
for frame in frames:
|
for frame in frames:
|
||||||
# Some devices send leading STX separately; we still log as-is.
|
# Some devices send leading STX separately; we still log as-is.
|
||||||
logger.log_line(f"[{now_ts()}] [{name}] {bytes_to_hex(frame)}")
|
logger.log_line(f"[{now_ts()}] [{name}] {bytes_to_hex(frame)}")
|
||||||
|
logger.log_raw(frame)
|
||||||
|
|
||||||
# If we have non-ETX data that looks like text, flush it as TEXT
|
# If we have non-ETX data that looks like text, flush it as TEXT
|
||||||
text = assembler.drain_as_text_if_any()
|
text = assembler.drain_as_text_if_any()
|
||||||
@@ -177,6 +188,7 @@ def forward_loop(
|
|||||||
except Exception:
|
except Exception:
|
||||||
s = repr(text)
|
s = repr(text)
|
||||||
logger.log_line(f"[{now_ts()}] [{name}] [TEXT] {s}")
|
logger.log_line(f"[{now_ts()}] [{name}] [TEXT] {s}")
|
||||||
|
logger.log_raw(text)
|
||||||
|
|
||||||
# minimal console heartbeat (cheap)
|
# minimal console heartbeat (cheap)
|
||||||
if not quiet and status_every_s > 0:
|
if not quiet and status_every_s > 0:
|
||||||
@@ -211,10 +223,12 @@ def main() -> int:
|
|||||||
print(f"Connected: {args.bw} <-> {args.s3}")
|
print(f"Connected: {args.bw} <-> {args.s3}")
|
||||||
|
|
||||||
os.makedirs(args.logdir, exist_ok=True)
|
os.makedirs(args.logdir, exist_ok=True)
|
||||||
log_name = _dt.datetime.now().strftime("s3_session_%Y%m%d_%H%M%S.log")
|
ts = _dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
log_path = os.path.join(args.logdir, log_name)
|
log_path = os.path.join(args.logdir, f"s3_session_{ts}.log")
|
||||||
logger = SessionLogger(log_path)
|
bin_path = os.path.join(args.logdir, f"s3_session_{ts}.bin")
|
||||||
print(f"[LOG] Writing session log to {log_path}")
|
logger = SessionLogger(log_path, bin_path)
|
||||||
|
print(f"[LOG] Writing hex log to {log_path}")
|
||||||
|
print(f"[LOG] Writing binary log to {bin_path}")
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] s3_bridge {VERSION} start")
|
logger.log_line(f"[{now_ts()}] [INFO] s3_bridge {VERSION} start")
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] BW={args.bw} S3={args.s3} baud={args.baud}")
|
logger.log_line(f"[{now_ts()}] [INFO] BW={args.bw} S3={args.s3} baud={args.baud}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Instantel MiniMate Plus — Blastware RS-232 Protocol Reference v0.1.0
|
# Instantel MiniMate Plus — Blastware RS-232 Protocol Reference
|
||||||
### "The Rosetta Stone"
|
### "The Rosetta Stone"
|
||||||
> Reverse-engineered via RS-232 serial bridge sniffing between Blastware software and an Instantel MiniMate Plus seismograph (S/N: BE18189).
|
> 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.
|
> All findings derived from live packet capture. No vendor documentation was used.
|
||||||
@@ -6,6 +6,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## 1. Physical Layer
|
||||||
|
|
||||||
| Parameter | Value | Certainty |
|
| Parameter | Value | Certainty |
|
||||||
@@ -20,60 +33,83 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 2. Frame Structure
|
## 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:
|
Every message follows this structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
[ACK] [STX] [PAYLOAD...] [CHECKSUM] [ETX]
|
[ACK] [DLE+STX] [PAYLOAD...] [CHECKSUM] [DLE+ETX]
|
||||||
0x41 0x02 N bytes 1 byte 0x03
|
0x41 0x10 0x02 N bytes 1 byte 0x10 0x03
|
||||||
```
|
```
|
||||||
|
|
||||||
### Byte Definitions
|
### Special Byte Definitions
|
||||||
|
|
||||||
| Byte | Value | Meaning | Certainty |
|
| Token | Raw Bytes | Meaning | Certainty |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ACK | `0x41` (ASCII `'A'`) | Acknowledgment / ready token. Always sent as a standalone byte before a frame. Both sides send it. | ✅ CONFIRMED |
|
| ACK | `0x41` (ASCII `'A'`) | Acknowledgment / ready token. Standalone single byte. Sent before every frame by both sides. | ✅ CONFIRMED |
|
||||||
| STX | `0x02` | Start of frame | ✅ CONFIRMED |
|
| DLE | `0x10` | Data Link Escape. Prefixes the next byte to give it special meaning. | ✅ CONFIRMED — 2026-02-26 |
|
||||||
| ETX | `0x03` | End of frame | ✅ CONFIRMED |
|
| STX | `0x10 0x02` | DLE+STX = Start of frame (two-byte sequence) | ✅ CONFIRMED — 2026-02-26 |
|
||||||
| CHECKSUM | 1 byte | 8-bit sum of all payload bytes between STX and CHECKSUM, modulo 256 | ✅ CONFIRMED (verified on multiple frames) |
|
| 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 |
|
||||||
|
|
||||||
### Important Notes
|
### DLE Byte Stuffing Rule
|
||||||
|
> ✅ CONFIRMED — 2026-02-26
|
||||||
|
|
||||||
- The `0x41` ACK byte **always arrives in a separate read() call** before the frame due to RS-232 inter-byte timing at 38400 baud.
|
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.
|
||||||
- Your frame parser **must buffer until ETX `0x03`** is received — do not treat each read() as a complete frame.
|
|
||||||
- The `0x41` and `0x02` are **not** part of the checksum calculation. Only bytes after STX and before CHECKSUM are summed.
|
- **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
|
### Checksum Verification Example
|
||||||
|
|
||||||
|
Raw frame on wire (with ACK and DLE framing):
|
||||||
```
|
```
|
||||||
Frame: 02 10 10 00 5B 00 00 00 00 00 00 00 00 00 00 00 00 00 6B 03
|
41 10 02 | 10 10 00 5B 00 00 00 00 00 00 00 00 00 00 00 00 00 | 6B | 10 03
|
||||||
Payload: 10+10+00+5B+00+00+00+00+00+00+00+00+00+00+00+00+00 = 0x6B ✅
|
^ACK^^STX^ ^---------- stuffed payload (0x10→0x10 0x10) ------^ ^chk^ ^ETX^
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After de-stuffing (`0x10 0x10` → `0x10`):
|
||||||
```
|
```
|
||||||
Frame: 10 02 00 10 10 A4 00 00 00 00 00 00 30 00 00 00 00 00 00 E4 03
|
De-stuffed: 10 00 5B 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||||
Payload: 10+02+00+10+10+A4+00+00+00+00+00+00+30+00+00+00+00+00+00 = 0xE4 ✅
|
Checksum: 10+00+5B+00+... = 0x6B ✅
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Payload Structure
|
## 3. Payload Structure
|
||||||
|
|
||||||
The payload (bytes between STX `0x02` and CHECKSUM) has consistent internal structure:
|
The payload (bytes between DLE+STX and CHECKSUM, after de-stuffing) has consistent internal structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
[CMD] [??] [ADDR] [FLAGS] [SUB_CMD] [OFFSET_HI] [OFFSET_LO] [PARAMS × N bytes]
|
[CMD] [DLE] [ADDR] [FLAGS] [SUB_CMD] [OFFSET_HI] [OFFSET_LO] [PARAMS × N]
|
||||||
|
xx 0x10 0x10 0x00 xx xx xx
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Position | Notes | Certainty |
|
| Field | Position | Notes | Certainty |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CMD | byte 0 | Command or response code | ✅ CONFIRMED |
|
| CMD | byte 0 | Command or response code | ✅ CONFIRMED |
|
||||||
| ?? | byte 1 | Always `0x10` in most frames. Possibly device class or protocol version | 🔶 INFERRED |
|
| 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`. Possibly device address on bus | 🔶 INFERRED |
|
| 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 |
|
| FLAGS | byte 3 | Usually `0x00`. Non-zero values seen in event-keyed requests. | 🔶 INFERRED |
|
||||||
| SUB_CMD | byte 4 | The actual operation being requested | ✅ CONFIRMED |
|
| SUB_CMD | byte 4 | The actual operation being requested. | ✅ CONFIRMED |
|
||||||
| OFFSET_HI | byte 5 | High byte of data offset for paged reads | ✅ CONFIRMED |
|
| OFFSET_HI | byte 5 | High byte of data offset for paged reads. | ✅ CONFIRMED |
|
||||||
| OFFSET_LO | byte 6 | Low byte of data offset | ✅ 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,77 +118,75 @@ The payload (bytes between STX `0x02` and CHECKSUM) has consistent internal stru
|
|||||||
### 4.1 ACK Handshake (Every Transaction)
|
### 4.1 ACK Handshake (Every Transaction)
|
||||||
|
|
||||||
```
|
```
|
||||||
Side A → 0x41 (ACK: "I'm ready / got your last frame")
|
Side A → 0x41 (ACK: "ready / received")
|
||||||
Side A → 02 [payload] [chk] 03 (actual frame)
|
Side A → 10 02 [payload] [chk] 10 03 (frame)
|
||||||
Side B → 0x41 (ACK)
|
Side B → 0x41 (ACK)
|
||||||
Side B → 02 [payload] [chk] 03 (response frame)
|
Side B → 10 02 [payload] [chk] 10 03 (response frame)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 Two-Step Paged Read Pattern
|
### 4.2 Two-Step Paged Read Pattern
|
||||||
|
|
||||||
All data reads use a two-step length-then-data pattern:
|
All data reads use a two-step length-prefixed pattern. It is not optional.
|
||||||
|
|
||||||
```
|
```
|
||||||
Step 1 — Ask how much data exists:
|
Step 1 — Request with offset=0 ("how much data is there?"):
|
||||||
BW → 0x41
|
BW → 0x41
|
||||||
BW → 02 [CMD] 10 10 00 [SUB] 00 00 [00 00 ...] [chk] 03 (offset = 0)
|
BW → 10 02 [CMD] 10 10 00 [SUB] 00 00 [00 00 ...] [chk] 10 03
|
||||||
|
|
||||||
Step 2 — Device replies with total length:
|
Step 2 — Device replies with total data length:
|
||||||
S3 → 0x41
|
S3 → 0x41
|
||||||
S3 → 02 [RSP] 00 10 10 [SUB] 00 00 00 00 00 00 [LEN_HI] [LEN_LO] ... [chk] 03
|
S3 → 10 02 [RSP] 00 10 10 [SUB] 00 00 00 00 00 00 [LEN_HI] [LEN_LO] [chk] 10 03
|
||||||
|
|
||||||
Step 3 — Request data at that length as offset:
|
Step 3 — Re-request using LEN as offset ("now send the data"):
|
||||||
BW → 0x41
|
BW → 0x41
|
||||||
BW → 02 [CMD] 10 10 00 [SUB] 00 00 [LEN_HI] [LEN_LO] [00 ...] [chk] 03
|
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:
|
Step 4 — Device sends actual data payload:
|
||||||
S3 → 0x41
|
S3 → 0x41
|
||||||
S3 → 02 [RSP] 00 10 10 [SUB] 00 00 [LEN_HI] [LEN_LO] ... [DATA...] [chk] 03
|
S3 → 10 02 [RSP] 00 10 10 [SUB] 00 00 [LEN_HI] [LEN_LO] [DATA...] [chk] 10 03
|
||||||
```
|
```
|
||||||
|
|
||||||
This two-step is used for **every** data command. It is not optional.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Command Reference Table
|
## 5. Command Reference Table
|
||||||
|
|
||||||
### 5.1 Request Commands (Blastware → S3)
|
### 5.1 Request Commands (Blastware → S3)
|
||||||
|
|
||||||
| CMD Byte | SUB Byte | Name | Description | Certainty |
|
| SUB Byte | Name | Description | Certainty |
|
||||||
|---|---|---|---|---|
|
|
||||||
| `02` | `5B` | **POLL / KEEPALIVE** | Sent continuously. Requests device identity/status. | ✅ CONFIRMED |
|
|
||||||
| `02` | `15` | **SERIAL NUMBER REQUEST** | Requests device serial number. | ✅ CONFIRMED |
|
|
||||||
| `02` | `01` | **FULL CONFIG READ** | Requests complete device configuration block (~0x98 bytes). Contains firmware version, model, serial, channel config, scaling. | ✅ CONFIRMED |
|
|
||||||
| `02` | `08` | **EVENT INDEX READ** | Requests the event record index (0x58 bytes). Contains event count and record pointers. | ✅ CONFIRMED |
|
|
||||||
| `02` | `06` | **CHANNEL CONFIG READ** | Requests channel configuration block (0x24 bytes). | ✅ CONFIRMED |
|
|
||||||
| `02` | `1C` | **TRIGGER CONFIG READ** | Requests trigger settings block (0x2C bytes). | ✅ CONFIRMED |
|
|
||||||
| `02` | `1E` | **EVENT HEADER READ** | Reads event header by index. Contains timestamp and sample rate info. | ✅ CONFIRMED |
|
|
||||||
| `02` | `0A` | **WAVEFORM HEADER READ** | Reads waveform header keyed by timestamp (0x30 bytes/page). | ✅ CONFIRMED |
|
|
||||||
| `02` | `0C` | **FULL WAVEFORM RECORD** | Downloads complete waveform record (0xD2 bytes/page). Contains project strings, PPV floats, channel labels. | ✅ CONFIRMED |
|
|
||||||
| `02` | `5A` | **BULK WAVEFORM STREAM** | Initiates bulk download of raw ADC sample data. Keyed by timestamp. Large multi-page transfer. | ✅ CONFIRMED |
|
|
||||||
| `02` | `24` | **WAVEFORM PAGE A?** | Paged waveform read, channel group A. | 🔶 INFERRED |
|
|
||||||
| `02` | `25` | **WAVEFORM PAGE B?** | Paged waveform read, channel group B. | 🔶 INFERRED |
|
|
||||||
| `02` | `1F` | **EVENT ADVANCE / ACK** | Sent after waveform download completes. Possibly advances record pointer or closes event. | 🔶 INFERRED |
|
|
||||||
|
|
||||||
### 5.2 Response Commands (S3 → Blastware)
|
|
||||||
|
|
||||||
Responses use the **bitwise complement** of the request CMD byte. Pattern: `CMD + RSP = 0xFF` (approximately). More precisely, response bytes observed:
|
|
||||||
|
|
||||||
| Request CMD | Response CMD | Notes | Certainty |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `02` + `5B` | `10 02` + `A4` | Poll response | ✅ CONFIRMED |
|
| `5B` | **POLL / KEEPALIVE** | Sent continuously (~every 80ms). Requests device identity/status. | ✅ CONFIRMED |
|
||||||
| `02` + `15` | `10 02` + `EA` | Serial number response | ✅ CONFIRMED |
|
| `15` | **SERIAL NUMBER REQUEST** | Requests device serial number. | ✅ CONFIRMED |
|
||||||
| `02` + `01` | `10 02` + `FE` | Full config response | ✅ CONFIRMED |
|
| `01` | **FULL CONFIG READ** | Requests complete device configuration block (~0x98 bytes). Firmware, model, serial, channel config, scaling factors. | ✅ CONFIRMED |
|
||||||
| `02` + `08` | `10 02` + `F7` | Event index response | ✅ CONFIRMED |
|
| `08` | **EVENT INDEX READ** | Requests the event record index (0x58 bytes). Event count and record pointers. | ✅ CONFIRMED |
|
||||||
| `02` + `06` | `10 02` + `F9` | Channel config response | ✅ CONFIRMED |
|
| `06` | **CHANNEL CONFIG READ** | Requests channel configuration block (0x24 bytes). | ✅ CONFIRMED |
|
||||||
| `02` + `1C` | `10 02` + `E3` | Trigger config response | ✅ CONFIRMED |
|
| `1C` | **TRIGGER CONFIG READ** | Requests trigger settings block (0x2C bytes). | ✅ CONFIRMED |
|
||||||
| `02` + `1E` | `10 02` + `E1` | Event header response | ✅ CONFIRMED |
|
| `1E` | **EVENT HEADER READ** | Reads event header by index. Contains timestamp and sample rate. | ✅ CONFIRMED |
|
||||||
| `02` + `0A` | `10 02` + `F5` | Waveform header response | ✅ CONFIRMED |
|
| `0A` | **WAVEFORM HEADER READ** | Reads waveform header keyed by timestamp (0x30 bytes/page). | ✅ CONFIRMED |
|
||||||
| `02` + `0C` | `10 02` + `F3` | Full waveform record response | ✅ CONFIRMED |
|
| `0C` | **FULL WAVEFORM RECORD** | Downloads complete waveform record (0xD2 bytes/page, 2 pages). Project strings, PPV floats, channel labels. | ✅ CONFIRMED |
|
||||||
| `02` + `5A` | `10 02` + `A5` | Bulk waveform stream response | ✅ CONFIRMED |
|
| `5A` | **BULK WAVEFORM STREAM** | Initiates bulk download of raw ADC sample data, keyed by timestamp. Large multi-page transfer. | ✅ CONFIRMED |
|
||||||
| `02` + `1F` | `10 02` + `E0` | Event advance response | 🔶 INFERRED |
|
| `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 |
|
||||||
|
|
||||||
> 🔶 **INFERRED pattern:** Response SUB byte appears to be `(0xFF - request SUB)`. E.g., `0xFF - 0x5B = 0xA4`. This holds for all observed pairs and can be used to predict unknown response codes.
|
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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,215 +194,298 @@ Responses use the **bitwise complement** of the request CMD byte. Pattern: `CMD
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. Device powers on / resets
|
1. Device powers on / resets
|
||||||
2. S3 → "Operating System" (raw ASCII string, no frame, no ACK)
|
2. S3 → "Operating System" (raw ASCII, no DLE framing — UART boot string)
|
||||||
3. BW → 0x41 + POLL (CMD 5B)
|
3. BW → 0x41 + POLL frame (SUB 5B)
|
||||||
4. S3 → 0x41 + POLL RESPONSE (CMD A4, length=0x30)
|
4. S3 → 0x41 + POLL RESPONSE (SUB A4, reports data length = 0x30)
|
||||||
5. BW → 0x41 + POLL (CMD 5B, offset=0x30)
|
5. BW → 0x41 + POLL frame (SUB 5B, offset = 0x30)
|
||||||
6. S3 → 0x41 + POLL RESPONSE with data: "Instantel" + "MiniMate Plus"
|
6. S3 → 0x41 + POLL RESPONSE with data: "Instantel" + "MiniMate Plus"
|
||||||
7. [Poll loop repeats 3-5× during init]
|
7. [Poll loop repeats 3–5× during initialization]
|
||||||
8. BW issues CMD 06 → channel config
|
8. BW → SUB 06 → channel config read
|
||||||
9. BW issues CMD 15 → serial number
|
9. BW → SUB 15 → serial number
|
||||||
10. BW issues CMD 01 → full config block
|
10. BW → SUB 01 → full config block
|
||||||
11. BW issues CMD 08 → event index
|
11. BW → SUB 08 → event index
|
||||||
12. BW issues CMD 1E → first event header
|
12. BW → SUB 1E → first event header
|
||||||
13. BW issues CMD 0A → waveform header (timestamp-keyed)
|
13. BW → SUB 0A → waveform header (timestamp-keyed)
|
||||||
14. BW issues CMD 0C → full waveform record download
|
14. BW → SUB 0C → full waveform record download (2 pages)
|
||||||
15. BW issues CMD 1F → advance to next event
|
15. BW → SUB 1F → advance / close event
|
||||||
16. [Repeat 12-15 for each stored event]
|
16. [Repeat steps 12–15 for each stored event]
|
||||||
17. BW issues CMD 5A → bulk raw waveform stream
|
17. BW → SUB 5A → bulk raw waveform stream
|
||||||
18. Poll loop resumes (CMD 5B keepalive every ~80ms)
|
18. Poll loop resumes (SUB 5B keepalive every ~80ms)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Known Data Payloads
|
## 7. Known Data Payloads
|
||||||
|
|
||||||
### 7.1 Poll Response (CMD A4) — Device Identity Block
|
### 7.1 Poll Response (SUB A4) — Device Identity Block
|
||||||
|
|
||||||
Triggered by two-step read with SUB `5B`.
|
Two-step read. Data payload = 0x30 bytes.
|
||||||
Data payload (0x30 bytes) contains:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Offset 0x00: 0x08 — string length prefix
|
Offset 0x00: 0x08 — string length prefix
|
||||||
Offset 0x01: "Instantel" — manufacturer name (null-padded to 20 bytes)
|
Offset 0x01: "Instantel" — manufacturer (null-padded to ~20 bytes)
|
||||||
Offset 0x15: "MiniMate Plus" — model name (null-padded to 20 bytes)
|
Offset 0x15: "MiniMate Plus" — model name (null-padded to ~20 bytes)
|
||||||
```
|
```
|
||||||
|
|
||||||
Raw example:
|
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
|
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
|
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 (CMD EA)
|
### 7.2 Serial Number Response (SUB EA)
|
||||||
|
|
||||||
Data payload (0x0A bytes):
|
|
||||||
|
|
||||||
|
Data payload = 0x0A bytes:
|
||||||
```
|
```
|
||||||
"BE18189" (7 ASCII bytes, null-terminated)
|
"BE18189\x00" — 7 ASCII bytes + null terminator
|
||||||
79 11 20 (3 trailing bytes — purpose unknown) ❓ SPECULATIVE: possibly manufacture date or calibration ID
|
79 11 20 — 3 trailing bytes (HW revision? calibration ID?) ❓
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.3 Full Config Response (CMD FE) — 0x98 bytes
|
### 7.3 Full Config Response (SUB FE) — 0x98 bytes
|
||||||
|
|
||||||
This is the richest single payload. Confirmed fields:
|
| 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: "BE18189\x00" — Serial number
|
Offset 0x00: 00 58 09 — Total index size or record count ❓
|
||||||
Offset 0x08: 79 11 — Unknown (hardware revision?) ❓
|
Offset 0x03: 00 00 00 01 — Possibly stored event count = 1 ❓
|
||||||
Offset 0x0A: 00 01 — Unknown flags
|
Offset 0x07: 01 07 CB 00 06 1E — Timestamp of event 1 (see §8)
|
||||||
Offset 0x0C: 10 02 9C 00 — Sub-block pointer or version tag
|
Offset 0x0D: 01 07 CB 00 14 00 — Timestamp of event 2 (see §8)
|
||||||
Offset 0x14: 3F 80 00 00 — IEEE 754 float = 1.0 (channel scale factor, Tran)
|
|
||||||
Offset 0x18: 41 00 00 00 — IEEE 754 float = 8.0 (channel scale factor, unknown)
|
|
||||||
Offset 0x1C: 3F 80 00 00 ×6 — IEEE 754 float = 1.0 each (remaining channel scales)
|
|
||||||
Offset 0x34: "S338.17\x00" — Firmware version string
|
|
||||||
Offset 0x3C: "10.72\x00" — Secondary firmware/DSP version
|
|
||||||
Offset 0x42: 07 E7 — 0x07E7 = 2023 decimal? Year field ❓
|
|
||||||
Offset 0x44: "Instantel\x00" — Manufacturer (repeated)
|
|
||||||
Offset 0x60: "Instantel\x00" — Manufacturer (second copy)
|
|
||||||
Offset 0x6D: "MiniMate Plus" — Model name
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 Event Index Response (CMD F7) — 0x58 bytes
|
|
||||||
|
|
||||||
```
|
|
||||||
Offset 0x00: 00 58 09 — Total records or index size ❓
|
|
||||||
Offset 0x03: 00 00 00 01 — Possibly record count = 1 ❓
|
|
||||||
Offset 0x07: 01 07 CB 00 06 1E — Timestamp 1 (see Section 8)
|
|
||||||
Offset 0x0D: 01 07 CB 00 14 00 — Timestamp 2
|
|
||||||
Offset 0x13: 00 00 00 17 3B — Unknown ❓
|
Offset 0x13: 00 00 00 17 3B — Unknown ❓
|
||||||
...
|
Offset 0x50: 10 02 FF DC — Sub-block pointer or data segment header ❓
|
||||||
Offset 0x50: 10 02 FF DC — Sub-block pointer ❓
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.5 Full Waveform Record (CMD F3) — 0xD2 bytes × 2 pages
|
### 7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages
|
||||||
|
|
||||||
Contains all metadata for a stored blast event. Confirmed ASCII strings:
|
Confirmed ASCII strings extracted from payload:
|
||||||
|
|
||||||
```
|
```
|
||||||
"Project:" — label
|
"Project:"
|
||||||
"I-70 at SR 51-75978 - Loc 1 - 4256 SR51 " — project description
|
"I-70 at SR 51-75978 - Loc 1 - 4256 SR51 " ← project description
|
||||||
"BE18189" — serial number
|
"BE18189" ← serial number
|
||||||
"Histogram" — record type label
|
"Histogram" ← record type
|
||||||
"Tran" — Transverse channel label
|
"Standard Recording Setup" ← setup name
|
||||||
"Vert" — Vertical channel label
|
"Client:"
|
||||||
"Long" — Longitudinal channel label
|
"Golden Triangle" ← client name
|
||||||
"MicL" — Microphone / air overpressure channel label
|
"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):
|
Peak values as IEEE 754 big-endian floats — event 1:
|
||||||
|
|
||||||
```
|
```
|
||||||
Tran: 3D BB 45 7A = 0.0916 in/s (or mm/s depending on unit config)
|
Tran: 3D BB 45 7A = 0.0916 (in/s — unit config dependent)
|
||||||
Vert: 3D B9 56 E1 = 0.0907 in/s
|
Vert: 3D B9 56 E1 = 0.0907
|
||||||
Long: 3D 75 C2 7C = 0.0605 in/s
|
Long: 3D 75 C2 7C = 0.0605
|
||||||
MicL: 39 BE 18 B8 = 0.000145 (PSI or linear dB) ❓ units unconfirmed
|
MicL: 39 BE 18 B8 = 0.000145 (PSI or dB linear — ❓ units unconfirmed)
|
||||||
```
|
```
|
||||||
|
|
||||||
Peak values (event 2):
|
Peak values — event 2:
|
||||||
|
|
||||||
```
|
```
|
||||||
Tran: 3D 56 CB B9 = 0.0521 in/s
|
Tran: 3D 56 CB B9 = 0.0521
|
||||||
Vert: 3C F5 C2 7C = 0.0300 in/s
|
Vert: 3C F5 C2 7C = 0.0300
|
||||||
Long: 3C F5 C2 7C = 0.0300 in/s
|
Long: 3C F5 C2 7C = 0.0300
|
||||||
MicL: 39 64 1D AA = 0.0000875
|
MicL: 39 64 1D AA = 0.0000875
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.6 Bulk Waveform Stream (CMD A5) — Raw ADC Sample Records
|
### 7.6 Bulk Waveform Stream (SUB A5) — Raw ADC Sample Records
|
||||||
|
|
||||||
Each repeating sample record structure (🔶 INFERRED):
|
Each repeating record (🔶 INFERRED structure):
|
||||||
|
|
||||||
```
|
```
|
||||||
[CH_ID] [S0_HI] [S0_LO] [S1_HI] [S1_LO] ... [S8_HI] [S8_LO] [00 00] [01] [FLOAT_HI] [FLOAT_MID] [FLOAT_LO]
|
[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
|
01 00 0A 00 0B 43 xx xx
|
||||||
```
|
```
|
||||||
|
|
||||||
- `CH_ID` — Channel identifier (01 = Tran, 02 = Vert, etc.) 🔶 INFERRED
|
- `CH_ID` — Channel identifier. `01` consistently observed. Full mapping unknown. 🔶 INFERRED
|
||||||
- 9× signed 16-bit big-endian ADC samples (~0x000A = baseline noise)
|
- 9× signed 16-bit big-endian ADC samples. Noise floor ≈ `0x000A`–`0x000B`
|
||||||
- Trailing IEEE 754 float — appears to be the peak value for this sample window
|
- `00 00` — separator / padding
|
||||||
- `0x43` prefix on float = value in range 130–260 (scaled mm/s or in/s ×1000) ❓
|
- `01` — unknown flag byte
|
||||||
|
- 3-byte partial IEEE 754 float — peak value for this sample window. `0x43` prefix = range 130–260
|
||||||
|
|
||||||
Sample record appears to be **fixed width**. At 1024 sps, 9 samples ≈ 8.8ms per record. ❓ SPECULATIVE — sample rate unconfirmed from this data alone.
|
> ❓ SPECULATIVE: At 1024 sps, 9 samples ≈ 8.8ms per record. Sample rate unconfirmed from captured data alone.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Timestamp Format
|
## 8. Timestamp Format
|
||||||
|
> 🔶 **Updated 2026-02-26** — Year field resolved. Confidence upgraded.
|
||||||
|
|
||||||
Timestamps appear in event headers and waveform keys. Observed example:
|
Timestamps are 6-byte sequences appearing in event headers and waveform keys.
|
||||||
|
|
||||||
|
**Observed example:**
|
||||||
```
|
```
|
||||||
01 07 CB 00 06 1E
|
01 07 CB 00 06 1E
|
||||||
```
|
```
|
||||||
|
|
||||||
Best guess decode (🔶 INFERRED):
|
**Decoded:**
|
||||||
|
|
||||||
```
|
| Byte(s) | Value | Meaning | Certainty |
|
||||||
01 — record type / validity flag?
|
|---|---|---|---|
|
||||||
07 CB — 0x07CB = 1995 ← suspicious, may be a different epoch or BCD
|
| `01` | 1 | Record validity / type flag | 🔶 INFERRED |
|
||||||
00 — padding?
|
| `07 CB` | 1995 | Year — 16-bit big-endian integer | ✅ CONFIRMED — 2026-02-26 |
|
||||||
06 — month (June)
|
| `00` | 0 | Unknown — possibly hours, minutes, or padding | ❓ SPECULATIVE |
|
||||||
1E — day (30)
|
| `06` | 6 | Month (June) | ✅ CONFIRMED |
|
||||||
```
|
| `1E` | 30 | Day (0x1E = 30 decimal) | ✅ CONFIRMED |
|
||||||
|
|
||||||
> ❓ **SPECULATIVE:** The year `0x07CB = 1995` seems wrong for modern equipment. Possible interpretations:
|
> ✅ **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.
|
||||||
> - BCD encoded: `07 CB` → not valid BCD
|
|
||||||
> - Epoch offset from some base year
|
> ❓ **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.
|
||||||
> - Or the full timestamp uses more bytes and the year field is elsewhere
|
|
||||||
>
|
|
||||||
> This needs more capture samples with known event dates to crack definitively.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Out-of-Band / Non-Frame Messages
|
## 9. Out-of-Band / Non-Frame Messages
|
||||||
|
|
||||||
Some messages arrive as **raw ASCII with no framing** at all:
|
|
||||||
|
|
||||||
| Message | Direction | Trigger | Certainty |
|
| Message | Direction | Trigger | Certainty |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `"Operating System"` | S3 → BW | Device boot/reset/UART init | ✅ CONFIRMED |
|
| `"Operating System"` | S3 → BW | Device boot / UART init / RTC reset | ✅ CONFIRMED |
|
||||||
|
|
||||||
> These raw ASCII bursts indicate the device's firmware is printing a boot string to the UART before switching to binary protocol mode. Your implementation should handle these gracefully — ignore any non-`0x02` STX bytes during the connection phase until the poll handshake succeeds.
|
> 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. Byte Escape / Stuffing
|
## 10. DLE Byte Stuffing
|
||||||
|
> ✅ **CONFIRMED — 2026-02-26** (previously ❓ SPECULATIVE)
|
||||||
|
|
||||||
> ❓ **SPECULATIVE / UNKNOWN**
|
This protocol uses standard **DLE (Data Link Escape) byte stuffing**, a classical technique used in protocols like IBM BISYNC dating to the 1970s.
|
||||||
|
|
||||||
The payload bytes `0x02` and `0x03` (STX/ETX) appear naturally in data payloads. It is not yet confirmed whether the protocol uses any byte stuffing or escaping to handle these collisions. In many embedded protocols these are escaped as `0x10 0x02` and `0x10 0x03` (DLE stuffing). The byte `0x10` appears frequently in payloads and may be a DLE (Data Link Escape) byte.
|
### Parser State Machine
|
||||||
|
|
||||||
> **Recommendation:** When building the frame parser, watch for `0x10` preceding `0x02` or `0x03` inside a frame — this may indicate DLE stuffing is in use.
|
```
|
||||||
|
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
|
## 11. Checksum Reference Implementation
|
||||||
|
> ⚠️ **Updated 2026-02-26** — Rewritten for correct DLE framing and byte stuffing.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
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:
|
def calc_checksum(payload: bytes) -> int:
|
||||||
"""
|
"""
|
||||||
8-bit sum of all payload bytes (between STX and CHKSUM), modulo 256.
|
8-bit sum of de-stuffed payload bytes, modulo 256.
|
||||||
Do NOT include STX (0x02), ACK (0x41), CHKSUM, or ETX (0x03).
|
Pass the original (pre-stuff) payload — not the wire bytes.
|
||||||
"""
|
"""
|
||||||
return sum(payload) & 0xFF
|
return sum(payload) & 0xFF
|
||||||
|
|
||||||
|
|
||||||
def build_frame(payload: bytes) -> bytes:
|
def build_frame(payload: bytes) -> bytes:
|
||||||
"""Build a complete frame: ACK + STX + payload + checksum + ETX"""
|
"""
|
||||||
|
Build a complete on-wire frame from a raw payload.
|
||||||
|
Output: ACK + DLE+STX + stuffed_payload + checksum + DLE+ETX
|
||||||
|
"""
|
||||||
chk = calc_checksum(payload)
|
chk = calc_checksum(payload)
|
||||||
return bytes([0x41, 0x02]) + payload + bytes([chk, 0x03])
|
stuffed = stuff(payload)
|
||||||
|
return bytes([ACK, DLE, STX]) + stuffed + bytes([chk, DLE, ETX])
|
||||||
|
|
||||||
|
|
||||||
def parse_frame(raw: bytes) -> bytes | None:
|
def parse_frame(raw: bytes) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Extract and validate payload from a raw frame.
|
Parse and validate a raw on-wire frame.
|
||||||
raw should start at STX (0x02) — strip leading 0x41 ACK before calling.
|
Accepts input starting with ACK (0x41) or DLE+STX (0x10 0x02).
|
||||||
Returns payload bytes if checksum valid, None otherwise.
|
Returns de-stuffed payload bytes on success, None on any error.
|
||||||
"""
|
"""
|
||||||
if raw[0] != 0x02 or raw[-1] != 0x03:
|
# Strip optional leading ACK
|
||||||
|
if raw and raw[0] == ACK:
|
||||||
|
raw = raw[1:]
|
||||||
|
|
||||||
|
# Validate frame delimiters
|
||||||
|
if len(raw) < 5:
|
||||||
return None
|
return None
|
||||||
payload = raw[1:-2]
|
if raw[0] != DLE or raw[1] != STX:
|
||||||
chk = raw[-2]
|
|
||||||
if calc_checksum(payload) != chk:
|
|
||||||
return None
|
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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -377,16 +494,16 @@ def parse_frame(raw: bytes) -> bytes | None:
|
|||||||
|
|
||||||
Build in this order — each step is independently testable:
|
Build in this order — each step is independently testable:
|
||||||
|
|
||||||
1. **Serial framer** — buffered reader that accumulates bytes until ETX `0x03`, strips ACK, validates checksum
|
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, wait for boot ASCII, send first POLL
|
2. **`connect(port, baud=38400)`** — open port, flush buffer, discard ASCII boot strings, send first POLL frame.
|
||||||
3. **`identify()`** — CMD `5B` two-step read → returns manufacturer, model strings
|
3. **`identify()`** — SUB `5B` two-step read → returns `{"manufacturer": "Instantel", "model": "MiniMate Plus"}`.
|
||||||
4. **`get_serial()`** — CMD `15` two-step read → returns serial number string
|
4. **`get_serial()`** — SUB `15` two-step read → returns serial number string.
|
||||||
5. **`get_config()`** — CMD `01` two-step read → returns full config dict (firmware, scaling, etc.)
|
5. **`get_config()`** — SUB `01` two-step read → returns full config dict (firmware, channel scales, etc.).
|
||||||
6. **`get_event_count()`** — CMD `08` two-step read → returns number of stored events
|
6. **`get_event_count()`** — SUB `08` two-step read → returns number of stored events.
|
||||||
7. **`get_event_header(index)`** — CMD `1E` read → returns timestamp, sample rate
|
7. **`get_event_header(index)`** — SUB `1E` read → returns timestamp dict.
|
||||||
8. **`get_event_record(timestamp)`** — CMD `0C` paginated read → returns PPV dict per channel
|
8. **`get_event_record(timestamp)`** — SUB `0C` paginated read → returns PPV dict per channel.
|
||||||
9. **`download_waveform(timestamp)`** — CMD `5A` bulk stream → returns raw ADC arrays 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
|
10. **`set_*()`** write commands — not yet captured, requires additional sniffing sessions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -398,26 +515,28 @@ Build in this order — each step is independently testable:
|
|||||||
| Model | MiniMate Plus |
|
| Model | MiniMate Plus |
|
||||||
| Serial Number | BE18189 |
|
| Serial Number | BE18189 |
|
||||||
| Firmware | S338.17 |
|
| Firmware | S338.17 |
|
||||||
| DSP/Secondary FW | 10.72 |
|
| DSP / Secondary FW | 10.72 |
|
||||||
| Channels | Tran, Vert, Long, MicL (4 channels) |
|
| Channels | Tran, Vert, Long, MicL (4 channels) |
|
||||||
| Sample Rate | ~1024 sps (❓ INFERRED) |
|
| Sample Rate | ~1024 sps (🔶 INFERRED) |
|
||||||
| Bridge Setup | COM5 (Blastware) ↔ COM4 (Device), 38400 baud |
|
| Bridge Config | COM5 (Blastware) ↔ COM4 (Device), 38400 baud |
|
||||||
|
| Capture Tool | s3_bridge v0.4.0 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Open Questions / Still Needs Cracking
|
## 14. Open Questions / Still Needs Cracking
|
||||||
|
|
||||||
| Question | Priority |
|
| Question | Priority | Added |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| Exact timestamp encoding (year field especially) | HIGH |
|
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 |
|
||||||
| Whether DLE `0x10` byte stuffing is used for STX/ETX in payload | HIGH |
|
| Meaning of `79 11 20` trailing bytes in serial number response | MEDIUM | 2026-02-26 |
|
||||||
| Meaning of `79 11` bytes following serial number | MEDIUM |
|
| 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 |
|
| Write / set commands for device configuration | MEDIUM | 2026-02-26 |
|
||||||
| Channel ID mapping in CMD `5A` stream (which byte = which sensor) | MEDIUM |
|
| Full trigger configuration field mapping (SUB `1C` response) | LOW | 2026-02-26 |
|
||||||
| Meaning of `0x07 E7` field in config block | LOW |
|
| Whether SUB `24`/`25` are distinct from SUB `5A` or redundant | LOW | 2026-02-26 |
|
||||||
| Full trigger configuration field mapping | LOW |
|
| Meaning of `0x07 E7` field in config block | LOW | 2026-02-26 |
|
||||||
| Whether `CMD 24`/`CMD 25` are distinct from `CMD 5A` or redundant | LOW |
|
| MicL channel units — PSI, dB linear, or dB(L)? | LOW | 2026-02-26 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Document generated from live RS-232 bridge captures. All findings are reverse-engineered. No Instantel proprietary documentation was referenced.*
|
*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.*
|
||||||
|
|||||||
Reference in New Issue
Block a user