133 lines
5.2 KiB
Python
133 lines
5.2 KiB
Python
# 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()
|