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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user