021b94fca2
sys.launch(path, arg) carries a string to the next app's init(arg), nil when there is none. States share no memory, so one string is the whole handoff; anything structured travels as a Lua literal the receiver loads. This is what a screen split across apps needs to say "collect a password for this network". sys.setAppName() retitles the status bar, defaulting to the directory name as before. A setter rather than a declared constant, so one app can retitle per screen. The bar needs no new invalidation path for it -- the name joins rotation and theme in the cache key -- and clips a name wide enough to reach the memory slot.
310 lines
9.4 KiB
C++
310 lines
9.4 KiB
C++
// App lifecycle: own the lua_State, dispatch the callbacks, and tear down safely.
|
|
// The bindings themselves live in bindings/, the loader in module_loader.cpp.
|
|
|
|
#include "lua_app.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "../settings.h"
|
|
#include "bindings.h"
|
|
|
|
extern "C" {
|
|
#include <lauxlib.h>
|
|
#include <lualib.h>
|
|
}
|
|
|
|
// LUA_EXTRASPACE is a pointer-sized block ahead of every lua_State, and Lua copies it
|
|
// into new coroutines, so the owner is reachable from any state without a global.
|
|
LuaApp* app(lua_State* L) { return *static_cast<LuaApp**>(lua_getextraspace(L)); }
|
|
|
|
void bindApp(lua_State* L, LuaApp* owner) { *static_cast<LuaApp**>(lua_getextraspace(L)) = owner; }
|
|
|
|
void LuaApp::mapTouch(const TS_Point& p, int16_t& x, int16_t& y) const {
|
|
int16_t nx = constrain(map(p.y, settings.touchX0, settings.touchX1, 0, PANEL_W), 0, PANEL_W - 1);
|
|
int16_t ny = constrain(map(p.x, settings.touchY0, settings.touchY1, 0, PANEL_H), 0, PANEL_H - 1);
|
|
switch (tft.getRotation() & 3) {
|
|
case 1:
|
|
x = ny;
|
|
y = PANEL_W - 1 - nx;
|
|
break;
|
|
case 2:
|
|
x = PANEL_W - 1 - nx;
|
|
y = PANEL_H - 1 - ny;
|
|
break;
|
|
case 3:
|
|
x = PANEL_H - 1 - ny;
|
|
y = nx;
|
|
break;
|
|
default:
|
|
x = nx;
|
|
y = ny;
|
|
break;
|
|
}
|
|
// App space starts below the bar, matching the viewport apps draw into. A tap on the
|
|
// bar itself lands at a negative y, which loop() drops.
|
|
y -= barInset();
|
|
}
|
|
|
|
// resetViewport() first: width()/height() report the *viewport* once one is set, so
|
|
// re-applying over an existing one would shrink the app area again every time.
|
|
// setRotation() leaves the old viewport metrics behind, so every rotation needs this too.
|
|
void LuaApp::applyViewport() {
|
|
int16_t inset = barInset();
|
|
tft.resetViewport();
|
|
if (inset > 0) tft.setViewport(0, inset, tft.width(), tft.height() - inset, true);
|
|
}
|
|
|
|
void LuaApp::setFullscreen(bool on) {
|
|
fullscreen = on;
|
|
applyViewport();
|
|
nextBarMs = 0; // leaving fullscreen left the bar's rows painted by the app
|
|
}
|
|
|
|
LuaApp::LuaApp(TFT_eSPI& tft, XPT2046_Touchscreen& touch) : tft(tft), touch(touch) {}
|
|
|
|
LuaApp::~LuaApp() {
|
|
if (state) lua_close(state);
|
|
}
|
|
|
|
bool LuaApp::hasGlobal(const char* name) {
|
|
lua_getglobal(state, name);
|
|
bool found = lua_isfunction(state, -1);
|
|
lua_pop(state, 1);
|
|
return found;
|
|
}
|
|
|
|
bool LuaApp::callGlobal(const char* name, int nargs) {
|
|
if (lua_pcall(state, nargs, 0, 0) != LUA_OK) {
|
|
fail(lua_tostring(state, -1));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void LuaApp::fireTouch(const char* name, int16_t x, int16_t y) {
|
|
if (!hasGlobal(name)) return;
|
|
lua_getglobal(state, name);
|
|
lua_pushinteger(state, x);
|
|
lua_pushinteger(state, y);
|
|
callGlobal(name, 2);
|
|
}
|
|
|
|
// Deliberately not themed: this can fire when the settings naming the theme are
|
|
// themselves unreadable, so it stays high contrast.
|
|
void LuaApp::fail(const char* message) {
|
|
Serial.printf("[lua] error: %s\n", message ? message : "(unknown)");
|
|
tft.fillScreen(TFT_WHITE);
|
|
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, bool isHome, const char* arg) {
|
|
closeState();
|
|
this->isHome = isHome;
|
|
homeArmed = false;
|
|
// 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 tap that launched this app may still be down; swallow that gesture's release.
|
|
lastTouched = true;
|
|
ignoreRelease = true;
|
|
state = luaL_newstate();
|
|
if (!state) return false;
|
|
bindApp(state, this);
|
|
luaL_openlibs(state);
|
|
registerBindings();
|
|
|
|
String appDir = path;
|
|
int slash = appDir.lastIndexOf('/');
|
|
appDir = slash > 0 ? appDir.substring(0, slash) : String("/");
|
|
installLoader(appDir.c_str());
|
|
appName = appDir.substring(appDir.lastIndexOf('/') + 1);
|
|
|
|
// Before the chunk runs: the bar's height sets the viewport, and an app measuring
|
|
// gui.height() at the top of init() has to see the area it actually owns.
|
|
loadStatusBar();
|
|
applyViewport();
|
|
// Painted before the app's chunk runs, not on the next bar tick: loading a script off
|
|
// the SD card takes long enough that the previous app's name would linger visibly.
|
|
if (barInset() > 0 && !barBroken) drawStatusBar();
|
|
|
|
if (loadScript(state, path) != LUA_OK) {
|
|
fail(lua_tostring(state, -1));
|
|
return false;
|
|
}
|
|
if (!callGlobal(path)) return false; // run chunk body
|
|
// 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 (arg) {
|
|
lua_pushstring(state, arg);
|
|
} else {
|
|
lua_pushnil(state); // pushed either way, so init(arg) reads the same in both cases
|
|
}
|
|
if (!callGlobal("init", 1)) return false;
|
|
return running();
|
|
}
|
|
|
|
// Reads an optional positive integer field off the bar module on the stack top.
|
|
static lua_Integer barField(lua_State* L, const char* key, lua_Integer fallback) {
|
|
lua_getfield(L, -1, key);
|
|
lua_Integer value = lua_isinteger(L, -1) ? lua_tointeger(L, -1) : fallback;
|
|
lua_pop(L, 1);
|
|
return value > 0 ? value : fallback;
|
|
}
|
|
|
|
// The bar is a Lua module like any other, loaded per app because the state is too. Its
|
|
// own table owns the geometry and the repaint rate: sys.setTickInterval() belongs to the
|
|
// app, and there is only one of those per state.
|
|
void LuaApp::loadStatusBar() {
|
|
barBroken = false;
|
|
barHeight = DEFAULT_BAR_H;
|
|
barIntervalMs = BAR_INTERVAL_MS;
|
|
lua_getglobal(state, "require");
|
|
lua_pushstring(state, "statusbar");
|
|
if (lua_pcall(state, 1, 1, 0) != LUA_OK || !lua_istable(state, -1)) {
|
|
Serial.printf("[statusbar] unavailable: %s\n", luaL_tolstring(state, -1, nullptr));
|
|
// Nothing has measured the panel yet, so surrendering the strip here is free. A bar
|
|
// that dies later keeps its rows, because the app has already laid itself out.
|
|
barBroken = true;
|
|
barHeight = 0;
|
|
lua_pop(state, 2);
|
|
return;
|
|
}
|
|
barHeight = barField(state, "height", DEFAULT_BAR_H);
|
|
barIntervalMs = std::max((lua_Integer)MIN_TICK_MS, barField(state, "interval", BAR_INTERVAL_MS));
|
|
lua_setglobal(state, "__statusbar");
|
|
nextBarMs = 0;
|
|
}
|
|
|
|
// Errors here are logged and disable the bar rather than killing the app: chrome that
|
|
// fails should not take the running program with it.
|
|
void LuaApp::drawStatusBar() {
|
|
lua_getglobal(state, "__statusbar");
|
|
lua_getfield(state, -1, "draw");
|
|
lua_remove(state, -2);
|
|
lua_pushboolean(state, !isHome); // whether to paint the home button
|
|
tft.resetViewport(); // the bar paints in panel coordinates, the app does not
|
|
if (lua_pcall(state, 1, 0, 0) != LUA_OK) {
|
|
Serial.printf("[statusbar] %s\n", luaL_tolstring(state, -1, nullptr));
|
|
lua_pop(state, 2);
|
|
barBroken = true;
|
|
}
|
|
applyViewport();
|
|
}
|
|
|
|
void LuaApp::setTickInterval(uint32_t intervalMs) {
|
|
tickIntervalMs = intervalMs;
|
|
nextTickMs = millis() + intervalMs;
|
|
}
|
|
|
|
void LuaApp::registerBindings() {
|
|
registerGui(state);
|
|
registerSys(state);
|
|
registerSettings(state);
|
|
registerInput(state);
|
|
registerFs(state);
|
|
registerWifi(state);
|
|
registerHttp(state);
|
|
}
|
|
|
|
// Deferred: sys.exit() and fail() run inside a lua_pcall, so closing the state
|
|
// here would free the VM that is still executing.
|
|
void LuaApp::requestExit() { exitRequested = true; }
|
|
|
|
void LuaApp::requestLaunch(const char* path, const char* arg) {
|
|
pendingLaunch = path;
|
|
pendingArg = arg ? arg : "";
|
|
pendingArgSet = arg != nullptr;
|
|
}
|
|
|
|
String LuaApp::takePendingLaunch() {
|
|
String path = pendingLaunch;
|
|
pendingLaunch = "";
|
|
return path;
|
|
}
|
|
|
|
String LuaApp::takePendingArg() {
|
|
String arg = pendingArg;
|
|
pendingArg = "";
|
|
pendingArgSet = false;
|
|
return arg;
|
|
}
|
|
|
|
void LuaApp::closeState() {
|
|
if (state) {
|
|
lua_close(state);
|
|
state = nullptr;
|
|
}
|
|
exitRequested = false;
|
|
}
|
|
|
|
void LuaApp::loop() {
|
|
if (exitRequested) closeState();
|
|
if (!state) return;
|
|
uint32_t now = millis();
|
|
|
|
bool touched = touch.touched();
|
|
if (touched) {
|
|
TS_Point p = touch.getPoint();
|
|
mapTouch(p, lastX, lastY);
|
|
}
|
|
|
|
// The bar is the host's: apps never see a touch in it, and its one control acts on
|
|
// release, so a press that slides off into the app cancels like any other button.
|
|
if (touched && lastY < 0) {
|
|
homeArmed = homeArmed || inHomeButton();
|
|
touched = false;
|
|
} else if (homeArmed) {
|
|
homeArmed = false;
|
|
if (lastY < 0) {
|
|
requestExit();
|
|
return;
|
|
}
|
|
}
|
|
if (touched && !lastTouched) {
|
|
fireTouch("on_touch_down", lastX, lastY);
|
|
if (!running()) return;
|
|
} else if (!touched && lastTouched && ignoreRelease) {
|
|
ignoreRelease = false; // the press that launched this app is not its own gesture
|
|
} else if (!touched && lastTouched) {
|
|
fireTouch("on_touch_up", lastX, lastY);
|
|
if (!running()) return;
|
|
fireTouch("on_touch", lastX, lastY); // tap alias, fired on release like a click
|
|
if (!running()) return;
|
|
}
|
|
lastTouched = touched;
|
|
|
|
if (tickIntervalMs > 0 && now >= nextTickMs) {
|
|
nextTickMs = now + tickIntervalMs;
|
|
lua_getglobal(state, "on_tick");
|
|
callGlobal("on_tick");
|
|
if (!running()) return;
|
|
}
|
|
|
|
if (now >= nextDrawMs && hasGlobal("draw")) {
|
|
nextDrawMs = now + DRAW_INTERVAL_MS;
|
|
lua_getglobal(state, "draw");
|
|
callGlobal("draw");
|
|
if (!running()) return;
|
|
}
|
|
|
|
if (barInset() > 0 && !barBroken && now >= nextBarMs) {
|
|
nextBarMs = now + barIntervalMs;
|
|
drawStatusBar();
|
|
}
|
|
}
|