From 3c19951c3c2449236033f44bd7d283980a074b9f Mon Sep 17 00:00:00 2001 From: Marcel Peterkau Date: Fri, 5 Sep 2025 18:01:54 +0200 Subject: [PATCH] engine tweaks --- app/simulation/modules/engine.py | 332 ++++++++++++++----------------- app/simulation/ui/engine.py | 95 +++++---- 2 files changed, 197 insertions(+), 230 deletions(-) diff --git a/app/simulation/modules/engine.py b/app/simulation/modules/engine.py index dce405d..8090676 100644 --- a/app/simulation/modules/engine.py +++ b/app/simulation/modules/engine.py @@ -1,268 +1,238 @@ # ============================= # app/simulation/modules/engine.py # ============================= - from __future__ import annotations from app.simulation.simulator import Module, Vehicle import math, random ENGINE_DEFAULTS = { - # Basis - "idle_rpm": 1200, - "max_rpm": 9000, - "rpm_rise_per_s": 4000, - "rpm_fall_per_s": 3000, - "throttle_curve": "linear", - - # Starter / Startlogik - "starter_rpm_nominal": 250.0, - "starter_voltage_min": 10.5, - "start_rpm_threshold": 210.0, - "stall_rpm": 500.0, - - # Thermische Einflüsse (nur fürs Derating/Viskosität benutzt) - "coolant_ambient_c": 20.0, - "idle_cold_gain_per_deg": 3.0, - "idle_cold_gain_max": 500.0, - - # Öl / Öldruck - "oil_pressure_idle_bar": 1.2, - "oil_pressure_slope_bar_per_krpm": 0.8, - "oil_pressure_off_floor_bar": 0.2, - - # Leistungsdaten - "engine_power_kw": 60.0, - "torque_peak_rpm": 7000.0, - - # Drive-by-wire / Regler - "throttle_plate_idle_min_pct": 6.0, - "throttle_plate_overrun_pct": 2.0, - "throttle_plate_tau_s": 0.08, - "torque_ctrl_kp": 1.2, - "torque_ctrl_ki": 0.6, - - # RPM-Jitter - "rpm_jitter_idle_amp_rpm": 12.0, - "rpm_jitter_high_amp_rpm": 4.0, - "rpm_jitter_tau_s": 0.20, - "rpm_jitter_off_threshold_rpm": 250.0, - - # UI - "throttle_pedal_pct": 0.0, + "idle_rpm": 1200, + "idle_rpm": 800, # normaler Idle + "cold_idle_rpm": 1050, # Kaltlauf-Idle + "cold_idle_end_c": 40.0, # bis zu dieser Kühlmitteltemp gilt cold_idle + "idle_cold_mode": "two_point", # "two_point" | "slope" + "max_rpm": 9000, + "rpm_rise_per_s": 4000, + "rpm_fall_per_s": 3000, + "throttle_curve": "linear", + "starter_rpm_nominal": 250.0, + "starter_voltage_min": 10.5, + "start_rpm_threshold": 210.0, + "stall_rpm": 500.0, + "coolant_ambient_c": 20.0, + "idle_cold_gain_per_deg": 3.0, + "idle_cold_gain_max": 500.0, + "oil_pressure_idle_bar": 1.2, + "oil_pressure_slope_bar_per_krpm": 0.8, + "oil_pressure_off_floor_bar": 0.2, + "engine_power_kw": 60.0, + "torque_peak_rpm": 7000.0, + "throttle_plate_idle_min_pct": 6.0, + "throttle_plate_overrun_pct": 2.0, + "throttle_plate_tau_s": 0.08, + "torque_ctrl_kp": 1.2, + "torque_ctrl_ki": 0.6, + "rpm_jitter_idle_amp_rpm": 12.0, + "rpm_jitter_high_amp_rpm": 4.0, + "rpm_jitter_tau_s": 0.20, + "rpm_jitter_off_threshold_rpm": 250.0, + "throttle_pedal_pct": 0.0, } class EngineModule(Module): PRIO = 20 NAME = "engine" - """ - Erweiterte Motormodellierung mit realistischem Jitter & Drive-by-Wire: - - OFF/ACC/ON/START Logik, Starten/Abwürgen - - Thermik (Kühlmittel/Öl), Öldruck ~ f(RPM) - - Startverhalten abhängig von Spannung & Öltemp - - Leistungsmodell via engine_power_kw + torque_peak_rpm - - Fahrerwunsch: throttle_pedal_pct (0..100) → Ziel-Leistungsanteil - * Drosselklappe (throttle_plate_pct) wird per PI-Regler geführt - * Mindestöffnung im Leerlauf, fast zu im Schubbetrieb - - Realistischer RPM-Jitter: - * bandbegrenztes Rauschen (1. Ordnung) mit Amplitude ~ f(RPM) - * kein Jitter unter einer Schwell-RPM oder wenn Motor aus - Outputs: - rpm, coolant_temp, oil_temp, oil_pressure - engine_available_torque_nm, engine_net_torque_nm - throttle_plate_pct (neu), throttle_pedal_pct (durchgereicht) - """ def __init__(self): self._running = False - self._oil_p_tau = 0.25 # Zeitkonstante Öldruck - # DBW intern + self._oil_p_tau = 0.25 self._plate_pct = 5.0 self._tc_i = 0.0 - # AR(1)-Noise self._rpm_noise = 0.0 + # NEU: Gnadenfrist nach dem Anspringen + self._post_start_s = 0.0 - # ---- helpers ---------------------------------------------------------- + # -- helpers (unverändert) -- def _curve(self, t: float, mode: str) -> float: if mode == "progressive": return t**1.5 if mode == "aggressive": return t**0.7 return t - - def _torque_at_rpm(self, power_kw: float, rpm: float, peak_rpm: float) -> float: + def _tmax_at_rpm(self, power_kw: float, rpm: float, peak_rpm: float) -> float: rpm = max(0.0, rpm) - t_max = (9550.0 * max(0.0, power_kw)) / max(500.0, peak_rpm) + t_peak = (9550.0 * max(0.0, power_kw)) / max(500.0, peak_rpm) x = min(math.pi, max(0.0, (rpm / max(1.0, peak_rpm)) * (math.pi/2))) - return max(0.0, t_max * math.sin(x)) - + return max(0.0, t_peak * math.sin(x)) def _plate_airflow_factor(self, plate_pct: float) -> float: theta = max(0.0, min(90.0, (plate_pct/100.0)*90.0)) * math.pi/180.0 return math.sin(theta)**2 - def _visco(self, temp_c: float) -> float: - # -10°C -> 0.6 … 20°C -> 0.8 … 90°C -> 1.0 if temp_c <= -10: return 0.6 if temp_c >= 90: return 1.0 if temp_c <= 20: return 0.6 + (temp_c + 10.0) * (0.2/30.0) return 0.8 + (temp_c - 20.0) * (0.2/70.0) - # ---- main ------------------------------------------------------------- def apply(self, v: Vehicle, dt: float) -> None: e = v.config.setdefault("engine", {}) - - # --- Config --- - idle = float(e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"])) - maxr = float(e.get("max_rpm", ENGINE_DEFAULTS["max_rpm"])) - rise = float(e.get("rpm_rise_per_s", ENGINE_DEFAULTS["rpm_rise_per_s"])) - fall = float(e.get("rpm_fall_per_s", ENGINE_DEFAULTS["rpm_fall_per_s"])) - thr_curve = e.get("throttle_curve", ENGINE_DEFAULTS["throttle_curve"]) - - ambient = float(e.get("coolant_ambient_c", ENGINE_DEFAULTS["coolant_ambient_c"])) - cold_gain_per_deg = float(e.get("idle_cold_gain_per_deg", ENGINE_DEFAULTS["idle_cold_gain_per_deg"])) - cold_gain_max = float(e.get("idle_cold_gain_max", ENGINE_DEFAULTS["idle_cold_gain_max"])) - - starter_nom = float(e.get("starter_rpm_nominal", ENGINE_DEFAULTS["starter_rpm_nominal"])) - starter_vmin= float(e.get("starter_voltage_min", ENGINE_DEFAULTS["starter_voltage_min"])) - start_rpm_th= float(e.get("start_rpm_threshold", ENGINE_DEFAULTS["start_rpm_threshold"])) - stall_rpm = float(e.get("stall_rpm", ENGINE_DEFAULTS["stall_rpm"])) - - power_kw = float(e.get("engine_power_kw", ENGINE_DEFAULTS["engine_power_kw"])) - peak_torque_rpm = float(e.get("torque_peak_rpm", ENGINE_DEFAULTS["torque_peak_rpm"])) - - oil_idle_bar = float(e.get("oil_pressure_idle_bar", ENGINE_DEFAULTS["oil_pressure_idle_bar"])) - oil_slope_bar_per_krpm = float(e.get("oil_pressure_slope_bar_per_krpm", ENGINE_DEFAULTS["oil_pressure_slope_bar_per_krpm"])) - oil_floor_off = float(e.get("oil_pressure_off_floor_bar", ENGINE_DEFAULTS["oil_pressure_off_floor_bar"])) - - plate_idle_min = float(e.get("throttle_plate_idle_min_pct", ENGINE_DEFAULTS["throttle_plate_idle_min_pct"])) - plate_overrun = float(e.get("throttle_plate_overrun_pct", ENGINE_DEFAULTS["throttle_plate_overrun_pct"])) - plate_tau = float(e.get("throttle_plate_tau_s", ENGINE_DEFAULTS["throttle_plate_tau_s"])) - torque_kp = float(e.get("torque_ctrl_kp", ENGINE_DEFAULTS["torque_ctrl_kp"])) - torque_ki = float(e.get("torque_ctrl_ki", ENGINE_DEFAULTS["torque_ctrl_ki"])) - - jitter_idle_amp= float(e.get("rpm_jitter_idle_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_idle_amp_rpm"])) - jitter_hi_amp = float(e.get("rpm_jitter_high_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_high_amp_rpm"])) - jitter_tau = float(e.get("rpm_jitter_tau_s", ENGINE_DEFAULTS["rpm_jitter_tau_s"])) - jitter_off_rpm = float(e.get("rpm_jitter_off_threshold_rpm", ENGINE_DEFAULTS["rpm_jitter_off_threshold_rpm"])) + # --- Config (wie gehabt) --- + idle=float(e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"])); maxr=float(e.get("max_rpm", ENGINE_DEFAULTS["max_rpm"])) + rise=float(e.get("rpm_rise_per_s", ENGINE_DEFAULTS["rpm_rise_per_s"])); fall=float(e.get("rpm_fall_per_s", ENGINE_DEFAULTS["rpm_fall_per_s"])) + thr_curve=e.get("throttle_curve", ENGINE_DEFAULTS["throttle_curve"]) + ambient=float(e.get("coolant_ambient_c", ENGINE_DEFAULTS["coolant_ambient_c"])) + cold_gain_per_deg=float(e.get("idle_cold_gain_per_deg", ENGINE_DEFAULTS["idle_cold_gain_per_deg"])) + cold_gain_max=float(e.get("idle_cold_gain_max", ENGINE_DEFAULTS["idle_cold_gain_max"])) + starter_nom=float(e.get("starter_rpm_nominal", ENGINE_DEFAULTS["starter_rpm_nominal"])) + starter_vmin=float(e.get("starter_voltage_min", ENGINE_DEFAULTS["starter_voltage_min"])) + start_rpm_th=float(e.get("start_rpm_threshold", ENGINE_DEFAULTS["start_rpm_threshold"])) + stall_rpm=float(e.get("stall_rpm", ENGINE_DEFAULTS["stall_rpm"])) + power_kw=float(e.get("engine_power_kw", ENGINE_DEFAULTS["engine_power_kw"])) + peak_torque_rpm=float(e.get("torque_peak_rpm", ENGINE_DEFAULTS["torque_peak_rpm"])) + oil_idle_bar=float(e.get("oil_pressure_idle_bar", ENGINE_DEFAULTS["oil_pressure_idle_bar"])) + oil_slope_bar_per_krpm=float(e.get("oil_pressure_slope_bar_per_krpm", ENGINE_DEFAULTS["oil_pressure_slope_bar_per_krpm"])) + oil_floor_off=float(e.get("oil_pressure_off_floor_bar", ENGINE_DEFAULTS["oil_pressure_off_floor_bar"])) + plate_idle_min=float(e.get("throttle_plate_idle_min_pct", ENGINE_DEFAULTS["throttle_plate_idle_min_pct"])) + plate_overrun=float(e.get("throttle_plate_overrun_pct", ENGINE_DEFAULTS["throttle_plate_overrun_pct"])) + plate_tau=float(e.get("throttle_plate_tau_s", ENGINE_DEFAULTS["throttle_plate_tau_s"])) + torque_kp=float(e.get("torque_ctrl_kp", ENGINE_DEFAULTS["torque_ctrl_kp"])) + torque_ki=float(e.get("torque_ctrl_ki", ENGINE_DEFAULTS["torque_ctrl_ki"])) + jitter_idle_amp=float(e.get("rpm_jitter_idle_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_idle_amp_rpm"])) + jitter_hi_amp=float(e.get("rpm_jitter_high_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_high_amp_rpm"])) + jitter_tau=float(e.get("rpm_jitter_tau_s", ENGINE_DEFAULTS["rpm_jitter_tau_s"])) + jitter_off_rpm=float(e.get("rpm_jitter_off_threshold_rpm", ENGINE_DEFAULTS["rpm_jitter_off_threshold_rpm"])) # --- State --- - rpm = float(v.ensure("rpm", 0.0)) - pedal = float(v.ensure("throttle_pedal_pct", float(e.get("throttle_pedal_pct", 0.0)))) - pedal = max(0.0, min(100.0, pedal)) - ign = str(v.ensure("ignition", "OFF")) - elx_v = float(v.ensure("elx_voltage", 0.0)) - cool = float(v.ensure("coolant_temp", ambient)) # nur lesen - oil = float(v.ensure("oil_temp", ambient)) # nur lesen - oil_p = float(v.ensure("oil_pressure", 0.0)) + rpm=float(v.ensure("rpm", 0.0)) + pedal=float(v.ensure("throttle_pedal_pct", float(e.get("throttle_pedal_pct", 0.0)))) + pedal=max(0.0, min(100.0, pedal)) + ign=str(v.ensure("ignition", "OFF")) + elx_v=float(v.ensure("elx_voltage", 0.0)) + cool=float(v.ensure("coolant_temp", ambient)) + oil=float(v.ensure("oil_temp", ambient)) + oil_p=float(v.ensure("oil_pressure", 0.0)) - # externe Momente (Alternator/Getriebe/…) - torque_load = max(0.0, v.acc_total("engine.torque_load_nm")) - torque_load = max(torque_load, float(v.get("engine_ext_torque_nm", 0.0))) # legacy fallback + torque_load=max(0.0, v.acc_total("engine.torque_load_nm")) + torque_load=max(torque_load, float(v.get("engine_ext_torque_nm", 0.0))) - # Dashboard-Metriken - v.register_metric("rpm", unit="RPM", fmt=".1f", label="Drehzahl", source="engine", priority=20) - v.register_metric("oil_pressure", unit="bar", fmt=".2f", label="Öldruck", source="engine", priority=42) - v.register_metric("engine_available_torque_nm", unit="Nm", fmt=".0f", label="Verfügbares Motormoment", source="engine", priority=43) - v.register_metric("engine_torque_load_nm", unit="Nm", fmt=".0f", label="Lastmoment ges.", source="engine", priority=44) - v.register_metric("engine_net_torque_nm", unit="Nm", fmt=".0f", label="Netto Motormoment", source="engine", priority=45) - v.register_metric("throttle_pedal_pct", unit="%", fmt=".0f", label="Gaspedal", source="engine", priority=46) - v.register_metric("throttle_plate_pct", unit="%", fmt=".0f", label="Drosselklappe", source="engine", priority=47) + # Dashboard (wie gehabt) + v.register_metric("rpm", unit="RPM", fmt=".1f", label="Drehzahl", source="engine", priority=20) + v.register_metric("oil_pressure", unit="bar", fmt=".2f", label="Öldruck", source="engine", priority=42) + v.register_metric("engine_available_torque_nm", unit="Nm", fmt=".0f", label="Verfügbares Motormoment", source="engine", priority=43) + v.register_metric("engine_torque_load_nm", unit="Nm", fmt=".0f", label="Lastmoment ges.", source="engine", priority=44) + v.register_metric("engine_net_torque_nm", unit="Nm", fmt=".0f", label="Netto Motormoment", source="engine", priority=45) + v.register_metric("throttle_pedal_pct", unit="%", fmt=".0f", label="Gaspedal", source="engine", priority=46) + v.register_metric("throttle_plate_pct", unit="%", fmt=".0f", label="Drosselklappe", source="engine", priority=47) - # --- Start-/Ziel-RPM Logik --- - # Starter-Viskositätseinfluss + # --- Startlogik + Post-Start-Grace --- vfac = 0.0 if elx_v <= starter_vmin else min(1.2, (elx_v - starter_vmin) / max(0.3, (12.6 - starter_vmin))) crank_rpm = starter_nom * vfac * self._visco(oil) - - # effektive Startschwelle (15..45% Idle) - start_rpm_min = 0.15 * idle - start_rpm_max = 0.45 * idle + start_rpm_min=0.15*idle; start_rpm_max=0.45*idle start_rpm_th_eff = max(start_rpm_min, min(start_rpm_th, start_rpm_max)) - if ign in ("OFF", "ACC"): + if ign in ("OFF","ACC"): self._running = False - target_rpm = 0.0 + self._post_start_s = 0.0 elif ign == "START": - target_rpm = crank_rpm - if not self._running and target_rpm >= start_rpm_th_eff and elx_v > starter_vmin: + # wenn Schwelle erreicht: Motor gilt als angesprungen + Gnadenfrist + if not self._running and crank_rpm >= start_rpm_th_eff and elx_v > starter_vmin: self._running = True + self._post_start_s = 1.2 + rpm = max(rpm, crank_rpm) # Starter dreht mit else: # ON if not self._running and rpm >= max(0.15*idle, start_rpm_th_eff*0.9): self._running = True - if self._running: - cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"], - (90.0 - cool) * cold_gain_per_deg)) - idle_eff = idle + cold_add - target_rpm = max(idle_eff, min(maxr, rpm)) - else: - target_rpm = 0.0 + self._post_start_s = 1.0 - # --- Basis-Moment & Derating --- - base_torque = self._torque_at_rpm(power_kw, max(1.0, rpm), peak_torque_rpm) - temp_derate = max(0.7, 1.0 - max(0.0, (oil - 110.0)) * 0.005) + if self._running and self._post_start_s > 0.0: + self._post_start_s = max(0.0, self._post_start_s - dt) + + # --- Drehmomentmodell --- + tmax_rpm = self._tmax_at_rpm(power_kw, max(1.0, rpm), peak_torque_rpm) + + cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"], (90.0 - cool) * cold_gain_per_deg)) + mode = str(e.get("idle_cold_mode", ENGINE_DEFAULTS["idle_cold_mode"])).lower() + cold_idle = float(e.get("cold_idle_rpm", ENGINE_DEFAULTS["cold_idle_rpm"])) + cold_end = float(e.get("cold_idle_end_c", ENGINE_DEFAULTS["cold_idle_end_c"])) + + if mode == "two_point": + idle_eff = cold_idle if cool < cold_end else idle + else: + # Fallback: alte dynamische Rampe + cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"], + (90.0 - cool) * float(e.get("idle_cold_gain_per_deg", + ENGINE_DEFAULTS["idle_cold_gain_per_deg"])))) + idle_eff = idle + cold_add + + rpm_err = max(0.0, idle_eff - rpm) + t_idle_cap = 0.35 * max(5.0, self._tmax_at_rpm(power_kw, max(500.0, idle_eff), peak_torque_rpm)) + t_idle_req = t_idle_cap * min(1.0, rpm_err / max(50.0, 0.2*idle_eff)) - # --- DBW (PI auf Torque-Anteil) --- demand = self._curve(pedal/100.0, thr_curve) - plate_target_min = plate_overrun if demand < 0.02 else plate_idle_min + t_driver_req = demand * tmax_rpm - airflow = self._plate_airflow_factor(self._plate_pct) - torque_avail = base_torque * airflow * temp_derate - torque_frac = 0.0 if base_torque <= 1e-6 else (torque_avail / (base_torque * temp_derate)) - err = max(0.0, demand) - max(0.0, min(1.0, torque_frac)) + # WICHTIG: Momentfreigabe auch in START, wenn _running bereits True + running_mode = self._running and (ign in ("ON","START")) + t_target = (max(t_idle_req, t_driver_req) if running_mode else 0.0) - if ign == "ON" and self._running: + temp_derate = max(0.6, 1.0 - max(0.0, (oil - 120.0)) * 0.006) + + norm_target = 0.0 if tmax_rpm <= 1e-6 else max(0.0, min(1.0, t_target / (tmax_rpm * temp_derate))) + norm_avail = self._plate_airflow_factor(self._plate_pct) + err = norm_target - norm_avail + + if running_mode: self._tc_i += err * torque_ki * dt else: - self._tc_i *= 0.95 + self._tc_i *= 0.9 plate_cmd = self._plate_pct + (torque_kp * err + self._tc_i) * 100.0 - plate_cmd = max(plate_target_min, min(100.0, plate_cmd)) + plate_min = plate_overrun if (demand < 0.02 and rpm > idle_eff + 200.0) else plate_idle_min + plate_cmd = max(plate_min, min(100.0, plate_cmd)) a_tau = min(1.0, dt / max(1e-3, plate_tau)) self._plate_pct = (1.0 - a_tau) * self._plate_pct + a_tau * plate_cmd - # aktualisiertes Moment airflow = self._plate_airflow_factor(self._plate_pct) - avail_torque = base_torque * airflow * temp_derate + avail_torque = tmax_rpm * airflow * temp_derate net_torque = max(0.0, avail_torque - max(0.0, torque_load)) - # --- Wärmeleistung pushen (W) --- - # mechanische Leistung: + # Wärme (wie gehabt) mech_power_w = net_torque * (2.0 * math.pi * rpm / 60.0) - # grober Wirkungsgrad (0.24..0.34 je nach Pedal/Kennlinie) - eta = 0.24 + 0.10 * self._curve(pedal/100.0, thr_curve) + eta = 0.24 + 0.10 * demand eta = max(0.05, min(0.45, eta)) fuel_power_w = mech_power_w / max(1e-3, eta) heat_w = max(0.0, fuel_power_w - mech_power_w) - # Idle-Basiswärme, damit im Leerlauf nicht auskühlt: idle_heat_w = 1500.0 * (rpm / max(1.0, idle)) heat_w = max(heat_w, idle_heat_w) v.push("thermal.heat_w", +heat_w, source="engine") - # --- Ziel-RPM aus Netto-Moment --- - if ign == "ON" and self._running: - cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"], - (90.0 - cool) * cold_gain_per_deg)) - idle_eff = idle + cold_add - denom = (base_torque * temp_derate + 1e-6) - torque_norm = 0.0 if denom <= 1e-8 else max(0.0, min(1.0, net_torque / denom)) - target_rpm = idle_eff + torque_norm * (maxr - idle_eff) + # RPM-Dynamik + if running_mode: + torque_norm = 0.0 if tmax_rpm <= 1e-6 else max(0.0, min(1.0, net_torque / tmax_rpm)) + free_target = idle_eff + torque_norm * (maxr - idle_eff) + if rpm < free_target: + rpm = min(free_target, rpm + rise * max(0.2, torque_norm) * dt) + else: + rpm = max(free_target, rpm - fall * dt) + elif ign == "START": + rpm = max(rpm, crank_rpm) + else: + rpm *= 0.98 - # Inertia - if rpm < target_rpm: rpm = min(target_rpm, rpm + rise * dt) - else: rpm = max(target_rpm, rpm - fall * dt) - - # Stall - if ign == "ON" and self._running and rpm < stall_rpm: + # Stall NUR wenn keine Gnadenfrist aktiv ist + if ign == "ON" and self._running and self._post_start_s <= 0.0 and rpm < stall_rpm: self._running = False + rpm = 0.0 - # --- Öldruck --- + # Öldruck if self._running and rpm > 0.0: over_krpm = max(0.0, (rpm - idle)/1000.0) oil_target = oil_idle_bar + oil_slope_bar_per_krpm * over_krpm - elif ign == "START" and target_rpm > 0.0: + elif ign == "START" and rpm > 0.0: oil_target = max(oil_floor_off, 0.4) else: oil_target = oil_floor_off a = min(1.0, dt / max(0.05, self._oil_p_tau)) oil_p = (1-a) * oil_p + a * oil_target - # --- RPM-Jitter --- + # Jitter (wie gehabt) if self._running and rpm >= jitter_off_rpm and ign == "ON": b = min(1.0, dt / max(1e-3, jitter_tau)) eta_n = random.uniform(-1.0, 1.0) @@ -273,12 +243,10 @@ class EngineModule(Module): else: self._rpm_noise *= 0.9 - # --- Clamp & Set --- - rpm = max(0.0, min(rpm, maxr)) + # Clamp & Set + rpm = max(0.0, min(rpm, maxr)) oil_p = max(oil_floor_off, min(8.0, oil_p)) - v.set("rpm", float(rpm)) - # Temperaturen NICHT setzen – CoolingModule ist owner! v.set("oil_pressure", float(oil_p)) v.set("engine_available_torque_nm", float(avail_torque)) v.set("engine_torque_load_nm", float(torque_load)) diff --git a/app/simulation/ui/engine.py b/app/simulation/ui/engine.py index 124d253..8e87dda 100644 --- a/app/simulation/ui/engine.py +++ b/app/simulation/ui/engine.py @@ -31,6 +31,13 @@ class EngineTab(UITab): rowL += 1 self.idle = tk.IntVar(); L("Leerlauf [RPM]", self.idle) + self.cold_idle = tk.IntVar(); L("Kaltlauf-Idle [RPM]", self.cold_idle) + self.cold_end = tk.DoubleVar(); L("Kaltlauf Ende bei [°C]", self.cold_end) + self.cold_mode = tk.StringVar(); L("Kaltlauf-Modus", self.cold_mode, kind="combo", + values=["two_point","slope"]) + + ttk.Separator(self.frame).grid(row=rowL, column=0, columnspan=2, sticky="ew", pady=(8,6)); rowL += 1 + self.maxrpm = tk.IntVar(); L("Max RPM", self.maxrpm) self.rise = tk.IntVar(); L("Anstieg [RPM/s]", self.rise) self.fall = tk.IntVar(); L("Abfall [RPM/s]", self.fall) @@ -49,12 +56,6 @@ class EngineTab(UITab): self.st_thr = tk.DoubleVar(); L("Start-Schwelle [RPM]", self.st_thr) self.stall = tk.DoubleVar(); L("Stall-Grenze [RPM]", self.stall) - ttk.Separator(self.frame).grid(row=rowL, column=0, columnspan=2, sticky="ew", pady=(8,6)); rowL += 1 - - self.o_idle = tk.DoubleVar(); L("Öldruck Leerlauf [bar]", self.o_idle) - self.o_slope= tk.DoubleVar(); L("Öldruck Steigung [bar/krpm]", self.o_slope) - self.o_floor= tk.DoubleVar(); L("Öldruck Boden [bar]", self.o_floor) - # ---------- Rechte Spalte ---------- rowR = 0 def R(lbl, var, w=12, kind="entry"): @@ -70,6 +71,10 @@ class EngineTab(UITab): s.grid(row=rowR, column=3, sticky="ew") rowR += 1 + self.amb_c = tk.DoubleVar(); R("Umgebung [°C]", self.amb_c) + + ttk.Separator(self.frame).grid(row=rowR, column=2, columnspan=2, sticky="ew", pady=(8,6)); rowR += 1 + self.dk_idle = tk.DoubleVar(); R("DK min Leerlauf [%]", self.dk_idle) self.dk_over = tk.DoubleVar(); R("DK Schub [%]", self.dk_over) self.dk_tau = tk.DoubleVar(); R("DK Zeitkonstante [s]", self.dk_tau) @@ -85,12 +90,6 @@ class EngineTab(UITab): ttk.Separator(self.frame).grid(row=rowR, column=2, columnspan=2, sticky="ew", pady=(8,6)); rowR += 1 - self.amb_c = tk.DoubleVar(); R("Umgebung [°C]", self.amb_c) - self.cold_k = tk.DoubleVar(); R("Kalt-Leerlauf +/°C [RPM/°C]", self.cold_k) - self.cold_max=tk.DoubleVar(); R("Kalt-Leerlauf max [RPM]", self.cold_max) - - ttk.Separator(self.frame).grid(row=rowR, column=2, columnspan=2, sticky="ew", pady=(8,6)); rowR += 1 - self.pedal = tk.DoubleVar(); R("Gaspedal [%]", self.pedal, kind="scale") # ---------- Buttons ---------- @@ -109,78 +108,78 @@ class EngineTab(UITab): e = dict(ENGINE_DEFAULTS); e.update(self.sim.v.config.get("engine", {})) # links - self.idle.set(e["idle_rpm"]) - self.maxrpm.set(e["max_rpm"]) - self.rise.set(e["rpm_rise_per_s"]) - self.fall.set(e["rpm_fall_per_s"]) - self.curve.set(e["throttle_curve"]) + self.idle.set(e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"])) + self.cold_idle.set(e.get("cold_idle_rpm", e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"]))) + self.cold_end.set(e.get("cold_idle_end_c", 50.0)) + self.cold_mode.set(e.get("idle_cold_mode", "two_point")) - self.power.set(e["engine_power_kw"]) - self.tqpeak.set(e["torque_peak_rpm"]) + self.maxrpm.set(e.get("max_rpm", ENGINE_DEFAULTS["max_rpm"])) + self.rise.set(e.get("rpm_rise_per_s", ENGINE_DEFAULTS["rpm_rise_per_s"])) + self.fall.set(e.get("rpm_fall_per_s", ENGINE_DEFAULTS["rpm_fall_per_s"])) + self.curve.set(e.get("throttle_curve", ENGINE_DEFAULTS["throttle_curve"])) - self.st_nom.set(e["starter_rpm_nominal"]) - self.st_vmin.set(e["starter_voltage_min"]) - self.st_thr.set(e["start_rpm_threshold"]) - self.stall.set(e["stall_rpm"]) + self.power.set(e.get("engine_power_kw", ENGINE_DEFAULTS["engine_power_kw"])) + self.tqpeak.set(e.get("torque_peak_rpm", ENGINE_DEFAULTS["torque_peak_rpm"])) - 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.st_nom.set(e.get("starter_rpm_nominal", ENGINE_DEFAULTS["starter_rpm_nominal"])) + self.st_vmin.set(e.get("starter_voltage_min", ENGINE_DEFAULTS["starter_voltage_min"])) + self.st_thr.set(e.get("start_rpm_threshold", ENGINE_DEFAULTS["start_rpm_threshold"])) + self.stall.set(e.get("stall_rpm", ENGINE_DEFAULTS["stall_rpm"])) # rechts - self.dk_idle.set(e["throttle_plate_idle_min_pct"]) - self.dk_over.set(e["throttle_plate_overrun_pct"]) - self.dk_tau.set(e["throttle_plate_tau_s"]) - self.tq_kp.set(e["torque_ctrl_kp"]) - self.tq_ki.set(e["torque_ctrl_ki"]) + self.amb_c.set(e.get("coolant_ambient_c", ENGINE_DEFAULTS["coolant_ambient_c"])) - self.jit_idle.set(e["rpm_jitter_idle_amp_rpm"]) - self.jit_high.set(e["rpm_jitter_high_amp_rpm"]) - self.jit_tau.set(e["rpm_jitter_tau_s"]) - self.jit_off.set(e["rpm_jitter_off_threshold_rpm"]) + self.dk_idle.set(e.get("throttle_plate_idle_min_pct", ENGINE_DEFAULTS["throttle_plate_idle_min_pct"])) + self.dk_over.set(e.get("throttle_plate_overrun_pct", ENGINE_DEFAULTS["throttle_plate_overrun_pct"])) + self.dk_tau.set(e.get("throttle_plate_tau_s", ENGINE_DEFAULTS["throttle_plate_tau_s"])) + self.tq_kp.set(e.get("torque_ctrl_kp", ENGINE_DEFAULTS["torque_ctrl_kp"])) + self.tq_ki.set(e.get("torque_ctrl_ki", ENGINE_DEFAULTS["torque_ctrl_ki"])) - self.amb_c.set(e["coolant_ambient_c"]) - self.cold_k.set(e["idle_cold_gain_per_deg"]) - self.cold_max.set(e["idle_cold_gain_max"]) + self.jit_idle.set(e.get("rpm_jitter_idle_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_idle_amp_rpm"])) + self.jit_high.set(e.get("rpm_jitter_high_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_high_amp_rpm"])) + self.jit_tau.set(e.get("rpm_jitter_tau_s", ENGINE_DEFAULTS["rpm_jitter_tau_s"])) + self.jit_off.set(e.get("rpm_jitter_off_threshold_rpm", ENGINE_DEFAULTS["rpm_jitter_off_threshold_rpm"])) - self.pedal.set(e["throttle_pedal_pct"]) + self.pedal.set(e.get("throttle_pedal_pct", ENGINE_DEFAULTS["throttle_pedal_pct"])) self._on_pedal_change() def apply(self): cfg = {"engine": { + # Idle & Kaltlauf (Zweipunkt) "idle_rpm": int(self.idle.get()), + "cold_idle_rpm": int(self.cold_idle.get()), + "cold_idle_end_c": float(self.cold_end.get()), + "idle_cold_mode": self.cold_mode.get(), + + # Basis/Leistung "max_rpm": int(self.maxrpm.get()), "rpm_rise_per_s": int(self.rise.get()), "rpm_fall_per_s": int(self.fall.get()), "throttle_curve": self.curve.get(), - "engine_power_kw": float(self.power.get()), "torque_peak_rpm": float(self.tqpeak.get()), + # Start/Abwürgen "starter_rpm_nominal": float(self.st_nom.get()), "starter_voltage_min": float(self.st_vmin.get()), "start_rpm_threshold": float(self.st_thr.get()), "stall_rpm": float(self.stall.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()), - + # Umgebung & DBW + "coolant_ambient_c": float(self.amb_c.get()), "throttle_plate_idle_min_pct": float(self.dk_idle.get()), "throttle_plate_overrun_pct": float(self.dk_over.get()), "throttle_plate_tau_s": float(self.dk_tau.get()), "torque_ctrl_kp": float(self.tq_kp.get()), "torque_ctrl_ki": float(self.tq_ki.get()), + # Jitter "rpm_jitter_idle_amp_rpm": float(self.jit_idle.get()), "rpm_jitter_high_amp_rpm": float(self.jit_high.get()), "rpm_jitter_tau_s": float(self.jit_tau.get()), "rpm_jitter_off_threshold_rpm": float(self.jit_off.get()), - "coolant_ambient_c": float(self.amb_c.get()), - "idle_cold_gain_per_deg": float(self.cold_k.get()), - "idle_cold_gain_max": float(self.cold_max.get()), - + # UI "throttle_pedal_pct": float(self.pedal.get()), }} self.sim.load_config(cfg)