feat(seismo_lab): Inspector tab — annotated hex reader for Series-3 binaries

New top-level "Inspector" tab: open any Series-3 waveform binary and read it as
a colour-coded hex dump driven by binary_annotate. Each region is labelled with
its offset range and size (header / STRT / per-channel sample records / footer),
and everything the decoder can't account for is painted UNKNOWN (red) so gaps
stand out — the point being to comb for undecoded data (e.g. a stored FFT/
spectral block). A summary shows total size, region count, and % unknown.

Read-only reader/translator; Series-3 only for now (Series-4 later). The GUI
needs tkinter + a display (not available in the dev venv); the annotator core it
calls is unit-tested headless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
This commit is contained in:
2026-09-12 05:27:43 +00:00
co-authored by Claude Opus 4.8
parent 845ec38f96
commit 11e3e515f3
+93
View File
@@ -54,6 +54,7 @@ from s3_analyzer import ( # noqa: E402
write_claude_export, write_claude_export,
) )
from frame_db import FrameDB # noqa: E402 from frame_db import FrameDB # noqa: E402
from minimateplus.binary_annotate import annotate_blastware_binary # noqa: E402
# ── colour palette ──────────────────────────────────────────────────────────── # ── colour palette ────────────────────────────────────────────────────────────
BG = "#1e1e1e" BG = "#1e1e1e"
@@ -2675,6 +2676,95 @@ class DownloadPanel(tk.Frame):
self._on_capture_ready(bw_path, s3_path, label) self._on_capture_ready(bw_path, s3_path, label)
# ─────────────────────────────────────────────────────────────────────────────
# Inspector panel — annotated hex view of a Series-3 binary
# ─────────────────────────────────────────────────────────────────────────────
class InspectorPanel(tk.Frame):
"""Load any Series-3 waveform binary and read it as an annotated hex dump.
Regions the decoder understands (header, STRT, per-channel sample records,
footer) are labelled and colour-coded; everything the decoder cannot account
for is flagged UNKNOWN, so undecoded bytes stand out for hand-inspection.
"""
_KIND_COLOR = {
"header": ACCENT,
"strt": YELLOW,
"sample": COL_S3,
"footer": FG_DIM,
"unknown": RED,
}
def __init__(self, parent: tk.Widget, initialdir=None, **kw) -> None:
super().__init__(parent, bg=BG, **kw)
self._path = None
self._initialdir = initialdir
self._build()
def _build(self) -> None:
bar = tk.Frame(self, bg=BG2)
bar.pack(side=tk.TOP, fill=tk.X)
tk.Button(bar, text="Open binary…", command=self._open, bg=BG3, fg=FG,
relief=tk.FLAT, font=MONO, activebackground=ACCENT).pack(side=tk.LEFT, padx=6, pady=6)
self._path_var = tk.StringVar(value="(no file loaded)")
tk.Label(bar, textvariable=self._path_var, bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=6)
self._summary_var = tk.StringVar(value="")
tk.Label(bar, textvariable=self._summary_var, bg=BG2, fg=FG, font=MONO).pack(side=tk.RIGHT, padx=10)
legend = tk.Frame(self, bg=BG2)
legend.pack(side=tk.TOP, fill=tk.X)
tk.Label(legend, text="legend:", bg=BG2, fg=FG_DIM, font=MONO).pack(side=tk.LEFT, padx=(8, 2))
for kind, color in self._KIND_COLOR.items():
tk.Label(legend, text=f"■ {kind}", bg=BG2, fg=color, font=MONO).pack(side=tk.LEFT, padx=5, pady=2)
self._text = scrolledtext.ScrolledText(
self, bg=BG, fg=FG, insertbackground=FG, font=MONO, wrap=tk.NONE, borderwidth=0)
self._text.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
for kind, color in self._KIND_COLOR.items():
self._text.tag_configure(kind, foreground=color)
self._text.tag_configure("label", foreground="#ffffff", font=("Consolas", 9, "bold"))
self._text.tag_configure("dim", foreground=FG_DIM)
self._text.configure(state=tk.DISABLED)
def _open(self) -> None:
p = filedialog.askopenfilename(title="Open a Series-3 binary", initialdir=self._initialdir)
if p:
self.load(Path(p))
def load(self, path: Path) -> None:
try:
raw = path.read_bytes()
spans = annotate_blastware_binary(raw)
except Exception as e: # noqa: BLE001 — surface any read/annotate failure to the user
messagebox.showerror("Inspector", f"Failed to read/annotate:\n{path}\n\n{e}")
return
self._path = path
self._path_var.set(str(path))
self._render(raw, spans)
def _render(self, raw: bytes, spans) -> None:
t = self._text
t.configure(state=tk.NORMAL)
t.delete("1.0", tk.END)
unknown = sum(s.end - s.start for s in spans if s.kind == "unknown")
pct = 100 * unknown / max(1, len(raw))
self._summary_var.set(f"{len(raw)} B · {len(spans)} regions · {pct:.1f}% unknown")
for s in spans:
t.insert(tk.END, f"\n── {s.label} [0x{s.start:04x}:0x{s.end:04x}] {s.end - s.start} B ──\n", ("label",))
self._insert_hex(t, raw, s.start, s.end, s.kind)
t.configure(state=tk.DISABLED)
def _insert_hex(self, t: tk.Text, raw: bytes, start: int, end: int, kind: str) -> None:
for off in range(start, end, 16):
row = raw[off:min(off + 16, end)]
hx = " ".join(f"{b:02x}" for b in row).ljust(16 * 3 - 1)
txt = "".join(chr(b) if 32 <= b < 127 else "." for b in row)
t.insert(tk.END, f" 0x{off:04x} ", ("dim",))
t.insert(tk.END, hx, (kind,))
t.insert(tk.END, f" {txt}\n", ("dim",))
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Main application window # Main application window
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -2730,6 +2820,9 @@ class SeismoLab(tk.Tk):
) )
nb.add(self._download_panel, text=" Download ") nb.add(self._download_panel, text=" Download ")
self._inspector_panel = InspectorPanel(nb)
nb.add(self._inspector_panel, text=" Inspector ")
self._nb = nb self._nb = nb
self.protocol("WM_DELETE_WINDOW", self._on_close) self.protocol("WM_DELETE_WINDOW", self._on_close)