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:
2026-08-01 17:58:11 -04:00
parent d338dfe3e8
commit 8045faddb4
13 changed files with 83 additions and 45 deletions
+9 -12
View File
@@ -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);
+31 -3
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);