| Line | Branch | Decision | Exec | Source |
|---|---|---|---|---|
| 1 | #include "pch.h" | |||
| 2 | #include "closed_loop_idle.h" | |||
| 3 | #include "engine_math.h" | |||
| 4 | #include "efitime.h" | |||
| 5 | #include "engine.h" | |||
| 6 | #include <rusefi/rusefi_math.h> | |||
| 7 | ||||
| 8 | // LTIT_TABLE_SIZE is defined in the header file | |||
| 9 | ||||
| 10 | #if EFI_IDLE_CONTROL | |||
| 11 | ||||
| 12 | 679 | LongTermIdleTrim::LongTermIdleTrim() { | ||
| 13 | 679 | initializeTableWithDefaults(); | ||
| 14 | 679 | emaError = 0.0f; //TODO: unused? | ||
| 15 | 679 | ltitTableInitialized = false; | ||
| 16 | 679 | m_pendingSave = false; | ||
| 17 | 679 | } | ||
| 18 | ||||
| 19 | 1271 | void LongTermIdleTrim::initializeTableWithDefaults() { | ||
| 20 | // Initialize with 100% (1.0 multiplier) as default | |||
| 21 |
2/2✓ Branch 0 taken 10168 times.
✓ Branch 1 taken 1271 times.
|
2/2✓ Decision 'true' taken 10168 times.
✓ Decision 'false' taken 1271 times.
|
11439 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 22 | 10168 | ltitTableHelper[i] = 100.0f; | ||
| 23 | } | |||
| 24 | 1271 | } | ||
| 25 | ||||
| 26 | //TODO: move? add? to validateConfigOnStartUpOrBurn | |||
| 27 | 598 | bool LongTermIdleTrim::hasValidData() const { | ||
| 28 | // More robust validation - check for reasonable range and distribution | |||
| 29 | 598 | int validCount = 0; | ||
| 30 | 598 | float totalValue = 0.0f; | ||
| 31 | ||||
| 32 |
2/2✓ Branch 0 taken 4784 times.
✓ Branch 1 taken 598 times.
|
2/2✓ Decision 'true' taken 4784 times.
✓ Decision 'false' taken 598 times.
|
5382 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 33 | 4784 | float value = static_cast<float>(config->ltitTable[i]); | ||
| 34 | ||||
| 35 | // Check if value is in reasonable range (50% to 150%) | |||
| 36 |
4/4✓ Branch 0 taken 40 times.
✓ Branch 1 taken 4744 times.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 6 times.
|
2/2✓ Decision 'true' taken 34 times.
✓ Decision 'false' taken 4750 times.
|
4784 | if (value >= 50.0f && value <= 150.0f) { |
| 37 | 34 | validCount++; | ||
| 38 | 34 | totalValue += value; | ||
| 39 | } | |||
| 40 | } | |||
| 41 | ||||
| 42 | // Require at least half the table to be valid and reasonable average | |||
| 43 |
2/2✓ Branch 0 taken 594 times.
✓ Branch 1 taken 4 times.
|
2/2✓ Decision 'true' taken 594 times.
✓ Decision 'false' taken 4 times.
|
598 | if (validCount < (LTIT_TABLE_SIZE / 2)) { |
| 44 | 594 | return false; | ||
| 45 | } | |||
| 46 | ||||
| 47 | 4 | float avgValue = totalValue / validCount; | ||
| 48 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
|
4 | return (avgValue >= 80.0f && avgValue <= 120.0f); // Reasonable average range | |
| 49 | } | |||
| 50 | ||||
| 51 | 595 | void LongTermIdleTrim::loadLtitFromConfig() { | ||
| 52 |
2/2✓ Branch 1 taken 3 times.
✓ Branch 2 taken 592 times.
|
2/2✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 592 times.
|
595 | if (hasValidData()) { |
| 53 | // Convert autoscaled uint16_t to float | |||
| 54 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 3 times.
|
2/2✓ Decision 'true' taken 24 times.
✓ Decision 'false' taken 3 times.
|
27 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 55 | 24 | ltitTableHelper[i] = static_cast<float>(config->ltitTable[i]); | ||
| 56 | } | |||
| 57 | ||||
| 58 | 3 | ltitTableInitialized = true; | ||
| 59 | } else { | |||
| 60 | //TODO: this is part of setDefaultEngineConfiguration? | |||
| 61 | // Initialize with defaults if no valid data | |||
| 62 | 592 | initializeTableWithDefaults(); | ||
| 63 | 592 | ltitTableInitialized = true; | ||
| 64 | } | |||
| 65 | 595 | } | ||
| 66 | ||||
| 67 | 2 | float LongTermIdleTrim::getLtitFactor(float rpm, float clt) const { | ||
| 68 | //TODO: rpm unused? | |||
| 69 | UNUSED(rpm); | |||
| 70 | ||||
| 71 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 1 time.
|
2 | if (!ltitTableInitialized) { |
| 72 | 1 | return 1.0f; // No correction if not initialized | ||
| 73 | } | |||
| 74 | ||||
| 75 | // Use 2D interpolation based only on CLT (temperature) | |||
| 76 | 1 | return interpolate2d(clt, config->cltIdleCorrBins, ltitTableHelper) * 0.01f; | ||
| 77 | } | |||
| 78 | ||||
| 79 | 7 | bool LongTermIdleTrim::isValidConditionsForLearning(float idleIntegral) const { | ||
| 80 | 7 | float minThreshold = engineConfiguration->ltitIntegratorThreshold; | ||
| 81 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 6 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 6 times.
|
7 | if (std::abs(idleIntegral) < minThreshold) { |
| 82 | 1 | return false; // Integrator too low - PID not working hard enough | ||
| 83 | } | |||
| 84 | ||||
| 85 | // Upper limit to avoid extreme conditions | |||
| 86 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 5 times.
|
6 | if (std::abs(idleIntegral) > 25.0f) { |
| 87 | 1 | return false; // Integrator too high - unstable conditions | ||
| 88 | } | |||
| 89 | ||||
| 90 | // Check if enough time has passed since ignition on | |||
| 91 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 4 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 4 times.
|
5 | if (!m_updateTimer.hasElapsedSec(engineConfiguration->ltitIgnitionOnDelay)) { |
| 92 | 1 | return false; | ||
| 93 | } | |||
| 94 | ||||
| 95 | // Must be in stable idle | |||
| 96 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
2/2✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 2 times.
|
4 | if (!isStableIdle) { |
| 97 | 2 | return false; | ||
| 98 | } | |||
| 99 | ||||
| 100 | 2 | return true; | ||
| 101 | } | |||
| 102 | ||||
| 103 | 9 | void LongTermIdleTrim::update(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral) { | ||
| 104 | //TODO: acActive unused, fan1Active, fan2Active, check comment about isValidConditionsForLearning | |||
| 105 | UNUSED(acActive); UNUSED(fan1Active); UNUSED(fan2Active); | |||
| 106 | ||||
| 107 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 8 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 8 times.
|
9 | if (!engineConfiguration->ltitEnabled) { |
| 108 | 1 | return; | ||
| 109 | } | |||
| 110 | ||||
| 111 | // Try to load data periodically until successful | |||
| 112 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 7 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 7 times.
|
8 | if (!ltitTableInitialized) { |
| 113 | 1 | loadLtitFromConfig(); | ||
| 114 | 1 | return; | ||
| 115 | } | |||
| 116 | ||||
| 117 | // Check ignition delay | |||
| 118 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 6 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 6 times.
|
7 | if (!m_updateTimer.hasElapsedSec(engineConfiguration->ltitIgnitionOnDelay)) { |
| 119 | 1 | m_stableIdleTimer.reset(); | ||
| 120 | 1 | isStableIdle = false; | ||
| 121 | 1 | return; | ||
| 122 | } | |||
| 123 | ||||
| 124 | 6 | auto& idleController = engine->engineModules.get<IdleController>(); | ||
| 125 | 6 | auto currentPhase = idleController->getCurrentPhase(); | ||
| 126 | ||||
| 127 | // LTIT should only learn during Phase::Idling | |||
| 128 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 5 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 5 times.
|
6 | if (currentPhase != IIdleController::Phase::Idling) { |
| 129 | 1 | m_stableIdleTimer.reset(); | ||
| 130 | 1 | isStableIdle = false; | ||
| 131 | 1 | return; | ||
| 132 | } | |||
| 133 | ||||
| 134 | // Check if we're in idle RPM range | |||
| 135 | 5 | float targetRpm = idleController->getTargetRpm(clt).ClosedLoopTarget; | ||
| 136 | 5 | float rpmDelta = std::abs(rpm - targetRpm); | ||
| 137 | 5 | bool isIdleRpm = rpmDelta < engineConfiguration->ltitStableRpmThreshold; | ||
| 138 | ||||
| 139 | // Check stability | |||
| 140 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 4 times.
|
5 | if (!isIdleRpm) { |
| 141 | 1 | m_stableIdleTimer.reset(); | ||
| 142 | 1 | isStableIdle = false; | ||
| 143 | 1 | return; | ||
| 144 | } | |||
| 145 | ||||
| 146 | // Check if stable for minimum time | |||
| 147 | // TODO: set ltitStableTime default value? also move to autoscale 0.1 like other configs with seconds? | |||
| 148 |
5/6✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 1 time.
|
2/2✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 1 time.
|
4 | if (!isStableIdle && m_stableIdleTimer.hasElapsedSec(engineConfiguration->ltitStableTime)) { |
| 149 | 3 | isStableIdle = true; | ||
| 150 | } | |||
| 151 | ||||
| 152 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 3 times.
|
4 | if (!isStableIdle) { |
| 153 | 1 | return; | ||
| 154 | } | |||
| 155 | ||||
| 156 |
2/2✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
|
2/2✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1 time.
|
3 | if (!idleController->isIdleClosedLoop) { |
| 157 | 2 | return; | ||
| 158 | } | |||
| 159 | ||||
| 160 | // Main table learning (now allowed even with AC/Fan active) | |||
| 161 | // Validate conditions | |||
| 162 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
|
1 | if (!isValidConditionsForLearning(idleIntegral)) { |
| 163 | ✗ | return; | ||
| 164 | } | |||
| 165 | ||||
| 166 | // Check minimum update interval (fixed slowCallback period: 50ms) | |||
| 167 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
|
1 | if (!m_updateTimer.hasElapsedSec(1.0f)) { |
| 168 | ✗ | return; | ||
| 169 | } | |||
| 170 | 1 | m_updateTimer.reset(); | ||
| 171 | ||||
| 172 | //TODO: docs?, weird use of non-public interpolation.h, we are trying to the get bin index for X temp? | |||
| 173 | // also ltitTableHelper scale will depend on clt idle bins? | |||
| 174 | // Use proper bin finding with getBin function for CLT only | |||
| 175 | 1 | auto cltBin = priv::getBin(clt, config->cltIdleCorrBins); | ||
| 176 | ||||
| 177 | // Apply correction rate in %/s (percentage per second) | |||
| 178 | // Using fixed slowCallback delta time (50ms = 0.05s) for consistent behavior | |||
| 179 | 1 | const float deltaTime = 0.05f; // SLOW_CALLBACK_PERIOD_MS / 1000.0f | ||
| 180 | 1 | float correctionPerSecond = idleIntegral * engineConfiguration->ltitCorrectionRate * 0.01f; // Convert % to decimal | ||
| 181 | 1 | float correction = correctionPerSecond * deltaTime; // Apply time-based correction | ||
| 182 | 1 | float alpha = engineConfiguration->ltitEmaAlpha / 255.0f; | ||
| 183 | ||||
| 184 | // Primary cell (largest weight) | |||
| 185 | 1 | float newValue = ltitTableHelper[cltBin.Idx] * (1.0f + correction); | ||
| 186 | 1 | newValue = alpha * newValue + (1.0f - alpha) * ltitTableHelper[cltBin.Idx]; | ||
| 187 | ||||
| 188 | // Apply clamping | |||
| 189 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
|
1 | float clampMin = engineConfiguration->ltitClampMin > 0 ? engineConfiguration->ltitClampMin : 0.0f; | |
| 190 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
|
1 | float clampMax = engineConfiguration->ltitClampMax > 0 ? engineConfiguration->ltitClampMax : 250.0f; | |
| 191 | 1 | ltitTableHelper[cltBin.Idx] = clampF(clampMin, newValue, clampMax); | ||
| 192 | ||||
| 193 | // Apply to adjacent cells with reduced weight (for better interpolation) | |||
| 194 | 1 | float adjWeight = 0.3f; // 30% weight for adjacent cells | ||
| 195 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 1 time.
|
4 | for (int di = -1; di <= 1; di++) { |
| 196 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 2 times.
|
3 | if (di == 0) continue; // Skip primary cell |
| 197 | ||||
| 198 | 2 | int adjI = cltBin.Idx + di; | ||
| 199 | ||||
| 200 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
1/2✓ Decision 'true' taken 2 times.
✗ Decision 'false' not taken.
|
2 | if (adjI >= 0 && adjI < LTIT_TABLE_SIZE) { |
| 201 | 2 | float adjCorrection = correction * adjWeight; | ||
| 202 | 2 | float adjNewValue = ltitTableHelper[adjI] * (1.0f + adjCorrection); | ||
| 203 | 2 | adjNewValue = alpha * adjNewValue + (1.0f - alpha) * ltitTableHelper[adjI]; | ||
| 204 | 2 | ltitTableHelper[adjI] = clampF(clampMin, adjNewValue, clampMax); | ||
| 205 | } | |||
| 206 | } | |||
| 207 | ||||
| 208 | 1 | updatedLtit = true; | ||
| 209 | } | |||
| 210 | ||||
| 211 | 9 | void LongTermIdleTrim::onIgnitionStateChanged(bool ignitionOn) { | ||
| 212 | 9 | m_ignitionState = ignitionOn; | ||
| 213 | ||||
| 214 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
|
2/2✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 2 times.
|
9 | if (ignitionOn) { |
| 215 | // Reset timers when ignition turns on | |||
| 216 | 7 | m_updateTimer.reset(); | ||
| 217 | 7 | m_stableIdleTimer.reset(); | ||
| 218 | 7 | isStableIdle = false; | ||
| 219 | 7 | m_pendingSave = false; | ||
| 220 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
1/2✓ Decision 'true' taken 2 times.
✗ Decision 'false' not taken.
|
2 | } else if (updatedLtit) { |
| 221 | // Schedule save after ignition off | |||
| 222 | // TODO: maybe move all this to EngineModule & use needsDelayedShutoff? | |||
| 223 | 2 | m_pendingSave = true; | ||
| 224 | 2 | m_ignitionOffTimer.reset(); | ||
| 225 | 2 | updatedLtit = false; | ||
| 226 | } | |||
| 227 | 9 | } | ||
| 228 | ||||
| 229 | 1103 | void LongTermIdleTrim::checkIfShouldSave() { | ||
| 230 | // Handle delayed save after ignition off | |||
| 231 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1101 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2/2✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1101 times.
|
1103 | if (m_pendingSave && !m_ignitionState) { |
| 232 | 2 | float saveDelaySeconds = engineConfiguration->ltitIgnitionOffSaveDelay; | ||
| 233 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
1/2✗ Decision 'true' not taken.
✓ Decision 'false' taken 2 times.
|
2 | if (saveDelaySeconds <= 0) { |
| 234 | //TODO: this is part of setDefaultEngineConfiguration? | |||
| 235 | ✗ | saveDelaySeconds = 5.0f; // Default 5 seconds | ||
| 236 | } | |||
| 237 | ||||
| 238 |
2/2✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 1 time.
|
2 | if (m_ignitionOffTimer.hasElapsedSec(saveDelaySeconds)) { |
| 239 | // TODO: copyArray? | |||
| 240 | // Save to flash memory | |||
| 241 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 1 time.
|
9 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 242 | // Convert float to autoscaled uint16_t | |||
| 243 | 8 | config->ltitTable[i] = static_cast<uint16_t>(ltitTableHelper[i]); | ||
| 244 | } | |||
| 245 | ||||
| 246 | #if EFI_PROD_CODE | |||
| 247 | //TODO: we need to use requestBurn here? | |||
| 248 | setNeedToWriteConfiguration(); | |||
| 249 | #endif // EFI_PROD_CODE | |||
| 250 | 1 | m_pendingSave = false; | ||
| 251 | } | |||
| 252 | } | |||
| 253 | 1103 | } | ||
| 254 | ||||
| 255 | //TODO: unused? | |||
| 256 | 2 | void LongTermIdleTrim::smoothLtitTable(float intensity) { | ||
| 257 |
4/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
|
2/2✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 1 time.
|
2 | if (!engineConfiguration->ltitEnabled || intensity <= 0.0f || intensity > 100.0f) { |
| 258 | 1 | return; // Invalid intensity or LTIT disabled | ||
| 259 | } | |||
| 260 | ||||
| 261 | // Normalize intensity to 0.0-1.0 range | |||
| 262 | 1 | float normalizedIntensity = intensity / 100.0f; | ||
| 263 | ||||
| 264 | // Apply 1D smoothing for the temperature-based curve | |||
| 265 | 1 | float temp[LTIT_TABLE_SIZE]; | ||
| 266 | ||||
| 267 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 1 time.
|
9 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 268 | 8 | float sum = ltitTableHelper[i]; | ||
| 269 | 8 | int count = 1; | ||
| 270 | ||||
| 271 | // Add values from adjacent cells if they exist | |||
| 272 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 1 time.
|
8 | if (i > 0) { |
| 273 | 7 | sum += ltitTableHelper[i-1]; | ||
| 274 | 7 | count++; | ||
| 275 | } | |||
| 276 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 1 time.
|
8 | if (i < LTIT_TABLE_SIZE-1) { |
| 277 | 7 | sum += ltitTableHelper[i+1]; | ||
| 278 | 7 | count++; | ||
| 279 | } | |||
| 280 | ||||
| 281 | // Calculate the average of the cell and its neighbors | |||
| 282 | 8 | float avg = sum / count; | ||
| 283 | ||||
| 284 | // Apply weighted average based on intensity | |||
| 285 | 8 | temp[i] = ltitTableHelper[i] * (1.0f - normalizedIntensity) + avg * normalizedIntensity; | ||
| 286 | } | |||
| 287 | ||||
| 288 | // Copy back the smoothed values | |||
| 289 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
|
2/2✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 1 time.
|
9 | for (int i = 0; i < LTIT_TABLE_SIZE; i++) { |
| 290 | 8 | ltitTableHelper[i] = temp[i]; | ||
| 291 | } | |||
| 292 | ||||
| 293 | // Mark for saving | |||
| 294 | 1 | m_pendingSave = true; | ||
| 295 | } | |||
| 296 | ||||
| 297 | #endif // EFI_IDLE_CONTROL | |||
| 298 |