Big event bugfix. see details:

## 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.
This commit is contained in:
2026-05-01 18:37:34 -04:00
parent 738b39f3cb
commit 0fbb39c21a
5 changed files with 235 additions and 158 deletions
+4 -128
View File
@@ -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
# ─────────────────────────────────────────────────────────────────────────────