# ============================= # app/simulation/ui/basic.py # ============================= from __future__ import annotations import tkinter as tk from tkinter import ttk from typing import Dict, Any from app.simulation.ui import UITab class BasicTab(UITab): NAME = "basic" TITLE = "Basisdaten" PRIO = 10 def __init__(self, parent, sim): self.sim = sim self.frame = ttk.Frame(parent, padding=8) for c in (0,1,2,3): self.frame.columnconfigure(c, weight=1) # ---------- Linke Spalte ---------- rowL = 0 def L(lbl, var=None, w=12, kind="entry", values=None): nonlocal rowL ttk.Label(self.frame, text=lbl).grid(row=rowL, column=0, sticky="w") if kind == "entry": ttk.Entry(self.frame, textvariable=var, width=w).grid(row=rowL, column=1, sticky="w") elif kind == "label": ttk.Label(self.frame, textvariable=var).grid(row=rowL, column=1, sticky="w") elif kind == "combo": ttk.Combobox(self.frame, textvariable=var, state="readonly", values=values or [], width=w)\ .grid(row=rowL, column=1, sticky="w") elif kind == "check": ttk.Checkbutton(self.frame, variable=var).grid(row=rowL, column=1, sticky="w") elif kind == "radio": f = ttk.Frame(self.frame); f.grid(row=rowL, column=1, sticky="w") for i,(t,vv) in enumerate(values or []): ttk.Radiobutton(f, text=t, value=vv, variable=var, command=self._apply_ign)\ .grid(row=0, column=i, padx=(0,6)) rowL += 1 # Vehicle self.type = tk.StringVar(); L("Fahrzeugtyp", self.type, kind="combo", values=["motorcycle","car","truck"]) self.mass = tk.DoubleVar(); L("Gewicht [kg]", self.mass) self.abs = tk.BooleanVar(); L("ABS vorhanden", self.abs, kind="check") self.tcs = tk.BooleanVar(); L("ASR/Traktionskontrolle", self.tcs, kind="check") ttk.Separator(self.frame).grid(row=rowL, column=0, columnspan=2, sticky="ew", pady=(8,6)); rowL += 1 # Environment / Ignition self.amb = tk.DoubleVar(); L("Umgebung [°C]", self.amb) self.ign = tk.StringVar(); L("Zündung", self.ign, kind="radio", values=[("OFF","OFF"),("ACC","ACC"),("ON","ON"),("START","START")]) ttk.Separator(self.frame).grid(row=rowL, column=0, columnspan=2, sticky="ew", pady=(8,6)); rowL += 1 # Live links (Labels) self.batt_v = tk.StringVar(); L("Batterie [V]", self.batt_v, kind="label") self.elx_v = tk.StringVar(); L("ELX/Bus [V]", self.elx_v, kind="label") self.soc = tk.StringVar(); L("SOC [0..1]", self.soc, kind="label") # ---------- Rechte Spalte ---------- rowR = 0 def R(lbl, var=None, w=12, kind="entry"): nonlocal rowR ttk.Label(self.frame, text=lbl).grid(row=rowR, column=2, sticky="w") if kind == "entry": ttk.Entry(self.frame, textvariable=var, width=w).grid(row=rowR, column=3, sticky="w") elif kind == "label": ttk.Label(self.frame, textvariable=var).grid(row=rowR, column=3, sticky="w") rowR += 1 # Live rechts (Labels) self.ibatt = tk.StringVar(); R("I Batterie [A] (+entlädt)", self.ibatt, kind="label") self.ialt = tk.StringVar(); R("I Lima [A]", self.ialt, kind="label") self.load_elx= tk.StringVar(); R("Last ELX [A]", self.load_elx, kind="label") self.load_bat= tk.StringVar(); R("Last Batterie [A]", self.load_bat, kind="label") self.load_tot= tk.StringVar(); R("Last gesamt [A]", self.load_tot, kind="label") ttk.Separator(self.frame).grid(row=rowR, column=2, columnspan=2, sticky="ew", pady=(8,6)); rowR += 1 # Electrical config self.bcap = tk.DoubleVar(); R("Batt Kap. [Ah]", self.bcap) self.brint = tk.DoubleVar(); R("Batt R_int [Ω]", self.brint) self.alt_v = tk.DoubleVar(); R("Reglerspannung [V]", self.alt_v) self.alt_a = tk.DoubleVar(); R("Lima Nennstrom [A]", self.alt_a) self.alt_ci = tk.IntVar(); R("Cut-In RPM", self.alt_ci) self.alt_fc = tk.IntVar(); R("Full-Cap RPM", self.alt_fc) self.alt_eta= tk.DoubleVar(); R("Lima η_mech [-]", self.alt_eta) self.alt_rat= tk.DoubleVar(); R("Lima Übersetzung [-]", self.alt_rat) self.alt_d0 = tk.DoubleVar(); R("Lima Drag Grund [Nm]", self.alt_d0) self.alt_d1 = tk.DoubleVar(); R("Lima Drag /krpm [Nm]", self.alt_d1) # ---------- Buttons ---------- rowBtns = max(rowL, rowR) + 1 btnrow = ttk.Frame(self.frame); btnrow.grid(row=rowBtns, column=0, columnspan=4, sticky="w", pady=(8,0)) ttk.Button(btnrow, text="Aktualisieren", command=self.refresh).pack(side="left") ttk.Button(btnrow, text="Anwenden", command=self.apply).pack(side="left", padx=(8,0)) self.refresh() # ------------ Logic ------------ def refresh(self): snap = self.sim.snapshot() vcfg = dict(self.sim.v.config.get("vehicle", {})) ecfg = dict(self.sim.v.config.get("electrical", {})) # Vehicle self.type.set(vcfg.get("type", "motorcycle")) self.mass.set(float(vcfg.get("mass_kg", 210.0))) self.abs.set(bool(vcfg.get("abs", True))) self.tcs.set(bool(vcfg.get("tcs", False))) # Env / Ign self.amb.set(float(snap.get("ambient_c", 20.0))) self.ign.set(str(snap.get("ignition", "ON"))) # Live left self.batt_v.set(f"{float(snap.get('battery_voltage', 12.6)):.2f}") self.elx_v.set(f"{float(snap.get('elx_voltage', 0.0)):.2f}") self.soc.set(f"{float(snap.get('battery_soc', 0.80)):.2f}") # Live right self.ibatt.set(f"{float(snap.get('battery_current_a', 0.0)):.2f}") self.ialt.set(f"{float(snap.get('alternator_current_a', 0.0)):.2f}") self.load_elx.set(f"{float(snap.get('elec_load_elx_a', 0.0)):.2f}") self.load_bat.set(f"{float(snap.get('elec_load_batt_a', 0.0)):.2f}") self.load_tot.set(f"{float(snap.get('elec_load_total_a', 0.0)):.2f}") # Electrical config self.bcap.set(float(ecfg.get("battery_capacity_ah", 8.0))) self.brint.set(float(ecfg.get("battery_r_int_ohm", 0.020))) self.alt_v.set(float(ecfg.get("alternator_reg_v", 14.2))) self.alt_a.set(float(ecfg.get("alternator_rated_a", 20.0))) self.alt_ci.set(int(ecfg.get("alt_cut_in_rpm", 1500))) self.alt_fc.set(int(ecfg.get("alt_full_rpm", 4000))) self.alt_eta.set(float(ecfg.get("alternator_mech_efficiency", 0.55))) self.alt_rat.set(float(ecfg.get("alternator_pulley_ratio", 1.0))) self.alt_d0.set(float(ecfg.get("alternator_drag_nm_idle", 0.15))) self.alt_d1.set(float(ecfg.get("alternator_drag_nm_per_krpm", 0.05))) def _apply_ign(self): self.sim.v.set("ignition", self.ign.get()) def apply(self): # Umgebung sofort in den State (wirkt auf Thermik) try: self.sim.v.set("ambient_c", float(self.amb.get())) except: pass cfg = { "vehicle": { "type": self.type.get(), "mass_kg": float(self.mass.get()), "abs": bool(self.abs.get()), "tcs": bool(self.tcs.get()), }, "electrical": { "battery_capacity_ah": float(self.bcap.get()), "battery_r_int_ohm": float(self.brint.get()), "alternator_reg_v": float(self.alt_v.get()), "alternator_rated_a": float(self.alt_a.get()), "alt_cut_in_rpm": int(self.alt_ci.get()), "alt_full_rpm": int(self.alt_fc.get()), "alternator_mech_efficiency": float(self.alt_eta.get()), "alternator_pulley_ratio": float(self.alt_rat.get()), "alternator_drag_nm_idle": float(self.alt_d0.get()), "alternator_drag_nm_per_krpm": float(self.alt_d1.get()), } } self.sim.load_config(cfg) # Save/Load Hooks für Gesamt-Export def save_into_config(self, out: Dict[str, Any]) -> None: out.setdefault("vehicle", {}).update({ "type": self.type.get(), "mass_kg": float(self.mass.get()), "abs": bool(self.abs.get()), "tcs": bool(self.tcs.get()), }) out.setdefault("electrical", {}).update({ "battery_capacity_ah": float(self.bcap.get()), "battery_r_int_ohm": float(self.brint.get()), "alternator_reg_v": float(self.alt_v.get()), "alternator_rated_a": float(self.alt_a.get()), "alt_cut_in_rpm": int(self.alt_ci.get()), "alt_full_rpm": int(self.alt_fc.get()), "alternator_mech_efficiency": float(self.alt_eta.get()), "alternator_pulley_ratio": float(self.alt_rat.get()), "alternator_drag_nm_idle": float(self.alt_d0.get()), "alternator_drag_nm_per_krpm": float(self.alt_d1.get()), }) def load_from_config(self, cfg: Dict[str, Any]) -> None: vcfg = cfg.get("vehicle", {}); ecfg = cfg.get("electrical", {}) self.type.set(vcfg.get("type", self.type.get())) self.mass.set(vcfg.get("mass_kg", self.mass.get())) self.abs.set(vcfg.get("abs", self.abs.get())) self.tcs.set(vcfg.get("tcs", self.tcs.get())) self.bcap.set(ecfg.get("battery_capacity_ah", self.bcap.get())) self.brint.set(ecfg.get("battery_r_int_ohm", self.brint.get())) self.alt_v.set(ecfg.get("alternator_reg_v", self.alt_v.get())) self.alt_a.set(ecfg.get("alternator_rated_a", self.alt_a.get())) self.alt_ci.set(ecfg.get("alt_cut_in_rpm", self.alt_ci.get())) self.alt_fc.set(ecfg.get("alt_full_rpm", self.alt_fc.get())) self.alt_eta.set(ecfg.get("alternator_mech_efficiency", self.alt_eta.get())) self.alt_rat.set(ecfg.get("alternator_pulley_ratio", self.alt_rat.get())) self.alt_d0.set(ecfg.get("alternator_drag_nm_idle", self.alt_d0.get())) self.alt_d1.set(ecfg.get("alternator_drag_nm_per_krpm", self.alt_d1.get())) # wichtig: hier KEIN sim.load_config()