refactor: run apps on the shared esp32-lua-api runtime
Replaces the firmware's own lua_State, bindings, module loader, node tree and navigation history with lib/esp32-lua-api. What is left in src/host is the hardware behind the provider interfaces plus the chrome the firmware owns: the viewport, the status bar and touch polling. src/lua became src/host because src is on the include path, so a directory named lua shadowed the library's <lua/providers.h> and #pragma once then silently skipped it.
This commit is contained in:
+14
-93
@@ -1,92 +1,44 @@
|
||||
// Board bring-up and the loop. Everything about running a Lua app -- the state, the
|
||||
// bindings, app loading, navigation history -- lives in the shared runtime behind LuaHost.
|
||||
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <TFT_eSPI.h>
|
||||
#include <WiFi.h>
|
||||
#include <XPT2046_Touchscreen.h>
|
||||
|
||||
#include "lua/lua_app.h"
|
||||
#include "host/lua_host.h"
|
||||
#include "net.h"
|
||||
#include "settings.h"
|
||||
|
||||
// SD card is on a separate bus from the display/touch (board schematic)
|
||||
static constexpr int SD_CS = 5;
|
||||
static constexpr int SD_SCK = 18;
|
||||
static constexpr int SD_MOSI = 23;
|
||||
static constexpr int SD_MISO = 19;
|
||||
static constexpr int TOUCH_CS = 33;
|
||||
|
||||
static const char* HOME = "/apps/Home/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
|
||||
// calls runs on this task, so the headroom belongs here rather than in each caller.
|
||||
SET_LOOP_TASK_STACK_SIZE(16 * 1024);
|
||||
|
||||
TFT_eSPI tft;
|
||||
SPIClass touchSpi(HSPI);
|
||||
SPIClass& sdSpi = SPI;
|
||||
XPT2046_Touchscreen touch(TOUCH_CS);
|
||||
LuaApp app(tft, touch);
|
||||
LuaHost host(tft, touch);
|
||||
|
||||
struct Route {
|
||||
String path;
|
||||
String arg;
|
||||
bool hasArg = false;
|
||||
};
|
||||
static bool halted = false;
|
||||
|
||||
static constexpr size_t MAX_HISTORY = 8;
|
||||
bool halted = false;
|
||||
Route currentRoute;
|
||||
Route pendingRoute;
|
||||
Route history[MAX_HISTORY];
|
||||
size_t historyDepth = 0;
|
||||
LuaApp::ExitAction pendingAction = LuaApp::ExitAction::None;
|
||||
|
||||
Route homeRoute() {
|
||||
Route route;
|
||||
route.path = HOME;
|
||||
return route;
|
||||
}
|
||||
|
||||
void pushHistory(const Route& route) {
|
||||
if (historyDepth == MAX_HISTORY) {
|
||||
// Home remains the eventual floor; discard the oldest intermediate route.
|
||||
for (size_t i = 1; i < MAX_HISTORY - 1; ++i) history[i] = history[i + 1];
|
||||
historyDepth--;
|
||||
}
|
||||
history[historyDepth++] = route;
|
||||
}
|
||||
|
||||
Route previousRoute() {
|
||||
return historyDepth > 0 ? history[--historyDepth] : homeRoute();
|
||||
}
|
||||
|
||||
// Last resort only: the UI lives in Lua, so this exists purely to explain why
|
||||
// nothing else could run.
|
||||
void fallbackScreen(const char* message) {
|
||||
static void fallbackScreen(const char* message) {
|
||||
tft.resetViewport(); // no app, no status bar: this message owns the panel
|
||||
tft.setRotation(0);
|
||||
tft.fillScreen(TFT_WHITE);
|
||||
tft.setTextSize(2);
|
||||
tft.setTextColor(TFT_RED, TFT_WHITE);
|
||||
tft.drawString(message, 10, 10);
|
||||
tft.setTextColor(TFT_BLACK, TFT_WHITE);
|
||||
tft.drawString("expected /apps/Home/main.lua", 10, 30);
|
||||
tft.drawString("expected /.lua/apps/Home/main.lua", 10, 34);
|
||||
Serial.printf("halted: %s\n", message);
|
||||
halted = true;
|
||||
}
|
||||
|
||||
void startApp(const Route& route) {
|
||||
// Full panel until the app's own status bar module reports its height in load().
|
||||
tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame
|
||||
tft.resetViewport(); // load() re-clips once the new app's bar reports its height
|
||||
Serial.printf("launching %s free=%u largest=%u\n", route.path.c_str(), ESP.getFreeHeap(),
|
||||
ESP.getMaxAllocHeap());
|
||||
const char* arg = route.hasArg ? route.arg.c_str() : nullptr;
|
||||
if (app.load(route.path.c_str(), historyDepth > 0, arg)) return;
|
||||
if (route.path == HOME && historyDepth == 0) fallbackScreen("home failed to start");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
tft.begin();
|
||||
@@ -100,51 +52,20 @@ void setup() {
|
||||
fallbackScreen("SD card mount failed");
|
||||
return;
|
||||
}
|
||||
|
||||
settings.load(); // absent file keeps the built-in defaults
|
||||
net::begin();
|
||||
currentRoute = homeRoute();
|
||||
startApp(currentRoute);
|
||||
if (!host.begin()) fallbackScreen("home failed to start");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Nothing here changes without a reset, so idle hard rather than spinning on it.
|
||||
if (halted) {
|
||||
delay(100);
|
||||
return;
|
||||
}
|
||||
net::loop();
|
||||
|
||||
if (app.running()) {
|
||||
app.loop();
|
||||
if (!app.running()) {
|
||||
pendingAction = app.takeExitAction();
|
||||
pendingRoute.path = app.takePendingLaunch();
|
||||
pendingRoute.hasArg = app.hasPendingArg();
|
||||
pendingRoute.arg = app.takePendingArg();
|
||||
}
|
||||
} else {
|
||||
// A failed child returns to its caller after leaving the message readable. Home has
|
||||
// no caller, so its failure still reaches the last-resort screen in startApp().
|
||||
if (app.takeFailure()) {
|
||||
delay(ERROR_HOLD_MS);
|
||||
if (pendingAction == LuaApp::ExitAction::None) pendingAction = LuaApp::ExitAction::Back;
|
||||
}
|
||||
|
||||
if (pendingAction == LuaApp::ExitAction::Launch) {
|
||||
pushHistory(currentRoute);
|
||||
currentRoute = pendingRoute;
|
||||
} else if (pendingAction == LuaApp::ExitAction::Replace) {
|
||||
currentRoute = pendingRoute;
|
||||
} else {
|
||||
currentRoute = previousRoute();
|
||||
}
|
||||
pendingAction = LuaApp::ExitAction::None;
|
||||
pendingRoute = {};
|
||||
startApp(currentRoute);
|
||||
}
|
||||
|
||||
// Hands the core to the idle task instead of spinning between polls: touch reads are
|
||||
// throttled to 3 ms and draws to 33 ms, so a millisecond of sleep costs nothing that
|
||||
// can be seen or felt.
|
||||
host.loop();
|
||||
// The launcher failing to start is the one error no app can recover from.
|
||||
if (!host.running()) fallbackScreen("home failed to start");
|
||||
delay(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user