GCC Code Coverage Report


Directory: ./
File: firmware/controllers/math/closed_loop_idle.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 97.9% 137 0 140
Functions: 100.0% 10 0 10
Branches: 84.8% 78 0 92
Decisions: 92.9% 65 - 70

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 678 LongTermIdleTrim::LongTermIdleTrim() {
13 678 initializeTableWithDefaults();
14 678 emaError = 0.0f; //TODO: unused?
15 678 ltitTableInitialized = false;
16 678 m_pendingSave = false;
17 678 }
18
19 1269 void LongTermIdleTrim::initializeTableWithDefaults() {
20 // Initialize with 100% (1.0 multiplier) as default
21
2/2
✓ Branch 0 taken 10152 times.
✓ Branch 1 taken 1269 times.
2/2
✓ Decision 'true' taken 10152 times.
✓ Decision 'false' taken 1269 times.
11421 for (int i = 0; i < LTIT_TABLE_SIZE; i++) {
22 10152 ltitTableHelper[i] = 100.0f;
23 }
24 1269 }
25
26 //TODO: move? add? to validateConfigOnStartUpOrBurn
27 597 bool LongTermIdleTrim::hasValidData() const {
28 // More robust validation - check for reasonable range and distribution
29 597 int validCount = 0;
30 597 float totalValue = 0.0f;
31
32
2/2
✓ Branch 0 taken 4776 times.
✓ Branch 1 taken 597 times.
2/2
✓ Decision 'true' taken 4776 times.
✓ Decision 'false' taken 597 times.
5373 for (int i = 0; i < LTIT_TABLE_SIZE; i++) {
33 4776 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 4736 times.
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 6 times.
2/2
✓ Decision 'true' taken 34 times.
✓ Decision 'false' taken 4742 times.
4776 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 593 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 593 times.
✓ Decision 'false' taken 4 times.
597 if (validCount < (LTIT_TABLE_SIZE / 2)) {
44 593 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 594 void LongTermIdleTrim::loadLtitFromConfig() {
52
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 591 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 591 times.
594 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 591 initializeTableWithDefaults();
63 591 ltitTableInitialized = true;
64 }
65 594 }
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 1122 void LongTermIdleTrim::checkIfShouldSave() {
230 // Handle delayed save after ignition off
231
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1120 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1120 times.
1122 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 1122 }
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