GCC Code Coverage Report


Directory: ./
File: firmware/controllers/lua/lua.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 81.0% 85 0 105
Functions: 83.3% 10 0 12
Branches: 71.7% 66 0 92
Decisions: 71.4% 30 - 42

Line Branch Decision Exec Source
1 #include "pch.h"
2
3 #include "rusefi_lua.h"
4 #include "thread_controller.h"
5
6 #if EFI_LUA
7
8 #include "lua.hpp"
9 #include "lua_heap.h"
10 #include "lua_hooks.h"
11 #include "can_filter.h"
12
13 #define TAG "LUA "
14
15 static bool withErrorLoading = false;
16 static int luaTickPeriodUs;
17
18 #if EFI_CAN_SUPPORT
19 static int recentRxCount = 0;
20 static int totalRxCount = 0;
21 static int rxTime;
22 #endif // EFI_CAN_SUPPORT
23
24 static int lua_setTickRate(lua_State* l) {
25 float userFreq = luaL_checknumber(l, 1);
26
27 // For instance BMW does 100 CAN messages per second on some IDs, let's allow at least twice that speed
28 // Limit to 1..200 hz
29 float freq = clampF(1, userFreq, 2000);
30 if (freq != userFreq) {
31 efiPrintf(TAG "clamping tickrate %f", freq);
32 }
33
34 if (freq > 150 && !engineConfiguration->luaCanRxWorkaround) {
35 efiPrintf(TAG "luaCanRxWorkaround recommended at high tick rate!");
36 }
37
38 luaTickPeriodUs = 1000000.0f / freq;
39 return 0;
40 }
41
42 309 static void loadLibraries(LuaHandle& ls) {
43 309 constexpr luaL_Reg libs[] = {
44 // TODO: do we even need the base lib?
45 //{ LUA_GNAME, luaopen_base },
46 { LUA_MATHLIBNAME, luaopen_math },
47 };
48
49
2/2
✓ Branch 1 taken 309 times.
✓ Branch 2 taken 309 times.
2/2
✓ Decision 'true' taken 309 times.
✓ Decision 'false' taken 309 times.
618 for (size_t i = 0; i < efi::size(libs); i++) {
50
1/1
✓ Branch 4 taken 309 times.
309 luaL_requiref(ls, libs[i].name, libs[i].func, 1);
51
1/1
✓ Branch 2 taken 309 times.
309 lua_pop(ls, 1);
52 }
53 309 }
54
55 309 static LuaHandle setupLuaState(lua_Alloc alloc) {
56
1/1
✓ Branch 2 taken 309 times.
309 LuaHandle ls = lua_newstate(alloc, NULL);
57
58
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 309 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 309 times.
309 if (!ls) {
59 criticalError("Failed to start Lua interpreter");
60
61 return nullptr;
62 }
63
64
1/1
✓ Branch 4 taken 309 times.
309 lua_atpanic(ls, [](lua_State* l) {
65 criticalError("Lua panic: %s", lua_tostring(l, -1));
66
67 // hang the lua thread
68 while (true) ;
69
70 return 0;
71 });
72
73 // Load Lua's own libraries
74
1/1
✓ Branch 1 taken 309 times.
309 loadLibraries(ls);
75
76 // Load rusEFI hooks
77
2/2
✓ Branch 2 taken 309 times.
✓ Branch 6 taken 309 times.
309 lua_register(ls, "setTickRate", lua_setTickRate);
78
1/1
✓ Branch 2 taken 309 times.
309 configureRusefiLuaHooks(ls);
79
80 // run a GC cycle
81
1/1
✓ Branch 2 taken 309 times.
309 lua_gc(ls, LUA_GCCOLLECT, 0);
82
83 // set GC settings
84 // see https://www.lua.org/manual/5.4/manual.html#2.5.1
85
1/1
✓ Branch 2 taken 309 times.
309 lua_gc(ls, LUA_GCINC, 50, 1000, 9);
86
87 309 return ls;
88 309 }
89
90 309 static bool loadScript(LuaHandle& ls, const char* scriptStr) {
91 309 efiPrintf(TAG "loading script length: %u...", std::strlen(scriptStr));
92
93
6/6
✓ Branch 2 taken 308 times.
✓ Branch 3 taken 1 time.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 302 times.
✓ Branch 8 taken 7 times.
✓ Branch 9 taken 302 times.
2/2
✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 302 times.
309 if (0 != luaL_dostring(ls, scriptStr)) {
94 7 withErrorLoading = true;
95 7 efiPrintf(TAG "ERROR loading script: %s", lua_tostring(ls, -1));
96 7 lua_pop(ls, 1);
97 7 return false;
98 }
99
100 302 efiPrintf(TAG "script loaded successfully!");
101
102 #if EFI_PROD_CODE
103 luaHeapPrintInfo();
104 #endif // EFI_PROD_CODE
105
106 302 return true;
107 }
108
109 #if !EFI_UNIT_TEST
110 static bool interactivePending = false;
111 static char interactiveCmd[100];
112
113 static void doInteractive(LuaHandle& ls) {
114 if (!interactivePending) {
115 // no cmd pending, return
116 return;
117 }
118
119 auto status = luaL_dostring(ls, interactiveCmd);
120
121 if (0 == status) {
122 // Function call was OK, resolve return value and print it
123 if (lua_isinteger(ls, -1)) {
124 efiPrintf(TAG "interactive returned integer: %d", lua_tointeger(ls, -1));
125 } else if (lua_isnumber(ls, -1)) {
126 efiPrintf(TAG "interactive returned number: %f", lua_tonumber(ls, -1));
127 } else if (lua_isstring(ls, -1)) {
128 efiPrintf(TAG "interactive returned string: '%s'", lua_tostring(ls, -1));
129 } else if (lua_isboolean(ls, -1)) {
130 efiPrintf(TAG "interactive returned bool: %s", lua_toboolean(ls, -1) ? "true" : "false");
131 } else if (lua_isnil(ls, -1)) {
132 efiPrintf(TAG "interactive returned nil.");
133 } else {
134 efiPrintf(TAG "interactive returned nothing.");
135 }
136 } else {
137 // error with interactive command, print it
138 efiPrintf(TAG "interactive error: %s", lua_tostring(ls, -1));
139 }
140
141 interactivePending = false;
142
143 lua_settop(ls, 0);
144 }
145
146 static uint32_t maxLuaDuration{};
147
148 static void invokeTick(LuaHandle& ls) {
149 ScopePerf perf(PE::LuaTickFunction);
150
151 // run the tick function
152 lua_getglobal(ls, "onTick");
153 if (lua_isnil(ls, -1)) {
154 // TODO: handle missing tick function
155 lua_settop(ls, 0);
156 return;
157 }
158 #if EFI_PROD_CODE
159 uint32_t before = port_rt_get_counter_value();
160 #endif // EFI_PROD_CODE
161
162 int status = lua_pcall(ls, 0, 0, 0);
163
164 #if EFI_PROD_CODE
165 uint32_t duration = port_rt_get_counter_value() - before;
166 maxLuaDuration = std::max(maxLuaDuration, duration);
167 #endif // EFI_PROD_CODE
168
169 if (0 != status) {
170 // error calling hook function
171 auto errMsg = lua_tostring(ls, -1);
172 efiPrintf(TAG "error %s", errMsg);
173 lua_pop(ls, 1);
174 }
175
176 lua_settop(ls, 0);
177 }
178
179 struct LuaThread : ThreadController<4096> {
180 LuaThread() : ThreadController("lua", PRIO_LUA) { }
181
182 void ThreadTask() override;
183 };
184
185 static void resetLua() {
186 engine->module<AcController>().unmock().isDisabledByLua = false;
187 #if EFI_CAN_SUPPORT
188 resetLuaCanRx();
189 #endif // EFI_CAN_SUPPORT
190
191 // De-init pins, they will reinit next start of the script.
192 luaDeInitPins();
193 }
194
195
196 static bool needsReset = false;
197
198 // Each invocation of runOneLua will:
199 // - create a new Lua instance
200 // - read the script from config
201 // - run the tick function until needsReset is set
202 // Returns true if it should be re-called immediately,
203 // or false if there was a problem setting up the interpreter
204 // or parsing the script.
205 static bool runOneLua(lua_Alloc alloc, const char* script) {
206 needsReset = false;
207
208 auto ls = setupLuaState(alloc);
209
210 // couldn't start Lua interpreter, bail out
211 if (!ls) {
212 return false;
213 }
214
215 // Reset default tick rate
216 luaTickPeriodUs = MS2US(5);
217
218 if (!loadScript(ls, script)) {
219 return false;
220 }
221
222 while (!needsReset && !chThdShouldTerminateX()) {
223 efitick_t beforeNt = getTimeNowNt();
224 #if EFI_CAN_SUPPORT
225 // First, process any pending can RX messages
226 totalRxCount += recentRxCount;
227 recentRxCount = doLuaCanRx(ls);
228 rxTime = getTimeNowNt() - beforeNt;
229 #endif // EFI_CAN_SUPPORT
230
231 // Next, check if there is a pending interactive command entered by the user
232 doInteractive(ls);
233
234 invokeTick(ls);
235
236 engine->outputChannels.luaLastCycleDuration = (getTimeNowNt() - beforeNt);
237 engine->outputChannels.luaInvocationCounter++;
238 chThdSleep(TIME_US2I(luaTickPeriodUs));
239
240 engine->engineState.luaDigitalState0 = getAuxDigital(0);
241 engine->engineState.luaDigitalState1 = getAuxDigital(1);
242 engine->engineState.luaDigitalState2 = getAuxDigital(2);
243 engine->engineState.luaDigitalState3 = getAuxDigital(3);
244 }
245
246 resetLua();
247
248 return true;
249 }
250
251 void LuaThread::ThreadTask() {
252 while (!chThdShouldTerminateX()) {
253 bool wasOk = runOneLua(luaHeapAlloc, config->luaScript);
254
255 auto usedAfterRun = luaHeapUsed();
256 if (usedAfterRun != 0) {
257 if (!withErrorLoading) {
258 efiPrintf(TAG "MEMORY LEAK DETECTED: %d bytes used after teardown", usedAfterRun);
259 }
260
261 // Lua blew up in some terrible way that left memory allocated, reset the heap
262 // so that subsequent runs don't overflow the heap
263 luaHeapReset();
264 }
265
266 // Reset any lua adjustments the script made
267 engine->resetLua();
268
269 if (!wasOk) {
270 // Something went wrong executing the script, spin
271 // until reset invoked (maybe the user fixed the script)
272 while (!needsReset) {
273 chThdSleepMilliseconds(100);
274 }
275 }
276 }
277 }
278
279 static LuaThread luaThread;
280
281 void startLua() {
282 luaHeapInit();
283
284 #if EFI_CAN_SUPPORT
285 initLuaCanRx();
286 #endif // EFI_CAN_SUPPORT
287
288 addConsoleActionII("set_lua_setting", [](int index, int value) {
289 engineConfiguration->scriptSetting[index] = value;
290 });
291
292 luaThread.start();
293
294 addConsoleActionS("lua", [](const char* str){
295 if (interactivePending) {
296 return;
297 }
298
299 strncpy(interactiveCmd, str, sizeof(interactiveCmd) - 1);
300 interactiveCmd[sizeof(interactiveCmd) - 1] = '\0';
301
302 interactivePending = true;
303 });
304
305 addConsoleAction("luareset", [](){
306 needsReset = true;
307 });
308
309 addConsoleAction("luamemory", [](){
310 efiPrintf("maxLuaDuration %lu", maxLuaDuration);
311 maxLuaDuration = 0;
312 efiPrintf("rx total/recent/dropped %d %d %d", totalRxCount,
313 recentRxCount, getLuaCanRxDropped());
314 efiPrintf("luaCycle %luus including luaRxTime %dus", NT2US(engine->outputChannels.luaLastCycleDuration),
315 NT2US(rxTime));
316
317 luaHeapPrintInfo();
318 });
319 }
320
321 #else // not EFI_UNIT_TEST
322
323 1 void startLua() { }
324
325 #include <stdexcept>
326 #include <string>
327
328 100 static LuaHandle runScript(const char* script) {
329 100 auto ls = setupLuaState(luaHeapAlloc);
330
331
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 100 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 100 times.
100 if (!ls) {
332 throw std::logic_error("Call to setupLuaState failed, returned null");
333 }
334
335
3/3
✓ Branch 1 taken 100 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 99 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 99 times.
100 if (!loadScript(ls, script)) {
336
1/1
✓ Branch 2 taken 1 time.
1 throw std::logic_error("Call to loadScript failed");
337 }
338
339
1/1
✓ Branch 2 taken 99 times.
99 lua_getglobal(ls, "testFunc");
340
3/3
✓ Branch 2 taken 99 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 98 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 98 times.
99 if (lua_isnil(ls, -1)) {
341
1/1
✓ Branch 2 taken 1 time.
1 throw std::logic_error("Failed to find function testFunc");
342 }
343
344
1/1
✓ Branch 2 taken 98 times.
98 int status = lua_pcall(ls, 0, 1, 0);
345
346
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 98 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 98 times.
98 if (0 != status) {
347 std::string msg = std::string("lua error while running script: ") + lua_tostring(ls, -1);
348 throw std::logic_error(msg);
349 }
350
351 98 return ls;
352 2 }
353
354 83 expected<float> testLuaReturnsNumberOrNil(const char* script) {
355
1/1
✓ Branch 2 taken 83 times.
83 auto ls = runScript(script);
356
357 // check nil return first
358
3/3
✓ Branch 2 taken 83 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 78 times.
2/2
✓ Decision 'true' taken 5 times.
✓ Decision 'false' taken 78 times.
83 if (lua_isnil(ls, -1)) {
359 5 return unexpected;
360 }
361
362 // If not nil, it should be a number
363
2/3
✓ Branch 2 taken 78 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 78 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 78 times.
78 if (!lua_isnumber(ls, -1)) {
364 throw std::logic_error("Returned value is not a number");
365 }
366
367 // pop the return value
368
1/1
✓ Branch 3 taken 78 times.
78 return lua_tonumber(ls, -1);
369 83 }
370
371 11 float testLuaReturnsNumber(const char* script) {
372
1/1
✓ Branch 2 taken 11 times.
11 auto ls = runScript(script);
373
374 // check the return value
375
2/3
✓ Branch 2 taken 11 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 11 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 11 times.
11 if (!lua_isnumber(ls, -1)) {
376 throw new std::logic_error("Returned value is not a number");
377 }
378
379 // pop the return value
380
1/1
✓ Branch 2 taken 11 times.
22 return lua_tonumber(ls, -1);
381 11 }
382
383 6 int testLuaReturnsInteger(const char* script) {
384
1/1
✓ Branch 2 taken 4 times.
6 auto ls = runScript(script);
385
386 // pop the return value;
387
3/3
✓ Branch 2 taken 4 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 2 times.
4 if (!lua_isinteger(ls, -1)) {
388
1/1
✓ Branch 2 taken 2 times.
2 throw std::logic_error("Returned value is not an integer");
389 }
390
391
1/1
✓ Branch 2 taken 2 times.
4 return lua_tointeger(ls, -1);
392 4 }
393
394 209 void testLuaExecString(const char* script) {
395
1/1
✓ Branch 2 taken 209 times.
209 auto ls = setupLuaState(luaHeapAlloc);
396
397
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 209 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 209 times.
209 if (!ls) {
398 throw std::logic_error("Call to setupLuaState failed, returned null");
399 }
400
401
3/3
✓ Branch 1 taken 209 times.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 203 times.
2/2
✓ Decision 'true' taken 6 times.
✓ Decision 'false' taken 203 times.
209 if (!loadScript(ls, script)) {
402
1/1
✓ Branch 2 taken 6 times.
6 throw std::logic_error("Call to loadScript failed");
403 }
404 412 }
405
406 #endif // EFI_UNIT_TEST
407 #endif // EFI_LUA
408
409 // This is technically non-compliant, but it's only used for lua float parsing.
410 // It doesn't properly handle very small and very large numbers, and doesn't
411 // parse numbers in the format 1.3e5 at all.
412 68 extern "C" float strtof_rusefi(const char* str, char** endPtr) {
413 68 bool afterDecimalPoint = false;
414 68 float div = 1; // Divider to place digits after the decimal point
415
416
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 68 times.
✗ Decision 'false' not taken.
68 if (endPtr) {
417 68 *endPtr = const_cast<char*>(str);
418 }
419
420 68 float integerPart = 0;
421 68 float fractionalPart = 0;
422
423
2/2
✓ Branch 0 taken 322 times.
✓ Branch 1 taken 67 times.
2/2
✓ Decision 'true' taken 322 times.
✓ Decision 'false' taken 67 times.
389 while (*str != '\0') {
424 322 char c = *str;
425 322 int digitVal = c - '0';
426
427
4/4
✓ Branch 0 taken 255 times.
✓ Branch 1 taken 67 times.
✓ Branch 2 taken 254 times.
✓ Branch 3 taken 1 time.
2/2
✓ Decision 'true' taken 254 times.
✓ Decision 'false' taken 68 times.
322 if (c >= '0' && c <= '9') {
428
2/2
✓ Branch 0 taken 93 times.
✓ Branch 1 taken 161 times.
2/2
✓ Decision 'true' taken 93 times.
✓ Decision 'false' taken 161 times.
254 if (!afterDecimalPoint) {
429 // Integer part
430 93 integerPart = 10 * integerPart + digitVal;
431 } else {
432 // Fractional part
433 161 fractionalPart = 10 * fractionalPart + digitVal;
434 161 div *= 10;
435 }
436
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 1 time.
2/2
✓ Decision 'true' taken 67 times.
✓ Decision 'false' taken 1 time.
68 } else if (c == '.') {
437 67 afterDecimalPoint = true;
438 } else {
439 1 break;
440 }
441
442 321 str++;
443
444
1/2
✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 321 times.
✗ Decision 'false' not taken.
321 if (endPtr) {
445 321 *endPtr = const_cast<char*>(str);
446 }
447 }
448
449 68 return integerPart + fractionalPart / div;
450 }
451