starting to implement realistic Vehicle simulation
This commit is contained in:
439
app/gui.py
439
app/gui.py
@@ -1,31 +1,59 @@
|
||||
# gui.py — Tk-App mit Interface-Dropdown, Link Up/Down, Settings-View/Save + CAN-Trace
|
||||
# Project layout (drop-in)
|
||||
#
|
||||
# app/
|
||||
# ├─ gui.py ← new main GUI with Simulator tabs + Save/Load
|
||||
# ├─ config.py (unchanged)
|
||||
# ├─ can.py (unchanged)
|
||||
# ├─ obd2.py (unchanged; GUI registers PIDs)
|
||||
# ├─ tabs/
|
||||
# │ ├─ __init__.py
|
||||
# │ ├─ basic.py ← base/vehicle tab (ignition, mass, type, ABS/TCS)
|
||||
# │ ├─ engine.py ← engine tab
|
||||
# │ ├─ gearbox.py ← gearbox tab
|
||||
# │ └─ dtc.py ← DTC toggles tab
|
||||
# └─ simulation/
|
||||
# ├─ __init__.py
|
||||
# ├─ simulator_main.py ← VehicleSimulator wrapper used by GUI
|
||||
# ├─ vehicle.py ← core state + module orchestration
|
||||
# └─ modules/
|
||||
# ├─ __init__.py
|
||||
# ├─ engine.py
|
||||
# ├─ gearbox.py
|
||||
# └─ abs_.py
|
||||
|
||||
|
||||
# =============================
|
||||
# app/gui.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from collections import deque, defaultdict
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
from collections import deque
|
||||
import subprocess
|
||||
|
||||
import can # nur für Trace-Reader
|
||||
import can # for trace
|
||||
|
||||
from .config import load_settings, setup_logging, SETTINGS_PATH, APP_ROOT
|
||||
from .simulator import EcuState, DrivelineModel
|
||||
from .config import load_settings, setup_logging
|
||||
from .obd2 import ObdResponder, make_speed_response, make_rpm_response
|
||||
from .can import (
|
||||
list_can_ifaces, link_up, link_down, link_state, link_kind,
|
||||
have_cap_netadmin, need_caps_message
|
||||
have_cap_netadmin
|
||||
)
|
||||
|
||||
# Simulator pieces
|
||||
from .simulation.simulator_main import VehicleSimulator
|
||||
from .tabs.basic import BasicTab
|
||||
from .tabs.engine import EngineTab
|
||||
from .tabs.gearbox import GearboxTab
|
||||
from .tabs.dtc import DtcTab
|
||||
from .tabs.dashboard import DashboardTab
|
||||
|
||||
# ---------- kleine Trace-Helfer ----------
|
||||
# ---------- CAN Trace Collector ----------
|
||||
class TraceCollector:
|
||||
"""
|
||||
Liest mit eigenem BufferedReader vom SocketCAN und sammelt Frames.
|
||||
- stream_buffer: deque mit (ts, id, dlc, data_bytes)
|
||||
- aggregate: dict[(id, dir)] -> {count, last_ts, last_data}
|
||||
"""
|
||||
def __init__(self, channel: str):
|
||||
self.channel = channel
|
||||
self.bus = None
|
||||
@@ -41,7 +69,8 @@ class TraceCollector:
|
||||
def _close(self):
|
||||
try:
|
||||
if self.bus: self.bus.shutdown()
|
||||
except Exception: pass
|
||||
except Exception:
|
||||
pass
|
||||
self.bus = None
|
||||
|
||||
def start(self):
|
||||
@@ -49,8 +78,10 @@ class TraceCollector:
|
||||
|
||||
def stop(self):
|
||||
self._run.clear()
|
||||
try: self._thread.join(timeout=1.0)
|
||||
except RuntimeError: pass
|
||||
try:
|
||||
self._thread.join(timeout=1.0)
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._close()
|
||||
|
||||
def _rx_loop(self):
|
||||
@@ -59,8 +90,7 @@ class TraceCollector:
|
||||
if self.bus is None:
|
||||
if link_state(self.channel) == "UP":
|
||||
try:
|
||||
self._open()
|
||||
backoff = 0.5
|
||||
self._open(); backoff = 0.5
|
||||
except Exception:
|
||||
time.sleep(backoff); backoff = min(5.0, backoff*1.7)
|
||||
continue
|
||||
@@ -73,9 +103,7 @@ class TraceCollector:
|
||||
with self.lock:
|
||||
self.stream_buffer.append((ts, msg.arbitration_id, msg.dlc, bytes(msg.data)))
|
||||
except (can.CanOperationError, OSError):
|
||||
# IF down → ruhig schließen, kein Traceback
|
||||
self._close()
|
||||
time.sleep(0.5)
|
||||
self._close(); time.sleep(0.5)
|
||||
except Exception:
|
||||
time.sleep(0.05)
|
||||
|
||||
@@ -84,11 +112,15 @@ class TraceCollector:
|
||||
return list(self.stream_buffer)
|
||||
|
||||
|
||||
# =============================
|
||||
# GUI Launcher (reworked layout)
|
||||
# =============================
|
||||
|
||||
def launch_gui():
|
||||
cfg = load_settings()
|
||||
logger = setup_logging(cfg)
|
||||
|
||||
# read config values
|
||||
# Config
|
||||
can_iface = (cfg.get("can", {}).get("interface")) or "can0"
|
||||
resp_id_raw = (cfg.get("can", {}).get("resp_id")) or "0x7E8"
|
||||
try:
|
||||
@@ -98,80 +130,93 @@ def launch_gui():
|
||||
timeout_ms = cfg.get("can", {}).get("timeout_ms", 200)
|
||||
bitrate = cfg.get("can", {}).get("baudrate", 500000)
|
||||
|
||||
ecu = EcuState(DrivelineModel())
|
||||
# Simulator
|
||||
sim = VehicleSimulator()
|
||||
|
||||
# OBD2 responder
|
||||
responder = ObdResponder(interface=can_iface, resp_id=resp_id, timeout_ms=timeout_ms, logger=logger)
|
||||
responder.register_pid(0x0D, lambda: make_speed_response(int(round(sim.snapshot()["speed_kmh"]))))
|
||||
responder.register_pid(0x0C, lambda: make_rpm_response(int(sim.snapshot()["rpm"])))
|
||||
|
||||
# register providers
|
||||
responder.register_pid(0x0D, lambda: make_speed_response(int(round(ecu.snapshot()[3]))))
|
||||
responder.register_pid(0x0C, lambda: make_rpm_response(int(ecu.snapshot()[2])))
|
||||
|
||||
# physics thread
|
||||
# Physics thread
|
||||
running = True
|
||||
def physics_loop():
|
||||
last = time.monotonic()
|
||||
while running:
|
||||
ecu.update()
|
||||
now = time.monotonic()
|
||||
dt = min(0.05, max(0.0, now - last))
|
||||
last = now
|
||||
sim.update(dt)
|
||||
time.sleep(0.02)
|
||||
t = threading.Thread(target=physics_loop, daemon=True)
|
||||
t.start()
|
||||
threading.Thread(target=physics_loop, daemon=True).start()
|
||||
|
||||
# Trace-Collector (eigener Bus, hört alles auf can_iface)
|
||||
tracer = TraceCollector(can_iface)
|
||||
tracer.start()
|
||||
tracer = TraceCollector(can_iface); tracer.start()
|
||||
|
||||
# --- Tk UI ---
|
||||
root = tk.Tk()
|
||||
root.title("OBD-II ECU Simulator – SocketCAN")
|
||||
# Tk window
|
||||
root = tk.Tk(); root.title("OBD-II ECU Simulator – SocketCAN")
|
||||
root.geometry(f"{cfg.get('ui',{}).get('window',{}).get('width',1100)}x{cfg.get('ui',{}).get('window',{}).get('height',720)}")
|
||||
|
||||
# window size from cfg
|
||||
try:
|
||||
w = int(cfg["ui"]["window"]["width"]); h = int(cfg["ui"]["window"]["height"])
|
||||
root.geometry(f"{w}x{h}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# fonts/styles
|
||||
family = cfg.get("ui", {}).get("font_family", "TkDefaultFont")
|
||||
size = int(cfg.get("ui", {}).get("font_size", 10))
|
||||
style = ttk.Style()
|
||||
style.configure("TLabel", font=(family, size))
|
||||
style.configure("Header.TLabel", font=(family, size+2, "bold"))
|
||||
style.configure("TButton", font=(family, size))
|
||||
style.configure("Small.TLabel", font=(family, max(8, size-1)))
|
||||
|
||||
# layout
|
||||
root.columnconfigure(0, weight=1); root.rowconfigure(0, weight=1)
|
||||
main = ttk.Frame(root, padding=10); main.grid(row=0, column=0, sticky="nsew")
|
||||
main.columnconfigure(1, weight=1)
|
||||
# Menu (Load/Save config)
|
||||
menubar = tk.Menu(root)
|
||||
filemenu = tk.Menu(menubar, tearoff=0)
|
||||
|
||||
# === Controls: Gear + Throttle ===
|
||||
ttk.Label(main, text="Gang").grid(row=0, column=0, sticky="w")
|
||||
gear_var = tk.IntVar(value=0)
|
||||
gear_box = ttk.Combobox(main, textvariable=gear_var, state="readonly", values=[0,1,2,3,4,5,6], width=5)
|
||||
gear_box.grid(row=0, column=1, sticky="w", padx=(6,12))
|
||||
gear_box.bind("<<ComboboxSelected>>", lambda _e: ecu.set_gear(gear_var.get()))
|
||||
def action_load():
|
||||
path = filedialog.askopenfilename(filetypes=[("JSON", "*.json"), ("All", "*.*")])
|
||||
if not path: return
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for tab in sim_tabs: tab.load_from_config(data)
|
||||
sim.load_config(data)
|
||||
messagebox.showinfo("Simulator", "Konfiguration geladen.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Laden fehlgeschlagen", str(e))
|
||||
|
||||
ttk.Label(main, text="Gas (%)").grid(row=1, column=0, sticky="w")
|
||||
thr = ttk.Scale(main, from_=0, to=100, orient="horizontal",
|
||||
command=lambda v: ecu.set_throttle(int(float(v))))
|
||||
thr.set(0)
|
||||
thr.grid(row=1, column=1, sticky="ew", padx=(6,12))
|
||||
def action_save():
|
||||
cfg_dict = sim.export_config()
|
||||
for tab in sim_tabs: tab.save_into_config(cfg_dict)
|
||||
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json"), ("All", "*.*")])
|
||||
if not path: return
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg_dict, f, indent=2)
|
||||
messagebox.showinfo("Simulator", "Konfiguration gespeichert.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Speichern fehlgeschlagen", str(e))
|
||||
|
||||
lbl_speed = ttk.Label(main, text="Speed: 0 km/h", style="Header.TLabel")
|
||||
lbl_rpm = ttk.Label(main, text="RPM: 0")
|
||||
lbl_speed.grid(row=2, column=0, columnspan=2, sticky="w", pady=(10,0))
|
||||
lbl_rpm.grid(row=3, column=0, columnspan=2, sticky="w")
|
||||
filemenu.add_command(label="Konfiguration laden…", command=action_load)
|
||||
filemenu.add_command(label="Konfiguration speichern…", command=action_save)
|
||||
filemenu.add_separator(); filemenu.add_command(label="Beenden", command=root.destroy)
|
||||
menubar.add_cascade(label="Datei", menu=filemenu)
|
||||
root.config(menu=menubar)
|
||||
|
||||
# === CAN Panel ===
|
||||
sep = ttk.Separator(main); sep.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(10,10))
|
||||
# ===== New Layout ======================================================
|
||||
# Grid with two rows:
|
||||
# Row 0: Left = CAN settings, Right = Simulator tabs
|
||||
# Row 1: Trace spanning both columns
|
||||
# ======================================================================
|
||||
|
||||
can_frame = ttk.LabelFrame(main, text="CAN & Settings", padding=10)
|
||||
can_frame.grid(row=5, column=0, columnspan=2, sticky="nsew")
|
||||
can_frame.columnconfigure(1, weight=1)
|
||||
root.columnconfigure(0, weight=1)
|
||||
root.columnconfigure(1, weight=2)
|
||||
root.rowconfigure(1, weight=1) # trace grows
|
||||
|
||||
# --- LEFT: CAN Settings ------------------------------------------------
|
||||
can_frame = ttk.LabelFrame(root, text="CAN & Settings", padding=8)
|
||||
can_frame.grid(row=0, column=0, sticky="nsew", padx=(8,4), pady=(8,4))
|
||||
for i in range(2): can_frame.columnconfigure(i, weight=1)
|
||||
|
||||
ttk.Label(can_frame, text="Interface").grid(row=0, column=0, sticky="w")
|
||||
iface_var = tk.StringVar(value=can_iface)
|
||||
iface_list = list_can_ifaces() or [can_iface]
|
||||
iface_dd = ttk.Combobox(can_frame, textvariable=iface_var, values=iface_list, state="readonly", width=12)
|
||||
iface_dd.grid(row=0, column=1, sticky="w", padx=(6,12))
|
||||
iface_dd.grid(row=0, column=1, sticky="ew", padx=(6,0))
|
||||
|
||||
def refresh_ifaces():
|
||||
lst = list_can_ifaces()
|
||||
@@ -179,44 +224,39 @@ def launch_gui():
|
||||
messagebox.showwarning("Interfaces", "Keine can*/vcan* Interfaces gefunden.")
|
||||
return
|
||||
iface_dd.config(values=lst)
|
||||
ttk.Button(can_frame, text="Refresh", command=refresh_ifaces).grid(row=0, column=2, padx=4)
|
||||
ttk.Button(can_frame, text="Refresh", command=refresh_ifaces).grid(row=0, column=2, padx=(6,0))
|
||||
|
||||
ttk.Label(can_frame, text="RESP-ID (hex)").grid(row=1, column=0, sticky="w")
|
||||
resp_var = tk.StringVar(value=f"0x{resp_id:03X}")
|
||||
resp_entry = ttk.Entry(can_frame, textvariable=resp_var, width=10)
|
||||
resp_entry.grid(row=1, column=1, sticky="w", padx=(6,12))
|
||||
ttk.Entry(can_frame, textvariable=resp_var, width=10).grid(row=1, column=1, sticky="w", padx=(6,0))
|
||||
|
||||
ttk.Label(can_frame, text="Timeout (ms)").grid(row=2, column=0, sticky="w")
|
||||
to_var = tk.IntVar(value=int(timeout_ms))
|
||||
to_spin = ttk.Spinbox(can_frame, from_=10, to=5000, increment=10, textvariable=to_var, width=8)
|
||||
to_spin.grid(row=2, column=1, sticky="w", padx=(6,12))
|
||||
ttk.Spinbox(can_frame, from_=10, to=5000, increment=10, textvariable=to_var, width=8).grid(row=2, column=1, sticky="w", padx=(6,0))
|
||||
|
||||
ttk.Label(can_frame, text="Bitrate").grid(row=3, column=0, sticky="w")
|
||||
br_var = tk.IntVar(value=int(bitrate))
|
||||
br_spin = ttk.Spinbox(can_frame, from_=20000, to=1000000, increment=10000, textvariable=br_var, width=10)
|
||||
br_spin.grid(row=3, column=1, sticky="w", padx=(6,12))
|
||||
ttk.Spinbox(can_frame, from_=20000, to=1000000, increment=10000, textvariable=br_var, width=10).grid(row=3, column=1, sticky="w", padx=(6,0))
|
||||
|
||||
# unter Bitrate-Spinbox
|
||||
set_params = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(can_frame, text="Bitrate beim UP setzen", variable=set_params).grid(row=3, column=2, sticky="w")
|
||||
|
||||
# add Kind-Anzeige
|
||||
kind_label = ttk.Label(can_frame, text=f"Kind: {link_kind(can_iface)}")
|
||||
kind_label.grid(row=0, column=3, sticky="w", padx=(12,0))
|
||||
kind_label = ttk.Label(can_frame, text=f"Kind: {link_kind(can_iface)}", style="Small.TLabel")
|
||||
kind_label.grid(row=4, column=0, columnspan=3, sticky="w", pady=(4,0))
|
||||
|
||||
# Buttons row
|
||||
btns = ttk.Frame(can_frame)
|
||||
btns.grid(row=5, column=0, columnspan=3, sticky="ew", pady=(8,0))
|
||||
btns.columnconfigure(0, weight=0)
|
||||
btns.columnconfigure(1, weight=0)
|
||||
btns.columnconfigure(2, weight=1)
|
||||
|
||||
# Link control
|
||||
def do_link_up():
|
||||
try:
|
||||
# Kind-Anzeige aktualisieren (falls Interface gewechselt)
|
||||
kind_label.config(text=f"Kind: {link_kind(iface_var.get())}")
|
||||
|
||||
if link_state(iface_var.get()) == "UP":
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist bereits UP")
|
||||
return
|
||||
# NEU: set_params aus Checkbox
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist bereits UP"); return
|
||||
link_up(iface_var.get(), bitrate=br_var.get(), fd=False, set_params=set_params.get())
|
||||
msg = f"{iface_var.get()} ist UP"
|
||||
# nach erfolgreichem link_up(...) – in gui.py
|
||||
try:
|
||||
out = subprocess.check_output(["ip", "-details", "-json", "link", "show", iface_var.get()], text=True)
|
||||
info = json.loads(out)[0]
|
||||
@@ -226,89 +266,76 @@ def launch_gui():
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist UP @ {br} bit/s (sample-point {sp})")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if set_params.get():
|
||||
msg += f" @ {br_var.get()} bit/s (falls vom Treiber unterstützt)"
|
||||
else:
|
||||
msg += " (Bitrate unverändert)"
|
||||
messagebox.showinfo("CAN", msg)
|
||||
except PermissionError as e:
|
||||
messagebox.showerror("Berechtigung", str(e))
|
||||
except Exception as e:
|
||||
messagebox.showerror("CAN", f"Link UP fehlgeschlagen:\n{e}")
|
||||
messagebox.showerror("CAN", f"Link UP fehlgeschlagen:{e}")
|
||||
|
||||
def do_link_down():
|
||||
try:
|
||||
if link_state(iface_var.get()) == "DOWN":
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist bereits DOWN")
|
||||
return
|
||||
link_down(iface_var.get())
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist DOWN")
|
||||
except PermissionError as e:
|
||||
messagebox.showerror("Berechtigung", str(e))
|
||||
messagebox.showinfo("CAN", f"{iface_var.get()} ist bereits DOWN"); return
|
||||
link_down(iface_var.get()); messagebox.showinfo("CAN", f"{iface_var.get()} ist DOWN")
|
||||
except Exception as e:
|
||||
messagebox.showerror("CAN", f"Link DOWN fehlgeschlagen:\n{e}")
|
||||
messagebox.showerror("CAN", f"Link DOWN fehlgeschlagen:{e}")
|
||||
|
||||
btn_up = ttk.Button(can_frame, text="Link UP", command=do_link_up)
|
||||
btn_down = ttk.Button(can_frame, text="Link DOWN", command=do_link_down)
|
||||
btn_up.grid(row=4, column=0, pady=(8,0), sticky="w")
|
||||
btn_down.grid(row=4, column=1, pady=(8,0), sticky="w")
|
||||
ttk.Button(btns, text="Link UP", command=do_link_up).grid(row=0, column=0, sticky="w")
|
||||
ttk.Button(btns, text="Link DOWN", command=do_link_down).grid(row=0, column=1, sticky="w", padx=(6,0))
|
||||
|
||||
# Rebind responder
|
||||
def do_rebind():
|
||||
nonlocal can_iface, resp_id, timeout_ms, bitrate, tracer
|
||||
can_iface = iface_var.get()
|
||||
try:
|
||||
new_resp = int(resp_var.get(), 16)
|
||||
except Exception:
|
||||
messagebox.showerror("RESP-ID", "Bitte gültige Hex-Zahl, z.B. 0x7E8")
|
||||
return
|
||||
resp_id = new_resp
|
||||
timeout_ms = to_var.get()
|
||||
bitrate = br_var.get()
|
||||
messagebox.showerror("RESP-ID", "Bitte gültige Hex-Zahl, z.B. 0x7E8"); return
|
||||
resp_id = new_resp; timeout_ms = to_var.get(); bitrate = br_var.get()
|
||||
try:
|
||||
responder.rebind(interface=can_iface, resp_id=resp_id)
|
||||
# Trace-Collector auf neues IF neu binden
|
||||
try:
|
||||
tracer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
tracer = TraceCollector(can_iface)
|
||||
tracer.start()
|
||||
tracer.stop(); tracer = TraceCollector(can_iface); tracer.start()
|
||||
messagebox.showinfo("CAN", f"Responder neu gebunden: {can_iface}, RESP 0x{resp_id:03X}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("CAN", f"Rebind fehlgeschlagen:\n{e}")
|
||||
messagebox.showerror("CAN", f"Rebind fehlgeschlagen:{e}")
|
||||
|
||||
ttk.Button(can_frame, text="Responder Rebind", command=do_rebind).grid(row=4, column=2, pady=(8,0), sticky="w")
|
||||
ttk.Button(btns, text="Responder Rebind", command=do_rebind).grid(row=0, column=2, sticky="w", padx=(12,0))
|
||||
|
||||
# CAP-Status
|
||||
caps_ok = have_cap_netadmin()
|
||||
cap_label = ttk.Label(can_frame, text=f"CAP_NET_ADMIN: {'yes' if caps_ok else 'no'}")
|
||||
cap_label.grid(row=6, column=0, columnspan=2, sticky="w", pady=(6,0))
|
||||
if not caps_ok:
|
||||
btn_up.state(["disabled"]); btn_down.state(["disabled"])
|
||||
ttk.Label(can_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))
|
||||
|
||||
# Statusbar
|
||||
status = ttk.Label(main, text=f"CAN: {can_iface} | RESP-ID: 0x{resp_id:03X}", relief="sunken", anchor="w")
|
||||
status.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10,0))
|
||||
# --- RIGHT: Simulator Tabs --------------------------------------------
|
||||
right = ttk.Frame(root)
|
||||
right.grid(row=0, column=1, sticky="nsew", padx=(4,8), pady=(8,4))
|
||||
right.columnconfigure(0, weight=1)
|
||||
right.rowconfigure(0, weight=1)
|
||||
|
||||
# === TRACE-FENSTER (unten) ===
|
||||
nb_sim = ttk.Notebook(right)
|
||||
nb_sim.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
basics_tab = BasicTab(nb_sim, sim)
|
||||
engine_tab = EngineTab(nb_sim, sim)
|
||||
gearbox_tab = GearboxTab(nb_sim, sim)
|
||||
dtc_tab = DtcTab(nb_sim, sim)
|
||||
dashboard_tab = DashboardTab(nb_sim, sim)
|
||||
sim_tabs = [basics_tab, engine_tab, gearbox_tab, dtc_tab, dashboard_tab]
|
||||
|
||||
nb_sim.add(basics_tab.frame, text="Basisdaten")
|
||||
nb_sim.add(engine_tab.frame, text="Motor")
|
||||
nb_sim.add(gearbox_tab.frame, text="Getriebe")
|
||||
nb_sim.add(dtc_tab.frame, text="DTCs")
|
||||
nb_sim.add(dashboard_tab.frame, text="Dashboard")
|
||||
|
||||
# --- BOTTOM: Trace (spans both columns) -------------------------------
|
||||
trace_frame = ttk.LabelFrame(root, text="CAN Trace", padding=6)
|
||||
trace_frame.grid(row=1, column=0, sticky="nsew")
|
||||
root.rowconfigure(1, weight=1)
|
||||
trace_frame.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=8, pady=(0,8))
|
||||
trace_frame.columnconfigure(0, weight=1)
|
||||
trace_frame.rowconfigure(1, weight=1)
|
||||
|
||||
# Controls: Mode, Pause, Clear, Autoscroll
|
||||
ctrl = ttk.Frame(trace_frame)
|
||||
ctrl.grid(row=0, column=0, sticky="ew", pady=(0,4))
|
||||
ctrl.columnconfigure(5, weight=1)
|
||||
|
||||
mode_var = tk.StringVar(value="stream") # "stream" | "aggregate"
|
||||
mode_var = tk.StringVar(value="stream")
|
||||
ttk.Label(ctrl, text="Modus:").grid(row=0, column=0, sticky="w")
|
||||
mode_dd = ttk.Combobox(ctrl, textvariable=mode_var, state="readonly", width=10,
|
||||
values=["stream", "aggregate"])
|
||||
mode_dd.grid(row=0, column=1, sticky="w", padx=(4,12))
|
||||
ttk.Combobox(ctrl, textvariable=mode_var, state="readonly", width=10, values=["stream","aggregate"])\
|
||||
.grid(row=0, column=1, sticky="w", padx=(4,12))
|
||||
|
||||
paused = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(ctrl, text="Pause", variable=paused).grid(row=0, column=2, sticky="w")
|
||||
@@ -316,121 +343,50 @@ def launch_gui():
|
||||
autoscroll = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(ctrl, text="Auto-Scroll", variable=autoscroll).grid(row=0, column=3, sticky="w")
|
||||
|
||||
def do_clear():
|
||||
nonlocal aggregate_cache
|
||||
tree.delete(*tree.get_children())
|
||||
aggregate_cache.clear()
|
||||
ttk.Button(ctrl, text="Clear", command=do_clear).grid(row=0, column=4, padx=(8,0), sticky="w")
|
||||
|
||||
# Treeview
|
||||
cols_stream = ("time", "dir", "id", "dlc", "data")
|
||||
cols_agg = ("id", "dir", "count", "last_time", "last_dlc", "last_data")
|
||||
|
||||
tree = ttk.Treeview(trace_frame, columns=cols_stream, show="headings", height=10)
|
||||
tree = ttk.Treeview(trace_frame, columns=("time","dir","id","dlc","data"), show="headings", height=10)
|
||||
tree.grid(row=1, column=0, sticky="nsew")
|
||||
sb_y = ttk.Scrollbar(trace_frame, orient="vertical", command=tree.yview)
|
||||
tree.configure(yscrollcommand=sb_y.set)
|
||||
sb_y.grid(row=1, column=1, sticky="ns")
|
||||
|
||||
def setup_columns(mode: str):
|
||||
tree.delete(*tree.get_children())
|
||||
if mode == "stream":
|
||||
tree.config(columns=cols_stream)
|
||||
headings = [("time","Time"),("dir","Dir"),("id","ID"),("dlc","DLC"),("data","Data")]
|
||||
widths = [140, 60, 90, 60, 520]
|
||||
else:
|
||||
tree.config(columns=cols_agg)
|
||||
headings = [("id","ID"),("dir","Dir"),("count","Count"),("last_time","Last Time"),("last_dlc","DLC"),("last_data","Last Data")]
|
||||
widths = [90, 60, 80, 140, 60, 520]
|
||||
for (col, text), w in zip(headings, widths):
|
||||
tree.heading(col, text=text)
|
||||
tree.column(col, width=w, anchor="w")
|
||||
setup_columns("stream")
|
||||
|
||||
aggregate_cache: dict[tuple[int,str], dict] = {}
|
||||
tree.configure(yscrollcommand=sb_y.set); sb_y.grid(row=1, column=1, sticky="ns")
|
||||
|
||||
def fmt_time(ts: float) -> str:
|
||||
# hh:mm:ss.mmm
|
||||
lt = time.localtime(ts)
|
||||
return time.strftime("%H:%M:%S", lt) + f".{int((ts%1)*1000):03d}"
|
||||
def fmt_id(i: int) -> str: return f"0x{i:03X}"
|
||||
def fmt_data(b: bytes) -> str: return " ".join(f"{x:02X}" for x in b)
|
||||
|
||||
def fmt_id(i: int) -> str:
|
||||
return f"0x{i:03X}"
|
||||
|
||||
def fmt_data(b: bytes) -> str:
|
||||
return " ".join(f"{x:02X}" for x in b)
|
||||
|
||||
# periodic UI update
|
||||
last_index = 0
|
||||
def tick():
|
||||
nonlocal can_iface, resp_id, last_index
|
||||
# Top-Status
|
||||
g, tval, rpm, spd = ecu.snapshot()
|
||||
caps = "CAP:yes" if have_cap_netadmin() else "CAP:no"
|
||||
st = link_state(can_iface)
|
||||
lbl_speed.config(text=f"Speed: {int(round(spd))} km/h")
|
||||
lbl_rpm.config(text=f"RPM: {rpm}")
|
||||
st = link_state(can_iface)
|
||||
kd = link_kind(can_iface)
|
||||
status.config(text=f"CAN: {can_iface}({st},{kd}) | RESP-ID: 0x{resp_id:03X} | Gear {g} | Throttle {tval}% | {caps}")
|
||||
nonlocal last_index
|
||||
snap = sim.snapshot()
|
||||
# Optional: könnte in eine Statusbar ausgelagert werden
|
||||
root.title(f"OBD-II ECU Simulator – RPM {int(snap['rpm'])} | {int(round(snap['speed_kmh']))} km/h")
|
||||
|
||||
|
||||
# Trace
|
||||
if not paused.get():
|
||||
mode = mode_var.get()
|
||||
buf = tracer.snapshot_stream()
|
||||
if mode == "stream":
|
||||
setup_columns("stream") if tree["columns"] != cols_stream else None
|
||||
# append new items
|
||||
buf = tracer.snapshot_stream()
|
||||
# nur neue ab letztem Index
|
||||
for ts, cid, dlc, data in buf[last_index:]:
|
||||
# Richtung heuristisch
|
||||
if cid == 0x7DF:
|
||||
d = "RX"
|
||||
elif cid == resp_id:
|
||||
d = "TX"
|
||||
else:
|
||||
d = "?"
|
||||
tree.insert("", "end",
|
||||
values=(fmt_time(ts), d, fmt_id(cid), dlc, fmt_data(data)))
|
||||
# autoscroll
|
||||
d = "RX" if cid == 0x7DF else ("TX" if cid == responder.resp_id else "?")
|
||||
tree.insert("", "end", values=(fmt_time(ts), d, fmt_id(cid), dlc, fmt_data(data)))
|
||||
if autoscroll.get() and buf[last_index:]:
|
||||
tree.see(tree.get_children()[-1])
|
||||
last_index = len(buf)
|
||||
else:
|
||||
setup_columns("aggregate") if tree["columns"] != cols_agg else None
|
||||
# baue Aggregat neu (leicht, schnell)
|
||||
buf = tracer.snapshot_stream()
|
||||
agg: dict[tuple[int,str], dict] = {}
|
||||
tree.delete(*tree.get_children())
|
||||
agg = {}
|
||||
for ts, cid, dlc, data in buf:
|
||||
if cid == 0x7DF:
|
||||
d = "RX"
|
||||
elif cid == resp_id:
|
||||
d = "TX"
|
||||
else:
|
||||
d = "?"
|
||||
d = "RX" if cid == 0x7DF else ("TX" if cid == responder.resp_id else "?")
|
||||
key = (cid, d)
|
||||
entry = agg.get(key)
|
||||
if entry is None:
|
||||
e = agg.get(key)
|
||||
if not e:
|
||||
agg[key] = {"count":1, "last_ts":ts, "last_dlc":dlc, "last_data":data}
|
||||
else:
|
||||
entry["count"] += 1
|
||||
if ts >= entry["last_ts"]:
|
||||
entry["last_ts"] = ts
|
||||
entry["last_dlc"] = dlc
|
||||
entry["last_data"] = data
|
||||
# nur neu zeichnen, wenn sich was ändert
|
||||
if agg != aggregate_cache:
|
||||
tree.delete(*tree.get_children())
|
||||
# sortiert nach ID, RX vor TX
|
||||
for (cid, d) in sorted(agg.keys(), key=lambda k:(k[0], 0 if k[1]=="RX" else 1)):
|
||||
e = agg[(cid,d)]
|
||||
tree.insert("", "end",
|
||||
values=(fmt_id(cid), d, e["count"],
|
||||
fmt_time(e["last_ts"]),
|
||||
e["last_dlc"], fmt_data(e["last_data"])))
|
||||
aggregate_cache.clear()
|
||||
aggregate_cache.update(agg)
|
||||
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)]
|
||||
tree.insert("", "end", values=(fmt_id(cid), d, e["count"], fmt_time(e["last_ts"]), e["last_dlc"], fmt_data(e["last_data"])) )
|
||||
last_index = len(buf)
|
||||
|
||||
root.after(50, tick)
|
||||
|
||||
@@ -439,14 +395,11 @@ def launch_gui():
|
||||
def on_close():
|
||||
nonlocal running
|
||||
running = False
|
||||
try:
|
||||
tracer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
responder.stop()
|
||||
try: tracer.stop()
|
||||
except Exception: pass
|
||||
try: responder.stop()
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
root.protocol("WM_DELETE_WINDOW", on_close)
|
||||
root.mainloop()
|
||||
root.mainloop()
|
Reference in New Issue
Block a user