Files
OBD2-Simulator/app/app.py

154 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
# NEU: sowohl altes (flach) als auch neues Format ("sim"/"app") akzeptieren
sim_block = data.get("sim") if isinstance(data, dict) else None
if sim_block:
sim.load_config(sim_block)
else:
sim.load_config(data)
# Tabs dürfen zusätzliche eigene Daten ziehen
for t in ui_tabs:
if hasattr(t, "load_from_config"):
t.load_from_config(sim_block or data)
messagebox.showinfo("Simulator", "Konfiguration geladen.")
def do_save():
# NEU: vollständige Sim-Config (inkl. Defaults) + App-Settings bündeln
sim_out = sim.export_config()
for t in ui_tabs:
if hasattr(t, "save_into_config"):
t.save_into_config(sim_out)
out = {
"app": cfg, # aktuelle App-Settings (CAN/UI/Logging etc.)
"sim": sim_out, # vollständige Modul-Configs (mit Defaults)
}
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON","*.json")])
if not path: return
with open(path,"w",encoding="utf-8") as f: json.dump(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()