#!/usr/bin/env python3 import re import sys import argparse from pathlib import Path import pandas as pd import numpy as np import matplotlib.pyplot as plt LOG_PATTERN = re.compile(r"(\d+)\s+(TX|RX)\s+0x([0-9A-Fa-f]+)\s+\d+\s+((?:[0-9A-Fa-f]{2}\s+)+)") def parse_trace(path: Path) -> pd.DataFrame: rows = [] with open(path, "r", errors="ignore") as f: for line in f: m = LOG_PATTERN.match(line) if not m: continue ts = int(m.group(1)) direction = m.group(2) can_id = int(m.group(3), 16) data = [int(x, 16) for x in m.group(4).split() if x.strip()] rows.append((ts, direction, can_id, data)) df = pd.DataFrame(rows, columns=["ts","dir","id","data"]) if df.empty: return df df["time_s"] = (df["ts"] - df["ts"].min())/1000.0 return df def be16(b): return (b[0]<<8) | b[1] def le16(b): return b[0] | (b[1]<<8) def main(): ap = argparse.ArgumentParser(description="Per-ID explorer: generate plots for 8-bit and 16-bit combinations") ap.add_argument("trace", help="Single-ID .trace file (from can_split_by_id.py)") ap.add_argument("--outdir", default=None, help="Output directory; default: _explore") ap.add_argument("--prefix", default="viz", help="File prefix for exports") ap.add_argument("--rx-only", action="store_true", help="Use only RX frames") args = ap.parse_args() trace = Path(args.trace) df = parse_trace(trace) if df.empty: print("No data in trace.", file=sys.stderr) sys.exit(1) if args.rx_only: df = df[df["dir"]=="RX"].copy() if df.empty: print("No RX frames.", file=sys.stderr) sys.exit(2) outdir = Path(args.outdir) if args.outdir else trace.with_suffix("").parent / (trace.stem + "_explore") outdir.mkdir(parents=True, exist_ok=True) # --- 8-bit channels --- for idx in range(8): vals = [d[idx] if len(d)>idx else None for d in df["data"].tolist()] times = [t for t, v in zip(df["time_s"].tolist(), vals) if v is not None] series = [v for v in vals if v is not None] if not series: continue plt.figure(figsize=(10,4)) plt.plot(times, series, marker=".", linestyle="-") plt.xlabel("Zeit (s)") plt.ylabel(f"Byte[{idx}] (8-bit)") plt.title(f"{trace.name} – 8-bit Byte {idx}") plt.grid(True) fn = outdir / f"{args.prefix}_byte{idx}.png" plt.tight_layout() plt.savefig(fn, dpi=150) plt.close() # --- 16-bit combos --- pairs = [(i,i+1) for i in range(7)] # LE for i,j in pairs: times, series = [], [] for t, d in zip(df["time_s"].tolist(), df["data"].tolist()): if len(d) > j: series.append(le16([d[i], d[j]])); times.append(t) if not series: continue plt.figure(figsize=(10,4)) plt.plot(times, series, marker=".", linestyle="-") plt.xlabel("Zeit (s)") plt.ylabel(f"LE16 @{i}-{j}") plt.title(f"{trace.name} – LE16 Bytes {i}-{j}") plt.grid(True) fn = outdir / f"{args.prefix}_le16_{i}-{j}.png" plt.tight_layout() plt.savefig(fn, dpi=150) plt.close() # BE for i,j in pairs: times, series = [], [] for t, d in zip(df["time_s"].tolist(), df["data"].tolist()): if len(d) > j: series.append(be16([d[i], d[j]])); times.append(t) if not series: continue plt.figure(figsize=(10,4)) plt.plot(times, series, marker=".", linestyle="-") plt.xlabel("Zeit (s)") plt.ylabel(f"BE16 @{i}-{j}") plt.title(f"{trace.name} – BE16 Bytes {i}-{j}") plt.grid(True) fn = outdir / f"{args.prefix}_be16_{i}-{j}.png" plt.tight_layout() plt.savefig(fn, dpi=150) plt.close() # Summary stats stats = [] # 8-bit stats for idx in range(8): vals = [d[idx] if len(d)>idx else None for d in df["data"].tolist()] vals = [v for v in vals if v is not None] if not vals: continue arr = np.array(vals, dtype=float) stats.append({"type":"byte8", "slot":idx, "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())}) # 16-bit stats for i,j in pairs: vals = [le16([d[i],d[j]]) for d in df["data"].tolist() if len(d)>j] if vals: arr = np.array(vals, dtype=float) stats.append({"type":"le16", "slot":f"{i}-{j}", "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())}) vals = [be16([d[i],d[j]]) for d in df["data"].tolist() if len(d)>j] if vals: arr = np.array(vals, dtype=float) stats.append({"type":"be16", "slot":f"{i}-{j}", "min":float(arr.min()), "max":float(arr.max()), "var":float(arr.var())}) pd.DataFrame(stats).to_csv(outdir / "summary_stats.csv", index=False) print(f"Exported 8-bit & 16-bit plots and summary_stats.csv to {outdir}") if __name__ == "__main__": main()