refactor: replace the TICK_MS global with app.setTickInterval()
A magic global that the runtime reads once at startup could not be changed later, gave no feedback when misspelled, and was a second spelling of a mechanism the sibling firmware already had. app.setTickInterval(ms) clamps to 33..3600000, takes 0 to stop, and errors when on_tick() is not defined -- by the time init() runs the chunk body has finished, so a missing callback is a typo rather than a race. Also drops the code comments pointing at the other repo. Where the two APIs agree or differ belongs in docs/lua-api-parity.md; a comment beside a constant explaining that another firmware picked the same number is noise a reader here cannot act on.
This commit is contained in:
@@ -39,7 +39,7 @@ firmware only draws a fallback screen if it cannot start.
|
||||
|
||||
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.
|
||||
(enabled by `app.setTickInterval(ms)`). Call `sys.exit()` to return to the launcher.
|
||||
|
||||
`require` reads from the SD card: `/apps/<name>/?.lua` first, then `/lib/?.lua`.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 `init()` / `draw()` / `on_tick()` callbacks. `init()` is required in both, so a
|
||||
`app.setTickInterval`, 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
|
||||
@@ -38,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 |
|
||||
|---|---|---|---|
|
||||
| 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. |
|
||||
| `fs.readFile` cap | 50000 bytes | 65536 bytes | Arbitrary in both. |
|
||||
|
||||
@@ -123,7 +123,7 @@ def render(modules):
|
||||
"-- Callbacks an app may define as globals:",
|
||||
"-- 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_tick() at the interval app.setTickInterval() asked for",
|
||||
"-- on_touch_down(x, y) finger down",
|
||||
"-- on_touch_up(x, y) finger up",
|
||||
"-- on_touch(x, y) tap, fired on release like a click",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
local ui = require("ui")
|
||||
|
||||
TICK_MS = 1000
|
||||
|
||||
local seconds = 0
|
||||
-- Draws straight to the panel rather than through components, so it reads the theme.
|
||||
local theme = ui.theme
|
||||
|
||||
function init()
|
||||
app.setTickInterval(1000)
|
||||
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)
|
||||
|
||||
@@ -3,9 +3,8 @@ local ui = require("ui")
|
||||
local screen
|
||||
local clock
|
||||
|
||||
-- Twice a second, so the seconds digit never visibly lags. WiFi and SNTP run in the
|
||||
-- background; until the clock means something the placeholder holds its width.
|
||||
TICK_MS = 500
|
||||
-- Until the clock means something the placeholder holds its width; WiFi and SNTP run
|
||||
-- in the background.
|
||||
local PLACEHOLDER = "--:--:--"
|
||||
|
||||
-- Rows are interactive containers rather than plain buttons, so each can carry a
|
||||
@@ -20,6 +19,7 @@ local function appRow(name)
|
||||
end
|
||||
|
||||
function init()
|
||||
app.setTickInterval(500) -- twice a second, so the seconds digit never visibly lags
|
||||
-- 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,7 +1,5 @@
|
||||
local ui = require("ui")
|
||||
|
||||
TICK_MS = 50
|
||||
|
||||
local INSET = 30
|
||||
|
||||
local screen, message
|
||||
@@ -242,6 +240,7 @@ function buildKeyboard()
|
||||
end
|
||||
|
||||
function init()
|
||||
app.setTickInterval(50) -- calibration samples the raw panel between draws
|
||||
buildMenu()
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// HTTP client. Signatures match crosspoint-reader's `http` table exactly, so a script
|
||||
// that talks to a server runs on either device; see docs/lua-api-parity.md.
|
||||
//
|
||||
// Unlike crosspoint, certificates are verified against the root bundle already
|
||||
// embedded in the framework. Nothing here lets a script turn that off: the signatures
|
||||
// have no options table to hang it on, and the one caller that would want it most --
|
||||
// a firmware update -- is the one that can least afford an unauthenticated peer.
|
||||
// HTTP client. Certificates are verified against the root bundle already embedded in
|
||||
// the framework, and nothing here lets a script turn that off: the signatures have no
|
||||
// options table to hang it on, and the one caller that would want it most -- a firmware
|
||||
// update -- is the one that can least afford an unauthenticated peer.
|
||||
|
||||
#include <HTTPClient.h>
|
||||
#include <SD.h>
|
||||
@@ -19,7 +16,7 @@ extern const uint8_t rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_star
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MAX_RESPONSE = 50000; // matches crosspoint's cap, so limits behave alike
|
||||
constexpr size_t MAX_RESPONSE = 50000;
|
||||
constexpr uint32_t TIMEOUT_MS = 60000;
|
||||
constexpr int REDIRECT_LIMIT = 5;
|
||||
constexpr size_t DOWNLOAD_CHUNK = 2048;
|
||||
@@ -64,8 +61,8 @@ void applyHeaders(lua_State* L, int index, HTTPClient& http) {
|
||||
}
|
||||
|
||||
// get/head/delete take headers in slot 2; post/patch take a body there and headers in 3.
|
||||
// crosspoint reinterprets a string in slot 2 as a body for every method, which turns a
|
||||
// mistyped header table into a silent protocol error, so this rejects it instead.
|
||||
// A string in slot 2 of a bodyless method is rejected rather than reinterpreted, since
|
||||
// a mistyped headers table would otherwise become a silent protocol error.
|
||||
int request(lua_State* L, const char* method, bool bodyExpected) {
|
||||
const char* url = luaL_checkstring(L, 1);
|
||||
const char* body = "";
|
||||
@@ -122,8 +119,8 @@ int fail(lua_State* L, const char* message) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Only the keys crosspoint accepts, and rejecting the rest: a misspelled `sha256` that
|
||||
// was quietly ignored would report a verified download that verified nothing.
|
||||
// Unknown keys are rejected: a misspelled `sha256` that was quietly ignored would
|
||||
// report a verified download that verified nothing.
|
||||
bool readOptions(lua_State* L, int index, uint32_t& maxBytes, uint32_t& expectedSize,
|
||||
String& sha256, const char*& error) {
|
||||
luaL_checktype(L, index, LUA_TTABLE);
|
||||
|
||||
@@ -96,12 +96,32 @@ static int logAt(lua_State* L, const char* level) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Rejected rather than ignored when on_tick is absent: the chunk body has already run
|
||||
// by the time init() calls this, so a missing callback is a typo, not a race.
|
||||
static int l_app_setTickInterval(lua_State* L) {
|
||||
lua_Integer requested = luaL_checkinteger(L, 1);
|
||||
if (requested <= 0) {
|
||||
app(L)->setTickInterval(0);
|
||||
return 0;
|
||||
}
|
||||
lua_getglobal(L, "on_tick");
|
||||
bool hasTick = lua_isfunction(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (!hasTick) return luaL_error(L, "app.setTickInterval() requires on_tick()");
|
||||
|
||||
uint32_t interval = requested < (lua_Integer)LuaApp::MIN_TICK_MS ? LuaApp::MIN_TICK_MS
|
||||
: requested > (lua_Integer)LuaApp::MAX_TICK_MS ? LuaApp::MAX_TICK_MS
|
||||
: (uint32_t)requested;
|
||||
app(L)->setTickInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_log_debug(lua_State* L) { return logAt(L, "debug"); }
|
||||
static int l_log_info(lua_State* L) { return logAt(L, "info"); }
|
||||
static int l_log_error(lua_State* L) { return logAt(L, "error"); }
|
||||
|
||||
// Parity with crosspoint: sleeping is the one thing a script cannot express itself,
|
||||
// since the runtime owns the loop.
|
||||
// Sleeping is the one thing a script cannot express itself, since the runtime owns
|
||||
// the loop.
|
||||
static int l_sys_delay(lua_State* L) {
|
||||
delay(luaL_checkinteger(L, 1));
|
||||
return 0;
|
||||
@@ -155,7 +175,15 @@ void registerSys(lua_State* L) {
|
||||
luaL_newlib(L, lib);
|
||||
lua_setglobal(L, "sys");
|
||||
|
||||
// Too small to deserve its own translation unit.
|
||||
// These two are too small to deserve their own translation units.
|
||||
static const luaL_Reg appLib[] = {
|
||||
// --- Sets how often on_tick() runs. Errors when on_tick is not defined.
|
||||
// @param intervalMs integer 0 stops ticking; anything else is clamped to 33..3600000.
|
||||
{"setTickInterval", l_app_setTickInterval},
|
||||
{nullptr, nullptr}};
|
||||
luaL_newlib(L, appLib);
|
||||
lua_setglobal(L, "app");
|
||||
|
||||
static const luaL_Reg logLib[] = {
|
||||
// --- Writes a debug line to the serial log.
|
||||
// @param message string
|
||||
|
||||
@@ -85,8 +85,8 @@ static int l_wifi_status(lua_State* L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parity with crosspoint, whose wifi.status() returns a bare string and so needs these
|
||||
// as separate calls. Here they are shorthands for fields status() already carries.
|
||||
// Shorthands for two fields status() already carries, for scripts that want one answer
|
||||
// without unpacking a table.
|
||||
static int l_wifi_isConnected(lua_State* L) {
|
||||
lua_pushboolean(L, WiFi.status() == WL_CONNECTED);
|
||||
return 1;
|
||||
|
||||
+10
-14
@@ -91,6 +91,9 @@ bool LuaApp::takeFailure() {
|
||||
|
||||
bool LuaApp::load(const char* path) {
|
||||
closeState();
|
||||
// Cleared per app: the interval outlives the app that set it, so a ticking app
|
||||
// followed by one that never ticks would keep calling a nil global.
|
||||
tickIntervalMs = 0;
|
||||
// The launcher tap may still be down; swallow that gesture's release.
|
||||
lastTouched = true;
|
||||
ignoreRelease = true;
|
||||
@@ -109,29 +112,22 @@ bool LuaApp::load(const char* path) {
|
||||
return false;
|
||||
}
|
||||
if (!callGlobal(path)) return false; // run chunk body
|
||||
// Required, as on crosspoint-reader: an app whose entry point is misspelled would
|
||||
// otherwise start, draw nothing, and give no hint why.
|
||||
// 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;
|
||||
if (hasGlobal("on_tick")) {
|
||||
tickIntervalMs = DEFAULT_TICK_MS;
|
||||
lua_getglobal(state, "TICK_MS");
|
||||
if (lua_isinteger(state, -1)) {
|
||||
tickIntervalMs = std::max<uint32_t>(lua_tointeger(state, -1), DRAW_INTERVAL_MS);
|
||||
}
|
||||
lua_pop(state, 1);
|
||||
nextTickMs = millis() + tickIntervalMs;
|
||||
}
|
||||
return running();
|
||||
}
|
||||
|
||||
void LuaApp::setTickInterval(uint32_t intervalMs) {
|
||||
tickIntervalMs = intervalMs;
|
||||
nextTickMs = millis() + intervalMs;
|
||||
}
|
||||
|
||||
void LuaApp::registerBindings() {
|
||||
registerGui(state);
|
||||
registerSys(state);
|
||||
|
||||
+6
-1
@@ -28,6 +28,12 @@ class LuaApp {
|
||||
// can leave the message on screen long enough to read.
|
||||
bool takeFailure();
|
||||
|
||||
// Set from Lua by app.setTickInterval(); 0 stops the ticks.
|
||||
void setTickInterval(uint32_t intervalMs);
|
||||
|
||||
static constexpr uint32_t MIN_TICK_MS = 33; // no point ticking faster than a draw
|
||||
static constexpr uint32_t MAX_TICK_MS = 3600000; // an hour
|
||||
|
||||
// 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;
|
||||
@@ -57,7 +63,6 @@ class LuaApp {
|
||||
uint32_t nextDrawMs = 0;
|
||||
|
||||
static constexpr uint32_t DRAW_INTERVAL_MS = 33;
|
||||
static constexpr uint32_t DEFAULT_TICK_MS = 100; // used when on_tick exists without TICK_MS
|
||||
|
||||
void registerBindings();
|
||||
bool callGlobal(const char* name, int nargs = 0);
|
||||
|
||||
+8
-1
@@ -4,6 +4,13 @@
|
||||
-- Point your editor's Lua language server at this file to get completion for the
|
||||
-- firmware API inside sdcard/apps and sdcard/lib.
|
||||
|
||||
---@class applib
|
||||
app = {}
|
||||
|
||||
--- Sets how often on_tick() runs. Errors when on_tick is not defined.
|
||||
---@param intervalMs integer 0 stops ticking; anything else is clamped to 33..3600000.
|
||||
function app.setTickInterval(intervalMs) end
|
||||
|
||||
---@class fslib
|
||||
fs = {}
|
||||
|
||||
@@ -299,7 +306,7 @@ function wifi.forget() end
|
||||
-- Callbacks an app may define as globals:
|
||||
-- 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_tick() at the interval app.setTickInterval() asked for
|
||||
-- on_touch_down(x, y) finger down
|
||||
-- on_touch_up(x, y) finger up
|
||||
-- on_touch(x, y) tap, fired on release like a click
|
||||
|
||||
@@ -13,6 +13,7 @@ local device = {
|
||||
rotation = 0,
|
||||
theme = "light",
|
||||
clockSynced = false,
|
||||
tickInterval = 0,
|
||||
timezone = "UTC0",
|
||||
raw = nil, -- pending raw touch reading, {x, y} or nil
|
||||
networks = {}, -- what wifi.scan() returns
|
||||
@@ -99,7 +100,14 @@ function device.install()
|
||||
forget = function() device.connected = nil return saved() end,
|
||||
}
|
||||
|
||||
log = {info = function() end}
|
||||
app = {
|
||||
setTickInterval = function(ms)
|
||||
assert(ms == 0 or on_tick, "setTickInterval without on_tick")
|
||||
device.tickInterval = ms
|
||||
end,
|
||||
}
|
||||
|
||||
log = {debug = function() end, info = function() end, error = function() end}
|
||||
|
||||
return device
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user