#include "lua_host.h" #include #include "../settings.h" extern "C" { #include #include } namespace { esp32lua::Paths slatePaths() { esp32lua::Paths paths; paths.apps = "/.lua/apps"; paths.data = "/.lua/data"; paths.lib = "/.lua/lib"; paths.home = "Home"; return paths; } // Reads an optional positive integer field off the bar module on the stack top. lua_Integer barField(lua_State *state, const char *key, lua_Integer fallback) { lua_getfield(state, -1, key); const lua_Integer value = lua_isinteger(state, -1) ? lua_tointeger(state, -1) : fallback; lua_pop(state, 1); return value > 0 ? value : fallback; } } // namespace // Called from the initializer list once every provider member exists, which // declaration order guarantees: the runtime holds pointers to them for its // whole life. esp32lua::Providers LuaHost::wire() { esp32lua::Providers providers; providers.log = &logProvider; providers.settings = &settingsProvider; providers.sys = &sysProvider; providers.fs = &fsProvider; providers.gui = &guiProvider; providers.http = &httpProvider; providers.timer = &timerProvider; providers.wifi = &wifiProvider; providers.ble = &bleProvider; providers.touch = &touchProvider; // No buttons on this board, so sys.hasFeature("buttons") is false and input // gains nothing. return providers; } LuaHost::LuaHost(TFT_eSPI &tft, XPT2046_Touchscreen &touch) : tft(tft), touchPanel(touch), settingsProvider(*this), guiProvider(tft, *this), touchProvider(touch, *this), runtime(wire(), slatePaths()) {} void LuaHost::mapTouch(const TS_Point &point, int16_t &x, int16_t &y) const { const int16_t nx = constrain(map(point.y, settings.touchX0, settings.touchX1, 0, PANEL_W), 0, PANEL_W - 1); const int16_t ny = constrain(map(point.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 pollTouch() 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 LuaHost::applyViewport() { const int16_t inset = barInset(); tft.resetViewport(); if (inset > 0) tft.setViewport(0, inset, tft.width(), tft.height() - inset, true); } void LuaHost::applyRotation() { tft.setRotation(settings.rotationIndex()); applyViewport(); refreshStatusBar(); // every slot in the bar just moved } void LuaHost::setFullscreen(bool on) { fullscreen = on; applyViewport(); nextBarMs = 0; // leaving fullscreen left the bar's rows painted by the app } bool LuaHost::begin() { prepareForApp(); if (!runtime.startApp("Home")) return false; settleNewApp(); return true; } void LuaHost::prepareForApp() { tft.setRotation( settings.rotationIndex()); // the previous app may have rotated the frame fullscreen = false; // Applied before the app loads, because init() measures the panel it was // given. The height is the last app's, which is the same module, and // settleNewApp() corrects it if that changes. applyViewport(); lastTouched = true; // the tap that launched this app may still be down ignoreRelease = true; // and its release is not this app's gesture backArmed = false; Serial.printf("[lua] launching free=%u largest=%u\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); } void LuaHost::settleNewApp() { hasBack = runtime.canGoBack(); // the runtime keeps the history; the bar only // offers the control loadStatusBar(); applyViewport(); if (barInset() > 0 && !barBroken) drawStatusBar(); const uint32_t now = millis(); nextDrawMs = now; lastDrawMs = now; Serial.printf("[lua] running %s\n", runtime.appPath().c_str()); } // The bar is a Lua module like any other, loaded per app because the state is // too. void LuaHost::loadStatusBar() { lua_State *state = runtime.state(); barBroken = false; 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. barBroken = true; barHeight = 0; lua_pop(state, 2); return; } barHeight = barField(state, "height", DEFAULT_BAR_H); barIntervalMs = std::max( DRAW_INTERVAL_MS, barField(state, "interval", BAR_INTERVAL_MS)); lua_setglobal(state, "__statusbar"); nextBarMs = 0; } // Errors here disable the bar rather than killing the app: chrome that fails // should not take the running program with it. void LuaHost::drawStatusBar() { lua_State *state = runtime.state(); lua_getglobal(state, "__statusbar"); lua_getfield(state, -1, "draw"); lua_remove(state, -2); lua_pushboolean(state, hasBack); 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(); } // Deliberately not themed: this can fire when the settings naming the theme are // themselves unreadable, so it stays high contrast. void LuaHost::fail(const char *message) { Serial.printf("[lua] %s\n", message ? message : "(unknown)"); tft.resetViewport(); tft.fillScreen(TFT_WHITE); tft.setTextSize(2); tft.setTextColor(TFT_RED, TFT_WHITE); tft.drawString("Lua error:", 8, 8); tft.setTextColor(TFT_BLACK, TFT_WHITE); tft.drawString(message ? message : "(unknown)", 8, 28); } void LuaHost::pollTouch() { bool touched = touchPanel.touched(); if (touched) mapTouch(touchPanel.getPoint(), 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) { backArmed = backArmed || inBackButton(); touched = false; } else if (backArmed) { backArmed = false; if (inBackButton()) { runtime.requestBack(); return; } } if (touched && !lastTouched) { movedX = lastX; movedY = lastY; runtime.callTouch(esp32lua::TouchPhase::Down, lastX, lastY); } else if (touched && lastTouched) { // The XPT2046 jitters a pixel or two under a still finger. Below this every // poll would fire a move, and a painting app would draw a blur where // nothing moved. if (abs(lastX - movedX) + abs(lastY - movedY) >= MOVE_EPSILON_PX) { movedX = lastX; movedY = lastY; runtime.callTouch(esp32lua::TouchPhase::Move, lastX, lastY); } } else if (!touched && lastTouched && ignoreRelease) { ignoreRelease = false; } else if (!touched && lastTouched) { // The runtime fires the on_touch tap alias after the release. runtime.callTouch(esp32lua::TouchPhase::Up, lastX, lastY); } lastTouched = touched; } void LuaHost::pollTimers() { std::vector due; timerProvider.collectDue(millis(), due); for (size_t at = 0; at < due.size(); at++) { runtime.callTimer(due[at]); if (runtime.hasPendingNavigation()) return; // the app is leaving; its other timers can wait } } void LuaHost::navigate() { prepareForApp(); if (!runtime.applyPendingNavigation()) { fail("app failed to start"); return; } settleNewApp(); } void LuaHost::loop() { if (!runtime.hasApp()) return; pollTouch(); if (!runtime.hasPendingNavigation()) pollTimers(); const uint32_t now = millis(); if (!runtime.hasPendingNavigation() && now >= nextDrawMs) { nextDrawMs = now + DRAW_INTERVAL_MS; runtime.callDraw(static_cast(now - lastDrawMs)); lastDrawMs = now; } if (!runtime.hasPendingNavigation() && barInset() > 0 && !barBroken && now >= nextBarMs) { nextBarMs = now + barIntervalMs; drawStatusBar(); } // Between batches, never inside one: swapping the lua_State mid-callback // would free the VM that is still executing. if (runtime.hasPendingNavigation()) navigate(); }