LCOV - code coverage report
Current view: top level - src - GripLoadEstimation.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 190 190
Test Date: 2026-03-01 21:30:38 Functions: 100.0 % 12 12
Branches: 76.0 % 192 146

             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 <iostream>
      19                 :             : #include <mutex>
      20                 :             : 
      21                 :             : extern std::recursive_mutex g_engine_mutex;
      22                 :             : #include <cmath>
      23                 :             : 
      24                 :             : using namespace ffb_math;
      25                 :             : 
      26                 :             : // Helper: Learn static front load reference (v0.7.46)
      27                 :         332 : void FFBEngine::update_static_load_reference(double current_load, double speed, double dt) {
      28         [ +  - ]:         332 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
      29         [ +  + ]:         332 :     if (m_static_load_latched) return; // Do not update if latched
      30                 :             : 
      31   [ +  +  +  + ]:         328 :     if (speed > 2.0 && speed < 15.0) {
      32         [ +  + ]:         205 :         if (m_static_front_load < 100.0) {
      33                 :           3 :              m_static_front_load = current_load;
      34                 :             :         } else {
      35                 :         202 :              m_static_front_load += (dt / 5.0) * (current_load - m_static_front_load);
      36                 :             :         }
      37   [ +  +  +  - ]:         123 :     } else if (speed >= 15.0 && m_static_front_load > 1000.0) {
      38                 :             :         // Latch the value once we exceed 15 m/s (aero begins to take over)
      39                 :           6 :         m_static_load_latched = true;
      40                 :             : 
      41                 :             :         // Save to config map (v0.7.70)
      42         [ +  - ]:           6 :         std::string vName = m_vehicle_name;
      43   [ +  -  +  +  :           6 :         if (vName != "Unknown" && vName != "") {
          +  -  +  +  +  
                      + ]
      44         [ +  - ]:           1 :             Config::SetSavedStaticLoad(vName, m_static_front_load);
      45                 :           1 :             Config::m_needs_save = true; // Flag main thread to write to disk
      46   [ +  -  +  -  :           1 :             std::cout << "[FFB] Latched and saved static load for " << vName << ": " << m_static_front_load << "N" << std::endl;
          +  -  +  -  +  
                -  +  - ]
      47                 :             :         }
      48                 :           6 :     }
      49                 :             : 
      50                 :             :     // Safety fallback: Ensure we have a sane baseline if learning failed
      51         [ +  + ]:         328 :     if (m_static_front_load < 1000.0) {
      52                 :           2 :         m_static_front_load = m_auto_peak_load * 0.5;
      53                 :             :     }
      54         [ +  + ]:         332 : }
      55                 :             : 
      56                 :             : // Initialize the load reference based on vehicle class and name seeding
      57                 :          52 : void FFBEngine::InitializeLoadReference(const char* className, const char* vehicleName) {
      58         [ +  - ]:          52 :     std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
      59                 :             : 
      60                 :             :     // v0.7.109: Perform a full normalization reset on car change
      61                 :             :     // This ensures that session-learned peaks from a previous car don't pollute the new session.
      62         [ +  - ]:          52 :     ResetNormalization();
      63                 :             : 
      64         [ +  - ]:          52 :     ParsedVehicleClass vclass = ParseVehicleClass(className, vehicleName);
      65                 :             : 
      66                 :             :     // Stage 3 Reset: Ensure peak load starts at class baseline
      67         [ +  - ]:          52 :     m_auto_peak_load = GetDefaultLoadForClass(vclass);
      68                 :             : 
      69   [ +  +  +  - ]:          52 :     std::string vName = vehicleName ? vehicleName : "Unknown";
      70                 :             : 
      71                 :             :     // Check if we already have a saved static load for this specific car (v0.7.70)
      72                 :          52 :     double saved_load = 0.0;
      73   [ +  -  +  + ]:          52 :     if (Config::GetSavedStaticLoad(vName, saved_load)) {
      74                 :           2 :         m_static_front_load = saved_load;
      75                 :           2 :         m_static_load_latched = true; // Skip the 2-15 m/s learning phase
      76   [ +  -  +  -  :           2 :         std::cout << "[FFB] Loaded persistent static load for " << vName << ": " << m_static_front_load << "N" << std::endl;
          +  -  +  -  +  
                -  +  - ]
      77                 :             :     } else {
      78                 :             :         // Reset static load reference for new car class
      79                 :          50 :         m_static_front_load = m_auto_peak_load * 0.5;
      80                 :          50 :         m_static_load_latched = false;
      81   [ +  -  +  -  :          50 :         std::cout << "[FFB] No saved load for " << vName << ". Learning required." << std::endl;
             +  -  +  - ]
      82                 :             :     }
      83                 :             : 
      84                 :          52 :     m_smoothed_tactile_mult = 1.0;
      85                 :             : 
      86                 :             :     std::cout << "[FFB] Vehicle Identification -> Detected Class: " << VehicleClassToString(vclass)
      87   [ +  -  +  -  :          52 :               << " | Seed Load: " << m_auto_peak_load << "N" 
          +  -  +  -  +  
                      - ]
      88                 :             :               << " (Raw -> Class: " << (className ? className : "Unknown") 
      89   [ +  -  +  -  :          52 :               << ", Name: " << vName << ")" << std::endl;
          +  +  +  -  +  
          -  +  -  +  -  
                   +  - ]
      90                 :          52 : }
      91                 :             : 
      92                 :             : // Helper: Calculate Raw Slip Angle for a pair of wheels (v0.4.9 Refactor)
      93                 :             : // Returns the average slip angle of two wheels using atan2(lateral_vel, longitudinal_vel)
      94                 :             : // v0.4.19: Removed abs() from lateral velocity to preserve sign for debug visualization
      95                 :       10469 : double FFBEngine::calculate_raw_slip_angle_pair(const TelemWheelV01& w1, const TelemWheelV01& w2) {
      96                 :       10469 :     double v_long_1 = std::abs(w1.mLongitudinalGroundVel);
      97                 :       10469 :     double v_long_2 = std::abs(w2.mLongitudinalGroundVel);
      98         [ +  + ]:       10469 :     if (v_long_1 < MIN_SLIP_ANGLE_VELOCITY) v_long_1 = MIN_SLIP_ANGLE_VELOCITY;
      99         [ +  + ]:       10469 :     if (v_long_2 < MIN_SLIP_ANGLE_VELOCITY) v_long_2 = MIN_SLIP_ANGLE_VELOCITY;
     100                 :       10469 :     double raw_angle_1 = std::atan2(w1.mLateralPatchVel, v_long_1);
     101                 :       10469 :     double raw_angle_2 = std::atan2(w2.mLateralPatchVel, v_long_2);
     102                 :       10469 :     return (raw_angle_1 + raw_angle_2) / 2.0;
     103                 :             : }
     104                 :             : 
     105                 :       49407 : double FFBEngine::calculate_slip_angle(const TelemWheelV01& w, double& prev_state, double dt) {
     106                 :       49407 :     double v_long = std::abs(w.mLongitudinalGroundVel);
     107         [ +  + ]:       49407 :     if (v_long < MIN_SLIP_ANGLE_VELOCITY) v_long = MIN_SLIP_ANGLE_VELOCITY;
     108                 :             :     
     109                 :             :     // v0.4.19: PRESERVE SIGN - Do NOT use abs() on lateral velocity
     110                 :             :     // Positive lateral vel (+X = left) → Positive slip angle
     111                 :             :     // Negative lateral vel (-X = right) → Negative slip angle
     112                 :             :     // This sign is critical for directional counter-steering
     113                 :       49407 :     double raw_angle = std::atan2(w.mLateralPatchVel, v_long);  // SIGN PRESERVED
     114                 :             :     
     115                 :             :     // LPF: Time Corrected Alpha (v0.4.37)
     116                 :             :     // Target: Alpha 0.1 at 400Hz (dt = 0.0025)
     117                 :             :     // Formula: alpha = dt / (tau + dt) -> 0.1 = 0.0025 / (tau + 0.0025) -> tau approx 0.0225s
     118                 :             :     // v0.4.40: Using configurable m_slip_angle_smoothing
     119                 :       49407 :     double tau = (double)m_slip_angle_smoothing;
     120         [ +  + ]:       49407 :     if (tau < 0.0001) tau = 0.0001; // Safety clamp 
     121                 :             :     
     122                 :       49407 :     double alpha = dt / (tau + dt);
     123                 :             :     
     124                 :             :     // Safety clamp
     125                 :       49407 :     alpha = (std::min)(1.0, (std::max)(0.001, alpha));
     126                 :       49407 :     prev_state = prev_state + alpha * (raw_angle - prev_state);
     127                 :       49407 :     return prev_state;
     128                 :             : }
     129                 :             : 
     130                 :             : // Helper: Calculate Grip with Fallback (v0.4.6 Hardening)
     131                 :       24703 : GripResult FFBEngine::calculate_grip(const TelemWheelV01& w1, 
     132                 :             :                           const TelemWheelV01& w2,
     133                 :             :                           double avg_load,
     134                 :             :                           bool& warned_flag,
     135                 :             :                           double& prev_slip1,
     136                 :             :                           double& prev_slip2,
     137                 :             :                           double car_speed,
     138                 :             :                           double dt,
     139                 :             :                           const char* vehicleName,
     140                 :             :                           const TelemInfoV01* data,
     141                 :             :                           bool is_front) {
     142                 :             :     GripResult result;
     143                 :       24703 :     double total_load = w1.mTireLoad + w2.mTireLoad;
     144         [ +  + ]:       24703 :     if (total_load > 1.0) {
     145                 :       18744 :         result.original = (w1.mGripFract * w1.mTireLoad + w2.mGripFract * w2.mTireLoad) / total_load;
     146                 :             :     } else {
     147                 :             :         // Fallback for zero load (e.g. jump/missing data)
     148                 :        5959 :         result.original = (w1.mGripFract + w2.mGripFract) / 2.0;
     149                 :             :     }
     150                 :             : 
     151                 :       24703 :     result.value = result.original;
     152                 :       24703 :     result.approximated = false;
     153                 :       24703 :     result.slip_angle = 0.0;
     154                 :             :     
     155                 :             :     // ==================================================================================
     156                 :             :     // CRITICAL LOGIC FIX (v0.4.14) - DO NOT MOVE INSIDE CONDITIONAL BLOCK
     157                 :             :     // ==================================================================================
     158                 :             :     // We MUST calculate slip angle every single frame, regardless of whether the 
     159                 :             :     // grip fallback is triggered or not.
     160                 :             :     //
     161                 :             :     // Reason 1 (Physics State): The Low Pass Filter (LPF) inside calculate_slip_angle 
     162                 :             :     //           relies on continuous execution. If we skip frames (because telemetry 
     163                 :             :     //           is good), the 'prev_slip' state becomes stale. When telemetry eventually 
     164                 :             :     //           fails, the LPF will smooth against ancient history, causing a math spike.
     165                 :             :     //
     166                 :             :     // Reason 2 (Dependency): The 'Rear Aligning Torque' effect (calculated later) 
     167                 :             :     //           reads 'result.slip_angle'. If we only calculate this when grip is 
     168                 :             :     //           missing, the Rear Torque effect will toggle ON/OFF randomly based on 
     169                 :             :     //           telemetry health, causing violent kicks and "reverse FFB" sensations.
     170                 :             :     // ==================================================================================
     171                 :       24703 :     double slip1 = calculate_slip_angle(w1, prev_slip1, dt);
     172                 :       24703 :     double slip2 = calculate_slip_angle(w2, prev_slip2, dt);
     173                 :       24703 :     result.slip_angle = (slip1 + slip2) / 2.0;
     174                 :             : 
     175                 :             :     // Fallback condition: Grip is essentially zero BUT car has significant load
     176   [ +  +  +  + ]:       24703 :     if (result.value < 0.0001 && avg_load > 100.0) {
     177                 :       20880 :         result.approximated = true;
     178                 :             :         
     179         [ +  + ]:       20880 :         if (car_speed < 5.0) {
     180                 :             :             // Note: We still keep the calculated slip_angle in result.slip_angle
     181                 :             :             // for visualization/rear torque, even if we force grip to 1.0 here.
     182                 :         566 :             result.value = 1.0; 
     183                 :             :         } else {
     184   [ +  +  +  +  :       20314 :             if (m_slope_detection_enabled && is_front && data) {
                   +  - ]
     185                 :             :                 // Dynamic grip estimation via derivative monitoring
     186                 :        2042 :                 result.value = calculate_slope_grip(
     187                 :        2042 :                     data->mLocalAccel.x / 9.81,
     188                 :             :                     result.slip_angle,
     189                 :             :                     dt,
     190                 :             :                     data
     191                 :             :                 );
     192                 :             :             } else {
     193                 :             :                 // v0.4.38: Combined Friction Circle (Advanced Reconstruction)
     194                 :             :                 
     195                 :             :                 // 1. Lateral Component (Alpha)
     196                 :             :                 // USE CONFIGURABLE THRESHOLD (v0.5.7)
     197                 :       18272 :                 double lat_metric = std::abs(result.slip_angle) / (double)m_optimal_slip_angle;
     198                 :             : 
     199                 :             :                 // 2. Longitudinal Component (Kappa)
     200                 :             :                 // Calculate manual slip for both wheels and average the magnitude
     201                 :       18272 :                 double ratio1 = calculate_manual_slip_ratio(w1, car_speed);
     202                 :       18272 :                 double ratio2 = calculate_manual_slip_ratio(w2, car_speed);
     203                 :       18272 :                 double avg_ratio = (std::abs(ratio1) + std::abs(ratio2)) / 2.0;
     204                 :             : 
     205                 :             :                 // USE CONFIGURABLE THRESHOLD (v0.5.7)
     206                 :       18272 :                 double long_metric = avg_ratio / (double)m_optimal_slip_ratio;
     207                 :             : 
     208                 :             :                 // 3. Combined Vector (Friction Circle)
     209                 :       18272 :                 double combined_slip = std::sqrt((lat_metric * lat_metric) + (long_metric * long_metric));
     210                 :             : 
     211                 :             :                 // 4. Map to Grip Fraction
     212                 :             : 
     213         [ +  + ]:       18272 :                 if (combined_slip > 1.0) {
     214                 :        7212 :                     double excess = combined_slip - 1.0;
     215                 :        7212 :                     result.value = 1.0 / (1.0 + excess * 2.0);
     216                 :             :                 } else {
     217                 :       11060 :                     result.value = 1.0;
     218                 :             :                 }
     219                 :             :             }
     220                 :             :         }
     221                 :             :         
     222                 :             :         
     223                 :             :         // Safety Clamp (v0.4.6): Never drop below 0.2 in approximation
     224                 :       20880 :         result.value = (std::max)(0.2, result.value);
     225                 :             :         
     226         [ +  + ]:       20880 :         if (!warned_flag) {
     227                 :         282 :             std::cout << "Warning: Data for mGripFract from the game seems to be missing for this car (" << vehicleName << "). (Likely Encrypted/DLC Content). A fallback estimation will be used." << std::endl;
     228                 :         282 :             warned_flag = true;
     229                 :             :         }
     230                 :             :     }
     231                 :             :     
     232                 :             :     // Apply Adaptive Smoothing (v0.7.47)
     233         [ +  + ]:       24703 :     double& state = is_front ? m_front_grip_smoothed_state : m_rear_grip_smoothed_state;
     234                 :       49406 :     result.value = apply_adaptive_smoothing(result.value, state, dt,
     235                 :       24703 :                                             (double)m_grip_smoothing_steady,
     236                 :       24703 :                                             (double)m_grip_smoothing_fast,
     237                 :       24703 :                                             (double)m_grip_smoothing_sensitivity);
     238                 :             : 
     239                 :       24703 :     result.value = (std::max)(0.0, (std::min)(1.0, result.value));
     240                 :       24703 :     return result;
     241                 :             : }
     242                 :             : 
     243                 :             : // Helper: Approximate Load (v0.4.5)
     244                 :         259 : double FFBEngine::approximate_load(const TelemWheelV01& w) {
     245                 :             :     // Base: Suspension Force + Est. Unsprung Mass (300N)
     246                 :             :     // Note: mSuspForce captures weight transfer and aero
     247                 :         259 :     return w.mSuspForce + 300.0;
     248                 :             : }
     249                 :             : 
     250                 :             : // Helper: Approximate Rear Load (v0.4.10)
     251                 :       24701 : double FFBEngine::approximate_rear_load(const TelemWheelV01& w) {
     252                 :             :     // Base: Suspension Force + Est. Unsprung Mass (300N)
     253                 :             :     // This captures weight transfer (braking/accel) and aero downforce implicitly via suspension compression
     254                 :       24701 :     return w.mSuspForce + 300.0;
     255                 :             : }
     256                 :             : 
     257                 :             : // Helper: Calculate Kinematic Load (v0.4.39)
     258                 :             : // Estimates tire load from chassis physics when telemetry (mSuspForce) is missing.
     259                 :             : // This is critical for encrypted DLC content where suspension sensors are blocked.
     260                 :        2364 : double FFBEngine::calculate_kinematic_load(const TelemInfoV01* data, int wheel_index) {
     261                 :             :     // 1. Static Weight Distribution
     262                 :        2364 :     bool is_rear = (wheel_index >= 2);
     263         [ +  + ]:        2364 :     double bias = is_rear ? m_approx_weight_bias : (1.0 - m_approx_weight_bias);
     264                 :        2364 :     double static_weight = (m_approx_mass_kg * 9.81 * bias) / 2.0;
     265                 :             : 
     266                 :             :     // 2. Aerodynamic Load (Velocity Squared)
     267                 :        2364 :     double speed = std::abs(data->mLocalVel.z);
     268                 :        2364 :     double aero_load = m_approx_aero_coeff * (speed * speed);
     269                 :        2364 :     double wheel_aero = aero_load / 4.0; 
     270                 :             : 
     271                 :             :     // 3. Longitudinal Weight Transfer (Braking/Acceleration)
     272                 :             :     // COORDINATE SYSTEM VERIFIED (v0.4.39):
     273                 :             :     // - LMU: +Z axis points REARWARD (out the back of the car)
     274                 :             :     // - Braking: Chassis decelerates → Inertial force pushes rearward → +Z acceleration
     275                 :             :     // - Result: Front wheels GAIN load, Rear wheels LOSE load
     276                 :             :     // - Source: docs/dev_docs/references/Reference - coordinate_system_reference.md
     277                 :             :     // 
     278                 :             :     // Formula: (Accel / g) * WEIGHT_TRANSFER_SCALE
     279                 :             :     // We use SMOOTHED acceleration to simulate chassis pitch inertia (~35ms lag)
     280                 :        2364 :     double long_transfer = (m_accel_z_smoothed / 9.81) * WEIGHT_TRANSFER_SCALE; 
     281         [ +  + ]:        2364 :     if (is_rear) long_transfer *= -1.0; // Subtract from Rear during Braking
     282                 :             : 
     283                 :             :     // 4. Lateral Weight Transfer (Cornering)
     284                 :             :     // COORDINATE SYSTEM VERIFIED (v0.4.39):
     285                 :             :     // - LMU: +X axis points LEFT (out the left side of the car)
     286                 :             :     // - Right Turn: Centrifugal force pushes LEFT → +X acceleration
     287                 :             :     // - Result: LEFT wheels (outside) GAIN load, RIGHT wheels (inside) LOSE load
     288                 :             :     // - Source: docs/dev_docs/references/Reference - coordinate_system_reference.md
     289                 :             :     // 
     290                 :             :     // Formula: (Accel / g) * WEIGHT_TRANSFER_SCALE * Roll_Stiffness
     291                 :             :     // We use SMOOTHED acceleration to simulate chassis roll inertia (~35ms lag)
     292                 :        2364 :     double lat_transfer = (m_accel_x_smoothed / 9.81) * WEIGHT_TRANSFER_SCALE * m_approx_roll_stiffness;
     293   [ +  +  +  + ]:        2364 :     bool is_left = (wheel_index == 0 || wheel_index == 2);
     294         [ +  + ]:        2364 :     if (!is_left) lat_transfer *= -1.0; // Subtract from Right wheels
     295                 :             : 
     296                 :             :     // Sum and Clamp
     297                 :        2364 :     double total_load = static_weight + wheel_aero + long_transfer + lat_transfer;
     298                 :        2364 :     return (std::max)(0.0, total_load);
     299                 :             : }
     300                 :             : 
     301                 :             : // Helper: Calculate Manual Slip Ratio (v0.4.6)
     302                 :       36549 : double FFBEngine::calculate_manual_slip_ratio(const TelemWheelV01& w, double car_speed_ms) {
     303                 :             :     // Safety Trap: Force 0 slip at very low speeds (v0.4.6)
     304         [ +  + ]:       36549 :     if (std::abs(car_speed_ms) < 2.0) return 0.0;
     305                 :             : 
     306                 :             :     // Radius in meters (stored as cm unsigned char)
     307                 :             :     // Explicit cast to double before division (v0.4.6)
     308                 :       36546 :     double radius_m = (double)w.mStaticUndeflectedRadius / 100.0;
     309         [ +  + ]:       36546 :     if (radius_m < 0.1) radius_m = 0.33; // Fallback if 0 or invalid
     310                 :             :     
     311                 :       36546 :     double wheel_vel = w.mRotation * radius_m;
     312                 :             :     
     313                 :             :     // Avoid div-by-zero at standstill
     314                 :       36546 :     double denom = std::abs(car_speed_ms);
     315         [ -  + ]:       36546 :     if (denom < 1.0) denom = 1.0;
     316                 :             :     
     317                 :             :     // Ratio = (V_wheel - V_car) / V_car
     318                 :             :     // Lockup: V_wheel < V_car -> Ratio < 0
     319                 :             :     // Spin: V_wheel > V_car -> Ratio > 0
     320                 :       36546 :     return (wheel_vel - car_speed_ms) / denom;
     321                 :             : }
     322                 :             : 
     323                 :             : // Helper: Calculate Grip Factor from Slope - v0.7.40 REWRITE
     324                 :             : // Robust projected slope estimation with hold-and-decay logic and torque-based anticipation.
     325                 :        2547 : double FFBEngine::calculate_slope_grip(double lateral_g, double slip_angle, double dt, const TelemInfoV01* data) {
     326                 :             :     // 1. Signal Hygiene (Slew Limiter & Pre-Smoothing)
     327                 :             : 
     328                 :             :     // Initialize slew limiter and smoothing state on first sample to avoid ramp-up transients
     329         [ +  + ]:        2547 :     if (m_slope_buffer_count == 0) {
     330                 :          37 :         m_slope_lat_g_prev = std::abs(lateral_g);
     331                 :          37 :         m_slope_lat_g_smoothed = std::abs(lateral_g);
     332                 :          37 :         m_slope_slip_smoothed = std::abs(slip_angle);
     333         [ +  + ]:          37 :         if (data) {
     334                 :          26 :             m_slope_torque_smoothed = std::abs(data->mSteeringShaftTorque);
     335                 :          26 :             m_slope_steer_smoothed = std::abs(data->mUnfilteredSteering);
     336                 :             :         }
     337                 :             :     }
     338                 :             : 
     339         [ +  - ]:        2547 :     double lat_g_slew = apply_slew_limiter(std::abs(lateral_g), m_slope_lat_g_prev, (double)m_slope_g_slew_limit, dt);
     340                 :        2547 :     m_debug_lat_g_slew = lat_g_slew;
     341                 :             : 
     342                 :        2547 :     double alpha_smooth = dt / (0.01 + dt);
     343                 :             : 
     344         [ +  + ]:        2547 :     if (m_slope_buffer_count > 0) {
     345                 :        2510 :         m_slope_lat_g_smoothed += alpha_smooth * (lat_g_slew - m_slope_lat_g_smoothed);
     346                 :        2510 :         m_slope_slip_smoothed += alpha_smooth * (std::abs(slip_angle) - m_slope_slip_smoothed);
     347         [ +  + ]:        2510 :         if (data) {
     348                 :        2023 :             m_slope_torque_smoothed += alpha_smooth * (std::abs(data->mSteeringShaftTorque) - m_slope_torque_smoothed);
     349                 :        2023 :             m_slope_steer_smoothed += alpha_smooth * (std::abs(data->mUnfilteredSteering) - m_slope_steer_smoothed);
     350                 :             :         }
     351                 :             :     }
     352                 :             : 
     353                 :             :     // 2. Update Buffers with smoothed values
     354                 :        2547 :     m_slope_lat_g_buffer[m_slope_buffer_index] = m_slope_lat_g_smoothed;
     355                 :        2547 :     m_slope_slip_buffer[m_slope_buffer_index] = m_slope_slip_smoothed;
     356         [ +  + ]:        2547 :     if (data) {
     357                 :        2049 :         m_slope_torque_buffer[m_slope_buffer_index] = m_slope_torque_smoothed;
     358                 :        2049 :         m_slope_steer_buffer[m_slope_buffer_index] = m_slope_steer_smoothed;
     359                 :             :     }
     360                 :             : 
     361                 :        2547 :     m_slope_buffer_index = (m_slope_buffer_index + 1) % SLOPE_BUFFER_MAX;
     362         [ +  + ]:        2547 :     if (m_slope_buffer_count < SLOPE_BUFFER_MAX) m_slope_buffer_count++;
     363                 :             : 
     364                 :             :     // 3. Calculate G-based Derivatives (Savitzky-Golay)
     365                 :        2547 :     double dG_dt = calculate_sg_derivative(m_slope_lat_g_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     366                 :        2547 :     double dAlpha_dt = calculate_sg_derivative(m_slope_slip_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     367                 :             : 
     368                 :        2547 :     m_slope_dG_dt = dG_dt;
     369                 :        2547 :     m_slope_dAlpha_dt = dAlpha_dt;
     370                 :             : 
     371                 :             :     // 4. Projected Slope Logic (G-based)
     372         [ +  + ]:        2547 :     if (std::abs(dAlpha_dt) > (double)m_slope_alpha_threshold) {
     373                 :         546 :         m_slope_hold_timer = SLOPE_HOLD_TIME;
     374                 :         546 :         m_debug_slope_num = dG_dt * dAlpha_dt;
     375                 :         546 :         m_debug_slope_den = (dAlpha_dt * dAlpha_dt) + 0.000001;
     376                 :         546 :         m_debug_slope_raw = m_debug_slope_num / m_debug_slope_den;
     377                 :         546 :         m_slope_current = std::clamp(m_debug_slope_raw, -20.0, 20.0);
     378                 :             :     } else {
     379                 :        2001 :         m_slope_hold_timer -= dt;
     380         [ +  + ]:        2001 :         if (m_slope_hold_timer <= 0.0) {
     381                 :        1846 :             m_slope_hold_timer = 0.0;
     382                 :        1846 :             m_slope_current += (double)m_slope_decay_rate * dt * (0.0 - m_slope_current);
     383                 :             :         }
     384                 :             :     }
     385                 :             : 
     386                 :             :     // 5. Calculate Torque-based Slope (Pneumatic Trail Anticipation)
     387   [ +  +  +  + ]:        2547 :     volatile bool can_calc_torque = (m_slope_use_torque && data != nullptr);
     388         [ +  + ]:        2547 :     if (can_calc_torque) {
     389                 :        2046 :         double dTorque_dt = calculate_sg_derivative(m_slope_torque_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     390                 :        2046 :         double dSteer_dt = calculate_sg_derivative(m_slope_steer_buffer, m_slope_buffer_count, m_slope_sg_window, dt, m_slope_buffer_index);
     391                 :             : 
     392         [ +  + ]:        2046 :         if (std::abs(dSteer_dt) > (double)m_slope_alpha_threshold) { // Unified threshold for steering movement
     393                 :         141 :             m_debug_slope_torque_num = dTorque_dt * dSteer_dt;
     394                 :         141 :             m_debug_slope_torque_den = (dSteer_dt * dSteer_dt) + 0.000001;
     395                 :         141 :             m_slope_torque_current = std::clamp(m_debug_slope_torque_num / m_debug_slope_torque_den, -50.0, 50.0);
     396                 :             :         } else {
     397                 :        1905 :             m_slope_torque_current += (double)m_slope_decay_rate * dt * (0.0 - m_slope_torque_current);
     398                 :             :         }
     399                 :             :     } else {
     400                 :         501 :         m_slope_torque_current = 20.0; // Positive value means no grip loss detected
     401                 :             :     }
     402                 :             : 
     403                 :        2547 :     double current_grip_factor = 1.0;
     404         [ +  - ]:        2547 :     double confidence = calculate_slope_confidence(dAlpha_dt);
     405                 :             : 
     406                 :             :     // 1. Calculate Grip Loss from G-Slope (Lateral Saturation)
     407         [ +  - ]:        2547 :     double loss_percent_g = inverse_lerp((double)m_slope_min_threshold, (double)m_slope_max_threshold, m_slope_current);
     408                 :             : 
     409                 :             :     // 2. Calculate Grip Loss from Torque-Slope (Pneumatic Trail Drop)
     410                 :        2547 :     double loss_percent_torque = 0.0;
     411   [ +  +  +  + ]:        2547 :     volatile bool use_torque_fusion = (m_slope_use_torque && data != nullptr);
     412         [ +  + ]:        2547 :     if (use_torque_fusion) {
     413         [ +  + ]:        2046 :         if (m_slope_torque_current < 0.0) {
     414                 :          38 :             loss_percent_torque = std::abs(m_slope_torque_current) * (double)m_slope_torque_sensitivity;
     415                 :          38 :             loss_percent_torque = (std::max)(0.0, (std::min)(1.0, loss_percent_torque));
     416                 :             :         }
     417                 :             :     }
     418                 :             : 
     419                 :             :     // 3. Fusion Logic (Max of both estimators)
     420                 :        2547 :     double loss_percent = (std::max)(loss_percent_g, loss_percent_torque);
     421                 :             :     
     422                 :             :     // Scale loss by confidence and apply floor (0.2)
     423                 :             :     // 0% loss (loss_percent=0) -> 1.0 factor
     424                 :             :     // 100% loss (loss_percent=1) -> 0.2 factor
     425                 :        2547 :     current_grip_factor = 1.0 - (loss_percent * 0.8 * confidence);
     426                 :             : 
     427                 :             :     // Apply Floor (Safety)
     428                 :        2547 :     current_grip_factor = (std::max)(0.2, (std::min)(1.0, current_grip_factor));
     429                 :             : 
     430                 :             :     // 5. Smoothing (v0.7.0)
     431                 :        2547 :     double alpha = dt / ((double)m_slope_smoothing_tau + dt);
     432                 :        2547 :     alpha = (std::max)(0.001, (std::min)(1.0, alpha));
     433                 :        2547 :     m_slope_smoothed_output += alpha * (current_grip_factor - m_slope_smoothed_output);
     434                 :             : 
     435                 :        2547 :     return m_slope_smoothed_output;
     436                 :             : }
     437                 :             : 
     438                 :             : // Helper: Calculate confidence factor for slope detection
     439                 :             : // Extracted to avoid code duplication between slope detection and logging
     440                 :        2825 : double FFBEngine::calculate_slope_confidence(double dAlpha_dt) {
     441         [ -  + ]:        2825 :     if (!m_slope_confidence_enabled) return 1.0;
     442                 :             : 
     443                 :             :     // v0.7.21 FIX: Use smoothstep confidence ramp [m_slope_alpha_threshold, m_slope_confidence_max_rate] rad/s
     444                 :             :     // to reject singularity artifacts near zero.
     445                 :        2825 :     return smoothstep((double)m_slope_alpha_threshold, (double)m_slope_confidence_max_rate, std::abs(dAlpha_dt));
     446                 :             : }
     447                 :             : 
     448                 :             : // Helper: Calculate Slip Ratio from wheel (v0.6.36 - Extracted from lambdas)
     449                 :             : // Unified slip ratio calculation for lockup and spin detection.
     450                 :             : // Returns the ratio of longitudinal slip: (PatchVel - GroundVel) / GroundVel
     451                 :       10188 : double FFBEngine::calculate_wheel_slip_ratio(const TelemWheelV01& w) {
     452                 :       10188 :     double v_long = std::abs(w.mLongitudinalGroundVel);
     453         [ +  + ]:       10188 :     if (v_long < MIN_SLIP_ANGLE_VELOCITY) v_long = MIN_SLIP_ANGLE_VELOCITY;
     454                 :       10188 :     return w.mLongitudinalPatchVel / v_long;
     455                 :             : }
        

Generated by: LCOV version 2.0-1