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.
This commit is contained in:
2026-08-01 17:52:32 -04:00
parent 6f2d618b8d
commit d338dfe3e8
11 changed files with 37 additions and 16 deletions
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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. |
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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})
+1 -1
View File
@@ -241,7 +241,7 @@ function buildKeyboard()
}
end
function setup()
function init()
buildMenu()
end
+14 -3
View File
@@ -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;
+5
View File
@@ -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;
+5
View File
@@ -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);
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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")