neraly complete Model of Driving done, but needs tweaking

This commit is contained in:
2025-09-05 14:54:29 +02:00
parent 0276a3fb3c
commit 6108413d7e
12 changed files with 1469 additions and 726 deletions

View File

@@ -1,7 +1,7 @@
# app/simulation/simulator.py
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, Any, List, Optional, Tuple, Type
from typing import Dict, Any, List, Optional
import importlib, pkgutil, inspect, pathlib
# ---------------------- Core: Vehicle + Accumulator-API ----------------------
@@ -11,17 +11,11 @@ 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)
- set/get/ensure: harte Zustandswerte
- push(key, delta, source): additiver Beitrag pro Frame (Source/Sink via Vorzeichen)
- acc_total(key): Summe aller Beiträge zu 'key'
- acc_breakdown(key): Beiträge je Quelle (Debug/Transparenz)
- acc_reset(): am Frame-Beginn alle Akkus löschen
"""
state: Dict[str, Any] = field(default_factory=dict)
config: Dict[str, Any] = field(default_factory=dict)
@@ -29,7 +23,7 @@ class Vehicle:
dashboard_specs: Dict[str, Dict[str, Any]] = field(default_factory=dict)
# Accumulatoren: key -> {source_name: float}
# Accumulator: key -> {source_name: float}
_acc: Dict[str, Dict[str, float]] = field(default_factory=dict)
# ---- state helpers ----
@@ -73,80 +67,37 @@ class Vehicle:
def snapshot(self) -> Dict[str, Any]:
return dict(self.state)
# ---- generic accumulators (per-frame) ----
# ---- 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())
return 0.0 if not bucket else 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 = 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: auch Subpackages zulassen
if ispkg:
continue
full_name = f"{pkg_name}.{modname}"
try:
@@ -154,60 +105,79 @@ def _discover_modules(pkg_name: str = "app.simulation.modules") -> List[Module]:
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:
if obj is Module or not issubclass(obj, Module):
continue
try:
inst = obj() # Module ohne args
inst = obj()
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.
"""
"""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)
self.module_defaults: Dict[str, Dict[str, Any]] = {}
for m in self.modules:
ns = getattr(m, "NAME", "").lower() or m.__class__.__name__.lower()
mod = importlib.import_module(m.__class__.__module__)
# Konvention: UPPER(NAME) + _DEFAULTS
key = f"{ns.upper()}_DEFAULTS"
defaults = getattr(mod, key, None)
if isinstance(defaults, dict):
self.module_defaults[ns] = dict(defaults)
def update(self, dt: float) -> None:
# pro Frame alle Akkumulatoren leeren
self.v.acc_reset()
self.v.acc_reset() # pro Frame Akkus leeren
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)}
"""
Exportiert einen *vollständigen* Snapshot:
- Modul-Defaults + Overrides (so fehlen keine Keys)
- alle übrigen Namespaces unverändert
- DTC separat
"""
out: Dict[str, Any] = {}
# für alte GUI-Knöpfe
# 1) Modul-Namespaces: Defaults + Overrides mergen
for ns, defs in self.module_defaults.items():
merged = dict(defs)
merged.update(self.v.config.get(ns, {}))
out[ns] = merged
# 2) übrige Namespaces (ohne bekannte Modul-Defaults) 1:1 übernehmen
for ns, data in self.v.config.items():
if ns not in out:
out[ns] = dict(data)
# 3) DTC anhängen
out["dtc"] = dict(self.v.dtc)
return out
# Falls noch benutzt:
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
self.v.set("throttle_pct", max(0, min(100, int(t))))