made everything modular

This commit is contained in:
2025-09-05 01:03:14 +02:00
parent 268dc201bf
commit 0276a3fb3c
21 changed files with 788 additions and 692 deletions

0
app/gui/__init__.py Normal file
View File

108
app/gui/can_panel.py Normal file
View File

@@ -0,0 +1,108 @@
# app/gui/can_panel.py
from __future__ import annotations
import json, subprocess, tkinter as tk
from tkinter import ttk, messagebox
from app.can import (
list_can_ifaces, link_up, link_down, link_state, link_kind, have_cap_netadmin
)
class CanPanel:
"""Fixes CAN- & Responder-Panel (oben links)."""
def __init__(self, parent, responder, initial_iface: str, initial_resp_id: int,
initial_timeout_ms: int, initial_bitrate: int, on_rebind_iface=None):
self.responder = responder
self.on_rebind_iface = on_rebind_iface or (lambda iface: None)
self.frame = ttk.LabelFrame(parent, text="CAN & Settings", padding=8)
for i in range(3): self.frame.columnconfigure(i, weight=1)
# Interface
ttk.Label(self.frame, text="Interface").grid(row=0, column=0, sticky="w")
self.iface_var = tk.StringVar(value=initial_iface)
self.iface_dd = ttk.Combobox(self.frame, textvariable=self.iface_var,
values=list_can_ifaces() or [initial_iface],
state="readonly", width=14)
self.iface_dd.grid(row=0, column=1, sticky="ew", padx=(6,0))
ttk.Button(self.frame, text="Refresh", command=self._refresh_ifaces).grid(row=0, column=2, sticky="w")
# RESP-ID
ttk.Label(self.frame, text="RESP-ID (hex)").grid(row=1, column=0, sticky="w")
self.resp_var = tk.StringVar(value=f"0x{initial_resp_id:03X}")
ttk.Entry(self.frame, textvariable=self.resp_var, width=10).grid(row=1, column=1, sticky="w", padx=(6,0))
# Timeout
ttk.Label(self.frame, text="Timeout (ms)").grid(row=2, column=0, sticky="w")
self.to_var = tk.IntVar(value=int(initial_timeout_ms))
ttk.Spinbox(self.frame, from_=10, to=5000, increment=10, textvariable=self.to_var, width=10)\
.grid(row=2, column=1, sticky="w", padx=(6,0))
# Bitrate
ttk.Label(self.frame, text="Bitrate").grid(row=3, column=0, sticky="w")
self.br_var = tk.IntVar(value=int(initial_bitrate))
ttk.Spinbox(self.frame, from_=20000, to=1000000, increment=10000, textvariable=self.br_var, width=12)\
.grid(row=3, column=1, sticky="w", padx=(6,0))
self.set_params = tk.BooleanVar(value=True)
ttk.Checkbutton(self.frame, text="Bitrate beim UP setzen", variable=self.set_params)\
.grid(row=3, column=2, sticky="w")
self.kind_label = ttk.Label(self.frame, text=f"Kind: {link_kind(initial_iface)}", style="Small.TLabel")
self.kind_label.grid(row=4, column=0, columnspan=3, sticky="w", pady=(4,0))
# Buttons
btns = ttk.Frame(self.frame); btns.grid(row=5, column=0, columnspan=3, sticky="ew", pady=(8,0))
ttk.Button(btns, text="Link UP", command=self._do_link_up).grid(row=0, column=0, sticky="w")
ttk.Button(btns, text="Link DOWN", command=self._do_link_down).grid(row=0, column=1, sticky="w", padx=(6,0))
ttk.Button(btns, text="Responder Rebind", command=self._do_rebind).grid(row=0, column=2, sticky="w", padx=(12,0))
ttk.Label(self.frame, text=f"CAP_NET_ADMIN: {'yes' if have_cap_netadmin() else 'no'}",
style="Small.TLabel").grid(row=6, column=0, columnspan=3, sticky="w", pady=(6,0))
# ---- actions ----
def _refresh_ifaces(self):
lst = list_can_ifaces()
if not lst:
messagebox.showwarning("Interfaces", "Keine can*/vcan* Interfaces gefunden.")
return
self.iface_dd.config(values=lst)
def _do_link_up(self):
iface = self.iface_var.get()
try:
self.kind_label.config(text=f"Kind: {link_kind(iface)}")
if link_state(iface) == "UP":
messagebox.showinfo("CAN", f"{iface} ist bereits UP"); return
link_up(iface, bitrate=self.br_var.get(), fd=False, set_params=self.set_params.get())
try:
out = subprocess.check_output(["ip","-details","-json","link","show",iface], text=True)
info = json.loads(out)[0]
bt = (info.get("linkinfo", {}) or {}).get("info_data", {}).get("bittiming") or {}
br = bt.get("bitrate"); sp = bt.get("sample-point")
if br:
messagebox.showinfo("CAN", f"{iface} ist UP @ {br} bit/s (SP {sp})")
except Exception:
pass
except Exception as e:
messagebox.showerror("CAN", f"Link UP fehlgeschlagen:\n{e}")
def _do_link_down(self):
iface = self.iface_var.get()
try:
if link_state(iface) == "DOWN":
messagebox.showinfo("CAN", f"{iface} ist bereits DOWN"); return
link_down(iface); messagebox.showinfo("CAN", f"{iface} ist DOWN")
except Exception as e:
messagebox.showerror("CAN", f"Link DOWN fehlgeschlagen:\n{e}")
def _do_rebind(self):
iface = self.iface_var.get()
try:
new_resp = int(self.resp_var.get(), 16)
except Exception:
messagebox.showerror("RESP-ID", "Bitte gültige Hex-Zahl, z.B. 0x7E8"); return
try:
self.responder.rebind(interface=iface, resp_id=new_resp, timeout_ms=self.to_var.get())
self.on_rebind_iface(iface) # TraceView umhängen
messagebox.showinfo("CAN", f"Responder neu gebunden: {iface}, RESP 0x{new_resp:03X}")
except Exception as e:
messagebox.showerror("CAN", f"Rebind fehlgeschlagen:\n{e}")

65
app/gui/dashboard.py Normal file
View File

@@ -0,0 +1,65 @@
# app/gui/dashboard.py
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
class DashboardView:
def __init__(self, parent, sim, refresh_ms: int = 250):
self.sim = sim
self.frame = ttk.LabelFrame(parent, text="Dashboard", padding=6)
self.refresh_ms = refresh_ms
cols = ("label", "value", "unit", "key", "module")
self.tree = ttk.Treeview(self.frame, columns=cols, show="headings")
for c, w in zip(cols, (160, 80, 40, 160, 80)):
self.tree.heading(c, text=c.capitalize())
self.tree.column(c, width=w, anchor="w")
self.tree.grid(row=0, 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=0, column=1, sticky="ns")
self.frame.columnconfigure(0, weight=1)
self.frame.rowconfigure(0, weight=1)
self._rows = {}
self._tick()
def _tick(self):
snap = self.sim.v.dashboard_snapshot()
specs = snap.get("specs", {})
values = snap.get("values", {})
# sort by priority then label
ordered = sorted(specs.values(), key=lambda s: (s.get("priority", 100), s.get("label", s["key"])))
seen_keys = set()
for spec in ordered:
k = spec["key"]; seen_keys.add(k)
label = spec.get("label", k)
unit = spec.get("unit", "") or ""
fmt = spec.get("fmt")
src = spec.get("source", "")
val = values.get(k, "")
if fmt and isinstance(val, (int, float)):
try:
val = format(val, fmt)
except Exception:
pass
row_id = self._rows.get(k)
row_vals = (label, val, unit, k, src)
if row_id is None:
row_id = self.tree.insert("", "end", values=row_vals)
self._rows[k] = row_id
else:
self.tree.item(row_id, values=row_vals)
# delete rows that disappeared
for k, rid in list(self._rows.items()):
if k not in seen_keys:
try: self.tree.delete(rid)
except Exception: pass
self._rows.pop(k, None)
try:
self.frame.after(self.refresh_ms, self._tick)
except tk.TclError:
pass

161
app/gui/trace.py Normal file
View File

@@ -0,0 +1,161 @@
# 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