Files
OBD2-Simulator/app/gui/trace.py

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