made everything modular
This commit is contained in:
0
app/gui/__init__.py
Normal file
0
app/gui/__init__.py
Normal file
108
app/gui/can_panel.py
Normal file
108
app/gui/can_panel.py
Normal 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
65
app/gui/dashboard.py
Normal 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
161
app/gui/trace.py
Normal 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
|
Reference in New Issue
Block a user