GCC Code Coverage Report


Directory: ./
File: firmware/controllers/sensors/impl/AemXSeriesLambda.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 82.0% 109 0 133
Functions: 100.0% 12 0 12
Branches: 79.4% 50 0 63
Decisions: 80.5% 33 - 41

Line Branch Decision Exec Source
1 #include "pch.h"
2 #include "ego.h"
3
4 #if EFI_CAN_SUPPORT || EFI_UNIT_TEST
5 #include "AemXSeriesLambda.h"
6 #include "wideband_firmware/for_rusefi/wideband_can.h"
7 #include "rusefi_wideband.h"
8
9 static constexpr uint32_t aem_base = 0x180;
10 static constexpr uint32_t rusefi_base = WB_DATA_BASE_ADDR;
11 // sensor transmits at 100hz, allow a frame to be missed
12 static constexpr efidur_t sensor_timeout = MS2NT(3 * WBO_TX_PERIOD_MS);
13
14 8 AemXSeriesWideband::AemXSeriesWideband(uint8_t sensorIndex, SensorType type)
15 : CanSensorBase(
16 0, // ID passed here doesn't matter since we override acceptFrame
17 type,
18 sensor_timeout
19 )
20 8 , m_sensorIndex(sensorIndex)
21 {
22 8 faultCode = m_faultCode = static_cast<uint8_t>(wbo::Fault::CanSilent);// silent, initial state is "no one has spoken to us so far"
23 8 isValid = m_isFault = m_afrIsValid = false;
24 // wait for first rusEFI WBO standard frame with protocol version field
25 8 fwUnsupported = true;
26 // Do not light "FW: need update" indicator until we get some data from WBO
27 8 fwOutdated = false;
28 8 }
29
30 53 can_wbo_type_e AemXSeriesWideband::sensorType() const {
31 53 return engineConfiguration->canWbo[m_sensorIndex].type;
32 }
33
34 13 uint32_t AemXSeriesWideband::getReCanId() const {
35 // 0th sensor is 0x190 and 0x191, 1st sensor is 0x192 and 0x193
36 13 return rusefi_base + 2 * engineConfiguration->canWbo[m_sensorIndex].reId;
37 }
38
39 5 uint32_t AemXSeriesWideband::getAemCanId() const {
40 // 0th sensor is 0x00000180, 1st sensor is 0x00000181, etc
41 5 return aem_base + engineConfiguration->canWbo[m_sensorIndex].aemId;
42 }
43
44 21 bool AemXSeriesWideband::acceptFrame(const size_t busIndex, const CANRxFrame& frame) const {
45 #if !EFI_UNIT_TEST
46 if (busIndex != getWidebandBus()) {
47 return false;
48 }
49 #endif
50
51
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 21 times.
21 if (frame.DLC != 8) {
52 return false;
53 }
54
55 21 can_wbo_type_e type = sensorType();
56
57
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 21 times.
21 if (type == DISABLED) {
58 return false;
59 }
60
61 // RusEFI wideband uses standard CAN IDs
62
4/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 1 time.
2/2
✓ Decision 'true' taken 13 times.
✓ Decision 'false' taken 8 times.
21 if ((!CAN_ISX(frame)) && (type == RUSEFI)) {
63 // 0th sensor is 0x190 and 0x191, 1st sensor is 0x192 and 0x193
64 13 uint32_t rusefiBaseId = getReCanId();
65
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
13 return ((CAN_SID(frame) == rusefiBaseId) || (CAN_SID(frame) == rusefiBaseId + 1));
66 }
67
68 // AEM uses extended CAN ID
69
4/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 2 times.
2/2
✓ Decision 'true' taken 5 times.
✓ Decision 'false' taken 3 times.
8 if ((CAN_ISX(frame)) && (type == AEM)) {
70 // 0th sensor is 0x00000180, 1st sensor is 0x00000181, etc
71 5 uint32_t aemXSeriesId = getAemCanId();
72 5 return (CAN_EID(frame) == aemXSeriesId);
73 }
74
75 3 return false;
76 }
77
78 9 bool AemXSeriesWideband::isHeaterAllowed() {
79
3/4
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 4 times.
9 return ((sensorType() == AEM) || (engine->engineState.heaterControlEnabled));
80 }
81
82 11 void AemXSeriesWideband::refreshState() {
83 11 can_wbo_type_e type = sensorType();
84
85
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 11 times.
11 if (getTimeNowNt() - sensor_timeout > m_lastUpdate) {
86 faultCode = static_cast<uint8_t>(wbo::Fault::CanSilent);
87 isValid = false;
88 return;
89 }
90
91
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
2/2
✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 3 times.
11 if (type == RUSEFI) {
92 // This is RE WBO
93
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
2/2
✓ Decision 'true' taken 4 times.
✓ Decision 'false' taken 4 times.
8 if (!isHeaterAllowed()) {
94 4 faultCode = static_cast<uint8_t>(wbo::Fault::NotAllowed);
95 4 isValid = false;
96 // Do not check for any errors while heater is not allowed
97 4 return;
98 }
99
100 4 isValid = m_afrIsValid;
101
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 (m_faultCode != static_cast<uint8_t>(wbo::Fault::None)) {
102 // Report error code from WBO
103 1 faultCode = m_faultCode;
104 1 isValid = false;
105 1 return;
106 }
107
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 3 times.
✗ Decision 'false' not taken.
3 } else if (type == AEM) {
108 // This is AEM with two flags only and no debug fields
109 3 heaterDuty = 0;
110 3 pumpDuty = 0;
111 3 tempC = 0;
112 3 nernstVoltage = 0;
113
114 3 isValid = m_afrIsValid;
115
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 (m_isFault) {
116 1 faultCode = HACK_INVALID_AEM;
117 1 isValid = false;
118 1 return;
119 }
120 } else {
121 // disabled or analog
122 // clear all livedata
123 heaterDuty = 0;
124 pumpDuty = 0;
125 tempC = 0;
126 nernstVoltage = 0;
127 isValid = false;
128 return;
129 }
130
131 5 faultCode = static_cast<uint8_t>(wbo::Fault::None);
132 }
133
134 12 void AemXSeriesWideband::decodeFrame(const CANRxFrame& frame, efitick_t nowNt) {
135 12 bool accepted = false;
136 // accept frame has already guaranteed that this message belongs to us
137 // We just have to check if it's AEM or rusEFI
138
2/2
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 3 times.
2/2
✓ Decision 'true' taken 9 times.
✓ Decision 'false' taken 3 times.
12 if (sensorType() == RUSEFI){
139
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 uint32_t id = CAN_ID(frame);
140
141 // rusEFI custom format
142
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 5 times.
2/2
✓ Decision 'true' taken 4 times.
✓ Decision 'false' taken 5 times.
9 if ((id & 0x1) != 0) {
143 // low bit is set, this is the "diag" frame
144 4 decodeRusefiDiag(frame);
145 } else {
146 // low bit not set, this is standard frame
147 5 accepted = decodeRusefiStandard(frame, nowNt);
148 }
149 } else /* if (sensorType() == AEM) */ {
150 3 accepted = decodeAemXSeries(frame, nowNt);
151 }
152
153
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 6 times.
2/2
✓ Decision 'true' taken 5 times.
✓ Decision 'false' taken 6 times.
11 if (accepted) {
154 5 m_lastUpdate = nowNt;
155 }
156
157 // Do refresh on each CAN packet
158 11 refreshState();
159 11 }
160
161 /**
162 * @return true if valid, false if invalid
163 */
164 6 bool AemXSeriesWideband::decodeAemXSeries(const CANRxFrame& frame, efitick_t nowNt) {
165 // we don't care
166 6 fwUnsupported = false;
167 6 fwOutdated = false;
168
169 // reports in 0.0001 lambda per LSB
170 6 uint16_t lambdaInt = SWAP_UINT16(frame.data16[0]);
171 6 float lambdaFloat = 0.0001f * lambdaInt;
172
173 // bit 6 indicates sensor fault
174 6 m_isFault = frame.data8[7] & 0x40;
175 // bit 7 indicates valid
176 6 m_afrIsValid = frame.data8[6] & 0x80;
177
178
4/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 2 times.
2/2
✓ Decision 'true' taken 4 times.
✓ Decision 'false' taken 2 times.
6 if ((m_isFault) || (!m_afrIsValid)) {
179 4 invalidate();
180 4 return false;
181 }
182
183 2 setValidValue(lambdaFloat, nowNt);
184 2 refreshSmoothedLambda(lambdaFloat);
185 2 return true;
186 }
187
188 5 bool AemXSeriesWideband::decodeRusefiStandard(const CANRxFrame& frame, efitick_t nowNt) {
189 5 auto data = reinterpret_cast<const wbo::StandardData*>(&frame.data8[0]);
190
191
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 5 times.
5 if (data->Version > RUSEFI_WIDEBAND_VERSION) {
192 firmwareError(ObdCode::OBD_WB_FW_Mismatch, "Wideband controller index %d has newer protocol version (0x%02x while 0x%02x supported), please update ECU FW!",
193 m_sensorIndex, data->Version, RUSEFI_WIDEBAND_VERSION);
194 fwUnsupported = true;
195 return false;
196 }
197
198
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 (data->Version < RUSEFI_WIDEBAND_VERSION_MIN) {
199 1 firmwareError(ObdCode::OBD_WB_FW_Mismatch, "Wideband controller index %d has outdated protocol version (0x%02x while minimum 0x%02x expected), please update WBO!",
200 1 m_sensorIndex, data->Version, RUSEFI_WIDEBAND_VERSION_MIN);
201 fwUnsupported = true;
202 return false;
203 }
204
205 4 fwUnsupported = false;
206 // compatible, but not latest
207 4 fwOutdated = (data->Version != RUSEFI_WIDEBAND_VERSION);
208 // TODO: request and check builddate
209
210 4 tempC = data->TemperatureC;
211 4 float lambda = 0.0001f * data->Lambda;
212 4 m_afrIsValid = data->Valid & 0x01;
213
214
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 (!m_afrIsValid) {
215 2 invalidate();
216 } else {
217 2 setValidValue(lambda, nowNt);
218 2 refreshSmoothedLambda(lambda);
219 }
220
221 4 return true;
222 }
223
224 4 void AemXSeriesWideband::refreshSmoothedLambda(float lambda) {
225
1/3
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
4 switch (type()) {
226
1/1
✓ Decision 'true' taken 4 times.
4 case SensorType::Lambda1: {
227 4 expAverageLambda1.setSmoothingFactor(engineConfiguration->afrExpAverageAlpha);
228 4 smoothedLambda1Sensor.setValidValue(expAverageLambda1.initOrAverage(lambda), getTimeNowNt());
229 4 break;
230 }
231 case SensorType::Lambda2: {
232 expAverageLambda2.setSmoothingFactor(engineConfiguration->afrExpAverageAlpha);
233 smoothedLambda2Sensor.setValidValue(expAverageLambda2.initOrAverage(lambda), getTimeNowNt());
234 break;
235 }
236 default:
237 break;
238 }
239 4 }
240
241 4 void AemXSeriesWideband::decodeRusefiDiag(const CANRxFrame& frame) {
242
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 4 times.
4 if (fwUnsupported) {
243 return;
244 }
245
246 4 auto data = reinterpret_cast<const wbo::DiagData*>(&frame.data8[0]);
247
248 // convert to percent
249 4 heaterDuty = data->HeaterDuty / 2.55f;
250 4 pumpDuty = data->PumpDuty / 2.55f;
251
252 // convert to volts
253 4 nernstVoltage = data->NernstDc * 0.001f;
254
255 // no conversion, just ohms
256 4 esr = data->Esr;
257
258 // This state is handle in refreshState()
259 //if (!isHeaterAllowed()) {
260 // m_faultCode = HACK_CRANKING_VALUE;
261 // return;
262 //}
263
264 4 m_faultCode = static_cast<uint8_t>(data->Status);
265
266
5/6
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 3 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 3 times.
4 if ((data->Status != wbo::Fault::None) && isHeaterAllowed()) {
267
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 auto code = m_sensorIndex == 0 ? ObdCode::Wideband_1_Fault : ObdCode::Wideband_2_Fault;
268 1 warning(code, "Wideband #%d fault: %s", (m_sensorIndex + 1), wbo::describeFault(data->Status));
269 }
270 }
271
272 #endif
273