# app/gui/app.py from __future__ import annotations import json, threading, time, tkinter as tk from tkinter import ttk, messagebox, filedialog from app.config import load_settings, setup_logging from app.obd2 import ObdResponder, make_speed_response, make_rpm_response from app.simulation.simulator import VehicleSimulator from app.simulation.ui import discover_ui_tabs from app.gui.trace import TraceView from app.gui.dashboard import DashboardView from app.gui.can_panel import CanPanel def launch_gui(): cfg = load_settings(); setup_logging(cfg) can_iface = (cfg.get("can", {}).get("interface")) or "can0" resp_id_raw = (cfg.get("can", {}).get("resp_id")) or "0x7E8" try: resp_id = int(resp_id_raw, 16) if isinstance(resp_id_raw, str) else int(resp_id_raw) except Exception: resp_id = 0x7E8 timeout_ms = cfg.get("can", {}).get("timeout_ms", 200) bitrate = cfg.get("can", {}).get("baudrate", 500000) # Simulator + OBD2 sim = VehicleSimulator() responder = ObdResponder(interface=can_iface, resp_id=resp_id, timeout_ms=timeout_ms) responder.register_pid(0x0D, lambda: make_speed_response(int(round(sim.snapshot().get("speed_kmh", 0))))) responder.register_pid(0x0C, lambda: make_rpm_response(int(sim.snapshot().get("rpm", 0)))) # Physics thread running = True def physics_loop(): last = time.monotonic() while running: now = time.monotonic() dt = min(0.05, max(0.0, now - last)); last = now sim.update(dt) time.sleep(0.02) threading.Thread(target=physics_loop, daemon=True).start() # --- 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)}") # ================== Panedwindow-Layout ================== # Haupt-Split: Links/Rechts (ein senkrechter Trenner) main_pw = tk.PanedWindow(root, orient="horizontal") main_pw.pack(fill="both", expand=True) # linke Spalte left_pw = tk.PanedWindow(main_pw, orient="vertical") main_pw.add(left_pw) # rechte Spalte right_pw = tk.PanedWindow(main_pw, orient="vertical") main_pw.add(right_pw) # --- Callback-Bridge für Rebind: erst später existiert trace_view --- trace_ref = {"obj": None} def on_rebind_iface(new_iface: str): tv = trace_ref["obj"] if tv: tv.rebind_interface(new_iface) # ----- Top-Left: CAN panel ----- can_panel = CanPanel( parent=left_pw, responder=responder, initial_iface=can_iface, initial_resp_id=resp_id, initial_timeout_ms=timeout_ms, initial_bitrate=bitrate, on_rebind_iface=on_rebind_iface ) left_pw.add(can_panel.frame) # ----- Bottom-Left: Trace ----- trace_view = TraceView(parent=left_pw, responder=responder, iface_initial=can_iface) trace_ref["obj"] = trace_view left_pw.add(trace_view.frame) # ----- Top-Right: dynamic tabs ----- nb = ttk.Notebook(right_pw) ui_tabs = discover_ui_tabs(nb, sim) for t in ui_tabs: title = getattr(t, "TITLE", getattr(t, "NAME", t.__class__.__name__)) nb.add(t.frame, text=title) right_pw.add(nb) # ----- Bottom-Right: Dashboard ----- dash_view = DashboardView(parent=right_pw, sim=sim, refresh_ms=250) right_pw.add(dash_view.frame) # ---------------- Menü (Load/Save) ---------------- menubar = tk.Menu(root); filemenu = tk.Menu(menubar, tearoff=0) def do_load(): path = filedialog.askopenfilename(filetypes=[("JSON","*.json"),("All","*.*")]) if not path: return with open(path,"r",encoding="utf-8") as f: data = json.load(f) for t in ui_tabs: if hasattr(t, "load_from_config"): t.load_from_config(data) sim.load_config(data) messagebox.showinfo("Simulator", "Konfiguration geladen.") def do_save(): cfg_out = sim.export_config() for t in ui_tabs: if hasattr(t, "save_into_config"): t.save_into_config(cfg_out) path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON","*.json")]) if not path: return with open(path,"w",encoding="utf-8") as f: json.dump(cfg_out, f, indent=2) messagebox.showinfo("Simulator", "Konfiguration gespeichert.") filemenu.add_command(label="Konfiguration laden…", command=do_load) filemenu.add_command(label="Konfiguration speichern…", command=do_save) filemenu.add_separator(); filemenu.add_command(label="Beenden", command=root.destroy) menubar.add_cascade(label="Datei", menu=filemenu); root.config(menu=menubar) # Title updater def tick_title(): snap = sim.snapshot() root.title(f"OBD-II ECU Simulator – RPM {int(snap.get('rpm',0))} | {int(round(snap.get('speed_kmh',0)))} km/h") try: root.after(300, tick_title) except tk.TclError: pass tick_title() def on_close(): nonlocal running running = False try: trace_view.stop() except Exception: pass try: responder.stop() finally: root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) root.mainloop()