starting to implement realistic Vehicle simulation
This commit is contained in:
		
							
								
								
									
										12
									
								
								app/tabs/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/tabs/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| # ============================= | ||||
| # app/tabs/__init__.py | ||||
| # ============================= | ||||
|  | ||||
| from __future__ import annotations | ||||
| from dataclasses import dataclass | ||||
| from typing import Protocol, Dict, Any | ||||
|  | ||||
| class SimTab(Protocol): | ||||
|     frame: any | ||||
|     def save_into_config(self, out: Dict[str, Any]) -> None: ... | ||||
|     def load_from_config(self, cfg: Dict[str, Any]) -> None: ... | ||||
							
								
								
									
										192
									
								
								app/tabs/basic.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								app/tabs/basic.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| # app/tabs/basic.py | ||||
| from __future__ import annotations | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from typing import Dict, Any | ||||
|  | ||||
| class BasicTab: | ||||
|     """Basis-Fahrzeug-Tab (Zündung & Elektrik).""" | ||||
|  | ||||
|     def __init__(self, parent, sim): | ||||
|         self.sim = sim | ||||
|         self.frame = ttk.Frame(parent, padding=8) | ||||
|         self.frame.columnconfigure(1, weight=1) | ||||
|  | ||||
|         row = 0 | ||||
|         # Vehicle basics ----------------------------------------------------------- | ||||
|         ttk.Label(self.frame, text="Fahrzeugtyp").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.type_var = tk.StringVar(value=self.sim.v.config.get("vehicle", {}).get("type", "motorcycle")) | ||||
|         ttk.Combobox(self.frame, textvariable=self.type_var, state="readonly", | ||||
|                      values=["motorcycle", "car", "truck"], width=16)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Gewicht [kg]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.mass_var = tk.DoubleVar(value=float(self.sim.v.config.get("vehicle", {}).get("mass_kg", 210.0))) | ||||
|         ttk.Entry(self.frame, textvariable=self.mass_var, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         self.abs_var = tk.BooleanVar(value=bool(self.sim.v.config.get("vehicle", {}).get("abs", True))) | ||||
|         ttk.Checkbutton(self.frame, text="ABS vorhanden", variable=self.abs_var)\ | ||||
|             .grid(row=row, column=0, columnspan=2, sticky="w"); row+=1 | ||||
|  | ||||
|         self.tcs_var = tk.BooleanVar(value=bool(self.sim.v.config.get("vehicle", {}).get("tcs", False))) | ||||
|         ttk.Checkbutton(self.frame, text="ASR/Traktionskontrolle", variable=self.tcs_var)\ | ||||
|             .grid(row=row, column=0, columnspan=2, sticky="w"); row+=1 | ||||
|  | ||||
|         ttk.Separator(self.frame).grid(row=row, column=0, columnspan=2, sticky="ew", pady=(6,6)); row+=1 | ||||
|  | ||||
|         # Ambient ----------------------------------------------------------------- | ||||
|         ttk.Label(self.frame, text="Umgebung [°C]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.ambient_var = tk.DoubleVar(value=float(self.sim.snapshot().get("ambient_c", 20.0))) | ||||
|         ttk.Entry(self.frame, textvariable=self.ambient_var, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         # Ignition ---------------------------------------------------------------- | ||||
|         ttk.Label(self.frame, text="Zündung").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.ign_var = tk.StringVar(value=str(self.sim.snapshot().get("ignition", "ON"))) | ||||
|         ign_frame = ttk.Frame(self.frame); ign_frame.grid(row=row-1, column=1, sticky="w") | ||||
|         for i, state in enumerate(["OFF", "ACC", "ON", "START"]): | ||||
|             ttk.Radiobutton(ign_frame, text=state, value=state, | ||||
|                             variable=self.ign_var, command=self._apply_ign)\ | ||||
|                 .grid(row=0, column=i, padx=(0,6)) | ||||
|  | ||||
|         # Live Electrical ---------------------------------------------------------- | ||||
|         ttk.Label(self.frame, text="Batterie [V]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.batt_v_var = tk.StringVar(value=f"{self.sim.snapshot().get('battery_voltage', 12.6):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.batt_v_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="ELX/Bus [V]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.elx_v_var = tk.StringVar(value=f"{self.sim.snapshot().get('elx_voltage', 0.0):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.elx_v_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="SOC [0..1]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.soc_var = tk.StringVar(value=f"{self.sim.snapshot().get('battery_soc', 0.8):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.soc_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="I Batterie [A] (+entlädt)").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.ibatt_var = tk.StringVar(value=f"{self.sim.snapshot().get('battery_current_a', 0.0):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.ibatt_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="I Lima [A]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.ialt_var = tk.StringVar(value=f"{self.sim.snapshot().get('alternator_current_a', 0.0):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.ialt_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Last gesamt [A]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.load_var = tk.StringVar(value=f"{self.sim.snapshot().get('elec_load_total_a', 0.0):.2f}") | ||||
|         ttk.Label(self.frame, textvariable=self.load_var).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Separator(self.frame).grid(row=row, column=0, columnspan=2, sticky="ew", pady=(6,6)); row+=1 | ||||
|  | ||||
|         # Electrical config -------------------------------------------------------- | ||||
|         econf = self.sim.v.config.get("electrical", {}) | ||||
|         ttk.Label(self.frame, text="Batt Kap. [Ah]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.bcap = tk.DoubleVar(value=float(econf.get("battery_capacity_ah", 8.0))) | ||||
|         ttk.Entry(self.frame, textvariable=self.bcap, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Batt R_int [Ω]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.brint = tk.DoubleVar(value=float(econf.get("battery_r_int_ohm", 0.020))) | ||||
|         ttk.Entry(self.frame, textvariable=self.brint, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Reglerspannung [V]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.alt_v = tk.DoubleVar(value=float(econf.get("alternator_reg_v", 14.2))) | ||||
|         ttk.Entry(self.frame, textvariable=self.alt_v, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Lima Nennstrom [A]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.alt_a = tk.DoubleVar(value=float(econf.get("alternator_rated_a", 20.0))) | ||||
|         ttk.Entry(self.frame, textvariable=self.alt_a, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Cut-In RPM").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.alt_cutin = tk.IntVar(value=int(econf.get("alt_cut_in_rpm", 1500))) | ||||
|         ttk.Entry(self.frame, textvariable=self.alt_cutin, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Full-Cap RPM").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.alt_full = tk.IntVar(value=int(econf.get("alt_full_rpm", 4000))) | ||||
|         ttk.Entry(self.frame, textvariable=self.alt_full, width=10).grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         # Apply -------------------------------------------------------------------- | ||||
|         ttk.Button(self.frame, text="Anwenden", command=self.apply)\ | ||||
|             .grid(row=row, column=0, pady=(8,0), sticky="w") | ||||
|  | ||||
|         # periodic UI refresh | ||||
|         self._tick() | ||||
|  | ||||
|     def _tick(self): | ||||
|         snap = self.sim.snapshot() | ||||
|         # Live-Werte | ||||
|         self.batt_v_var.set(f"{snap.get('battery_voltage', 0):.2f}") | ||||
|         self.elx_v_var.set(f"{snap.get('elx_voltage', 0):.2f}") | ||||
|         self.soc_var.set(f"{snap.get('battery_soc', 0.0):.2f}") | ||||
|         self.ibatt_var.set(f"{snap.get('battery_current_a', 0.0):.2f}") | ||||
|         self.ialt_var.set(f"{snap.get('alternator_current_a', 0.0):.2f}") | ||||
|         self.load_var.set(f"{snap.get('elec_load_total_a', 0.0):.2f}") | ||||
|  | ||||
|         # START→ON aus dem Modul spiegeln | ||||
|         curr_ign = snap.get("ignition") | ||||
|         if curr_ign and curr_ign != self.ign_var.get(): | ||||
|             self.ign_var.set(curr_ign) | ||||
|  | ||||
|         try: | ||||
|             self.frame.after(200, self._tick) | ||||
|         except tk.TclError: | ||||
|             pass | ||||
|  | ||||
|     def _apply_ign(self): | ||||
|         # Zündung live setzen | ||||
|         self.sim.v.set("ignition", self.ign_var.get()) | ||||
|  | ||||
|     def apply(self): | ||||
|         # Ambient in State (wirkt sofort auf Thermik, andere Module lesen das) | ||||
|         try: | ||||
|             self.sim.v.set("ambient_c", float(self.ambient_var.get())) | ||||
|         except Exception: | ||||
|             pass | ||||
|  | ||||
|         cfg = { | ||||
|             "vehicle": { | ||||
|                 "type": self.type_var.get(), | ||||
|                 "mass_kg": float(self.mass_var.get()), | ||||
|                 "abs": bool(self.abs_var.get()), | ||||
|                 "tcs": bool(self.tcs_var.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_cutin.get()), | ||||
|                 "alt_full_rpm": int(self.alt_full.get()), | ||||
|             } | ||||
|         } | ||||
|         self.sim.load_config(cfg) | ||||
|  | ||||
|     def save_into_config(self, out: Dict[str, Any]) -> None: | ||||
|         out.setdefault("vehicle", {}) | ||||
|         out["vehicle"].update({ | ||||
|             "type": self.type_var.get(), | ||||
|             "mass_kg": float(self.mass_var.get()), | ||||
|             "abs": bool(self.abs_var.get()), | ||||
|             "tcs": bool(self.tcs_var.get()), | ||||
|         }) | ||||
|         out.setdefault("electrical", {}) | ||||
|         out["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_cutin.get()), | ||||
|             "alt_full_rpm": int(self.alt_full.get()), | ||||
|         }) | ||||
|  | ||||
|     def load_from_config(self, cfg: Dict[str, Any]) -> None: | ||||
|         vcfg = cfg.get("vehicle", {}) | ||||
|         self.type_var.set(vcfg.get("type", self.type_var.get())) | ||||
|         self.mass_var.set(vcfg.get("mass_kg", self.mass_var.get())) | ||||
|         self.abs_var.set(vcfg.get("abs", self.abs_var.get())) | ||||
|         self.tcs_var.set(vcfg.get("tcs", self.tcs_var.get())) | ||||
|         ecfg = cfg.get("electrical", {}) | ||||
|         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_cutin.set(ecfg.get("alt_cut_in_rpm", self.alt_cutin.get())) | ||||
|         self.alt_full.set(ecfg.get("alt_full_rpm", self.alt_full.get())) | ||||
|         # wichtig: NICHT self.sim.load_config(cfg) hier! | ||||
							
								
								
									
										77
									
								
								app/tabs/dashboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								app/tabs/dashboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| # app/tabs/dashboard.py | ||||
| from __future__ import annotations | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
|  | ||||
| class DashboardTab: | ||||
|     """Zeigt dynamisch alle im Vehicle registrierten Dashboard-Metriken.""" | ||||
|     def __init__(self, parent, sim): | ||||
|         self.sim = sim | ||||
|         self.frame = ttk.Frame(parent, padding=8) | ||||
|         self.tree = ttk.Treeview(self.frame, columns=("label","value","unit","key","source"), show="headings", height=12) | ||||
|         self.tree.heading("label", text="Parameter") | ||||
|         self.tree.heading("value", text="Wert") | ||||
|         self.tree.heading("unit",  text="Einheit") | ||||
|         self.tree.heading("key",   text="Key") | ||||
|         self.tree.heading("source",text="Modul") | ||||
|         self.tree.column("label", width=180, anchor="w") | ||||
|         self.tree.column("value", width=120, anchor="e") | ||||
|         self.tree.column("unit",  width=80,  anchor="w") | ||||
|         self.tree.column("key",   width=180, anchor="w") | ||||
|         self.tree.column("source",width=100, anchor="w") | ||||
|         self.tree.grid(row=0, column=0, sticky="nsew") | ||||
|         sb = ttk.Scrollbar(self.frame, orient="vertical", command=self.tree.yview) | ||||
|         self.tree.configure(yscrollcommand=sb.set) | ||||
|         sb.grid(row=0, column=1, sticky="ns") | ||||
|         self.frame.columnconfigure(0, weight=1) | ||||
|         self.frame.rowconfigure(0, weight=1) | ||||
|  | ||||
|         self._last_keys = None | ||||
|         self._tick() | ||||
|  | ||||
|     def _format_value(self, val, fmt): | ||||
|         if fmt: | ||||
|             try: | ||||
|                 return f"{val:{fmt}}" | ||||
|             except Exception: | ||||
|                 return str(val) | ||||
|         return str(val) | ||||
|  | ||||
|     def _tick(self): | ||||
|         snap = self.sim.v.dashboard_snapshot() | ||||
|         specs = snap["specs"] | ||||
|         values = snap["values"] | ||||
|  | ||||
|         keys = sorted(specs.keys(), key=lambda k: (specs[k].get("priority", 999), specs[k].get("label", k))) | ||||
|         if keys != self._last_keys: | ||||
|             # rebuild table | ||||
|             for item in self.tree.get_children(): | ||||
|                 self.tree.delete(item) | ||||
|             for k in keys: | ||||
|                 spec = specs[k] | ||||
|                 lbl = spec.get("label", k) | ||||
|                 unit = spec.get("unit", "") | ||||
|                 src  = spec.get("source", "") | ||||
|                 val  = self._format_value(values.get(k, ""), spec.get("fmt")) | ||||
|                 self.tree.insert("", "end", iid=k, values=(lbl, val, unit, k, src)) | ||||
|             self._last_keys = keys | ||||
|         else: | ||||
|             # update values only | ||||
|             for k in keys: | ||||
|                 spec = specs[k] | ||||
|                 val = self._format_value(values.get(k, ""), spec.get("fmt")) | ||||
|                 try: | ||||
|                     self.tree.set(k, "value", val) | ||||
|                 except tk.TclError: | ||||
|                     pass | ||||
|  | ||||
|         try: | ||||
|             self.frame.after(200, self._tick) | ||||
|         except tk.TclError: | ||||
|             pass | ||||
|  | ||||
|     # Config-API no-ops (für Konsistenz mit anderen Tabs) | ||||
|     def save_into_config(self, out):  # pragma: no cover | ||||
|         pass | ||||
|     def load_from_config(self, cfg):  # pragma: no cover | ||||
|         pass | ||||
							
								
								
									
										41
									
								
								app/tabs/dtc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								app/tabs/dtc.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| # ============================= | ||||
| # app/tabs/dtc.py | ||||
| # ============================= | ||||
|  | ||||
| from __future__ import annotations | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from typing import Dict, Any | ||||
|  | ||||
| DTC_LIST = [ | ||||
|     ("P0300", "Random/Multiple Cylinder Misfire"), | ||||
|     ("P0130", "O2 Sensor Circuit (Bank1-Sensor1)"), | ||||
|     ("C0035", "Wheel Speed Sensor LF"), | ||||
|     ("U0121", "Lost Communication With ABS") | ||||
| ] | ||||
|  | ||||
| class DtcTab: | ||||
|     def __init__(self, parent, sim): | ||||
|         self.sim = sim | ||||
|         self.frame = ttk.Frame(parent, padding=8) | ||||
|         self.vars: Dict[str, tk.BooleanVar] = {} | ||||
|         row = 0 | ||||
|         ttk.Label(self.frame, text="Diagnose-Flags (Demo)", style="Header.TLabel").grid(row=row, column=0, sticky="w"); row += 1 | ||||
|         for code, label in DTC_LIST: | ||||
|             var = tk.BooleanVar(value=False) | ||||
|             ttk.Checkbutton(self.frame, text=f"{code} – {label}", variable=var).grid(row=row, column=0, sticky="w") | ||||
|             self.vars[code] = var; row += 1 | ||||
|         ttk.Button(self.frame, text="Alle löschen", command=self.clear_all).grid(row=row, column=0, sticky="w", pady=(8,0)) | ||||
|  | ||||
|     def clear_all(self): | ||||
|         for v in self.vars.values(): v.set(False) | ||||
|  | ||||
|     def save_into_config(self, out: Dict[str, Any]) -> None: | ||||
|         out.setdefault("dtc", {}) | ||||
|         out["dtc"].update({code: bool(v.get()) for code, v in self.vars.items()}) | ||||
|  | ||||
|     def load_from_config(self, cfg: Dict[str, Any]) -> None: | ||||
|         dtc = cfg.get("dtc", {}) | ||||
|         for code, v in self.vars.items(): | ||||
|             v.set(bool(dtc.get(code, False))) | ||||
|         self.sim.load_config(cfg) | ||||
							
								
								
									
										176
									
								
								app/tabs/engine.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								app/tabs/engine.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| # app/tabs/engine.py | ||||
| from __future__ import annotations | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from typing import Dict, Any | ||||
| # Wichtig: Defaults aus dem Modul importieren | ||||
| from app.simulation.modules.engine import ENGINE_DEFAULTS | ||||
|  | ||||
| class EngineTab: | ||||
|     def __init__(self, parent, sim): | ||||
|         self.sim = sim | ||||
|         self.frame = ttk.Frame(parent, padding=8) | ||||
|         self.frame.columnconfigure(1, weight=1) | ||||
|  | ||||
|         # ------------- Widgets anlegen (OHNE Defaultwerte eintragen) -------------- | ||||
|         row = 0 | ||||
|         ttk.Label(self.frame, text="Leerlauf [RPM]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.idle_var = tk.IntVar(); ttk.Entry(self.frame, textvariable=self.idle_var, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Max RPM").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.maxrpm_var = tk.IntVar(); ttk.Entry(self.frame, textvariable=self.maxrpm_var, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Anstieg [RPM/s]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.rise_var = tk.IntVar(); ttk.Entry(self.frame, textvariable=self.rise_var, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Abfall [RPM/s]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.fall_var = tk.IntVar(); ttk.Entry(self.frame, textvariable=self.fall_var, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Gaspedal-Kennlinie").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.thr_curve = tk.StringVar() | ||||
|         ttk.Combobox(self.frame, textvariable=self.thr_curve, state="readonly", | ||||
|                      values=["linear","progressive","aggressive"])\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Separator(self.frame).grid(row=row, column=0, columnspan=2, sticky="ew", pady=(8,6)); row+=1 | ||||
|  | ||||
|         # Leistung | ||||
|         ttk.Label(self.frame, text="Motorleistung [kW]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.power_kw = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.power_kw, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Drehmoment-Peak [RPM]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.peak_rpm = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.peak_rpm, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Separator(self.frame).grid(row=row, column=0, columnspan=2, sticky="ew", pady=(8,6)); row+=1 | ||||
|  | ||||
|         # Starter | ||||
|         ttk.Label(self.frame, text="Starter Nenn-RPM").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.starter_nom = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.starter_nom, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Starter min. Spannung [V]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.starter_vmin = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.starter_vmin, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Start-Schwelle [RPM]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.start_th = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.start_th, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="Stall-Grenze [RPM]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.stall_rpm = tk.DoubleVar(); ttk.Entry(self.frame, textvariable=self.stall_rpm, width=10)\ | ||||
|             .grid(row=row-1, column=1, sticky="w") | ||||
|  | ||||
|         ttk.Separator(self.frame).grid(row=row, column=0, columnspan=2, sticky="ew", pady=(8,6)); row+=1 | ||||
|  | ||||
|         # Thermik (analog – Variablen ohne Defaults anlegen) ... | ||||
|         self.amb_c = tk.DoubleVar(); self.c_warm = tk.DoubleVar(); self.c_cool = tk.DoubleVar() | ||||
|         self.o_warm = tk.DoubleVar(); self.o_cool = tk.DoubleVar() | ||||
|         self.cold_gain = tk.DoubleVar(); self.cold_gain_max = tk.DoubleVar() | ||||
|         # (Labels/Entries spar ich hier ab – wie gehabt weiterführen) | ||||
|  | ||||
|         # Öl, DBW, Jitter, Pedal | ||||
|         self.o_idle = tk.DoubleVar(); self.o_slope = tk.DoubleVar(); self.o_floor = tk.DoubleVar() | ||||
|         self.plate_idle_min = tk.DoubleVar(); self.plate_overrun = tk.DoubleVar(); self.plate_tau = tk.DoubleVar() | ||||
|         self.torque_kp = tk.DoubleVar(); self.torque_ki = tk.DoubleVar() | ||||
|         self.jitter_idle = tk.DoubleVar(); self.jitter_high = tk.DoubleVar() | ||||
|         self.jitter_tau = tk.DoubleVar(); self.jitter_off = tk.DoubleVar() | ||||
|  | ||||
|         ttk.Label(self.frame, text="Gaspedal [%]").grid(row=row, column=0, sticky="w"); row+=1 | ||||
|         self.pedal_var = tk.DoubleVar() | ||||
|         self.pedal_scale = ttk.Scale(self.frame, from_=0.0, to=100.0, variable=self.pedal_var) | ||||
|         self.pedal_scale.grid(row=row-1, column=1, sticky="ew") | ||||
|  | ||||
|         # Buttons | ||||
|         row += 1 | ||||
|         btnrow = ttk.Frame(self.frame); btnrow.grid(row=row, column=0, columnspan=2, 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)) | ||||
|  | ||||
|         # Zum Start einmal „live“ laden: | ||||
|         self.refresh() | ||||
|  | ||||
|     # liest IMMER effektiv: config.get(key, ENGINE_DEFAULTS[key]) | ||||
|     def refresh(self): | ||||
|         e = dict(ENGINE_DEFAULTS) | ||||
|         e.update(self.sim.v.config.get("engine", {}))  # Config über default mergen | ||||
|  | ||||
|         self.idle_var.set(e["idle_rpm"]) | ||||
|         self.maxrpm_var.set(e["max_rpm"]) | ||||
|         self.rise_var.set(e["rpm_rise_per_s"]) | ||||
|         self.fall_var.set(e["rpm_fall_per_s"]) | ||||
|         self.thr_curve.set(e["throttle_curve"]) | ||||
|         self.power_kw.set(e["engine_power_kw"]) | ||||
|         self.peak_rpm.set(e["torque_peak_rpm"]) | ||||
|  | ||||
|         self.starter_nom.set(e["starter_rpm_nominal"]) | ||||
|         self.starter_vmin.set(e["starter_voltage_min"]) | ||||
|         self.start_th.set(e["start_rpm_threshold"]) | ||||
|         self.stall_rpm.set(e["stall_rpm"]) | ||||
|  | ||||
|         self.amb_c.set(e["coolant_ambient_c"]) | ||||
|         self.c_warm.set(e["coolant_warm_rate_c_per_s"]) | ||||
|         self.c_cool.set(e["coolant_cool_rate_c_per_s"]) | ||||
|         self.o_warm.set(e["oil_warm_rate_c_per_s"]) | ||||
|         self.o_cool.set(e["oil_cool_rate_c_per_s"]) | ||||
|         self.cold_gain.set(e["idle_cold_gain_per_deg"]) | ||||
|         self.cold_gain_max.set(e["idle_cold_gain_max"]) | ||||
|  | ||||
|         self.o_idle.set(e["oil_pressure_idle_bar"]) | ||||
|         self.o_slope.set(e["oil_pressure_slope_bar_per_krpm"]) | ||||
|         self.o_floor.set(e["oil_pressure_off_floor_bar"]) | ||||
|  | ||||
|         self.plate_idle_min.set(e["throttle_plate_idle_min_pct"]) | ||||
|         self.plate_overrun.set(e["throttle_plate_overrun_pct"]) | ||||
|         self.plate_tau.set(e["throttle_plate_tau_s"]) | ||||
|         self.torque_kp.set(e["torque_ctrl_kp"]) | ||||
|         self.torque_ki.set(e["torque_ctrl_ki"]) | ||||
|  | ||||
|         self.jitter_idle.set(e["rpm_jitter_idle_amp_rpm"]) | ||||
|         self.jitter_high.set(e["rpm_jitter_high_amp_rpm"]) | ||||
|         self.jitter_tau.set(e["rpm_jitter_tau_s"]) | ||||
|         self.jitter_off.set(e["rpm_jitter_off_threshold_rpm"]) | ||||
|  | ||||
|         self.pedal_var.set(e["throttle_pedal_pct"]) | ||||
|  | ||||
|     def apply(self): | ||||
|         # Nur hier wird geschrieben | ||||
|         cfg = {"engine": { | ||||
|             "idle_rpm": int(self.idle_var.get()), | ||||
|             "max_rpm": int(self.maxrpm_var.get()), | ||||
|             "rpm_rise_per_s": int(self.rise_var.get()), | ||||
|             "rpm_fall_per_s": int(self.fall_var.get()), | ||||
|             "throttle_curve": self.thr_curve.get(), | ||||
|             "engine_power_kw": float(self.power_kw.get()), | ||||
|             "torque_peak_rpm": float(self.peak_rpm.get()), | ||||
|             "starter_rpm_nominal": float(self.starter_nom.get()), | ||||
|             "starter_voltage_min": float(self.starter_vmin.get()), | ||||
|             "start_rpm_threshold": float(self.start_th.get()), | ||||
|             "stall_rpm": float(self.stall_rpm.get()), | ||||
|             "coolant_ambient_c": float(self.amb_c.get()), | ||||
|             "coolant_warm_rate_c_per_s": float(self.c_warm.get()), | ||||
|             "coolant_cool_rate_c_per_s": float(self.c_cool.get()), | ||||
|             "oil_warm_rate_c_per_s": float(self.o_warm.get()), | ||||
|             "oil_cool_rate_c_per_s": float(self.o_cool.get()), | ||||
|             "idle_cold_gain_per_deg": float(self.cold_gain.get()), | ||||
|             "idle_cold_gain_max": float(self.cold_gain_max.get()), | ||||
|             "oil_pressure_idle_bar": float(self.o_idle.get()), | ||||
|             "oil_pressure_slope_bar_per_krpm": float(self.o_slope.get()), | ||||
|             "oil_pressure_off_floor_bar": float(self.o_floor.get()), | ||||
|             "throttle_plate_idle_min_pct": float(self.plate_idle_min.get()), | ||||
|             "throttle_plate_overrun_pct": float(self.plate_overrun.get()), | ||||
|             "throttle_plate_tau_s": float(self.plate_tau.get()), | ||||
|             "torque_ctrl_kp": float(self.torque_kp.get()), | ||||
|             "torque_ctrl_ki": float(self.torque_ki.get()), | ||||
|             "rpm_jitter_idle_amp_rpm": float(self.jitter_idle.get()), | ||||
|             "rpm_jitter_high_amp_rpm": float(self.jitter_high.get()), | ||||
|             "rpm_jitter_tau_s": float(self.jitter_tau.get()), | ||||
|             "rpm_jitter_off_threshold_rpm": float(self.jitter_off.get()), | ||||
|             "throttle_pedal_pct": float(self.pedal_var.get()), | ||||
|         }} | ||||
|         self.sim.load_config(cfg) | ||||
							
								
								
									
										66
									
								
								app/tabs/gearbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/tabs/gearbox.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| # ============================= | ||||
| # app/tabs/gearbox.py | ||||
| # ============================= | ||||
|  | ||||
| from __future__ import annotations | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from typing import Dict, Any, List | ||||
|  | ||||
| class GearboxTab: | ||||
|     def __init__(self, parent, sim): | ||||
|         self.sim = sim | ||||
|         self.frame = ttk.Frame(parent, padding=8) | ||||
|         self.frame.columnconfigure(1, weight=1) | ||||
|  | ||||
|         ttk.Label(self.frame, text="Gänge (inkl. Leerlauf als 0)").grid(row=0, column=0, sticky="w") | ||||
|         self.gears_var = tk.IntVar(value=6) | ||||
|         ttk.Spinbox(self.frame, from_=1, to=10, textvariable=self.gears_var, width=6, command=self._rebuild_ratios).grid(row=0, column=1, sticky="w") | ||||
|  | ||||
|         self.reverse_var = tk.BooleanVar(value=False) | ||||
|         ttk.Checkbutton(self.frame, text="Rückwärtsgang vorhanden", variable=self.reverse_var).grid(row=1, column=0, columnspan=2, sticky="w") | ||||
|  | ||||
|         ttk.Label(self.frame, text="km/h pro 1000 RPM je Gang").grid(row=2, column=0, sticky="w", pady=(6,0)) | ||||
|         self.ratio_frame = ttk.Frame(self.frame); self.ratio_frame.grid(row=3, column=0, columnspan=2, sticky="ew") | ||||
|         self.ratio_vars: List[tk.DoubleVar] = [] | ||||
|         self._rebuild_ratios() | ||||
|  | ||||
|         ttk.Button(self.frame, text="Anwenden", command=self.apply).grid(row=4, column=0, pady=(8,0), sticky="w") | ||||
|  | ||||
|     def _rebuild_ratios(self): | ||||
|         for w in self.ratio_frame.winfo_children(): w.destroy() | ||||
|         self.ratio_vars.clear() | ||||
|         n = int(self.gears_var.get()) | ||||
|         for i in range(1, n+1): | ||||
|             ttk.Label(self.ratio_frame, text=f"Gang {i}").grid(row=i-1, column=0, sticky="w") | ||||
|             v = tk.DoubleVar(value= [12.0,19.0,25.0,32.0,38.0,45.0][i-1] if i-1 < 6 else 45.0) | ||||
|             ttk.Entry(self.ratio_frame, textvariable=v, width=8).grid(row=i-1, column=1, sticky="w", padx=(6,12)) | ||||
|             self.ratio_vars.append(v) | ||||
|  | ||||
|     def apply(self): | ||||
|         ratios = [float(v.get()) for v in self.ratio_vars] | ||||
|         cfg = {"gearbox": { | ||||
|             "num_gears": int(self.gears_var.get()), | ||||
|             "reverse": bool(self.reverse_var.get()), | ||||
|             "kmh_per_krpm": [0.0] + ratios  # index 0 reserved for neutral | ||||
|         }} | ||||
|         self.sim.load_config(cfg) | ||||
|  | ||||
|     def save_into_config(self, out: Dict[str, Any]) -> None: | ||||
|         out.setdefault("gearbox", {}) | ||||
|         out["gearbox"].update({ | ||||
|             "num_gears": int(self.gears_var.get()), | ||||
|             "reverse": bool(self.reverse_var.get()), | ||||
|             "kmh_per_krpm": [0.0] + [float(v.get()) for v in self.ratio_vars] | ||||
|         }) | ||||
|  | ||||
|     def load_from_config(self, cfg: Dict[str, Any]) -> None: | ||||
|         g = cfg.get("gearbox", {}) | ||||
|         n = int(g.get("num_gears", self.gears_var.get())) | ||||
|         self.gears_var.set(n); self.reverse_var.set(g.get("reverse", self.reverse_var.get())) | ||||
|         self._rebuild_ratios() | ||||
|         ratios = g.get("kmh_per_krpm") or ([0.0] + [v.get() for v in self.ratio_vars]) | ||||
|         for i, v in enumerate(self.ratio_vars, start=1): | ||||
|             try: v.set(float(ratios[i])) | ||||
|             except Exception: pass | ||||
|         self.sim.load_config(cfg) | ||||
		Reference in New Issue
	
	Block a user