diff --git a/seismo_lab.py b/seismo_lab.py index 1986127..5cad795 100644 --- a/seismo_lab.py +++ b/seismo_lab.py @@ -54,6 +54,7 @@ from s3_analyzer import ( # noqa: E402 write_claude_export, ) from frame_db import FrameDB # noqa: E402 +from minimateplus.binary_annotate import annotate_blastware_binary # noqa: E402 # ── colour palette ──────────────────────────────────────────────────────────── BG = "#1e1e1e" @@ -2675,6 +2676,95 @@ class DownloadPanel(tk.Frame): 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 # ───────────────────────────────────────────────────────────────────────────── @@ -2730,6 +2820,9 @@ class SeismoLab(tk.Tk): ) nb.add(self._download_panel, text=" Download ") + self._inspector_panel = InspectorPanel(nb) + nb.add(self._inspector_panel, text=" Inspector ") + self._nb = nb self.protocol("WM_DELETE_WINDOW", self._on_close)