# app/gui/trace.py from __future__ import annotations import time, json, threading from collections import deque import tkinter as tk from tkinter import ttk, messagebox import can from app.can import list_can_ifaces, link_up, link_down, link_state, link_kind, have_cap_netadmin class TraceCollector: def __init__(self, channel: str): self.channel = channel self.bus = None self._run = threading.Event(); self._run.set() self._thread = threading.Thread(target=self._rx_loop, name="CAN-TRACE", daemon=True) self.stream_buffer = deque(maxlen=2000) self.lock = threading.Lock() def _open(self): self._close() self.bus = can.interface.Bus(channel=self.channel, interface="socketcan") def _close(self): try: if self.bus: self.bus.shutdown() except Exception: pass self.bus = None def start(self): self._thread.start() def stop(self): self._run.clear() try: self._thread.join(timeout=1.0) except RuntimeError: pass self._close() def _rx_loop(self): backoff = 0.5 while self._run.is_set(): if self.bus is None: if link_state(self.channel) == "UP": try: self._open(); backoff = 0.5 except Exception: time.sleep(backoff); backoff = min(5.0, backoff*1.7) continue else: time.sleep(0.5); continue try: msg = self.bus.recv(0.05) if msg and not msg.is_error_frame and not msg.is_remote_frame: ts = time.time() with self.lock: self.stream_buffer.append((ts, msg.arbitration_id, msg.dlc, bytes(msg.data))) except (can.CanOperationError, OSError): self._close(); time.sleep(0.5) except Exception: time.sleep(0.05) def snapshot_stream(self): with self.lock: return list(self.stream_buffer) class TraceView: def __init__(self, parent, responder, iface_initial: str): self.responder = responder self.collector = TraceCollector(iface_initial); self.collector.start() self.frame = ttk.LabelFrame(parent, text="CAN Trace", padding=6) self.frame.columnconfigure(0, weight=1) self.frame.rowconfigure(1, weight=1) # controls ctrl = ttk.Frame(self.frame); ctrl.grid(row=0, column=0, sticky="ew", pady=(0,4)) ctrl.columnconfigure(6, weight=1) self.mode_var = tk.StringVar(value="stream") ttk.Label(ctrl, text="Modus:").grid(row=0, column=0, sticky="w") ttk.Combobox(ctrl, textvariable=self.mode_var, state="readonly", width=10, values=["stream","aggregate"])\ .grid(row=0, column=1, sticky="w", padx=(4,12)) self.paused = tk.BooleanVar(value=False) ttk.Checkbutton(ctrl, text="Pause", variable=self.paused).grid(row=0, column=2, sticky="w") self.autoscroll = tk.BooleanVar(value=True) ttk.Checkbutton(ctrl, text="Auto-Scroll", variable=self.autoscroll).grid(row=0, column=3, sticky="w") ttk.Button(ctrl, text="Clear", command=self._clear).grid(row=0, column=4, padx=(8,0)) # tree self.tree = ttk.Treeview(self.frame, columns=("time","dir","id","dlc","data"), show="headings", height=10) self.tree.grid(row=1, column=0, sticky="nsew") sb_y = ttk.Scrollbar(self.frame, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=sb_y.set); sb_y.grid(row=1, column=1, sticky="ns") self._last_index = 0 self._tick() def _clear(self): self.tree.delete(*self.tree.get_children()) self._last_index = 0 def _tick(self): if not self.paused.get(): buf = self.collector.snapshot_stream() mode = self.mode_var.get() if mode == "stream": for ts, cid, dlc, data in buf[self._last_index:]: d = "RX" if cid == 0x7DF else ("TX" if cid == self.responder.resp_id else "?") self.tree.insert("", "end", values=(self._fmt_time(ts), d, self._fmt_id(cid), dlc, self._fmt_data(data))) if self.autoscroll.get() and buf[self._last_index:]: self.tree.see(self.tree.get_children()[-1]) else: self.tree.delete(*self.tree.get_children()) agg = {} for ts, cid, dlc, data in buf: d = "RX" if cid == 0x7DF else ("TX" if cid == self.responder.resp_id else "?") key = (cid, d) e = agg.get(key) if not e: agg[key] = {"count":1, "last_ts":ts, "last_dlc":dlc, "last_data":data} else: e["count"] += 1 if ts >= e["last_ts"]: e["last_ts"], e["last_dlc"], e["last_data"] = ts, dlc, data for (cid, d) in sorted(agg.keys(), key=lambda k:(k[0], 0 if k[1]=="RX" else 1)): e = agg[(cid, d)] self.tree.insert("", "end", values=(self._fmt_id(cid), d, e["count"], self._fmt_time(e["last_ts"]), e["last_dlc"], self._fmt_data(e["last_data"]))) self._last_index = len(buf) try: self.frame.after(50, self._tick) except tk.TclError: pass @staticmethod def _fmt_time(ts: float) -> str: import time as _t lt = _t.localtime(ts) return _t.strftime("%H:%M:%S", lt) + f".{int((ts%1)*1000):03d}" @staticmethod def _fmt_id(i: int) -> str: return f"0x{i:03X}" @staticmethod def _fmt_data(b: bytes) -> str: return " ".join(f"{x:02X}" for x in b) def rebind_interface(self, iface: str): # Collector auf neues Interface umhängen try: self.collector.stop() except Exception: pass self.collector = TraceCollector(iface) self.collector.start() def stop(self): try: self.collector.stop() except Exception: pass