made everything modular
This commit is contained in:
132
app/app.py
Normal file
132
app/app.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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()
|
Reference in New Issue
Block a user