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")