LCOV - code coverage report
Current view: top level - physics - GripLoadEstimation.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 236 236
Test Date: 2026-03-18 19:01:10 Functions: 100.0 % 12 12
Branches: 78.0 % 186 145

             Branch data     Line data    Source code
       1                 :             : // GripLoadEstimation.cpp
       2                 :             : // ---------------------------------------------------------------------------
       3                 :             : // Grip and Load Estimation methods for FFBEngine.
       4                 :             : //
       5                 :             : // This file is a source-level split of FFBEngine.cpp (Approach A).
       6                 :             : // All functions here are FFBEngine member methods; FFBEngine.h is unchanged.
       7                 :             : //
       8                 :             : // Rationale: These functions form a self-contained "data preparation" layer —
       9                 :             : // they take raw (possibly broken/encrypted) telemetry and produce the best
      10                 :             : // available grip and load values. The FFB engine then consumes those values
      11                 :             : // without needing to know how they were estimated.
      12                 :             : //
      13                 :             : // See docs/dev_docs/reports/FFBEngine_refactoring_analysis.md for full details.
      14                 :             : // ---------------------------------------------------------------------------
      15                 :             : 
      16                 :             : #include "FFBEngine.h"
      17                 :             : #include "Config.h"
      18                 :             : #include "Logger.h"
      19                 :             : #include "Logger.h"
      20                 :             : #include <iostream>
      21                 :             : #include <mutex>
      22                 :             : 
      23                 :             : extern std::recursive_mutex g_engine_mutex;
      24                 :             : #include <cmath>
      25                 :             : #include "StringUtils.h"
      26                 :             : 
      27                 :             : using namespace ffb_math;
      28                 :             : 
      29                 :             : // Helper: Learn static front and rear load reference (v0.7.46, expanded v0.7.164)
      30                 :       16002 : void FFBEngine::update_static_load_reference(double current_front_load, double current_rear_load, double speed, double dt) {
      31         [ +  - ]:       16002 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
      32         [ +  + ]:       16002 :     if (m_static_load_latched) return; // Do not update if latched
      33                 :             : 
      34   [ +  +  +  + ]:        3595 :     if (speed > 2.0 && speed < 15.0) {
      35                 :             :         // Front Load Learning
      36         [ +  + ]:        2171 :         if (m_static_front_load < 100.0) {
      37                 :           3 :              m_static_front_load = current_front_load;
      38                 :             :         } else {
      39                 :        2168 :              m_static_front_load += (dt / 5.0) * (current_front_load - m_static_front_load);
      40                 :             :         }
      41                 :             :         // Rear Load Learning
      42         [ +  + ]:        2171 :         if (m_static_rear_load < 100.0) {
      43                 :           9 :              m_static_rear_load = current_rear_load;
      44                 :             :         } else {
      45                 :        2162 :              m_static_rear_load += (dt / 5.0) * (current_rear_load - m_static_rear_load);
      46                 :             :         }
      47   [ +  +  +  +  :        1424 :     } else if (speed >= 15.0 && m_static_front_load > 1000.0 && m_static_rear_load > 1000.0) {
                   +  + ]
      48                 :             :         // Latch the value once we exceed 15 m/s (aero begins to take over)
      49                 :         148 :         m_static_load_latched = true;
      50                 :             : 
      51                 :             :         // Save to config map (v0.7.70)
      52         [ +  - ]:         148 :         std::string vName = m_metadata.GetVehicleName();
      53   [ +  -  +  +  :         148 :         if (vName != "Unknown" && vName != "") {
          +  -  +  -  +  
                      + ]
      54         [ +  - ]:          20 :             Config::SetSavedStaticLoad(vName, m_static_front_load);
      55   [ +  -  +  - ]:          20 :             Config::SetSavedStaticLoad(vName + "_rear", m_static_rear_load);
      56                 :          20 :             Config::m_needs_save = true; // Flag main thread to write to disk
      57   [ +  -  +  - ]:          20 :             Logger::Get().LogFile("[FFB] Latched and saved static loads for %s: F=%.2fN, R=%.2fN", vName.c_str(), m_static_front_load, m_static_rear_load);
      58                 :             :         }
      59                 :         148 :     }
      60                 :             : 
      61                 :             :     // Safety fallback: Ensure we have a sane baseline if learning failed
      62         [ +  + ]:        3595 :     if (m_static_front_load < 1000.0) {
      63                 :           4 :         m_static_front_load = m_auto_peak_front_load * 0.5;
      64                 :             :     }
      65         [ +  + ]:        3595 :     if (m_static_rear_load < 1000.0) {
      66                 :         175 :         m_static_rear_load = m_auto_peak_front_load * 0.5;
      67                 :             :     }
      68         [ +  + ]:       16002 : }
      69                 :             : 
      70                 :             : // Initialize the load reference based on vehicle class and name seeding
      71                 :         129 : void FFBEngine::InitializeLoadReference(const char* className, const char* vehicleName) {
      72         [ +  - ]:         129 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
      73                 :             : 
      74                 :             :     // v0.7.109: Perform a full normalization reset on car change
      75                 :             :     // This ensures that session-learned peaks from a previous car don't pollute the new session.
      76         [ +  - ]:         129 :     ResetNormalization();
      77                 :             : 
      78                 :             :     // --- FIX #374 & #379: Reset all historical data across car change ---
      79                 :         129 :     m_metadata.ResetWarnings();
      80                 :         129 :     m_missing_load_frames = 0;
      81                 :         129 :     m_missing_lat_force_front_frames = 0;
      82                 :         129 :     m_missing_lat_force_rear_frames = 0;
      83                 :         129 :     m_missing_susp_force_frames = 0;
      84                 :         129 :     m_missing_susp_deflection_frames = 0;
      85                 :         129 :     m_missing_vert_deflection_frames = 0;
      86                 :             : 
      87                 :         129 :     m_warned_load = false;
      88                 :         129 :     m_warned_grip = false;
      89                 :         129 :     m_warned_rear_grip = false;
      90                 :         129 :     m_warned_lat_force_front = false;
      91                 :         129 :     m_warned_lat_force_rear = false;
      92                 :         129 :     m_warned_susp_force = false;
      93                 :         129 :     m_warned_susp_deflection = false;
      94                 :         129 :     m_warned_vert_deflection = false;
      95                 :             : 
      96                 :             :     // Reset seeding flags to trigger fresh capture on first frame of new car
      97                 :         129 :     m_yaw_rate_seeded = false;
      98                 :         129 :     m_yaw_accel_seeded = false;
      99                 :         129 :     m_local_vel_seeded = false;
     100                 :         129 :     m_yaw_rate_log_seeded = false;
     101                 :         129 :     m_derivatives_seeded = false;
     102                 :             : 
     103                 :             :     // Clear slope detection buffers
     104                 :         129 :     m_slope_buffer_count = 0;
     105                 :         129 :     m_slope_buffer_index = 0;
     106         [ +  - ]:         129 :     m_slope_lat_g_buffer.fill(0.0);
     107         [ +  - ]:         129 :     m_slope_slip_buffer.fill(0.0);
     108         [ +  - ]:         129 :     m_slope_torque_buffer.fill(0.0);
     109         [ +  - ]:         129 :     m_slope_steer_buffer.fill(0.0);
     110                 :         129 :     m_slope_current = 0.0;
     111                 :         129 :     m_slope_smoothed_output = 1.0;
     112                 :             :     // -----------------------------------------------------------------------
     113                 :             : 
     114         [ +  - ]:         129 :     ParsedVehicleClass vclass = ParseVehicleClass(className, vehicleName);
     115                 :             : 
     116                 :             :     // Stage 3 Reset: Ensure peak load starts at class baseline
     117         [ +  - ]:         129 :     m_auto_peak_front_load = GetDefaultLoadForClass(vclass);
     118                 :             : 
     119   [ +  -  +  - ]:         129 :     std::string vName = vehicleName ? vehicleName : "Unknown";
     120                 :             : 
     121                 :             :     // Check if we already have a saved static load for this specific car (v0.7.70)
     122                 :         129 :     double saved_front_load = 0.0;
     123                 :         129 :     double saved_rear_load = 0.0;
     124   [ +  -  +  + ]:         129 :     if (Config::GetSavedStaticLoad(vName, saved_front_load)) {
     125                 :          64 :         m_static_front_load = saved_front_load;
     126                 :             : 
     127   [ +  -  +  -  :          64 :         if (Config::GetSavedStaticLoad(vName + "_rear", saved_rear_load)) {
                   +  + ]
     128                 :          55 :             m_static_rear_load = saved_rear_load;
     129                 :             :         } else {
     130                 :             :             // Migration: If we have front but no rear, estimate rear based on class default
     131                 :           9 :             m_static_rear_load = m_auto_peak_front_load * 0.5;
     132                 :             :         }
     133                 :             : 
     134                 :          64 :         m_static_load_latched = true; // Skip the 2-15 m/s learning phase
     135   [ +  -  +  - ]:          64 :         Logger::Get().LogFile("[FFB] Loaded persistent static loads for %s: F=%.2fN, R=%.2fN", vName.c_str(), m_static_front_load, m_static_rear_load);
     136                 :             :     } else {
     137                 :             :         // Reset static load reference for new car class
     138                 :          65 :         m_static_front_load = m_auto_peak_front_load * 0.5;
     139                 :          65 :         m_static_rear_load = m_auto_peak_front_load * 0.5;
     140                 :          65 :         m_static_load_latched = false;
     141   [ +  -  +  - ]:          65 :         Logger::Get().LogFile("[FFB] No saved load for %s. Learning required.", vName.c_str());
     142                 :             :     }
     143                 :             : 
     144                 :         129 :     m_smoothed_vibration_mult = 1.0;
     145                 :             : 
     146                 :             :     // v0.7.119: Update engine's car name immediately to prevent seeding loop (Issue #238)
     147         [ +  - ]:         129 :     m_metadata.m_last_handled_vehicle_name = vName;
     148                 :             : 
     149                 :             :     // Sync metadata manager state
     150         [ +  - ]:         129 :     m_metadata.UpdateInternal(className, vehicleName, nullptr);
     151                 :             : 
     152                 :             :     // Issue #368: Log strings with quotes to reveal hidden spaces if detection fails
     153   [ +  -  +  +  :         129 :     Logger::Get().LogFile("[FFB] Vehicle Identification -> Detected Class: %s | Seed Load: %.2fN (Raw -> Class: '%s', Name: '%s')",
             +  -  +  - ]
     154                 :             :         VehicleClassToString(vclass), m_auto_peak_front_load, (className ? className : "Unknown"), vName.c_str());
     155                 :         129 : }
     156                 :             : 
     157                 :             : // Helper: Calculate Raw Slip Angle for a pair of wheels (v0.4.9 Refactor)
     158                 :             : // Returns the average slip angle of two wheels using atan2(lateral_vel, longitudinal_vel)
     159                 :             : // v0.4.19: Removed abs() from lateral velocity to preserve sign for debug visualization
     160                 :       31673 : double FFBEngine::calculate_raw_slip_angle_pair(const TelemWheelV01& w1, const TelemWheelV01& w2) {
     161                 :       31673 :     double v_long_1 = std::abs(w1.mLongitudinalGroundVel);
     162                 :       31673 :     double v_long_2 = std::abs(w2.mLongitudinalGroundVel);
     163         [ +  + ]:       31673 :     if (v_long_1 < MIN_SLIP_ANGLE_VELOCITY) v_long_1 = MIN_SLIP_ANGLE_VELOCITY;
     164         [ +  + ]:       31673 :     if (v_long_2 < MIN_SLIP_ANGLE_VELOCITY) v_long_2 = MIN_SLIP_ANGLE_VELOCITY;
     165                 :       31673 :     double raw_angle_1 = std::atan2(w1.mLateralPatchVel, v_long_1);
     166                 :       31673 :     double raw_angle_2 = std::atan2(w2.mLateralPatchVel, v_long_2);
     167                 :       31673 :     return (raw_angle_1 + raw_angle_2) / 2.0;
     168                 :             : }
     169                 :             : 
     170                 :       63969 : double FFBEngine::calculate_slip_angle(const TelemWheelV01& w, double& prev_state, double dt) {
     171                 :       63969 :     double v_long = std::abs(w.mLongitudinalGroundVel);
     172         [ +  + ]:       63969 :     if (v_long < MIN_SLIP_ANGLE_VELOCITY) v_long = MIN_SLIP_ANGLE_VELOCITY;
     173                 :             :     
     174                 :             :     // v0.4.19: PRESERVE SIGN - Do NOT use abs() on lateral velocity
     175                 :             :     // Positive lateral vel (+X = left) → Positive slip angle
     176                 :             :     // Negative lateral vel (-X = right) → Negative slip angle
     177                 :             :     // This sign is critical for directional counter-steering
     178                 :       63969 :     double raw_angle = std::atan2(w.mLateralPatchVel, v_long);  // SIGN PRESERVED
     179                 :             :     
     180                 :             :     // LPF: Time Corrected Alpha (v0.4.37)
     181                 :             :     // Target: Alpha 0.1 at 400Hz (dt = 0.0025)
     182                 :             :     // Formula: alpha = dt / (tau + dt) -> 0.1 = 0.0025 / (tau + 0.0025) -> tau approx 0.0225s
     183                 :             :     // v0.4.40: Using configurable m_slip_angle_smoothing
     184                 :       63969 :     double tau = (double)m_slip_angle_smoothing;
     185         [ +  + ]:       63969 :     if (tau < 0.0001) tau = 0.0001; // Safety clamp 
     186                 :             :     
     187                 :       63969 :     double alpha = dt / (tau + dt);
     188                 :             :     
     189                 :             :     // Safety clamp
     190                 :       63969 :     alpha = (std::min)(1.0, (std::max)(0.001, alpha));
     191                 :       63969 :     prev_state = prev_state + alpha * (raw_angle - prev_state);
     192                 :       63969 :     return prev_state;
     193                 :             : }
     194                 :             : 
     195                 :             : // Helper: Calculate Axle Grip with Fallback (v0.4.6 Hardening)
     196                 :             : // This function calculates the average grip for a pair of wheels (axle).
     197                 :             : // If the primary telemetry grip is missing, it reconstructs it from slip angle and ratio.
     198                 :             : // The avg_axle_load parameter is used as a threshold for triggering the reconstruction fallback.
     199                 :       31984 : GripResult FFBEngine::calculate_axle_grip(const TelemWheelV01& w1,
     200                 :             :                           const TelemWheelV01& w2,
     201                 :             :                           double avg_axle_load,
     202                 :             :                           bool& warned_flag,
     203                 :             :                           double& prev_slip1,
     204                 :             :                           double& prev_slip2,
     205                 :             :                           double& prev_load1, // NEW: State for load smoothing
     206                 :             :                           double& prev_load2, // NEW: State for load smoothing
     207                 :             :                           double car_speed,
     208                 :             :                           double dt,
     209                 :             :                           const char* vehicleName,
     210                 :             :                           const TelemInfoV01* data,
     211                 :             :                           bool is_front) {
     212                 :             :     // Note on mGripFract: The LMU InternalsPlugin.hpp comments state this is the
     213                 :             :     // "fraction of the contact patch that is sliding". This is poorly phrased.
     214                 :             :     // In actual telemetry output, 1.0 = Full Adhesion (Gripping) and 0.0 = Fully Sliding.
     215                 :             :     GripResult result;
     216                 :       31984 :     double total_load = w1.mTireLoad + w2.mTireLoad;
     217         [ +  + ]:       31984 :     if (total_load > 1.0) {
     218                 :       25509 :         result.original = (w1.mGripFract * w1.mTireLoad + w2.mGripFract * w2.mTireLoad) / total_load;
     219                 :             :     } else {
     220                 :             :         // Fallback for zero load (e.g. jump/missing data)
     221                 :        6475 :         result.original = (w1.mGripFract + w2.mGripFract) / 2.0;
     222                 :             :     }
     223                 :             : 
     224                 :       31984 :     result.value = result.original;
     225                 :       31984 :     result.approximated = false;
     226                 :       31984 :     result.slip_angle = 0.0;
     227                 :             :     
     228                 :             :     // ==================================================================================
     229                 :             :     // CRITICAL LOGIC FIX (v0.4.14) - DO NOT MOVE INSIDE CONDITIONAL BLOCK
     230                 :             :     // ==================================================================================
     231                 :             :     // We MUST calculate slip angle every single frame, regardless of whether the 
     232                 :             :     // grip fallback is triggered or not.
     233                 :             :     //
     234                 :             :     // Reason 1 (Physics State): The Low Pass Filter (LPF) inside calculate_slip_angle 
     235                 :             :     //           relies on continuous execution. If we skip frames (because telemetry 
     236                 :             :     //           is good), the 'prev_slip' state becomes stale. When telemetry eventually 
     237                 :             :     //           fails, the LPF will smooth against ancient history, causing a math spike.
     238                 :             :     //
     239                 :             :     // Reason 2 (Dependency): The 'Rear Aligning Torque' effect (calculated later) 
     240                 :             :     //           reads 'result.slip_angle'. If we only calculate this when grip is 
     241                 :             :     //           missing, the Rear Torque effect will toggle ON/OFF randomly based on 
     242                 :             :     //           telemetry health, causing violent kicks and "reverse FFB" sensations.
     243                 :             :     // ==================================================================================
     244                 :       31984 :     double slip1 = calculate_slip_angle(w1, prev_slip1, dt);
     245                 :       31984 :     double slip2 = calculate_slip_angle(w2, prev_slip2, dt);
     246                 :       31984 :     result.slip_angle = (slip1 + slip2) / 2.0;
     247                 :             : 
     248                 :             :     // ==================================================================================
     249                 :             :     // SHADOW MODE: Always calculate slope grip if enabled (for diagnostics and logging)
     250                 :             :     // This allows us to compare our math against unencrypted cars to tune the algorithm.
     251                 :             :     // ==================================================================================
     252                 :       31984 :     double slope_grip_estimate = 1.0;
     253   [ +  +  +  +  :       31984 :     if (is_front && data && car_speed >= 5.0) {
                   +  + ]
     254                 :       14699 :         slope_grip_estimate = calculate_slope_grip(
     255                 :       14699 :             data->mLocalAccel.x / 9.81,
     256                 :             :             result.slip_angle,
     257                 :             :             dt,
     258                 :             :             data
     259                 :             :         );
     260                 :             :     }
     261                 :             : 
     262                 :             :     // Fallback condition: Grip is essentially zero BUT car has significant load
     263   [ +  +  +  + ]:       31984 :     if (result.value < 0.0001 && avg_axle_load > 100.0) {
     264                 :       27882 :         result.approximated = true;
     265                 :             :         
     266         [ +  + ]:       27882 :         if (car_speed < 5.0) {
     267                 :             :             // Note: We still keep the calculated slip_angle in result.slip_angle
     268                 :             :             // for visualization/rear torque, even if we force grip to 1.0 here.        
     269                 :         876 :             result.value = 1.0; 
     270                 :             :         } else {
     271   [ +  +  +  +  :       27006 :             if (m_slope_detection_enabled && is_front && data) {
                   +  - ]
     272                 :        2042 :                 result.value = slope_grip_estimate;
     273                 :             :             } else {
     274                 :             :                 // --- REFINED: Load-Sensitive Continuous Friction Circle ---
     275                 :             :                 
     276                 :       49928 :                 auto calc_wheel_grip = [&](const TelemWheelV01& w, double slip_angle, double& prev_load) {
     277                 :             :                     // 1. Dynamic Load Sensitivity with Slew/Smoothing
     278   [ +  +  +  - ]:       49928 :                     double raw_load = warned_flag ? approximate_load(w) : w.mTireLoad;
     279                 :             :                     
     280                 :             :                     // Smooth the load to prevent curb strikes from causing threshold jitter (50ms tau)
     281                 :       49928 :                     double load_alpha = dt / (0.050 + dt);
     282                 :       49928 :                     prev_load += load_alpha * (raw_load - prev_load);
     283                 :       49928 :                     double current_load = prev_load;
     284                 :             :                     
     285                 :             :                     // Calculate how loaded the tire is compared to the car's static weight
     286         [ +  + ]:       49928 :                     double static_ref = is_front ? m_static_front_load : m_static_rear_load;
     287                 :       49928 :                     double load_ratio = current_load / (static_ref + 1.0);
     288                 :       49928 :                     load_ratio = std::clamp(load_ratio, 0.25, 4.0); // Safety bounds
     289                 :             :                     
     290                 :             :                     // Tire physics: Optimal slip angle increases with load (Hertzian cube root)
     291                 :             :                     // Note: Future thermal/pressure multipliers would be applied here
     292                 :       49928 :                     double dynamic_slip_angle = m_optimal_slip_angle * std::pow(load_ratio, 0.333);
     293                 :             : 
     294                 :             :                     // 2. Lateral Component
     295                 :       49928 :                     double lat_metric = std::abs(slip_angle) / dynamic_slip_angle;
     296                 :             : 
     297                 :             :                     // 3. Longitudinal Component
     298                 :       49928 :                     double long_ratio = calculate_manual_slip_ratio(w, car_speed);
     299                 :       49928 :                     double long_metric = std::abs(long_ratio) / (double)m_optimal_slip_ratio;
     300                 :             : 
     301                 :             :                     // 4. Combined Vector (Friction Circle)
     302                 :       49928 :                     double combined_slip = std::sqrt((lat_metric * lat_metric) + (long_metric * long_metric));
     303                 :             : 
     304                 :             :                     // 5. Continuous Falloff Curve with Sliding Friction Asymptote
     305                 :       49928 :                     double cs2 = combined_slip * combined_slip;
     306                 :       49928 :                     double cs4 = cs2 * cs2;
     307                 :             :                     
     308                 :       49928 :                     const double MIN_SLIDING_GRIP = 0.05; 
     309                 :       49928 :                     return MIN_SLIDING_GRIP + ((1.0 - MIN_SLIDING_GRIP) / (1.0 + cs4)); 
     310                 :       24964 :                 };
     311                 :             : 
     312                 :             :                 // Calculate grip for each wheel independently, passing the state reference
     313         [ +  - ]:       24964 :                 double grip1 = calc_wheel_grip(w1, slip1, prev_load1);
     314         [ +  - ]:       24964 :                 double grip2 = calc_wheel_grip(w2, slip2, prev_load2);
     315                 :             : 
     316                 :             :                 // Average the resulting grip fractions
     317                 :       24964 :                 result.value = (grip1 + grip2) / 2.0;
     318                 :             :             }
     319                 :             :         }
     320                 :             :         
     321                 :             :         // Clamp to standard 0.0 - 1.0 bounds
     322                 :       27882 :         result.value = std::clamp(result.value, 0.0, 1.0);
     323                 :             :         
     324         [ +  + ]:       27882 :         if (!warned_flag) {
     325                 :         488 :             Logger::Get().LogFile("Warning: Data for mGripFract from the game seems to be missing for this car (%s). (Likely Encrypted/DLC Content). A fallback estimation will be used.", vehicleName);
     326                 :         488 :             warned_flag = true;
     327                 :             :         }
     328                 :             :     }    
     329                 :             : 
     330                 :             :     // Apply Adaptive Smoothing (v0.7.47)
     331         [ +  + ]:       31984 :     double& state = is_front ? m_front_grip_smoothed_state : m_rear_grip_smoothed_state;
     332                 :       63968 :     result.value = apply_adaptive_smoothing(result.value, state, dt,
     333                 :       31984 :                                             (double)m_grip_smoothing_steady,
     334                 :       31984 :                                             (double)m_grip_smoothing_fast,
     335                 :       31984 :                                             (double)m_grip_smoothing_sensitivity);
     336                 :             : 
     337                 :       31984 :     result.value = (std::max)(0.0, (std::min)(1.0, result.value));
     338                 :       31984 :     return result;
     339                 :             : }
     340                 :             : 
     341                 :             : // ========================================================================================
     342                 :             : // CRITICAL VEHICLE DYNAMICS NOTE: mSuspForce vs Wheel Load
     343                 :             : // ========================================================================================
     344                 :             : // The LMU telemetry channel `mSuspForce` represents the load on the internal PUSHROD,
     345                 :             : // NOT the load at the tire contact patch.
     346                 :             : // Because race cars use bellcranks, the pushrod has a mechanical advantage (Motion Ratio).
     347                 :             : // For example, a Hypercar with a Motion Ratio of 0.5 means the pushrod moves half as far
     348                 :             : // as the wheel, and therefore carries TWICE the force of the wheel.
     349                 :             : // To approximate the actual Tire Load, we MUST multiply mSuspForce by the Motion Ratio,
     350                 :             : // and then add the unsprung mass (the weight of the wheel, tire, and brakes).
     351                 :             : // ========================================================================================
     352                 :             : 
     353                 :             : // Helper: Approximate Load (v0.4.5 / Improved v0.7.171 / Refactored v0.7.175)
     354                 :             : // Uses class-specific motion ratios to convert pushrod force (mSuspForce) to wheel load,
     355                 :             : // and adds an estimate for front unsprung mass.
     356                 :       57335 : double FFBEngine::approximate_load(const TelemWheelV01& w) {
     357                 :       57335 :     ParsedVehicleClass vclass = m_metadata.GetCurrentClass();
     358                 :       57335 :     double motion_ratio = GetMotionRatioForClass(vclass);
     359                 :       57335 :     double unsprung_weight = GetUnsprungWeightForClass(vclass, false /* is_rear */);
     360                 :             : 
     361                 :       57335 :     return (std::max)(0.0, (w.mSuspForce * motion_ratio) + unsprung_weight);
     362                 :             : }
     363                 :             : 
     364                 :             : // Helper: Approximate Rear Load (v0.4.10 / Improved v0.7.171 / Refactored v0.7.175)
     365                 :             : // Similar to approximate_load, but uses rear-specific unsprung mass estimates.
     366                 :        6110 : double FFBEngine::approximate_rear_load(const TelemWheelV01& w) {
     367                 :        6110 :     ParsedVehicleClass vclass = m_metadata.GetCurrentClass();
     368                 :        6110 :     double motion_ratio = GetMotionRatioForClass(vclass);
     369                 :        6110 :     double unsprung_weight = GetUnsprungWeightForClass(vclass, true /* is_rear */);
     370                 :             : 
     371                 :        6110 :     return (std::max)(0.0, (w.mSuspForce * motion_ratio) + unsprung_weight);
     372                 :             : }
     373                 :             : 
     374                 :             : // Helper: Calculate Manual Slip Ratio (v0.4.6)
     375                 :       49933 : double FFBEngine::calculate_manual_slip_ratio(const TelemWheelV01& w, double car_speed_ms) {
     376                 :             :     // Safety Trap: Force 0 slip at very low speeds (v0.4.6)
     377         [ +  + ]:       49933 :     if (std::abs(car_speed_ms) < 2.0) return 0.0;
     378                 :             : 
     379                 :             :     // Radius in meters (stored as cm unsigned char)
     380                 :             :     // Explicit cast to double before division (v0.4.6)
     381                 :       49930 :     double radius_m = (double)w.mStaticUndeflectedRadius / 100.0;
     382         [ +  + ]:       49930 :     if (radius_m < 0.1) radius_m = 0.33; // Fallback if 0 or invalid
     383                 :             :     
     384                 :       49930 :     double wheel_vel = w.mRotation * radius_m;
     385                 :             :     
     386                 :             :     // Avoid div-by-zero at standstill
     387                 :       49930 :     double denom = std::abs(car_speed_ms);
     388         [ -  + ]:       49930 :     if (denom < 1.0) denom = 1.0;
     389                 :             :     
     390                 :             :     // Ratio = (V_wheel - V_car) / V_car
     391                 :             :     // Lockup: V_wheel < V_car -> Ratio < 0
     392                 :             :     // Spin: V_wheel > V_car -> Ratio > 0
     393                 :       49930 :     return (wheel_vel - car_speed_ms) / denom;
     394                 :             : }
     395                 :             : 
     396                 :             : // Helper: Calculate Grip Factor from Slope - v0.7.40 REWRITE
     397                 :             : // Robust projected slope estimation with hold-and-decay logic and torque-based anticipation.
     398                 :       15204 : double FFBEngine::calculate_slope_grip(double lateral_g, double slip_angle, double dt, const TelemInfoV01* data) {
     399                 :             :     // 1. Signal Hygiene (Slew Limiter & Pre-Smoothing)
     400                 :             : 
     401                 :             :     // Initialize slew limiter and smoothing state on first sample to avoid ramp-up transients
     402         [ +  + ]:       15204 :     if (m_slope_buffer_count == 0) {
     403                 :         262 :         m_slope_lat_g_prev = std::abs(lateral_g);
     404                 :         262 :         m_slope_lat_g_smoothed = std::abs(lateral_g);
     405                 :         262 :         m_slope_slip_smoothed = std::abs(slip_angle);
     406         [ +  + ]:         262 :         if (data) {
     407                 :         251 :             m_slope_torque_smoothed = std::abs(data->mSteeringShaftTorque);
     408                 :         251 :             m_slope_steer_smoothed = std::abs(data->mUnfilteredSteering);
     409                 :             :         }
     410                 :             :     }
     411                 :             : 
     412         [ +  - ]:       15204 :     double lat_g_slew = apply_slew_limiter(std::abs(lateral_g), m_slope_lat_g_prev, (double)m_slope_g_slew_limit, dt);
     413                 :       15204 :     m_debug_lat_g_slew = lat_g_slew;
     414                 :             : 
     415                 :       15204 :     double alpha_smooth = dt / (0.01 + dt);
     416                 :             : 
     417         [ +  + ]:       15204 :     if (m_slope_buffer_count > 0) {
     418                 :       14942 :         m_slope_lat_g_smoothed += alpha_smooth * (lat_g_slew - m_slope_lat_g_smoothed);
     419                 :       14942 :         m_slope_slip_smoothed += alpha_smooth * (std::abs(slip_angle) - m_slope_slip_smoothed);
     420         [ +  + ]:       14942 :         if (data) {
     421                 :       14455 :             m_slope_torque_smoothed += alpha_smooth * (std::abs(data->mSteeringShaftTorque) - m_slope_torque_smoothed);
     422                 :       14455 :             m_slope_steer_smoothed += alpha_smooth * (std::abs(data->mUnfilteredSteering) - m_slope_steer_smoothed);
     423                 :             :         }
     424                 :             :     }
     425                 :             : 
     426                 :             :     // 2. Update Buffers with smoothed values
     427                 :       15204 :     m_slope_lat_g_buffer[m_slope_buffer_index] = m_slope_lat_g_smoothed;
     428                 :       15204 :     m_slope_slip_buffer[m_slope_buffer_index] = m_slope_slip_smoothed;
     429         [ +  + ]:       15204 :     if (data) {
     430                 :       14706 :         m_slope_torque_buffer[m_slope_buffer_index] = m_slope_torque_smoothed;
     431                 :       14706 :         m_slope_steer_buffer[m_slope_buffer_index] = m_slope_steer_smoothed;
     432                 :             :     }
     433                 :             : 
     434                 :       15204 :     m_slope_buffer_index = (m_slope_buffer_index + 1) % SLOPE_BUFFER_MAX;
     435         [ +  + ]:       15204 :     if (m_slope_buffer_count < SLOPE_BUFFER_MAX) m_slope_buffer_count++;
     436                 :             : 
     437                 :             :     // 3. Calculate G-based Derivatives (Savitzky-Golay)
     438                 :       15204 :     double dG_dt = calculate_sg_derivative(m_slope_lat_g_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     439                 :       15204 :     double dAlpha_dt = calculate_sg_derivative(m_slope_slip_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     440                 :             : 
     441                 :       15204 :     m_slope_dG_dt = dG_dt;
     442                 :       15204 :     m_slope_dAlpha_dt = dAlpha_dt;
     443                 :             : 
     444                 :             :     // 4. Projected Slope Logic (G-based)
     445         [ +  + ]:       15204 :     if (std::abs(dAlpha_dt) > (double)m_slope_alpha_threshold) {
     446                 :        1606 :         m_slope_hold_timer = SLOPE_HOLD_TIME;
     447                 :        1606 :         m_debug_slope_num = dG_dt * dAlpha_dt;
     448                 :        1606 :         m_debug_slope_den = (dAlpha_dt * dAlpha_dt) + 0.000001;
     449                 :        1606 :         m_debug_slope_raw = m_debug_slope_num / m_debug_slope_den;
     450                 :        1606 :         m_slope_current = std::clamp(m_debug_slope_raw, -20.0, 20.0);
     451                 :             :     } else {
     452                 :       13598 :         m_slope_hold_timer -= dt;
     453         [ +  + ]:       13598 :         if (m_slope_hold_timer <= 0.0) {
     454                 :       13190 :             m_slope_hold_timer = 0.0;
     455                 :       13190 :             m_slope_current += (double)m_slope_decay_rate * dt * (0.0 - m_slope_current);
     456                 :             :         }
     457                 :             :     }
     458                 :             : 
     459                 :             :     // 5. Calculate Torque-based Slope (Pneumatic Trail Anticipation)
     460   [ +  +  +  + ]:       15204 :     volatile bool can_calc_torque = (m_slope_use_torque && data != nullptr);
     461         [ +  + ]:       15204 :     if (can_calc_torque) {
     462                 :       14703 :         double dTorque_dt = calculate_sg_derivative(m_slope_torque_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     463                 :       14703 :         double dSteer_dt = calculate_sg_derivative(m_slope_steer_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     464                 :             : 
     465         [ +  + ]:       14703 :         if (std::abs(dSteer_dt) > (double)m_slope_alpha_threshold) { // Unified threshold for steering movement
     466                 :         145 :             m_debug_slope_torque_num = dTorque_dt * dSteer_dt;
     467                 :         145 :             m_debug_slope_torque_den = (dSteer_dt * dSteer_dt) + 0.000001;
     468                 :         145 :             m_slope_torque_current = std::clamp(m_debug_slope_torque_num / m_debug_slope_torque_den, -50.0, 50.0);
     469                 :             :         } else {
     470                 :       14558 :             m_slope_torque_current += (double)m_slope_decay_rate * dt * (0.0 - m_slope_torque_current);
     471                 :             :         }
     472                 :             :     } else {
     473                 :         501 :         m_slope_torque_current = 20.0; // Positive value means no grip loss detected
     474                 :             :     }
     475                 :             : 
     476                 :       15204 :     double current_grip_factor = 1.0;
     477         [ +  - ]:       15204 :     double confidence = calculate_slope_confidence(dAlpha_dt);
     478                 :             : 
     479                 :             :     // 1. Calculate Grip Loss from G-Slope (Lateral Saturation)
     480         [ +  - ]:       15204 :     double loss_percent_g = inverse_lerp((double)m_slope_min_threshold, (double)m_slope_max_threshold, m_slope_current);
     481                 :             : 
     482                 :             :     // 2. Calculate Grip Loss from Torque-Slope (Pneumatic Trail Drop)
     483                 :       15204 :     double loss_percent_torque = 0.0;
     484   [ +  +  +  + ]:       15204 :     volatile bool use_torque_fusion = (m_slope_use_torque && data != nullptr);
     485         [ +  + ]:       15204 :     if (use_torque_fusion) {
     486         [ +  + ]:       14703 :         if (m_slope_torque_current < 0.0) {
     487                 :          42 :             loss_percent_torque = std::abs(m_slope_torque_current) * (double)m_slope_torque_sensitivity;
     488                 :          42 :             loss_percent_torque = (std::max)(0.0, (std::min)(1.0, loss_percent_torque));
     489                 :             :         }
     490                 :             :     }
     491                 :             : 
     492                 :             :     // 3. Fusion Logic (Max of both estimators)
     493                 :       15204 :     double loss_percent = (std::max)(loss_percent_g, loss_percent_torque);
     494                 :             :     
     495                 :             :     // Scale loss by confidence and apply floor (0.2)
     496                 :             :     // 0% loss (loss_percent=0) -> 1.0 factor
     497                 :             :     // 100% loss (loss_percent=1) -> 0.2 factor
     498                 :       15204 :     current_grip_factor = 1.0 - (loss_percent * 0.8 * confidence);
     499                 :             : 
     500                 :             :     // Apply Floor (Safety)
     501                 :       15204 :     current_grip_factor = (std::max)(0.2, (std::min)(1.0, current_grip_factor));
     502                 :             : 
     503                 :             :     // 5. Smoothing (v0.7.0)
     504                 :       15204 :     double alpha = dt / ((double)m_slope_smoothing_tau + dt);
     505                 :       15204 :     alpha = (std::max)(0.001, (std::min)(1.0, alpha));
     506                 :       15204 :     m_slope_smoothed_output += alpha * (current_grip_factor - m_slope_smoothed_output);
     507                 :             : 
     508                 :       15204 :     return m_slope_smoothed_output;
     509                 :             : }
     510                 :             : 
     511                 :             : // Helper: Calculate confidence factor for slope detection
     512                 :             : // Extracted to avoid code duplication between slope detection and logging
     513                 :       15247 : double FFBEngine::calculate_slope_confidence(double dAlpha_dt) {
     514         [ -  + ]:       15247 :     if (!m_slope_confidence_enabled) return 1.0;
     515                 :             : 
     516                 :             :     // v0.7.21 FIX: Use smoothstep confidence ramp [m_slope_alpha_threshold, m_slope_confidence_max_rate] rad/s
     517                 :             :     // to reject singularity artifacts near zero.
     518                 :       15247 :     return smoothstep((double)m_slope_alpha_threshold, (double)m_slope_confidence_max_rate, std::abs(dAlpha_dt));
     519                 :             : }
     520                 :             : 
     521                 :             : // Helper: Calculate Slip Ratio from wheel (v0.6.36 - Extracted from lambdas)
     522                 :             : // Unified slip ratio calculation for lockup and spin detection.
     523                 :             : // Returns the ratio of longitudinal slip: (PatchVel - GroundVel) / GroundVel
     524                 :       11288 : double FFBEngine::calculate_wheel_slip_ratio(const TelemWheelV01& w) {
     525                 :       11288 :     double v_long = std::abs(w.mLongitudinalGroundVel);
     526         [ +  + ]:       11288 :     if (v_long < MIN_SLIP_ANGLE_VELOCITY) v_long = MIN_SLIP_ANGLE_VELOCITY;
     527                 :       11288 :     return w.mLongitudinalPatchVel / v_long;
     528                 :             : }
        

Generated by: LCOV version 2.0-1