feat: added raw binary data tracking for accurate format parser.

This commit is contained in:
serversdwn
2026-03-02 15:47:52 -05:00
parent 413fc53a39
commit 43c9c8b3a3
2 changed files with 246 additions and 140 deletions

View File

@@ -1,29 +1,23 @@
#!/usr/bin/env python3
"""
s3_bridge.py — S3 <-> Blastware serial bridge with frame-aware session logging
Version: v0.4.0
s3_bridge.py — S3 <-> Blastware serial bridge with raw binary capture + DLE-aware text framing
Version: v0.5.0
Key features:
- Low CPU: avoids per-byte console printing
- Forwards bytes immediately (true bridge)
- 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
- Dual log output: hex text log (.log) AND raw binary log (.bin) written simultaneously
- Interactive annotation: type 'm' + Enter to stamp a [MARK] into both logs mid-capture
- Binary sentinel markers: out-of-band FF FF FF FF <len> <label> in .bin for programmatic correlation
- Auto-marks on session start and end
Whats new vs v0.4.0:
- .bin is now a TRUE raw capture stream with direction + timestamps (record container format).
- .log remains human-friendly and frame-oriented, but frame detection is now DLE-aware:
- frame start = 0x10 0x02 (DLE STX)
- frame end = 0x10 0x03 (DLE ETX)
(No longer splits on bare 0x03.)
- Marks/Info are stored as proper record types in .bin (no unsafe sentinel bytes).
Usage examples:
python s3_bridge.py
python s3_bridge.py --bw COM5 --s3 COM4 --baud 38400
python s3_bridge.py --quiet
Annotation:
While running, type 'm' and press Enter. You will be prompted for a label.
The mark is written to the .log as:
[HH:MM:SS.mmm] >>> MARK: your label here
And to the .bin as an out-of-band sentinel (never valid frame data):
FF FF FF FF <1-byte length> <label bytes>
BIN record format (little-endian):
[type:1][ts_us:8][len:4][payload:len]
Types:
0x01 BW->S3 bytes
0x02 S3->BW bytes
0x03 MARK (utf-8)
0x04 INFO (utf-8)
"""
from __future__ import annotations
@@ -39,38 +33,56 @@ from typing import Optional
import serial
VERSION = "v0.5.0"
VERSION = "v0.4.0"
DLE = 0x10
STX = 0x02
ETX = 0x03
ACK = 0x41
# Sentinel prefix for binary markers. Four 0xFF bytes can never appear in
# valid Instantel DLE-framed data (0xFF is not a legal protocol byte in any
# framing position), so this sequence is unambiguously out-of-band.
BIN_MARK_SENTINEL = b"\xFF\xFF\xFF\xFF"
REC_BW = 0x01
REC_S3 = 0x02
REC_MARK = 0x03
REC_INFO = 0x04
def now_ts() -> str:
# Local time with milliseconds, like [13:37:06.239]
t = _dt.datetime.now()
return t.strftime("%H:%M:%S.") + f"{int(t.microsecond/1000):03d}"
def now_us() -> int:
# Wall-clock microseconds (fine for correlation). If you want monotonic, we can switch.
return int(time.time() * 1_000_000)
def bytes_to_hex(b: bytes) -> str:
return " ".join(f"{x:02X}" for x in b)
def looks_like_text(b: bytes) -> bool:
# Heuristic: mostly printable ASCII plus spaces
if not b:
return False
printable = 0
for x in b:
if x in (9, 10, 13): # \t \n \r
if x in (9, 10, 13):
printable += 1
elif 32 <= x <= 126:
printable += 1
return (printable / len(b)) >= 0.90
def pack_u32_le(n: int) -> bytes:
return bytes((n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF, (n >> 24) & 0xFF))
def pack_u64_le(n: int) -> bytes:
out = []
for i in range(8):
out.append((n >> (8 * i)) & 0xFF)
return bytes(out)
class SessionLogger:
def __init__(self, path: str, bin_path: str):
self.path = path
@@ -83,24 +95,24 @@ class SessionLogger:
with self._lock:
self._fh.write(line + "\n")
def log_raw(self, data: bytes) -> None:
"""Write raw bytes directly to the binary log."""
def bin_write_record(self, rec_type: int, payload: bytes, ts_us: Optional[int] = None) -> None:
if ts_us is None:
ts_us = now_us()
header = bytes([rec_type]) + pack_u64_le(ts_us) + pack_u32_le(len(payload))
with self._lock:
self._bin_fh.write(data)
self._bin_fh.write(header)
if payload:
self._bin_fh.write(payload)
def log_mark(self, label: str) -> None:
"""
Write an annotation mark to both logs simultaneously.
.log — visually distinct line: [TS] >>> MARK: label
.bin — out-of-band sentinel: FF FF FF FF <len> <label utf-8, max 255 bytes>
"""
ts = now_ts()
label_bytes = label.encode("utf-8", errors="replace")[:255]
sentinel = BIN_MARK_SENTINEL + bytes([len(label_bytes)]) + label_bytes
with self._lock:
self._fh.write(f"[{ts}] >>> MARK: {label}\n")
self._bin_fh.write(sentinel)
self.log_line(f"[{ts}] >>> MARK: {label}")
self.bin_write_record(REC_MARK, label.encode("utf-8", errors="replace"))
def log_info(self, msg: str) -> None:
ts = now_ts()
self.log_line(f"[{ts}] [INFO] {msg}")
self.bin_write_record(REC_INFO, msg.encode("utf-8", errors="replace"))
def close(self) -> None:
with self._lock:
@@ -112,51 +124,87 @@ class SessionLogger:
self._bin_fh.close()
class FrameAssembler:
class DLEFrameSniffer:
"""
Maintains a rolling buffer of bytes for one direction and emits complete frames.
We treat ETX=0x03 as an end-of-frame marker.
DLE-aware sniffer for logging only.
Extracts:
- ACK bytes (0x41) as single-byte events
- DLE-framed blocks starting at 10 02 and ending at 10 03
- Occasional ASCII bursts (e.g. "Operating System") outside framing
It does NOT modify bytes; it just segments them for the .log.
"""
def __init__(self):
self.buf = bytearray()
def push(self, chunk: bytes) -> list[bytes]:
def push(self, chunk: bytes) -> list[tuple[str, bytes]]:
if chunk:
self.buf.extend(chunk)
frames: list[bytes] = []
events: list[tuple[str, bytes]] = []
# Opportunistically peel off leading ACK(s) when idle-ish.
# We do this only when an ACK is not inside a frame (frames start with DLE).
while self.buf and self.buf[0] == ACK:
events.append(("ACK", bytes([ACK])))
del self.buf[0]
# Try to parse frames: find DLE STX then scan for DLE ETX
while True:
try:
etx_i = self.buf.index(0x03)
except ValueError:
# Find start of frame
start = self._find_dle_stx(self.buf)
if start is None:
# No frame start. Maybe text?
txt = bytes(self.buf)
if looks_like_text(txt):
events.append(("TEXT", txt))
self.buf.clear()
break
# include ETX byte
frame = bytes(self.buf[: etx_i + 1])
del self.buf[: etx_i + 1]
# Emit any leading text before the frame
if start > 0:
leading = bytes(self.buf[:start])
if looks_like_text(leading):
events.append(("TEXT", leading))
else:
# Unknown junk; still preserve in log as RAW so you can see it
events.append(("RAW", leading))
del self.buf[:start]
# ignore empty noise
if frame:
frames.append(frame)
# Now buf starts with DLE STX
end = self._find_dle_etx(self.buf)
if end is None:
break # need more bytes
return frames
frame = bytes(self.buf[:end])
del self.buf[:end]
def drain_as_text_if_any(self) -> Optional[bytes]:
"""
If buffer contains non-framed data (no ETX) and looks like text, emit it.
Useful for things like "Operating System" that come as raw ASCII.
"""
if not self.buf:
return None
b = bytes(self.buf)
if looks_like_text(b):
self.buf.clear()
return b
events.append(("FRAME", frame))
# peel off any ACKs that may immediately follow
while self.buf and self.buf[0] == ACK:
events.append(("ACK", bytes([ACK])))
del self.buf[0]
return events
@staticmethod
def _find_dle_stx(b: bytearray) -> Optional[int]:
for i in range(len(b) - 1):
if b[i] == DLE and b[i + 1] == STX:
return i
return None
@staticmethod
def _find_dle_etx(b: bytearray) -> Optional[int]:
# Find first occurrence of DLE ETX after the initial DLE STX.
# Return index *after* ETX (slice end).
for i in range(2, len(b) - 1):
if b[i] == DLE and b[i + 1] == ETX:
return i + 2
return None
def open_serial(port: str, baud: int) -> serial.Serial:
# timeout keeps read() from blocking forever, enabling clean Ctrl+C shutdown
return serial.Serial(
port=port,
baudrate=baud,
@@ -170,6 +218,7 @@ def open_serial(port: str, baud: int) -> serial.Serial:
def forward_loop(
name: str,
rec_type: int,
src: serial.Serial,
dst: serial.Serial,
logger: SessionLogger,
@@ -177,22 +226,24 @@ def forward_loop(
quiet: bool,
status_every_s: float,
) -> None:
assembler = FrameAssembler()
sniffer = DLEFrameSniffer()
last_status = time.monotonic()
while not stop.is_set():
try:
n = src.in_waiting
if n:
chunk = src.read(n if n < 4096 else 4096)
else:
chunk = src.read(1) # will return b"" after timeout
chunk = src.read(n if n and n < 4096 else (4096 if n else 1))
except serial.SerialException as e:
logger.log_line(f"[{now_ts()}] [ERROR] {name} serial exception: {e!r}")
break
if chunk:
# forward immediately
ts = now_us()
# 1) RAW BIN CAPTURE (absolute truth)
logger.bin_write_record(rec_type, chunk, ts_us=ts)
# 2) Forward immediately (bridge behavior)
try:
dst.write(chunk)
except serial.SerialTimeoutException:
@@ -201,55 +252,42 @@ def forward_loop(
logger.log_line(f"[{now_ts()}] [ERROR] {name} dst write exception: {e!r}")
break
# frame-aware logging
frames = assembler.push(chunk)
for frame in frames:
# Some devices send leading STX separately; we still log as-is.
logger.log_line(f"[{now_ts()}] [{name}] {bytes_to_hex(frame)}")
logger.log_raw(frame)
# 3) Human-friendly .log segmentation (DLE-aware)
for kind, payload in sniffer.push(chunk):
if kind == "ACK":
logger.log_line(f"[{now_ts()}] [{name}] [ACK] 41")
elif kind == "FRAME":
logger.log_line(f"[{now_ts()}] [{name}] {bytes_to_hex(payload)}")
elif kind == "TEXT":
try:
s = payload.decode("ascii", errors="replace").strip("\r\n")
except Exception:
s = repr(payload)
logger.log_line(f"[{now_ts()}] [{name}] [TEXT] {s}")
else: # RAW
logger.log_line(f"[{now_ts()}] [{name}] [RAW] {bytes_to_hex(payload)}")
# If we have non-ETX data that looks like text, flush it as TEXT
text = assembler.drain_as_text_if_any()
if text is not None:
try:
s = text.decode("ascii", errors="replace").strip("\r\n")
except Exception:
s = repr(text)
logger.log_line(f"[{now_ts()}] [{name}] [TEXT] {s}")
logger.log_raw(text)
# minimal console heartbeat (cheap)
if not quiet and status_every_s > 0:
now = time.monotonic()
if (now - last_status) >= status_every_s:
print(f"[{now_ts()}] {name} alive")
last_status = now
# tiny sleep only when idle to avoid spin
if not chunk:
time.sleep(0.002)
def annotation_loop(logger: SessionLogger, stop: threading.Event) -> None:
"""
Runs on the main thread (or a dedicated thread) reading stdin.
Type 'm' + Enter to trigger an annotation prompt.
Any other non-empty input is ignored with a hint.
Bare Enter (empty line) is silently ignored to prevent accidental marks.
"""
print("[MARK] Type 'm' + Enter to annotate the capture. Ctrl+C to stop.")
while not stop.is_set():
try:
line = input()
except EOFError:
# stdin closed (e.g. piped input exhausted)
break
except KeyboardInterrupt:
except (EOFError, KeyboardInterrupt):
break
line = line.strip()
if not line:
continue # bare Enter — ignore silently
continue
if line.lower() == "m":
try:
@@ -292,11 +330,12 @@ def main() -> int:
log_path = os.path.join(args.logdir, f"s3_session_{ts}.log")
bin_path = os.path.join(args.logdir, f"s3_session_{ts}.bin")
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] BW={args.bw} S3={args.s3} baud={args.baud}")
logger.log_info(f"s3_bridge {VERSION} start")
logger.log_info(f"BW={args.bw} S3={args.s3} baud={args.baud}")
logger.log_mark(f"SESSION START — BW={args.bw} S3={args.s3} baud={args.baud}")
stop = threading.Event()
@@ -309,16 +348,15 @@ def main() -> int:
t1 = threading.Thread(
target=forward_loop,
name="BW_to_S3",
args=("BW->S3", bw, s3, logger, stop, args.quiet, args.status_every),
args=("BW->S3", REC_BW, bw, s3, logger, stop, args.quiet, args.status_every),
daemon=True,
)
t2 = threading.Thread(
target=forward_loop,
name="S3_to_BW",
args=("S3->BW", s3, bw, logger, stop, args.quiet, args.status_every),
args=("S3->BW", REC_S3, s3, bw, logger, stop, args.quiet, args.status_every),
daemon=True,
)
# Annotation loop runs in its own daemon thread so it doesn't block shutdown
t_ann = threading.Thread(
target=annotation_loop,
name="Annotator",
@@ -335,12 +373,11 @@ def main() -> int:
time.sleep(0.05)
finally:
print("\n[INFO] Ctrl+C detected, shutting down...")
logger.log_line(f"[{now_ts()}] [INFO] shutdown requested")
logger.log_info("shutdown requested")
stop.set()
t1.join(timeout=1.0)
t2.join(timeout=1.0)
# t_ann is daemon — don't join, it may be blocked on input()
try:
bw.close()
@@ -352,8 +389,7 @@ def main() -> int:
pass
logger.log_mark("SESSION END")
logger.log_line(f"[{now_ts()}] [INFO] ports closed, session end")
print("[LOG] Closing session log")
logger.log_info("ports closed, session end")
logger.close()
return 0

View File

@@ -1,7 +1,7 @@
# 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.
> 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.
@@ -32,6 +32,14 @@
| 2026-02-26 | §7.6 Channel Config Float Layout | **NEW SECTION:** Trigger level confirmed as IEEE 754 BE float in in/s. Alarm level identified as adjacent float = 1.0 in/s. Unit string `"in./s"` embedded inline. `0x082A` removed as trigger level candidate. |
| 2026-03-01 | §7.6 Channel Config Float Layout | **UPGRADED:** Alarm level offset fully confirmed via controlled capture (alarm 1.0→2.0, trigger 0.5→0.6). Complete per-channel layout documented. Three-channel repetition confirmed (Tran, Vert, Long). Certainty upgraded to ✅ CONFIRMED. |
| 2026-03-01 | §7.7 `.set` File Format | **NEW SECTION:** Blastware save-to-disk format decoded. Little-endian binary struct matching wire protocol payload. Full per-channel block layout mapped. Record time confirmed as uint32 at +16. MicL unit string confirmed as `"psi\0"`. `0x082A` mystery noted — not obviously record time, needs one more capture to resolve. |
| 2026-03-02 | §7.4 Event Index Block | **CONFIRMED:** Backlight and power save offsets independently confirmed via device-set capture (backlight=100=0x64 at +75, power-save=30=0x1E at +83). On-device change visible in S3→BW read response — no Blastware write involved. Offsets are ✅ CONFIRMED. |
| 2026-03-02 | §7.4 Event Index Block | **NEW:** `Monitoring LCD Cycle` identified at offsets +84/+85 as uint16 BE. Default value = 65500 (0xFFDC) = effectively disabled / maximum. Confirmed from operator manual §3.13.1g. |
| 2026-03-02 | §7.4 Event Index Block | **UPDATED:** Backlight confirmed as uint8 range 0255 seconds per operator manual §3.13.1e ("adjustable timer, 0 to 255 seconds"). Power save unit confirmed as minutes per operator manual §3.13.1f. |
| 2026-03-02 | Global | **NEW SOURCE:** Operator manual (716U0101 Rev 15) added as reference. Cross-referencing settings definitions, ranges, and units. Header updated. |
| 2026-03-02 | §14 Open Questions | Float 6.2061 in/s mystery: manual confirms only two geo ranges (1.25 in/s and 10.0 in/s). 6.2061 is NOT a user-selectable range → likely internal ADC full-scale calibration constant or hardware range ceiling. Downgraded to LOW priority. |
| 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. |
---
@@ -333,15 +341,41 @@ Unit 2: serial="BE11529" trail=70 11 firmware=S337.17
### 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 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 ❓
Offset +00: 00 58 09 — Total index size or record count ❓
Offset +03: 00 00 00 01 — Possibly stored event count = 1 ❓
Offset +07: 01 07 CB 00 06 1E — Timestamp of event 1 (see §8)
Offset +0D: 01 07 CB 00 14 00 — Timestamp of event 2 (see §8)
Offset +13: 00 00 00 17 3B — Unknown ❓
Offset +4B: [backlight] — BACKLIGHT ON TIME ✅ CONFIRMED
Offset +4C: 00 — padding (backlight is uint8, not uint16)
Offset +53: [power_save] — POWER SAVING TIMEOUT ✅ CONFIRMED
Offset +54: [lcd_hi] [lcd_lo] — MONITORING LCD CYCLE (uint16 BE) ✅ CONFIRMED
```
| Offset | Size | Type | Known values | Meaning | Certainty |
|---|---|---|---|---|---|
| +4B | 1 | uint8 | 250, 100 | **BACKLIGHT ON TIME** (0255 seconds per manual) | ✅ CONFIRMED |
| +4C | 1 | — | 0x00 | Padding / high byte of potential uint16 | 🔶 INFERRED |
| +53 | 1 | uint8 | 10, 30 | **POWER SAVING TIMEOUT** (minutes) | ✅ CONFIRMED |
| +54..+55 | 2 | uint16 BE | 0xFFDC = 65500 | **MONITORING LCD CYCLE** (seconds; 65500 ≈ disabled/max) | ✅ CONFIRMED |
**Confirmation captures:**
| Capture | Backlight (+4B) | Power Save (+53) | LCD Cycle (+54/55) |
|---|---|---|---|
| `20260301_160702` (BW-written) | `0xFA` = 250 | `0x0A` = 10 min | `0xFF 0xDC` = 65500 |
| `20260302_144606` (device-set) | `0x64` = 100 | `0x1E` = 30 min | `0xFF 0xDC` = 65500 |
> 📖 **Manual cross-reference (716U0101 Rev 15, §3.13.1):**
> - Backlight On Time: "adjustable timer, from 0 to 255 seconds" (§3.13.1e)
> - Power Saving Timeout: "automatically turns the Minimate Plus off" — stored in minutes (§3.13.1f)
> - Monitoring LCD Cycle: "cycles off for the time period... set to zero to turn off" — 65500 = effectively disabled (§3.13.1g)
### 7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages
> ✅ **2026-02-26 — UPDATED:** Project strings field layout confirmed by diffing compliance setup write payload (SUB `71`). Client field change `"Hello Claude"` → `"Claude test2"` isolated exact byte position.
@@ -741,24 +775,60 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
## 14. Open Questions / Still Needs Cracking
| Question | Priority | Added |
|---|---|---|
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 |
| `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 | MEDIUM | 2026-02-26 |
| `0x082A` in channel config block — not trigger, alarm, or record time. Likely one of the unknown uint16 fields (+0A to +10 in per-channel struct). Needs capture changing sample rate or histogram interval to isolate. | MEDIUM | 2026-03-01 |
| Max range float = 6.2061 in/s — meaning unclear. UI shows "Normal 10.000 in/s" range setting. | LOW | 2026-02-26 |
| Unknown uint16 fields at channel block +0A (=80), +0C (=15), +0E (=40), +10 (=21) — likely sensitivity, gain, 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 | LOW | 2026-02-26 |
| MicL channel units — **RESOLVED: psi**, confirmed from `.set` file unit string `"psi\0"` | RESOLVED | 2026-03-01 |
| Question | Priority | Added | Notes |
|---|---|---|---|
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 | |
| `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 | MEDIUM | 2026-02-26 | |
| `0x082A` in channel config block — not trigger, alarm, or record time directly. **Hypothesis:** total sample count at 1024 sps: 2 sec record + 0.25 pre-trigger = 2.25 sec × ~930 sps? Or encoded differently. Capture with different record time needed. | MEDIUM | 2026-03-01 | Updated 2026-03-02 |
| 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 | LOW | 2026-02-26 | |
| **Trigger Sample Width** — setting confirmed in manual (default=2 samples, §3.13.1h). Location in protocol not yet mapped. | LOW | 2026-03-02 | NEW |
| **Auto Window** — "1 to 9 seconds" per manual (§3.13.1b). Location in protocol not yet mapped. | LOW | 2026-03-02 | NEW |
| **Auxiliary Trigger** — Enabled/Disabled per manual (§3.13.1d). Location in protocol not yet mapped. | LOW | 2026-03-02 | NEW |
| **Max Geo Range float 6.2061 in/s** — NOT a user-selectable range (manual only shows 1.25 and 10.0 in/s). Likely internal ADC full-scale constant or hardware range ceiling. Not worth capturing. | LOW | 2026-02-26 | Downgraded 2026-03-02 |
| 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 | |
---
*All findings reverse-engineered from live RS-232 bridge captures. No Instantel proprietary documentation was referenced or used.*
---
## Appendix B — Operator Manual Cross-Reference (716U0101 Rev 15)
> Added 2026-03-02. Cross-referencing confirms setting names, ranges, units, and behavior for fields found in protocol captures. The manual does NOT describe the wire protocol — it describes the user-facing device interface. Use to infer data types, ranges, and semantics of protocol fields.
| Setting Name (Manual) | Manual Location | Protocol Location | Type | Range / Notes |
|---|---|---|---|---|
| Backlight On Time | §3.13.1e | Event Index +4B | uint8 | 0255 seconds |
| Power Saving Timeout | §3.13.1f | Event Index +53 | uint8 | minutes (user sets 160+) |
| Monitoring LCD Cycle | §3.13.1g | Event Index +54/55 | uint16 BE | seconds; 0=off; 65500≈disabled |
| Trigger Level (Geo) | §3.8.6 | Channel block, float | float32 BE | 0.00510.000 in/s |
| Alarm Level (Geo) | §3.9.9 | Channel block, float | float32 BE | higher than trigger level |
| Trigger Level (Mic) | §3.8.6 | Channel block, float | float32 BE | 100148 dB in 1 dB steps |
| Alarm Level (Mic) | §3.9.10 | Channel block, float | float32 BE | higher than mic trigger |
| Record Time | §3.8.9 | `.set` +16 confirmed | uint32 | 1500 seconds |
| Max Geo Range | §3.8.4 | Channel block, float | float32 BE | 1.25 or 10.0 in/s (user); 6.2061 in protocol = internal constant |
| Microphone Units | §3.9.7 | Inline unit string | char[4] | `"psi\0"`, `"pa.\0"`, `"dB\0\0"` |
| Sample Rate | §3.8.2 | Unknown — needs capture | — | 1024, 2048, 4096 (compliance); up to 65536 (advanced) |
| Record Mode | §3.8.1 | Unknown | — | Single Shot, Continuous, Manual, Histogram, Histogram Combo |
| Trigger Sample Width | §3.13.1h | **NOT YET MAPPED** | uint8? | Default=2 samples |
| Auto Window | §3.13.1b | **NOT YET MAPPED** | uint8? | 19 seconds |
| Auxiliary Trigger | §3.13.1d | **NOT YET MAPPED** | bool | Enabled/Disabled |
| 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 |
---
*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.*