Compare commits
2 Commits
0ad1505cc5
...
43c9c8b3a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43c9c8b3a3 | ||
|
|
413fc53a39 |
@@ -1,29 +1,23 @@
|
|||||||
#!/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 raw binary capture + DLE-aware text framing
|
||||||
Version: v0.4.0
|
Version: v0.5.0
|
||||||
|
|
||||||
Key features:
|
What’s new vs v0.4.0:
|
||||||
- Low CPU: avoids per-byte console printing
|
- .bin is now a TRUE raw capture stream with direction + timestamps (record container format).
|
||||||
- Forwards bytes immediately (true bridge)
|
- .log remains human-friendly and frame-oriented, but frame detection is now DLE-aware:
|
||||||
- Frame-aware logging: buffers per direction until ETX (0x03), then logs full frame on one line
|
- frame start = 0x10 0x02 (DLE STX)
|
||||||
- Also logs plain ASCII bursts (e.g., "Operating System") cleanly
|
- frame end = 0x10 0x03 (DLE ETX)
|
||||||
- Dual log output: hex text log (.log) AND raw binary log (.bin) written simultaneously
|
(No longer splits on bare 0x03.)
|
||||||
- Interactive annotation: type 'm' + Enter to stamp a [MARK] into both logs mid-capture
|
- Marks/Info are stored as proper record types in .bin (no unsafe sentinel bytes).
|
||||||
- Binary sentinel markers: out-of-band FF FF FF FF <len> <label> in .bin for programmatic correlation
|
|
||||||
- Auto-marks on session start and end
|
|
||||||
|
|
||||||
Usage examples:
|
BIN record format (little-endian):
|
||||||
python s3_bridge.py
|
[type:1][ts_us:8][len:4][payload:len]
|
||||||
python s3_bridge.py --bw COM5 --s3 COM4 --baud 38400
|
Types:
|
||||||
python s3_bridge.py --quiet
|
0x01 BW->S3 bytes
|
||||||
|
0x02 S3->BW bytes
|
||||||
Annotation:
|
0x03 MARK (utf-8)
|
||||||
While running, type 'm' and press Enter. You will be prompted for a label.
|
0x04 INFO (utf-8)
|
||||||
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>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -39,38 +33,56 @@ from typing import Optional
|
|||||||
|
|
||||||
import serial
|
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
|
REC_BW = 0x01
|
||||||
# valid Instantel DLE-framed data (0xFF is not a legal protocol byte in any
|
REC_S3 = 0x02
|
||||||
# framing position), so this sequence is unambiguously out-of-band.
|
REC_MARK = 0x03
|
||||||
BIN_MARK_SENTINEL = b"\xFF\xFF\xFF\xFF"
|
REC_INFO = 0x04
|
||||||
|
|
||||||
|
|
||||||
def now_ts() -> str:
|
def now_ts() -> str:
|
||||||
# Local time with milliseconds, like [13:37:06.239]
|
|
||||||
t = _dt.datetime.now()
|
t = _dt.datetime.now()
|
||||||
return t.strftime("%H:%M:%S.") + f"{int(t.microsecond/1000):03d}"
|
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:
|
def bytes_to_hex(b: bytes) -> str:
|
||||||
return " ".join(f"{x:02X}" for x in b)
|
return " ".join(f"{x:02X}" for x in b)
|
||||||
|
|
||||||
|
|
||||||
def looks_like_text(b: bytes) -> bool:
|
def looks_like_text(b: bytes) -> bool:
|
||||||
# Heuristic: mostly printable ASCII plus spaces
|
|
||||||
if not b:
|
if not b:
|
||||||
return False
|
return False
|
||||||
printable = 0
|
printable = 0
|
||||||
for x in b:
|
for x in b:
|
||||||
if x in (9, 10, 13): # \t \n \r
|
if x in (9, 10, 13):
|
||||||
printable += 1
|
printable += 1
|
||||||
elif 32 <= x <= 126:
|
elif 32 <= x <= 126:
|
||||||
printable += 1
|
printable += 1
|
||||||
return (printable / len(b)) >= 0.90
|
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:
|
class SessionLogger:
|
||||||
def __init__(self, path: str, bin_path: str):
|
def __init__(self, path: str, bin_path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -83,24 +95,24 @@ class SessionLogger:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._fh.write(line + "\n")
|
self._fh.write(line + "\n")
|
||||||
|
|
||||||
def log_raw(self, data: bytes) -> None:
|
def bin_write_record(self, rec_type: int, payload: bytes, ts_us: Optional[int] = None) -> None:
|
||||||
"""Write raw bytes directly to the binary log."""
|
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:
|
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:
|
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()
|
ts = now_ts()
|
||||||
label_bytes = label.encode("utf-8", errors="replace")[:255]
|
self.log_line(f"[{ts}] >>> MARK: {label}")
|
||||||
sentinel = BIN_MARK_SENTINEL + bytes([len(label_bytes)]) + label_bytes
|
self.bin_write_record(REC_MARK, label.encode("utf-8", errors="replace"))
|
||||||
with self._lock:
|
|
||||||
self._fh.write(f"[{ts}] >>> MARK: {label}\n")
|
def log_info(self, msg: str) -> None:
|
||||||
self._bin_fh.write(sentinel)
|
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:
|
def close(self) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -112,51 +124,87 @@ class SessionLogger:
|
|||||||
self._bin_fh.close()
|
self._bin_fh.close()
|
||||||
|
|
||||||
|
|
||||||
class FrameAssembler:
|
class DLEFrameSniffer:
|
||||||
"""
|
"""
|
||||||
Maintains a rolling buffer of bytes for one direction and emits complete frames.
|
DLE-aware sniffer for logging only.
|
||||||
We treat ETX=0x03 as an end-of-frame marker.
|
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):
|
def __init__(self):
|
||||||
self.buf = bytearray()
|
self.buf = bytearray()
|
||||||
|
|
||||||
def push(self, chunk: bytes) -> list[bytes]:
|
def push(self, chunk: bytes) -> list[tuple[str, bytes]]:
|
||||||
if chunk:
|
if chunk:
|
||||||
self.buf.extend(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:
|
while True:
|
||||||
try:
|
# Find start of frame
|
||||||
etx_i = self.buf.index(0x03)
|
start = self._find_dle_stx(self.buf)
|
||||||
except ValueError:
|
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
|
break
|
||||||
|
|
||||||
# include ETX byte
|
# Emit any leading text before the frame
|
||||||
frame = bytes(self.buf[: etx_i + 1])
|
if start > 0:
|
||||||
del self.buf[: etx_i + 1]
|
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
|
# Now buf starts with DLE STX
|
||||||
if frame:
|
end = self._find_dle_etx(self.buf)
|
||||||
frames.append(frame)
|
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]:
|
events.append(("FRAME", frame))
|
||||||
"""
|
|
||||||
If buffer contains non-framed data (no ETX) and looks like text, emit it.
|
# peel off any ACKs that may immediately follow
|
||||||
Useful for things like "Operating System" that come as raw ASCII.
|
while self.buf and self.buf[0] == ACK:
|
||||||
"""
|
events.append(("ACK", bytes([ACK])))
|
||||||
if not self.buf:
|
del self.buf[0]
|
||||||
return None
|
|
||||||
b = bytes(self.buf)
|
return events
|
||||||
if looks_like_text(b):
|
|
||||||
self.buf.clear()
|
@staticmethod
|
||||||
return b
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def open_serial(port: str, baud: int) -> serial.Serial:
|
def open_serial(port: str, baud: int) -> serial.Serial:
|
||||||
# timeout keeps read() from blocking forever, enabling clean Ctrl+C shutdown
|
|
||||||
return serial.Serial(
|
return serial.Serial(
|
||||||
port=port,
|
port=port,
|
||||||
baudrate=baud,
|
baudrate=baud,
|
||||||
@@ -170,6 +218,7 @@ def open_serial(port: str, baud: int) -> serial.Serial:
|
|||||||
|
|
||||||
def forward_loop(
|
def forward_loop(
|
||||||
name: str,
|
name: str,
|
||||||
|
rec_type: int,
|
||||||
src: serial.Serial,
|
src: serial.Serial,
|
||||||
dst: serial.Serial,
|
dst: serial.Serial,
|
||||||
logger: SessionLogger,
|
logger: SessionLogger,
|
||||||
@@ -177,22 +226,24 @@ def forward_loop(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
status_every_s: float,
|
status_every_s: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
assembler = FrameAssembler()
|
sniffer = DLEFrameSniffer()
|
||||||
last_status = time.monotonic()
|
last_status = time.monotonic()
|
||||||
|
|
||||||
while not stop.is_set():
|
while not stop.is_set():
|
||||||
try:
|
try:
|
||||||
n = src.in_waiting
|
n = src.in_waiting
|
||||||
if n:
|
chunk = src.read(n if n and n < 4096 else (4096 if n else 1))
|
||||||
chunk = src.read(n if n < 4096 else 4096)
|
|
||||||
else:
|
|
||||||
chunk = src.read(1) # will return b"" after timeout
|
|
||||||
except serial.SerialException as e:
|
except serial.SerialException as e:
|
||||||
logger.log_line(f"[{now_ts()}] [ERROR] {name} serial exception: {e!r}")
|
logger.log_line(f"[{now_ts()}] [ERROR] {name} serial exception: {e!r}")
|
||||||
break
|
break
|
||||||
|
|
||||||
if chunk:
|
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:
|
try:
|
||||||
dst.write(chunk)
|
dst.write(chunk)
|
||||||
except serial.SerialTimeoutException:
|
except serial.SerialTimeoutException:
|
||||||
@@ -201,55 +252,42 @@ def forward_loop(
|
|||||||
logger.log_line(f"[{now_ts()}] [ERROR] {name} dst write exception: {e!r}")
|
logger.log_line(f"[{now_ts()}] [ERROR] {name} dst write exception: {e!r}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# frame-aware logging
|
# 3) Human-friendly .log segmentation (DLE-aware)
|
||||||
frames = assembler.push(chunk)
|
for kind, payload in sniffer.push(chunk):
|
||||||
for frame in frames:
|
if kind == "ACK":
|
||||||
# Some devices send leading STX separately; we still log as-is.
|
logger.log_line(f"[{now_ts()}] [{name}] [ACK] 41")
|
||||||
logger.log_line(f"[{now_ts()}] [{name}] {bytes_to_hex(frame)}")
|
elif kind == "FRAME":
|
||||||
logger.log_raw(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:
|
if not quiet and status_every_s > 0:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if (now - last_status) >= status_every_s:
|
if (now - last_status) >= status_every_s:
|
||||||
print(f"[{now_ts()}] {name} alive")
|
print(f"[{now_ts()}] {name} alive")
|
||||||
last_status = now
|
last_status = now
|
||||||
|
|
||||||
# tiny sleep only when idle to avoid spin
|
|
||||||
if not chunk:
|
if not chunk:
|
||||||
time.sleep(0.002)
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
|
||||||
def annotation_loop(logger: SessionLogger, stop: threading.Event) -> None:
|
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.")
|
print("[MARK] Type 'm' + Enter to annotate the capture. Ctrl+C to stop.")
|
||||||
while not stop.is_set():
|
while not stop.is_set():
|
||||||
try:
|
try:
|
||||||
line = input()
|
line = input()
|
||||||
except EOFError:
|
except (EOFError, KeyboardInterrupt):
|
||||||
# stdin closed (e.g. piped input exhausted)
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue # bare Enter — ignore silently
|
continue
|
||||||
|
|
||||||
if line.lower() == "m":
|
if line.lower() == "m":
|
||||||
try:
|
try:
|
||||||
@@ -292,11 +330,12 @@ def main() -> int:
|
|||||||
log_path = os.path.join(args.logdir, f"s3_session_{ts}.log")
|
log_path = os.path.join(args.logdir, f"s3_session_{ts}.log")
|
||||||
bin_path = os.path.join(args.logdir, f"s3_session_{ts}.bin")
|
bin_path = os.path.join(args.logdir, f"s3_session_{ts}.bin")
|
||||||
logger = SessionLogger(log_path, bin_path)
|
logger = SessionLogger(log_path, bin_path)
|
||||||
|
|
||||||
print(f"[LOG] Writing hex log to {log_path}")
|
print(f"[LOG] Writing hex log to {log_path}")
|
||||||
print(f"[LOG] Writing binary log to {bin_path}")
|
print(f"[LOG] Writing binary log to {bin_path}")
|
||||||
|
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] s3_bridge {VERSION} start")
|
logger.log_info(f"s3_bridge {VERSION} start")
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] BW={args.bw} S3={args.s3} baud={args.baud}")
|
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}")
|
logger.log_mark(f"SESSION START — BW={args.bw} S3={args.s3} baud={args.baud}")
|
||||||
|
|
||||||
stop = threading.Event()
|
stop = threading.Event()
|
||||||
@@ -309,16 +348,15 @@ def main() -> int:
|
|||||||
t1 = threading.Thread(
|
t1 = threading.Thread(
|
||||||
target=forward_loop,
|
target=forward_loop,
|
||||||
name="BW_to_S3",
|
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,
|
daemon=True,
|
||||||
)
|
)
|
||||||
t2 = threading.Thread(
|
t2 = threading.Thread(
|
||||||
target=forward_loop,
|
target=forward_loop,
|
||||||
name="S3_to_BW",
|
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,
|
daemon=True,
|
||||||
)
|
)
|
||||||
# Annotation loop runs in its own daemon thread so it doesn't block shutdown
|
|
||||||
t_ann = threading.Thread(
|
t_ann = threading.Thread(
|
||||||
target=annotation_loop,
|
target=annotation_loop,
|
||||||
name="Annotator",
|
name="Annotator",
|
||||||
@@ -335,12 +373,11 @@ def main() -> int:
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
finally:
|
finally:
|
||||||
print("\n[INFO] Ctrl+C detected, shutting down...")
|
print("\n[INFO] Ctrl+C detected, shutting down...")
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] shutdown requested")
|
logger.log_info("shutdown requested")
|
||||||
|
|
||||||
stop.set()
|
stop.set()
|
||||||
t1.join(timeout=1.0)
|
t1.join(timeout=1.0)
|
||||||
t2.join(timeout=1.0)
|
t2.join(timeout=1.0)
|
||||||
# t_ann is daemon — don't join, it may be blocked on input()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bw.close()
|
bw.close()
|
||||||
@@ -352,8 +389,7 @@ def main() -> int:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
logger.log_mark("SESSION END")
|
logger.log_mark("SESSION END")
|
||||||
logger.log_line(f"[{now_ts()}] [INFO] ports closed, session end")
|
logger.log_info("ports closed, session end")
|
||||||
print("[LOG] Closing session log")
|
|
||||||
logger.close()
|
logger.close()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Instantel MiniMate Plus — Blastware RS-232 Protocol Reference
|
# 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.
|
> Cross-referenced against Instantel MiniMate Plus Operator Manual (716U0101 Rev 15) from v0.18 onward.
|
||||||
> **Certainty Ratings:** ✅ CONFIRMED | 🔶 INFERRED | ❓ SPECULATIVE
|
> **Certainty Ratings:** ✅ CONFIRMED | 🔶 INFERRED | ❓ SPECULATIVE
|
||||||
> Certainty ratings apply only to protocol semantics, not to capture tooling behavior.
|
> Certainty ratings apply only to protocol semantics, not to capture tooling behavior.
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
| Date | Section | Change |
|
| Date | Section | Change |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 2026-02-25 | Initial | Document created from first hex dump analysis |
|
| 2026-02-26 | Initial | Document created from first hex dump analysis |
|
||||||
| 2026-02-25 | §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 | §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-25 | §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 | §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-25 | §10 DLE Stuffing | **UPGRADED:** Section upgraded from ❓ SPECULATIVE to ✅ CONFIRMED. Full stuffing rules and parser state machine documented. |
|
| 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 | §11 Checksum | **UPDATED:** Frame builder and parser rewritten to handle DLE framing and byte stuffing correctly. |
|
||||||
| 2026-02-26 | §14 Open Questions | DLE question removed (resolved). Timestamp year question removed (resolved). |
|
| 2026-02-26 | §14 Open Questions | DLE question removed (resolved). Timestamp year question removed (resolved). |
|
||||||
| 2026-02-26 | §7.2 Serial Number Response | **CORRECTED:** Trailing bytes are `0x79 0x11` only (2 bytes, not 3). `0x20` was misidentified as a trailing byte — it is the frame checksum. |
|
| 2026-02-26 | §7.2 Serial Number Response | **CORRECTED:** Trailing bytes are `0x79 0x11` only (2 bytes, not 3). `0x20` was misidentified as a trailing byte — it is the frame checksum. |
|
||||||
@@ -30,7 +30,16 @@
|
|||||||
| 2026-02-26 | §15 → Appendix A | **RENAMED:** Binary log format section moved to Appendix A with explicit note that it describes tooling behavior, not protocol. |
|
| 2026-02-26 | §15 → Appendix A | **RENAMED:** Binary log format section moved to Appendix A with explicit note that it describes tooling behavior, not protocol. |
|
||||||
| 2026-02-26 | Header | **ADDED:** Certainty legend clarification — ratings apply to protocol semantics only, not tooling behavior. |
|
| 2026-02-26 | Header | **ADDED:** Certainty legend clarification — ratings apply to protocol semantics only, not tooling behavior. |
|
||||||
| 2026-02-26 | §7.6 Channel Config Float Layout | **NEW SECTION:** Trigger level confirmed as IEEE 754 BE float in in/s. Alarm level identified as adjacent float = 1.0 in/s. Unit string `"in./s"` embedded inline. `0x082A` removed as trigger level candidate. |
|
| 2026-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-02-25 | Appendix A | **UPDATED:** v0.4.0 — annotation markers added. `.bin` sentinel format documented. Parser caveat added for SUB `5A` raw ADC payloads. |
|
| 2026-03-01 | §7.6 Channel Config Float Layout | **UPGRADED:** Alarm level offset fully confirmed via controlled capture (alarm 1.0→2.0, trigger 0.5→0.6). Complete per-channel layout documented. Three-channel repetition confirmed (Tran, Vert, Long). Certainty upgraded to ✅ CONFIRMED. |
|
||||||
|
| 2026-03-01 | §7.7 `.set` File Format | **NEW SECTION:** Blastware save-to-disk format decoded. Little-endian binary struct matching wire protocol payload. Full per-channel block layout mapped. Record time confirmed as uint32 at +16. MicL unit string confirmed as `"psi\0"`. `0x082A` mystery noted — not obviously record time, needs one more capture to resolve. |
|
||||||
|
| 2026-03-02 | §7.4 Event Index Block | **CONFIRMED:** Backlight and power save offsets independently confirmed via device-set capture (backlight=100=0x64 at +75, power-save=30=0x1E at +83). On-device change visible in S3→BW read response — no Blastware write involved. Offsets are ✅ CONFIRMED. |
|
||||||
|
| 2026-03-02 | §7.4 Event Index Block | **NEW:** `Monitoring LCD Cycle` identified at offsets +84/+85 as uint16 BE. Default value = 65500 (0xFFDC) = effectively disabled / maximum. Confirmed from operator manual §3.13.1g. |
|
||||||
|
| 2026-03-02 | §7.4 Event Index Block | **UPDATED:** Backlight confirmed as uint8 range 0–255 seconds per operator manual §3.13.1e ("adjustable timer, 0 to 255 seconds"). Power save unit confirmed as minutes per operator manual §3.13.1f. |
|
||||||
|
| 2026-03-02 | Global | **NEW SOURCE:** Operator manual (716U0101 Rev 15) added as reference. Cross-referencing settings definitions, ranges, and units. Header updated. |
|
||||||
|
| 2026-03-02 | §14 Open Questions | Float 6.2061 in/s mystery: manual confirms only two geo ranges (1.25 in/s and 10.0 in/s). 6.2061 is NOT a user-selectable range → 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. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -332,15 +341,41 @@ Unit 2: serial="BE11529" trail=70 11 firmware=S337.17
|
|||||||
|
|
||||||
### 7.4 Event Index Response (SUB F7) — 0x58 bytes
|
### 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 +00: 00 58 09 — Total index size or record count ❓
|
||||||
Offset 0x03: 00 00 00 01 — Possibly stored event count = 1 ❓
|
Offset +03: 00 00 00 01 — Possibly stored event count = 1 ❓
|
||||||
Offset 0x07: 01 07 CB 00 06 1E — Timestamp of event 1 (see §8)
|
Offset +07: 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 +0D: 01 07 CB 00 14 00 — Timestamp of event 2 (see §8)
|
||||||
Offset 0x13: 00 00 00 17 3B — Unknown ❓
|
Offset +13: 00 00 00 17 3B — Unknown ❓
|
||||||
Offset 0x50: 10 02 FF DC — Sub-block pointer or data segment header ❓
|
Offset +4B: [backlight] — BACKLIGHT ON TIME ✅ CONFIRMED
|
||||||
|
Offset +4C: 00 — padding (backlight is uint8, not uint16)
|
||||||
|
Offset +53: [power_save] — POWER SAVING TIMEOUT ✅ CONFIRMED
|
||||||
|
Offset +54: [lcd_hi] [lcd_lo] — MONITORING LCD CYCLE (uint16 BE) ✅ CONFIRMED
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Offset | Size | Type | Known values | Meaning | Certainty |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| +4B | 1 | uint8 | 250, 100 | **BACKLIGHT ON TIME** (0–255 seconds per manual) | ✅ CONFIRMED |
|
||||||
|
| +4C | 1 | — | 0x00 | Padding / high byte of potential uint16 | 🔶 INFERRED |
|
||||||
|
| +53 | 1 | uint8 | 10, 30 | **POWER SAVING TIMEOUT** (minutes) | ✅ CONFIRMED |
|
||||||
|
| +54..+55 | 2 | uint16 BE | 0xFFDC = 65500 | **MONITORING LCD CYCLE** (seconds; 65500 ≈ disabled/max) | ✅ CONFIRMED |
|
||||||
|
|
||||||
|
**Confirmation captures:**
|
||||||
|
|
||||||
|
| Capture | Backlight (+4B) | Power Save (+53) | LCD Cycle (+54/55) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `20260301_160702` (BW-written) | `0xFA` = 250 | `0x0A` = 10 min | `0xFF 0xDC` = 65500 |
|
||||||
|
| `20260302_144606` (device-set) | `0x64` = 100 | `0x1E` = 30 min | `0xFF 0xDC` = 65500 |
|
||||||
|
|
||||||
|
> 📖 **Manual cross-reference (716U0101 Rev 15, §3.13.1):**
|
||||||
|
> - Backlight On Time: "adjustable timer, from 0 to 255 seconds" (§3.13.1e)
|
||||||
|
> - Power Saving Timeout: "automatically turns the Minimate Plus off" — stored in minutes (§3.13.1f)
|
||||||
|
> - Monitoring LCD Cycle: "cycles off for the time period... set to zero to turn off" — 65500 = effectively disabled (§3.13.1g)
|
||||||
|
|
||||||
### 7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages
|
### 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.
|
> ✅ **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.
|
||||||
@@ -383,27 +418,106 @@ Confirmed ASCII strings extracted from payload:
|
|||||||
|
|
||||||
### 7.6 Channel Config Float Layout (SUB E5 / SUB 71)
|
### 7.6 Channel Config Float Layout (SUB E5 / SUB 71)
|
||||||
|
|
||||||
> ✅ **CONFIRMED — 2026-02-26** from trigger level change capture (session `193237`). Trigger changed `0.500 → 0.200 in/s`, alarm level independently read as `1.0 in/s`.
|
> ✅ **CONFIRMED — 2026-03-01** from controlled captures (sessions `193237` and `151147`). Trigger changed `0.500 → 0.200`, then `0.200 → 0.600`. Alarm changed `1.0 → 2.0`. All positions confirmed.
|
||||||
|
|
||||||
The SUB `1A` read response (`E5`) and SUB `71` write block contain per-channel threshold and scaling values packed as **IEEE 754 big-endian floats**, with an inline unit string:
|
The SUB `1A` read response (`E5`) and SUB `71` write block contain per-channel threshold and scaling values packed as **IEEE 754 big-endian floats**, with inline unit strings. This layout repeats **once per geophone channel** (Tran, Vert, Long — 3×):
|
||||||
|
|
||||||
```
|
```
|
||||||
[max_range float] [trigger float] ["in.\0"] [alarm float] ["/s\0\0"]
|
[00 00] [max_range float] [00 00] [trigger float] ["in.\0"] [alarm float] ["/s\0\0"] [00 01] [chan_label...]
|
||||||
40 C6 97 FD 3E 4C CC CD 69 6E 2E 3F 80 00 00 2F 73 00 00
|
40 C6 97 FD 3F 19 99 9A 69 6E 2E 40 00 00 00 2F 73 00 00
|
||||||
= 6.206 = 0.200 in/s "in." = 1.000 in/s "/s"
|
= 6.206 = 0.600 in/s "in." = 2.000 in/s "/s"
|
||||||
```
|
```
|
||||||
|
|
||||||
| Float | Value observed | Meaning | Certainty |
|
| Field | Example bytes | Decoded | Certainty |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `40 C6 97 FD` | 6.206 | Maximum range (likely full-scale ADC range in in/s) | 🔶 INFERRED |
|
| `[00 00]` | `00 00` | Separator / padding | 🔶 INFERRED |
|
||||||
| `3E 4C CC CD` | 0.200 | **Geophone trigger level** — changed `0.500 → 0.200` in capture | ✅ CONFIRMED |
|
| Max range float | `40 C6 97 FD` | 6.206 — full-scale range in in/s | 🔶 INFERRED |
|
||||||
| `3F 80 00 00` | 1.000 | **Geophone alarm level** — matched UI value of 1.0 in/s | ✅ CONFIRMED |
|
| `[00 00]` | `00 00` | Separator / padding | 🔶 INFERRED |
|
||||||
|
| **Trigger level** | `3F 19 99 9A` | **0.600 in/s** — IEEE 754 BE float | ✅ CONFIRMED |
|
||||||
|
| Unit string | `69 6E 2E 00` | `"in.\0"` | ✅ CONFIRMED |
|
||||||
|
| **Alarm level** | `40 00 00 00` | **2.000 in/s** — IEEE 754 BE float | ✅ CONFIRMED |
|
||||||
|
| Unit string | `2F 73 00 00` | `"/s\0\0"` | ✅ CONFIRMED |
|
||||||
|
| `[00 01]` | `00 01` | Unknown flag / separator | 🔶 INFERRED |
|
||||||
|
| Channel label | e.g. `56 65 72 74` | `"Vert"` — identifies which channel | ✅ CONFIRMED |
|
||||||
|
|
||||||
Unit strings `"in.\0"` and `"/s\0\0"` are embedded inline between the floats, confirming values are stored natively in **imperial units (in/s)** regardless of display locale.
|
**State transitions observed across captures:**
|
||||||
|
|
||||||
> ❓ **`0x082A` (= 2090)** — appears in the same block but did not change when trigger or alarm level was adjusted. Previous hypothesis that it was the trigger level is incorrect. Possibly record time, sample count, or a different threshold. Needs a targeted capture changing a known integer field to identify.
|
| Capture | Trigger | Alarm | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `193237` (read) | `3F000000` = 0.500 | `3F800000` = 1.000 | Device state before any change |
|
||||||
|
| `193237` (write 1) | `3E4CCCCD` = 0.200 | `3F800000` = 1.000 | Trigger changed only |
|
||||||
|
| `151147` (write 1) | `3E4CCCCD` = 0.200 | `40000000` = 2.000 | Alarm changed, trigger carried over |
|
||||||
|
| `151147` (write 2) | `3F19999A` = 0.600 | `40000000` = 2.000 | Trigger changed, alarm carried over |
|
||||||
|
|
||||||
|
Values are stored natively in **imperial units (in/s)** — unit strings `"in."` and `"/s"` embedded inline confirm this regardless of display locale.
|
||||||
|
|
||||||
|
> ❓ **`0x082A` (= 2090)** — appears in the same block but did not change when trigger or alarm level was adjusted. Possibly record time, sample count, or a different threshold. Needs a targeted capture changing a known integer field to identify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.7 Blastware `.set` File Format
|
||||||
|
|
||||||
|
> 🔶 **INFERRED — 2026-03-01** from `Standard_Recording_Setup.set` cross-referenced against known wire payloads.
|
||||||
|
|
||||||
|
Blastware's "save setup to disk" feature produces a binary `.set` file that is structurally identical to the wire protocol payload, but with **all multi-byte values in little-endian byte order** (Windows-native) rather than the big-endian order used on the wire. No DLE framing, no checksums — raw struct dump.
|
||||||
|
|
||||||
|
**File layout (2522 bytes observed):**
|
||||||
|
|
||||||
|
```
|
||||||
|
0x0000 Header / metadata block (~40 bytes) — partially decoded
|
||||||
|
0x002A "Standard Recording Setup.set\0" — setup filename, null-padded
|
||||||
|
0x0078 Project strings block — same layout as SUB 71 wire payload
|
||||||
|
"Project:\0" + value, "Client:\0" + value, "User Name:\0" + value,
|
||||||
|
"Seis Loc:\0" + value, "Extended Notes\0" + value
|
||||||
|
0x06A0 Channel records block — one record per channel (geo×3 + mic×1 + duplicates)
|
||||||
|
0x0820 Device info block — serial number, firmware, model strings
|
||||||
|
0x08C0 Event index / timestamp block
|
||||||
|
0x0910 Histogram / reporting config
|
||||||
|
0x09D0 Trailer (10 bytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Per-channel record layout (little-endian, ~46 bytes per channel):**
|
||||||
|
|
||||||
|
```
|
||||||
|
offset size type value (Tran example) meaning
|
||||||
|
+00 2 uint16 0x0001 channel type (1=geophone, 0=mic)
|
||||||
|
+02 4 char[4] "Tran" channel label
|
||||||
|
+06 2 uint16 0x0000 padding
|
||||||
|
+08 2 uint16 0x0001 unknown
|
||||||
|
+0A 2 uint16 0x0050 = 80 unknown (sensitivity? gain?)
|
||||||
|
+0C 2 uint16 0x000F = 15 unknown
|
||||||
|
+0E 2 uint16 0x0028 = 40 unknown
|
||||||
|
+10 2 uint16 0x0015 = 21 unknown
|
||||||
|
+12 4 bytes 03 02 04 01 flags (recording mode etc.)
|
||||||
|
+16 4 uint32 0x00000003 record time in seconds ✅ CONFIRMED
|
||||||
|
+1A 4 float32 6.2061 max range (in/s for geo, psi for mic)
|
||||||
|
+1E 2 00 00 padding
|
||||||
|
+20 4 float32 0.6000 trigger level ✅ CONFIRMED
|
||||||
|
+24 4 char[4] "in.\0" / "psi\0" unit string (geo vs mic)
|
||||||
|
+28 4 float32 2.0000 alarm level ✅ CONFIRMED
|
||||||
|
+2C 4 char[4] "/s\0\0" / varies unit string 2
|
||||||
|
```
|
||||||
|
|
||||||
|
**MicL channel differences:**
|
||||||
|
- `channel_type` = 0 (vs 1 for geophones)
|
||||||
|
- trigger = 0.009, alarm = 0.021 (in psi)
|
||||||
|
- unit string = `"psi\0"` instead of `"in.\0"` — **confirms MicL units are psi** ✅
|
||||||
|
|
||||||
|
**Endianness summary:**
|
||||||
|
|
||||||
|
| Context | Byte order | Example (0.6 in/s trigger) |
|
||||||
|
|---|---|---|
|
||||||
|
| `.set` file | Little-endian | `9A 99 19 3F` |
|
||||||
|
| Wire protocol (SUB 71 / E5) | Big-endian | `3F 19 99 9A` |
|
||||||
|
|
||||||
|
> ❓ **`0x082A`** — still unidentified. Record time in the `.set` file = `0x00000003` (3 sec), which would be `00 00 00 03` on wire — not `0x082A`. The original sessions had record time = 2, which would be `00 00 00 02`. `0x082A` = 2090 doesn't match any obvious record time encoding. May correspond to one of the unknown uint16 fields at +0A through +10. A capture changing sample rate or histogram interval would help isolate it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.5 Full Waveform Record (SUB F3) — 0xD2 bytes × 2 pages
|
||||||
|
|
||||||
|
Peak values as IEEE 754 big-endian floats (restored section header):
|
||||||
|
|
||||||
> 🔶 **Pending confirmation:** Alarm level identification is based on value match (`3F 80 00 00` = 1.0 = UI value). A capture changing the alarm level will confirm the exact byte offset.
|
|
||||||
```
|
```
|
||||||
Tran: 3D BB 45 7A = 0.0916 (in/s — unit config dependent)
|
Tran: 3D BB 45 7A = 0.0916 (in/s — unit config dependent)
|
||||||
Vert: 3D B9 56 E1 = 0.0907
|
Vert: 3D B9 56 E1 = 0.0907
|
||||||
@@ -632,7 +746,7 @@ Build in this order — each step is independently testable:
|
|||||||
| 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 Config | COM5 (Blastware) ↔ COM4 (Device), 38400 baud |
|
| Bridge Config | COM5 (Blastware) ↔ COM4 (Device), 38400 baud |
|
||||||
| Capture Tool | s3_bridge v0.4.0 (annotation markers, dual .log/.bin output) |
|
| Capture Tool | s3_bridge v0.4.0 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -640,13 +754,10 @@ Build in this order — each step is independently testable:
|
|||||||
|
|
||||||
## Appendix A — s3_bridge Capture Format
|
## Appendix A — s3_bridge Capture Format
|
||||||
> ✅ **CONFIRMED — 2026-02-26**
|
> ✅ **CONFIRMED — 2026-02-26**
|
||||||
> ⚠️ **Updated for v0.4.0 — annotation markers added.**
|
|
||||||
|
|
||||||
> ⚠️ **This behavior is not part of the Instantel protocol. It is an artifact of the bridge logger implementation.**
|
> ⚠️ **This behavior is not part of the Instantel protocol. It is an artifact of the bridge logger implementation.**
|
||||||
|
|
||||||
### A.1 Binary modifications
|
The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger makes one modification:
|
||||||
|
|
||||||
The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger makes one modification to frame data:
|
|
||||||
|
|
||||||
| Wire sequence | In .bin file | Notes |
|
| Wire sequence | In .bin file | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -660,55 +771,64 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
|
|||||||
|
|
||||||
> ⚠️ This means checksums cannot be verified on frames where the stuffed payload ends in `0x10` — that trailing `0x10` would normally be the DLE prefix of ETX, but the logger strips it, making the frame boundary ambiguous in that edge case. In practice this has not been observed in captured data.
|
> ⚠️ This means checksums cannot be verified on frames where the stuffed payload ends in `0x10` — that trailing `0x10` would normally be the DLE prefix of ETX, but the logger strips it, making the frame boundary ambiguous in that edge case. In practice this has not been observed in captured data.
|
||||||
|
|
||||||
### A.2 Annotation markers (v0.4.0+)
|
|
||||||
|
|
||||||
When the operator types `m` + Enter during a capture, both files receive a marker at that timestamp.
|
|
||||||
|
|
||||||
**`.log` format:**
|
|
||||||
```
|
|
||||||
[HH:MM:SS.mmm] >>> MARK: label text here
|
|
||||||
```
|
|
||||||
The `>>>` prefix never appears in frame log lines (which use `[direction]`) and is trivially skippable by a parser.
|
|
||||||
|
|
||||||
**`.bin` format — out-of-band sentinel:**
|
|
||||||
```
|
|
||||||
FF FF FF FF <len: 1 byte> <label: len bytes, UTF-8>
|
|
||||||
```
|
|
||||||
|
|
||||||
The four `0xFF` sentinel bytes are chosen because `0xFF` is not a valid byte in any Instantel framing position:
|
|
||||||
- Not a valid ACK (`0x41`), DLE (`0x10`), STX (`0x02`), or ETX (`0x03`)
|
|
||||||
- The `0xFF - SUB` response pattern produces values like `0xA4`, `0xEA`, `0xFE` — never a bare `0xFF` in the framing layer
|
|
||||||
|
|
||||||
**⚠️ Parser caveat — SUB `5A` raw ADC payloads:**
|
|
||||||
The sentinel assumption is robust for the framing layer, but the raw ADC sample data in SUB `5A` bulk waveform streams is less constrained. High-amplitude samples could theoretically produce `FF FF FF FF` within the data portion of a frame. **Do not scan the entire `.bin` file as a flat byte stream for sentinels.** Instead:
|
|
||||||
1. Parse frame boundaries first (walk `0x41` ACK → `0x10 0x02` STX → ... → bare `0x03` ETX)
|
|
||||||
2. Only scan for `FF FF FF FF` in the **gaps between frames** — sentinels are always written between complete frames, never mid-frame
|
|
||||||
3. Any `FF FF FF FF` appearing inside a frame boundary is ADC data, not a marker
|
|
||||||
|
|
||||||
Session start and end are automatically marked in both files.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Open Questions / Still Needs Cracking
|
## 14. Open Questions / Still Needs Cracking
|
||||||
|
|
||||||
| Question | Priority | Added |
|
| Question | Priority | Added | Notes |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 |
|
| 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 |
|
| `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 |
|
| 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 |
|
| 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 `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 |
|
| 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 |
|
| Full field mapping of SUB `1A` / response `E5` — channel scaling / compliance config block | MEDIUM | 2026-02-26 | |
|
||||||
| `0x082A` in channel config block — not trigger or alarm level. Possibly record time, sample count, or secondary threshold. Needs targeted capture. | 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 |
|
||||||
| Geophone alarm level float offset confirmation — value match suggests `3F 80 00 00` at known position, needs change capture to confirm. | LOW | 2026-02-26 |
|
| 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 | |
|
||||||
| Max range float `40 C6 97 FD` = 6.206 — meaning unclear. Screenshot shows "Normal 10.000 in/s" range setting. | LOW | 2026-02-26 |
|
| Full trigger configuration field mapping (SUB `1C` / write `82`) | LOW | 2026-02-26 | |
|
||||||
| 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 | |
|
||||||
| 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 | |
|
||||||
| 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 |
|
||||||
| MicL channel units — PSI, dB linear, or dB(L)? | LOW | 2026-02-26 |
|
| **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 | 0–255 seconds |
|
||||||
|
| Power Saving Timeout | §3.13.1f | Event Index +53 | uint8 | minutes (user sets 1–60+) |
|
||||||
|
| Monitoring LCD Cycle | §3.13.1g | Event Index +54/55 | uint16 BE | seconds; 0=off; 65500≈disabled |
|
||||||
|
| Trigger Level (Geo) | §3.8.6 | Channel block, float | float32 BE | 0.005–10.000 in/s |
|
||||||
|
| Alarm Level (Geo) | §3.9.9 | Channel block, float | float32 BE | higher than trigger level |
|
||||||
|
| Trigger Level (Mic) | §3.8.6 | Channel block, float | float32 BE | 100–148 dB in 1 dB steps |
|
||||||
|
| Alarm Level (Mic) | §3.9.10 | Channel block, float | float32 BE | higher than mic trigger |
|
||||||
|
| Record Time | §3.8.9 | `.set` +16 confirmed | uint32 | 1–500 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? | 1–9 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.*
|
*This is a living document — append changelog entries and timestamps as new findings are confirmed or corrected.*
|
||||||
|
|||||||
139
docs/instantel_protocol_session_summary_2-26_3-1.md
Normal file
139
docs/instantel_protocol_session_summary_2-26_3-1.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Instantel MiniMate Plus — RS-232 Protocol RE
|
||||||
|
**Session Summary: Chat Compacted 2026-03-01**
|
||||||
|
Device: MiniMate Plus S/N BE18189
|
||||||
|
FW S338.17 / DSP 10.72
|
||||||
|
Capture: 38400 baud, COM4/COM5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 1 — Protocol Foundations & Write Command Discovery
|
||||||
|
**2026-02-27**
|
||||||
|
|
||||||
|
## Frame Structure Confirmed
|
||||||
|
- DLE framing: `ACK (0x41)` + `DLE+STX (0x10 0x02)` … payload … checksum … `DLE+ETX (0x10 0x03)`
|
||||||
|
- DLE byte stuffing: `0x10` in payload → `0x10 0x10` on wire
|
||||||
|
- Checksum: 8-bit sum of de-stuffed payload bytes, mod 256
|
||||||
|
- Payload structure:
|
||||||
|
`CMD | DLE | ADDR | FLAGS | SUB | OFFSET_HI | OFFSET_LO | data…`
|
||||||
|
- All BW→S3 requests use `CMD=0x02`
|
||||||
|
- All responses use CMD matching the DLE prefix
|
||||||
|
- Response `SUB = 0xFF − Request SUB`
|
||||||
|
|
||||||
|
## Session Startup Sequence
|
||||||
|
Device boot prints ASCII **“Operating System”** before binary protocol mode.
|
||||||
|
|
||||||
|
Blastware init sequence:
|
||||||
|
1. POLL (SUB 5B)
|
||||||
|
2. Channel config (06)
|
||||||
|
3. Serial (15)
|
||||||
|
4. Full config (01)
|
||||||
|
5. Event index (08)
|
||||||
|
6. Event headers (1E)
|
||||||
|
7. Waveform records (0C)
|
||||||
|
8. Bulk stream (5A)
|
||||||
|
|
||||||
|
## Write Commands Discovered
|
||||||
|
|
||||||
|
| SUB (Req) | SUB (Resp) | Function |
|
||||||
|
|---|---|---|
|
||||||
|
| 0x71 | 0x8E | Trigger config write |
|
||||||
|
| 0x72 | 0x8D | Trigger config page 2 |
|
||||||
|
| 0x73 | 0x8C | Unknown write |
|
||||||
|
| 0x74 | 0x8B | Unknown write |
|
||||||
|
| 0x82 | 0x7D | Unknown write (post config) |
|
||||||
|
| 0x83 | 0x7C | Unknown write (terminal) |
|
||||||
|
| 0x68 | 0x97 | Event index write? |
|
||||||
|
| 0x09 | 0xF6 | Unknown read |
|
||||||
|
| 0x1A | 0xE5 | Unknown multi-page read |
|
||||||
|
| 0x2E | 0xD1 | Unknown short read |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 2 — Trigger & Alarm Level Floats
|
||||||
|
**2026-03-01 ~20:51**
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
- Trigger & alarm levels are IEEE‑754 single‑precision **big‑endian floats**
|
||||||
|
- Trigger level change verified (0.5 → 0.2 in/s)
|
||||||
|
- Alarm level verified (1.0 → 2.0 in/s)
|
||||||
|
- Unit strings embedded inline (`"psi"`, `"in./s"`)
|
||||||
|
- `0x082A` ruled out as trigger candidate
|
||||||
|
|
||||||
|
## SUB 71 Float Offsets
|
||||||
|
|
||||||
|
| Offset | Field | Value | Encoding |
|
||||||
|
|---|---|---|---|
|
||||||
|
| d[32..35] | MicL trigger | 0.0450 psi | IEEE754 BE |
|
||||||
|
| d[38..41] | MicL low thresh | 0.0100 psi | IEEE754 BE |
|
||||||
|
| d[46..49] | MicL alarm | 0.0210 psi | IEEE754 BE |
|
||||||
|
| d[42..44] | Units | psi\0 | ASCII |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 3 — Multi‑Parameter Capture
|
||||||
|
**2026-03-01 ~20:53**
|
||||||
|
|
||||||
|
| Parameter | Change | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| Alarm level | 2.0 in/s | Confirmed |
|
||||||
|
| Trigger level | 0.6 in/s | Confirmed |
|
||||||
|
| Record time | 3s | Confirmed |
|
||||||
|
| Sentinels | FF FF FF FF | Write boundaries confirmed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 4 — .set File Decode
|
||||||
|
**2026-03-01 ~20:55**
|
||||||
|
|
||||||
|
## .set Format
|
||||||
|
- Binary per‑channel structs
|
||||||
|
- Backlight field at **+0x0C**
|
||||||
|
- MicL units confirmed as **psi**
|
||||||
|
- Record time offset confirmed
|
||||||
|
|
||||||
|
Unknown uint16 fields:
|
||||||
|
- +0x0A = 80
|
||||||
|
- +0x0E = 40
|
||||||
|
- +0x10 = 21
|
||||||
|
|
||||||
|
## Backlight / Power Saving Tests
|
||||||
|
Changes tested:
|
||||||
|
- Backlight 15 → 30
|
||||||
|
- Power save 2 → 5
|
||||||
|
- Mic dB toggle
|
||||||
|
|
||||||
|
Result:
|
||||||
|
- SUB 71 frames identical
|
||||||
|
- No new writes after sentinels
|
||||||
|
- Device confirmed to support settings → offsets unknown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Current State — Pending Capture
|
||||||
|
|
||||||
|
Next capture targets:
|
||||||
|
- Backlight = 250 → search `0xFA`
|
||||||
|
- Power saving = 10 → search `0x0A`
|
||||||
|
- Possible encodings:
|
||||||
|
- uint16 BE
|
||||||
|
- uint32 BE
|
||||||
|
- Little‑endian variants
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Open Questions
|
||||||
|
|
||||||
|
| Question | Priority | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| Timestamp byte 3 | MEDIUM | Open |
|
||||||
|
| Serial response trailing bytes | MEDIUM | Open |
|
||||||
|
| Channel ID mapping | MEDIUM | Open |
|
||||||
|
| Write config coverage | MEDIUM | Partial |
|
||||||
|
| Backlight offsets | HIGH | Active |
|
||||||
|
| MicL units | LOW | Resolved |
|
||||||
|
| SUB 24/25 vs 5A | LOW | Open |
|
||||||
|
| 0x07E7 config field | LOW | Open |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
All findings reverse‑engineered from RS‑232 captures. No vendor docs used.
|
||||||
Reference in New Issue
Block a user