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 |