From 0fbb39c21abf8f31f0e78d1045a89a0c02a18805 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 1 May 2026 18:37:34 -0400 Subject: [PATCH] Big event bugfix. see details: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## v0.13.0 — 2026-05-01 ### Fixed - **SUB 5A bulk waveform stream — over-read bug for events ≥ 2 sec.** `read_bulk_waveform_stream` was walking the chunk counter past the actual end of the event, picking up post-event circular-buffer garbage that corrupted reconstructed Blastware files for any waveform > ~1 sec. The loop now extracts the event's `end_offset` from the STRT record at `data[23:27]` of the probe response and stops the chunk walk when the next counter would step past it. Verified against three BW MITM captures (4-27-26 + 5-1-26): 2-sec event drops from 37 over-read chunks to 7 bounded chunks; 3-sec drops to 9; non-zero-start "event 2" drops to 9. ### Added - `framing.bulk_waveform_term_v2(key4, end_offset, last_chunk_counter)` — computes the corrected SUB 5A TERM frame's `(offset_word, params)` per the formula confirmed across all 3 BW captures. Not yet wired into `read_bulk_waveform_stream` (the legacy TERM is still used to preserve the existing `blastware_file.write_blastware_file` frame-structure expectations); available for the next iteration that switches to BW's 0x0200 chunk step. - `framing.parse_strt_end_offset(a5_data)` — extracts the event-end pointer from the STRT record in an A5 response payload. --- CHANGELOG.md | 54 ++++++++++++++ CLAUDE.md | 4 +- minimateplus/framing.py | 148 ++++++++++++++++++++++++++++++++++----- minimateplus/protocol.py | 55 ++++++++++++--- seismo_lab.py | 132 ++-------------------------------- 5 files changed, 235 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 609d889..3737924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,60 @@ All notable changes to seismo-relay are documented here. --- +## v0.13.0 — 2026-05-01 + +### Fixed + +- **SUB 5A bulk waveform stream — over-read bug for events ≥ 2 sec.** + `read_bulk_waveform_stream` was walking the chunk counter past the actual + end of the event, picking up post-event circular-buffer garbage that + corrupted reconstructed Blastware files for any waveform > ~1 sec. The + loop now extracts the event's `end_offset` from the STRT record at + `data[23:27]` of the probe response and stops the chunk walk when the next + counter would step past it. Verified against three BW MITM captures + (4-27-26 + 5-1-26): 2-sec event drops from 37 over-read chunks to 7 + bounded chunks; 3-sec drops to 9; non-zero-start "event 2" drops to 9. + +### Added + +- `framing.bulk_waveform_term_v2(key4, end_offset, last_chunk_counter)` — + computes the corrected SUB 5A TERM frame's `(offset_word, params)` per the + formula confirmed across all 3 BW captures. Not yet wired into + `read_bulk_waveform_stream` (the legacy TERM is still used to preserve the + existing `blastware_file.write_blastware_file` frame-structure expectations); + available for the next iteration that switches to BW's 0x0200 chunk step. +- `framing.parse_strt_end_offset(a5_data)` — extracts the event-end pointer + from the STRT record in an A5 response payload. + +### Documentation + +- **CLAUDE.md and `docs/instantel_protocol_reference.md` extensively + rewritten** to reflect the corrected SUB 5A protocol. See: + - CLAUDE.md "SUB 5A — chunk counter formula (REWRITTEN 2026-05-01)" + - CLAUDE.md "SUB 5A — STRT record encodes end_offset" + - CLAUDE.md "SUB 5A — TERM frame formula" + - CLAUDE.md "SUB 5A — fixed metadata pages 0x1002 and 0x1004" + - CLAUDE.md "SUB 0A — WAVEHDR response length distinguishes events from + boundaries" (0x46 = real event, 0x2C = boundary marker) + - protocol reference §7.8.5 / §7.8.6 / §7.8.7 / §7.8.8 +- The previous chunk-counter formula (`max(key4[2:4], 0x0400) + (chunk-1) * + 0x0400`) is now marked DEPRECATED and explicitly tagged WRONG with + pointers to the new sections, so future work doesn't re-derive it. + +### Known minor diffs vs Blastware (deferred to a follow-up) + +- We still use the OLD 0x0400 chunk step rather than BW's 0x0200; switching + also requires updating `blastware_file.write_blastware_file`'s skip values + and "extra chunk after metadata" logic, which depends on a fresh capture + to verify. +- We still use the legacy fixed `offset_word=0x005A` TERM frame rather than + BW's `end_offset - next_boundary` formula, for the same reason. +- Two fixed metadata pages at counter `0x1002` and `0x1004` are not yet + read explicitly; under the current 0x0400 walk their content is reachable + via the sample chunk that covers buffer addresses `[0x1000, 0x1400)`. + +--- + ## v0.12.5 — 2026-04-21 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 16089fe..240e96b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem -(Sierra Wireless RV50 / RV55). Current version: **v0.12.3**. +(Sierra Wireless RV50 / RV55). Current version: **v0.13.0**. When new information about the protocol is discovered, please update the instantel_protocol_reference.md with the findings in addition to this document @@ -41,7 +41,7 @@ Full read pipeline + write pipeline + erase pipeline + monitor log + call home c | Event header / first key | 1E | ✅ | | Waveform header | 0A | ✅ | | Waveform record (peaks, timestamp, project) | 0C | ✅ | -| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ new v0.6.0 | +| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ over-read bug fixed v0.13.0 (chunk loop bounded by STRT end_offset); minor wire diffs vs BW deferred — see "SUB 5A — chunk counter formula" | | Event advance / next key | 1F | ✅ | | **Write commands (push config to device)** | **68–83** | ✅ new v0.8.0 | | **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ new v0.9.0 | diff --git a/minimateplus/framing.py b/minimateplus/framing.py index 3adf4ce..2011cc9 100644 --- a/minimateplus/framing.py +++ b/minimateplus/framing.py @@ -123,8 +123,11 @@ def build_5a_frame(offset_word: int, raw_params: bytes) -> bytes: Returns: Complete frame bytes: [ACK][STX][stuffed_section][chk][ETX] """ - if len(raw_params) not in (10, 11): - raise ValueError(f"raw_params must be 10 or 11 bytes, got {len(raw_params)}") + if len(raw_params) not in (10, 11, 12): + # 10 = termination params; 11 = regular probe / chunk params; + # 12 = metadata-page params (extra trailing 0x00 — BW byte-perfect quirk + # for the two fixed metadata reads at counter=0x1002 and 0x1004). + raise ValueError(f"raw_params must be 10/11/12 bytes, got {len(raw_params)}") # Build stuffed section between STX and checksum s = bytearray() @@ -398,28 +401,21 @@ def bulk_waveform_params(key4: bytes, counter: int, *, is_probe: bool = False) - def bulk_waveform_term_params(key4: bytes, counter: int) -> bytes: """ - Build the 10-byte params block for the SUB 5A termination request. + DEPRECATED 2026-05-01 — see bulk_waveform_term_v2(). - The termination request uses offset=0x005A and a DIFFERENT params layout — - the leading 0x00 byte is dropped, key4[0:2] shifts to params[0:2], and the - counter high byte is at params[2]: + Build the 10-byte params block for the SUB 5A termination request, OLD layout + (used in conjunction with the fixed offset_word=0x005A). Kept for backward + compatibility — produces a tiny ~100-byte device-side terminator response + rather than the proper partial-last-chunk + footer payload that BW gets. params[0] = key4[0] params[1] = key4[1] params[2] = (counter >> 8) & 0xFF params[3:] = zeros - Counter for the termination request = last_regular_counter + 0x0400. - - Confirmed from 1-2-26 BW TX capture: final request (frame 83) uses - offset=0x005A, params[0:3] = key4[0:2] + term_counter_hi. - - Args: - key4: 4-byte waveform key. - counter: Termination counter (= last regular counter + 0x0400). - - Returns: - 10-byte params block. + Use bulk_waveform_term_v2() for new code — it computes the verified + offset_word + params from end_offset (extracted from STRT) and the last + chunk counter. """ if len(key4) != 4: raise ValueError(f"waveform key must be 4 bytes, got {len(key4)}") @@ -430,6 +426,123 @@ def bulk_waveform_term_params(key4: bytes, counter: int) -> bytes: return bytes(p) +def bulk_waveform_term_v2( + key4: bytes, + end_offset: int, + last_chunk_counter: int, +) -> tuple[int, bytes]: + """ + Compute the SUB 5A TERM frame's offset_word and 10-byte params block. + + Confirmed across 3 events (4-27-26 + 5-1-26 captures): + + next_boundary = last_chunk_counter + 0x0200 + offset_word = end_offset - next_boundary (residual byte count) + params[0] = key4[0] (= 0x01 on every observed device) + params[1] = key4[1] (= 0x11) + params[2] = (next_boundary >> 8) & 0xFF + params[3] = next_boundary & 0xFF + params[4:10] = zeros + + Verification: + | end_offset | last_chunk | next_boundary | offset_word | params[2:4] | + | 0x1ABE | 0x1800 | 0x1A00 | 0x00BE | 1A 00 | + | 0x21F2 | 0x1E00 | 0x2000 | 0x01F2 | 20 00 | + | 0x417E | 0x3E38 | 0x4038 | 0x0146 | 40 38 | + + The device receives `requested_address = (params[2] << 8) | offset_word` + and replies with `(end_offset - next_boundary)` bytes of waveform tail + starting at `next_boundary` — including the 26-byte file footer. + + Args: + key4: 4-byte waveform key for this event. + end_offset: Event-end pointer (= `(end_key[2] << 8) | end_key[3]` + from the STRT record at data[23:27] of A5[0]). + last_chunk_counter: Counter of the last full 0x0200-byte chunk fetched + (the chunk that covers [last_chunk_counter, + last_chunk_counter + 0x0200)). + + Returns: + (offset_word, params10) tuple. Pass as + `build_5a_frame(offset_word, params)`. + + Raises: + ValueError: on inconsistent inputs. + """ + if len(key4) != 4: + raise ValueError(f"waveform key must be 4 bytes, got {len(key4)}") + next_boundary = last_chunk_counter + 0x0200 + if next_boundary > 0xFFFF: + raise ValueError( + f"next_boundary 0x{next_boundary:04X} exceeds uint16; check inputs" + ) + if end_offset <= last_chunk_counter: + raise ValueError( + f"end_offset 0x{end_offset:04X} must be > " + f"last_chunk_counter 0x{last_chunk_counter:04X}" + ) + offset_word = end_offset - next_boundary + if offset_word < 0: + # Last chunk overshot end_offset; caller should have stopped one chunk + # earlier. Treat as zero residual. + offset_word = 0 + if offset_word > 0xFFFF: + raise ValueError( + f"offset_word 0x{offset_word:04X} exceeds uint16" + ) + p = bytearray(10) + p[0] = key4[0] + p[1] = key4[1] + p[2] = (next_boundary >> 8) & 0xFF + p[3] = next_boundary & 0xFF + return offset_word, bytes(p) + + +# ── End-offset extraction from STRT record ──────────────────────────────────── + +STRT_MARKER = b"STRT" + + +def parse_strt_end_offset(a5_data: bytes) -> Optional[int]: + """ + Extract the event-end offset from the STRT record in an A5 response payload. + + The first A5 response (the probe response, or the first chunk for events + with non-zero start_key[2:4]) contains a STRT record at byte offset 17 of + `data`. Layout: + + data[17:21] "STRT" + data[21:23] ff fe sentinel + data[23:27] end_key ← 4-byte key of where this event ENDS + data[27:31] start_key + ... + + Returns `(end_key[2] << 8) | end_key[3]` — the absolute device-buffer + address where the event ends. Use this to bound the chunk loop and to + compute the TERM frame. + + Verified end_offset values: + | event start_key | end_key | end_offset | + | 01110000 | 01111ABE | 0x1ABE | + | 01110000 | 011121F2 | 0x21F2 | + | 011121F2 | 0111417E | 0x417E | + + Args: + a5_data: The `data` field of an A5 response frame (frame.data). + + Returns: + The end_offset (uint16) if STRT is found, else None. + """ + pos = a5_data.find(STRT_MARKER) + if pos < 0 or pos + 10 > len(a5_data): + return None + # data[pos+4:pos+6] is "ff fe"; data[pos+6:pos+10] is end_key. + end_key = a5_data[pos + 6 : pos + 10] + if len(end_key) < 4: + return None + return (end_key[2] << 8) | end_key[3] + + # ── Pre-built POLL frames ───────────────────────────────────────────────────── # # POLL (SUB 0x5B) uses the same two-step pattern as all other reads — the @@ -470,7 +583,6 @@ class S3Frame: # ── Streaming S3 frame parser ───────────────────────────────────────────────── - class S3FrameParser: """ Incremental byte-stream parser for S3→BW response frames. diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index 0a69f93..2abeb1a 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -35,6 +35,8 @@ from .framing import ( token_params, bulk_waveform_params, bulk_waveform_term_params, + bulk_waveform_term_v2, + parse_strt_end_offset, POLL_PROBE, POLL_DATA, SESSION_RESET, @@ -122,16 +124,21 @@ DATA_LENGTHS: dict[int, int] = { } # SUB 5A (BULK_WAVEFORM_STREAM) protocol constants. -# Confirmed from 1-2-26 BW TX capture analysis (2026-04-02). -_BULK_CHUNK_OFFSET = 0x1004 # offset field for probe + all regular chunk requests ✅ -_BULK_TERM_OFFSET = 0x005A # offset field for termination request ✅ -_BULK_COUNTER_STEP = 0x0400 # chunk counter increment per chunk ✅ -# Chunk counter formula: key4[2:4] + (chunk_num - 1) * 0x0400 -# where key4[2:4] is the event's circular-buffer base offset ((key4[2]<<8)|key4[3]). -# Earlier captures showed 0x1004 for chunk 1 of key 01110000 — that was a Blastware -# artifact. For keys where key4[2:4] != 0x0000 (e.g. key 01111884) the old -# "n * 0x0400" formula sends counters from the wrong buffer region and the device -# returns data from a different event. Confirmed correct 2026-04-24. +# +# 2026-05-01 minimal-fix: the chunk-counter walk is now bounded by the event's +# `end_offset` extracted from the STRT record at data[23:27] of the probe +# response. Without this bound the loop kept asking for chunks past the event +# end and the device responded with post-event circular-buffer garbage, +# corrupting reconstructed Blastware files for events ≥ 2 sec. +# +# We keep the OLD 0x0400 chunk step here (BW actually uses 0x0200 — see §7.8.5 +# of the protocol reference for the corrected understanding) because the +# existing blastware_file.py builder relies on the 0x0400-step frame structure +# to produce valid files. Switching to BW's 0x0200 step is a separate task +# that also requires updating the file builder. +_BULK_CHUNK_OFFSET = 0x1004 # offset_word for probe + all chunk requests +_BULK_TERM_OFFSET = 0x005A # offset_word for the legacy terminator +_BULK_COUNTER_STEP = 0x0400 # chunk counter increment # Default timeout values (seconds). # MiniMate Plus is a slow device — keep these generous. @@ -610,6 +617,24 @@ class MiniMateProtocol: frames_data.append(rsp) log.debug("5A A5[0] page_key=0x%04X %d bytes", rsp.page_key, len(rsp.data)) + # ── Parse STRT end_offset from probe response (NEW 2026-05-01) ──────── + # The first A5 response contains a STRT record at data[17:]. The + # bytes at data[23:27] are the event's end-key, whose low 16 bits + # are the absolute device-buffer address where the event ends. Use + # this to bound the chunk loop and stop the over-read past event end. + # See docs/instantel_protocol_reference.md §7.8.5 and CLAUDE.md + # "SUB 5A — STRT record encodes end_offset". + _end_offset = parse_strt_end_offset(rsp.data) + if _end_offset is None: + # Defensive fallback — let max_chunks cap the walk. + log.warning("5A: STRT not found in probe; cannot bound chunk loop") + _end_offset = 0xFFFF + else: + log.debug( + "5A STRT start_offset=0x%04X end_offset=0x%04X size=0x%04X", + _key4_offset, _end_offset, _end_offset - _key4_offset, + ) + # ── Step 2: chunk loop ─────────────────────────────────────────────── # Counter formula: _chunk_base + (chunk_num - 1) * 0x0400 # where _chunk_base = max(key4[2:4], 0x0400). @@ -629,6 +654,16 @@ class MiniMateProtocol: _chunk_base = max(_key4_offset, _BULK_COUNTER_STEP) for chunk_num in range(1, max_chunks + 1): counter = _chunk_base + (chunk_num - 1) * _BULK_COUNTER_STEP + # Stop when we'd step past the event end (NEW 2026-05-01). Without + # this, the device returns post-event circular-buffer data which + # corrupts the reconstructed file for events ≥ 2 sec. + if counter >= _end_offset: + log.debug( + "5A chunk loop done at counter=0x%04X (end=0x%04X); " + "%d chunks fetched", + counter, _end_offset, len(frames_data), + ) + break params = bulk_waveform_params(key4, counter) log.debug("5A chunk %d counter=0x%04X", chunk_num, counter) self._send(build_5a_frame(_BULK_CHUNK_OFFSET, params)) diff --git a/seismo_lab.py b/seismo_lab.py index 723a921..f78e6a1 100644 --- a/seismo_lab.py +++ b/seismo_lab.py @@ -114,8 +114,6 @@ class BridgePanel(tk.Frame): on_capture_complete(bw_path, s3_path, label)— a capture segment finished """ - def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped, - on_capture_started=None, on_capture_complete=None, **kw): def __init__(self, parent: tk.Widget, on_bridge_started, on_bridge_stopped, on_capture_started=None, on_capture_complete=None, **kw): super().__init__(parent, bg=BG2, **kw) @@ -141,10 +139,6 @@ class BridgePanel(tk.Frame): self._cap_history: list[dict] = [] # {label, status, bw, s3} # mode self._mode = tk.StringVar(value="serial") - # Capture state - self._capturing = False - self._cap_label: Optional[str] = None - self._cap_history: list[dict] = [] # {label, status, bw, s3} self._build() self._poll_stdout() self._poll_tcp_log() @@ -246,18 +240,6 @@ class BridgePanel(tk.Frame): command=self._stop_capture, state="disabled") self.stop_cap_btn.pack(side=tk.LEFT, padx=4) - tk.Frame(btn_row, bg=BG2, width=16).pack(side=tk.LEFT) # spacer - - self.cap_btn = tk.Button(btn_row, text="⬤ New Capture", bg=ORANGE, fg="#000000", - relief="flat", padx=10, cursor="hand2", font=MONO_B, - command=self._start_capture, state="disabled") - self.cap_btn.pack(side=tk.LEFT, padx=4) - - self.stop_cap_btn = tk.Button(btn_row, text="■ Stop Capture", bg=BG3, fg=RED, - relief="flat", padx=10, cursor="hand2", font=MONO_B, - command=self._stop_capture, state="disabled") - self.stop_cap_btn.pack(side=tk.LEFT, padx=4) - self.mark_btn = tk.Button(btn_row, text="Add Mark", bg=BG3, fg=FG, relief="flat", padx=10, cursor="hand2", font=MONO, command=self.add_mark, state="disabled") @@ -319,7 +301,6 @@ class BridgePanel(tk.Frame): # Log output self.log_view = scrolledtext.ScrolledText( - self, height=14, font=MONO_SM, self, height=14, font=MONO_SM, bg=BG, fg=FG, insertbackground=FG, relief="flat", state="disabled", @@ -462,15 +443,12 @@ class BridgePanel(tk.Frame): self.start_btn.configure(state="disabled") self.stop_btn.configure(state="normal", bg=RED) self.cap_btn.configure(state="normal") - self.cap_btn.configure(state="normal") self._append_log(f"== Bridge started [{ts}] ==\n") self._append_log(" Click 'New Capture' when ready to record.\n") - self._on_started(struct_bin_path) - # Notify parent — no raw files yet, just the structured bin path self._on_started(struct_bin_path) - def stop_bridge(self) -> None: + def _stop_serial(self) -> None: if self.process and self.process.poll() is None: self.process.terminate() try: @@ -480,17 +458,6 @@ class BridgePanel(tk.Frame): self._bridge_ended() self._on_stopped() - def _bridge_ended(self) -> None: - self.status_var.set("Stopped") - self.start_btn.configure(state="normal") - self.stop_btn.configure(state="disabled", bg=BG3) - self.cap_btn.configure(state="disabled") - self.stop_cap_btn.configure(state="disabled", bg=BG3) - self.mark_btn.configure(state="disabled") - self._capturing = False - self._cap_label = None - self._append_log("== Bridge stopped ==\n") - def _reader_thread(self) -> None: if not self.process or not self.process.stdout: return @@ -531,9 +498,7 @@ class BridgePanel(tk.Frame): # ── capture control ─────────────────────────────────────────────────── def _start_capture(self) -> None: - """Ask for a label and tell the bridge to start writing raw tap files.""" - if not self.process or self.process.poll() is not None: - return + """Ask for a label and start writing raw tap files (serial subprocess or TCP files).""" label = simpledialog.askstring( "New Capture", "Label for this capture\n(e.g. 'recording_mode_continuous').\nLeave blank for timestamp only:", @@ -542,88 +507,6 @@ class BridgePanel(tk.Frame): if label is None: return # user hit Cancel label = label.strip() - try: - self.process.stdin.write(f"CAP_START:{label}\n") - self.process.stdin.flush() - except Exception as e: - messagebox.showerror("Error", f"Failed to start capture:\n{e}") - return - self._capturing = True - self._cap_label = label or datetime.datetime.now().strftime("%H%M%S") - self.cap_btn.configure(state="disabled") - self.stop_cap_btn.configure(state="normal", bg=RED) - self.mark_btn.configure(state="normal") - self._append_log(f"[CAPTURE] Starting: {self._cap_label!r}...\n") - # Add to history as recording (paths filled in when [CAP_START] arrives) - self._cap_history.append({"label": self._cap_label, "status": "recording", - "bw": None, "s3": None}) - self._refresh_hist() - - def _stop_capture(self) -> None: - """Tell the bridge to flush and close the current raw tap files.""" - if not self.process or self.process.poll() is not None: - return - try: - self.process.stdin.write("CAP_STOP\n") - self.process.stdin.flush() - except Exception as e: - messagebox.showerror("Error", f"Failed to stop capture:\n{e}") - # UI is updated when [CAP_STOP] arrives in stdout - - def _on_cap_started_msg(self, bw_path: str, s3_path: str) -> None: - """Called when bridge confirms capture has started (files are open).""" - # Fill in paths for the last 'recording' history entry - for entry in reversed(self._cap_history): - if entry["status"] == "recording" and entry["bw"] is None: - entry["bw"] = bw_path - entry["s3"] = s3_path - break - if self._on_cap_started: - self._on_cap_started(bw_path, s3_path, self._cap_label or "") - - def _on_cap_stopped_msg(self, bw_path: str, s3_path: str) -> None: - """Called when bridge confirms capture has stopped (files are closed).""" - label = self._cap_label or "capture" - # Mark history entry as done - for entry in reversed(self._cap_history): - if entry["status"] == "recording": - entry["status"] = "done" - entry["bw"] = bw_path - entry["s3"] = s3_path - break - self._refresh_hist() - self._capturing = False - self._cap_label = None - self.cap_btn.configure(state="normal") - self.stop_cap_btn.configure(state="disabled", bg=BG3) - self._append_log(f"[CAPTURE] Done: {label!r} — ready in Analyzer\n") - if self._on_cap_complete: - self._on_cap_complete(bw_path, s3_path, label) - - def _refresh_hist(self) -> None: - self._hist_lb.delete(0, tk.END) - for entry in self._cap_history: - icon = "🔴" if entry["status"] == "recording" else "✅" - label = entry["label"] or "(unlabeled)" - self._hist_lb.insert(tk.END, f" {icon} {label}") - if self._cap_history: - self._hist_lb.see(tk.END) - - def _on_hist_dblclick(self, _e=None) -> None: - sel = self._hist_lb.curselection() - if not sel: - return - entry = self._cap_history[sel[0]] - if entry["status"] == "done" and entry["bw"] and entry["s3"]: - if self._on_cap_complete: - self._on_cap_complete(entry["bw"], entry["s3"], entry["label"]) - - # ── mark ────────────────────────────────────────────────────────────── - - def add_mark(self) -> None: - if not self.process or not self.process.stdin or self.process.poll() is not None: - return - label = label.strip() self._capturing = True self._cap_label = label or datetime.datetime.now().strftime("%H%M%S") @@ -664,6 +547,7 @@ class BridgePanel(tk.Frame): self._append_log(f"[CAPTURE] Starting: {self._cap_label!r}...\n") def _stop_capture(self) -> None: + """Flush and close the current raw tap files (TCP) or signal the bridge subprocess (serial).""" if self._mode.get() == "tcp": with self._tcp_cap_lock: bw_path = self._tcp_cap_bw_path @@ -686,6 +570,7 @@ class BridgePanel(tk.Frame): self.process.stdin.flush() except Exception as e: messagebox.showerror("Error", f"Failed to stop capture:\n{e}") + # UI is updated when [CAP_STOP] arrives in stdout # ── TCP mode ────────────────────────────────────────────────────────── @@ -1173,14 +1058,6 @@ class AnalyzerPanel(tk.Frame): self.state.bw_path = bwp self._do_analyze(s3p, bwp) - def _browse_bin(self) -> None: - path = filedialog.askopenfilename( - title="Select session .bin file", - filetypes=[("Binary", "*.bin"), ("All files", "*.*")], - ) - if path: - self.bin_var.set(path) - def _do_analyze(self, s3_path: Path, bw_path: Path) -> None: self.status_var.set("Parsing...") self.update_idletasks() @@ -1611,7 +1488,6 @@ class AnalyzerPanel(tk.Frame): w.configure(state="normal"); w.insert(tk.END, "\n"); w.configure(state="disabled") -# ───────────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────────── # Serial Watch panel — tap the RS-232 line between device and modem # ─────────────────────────────────────────────────────────────────────────────