#pragma once #include "FluidSim/MachinePorts.hpp" #include #include namespace fluidsim::port { // Accepts only coherent, current channels from one physical crank pickup. // Signed reverse measurements remain valid sensors, but cannot drive this // forward-only ignition controller. A stationary crank is valid. std::optional ecu_crank_sample(const SignalValue& speed, const SignalValue& angle,double time); class CrankIgnitionClock { public: struct Result {bool valid{};unsigned sparks{};}; // Crank-only wasted spark: one event per revolution at the configured // pickup angle, without consulting a cylinder's hidden 720-degree phase. Result sample(const SignalValue& speed,const SignalValue& angle, double time,double advance_degrees,double tdc_degrees=0); void reset(){previous_.reset();last_event_cycle_.reset();} private: std::optional previous_; std::optional last_event_cycle_; }; // Ideal sensors and injectors; calibration parameters are not engine measurements. struct EcuConfig { enum class IdleControl { Feedback, FixedOpening }; IdleControl idle_control{IdleControl::Feedback}; struct IgnitionPoint { float rpm, degrees_btdc; }; std::vector ignition_map{{0, 10}, {15000, 40}}; float idle_rpm{1800.0f}; float idle_base_opening{0.08f}, idle_max_opening{0.25f}; float idle_kp{0.0001f}, idle_ki{0.00004f}; float throttle_time_constant{0.001f}, throttle_rate{1000.0f}; // seconds, full travel/second // Zero uses the opening calibration, preserving symmetric response. float throttle_closing_time_constant{}, throttle_closing_rate{}; float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f}; bool rev_limit_enabled{true}; bool rev_limit_fuel_cut_enabled{true}; bool rev_limit_ignition_cut_enabled{true}; float rev_limit_cut_time{}; // Minimum cut duration in seconds, followed by RPM hysteresis. float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f}; bool overrun_fuel_cut_enabled{true}; bool overrun_ignition_cut_enabled{false}; float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC float stoichiometric_afr{14.7f}, power_afr{12.8f}; }; class Ecu : public EcuConfig { public: void step(float dt, float pedal, float rpm); float fuel_for_air(float trapped_air_mass) const; float throttle_opening() const { return opening_; } float throttle_target() const { return target_; } float target_afr() const { return afr_; } float spark_advance() const { return spark_; } bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; } bool ignition_cut() const { return (limiter_ && rev_limit_ignition_cut_enabled) || (overrun_ && overrun_ignition_cut_enabled) || fault_; } bool rev_limited() const { return limiter_; } bool idle_active() const { return idle_; } bool sensor_fault() const { return fault_; } private: float filtered_rpm_{}; bool initialized_{}; float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f}; bool limiter_{}, overrun_{}, fault_{}, idle_{}; float limiter_time_{}; }; // Separate from the legacy ECU: measurements are supplied through typed wires. // Alpha-N cells are calibrated kg per cylinder per four-stroke cycle, indexed // by RPM rows and measured TPS columns. No trapped-air-mass input is available. struct SensorEcuConfig { enum class Strategy {Ignition,AlphaN,SpeedDensity}; Strategy strategy{Strategy::Ignition}; std::vector ignition_map{{0,10},{6000,30}}; std::vector fuel_rpm{0,6000},fuel_tps{0,1}; std::vector fuel_per_cycle{0.000002,0.00002,0.000002,0.00002}; // Open-loop dry ideal-gas estimate per cylinder. Geometry, VE and fuel AFR // are calibration; MAP and IAT must arrive as current wired measurements. double displacement{.0003},air_gas_constant{287},target_afr{14.7}; std::vector ve_rpm{0,6000},ve_pressure{20000,100000}; std::vector volumetric_efficiency{.9,.9,.9,.9}; bool rev_limit_enabled{true}; double rev_limit_rpm{7000},rev_resume_rpm{6800}; }; inline constexpr int sensor_ecu_strategy_count=3; const char* sensor_ecu_strategy_key(SensorEcuConfig::Strategy); const char* sensor_ecu_strategy_name(SensorEcuConfig::Strategy); SensorEcuConfig::Strategy sensor_ecu_strategy_from_key(std::string_view); SensorEcuConfig::Strategy next_sensor_ecu_strategy(SensorEcuConfig::Strategy); struct SensorEcuMeasurements {SignalValue speed,angle,tps,map,iat;}; struct SensorEcuState { bool sensor_fault{true},rev_limited{}; double spark_advance{},fuel_per_cycle{},fuel_mass_flow{}; double estimated_air_per_cycle{},volumetric_efficiency{}; unsigned spark_events{}; }; class SensorEcu { public: explicit SensorEcu(SensorEcuConfig config); void apply_calibration(SensorEcuConfig config); const SensorEcuState& sample(const SignalValue& speed,const SignalValue& angle, const SignalValue& tps,double time); const SensorEcuState& sample(const SensorEcuMeasurements&,double time); const SensorEcuState& state()const{return state_;} void reset(){clock_.reset();state_={};limiter_=false;} private: SensorEcuConfig config_; CrankIgnitionClock clock_; SensorEcuState state_; bool limiter_{}; }; } // namespace fluidsim::port