Files
OBD2-Simulator/app/simulation/modules/gearbox.py

228 lines
9.6 KiB
Python

# =============================
# app/simulation/modules/gearbox.py
# =============================
from __future__ import annotations
from app.simulation.simulator import Module, Vehicle
import math
GEARBOX_DEFAULTS = {
# Übersetzungen
"primary_ratio": 1.84, # Kurbelwelle -> Getriebeeingang
# Gangübersetzungen (Index 0 = Neutral/N = 0.0)
"gear_ratios": [0.0, 2.60, 1.90, 1.55, 1.35, 1.20, 1.07],
# Ketten-/Endübersetzung via Zähne
"front_sprocket_teeth": 16,
"rear_sprocket_teeth": 45,
# Rad/Reifen
"wheel_radius_m": 0.31, # dynamischer Halbmesser
"drivetrain_efficiency": 0.93, # Wirkungsgrad Kurbel -> Rad
"rpm_couple_gain": 0.20, # wie stark Engine-RPM zum Rad synchronisiert wird (0..1)
# Fahrzeug / Widerstände
"rolling_c": 0.015, # Rollwiderstandskoeff.
"air_density": 1.2, # kg/m^3
"aero_cd": 0.6,
"frontal_area_m2": 0.6,
# Kupplung (auto)
"clutch_max_torque_nm": 220.0, # max übertragbares Drehmoment (bei c=1)
"clutch_aggressiveness": 0.6, # 0..1 (0 = sehr sanft, 1 = sehr bissig)
"clutch_curve": "linear", # "linear" | "progressive" | "soft"
"clutch_drag_nm": 1.0, # Restschleppmoment bei getrennt
"shift_time_s": 0.15, # Schaltzeit, während der entkuppelt wird
"sync_rpm_band": 200.0, # RPM-Band, in dem als „synchron“ gilt
# Reifenhaftung (einfaches Limit)
"tire_mu_peak": 1.10, # statischer Reibkoeffizient (Peak)
"tire_mu_slide": 0.85, # Gleitreibung
"rear_static_weight_frac": 0.60 # statischer Lastanteil auf Antriebsrad
}
class GearboxModule(Module):
PRIO = 30
NAME = "gearbox"
def __init__(self):
# interner Zustand
self._clutch = 0.0 # 0..1
self._shift_t = 0.0
self._target_gear = None
self._wheel_v = 0.0 # m/s
def apply(self, v: Vehicle, dt: float) -> None:
# --- Dashboard-Registrierungen ---
v.register_metric("speed_kmh", label="Geschwindigkeit", unit="km/h", fmt=".1f", source="gearbox", priority=30)
v.register_metric("gear", label="Gang", fmt="", source="gearbox", priority=25)
v.register_metric("clutch_pct", label="Kupplung", unit="%", fmt=".0f", source="gearbox", priority=26)
v.register_metric("wheel_slip_pct", label="Reifenschlupf", unit="%", fmt=".0f", source="gearbox", priority=27)
# --- Config / Inputs ---
gb = dict(GEARBOX_DEFAULTS)
gb.update(v.config.get("gearbox", {}))
primary = float(gb["primary_ratio"])
gear_ratios = list(gb["gear_ratios"])
z_f = int(gb["front_sprocket_teeth"])
z_r = int(gb["rear_sprocket_teeth"])
final = (z_r / max(1, z_f))
r_w = float(gb["wheel_radius_m"])
eta = float(gb["drivetrain_efficiency"])
couple_gain = float(gb["rpm_couple_gain"])
c_rr = float(gb["rolling_c"])
rho = float(gb["air_density"])
cd = float(gb["aero_cd"])
A = float(gb["frontal_area_m2"])
clutch_Tmax = float(gb["clutch_max_torque_nm"])
clutch_agr = min(1.0, max(0.0, float(gb["clutch_aggressiveness"])))
clutch_curve= str(gb["clutch_curve"]).lower()
clutch_drag = float(gb["clutch_drag_nm"])
shift_time = float(gb["shift_time_s"])
sync_band = float(gb["sync_rpm_band"])
mu_peak = float(gb["tire_mu_peak"])
mu_slide= float(gb["tire_mu_slide"])
rear_w = float(gb["rear_static_weight_frac"])
m = float(v.config.get("vehicle", {}).get("mass_kg", 210.0))
g = 9.81
# State
gear = int(v.ensure("gear", 0))
ign = str(v.ensure("ignition", "OFF"))
rpm = float(v.ensure("rpm", 1200.0))
pedal= float(v.ensure("throttle_pedal_pct", 0.0))
pedal = max(0.0, min(100.0, pedal))
# verfügbare Motordaten
eng_avail_T = float(v.get("engine_available_torque_nm", 0.0)) # „kann liefern“
# Hinweis: die Engine zieht später v.acc_total("engine.torque_load_nm") ab.
# Pending Shift Commands (vom UI gesetzt und dann zurücksetzen)
up_req = bool(v.ensure("gear_shift_up", False))
down_req = bool(v.ensure("gear_shift_down", False))
to_N_req = bool(v.ensure("gear_set_neutral", False))
if up_req: v.set("gear_shift_up", False)
if down_req: v.set("gear_shift_down", False)
if to_N_req: v.set("gear_set_neutral", False)
# --- Schaltlogik ---
if self._shift_t > 0.0:
self._shift_t -= dt
# währenddessen Kupplung öffnen
self._clutch = max(0.0, self._clutch - self._rate_from_agr(1.0, clutch_agr) * dt)
if self._shift_t <= 0.0 and self._target_gear is not None:
gear = int(self._target_gear)
v.set("gear", gear)
self._target_gear = None
else:
# neue Requests annehmen, wenn nicht bereits am Limit
if to_N_req:
self._target_gear = 0
self._shift_t = shift_time
elif up_req and gear < min(6, len(gear_ratios)-1):
self._target_gear = gear + 1
self._shift_t = shift_time
elif down_req and gear > 0:
self._target_gear = gear - 1
self._shift_t = shift_time
# --- Gesamtübersetzung und Soll-Drehzahlbezug ---
gear_ratio = float(gear_ratios[gear]) if 0 <= gear < len(gear_ratios) else 0.0
overall = primary * gear_ratio * final # Kurbel -> Rad
wheel_omega = self._wheel_v / max(1e-6, r_w) # rad/s
eng_omega_from_wheel = wheel_omega * overall
rpm_from_wheel = eng_omega_from_wheel * 60.0 / (2.0 * math.pi)
# --- Kupplungs-Automat ---
# Zielschließung aus Schlupf und Fahrerwunsch
slip_rpm = abs(rpm - rpm_from_wheel)
slip_norm = min(1.0, slip_rpm / max(1.0, sync_band))
base_target = max(0.0, min(1.0, (pedal/100.0)*0.6 + (1.0 - slip_norm)*0.6))
target_c = self._shape(base_target, clutch_curve)
# Bei N oder ohne Übersetzung kein Kraftschluss
if gear == 0 or overall <= 1e-6 or ign in ("OFF","ACC"):
target_c = 0.0
# Sanfte Anti-Abwürg-Logik: ist RPM sehr niedrig und Radlast hoch -> etwas öffnen
if rpm < 1500.0 and slip_rpm > 200.0:
target_c = min(target_c, 0.6)
# Dynamik der Kupplung (Annäherung Richtung target_c)
rate = self._rate_from_agr(target_c, clutch_agr) # s^-1
self._clutch += (target_c - self._clutch) * min(1.0, rate * dt)
self._clutch = max(0.0, min(1.0, self._clutch))
# --- Übertragbares Motormoment durch Kupplung ---
clutch_cap = clutch_Tmax * self._clutch
T_engine_to_input = max(0.0, min(eng_avail_T, clutch_cap))
# --- Rad-Seite: aus Motor via Übersetzung ---
T_wheel_from_engine = T_engine_to_input * overall * eta if overall > 0.0 else 0.0 # Nm am Rad
# --- Reibungs-/Luftwiderstand ---
v_ms = max(0.0, self._wheel_v)
F_roll = m * g * c_rr
F_aero = 0.5 * rho * cd * A * v_ms * v_ms
F_res = F_roll + F_aero
# --- Reifen-Force-Limit & Schlupf ---
N_rear = m * g * rear_w
F_trac_cap = mu_peak * N_rear
F_from_engine = T_wheel_from_engine / max(1e-6, r_w)
slip = 0.0
F_trac = F_from_engine
if abs(F_from_engine) > F_trac_cap:
slip = min(1.0, (abs(F_from_engine) - F_trac_cap) / max(1.0, F_from_engine))
# im Schlupf auf Slide-Niveau kappen
F_trac = math.copysign(mu_slide * N_rear, F_from_engine)
# --- Fahrzeugdynamik: a = (F_trac - F_res)/m ---
a = (F_trac - F_res) / max(1.0, m)
self._wheel_v = max(0.0, self._wheel_v + a * dt)
speed_kmh = self._wheel_v * 3.6
v.set("speed_kmh", float(speed_kmh))
v.set("gear", int(gear))
v.set("clutch_pct", float(self._clutch * 100.0))
v.set("wheel_slip_pct", float(max(0.0, min(1.0, slip)) * 100.0))
# --- Reaktionsmoment zurück an den Motor (Last) ---
# aus tatsächlich wirkender Traktionskraft (nach Grip-Limit)
T_engine_load = 0.0
if overall > 0.0 and self._clutch > 0.0:
T_engine_load = (abs(F_trac) * r_w) / max(1e-6, (overall * eta))
# kleiner Schlepp bei getrennt
if self._clutch < 0.1:
T_engine_load += clutch_drag * (1.0 - self._clutch)
if T_engine_load > 0.0:
v.push("engine.torque_load_nm", +T_engine_load, source="driveline")
# --- RPM-Kopplung (sanfte Synchronisierung) ---
if overall > 0.0 and self._clutch > 0.2 and ign in ("ON","START"):
alpha = min(1.0, couple_gain * self._clutch * dt / max(1e-3, 0.1))
rpm = (1.0 - alpha) * rpm + alpha * rpm_from_wheel
v.set("rpm", float(rpm))
# ----- Helpers -----
def _rate_from_agr(self, target_c: float, agr: float) -> float:
"""Engage/Release-Geschwindigkeit [1/s] in Abhängigkeit der Aggressivität."""
# 0.05s (bissig) bis 0.5s (sanft) für ~63%-Annäherung
tau = 0.5 - 0.45 * agr
if target_c < 0.1: # Öffnen etwas flotter
tau *= 0.7
return 1.0 / max(0.05, tau)
def _shape(self, x: float, curve: str) -> float:
x = max(0.0, min(1.0, x))
if curve == "progressive":
return x * x
if curve == "soft":
return math.sqrt(x)
return x # linear