From d338dfe3e8d95502a334698cbdc2e49cb32719e0 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Sat, 1 Aug 2026 17:52:32 -0400 Subject: [PATCH] refactor: rename the app entry point to init() and require it crosspoint-reader calls it init() and requires it; this called it setup() and treated it as optional. Same concept, two spellings, so an app could not move between the two firmwares for no reason worth defending. init() wins because it is also the stricter contract: a misspelled entry point is now an error instead of an app that starts, draws nothing, and explains nothing. Requiring it exposed that error screens were unreadable. fail() painted the message and the host relaunched the launcher over it on the very next frame, so every Lua error was serial-only -- which would have made "Missing init()" useless to anyone holding the device rather than a console. --- README.md | 2 +- docs/lua-api-parity.md | 4 ++-- scripts/gen_lua_stubs.py | 2 +- sdcard/apps/hello/main.lua | 2 +- sdcard/apps/launcher/main.lua | 2 +- sdcard/apps/settings/main.lua | 2 +- src/lua/lua_app.cpp | 17 ++++++++++++++--- src/lua/lua_app.h | 5 +++++ src/main.cpp | 5 +++++ stubs/esp32lcd.lua | 2 +- test/settings_calibration.lua | 10 +++++----- 11 files changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e28fa8d..ed3e4aa 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ firmware only draws a fallback screen if it cannot start. ## Lua API -Apps define optional callbacks: `setup()`, `draw()` (~30fps cap), `on_touch_down(x, y)`, +Apps define `init()`, which is required, plus optional `draw()` (~30fps cap), `on_touch_down(x, y)`, `on_touch_up(x, y)`, `on_touch(x, y)` (tap alias, fired on release), and `on_tick()` (enabled by setting `TICK_MS`). Call `sys.exit()` to return to the launcher. diff --git a/docs/lua-api-parity.md b/docs/lua-api-parity.md index 8881642..0e55702 100644 --- a/docs/lua-api-parity.md +++ b/docs/lua-api-parity.md @@ -12,7 +12,8 @@ Compared against crosspoint-reader at `src/util/lua/LuaBindings*.cpp`. `fs.listDirs`, `fs.listFiles`, `fs.exists`, `fs.readFile`, `fs.writeFile`, `gui.width`, `gui.height`, `gui.fillRect`, `gui.drawRect`, `gui.drawLine`, `sys.millis`, `sys.delay`, `sys.exit`, `log.debug/info/error`, the whole `http` table, -and the `draw()` / `on_tick()` callbacks. +and the `init()` / `draw()` / `on_tick()` callbacks. `init()` is required in both, so a +misspelled entry point is an error rather than an app that quietly draws nothing. ## Deliberate differences @@ -37,7 +38,6 @@ Each of these is the same concept spelled two ways. Fixing them means changing o | Concern | crosspoint-reader | esp32-lcd | Suggested resolution | |---|---|---|---| -| Entry callback | `init()`, required | `setup()`, optional | Pick one name. Neither is better. | | Tick interval | `app.setTickInterval(ms)` | global `TICK_MS` | Pick one mechanism. | | `wifi.status()` | returns a **string** | returns a **table** of state/ssid/ip/rssi | Same name, incompatible types — the sharpest edge here. This repo added `isConnected()` and `localIP()` so the crosspoint idioms work either way. | | `wifi.connect()` | no arguments, uses stored credentials | `(ssid, password)`, saves them | Both are wanted: a no-argument reconnect and an explicit join. | diff --git a/scripts/gen_lua_stubs.py b/scripts/gen_lua_stubs.py index cca033a..99a5b0e 100644 --- a/scripts/gen_lua_stubs.py +++ b/scripts/gen_lua_stubs.py @@ -121,7 +121,7 @@ def render(modules): out.extend( [ "-- Callbacks an app may define as globals:", - "-- setup() once, before the first draw", + "-- init() once, before the first draw (required)", "-- draw() every 33 ms while the app runs", "-- on_tick() every TICK_MS, when that global is set", "-- on_touch_down(x, y) finger down", diff --git a/sdcard/apps/hello/main.lua b/sdcard/apps/hello/main.lua index b5e4ef2..97565bd 100644 --- a/sdcard/apps/hello/main.lua +++ b/sdcard/apps/hello/main.lua @@ -6,7 +6,7 @@ local seconds = 0 -- Draws straight to the panel rather than through components, so it reads the theme. local theme = ui.theme -function setup() +function init() gui.clear(theme.bg) gui.drawText("hello from sd card", 10, 10, theme.fg, theme.bg) gui.drawText("touch the screen", 10, 30, theme.muted, theme.bg) diff --git a/sdcard/apps/launcher/main.lua b/sdcard/apps/launcher/main.lua index 55ff9ee..53a3d38 100644 --- a/sdcard/apps/launcher/main.lua +++ b/sdcard/apps/launcher/main.lua @@ -19,7 +19,7 @@ local function appRow(name) } end -function setup() +function init() -- Fixed width: drawText paints an opaque background only under its own glyphs, so a -- shorter label would leave the tail of the previous one on screen. clock = ui.text(PLACEHOLDER, {w = gui.textWidth(PLACEHOLDER), color = ui.theme.muted}) diff --git a/sdcard/apps/settings/main.lua b/sdcard/apps/settings/main.lua index 9ceedc1..0f4f6cc 100644 --- a/sdcard/apps/settings/main.lua +++ b/sdcard/apps/settings/main.lua @@ -241,7 +241,7 @@ function buildKeyboard() } end -function setup() +function init() buildMenu() end diff --git a/src/lua/lua_app.cpp b/src/lua/lua_app.cpp index 0a778a2..62a9ae4 100644 --- a/src/lua/lua_app.cpp +++ b/src/lua/lua_app.cpp @@ -79,9 +79,16 @@ void LuaApp::fail(const char* message) { tft.setTextColor(TFT_RED, TFT_WHITE); tft.drawString("Lua error:", 8, 8); tft.drawString(message ? message : "(unknown)", 8, 28); + failed = true; requestExit(); } +bool LuaApp::takeFailure() { + bool value = failed; + failed = false; + return value; +} + bool LuaApp::load(const char* path) { closeState(); // The launcher tap may still be down; swallow that gesture's release. @@ -102,10 +109,14 @@ bool LuaApp::load(const char* path) { return false; } if (!callGlobal(path)) return false; // run chunk body - if (hasGlobal("setup")) { - lua_getglobal(state, "setup"); - if (!callGlobal("setup")) return false; + // Required, as on crosspoint-reader: an app whose entry point is misspelled would + // otherwise start, draw nothing, and give no hint why. + if (!hasGlobal("init")) { + fail("Missing init()"); + return false; } + lua_getglobal(state, "init"); + if (!callGlobal("init")) return false; // Cleared first: the interval outlives the app that set it, so a ticking app // followed by one without on_tick would otherwise keep calling a nil global. tickIntervalMs = 0; diff --git a/src/lua/lua_app.h b/src/lua/lua_app.h index ddb7c38..ac0c072 100644 --- a/src/lua/lua_app.h +++ b/src/lua/lua_app.h @@ -24,6 +24,10 @@ class LuaApp { bool running() const { return state != nullptr && !exitRequested; } void requestExit(); + // True once if the app died on an error rather than calling sys.exit(), so the host + // can leave the message on screen long enough to read. + bool takeFailure(); + // Panel geometry in its rotation-0 frame; calibration is stored in this space // so rotating the UI never needs a recalibration. static constexpr int16_t PANEL_W = 320; @@ -37,6 +41,7 @@ class LuaApp { private: lua_State* state = nullptr; bool exitRequested = false; + bool failed = false; String pendingLaunch; bool lastTouched = false; bool ignoreRelease = false; diff --git a/src/main.cpp b/src/main.cpp index ca0c706..a9780ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ static constexpr int SD_MISO = 19; static constexpr int TOUCH_CS = 33; static const char* LAUNCHER = "/apps/launcher/main.lua"; +static constexpr uint32_t ERROR_HOLD_MS = 5000; // Arduino's default 8KB loop stack is not enough once a TLS handshake runs inside a // Lua binding: the first HTTPS download tripped the stack canary. Everything the VM @@ -79,6 +80,10 @@ void loop() { return; } + // An app that died left its message on screen, and the launcher is about to paint + // over it. Serial alone is no help to anyone holding the device. + if (app.takeFailure()) delay(ERROR_HOLD_MS); + String path = nextApp.length() > 0 ? nextApp : String(LAUNCHER); nextApp = ""; startApp(path); diff --git a/stubs/esp32lcd.lua b/stubs/esp32lcd.lua index a8ce4ae..5ab4ca1 100644 --- a/stubs/esp32lcd.lua +++ b/stubs/esp32lcd.lua @@ -297,7 +297,7 @@ function wifi.disconnect() end function wifi.forget() end -- Callbacks an app may define as globals: --- setup() once, before the first draw +-- init() once, before the first draw (required) -- draw() every 33 ms while the app runs -- on_tick() every TICK_MS, when that global is set -- on_touch_down(x, y) finger down diff --git a/test/settings_calibration.lua b/test/settings_calibration.lua index 52b57ce..9245cb4 100644 --- a/test/settings_calibration.lua +++ b/test/settings_calibration.lua @@ -47,7 +47,7 @@ local fx0, _, fx1 = computeCalibration(s3, s4, w, h, inset) assert(fx0 > fx1, "flipped axis should descend") -- Rotation cycles through the four quarter turns and wraps back to 0. -setup() +init() for _, expected in ipairs({90, 180, 270, 0}) do tapRow("rotation:") assert(sys.getRotation() == expected, "rotation " .. sys.getRotation()) @@ -55,14 +55,14 @@ end -- Timezone cycles through the picker list and stores the POSIX rule, not the name. local zones = require("timezones") -setup() +init() tapRow("timezone:") assert(sys.getTimezone() == zones[2].tz, "timezone " .. sys.getTimezone()) tapRow("timezone:") assert(sys.getTimezone() == zones[3].tz, "timezone " .. sys.getTimezone()) -- Calibration collects one sample per target and saves on the second release. -setup() +init() tapRow("calibrate") on_tick() -- release after the menu tap arms sampling device.raw = {s1.x, s1.y} @@ -78,7 +78,7 @@ assert(saved, "calibration was not saved") assert(math.abs(saved[1] - 200) <= 1, "saved x0 " .. saved[1]) -- Open networks connect directly from scan results. -setup() +init() device.networks = {{ssid = "qemu", rssi = -25, secure = false}} tapRow("wifi:") tapRow("scan networks") @@ -88,7 +88,7 @@ assert(device.connected, "open network should connect without a keyboard") assert(device.connected[1] == "qemu" and device.connected[2] == "", "open wifi connect") -- Secure networks route through the keyboard and preserve typed punctuation. -setup() +init() device.networks = {{ssid = "secure", rssi = -40, secure = true}} tapRow("wifi:") tapRow("scan networks")