feat: added raw binary data tracking for accurate format parser.
This commit is contained in:
@@ -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
|
||||
What’s 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,12 +389,11 @@ 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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(main())
|
||||
@@ -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 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. |
|
||||
|
||||
---
|
||||
|
||||
@@ -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** (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
|
||||
|
||||
> ✅ **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 | 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.*
|
||||
|
||||
Reference in New Issue
Block a user