From 4331215e23768b04b318af38a6ded4f462052ea6 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Tue, 21 Apr 2026 16:07:24 -0400 Subject: [PATCH] feat(protocol): enhance raw capture functionality and documentation updates - Update `s3_bridge.py` to default raw capture file paths to "auto" for timestamped naming. - Modify `gui_bridge.py` to pre-check raw capture options and streamline path handling. - Extend `ach_server.py` to save both incoming and outgoing raw bytes for analysis. - Revise `CHANGELOG.md` and `instantel_protocol_reference.md` to reflect changes in recording mode handling and compliance data encoding. --- CHANGELOG.md | 53 ++++++++++++++++++++ CLAUDE.md | 73 ++++++++++++++++++++++++---- bridges/ach_server.py | 52 ++++++++++++++------ bridges/gui_bridge.py | 63 +++++++++++++----------- bridges/s3-bridge/s3_bridge.py | 26 +++++++--- docs/instantel_protocol_reference.md | 3 +- 6 files changed, 207 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0bf491..609d889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,59 @@ All notable changes to seismo-relay are documented here. --- +## v0.12.5 — 2026-04-21 + +### Changed + +- **`s3_bridge.py` — raw captures always-on by default** — `--raw-bw` and `--raw-s3` now + default to `"auto"` instead of `None`. Every bridge session automatically generates + timestamped `raw_bw_.bin` and `raw_s3_.bin` files alongside the `.bin`/`.log` + session files. Pass `--raw-bw ""` (explicit empty string) to disable if needed. + +- **`gui_bridge.py` — raw capture checkboxes pre-checked** — Both "BW→S3 raw" and + "S3→BW raw" checkboxes start checked. Path fields are empty by default (bridge auto-names + the files). Unchecking a box passes `--raw-bw ""` to explicitly disable capture. + +- **`ach_server.py` — TX capture added (`raw_tx_.bin`)** — Every ACH inbound session + now saves both directions: `raw_rx_.bin` (device → us, S3 side, as before) and + `raw_tx_.bin` (us → device, BW side). Both files are usable in the Analyzer. + TX bytes are buffered in memory until startup handshake succeeds (same as RX), preventing + scanner probes from creating empty files. + +--- + +## v0.12.4 — 2026-04-21 (protocol analysis / docs only — no code changes) + +### Discovered + +- **compliance_raw is wire-encoded, not logical bytes** — `read_compliance_config()` returns + bytes that include DLE prefix bytes (`0x10`) before any `0x03` values (because S3FrameParser + preserves DLE+ETX inner-frame pairs as two literal bytes). The previous CLAUDE.md claim that + "S3FrameParser handles this transparently so compliance_raw contains logical bytes" was wrong. + +- **anchor-9 behavior per recording mode** (confirmed from 4-20-26 BW write captures): + - Single Shot (0x00) / Continuous (0x01): anchor-9 = `0x00` + - Histogram (0x03): anchor-9 = `0x10` — the E5 DLE prefix for the `0x03` recording_mode byte + - Histogram+Continuous (0x04): anchor-9 = `0x10` — an actual stored config byte for this mode + Anchor position shifts by ±1 when recording_mode = `0x03` due to the extra DLE byte; the + dynamic anchor search (`buf.find(ANCHOR, 0, 150)`) handles this correctly without code changes. + +- **Write frame ETX escaping** — BW escapes `0x03` bytes in write frame data as `0x10 0x03` + on the wire. Our `build_bw_write_frame` sends data bytes raw without ETX escaping. Device + accepts our raw writes for all tested modes. Hypothesis: device write parser uses the + offset/length field for frame boundaries, not ETX scanning, making ETX escaping optional. + Histogram mode (recording_mode = 0x03) write via SFM from a non-Histogram starting state + not yet tested. + +- **BW write payload vs E5 read payload are byte-identical** around the anchor region (confirmed + by comparing 3-11-26 BW TX and S3 captures). BW does NOT strip DLE prefix bytes before writing; + it round-trips the wire-encoded bytes verbatim with only the modified fields changed. + +- **Capture folder content catalogued** — see CLAUDE.md "BW capture reference" table for a + summary of all available protocol captures and their contents. + +--- + ## v0.12.3 — 2026-04-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index bdcf66d..96f1c69 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -386,7 +386,9 @@ bytes `\x01\x2c` = 300 (5-minute default histogram interval); changes when inter | Offset | Field | Format | Notes | |---|---|---|---| -| anchor − 8 | recording_mode | uint8 | **Same offset for both read and write.** The byte at anchor−7 is a `0x10` DLE marker regenerated by device firmware in every E5 response — do NOT overwrite it. Writing to anchor−7 causes anchor drift (+1 per write cycle). CORRECTION 2026-04-21: previous doc stated anchor−7 for write; empirically wrong. | +| anchor − 9 | mode_prefix | uint8 | `0x00` for Single Shot / Continuous; `0x10` for Histogram (DLE prefix in E5 encoding) and Histogram+Continuous (actual config byte). See "compliance_raw DLE encoding" note below. | +| anchor − 8 | recording_mode | uint8 | **Same offset for both read and write** — confirmed 2026-04-21. `_encode_compliance_config` writes `buf[anc-8]`. NOTE: for Histogram (0x03), E5 encodes the value as `0x10 0x03` so compliance_raw[anc-9]=0x10, compliance_raw[anc-8]=0x03. | +| anchor − 7 | constant | `0x10` | Always `0x10` in both E5 read and BW write payloads (not a DLE marker — it is part of the sample_rate field area). Do NOT overwrite. | | anchor − 6 | sample_rate | uint16 BE | same in read & write | | anchor − 4 | histogram_interval_sec | uint16 BE | seconds; same in read & write ✅ 2026-04-20 | | anchor − 2 | `0x00 0x00` | padding | | @@ -395,15 +397,42 @@ bytes `\x01\x2c` = 300 (5-minute default histogram interval); changes when inter **recording_mode enum** (confirmed 2026-04-20 from 4-20-26 captures): -| Value | Mode | -|---|---| -| `0x00` | Single Shot | -| `0x01` | Continuous | -| `0x02` | ❓ not observed | -| `0x03` | Histogram | -| `0x04` | Histogram + Continuous | +| Value | Mode | anchor-9 in compliance_raw | +|---|---|---| +| `0x00` | Single Shot | `0x00` | +| `0x01` | Continuous | `0x00` | +| `0x02` | ❓ not observed | ❓ | +| `0x03` | Histogram | `0x10` (DLE prefix from E5 wire encoding of 0x03) | +| `0x04` | Histogram + Continuous | `0x10` (actual config byte for this mode) | -**DLE escaping in write frames — CONFIRMED 2026-04-20:** Write frame data payloads DO escape `0x03` (ETX) bytes with a `0x10` DLE prefix. For histogram_interval = 900 (0x0384), the wire carries `10 03 84` — the `0x03` high byte is preceded by a DLE escape. After DLE destuffing (`10 XX → XX`), the logical field value is correctly `03 84` = 900. The CLAUDE.md claim that write frame data is "written RAW" was incorrect; at minimum ETX (0x03) bytes are escaped. S3FrameParser handles this transparently so the decoded `compliance_raw` always contains logical (destuffed) bytes. +**compliance_raw DLE encoding — IMPORTANT (confirmed 2026-04-21 from 4-20-26 captures):** +`compliance_raw` (returned by `read_compliance_config()`) is NOT purely logical bytes — it is +the wire-encoded representation where `0x03` bytes in the config are preceded by a `0x10` DLE +prefix (because S3FrameParser preserves DLE+ETX inner-frame pairs as two literal bytes). + +Consequences: +- When recording_mode = `0x03` (Histogram), `compliance_raw[anc-9] = 0x10` (DLE prefix) and + `compliance_raw[anc-8] = 0x03` (the value). The anchor position is +1 compared to modes + without `0x03` bytes before the anchor. +- For Histogram+Continuous (`0x04`), `compliance_raw[anc-9] = 0x10` for a different reason: + it is an actual stored config byte, not a DLE prefix. +- The anchor search (`buf.find(b'\xbe\x80\x00\x00\x00\x00', 0, 150)`) correctly locates + the anchor regardless of these mode-dependent shifts. +- When SFM writes recording_mode and round-trips the rest verbatim, the byte at `anc-9` is + preserved from the previous read. This means transitioning Histogram→other modes via SFM + leaves a `0x10` at `anc-9`. The device stores it as a literal byte; it does not affect + recording mode operation (which is at `anc-8`), but differs from what BW writes. This is a + known minor discrepancy that does not impact device behavior. +- **Histogram recording mode (0x03) write via SFM**: untested. When starting from a mode with + `anc-9 = 0x00`, SFM writes bare `0x03` at anc-8. BW would write `0x10 0x03`. Device likely + accepts both (write frames probably use offset/length for framing, not ETX scanning). + +**DLE escaping in write frames — confirmed 2026-04-20:** Blastware escapes `0x03` bytes in +write frame data as `0x10 0x03` on the wire (defensive ETX escaping). Our `build_bw_write_frame` +does NOT do this escaping — it sends data bytes raw. Device acceptance of bare `0x03` bytes +in write frame data is confirmed for the tested modes (Single Shot, Continuous, Histogram+Continuous +where `0x10 0x03` already appears from round-tripping). Histogram mode (bare `0x03` write from +non-Histogram starting state) has not been directly tested. ### SUB 0C — Waveform Record (210 bytes = data[11:11+0xD2]) @@ -1067,9 +1096,33 @@ body) because writing a dial string may require DLE escaping for embedded contro - **Database** — SQLite store for events + monitor log entries; dedup by key; queryable - **Histograms** — decode histogram-mode A5 data (noise floor tracking) +- **Blastware-compatible file output** — .MLG and binary waveform file generation matching Blastware format (needed for interoperability with existing workflows) - Compliance config encoder — build raw write payloads from a `ComplianceConfig` object +- **Test Histogram recording mode (0x03) write via SFM** — confirmed working for Single Shot / Continuous / Histogram+Continuous; Histogram (0x03) needs a live test from a non-Histogram starting state (bare 0x03 in write vs BW's DLE-escaped `10 03`) +- **Compliance write anchor-9 cleanup** — when changing recording_mode via SFM, the byte at anchor-9 is not explicitly managed. A spurious `0x10` may persist after Histogram→other mode transitions. Does not affect device operation but differs from BW's byte-perfect output. - Locate "Sensor Check" byte in compliance config (need capture with Disabled vs Before-monitoring) - Call Home — map time slots 3/4 offsets; add dial_string write support; confirm `modem_power_relay_enabled` - Modem manager — push RV50/RV55 configs via Sierra Wireless API - RV55 DCD/DTR issue — newer RV55 firmware doesn't assert DCD by default; units don't - resume monitoring after call-home disconnect (`--restart-monitoring` flag deferred) \ No newline at end of file + resume monitoring after call-home disconnect (`--restart-monitoring` flag deferred) + +## BW capture reference + +`bridges/captures/` contains the following BW TX + S3 response captures for protocol analysis: + +| Folder / File | Contents | +|---|---| +| `3-11-26/raw_bw_20260311_170151.bin` | Full compliance write + event download (SUBs 68→83 confirmed, frames 102–112) | +| `4-20-26/raw_bw_*_recording_mode_*.bin` | Recording mode changes: Continuous→Single Shot, →Histogram, →Histogram+Continuous | +| `4-20-26/histogram interval/` | Histogram interval changes: 1min, 5min, 15min, 15sec | +| `4-20-26/geo sensitivity/` | Geo sensitivity changes: 1.25 in/s (Sensitive), 10 in/s (Normal) | +| `4-20-26/call home settings/` | Call home config read/write captures | +| `4-8-26/` | Monitor status read, start/stop monitoring, SESSION_RESET signal, sensor check | +| `4-3-26-multi_event/` | Browse-mode S3 capture with 2+ events (1E/0A/1F iteration confirmed) | +| `4-2-26/` | Download-mode BW TX capture (5A bulk stream, POLL×3 requirement confirmed) | +| `3-31-26/` | Single-event download (148 BW / 147 S3 frames) | +| `mitm/ach_mitm_20260411_001912/` | Full ACH call-home MITM (erase protocol, 0xA3/0x06/0xA2 confirmed) | + +To parse BW TX captures: use `bridges/captures/` scripts or adapt the `find_write_frames()` pattern +in `/tmp/analyze_write_payload.py` — it correctly handles `0x10 0x03` DLE-escaped ETX bytes +inside write frame data (the naive parser terminates early at the escaped `0x03`). \ No newline at end of file diff --git a/bridges/ach_server.py b/bridges/ach_server.py index 65fb9e9..edd4f2e 100644 --- a/bridges/ach_server.py +++ b/bridges/ach_server.py @@ -35,6 +35,7 @@ Output per session device_info.json — serial number, firmware version, calibration date, etc. events.json — all events: timestamp, PPV per channel, peaks, metadata raw_rx_.bin — raw bytes from the device (S3 side) for Analyzer + raw_tx_.bin — raw bytes we sent to the device (BW side) for Analyzer session_.log — detailed protocol log What to look for @@ -172,16 +173,24 @@ class AchSession: transport = SocketTransport(self.sock, peer=self.peer) # Collect raw bytes in memory until startup succeeds, then flush to disk. - raw_buf: list[bytes] = [] - _orig_read = transport.read + raw_rx_buf: list[bytes] = [] # device → us (S3 side) + raw_tx_buf: list[bytes] = [] # us → device (BW side) + _orig_read = transport.read + _orig_write = transport.write def tapped_read(n: int) -> bytes: data = _orig_read(n) if data: - raw_buf.append(data) + raw_rx_buf.append(data) return data - transport.read = tapped_read # type: ignore[method-assign] + def tapped_write(data: bytes) -> None: + _orig_write(data) + if data: + raw_tx_buf.append(data) + + transport.read = tapped_read # type: ignore[method-assign] + transport.write = tapped_write # type: ignore[method-assign] serial: Optional[str] = None @@ -201,23 +210,35 @@ class AchSession: # Startup succeeded — this is a real unit. Create session dir now. session_dir = self.output_dir / f"ach_inbound_{ts}" session_dir.mkdir(parents=True, exist_ok=True) - log_path = session_dir / f"session_{ts}.log" - raw_path = session_dir / f"raw_rx_{ts}.bin" + log_path = session_dir / f"session_{ts}.log" + raw_rx_path = session_dir / f"raw_rx_{ts}.bin" # device → us (S3 side) + raw_tx_path = session_dir / f"raw_tx_{ts}.bin" # us → device (BW side) - # Flush buffered raw bytes to file and switch to direct file writes. - raw_fh = open(raw_path, "wb") - for chunk in raw_buf: - raw_fh.write(chunk) - raw_buf.clear() + # Flush buffered bytes to files and switch to direct file writes. + raw_rx_fh = open(raw_rx_path, "wb") + raw_tx_fh = open(raw_tx_path, "wb") + for chunk in raw_rx_buf: + raw_rx_fh.write(chunk) + for chunk in raw_tx_buf: + raw_tx_fh.write(chunk) + raw_rx_buf.clear() + raw_tx_buf.clear() def tapped_read_file(n: int) -> bytes: data = _orig_read(n) if data: - raw_fh.write(data) - raw_fh.flush() + raw_rx_fh.write(data) + raw_rx_fh.flush() return data - transport.read = tapped_read_file # type: ignore[method-assign] + def tapped_write_file(data: bytes) -> None: + _orig_write(data) + if data: + raw_tx_fh.write(data) + raw_tx_fh.flush() + + transport.read = tapped_read_file # type: ignore[method-assign] + transport.write = tapped_write_file # type: ignore[method-assign] # Wire up file handler now that the session dir exists. fh = logging.FileHandler(log_path, encoding="utf-8") @@ -530,7 +551,8 @@ class AchSession: log.warning(" [WARN] Failed to restart monitoring: %s", exc) finally: - raw_fh.close() + raw_rx_fh.close() + raw_tx_fh.close() client.close() # closes transport / socket cleanly root_logger.removeHandler(fh) fh.close() diff --git a/bridges/gui_bridge.py b/bridges/gui_bridge.py index c0ae686..7028baf 100644 --- a/bridges/gui_bridge.py +++ b/bridges/gui_bridge.py @@ -58,16 +58,24 @@ class BridgeGUI(tk.Tk): tk.Entry(self, textvariable=self.logdir_var, width=24).grid(row=1, column=3, sticky="we", **pad) tk.Button(self, text="Browse", command=self._choose_dir).grid(row=1, column=4, sticky="w", **pad) - # Row 2: Raw taps - self.raw_bw_var = tk.StringVar(value="") - self.raw_s3_var = tk.StringVar(value="") - tk.Checkbutton(self, text="Save BW->S3 raw", command=self._toggle_raw_bw, onvalue="1", offvalue="").grid(row=2, column=0, sticky="w", **pad) - tk.Entry(self, textvariable=self.raw_bw_var, width=28).grid(row=2, column=1, columnspan=3, sticky="we", **pad) - tk.Button(self, text="...", command=lambda: self._choose_file(self.raw_bw_var, "bw")).grid(row=2, column=4, **pad) + # Row 2: Raw taps — ON by default; "auto" = timestamped name; blank checkbox = disabled + self.raw_bw_enabled = tk.IntVar(value=1) + self.raw_s3_enabled = tk.IntVar(value=1) + # Path fields: empty means "auto" (bridge picks a timestamped name) + self.raw_bw_path_var = tk.StringVar(value="") + self.raw_s3_path_var = tk.StringVar(value="") - tk.Checkbutton(self, text="Save S3->BW raw", command=self._toggle_raw_s3, onvalue="1", offvalue="").grid(row=3, column=0, sticky="w", **pad) - tk.Entry(self, textvariable=self.raw_s3_var, width=28).grid(row=3, column=1, columnspan=3, sticky="we", **pad) - tk.Button(self, text="...", command=lambda: self._choose_file(self.raw_s3_var, "s3")).grid(row=3, column=4, **pad) + tk.Checkbutton(self, text="BW→S3 raw (auto)", variable=self.raw_bw_enabled, + command=self._toggle_raw_bw).grid(row=2, column=0, sticky="w", **pad) + tk.Entry(self, textvariable=self.raw_bw_path_var, width=28, + fg="grey").grid(row=2, column=1, columnspan=3, sticky="we", **pad) + tk.Button(self, text="...", command=lambda: self._choose_file(self.raw_bw_path_var, "bw")).grid(row=2, column=4, **pad) + + tk.Checkbutton(self, text="S3→BW raw (auto)", variable=self.raw_s3_enabled, + command=self._toggle_raw_s3).grid(row=3, column=0, sticky="w", **pad) + tk.Entry(self, textvariable=self.raw_s3_path_var, width=28, + fg="grey").grid(row=3, column=1, columnspan=3, sticky="we", **pad) + tk.Button(self, text="...", command=lambda: self._choose_file(self.raw_s3_path_var, "s3")).grid(row=3, column=4, **pad) # Row 4: Status + buttons self.status_var = tk.StringVar(value="Idle") @@ -102,13 +110,11 @@ class BridgeGUI(tk.Tk): var.set(filename) def _toggle_raw_bw(self) -> None: - if not self.raw_bw_var.get(): - # default name - self.raw_bw_var.set(os.path.join(self.logdir_var.get(), "raw_bw.bin")) + # Checkbox toggled — no path action needed; enabled state drives the flag. + pass def _toggle_raw_s3(self) -> None: - if not self.raw_s3_var.get(): - self.raw_s3_var.set(os.path.join(self.logdir_var.get(), "raw_s3.bin")) + pass def start_bridge(self) -> None: if self.process and self.process.poll() is None: @@ -126,23 +132,22 @@ class BridgeGUI(tk.Tk): args = [sys.executable, BRIDGE_PATH, "--bw", bw, "--s3", s3, "--baud", baud, "--logdir", logdir] - ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + # Raw tap flags. + # Checkbox on + empty path → pass "auto" (bridge generates timestamped name). + # Checkbox on + explicit path → pass that path. + # Checkbox off → pass "" to disable (overrides bridge's auto default). + raw_bw_explicit = self.raw_bw_path_var.get().strip() + raw_s3_explicit = self.raw_s3_path_var.get().strip() - raw_bw = self.raw_bw_var.get().strip() - raw_s3 = self.raw_s3_var.get().strip() + if self.raw_bw_enabled.get(): + args += ["--raw-bw", raw_bw_explicit if raw_bw_explicit else "auto"] + else: + args += ["--raw-bw", ""] # explicit disable - # If the user left the default generic name, replace with a timestamped one - # so each session gets its own file. - if raw_bw: - if os.path.basename(raw_bw) in ("raw_bw.bin", "raw_bw"): - raw_bw = os.path.join(os.path.dirname(raw_bw) or logdir, f"raw_bw_{ts}.bin") - self.raw_bw_var.set(raw_bw) - args += ["--raw-bw", raw_bw] - if raw_s3: - if os.path.basename(raw_s3) in ("raw_s3.bin", "raw_s3"): - raw_s3 = os.path.join(os.path.dirname(raw_s3) or logdir, f"raw_s3_{ts}.bin") - self.raw_s3_var.set(raw_s3) - args += ["--raw-s3", raw_s3] + if self.raw_s3_enabled.get(): + args += ["--raw-s3", raw_s3_explicit if raw_s3_explicit else "auto"] + else: + args += ["--raw-s3", ""] # explicit disable try: self.process = subprocess.Popen( diff --git a/bridges/s3-bridge/s3_bridge.py b/bridges/s3-bridge/s3_bridge.py index f3e1770..aa0ecac 100644 --- a/bridges/s3-bridge/s3_bridge.py +++ b/bridges/s3-bridge/s3_bridge.py @@ -390,8 +390,14 @@ def main() -> int: ap.add_argument("--s3", default="COM5", help="S3-side COM port (default: COM5)") ap.add_argument("--baud", type=int, default=38400, help="Baud rate (default: 38400)") ap.add_argument("--logdir", default=".", help="Directory to write session logs into (default: .)") - ap.add_argument("--raw-bw", default=None, help="Optional file to append raw bytes sent from BW->S3 (no headers)") - ap.add_argument("--raw-s3", default=None, help="Optional file to append raw bytes sent from S3->BW (no headers)") + ap.add_argument("--raw-bw", default="auto", + help="File to append raw bytes sent from BW->S3 (no headers). " + "Default 'auto' generates a timestamped name in --logdir. " + "Pass an empty string to disable.") + ap.add_argument("--raw-s3", default="auto", + help="File to append raw bytes sent from S3->BW (no headers). " + "Default 'auto' generates a timestamped name in --logdir. " + "Pass an empty string to disable.") ap.add_argument("--quiet", action="store_true", help="No console heartbeat output") ap.add_argument("--status-every", type=float, default=0.0, help="Seconds between console heartbeat lines (default: 0 = off)") args = ap.parse_args() @@ -414,12 +420,16 @@ def main() -> int: # If raw tap flags were passed without a path (bare --raw-bw / --raw-s3), # or if the sentinel value "auto" is used, generate a timestamped name. # If a specific path was provided, use it as-is (caller's responsibility). - raw_bw_path = args.raw_bw - raw_s3_path = args.raw_s3 - if raw_bw_path in (None, "", "auto"): - raw_bw_path = os.path.join(args.logdir, f"raw_bw_{ts}.bin") if args.raw_bw is not None else None - if raw_s3_path in (None, "", "auto"): - raw_s3_path = os.path.join(args.logdir, f"raw_s3_{ts}.bin") if args.raw_s3 is not None else None + # Resolve raw tap paths. + # "auto" (default) → timestamped file in logdir (always captured). + # Explicit path → use verbatim. + # None or "" → disabled (pass --raw-bw "" to suppress capture). + raw_bw_path: Optional[str] = args.raw_bw if args.raw_bw else None + raw_s3_path: Optional[str] = args.raw_s3 if args.raw_s3 else None + if raw_bw_path == "auto": + raw_bw_path = os.path.join(args.logdir, f"raw_bw_{ts}.bin") + if raw_s3_path == "auto": + raw_s3_path = os.path.join(args.logdir, f"raw_s3_{ts}.bin") logger = SessionLogger(log_path, bin_path, raw_bw_path=raw_bw_path, raw_s3_path=raw_s3_path) diff --git a/docs/instantel_protocol_reference.md b/docs/instantel_protocol_reference.md index 7b49bcd..fc211be 100644 --- a/docs/instantel_protocol_reference.md +++ b/docs/instantel_protocol_reference.md @@ -104,7 +104,8 @@ | 2026-04-11 | §7.11 (NEW) | **NEW — §7.11 Erase-All Protocol added.** Full wire sequence, SUB 0x06 storage range payload layout, post-erase key counter reset (resets to `0x01110000`). Confirmed from 4-11-26 MITM capture of live Blastware ACH session. | | 2026-04-11 | §14.6 | **RESOLVED — ACH Session Lifecycle is no longer "Future".** `bridges/ach_server.py` fully implements inbound ACH: POLL handshake, device info, event download. State tracked via `ach_state.json` (key-based, with `max_downloaded_key` for post-erase detection). `--clear-after-download` flag added for the standard delete-after-upload workflow. | | 2026-04-17 | §7.6.2, §14 | **RESOLVED — Float 6.206053 at channel_label+28 is the ADC-to-velocity scale factor.** Confirmed from Series III Interface Handbook §4.5 formula: `Range (×1) = 1.61133 V / Sensitivity (V/unit)`. For the standard Instantel geophone at Normal range (10.000 in/s): Sensitivity = 1.61133 / 10 = 0.161133 V/(in/s). The stored value is the **inverse sensitivity** = 1/0.161133 = **6.206053 (in/s)/V**. Cross-check: 1.61133 V × 6.206053 = 10.000 in/s ✅. The firmware uses it as: `PPV (in/s) = ADC_voltage (V) × 6.206053`. Value is identical on all Instantel standard geophones — it is a hardware/firmware constant, NOT a user-configurable setting. Do NOT write this field. Open question §14 item "Max Geo Range float 6.2061" is now **RESOLVED**. | -| 2026-04-20 | §7.6.4 (NEW), §7.9, Appendix B | **CONFIRMED — Recording Mode byte location.** Three targeted captures (4-20-26) confirmed `recording_mode` at **`cfg[5]`** in the SUB 71 write payload (3-chunk compliance write). Method: single Blastware session, one initial E5 config pull, then three sequential "Send to unit" writes changing Recording Mode only. Diff of SUB 71 chunk-1 payloads: only `cfg[5]` and `cfg[1024]` changed; `cfg[1024]` delta exactly equals `cfg[5]` delta (chunk running checksum). In the E5 read response (sub-frame 1, page=0x0010), the field is at **`data[17]`** (= **anchor − 4** from the 10-byte anchor), one position earlier than in the write payload due to an extra `0x10` byte at `data[18]` present only in the read format. Enum: `0x00`=Single Shot, `0x01`=Continuous, `0x03`=Histogram, `0x04`=Histogram+Continuous. `0x02` value not yet observed. See §7.6.4 for full details. | +| 2026-04-20 | §7.6.4 (NEW), §7.9, Appendix B | **CONFIRMED — Recording Mode byte location.** Three targeted captures (4-20-26) confirmed `recording_mode` at anchor−8 in both the E5 read payload and the BW write payload (6-byte anchor `\xbe\x80\x00\x00\x00\x00`). BW write payload and E5 read payload are **byte-identical** around the anchor region — Blastware round-trips the wire-encoded E5 bytes verbatim with only the target field modified. Anchor position varies by ±1 depending on whether recording_mode = 0x03 (Histogram), because E5 wire-encodes `0x03` as the inner DLE+ETX pair `\x10\x03` (2 bytes), which S3FrameParser preserves as two literal bytes in `compliance_raw`. Enum: `0x00`=Single Shot, `0x01`=Continuous, `0x03`=Histogram, `0x04`=Histogram+Continuous. `0x02` value not yet observed. The byte at anchor−9 is `0x00` for Single Shot / Continuous, and `0x10` for Histogram (DLE prefix from E5 encoding) and Histogram+Continuous (actual config byte). See §7.6.4 for full details. | +| 2026-04-21 | §7.6.2, §5.3 | **CORRECTED — compliance_raw contains wire-encoded bytes, NOT logical bytes.** S3FrameParser appends DLE+ETX inner-frame pairs as two literal bytes to the frame body. Any `0x03` values in the compliance config appear in `compliance_raw` as `\x10\x03` (two bytes), not as a single `0x03`. The previous claim "S3FrameParser handles this transparently so compliance_raw contains logical (destuffed) bytes" was wrong. Consequence: `compliance_raw` is the wire-encoded E5 payload; anchor-relative reads work correctly because the anchor position automatically accounts for any DLE-encoded bytes before it. For write-back, round-tripping `compliance_raw` verbatim sends the correct wire bytes to the device. **DLE ETX escaping in write frames:** Blastware escapes `0x03` bytes in write frame data as `\x10\x03` on wire; our `build_bw_write_frame` does not (writes data raw). Device is confirmed to accept raw writes for all tested modes — likely uses the offset/length field for write frame framing, not ETX scanning. | | 2026-04-20 | §7.6.2, §7.9, Appendix B | **CONFIRMED — Geophone maximum range / sensitivity selector byte location.** Two targeted captures (4-20-26, geo sensitivity folder): one at Normal 10.000 in/s, one at Sensitive 1.250 in/s. E5 read payload diff: exactly 3 bytes differ at channel_label+33 for Tran/Vert/Long. Values: `0x00`=Normal 10.000 in/s, `0x01`=Sensitive 1.250 in/s. Same offset applies to the SUB 71 write payload (which is the same 2126-byte E5-format buffer round-tripped verbatim). **`channel_label+20` reads `0x01` in ALL captures regardless of range setting — it is NOT this field.** Previous hypothesis (uint8 at Tran+20, 0x01=Normal) was WRONG. Stored as `geo_range` in `ComplianceConfig`. Encoded to all three geo channel blocks (Tran/Vert/Long) at label+33. | | 2026-04-20 | §5.1, §5.3, §7.12 (NEW) | **NEW — Auto Call Home config protocol confirmed from 4-20-26 call home settings captures.** SUB 0x2C (Call Home Config READ, response 0xD3, data offset 0x7C=124) and SUB 0x7E/0x7F (WRITE + CONFIRM, response 0x81/0x80) confirmed. Write payload = read payload (125 bytes) + `\x00\x00` (127 bytes total). **DLE-escaped ETX at raw[117:119]:** the device returns logical value 0x03 (num_retries=3) as `\x10\x03` on the wire — S3FrameParser preserves both bytes as two literals, causing a +1 byte shift for all subsequent fields. Write frame sends these bytes verbatim (device interprets `\x10\x03` as literal value 3). Field map confirmed from 10-frame BW TX diff. See §7.12 for full layout. |