made everything modular
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
# app/simulation/modules/abs.py
|
||||
from __future__ import annotations
|
||||
from ..vehicle import Vehicle, Module
|
||||
|
||||
class AbsModule(Module):
|
||||
"""Stub: deceleration limiting if ABS enabled (future: needs braking input)."""
|
||||
def apply(self, v: Vehicle, dt: float) -> None:
|
||||
_abs = bool(v.config.get("vehicle", {}).get("abs", True))
|
||||
if not _abs:
|
||||
return
|
||||
# braking model folgt später
|
@@ -1,6 +1,9 @@
|
||||
# =============================
|
||||
# app/simulation/modules/basic.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
from ..vehicle import Vehicle, Module
|
||||
from app.simulation.simulator import Module, Vehicle
|
||||
import bisect
|
||||
|
||||
def _ocv_from_soc(soc: float, table: dict[float, float]) -> float:
|
||||
@@ -17,6 +20,8 @@ def _ocv_from_soc(soc: float, table: dict[float, float]) -> float:
|
||||
return y0 + t*(y1 - y0)
|
||||
|
||||
class BasicModule(Module):
|
||||
PRIO = 10
|
||||
NAME = "basic"
|
||||
"""
|
||||
- Zündungslogik inkl. START→ON nach crank_time_s
|
||||
- Ambient-Temperatur als globale Umweltgröße
|
||||
|
@@ -3,7 +3,7 @@
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
from ..vehicle import Vehicle, Module
|
||||
from app.simulation.simulator import Module, Vehicle
|
||||
import random, math
|
||||
|
||||
# Ein einziger Wahrheitsanker für alle Defaults:
|
||||
@@ -50,6 +50,8 @@ ENGINE_DEFAULTS = {
|
||||
}
|
||||
|
||||
class EngineModule(Module):
|
||||
PRIO = 20
|
||||
NAME = "engine"
|
||||
"""
|
||||
Erweiterte Motormodellierung mit realistischem Jitter & Drive-by-Wire:
|
||||
- OFF/ACC/ON/START Logik, Starten/Abwürgen
|
||||
@@ -310,16 +312,17 @@ class EngineModule(Module):
|
||||
self._rpm_noise *= 0.9
|
||||
|
||||
# --- Klammern & Setzen -----------------------------------------------------
|
||||
rpm = max(0.0, min(rpm, maxr))
|
||||
cool = max(-40.0, min(cool, 120.0))
|
||||
oil = max(-40.0, min(oil, 150.0))
|
||||
rpm = max(0.0, min(rpm, maxr))
|
||||
cool = max(-40.0, min(cool, 120.0))
|
||||
oil = max(-40.0, min(oil, 150.0))
|
||||
oil_p = max(oil_floor_off if not self._running else oil_floor_off, min(8.0, oil_p))
|
||||
|
||||
v.set("rpm", int(rpm))
|
||||
v.set("coolant_temp", round(cool, 1))
|
||||
v.set("oil_temp", round(oil, 1))
|
||||
v.set("oil_pressure", round(oil_p, 2))
|
||||
# WICHTIG: NICHT runden – das macht das Dashboard per fmt
|
||||
v.set("coolant_temp", float(cool))
|
||||
v.set("oil_temp", float(oil))
|
||||
v.set("oil_pressure", float(oil_p))
|
||||
v.set("engine_available_torque_nm", float(avail_torque))
|
||||
v.set("engine_net_torque_nm", float(net_torque))
|
||||
v.set("throttle_pedal_pct", float(pedal))
|
||||
v.set("throttle_plate_pct", float(self._plate_pct))
|
||||
v.set("throttle_plate_pct", float(self._plate_pct))
|
@@ -1,8 +1,13 @@
|
||||
# =============================
|
||||
# app/simulation/modules/gearbox.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
from ..vehicle import Vehicle, Module
|
||||
from app.simulation.simulator import Module, Vehicle
|
||||
|
||||
class GearboxModule(Module):
|
||||
PRIO = 30
|
||||
NAME = "gearbox"
|
||||
"""Koppelt Engine-RPM ↔ Wheel-Speed; registriert speed_kmh/gear fürs Dashboard."""
|
||||
def __init__(self):
|
||||
self.speed_tau = 0.3
|
||||
|
213
app/simulation/simulator.py
Normal file
213
app/simulation/simulator.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# app/simulation/simulator.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, List, Optional, Tuple, Type
|
||||
import importlib, pkgutil, inspect, pathlib
|
||||
|
||||
# ---------------------- Core: Vehicle + Accumulator-API ----------------------
|
||||
|
||||
@dataclass
|
||||
class Vehicle:
|
||||
"""
|
||||
State-/Config-Container + Dashboard-Registry + generische Frame-Akkumulatoren.
|
||||
|
||||
Grundprinzip:
|
||||
- set(key, value): harter Setzer (eine Quelle „besitzt“ den Wert)
|
||||
- get/ensure: lesen/initialisieren
|
||||
- push(key, delta, source): additiv beitragen (Source/Sink über Vorzeichen)
|
||||
- acc_total(key): Summe aller Beiträge in diesem Frame
|
||||
- acc_breakdown(key): Beiträge je Quelle (Debug/Transparenz)
|
||||
- acc_reset(): zu Framebeginn alle Akkus löschen
|
||||
|
||||
Konvention (Empfehlung, aber nicht erzwungen):
|
||||
* Positive Beiträge „belasten“ (z. B. Widerstandsmoment, Laststrom)
|
||||
* Negative Beiträge „speisen“ (z. B. Generator-Moment, Einspeisestrom)
|
||||
"""
|
||||
state: Dict[str, Any] = field(default_factory=dict)
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
dtc: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
dashboard_specs: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
# Accumulatoren: key -> {source_name: float}
|
||||
_acc: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||||
|
||||
# ---- state helpers ----
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self.state.get(key, default)
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
self.state[key] = value
|
||||
|
||||
def ensure(self, key: str, default: Any) -> Any:
|
||||
if key not in self.state:
|
||||
self.state[key] = default
|
||||
return self.state[key]
|
||||
|
||||
# ---- dashboard helpers ----
|
||||
def register_metric(
|
||||
self, key: str, *,
|
||||
label: Optional[str] = None,
|
||||
unit: Optional[str] = None,
|
||||
fmt: Optional[str] = None,
|
||||
source: Optional[str] = None,
|
||||
priority: int = 100,
|
||||
overwrite: bool = False,
|
||||
) -> None:
|
||||
spec = self.dashboard_specs.get(key)
|
||||
if spec and not overwrite:
|
||||
if label and not spec.get("label"): spec["label"] = label
|
||||
if unit and not spec.get("unit"): spec["unit"] = unit
|
||||
if fmt and not spec.get("fmt"): spec["fmt"] = fmt
|
||||
if source and not spec.get("source"): spec["source"] = source
|
||||
if spec.get("priority") is None: spec["priority"] = priority
|
||||
return
|
||||
self.dashboard_specs[key] = {
|
||||
"key": key, "label": label or key, "unit": unit, "fmt": fmt,
|
||||
"source": source, "priority": priority,
|
||||
}
|
||||
|
||||
def dashboard_snapshot(self) -> Dict[str, Any]:
|
||||
return {"specs": dict(self.dashboard_specs), "values": dict(self.state)}
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return dict(self.state)
|
||||
|
||||
# ---- generic accumulators (per-frame) ----
|
||||
def acc_reset(self) -> None:
|
||||
self._acc.clear()
|
||||
|
||||
def push(self, key: str, delta: float, source: Optional[str] = None) -> None:
|
||||
"""
|
||||
Additiver Beitrag zu einer Größe.
|
||||
Vorzeichen: + belastet / - speist (Empfehlung).
|
||||
"""
|
||||
src = source or "anon"
|
||||
bucket = self._acc.setdefault(key, {})
|
||||
bucket[src] = bucket.get(src, 0.0) + float(delta)
|
||||
|
||||
def acc_total(self, key: str) -> float:
|
||||
bucket = self._acc.get(key)
|
||||
if not bucket: return 0.0
|
||||
return sum(bucket.values())
|
||||
|
||||
def acc_breakdown(self, key: str) -> Dict[str, float]:
|
||||
return dict(self._acc.get(key, {}))
|
||||
|
||||
# ---- Backwards-compat convenience for your current Basic code ----
|
||||
def elec_reset_frame(self) -> None:
|
||||
# map legacy helpers auf generisches System
|
||||
# loads + sources werden in einem Kanal gesammelt
|
||||
# (loads positiv, sources negativ)
|
||||
# Diese Methode ist mittlerweile redundant, acc_reset() macht alles.
|
||||
pass
|
||||
|
||||
def elec_add_load(self, name: str, amps: float) -> None:
|
||||
self.push("elec.current", +max(0.0, float(amps)), source=name)
|
||||
|
||||
def elec_add_source(self, name: str, amps: float) -> None:
|
||||
self.push("elec.current", -max(0.0, float(amps)), source=name)
|
||||
|
||||
def elec_totals(self) -> Tuple[float, float]:
|
||||
"""
|
||||
Gibt (loads_a_positiv, sources_a_positiv) zurück.
|
||||
Intern liegt alles algebraisch in 'elec.current'.
|
||||
"""
|
||||
bd = self.acc_breakdown("elec.current")
|
||||
loads = sum(v for v in bd.values() if v > 0)
|
||||
sources = sum(-v for v in bd.values() if v < 0)
|
||||
return (loads, sources)
|
||||
|
||||
# ---------------------------- Module Base + Loader ----------------------------
|
||||
|
||||
class Module:
|
||||
"""
|
||||
Basisklasse für alle Module. Jedes Modul:
|
||||
- deklariert PRIO (klein = früher)
|
||||
- hat NAME (für Debug/Registry)
|
||||
- implementiert apply(v, dt)
|
||||
"""
|
||||
PRIO: int = 100
|
||||
NAME: str = "module"
|
||||
|
||||
def apply(self, v: Vehicle, dt: float) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def _discover_modules(pkg_name: str = "app.simulation.modules") -> List[Module]:
|
||||
"""
|
||||
Sucht in app/simulation/modules nach Klassen, die Module erben,
|
||||
instanziert sie und sortiert nach PRIO.
|
||||
"""
|
||||
mods: List[Module] = []
|
||||
try:
|
||||
pkg = importlib.import_module(pkg_name)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Module package '{pkg_name}' konnte nicht geladen werden: {exc}")
|
||||
|
||||
pkg_path = pathlib.Path(pkg.__file__).parent
|
||||
for _, modname, ispkg in pkgutil.iter_modules([str(pkg_path)]):
|
||||
if ispkg: # optional: auch Subpackages zulassen
|
||||
continue
|
||||
full_name = f"{pkg_name}.{modname}"
|
||||
try:
|
||||
m = importlib.import_module(full_name)
|
||||
except Exception as exc:
|
||||
print(f"[loader] Fehler beim Import {full_name}: {exc}")
|
||||
continue
|
||||
|
||||
for _, obj in inspect.getmembers(m, inspect.isclass):
|
||||
if not issubclass(obj, Module):
|
||||
continue
|
||||
if obj is Module:
|
||||
continue
|
||||
try:
|
||||
inst = obj() # Module ohne args
|
||||
except Exception as exc:
|
||||
print(f"[loader] Kann {obj.__name__} nicht instanziieren: {exc}")
|
||||
continue
|
||||
mods.append(inst)
|
||||
|
||||
# sortieren nach PRIO; bei Gleichstand NAME als Tie-Break
|
||||
mods.sort(key=lambda x: (getattr(x, "PRIO", 100), getattr(x, "NAME", x.__class__.__name__)))
|
||||
return mods
|
||||
|
||||
# ------------------------------- Simulator API --------------------------------
|
||||
|
||||
class VehicleSimulator:
|
||||
"""
|
||||
Öffentliche Fassade für GUI/Tests.
|
||||
Lädt Module dynamisch, führt sie pro Tick in PRIO-Reihenfolge aus.
|
||||
"""
|
||||
def __init__(self, modules_package: str = "app.simulation.modules"):
|
||||
self.v = Vehicle()
|
||||
self.modules: List[Module] = _discover_modules(modules_package)
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
# pro Frame alle Akkumulatoren leeren
|
||||
self.v.acc_reset()
|
||||
for m in self.modules:
|
||||
try:
|
||||
m.apply(self.v, dt)
|
||||
except Exception as exc:
|
||||
print(f"[sim] Modul {getattr(m, 'NAME', m.__class__.__name__)} Fehler: {exc}")
|
||||
|
||||
# Kompatible Hilfsfunktionen für GUI
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return self.v.snapshot()
|
||||
|
||||
def load_config(self, cfg: Dict[str, Any]) -> None:
|
||||
# Namespaced-Merge; Keys bleiben modul-spezifisch
|
||||
for k, sub in cfg.items():
|
||||
self.v.config.setdefault(k, {}).update(sub if isinstance(sub, dict) else {})
|
||||
if "dtc" in cfg:
|
||||
self.v.dtc.update(cfg["dtc"])
|
||||
|
||||
def export_config(self) -> Dict[str, Any]:
|
||||
return {ns: dict(data) for ns, data in self.v.config.items()} | {"dtc": dict(self.v.dtc)}
|
||||
|
||||
# für alte GUI-Knöpfe
|
||||
def set_gear(self, g: int) -> None:
|
||||
self.v.set("gear", max(0, min(10, int(g))))
|
||||
|
||||
def set_throttle(self, t: int) -> None:
|
||||
self.v.set("throttle_pct", max(0, min(100, int(t)))) # falls noch genutzt
|
@@ -1,46 +0,0 @@
|
||||
# app/simulation/simulator_main.py
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Any
|
||||
from .vehicle import Vehicle, Orchestrator
|
||||
from .modules.engine import EngineModule
|
||||
from .modules.gearbox import GearboxModule
|
||||
from .modules.abs import AbsModule
|
||||
from .modules.basic import BasicModule
|
||||
|
||||
class VehicleSimulator:
|
||||
def __init__(self):
|
||||
self.v = Vehicle()
|
||||
self.orch = Orchestrator(self.v)
|
||||
# order matters: base → engine → gearbox → abs
|
||||
self.orch.add(BasicModule())
|
||||
self.orch.add(EngineModule())
|
||||
self.orch.add(GearboxModule())
|
||||
self.orch.add(AbsModule())
|
||||
|
||||
# control from GUI
|
||||
def set_gear(self, g: int) -> None:
|
||||
self.v.set("gear", max(0, min(10, int(g))))
|
||||
def set_throttle(self, t: int) -> None:
|
||||
self.v.set("throttle_pct", max(0, min(100, int(t))))
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
self.orch.update(dt)
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return self.v.snapshot()
|
||||
|
||||
# config I/O (compat with old layout)
|
||||
def load_config(self, cfg: Dict[str, Any]) -> None:
|
||||
for k in ("engine","gearbox","vehicle"):
|
||||
if k in cfg:
|
||||
self.v.config.setdefault(k, {}).update(cfg[k])
|
||||
if "dtc" in cfg:
|
||||
self.v.dtc.update(cfg["dtc"])
|
||||
|
||||
def export_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"engine": dict(self.v.config.get("engine", {})),
|
||||
"gearbox": dict(self.v.config.get("gearbox", {})),
|
||||
"vehicle": dict(self.v.config.get("vehicle", {})),
|
||||
"dtc": dict(self.v.dtc),
|
||||
}
|
52
app/simulation/ui/__init__.py
Normal file
52
app/simulation/ui/__init__.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# =============================
|
||||
# app/simulation/ui/__init__.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Optional, Type
|
||||
import importlib, inspect, pkgutil, pathlib
|
||||
|
||||
class UITab:
|
||||
"""
|
||||
Basis für alle Tabs. Erwarte:
|
||||
- class-attr: NAME, TITLE, PRIO
|
||||
- __init__(parent, sim) erzeugt self.frame (tk.Frame/ttk.Frame)
|
||||
- optionale Methoden: apply(), save_into_config(out), load_from_config(cfg)
|
||||
"""
|
||||
NAME: str = "tab"
|
||||
TITLE: str = "Tab"
|
||||
PRIO: int = 100
|
||||
|
||||
# No-ops für Save/Load
|
||||
def apply(self): pass
|
||||
def save_into_config(self, out): pass
|
||||
def load_from_config(self, cfg): pass
|
||||
|
||||
def discover_ui_tabs(parent, sim, pkg_name: str = "app.simulation.ui") -> List[UITab]:
|
||||
"""Lädt alle Unter-Module von pkg_name, instanziiert Klassen, die UITab erben."""
|
||||
tabs: List[UITab] = []
|
||||
pkg = importlib.import_module(pkg_name)
|
||||
pkg_path = pathlib.Path(pkg.__file__).parent
|
||||
|
||||
for _, modname, ispkg in pkgutil.iter_modules([str(pkg_path)]):
|
||||
if ispkg: # (optional: Subpackages zulassen – hier überspringen)
|
||||
continue
|
||||
full = f"{pkg_name}.{modname}"
|
||||
try:
|
||||
m = importlib.import_module(full)
|
||||
except Exception as exc:
|
||||
print(f"[ui-loader] Importfehler {full}: {exc}")
|
||||
continue
|
||||
|
||||
for _, obj in inspect.getmembers(m, inspect.isclass):
|
||||
if obj is UITab or not issubclass(obj, UITab):
|
||||
continue
|
||||
try:
|
||||
inst = obj(parent, sim)
|
||||
except Exception as exc:
|
||||
print(f"[ui-loader] Instanzierung fehlgeschlagen {obj.__name__}: {exc}")
|
||||
continue
|
||||
tabs.append(inst)
|
||||
|
||||
tabs.sort(key=lambda t: (getattr(t, "PRIO", 100), getattr(t, "NAME", t.__class__.__name__)))
|
||||
return tabs
|
199
app/simulation/ui/basic.py
Normal file
199
app/simulation/ui/basic.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# =============================
|
||||
# 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
|
||||
"""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!
|
45
app/simulation/ui/dtc.py
Normal file
45
app/simulation/ui/dtc.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# =============================
|
||||
# app/simulation/ui/dtc.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Dict, Any
|
||||
from app.simulation.ui import UITab
|
||||
|
||||
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(UITab):
|
||||
NAME = "dtc"
|
||||
TITLE = "Fehlercodes"
|
||||
PRIO = 10
|
||||
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)
|
184
app/simulation/ui/engine.py
Normal file
184
app/simulation/ui/engine.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# =============================
|
||||
# app/simulation/ui/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
|
||||
from app.simulation.ui import UITab
|
||||
|
||||
|
||||
class EngineTab(UITab):
|
||||
NAME = "engine"
|
||||
TITLE = "Motor"
|
||||
PRIO = 10
|
||||
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)
|
72
app/simulation/ui/gearbox.py
Normal file
72
app/simulation/ui/gearbox.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# =============================
|
||||
# app/simulation/ui/gearbox.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Dict, Any, List
|
||||
from app.simulation.ui import UITab
|
||||
|
||||
|
||||
class GearboxTab(UITab):
|
||||
NAME = "gearbox"
|
||||
TITLE = "Getriebe"
|
||||
PRIO = 10
|
||||
|
||||
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)
|
@@ -1,122 +0,0 @@
|
||||
# app/simulation/vehicle.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Any, List
|
||||
|
||||
@dataclass
|
||||
class Vehicle:
|
||||
"""Dynamic property-bag vehicle."""
|
||||
state: Dict[str, Any] = field(default_factory=lambda: {
|
||||
"rpm": 1400,
|
||||
"speed_kmh": 0.0,
|
||||
"gear": 0,
|
||||
"throttle_pct": 0,
|
||||
"ignition": "OFF",
|
||||
# elektrische Live-Werte
|
||||
"battery_voltage": 12.6, # Batterie-Klemmenspannung
|
||||
"elx_voltage": 0.0, # Bordnetz/Bus-Spannung
|
||||
"system_voltage": 12.4, # alias
|
||||
"battery_soc": 0.80, # 0..1
|
||||
"battery_current_a": 0.0, # + entlädt, – lädt
|
||||
"alternator_current_a": 0.0, # von Lima geliefert
|
||||
"elec_load_total_a": 0.0, # Summe aller Verbraucher
|
||||
"ambient_c": 20.0,
|
||||
})
|
||||
|
||||
config: Dict[str, Any] = field(default_factory=lambda: {
|
||||
"vehicle": {
|
||||
"type": "motorcycle",
|
||||
"mass_kg": 210.0,
|
||||
"abs": True,
|
||||
"tcs": False,
|
||||
},
|
||||
# Elektrik-Parameter (global)
|
||||
"electrical": {
|
||||
"battery_capacity_ah": 8.0,
|
||||
"battery_r_int_ohm": 0.020, # ~20 mΩ
|
||||
# sehr einfache OCV(SOC)-Kennlinie
|
||||
"battery_ocv_v": { # bei ~20°C
|
||||
0.0: 11.8, 0.1: 12.0, 0.2: 12.1, 0.3: 12.2, 0.4: 12.3,
|
||||
0.5: 12.45, 0.6: 12.55, 0.7: 12.65, 0.8: 12.75, 0.9: 12.85,
|
||||
1.0: 12.95
|
||||
},
|
||||
"alternator_reg_v": 14.2,
|
||||
"alternator_rated_a": 20.0, # Nennstrom
|
||||
"alt_cut_in_rpm": 1500, # ab hier fängt sie an zu liefern
|
||||
"alt_full_rpm": 4000, # ab hier volle Kapazität
|
||||
},
|
||||
})
|
||||
|
||||
dtc: Dict[str, bool] = field(default_factory=dict)
|
||||
dashboard_specs: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
# accumulator für dieses Sim-Frame
|
||||
_elec_loads_a: Dict[str, float] = field(default_factory=dict)
|
||||
_elec_sources_a: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
# ---- helpers for modules ----
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
return self.state.get(key, default)
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
self.state[key] = value
|
||||
|
||||
def ensure(self, key: str, default: Any) -> Any:
|
||||
return self.state.setdefault(key, default)
|
||||
|
||||
# Dashboard registry (wie gehabt)
|
||||
def register_metric(self, key: str, *, label: str | None = None, unit: str | None = None,
|
||||
fmt: str | None = None, source: str | None = None,
|
||||
priority: int = 100, overwrite: bool = False) -> None:
|
||||
spec = self.dashboard_specs.get(key)
|
||||
if spec and not overwrite:
|
||||
if label and not spec.get("label"): spec["label"] = label
|
||||
if unit and not spec.get("unit"): spec["unit"] = unit
|
||||
if fmt and not spec.get("fmt"): spec["fmt"] = fmt
|
||||
if source and not spec.get("source"): spec["source"] = source
|
||||
if spec.get("priority") is None: spec["priority"] = priority
|
||||
return
|
||||
self.dashboard_specs[key] = {
|
||||
"key": key, "label": label or key, "unit": unit, "fmt": fmt,
|
||||
"source": source, "priority": priority,
|
||||
}
|
||||
|
||||
def dashboard_snapshot(self) -> Dict[str, Any]:
|
||||
return {"specs": dict(self.dashboard_specs), "values": dict(self.state)}
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return dict(self.state)
|
||||
|
||||
# ---- Electrical frame helpers ----
|
||||
def elec_reset_frame(self) -> None:
|
||||
self._elec_loads_a.clear()
|
||||
self._elec_sources_a.clear()
|
||||
|
||||
def elec_add_load(self, name: str, amps: float) -> None:
|
||||
# positive Werte = Stromaufnahme
|
||||
self._elec_loads_a[name] = max(0.0, float(amps))
|
||||
|
||||
def elec_add_source(self, name: str, amps: float) -> None:
|
||||
# positive Werte = Einspeisung
|
||||
self._elec_sources_a[name] = max(0.0, float(amps))
|
||||
|
||||
def elec_totals(self) -> tuple[float, float]:
|
||||
return sum(self._elec_loads_a.values()), sum(self._elec_sources_a.values())
|
||||
|
||||
class Module:
|
||||
def apply(self, v: Vehicle, dt: float) -> None:
|
||||
pass
|
||||
|
||||
class Orchestrator:
|
||||
def __init__(self, vehicle: Vehicle):
|
||||
self.vehicle = vehicle
|
||||
self.modules: List[Module] = []
|
||||
|
||||
def add(self, m: Module):
|
||||
self.modules.append(m)
|
||||
|
||||
def update(self, dt: float):
|
||||
# Pro Frame die Electrical-Recorder nullen
|
||||
self.vehicle.elec_reset_frame()
|
||||
for m in self.modules:
|
||||
m.apply(self.vehicle, dt)
|
Reference in New Issue
Block a user