162 lines
6.2 KiB
Python
162 lines
6.2 KiB
Python
# 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
|