diff --git a/.pi/skills/test-e32r40t-firmware/SKILL.md b/.pi/skills/test-e32r40t-firmware/SKILL.md index 441c749..1b72222 100644 --- a/.pi/skills/test-e32r40t-firmware/SKILL.md +++ b/.pi/skills/test-e32r40t-firmware/SKILL.md @@ -35,7 +35,7 @@ boot .pio/build/esp32-32e/firmware.bin --sdcard sdcard/ --fresh-sd wait-log "launcher ready" --timeout 120 tap 60 55 -wait-log "launching /apps/" +wait-log "running Hello" wait-log "\[lua\] .* started" capture _scratch/emulator-app.png @@ -58,8 +58,8 @@ printf '%s\n' 'boot .pio/build/esp32-32e/firmware.bin' 'wait-log "launcher ready Lua logs carry a level in the prefix (`[lua:info] home ready`), so match `\[lua:info\]` and not `\[lua\]`. -Firmware sync points, in order: `launching /apps//main.lua` (printed by the -runtime for every app, including the launcher itself), `[lua:info] home ready` (menu +Firmware sync points, in order: `running ` (printed by the host for every app, +including the launcher itself), `[lua:info] home ready` (menu drawn, touch accepted), then whatever the app logs through `log.info(...)`. Exiting an app relaunches the launcher, so both lines repeat. @@ -68,7 +68,7 @@ full-screen clear under TCG, and a 250 ms press that lands during it is missed entirely. Wait for the app's own ready log, then `sleep` a few seconds. - Use `wait-log REGEX` as the default synchronization and assertion mechanism. -- Assert on the `launching ...` line rather than on tap coordinates: menu rows follow SD directory order, which the image build decides. +- Assert on the `running ...` line rather than on tap coordinates: menu rows follow SD directory order, which the image build decides. - Use `wait-frame [COUNT]` only when a panel redraw is itself the behavior under test. - Use `wait-idle SECONDS` only when silence is the actual readiness signal. - Capture and inspect the final screen after semantic log assertions. @@ -77,7 +77,7 @@ entirely. Wait for the app's own ready log, then `sleep` a few seconds. `tap` always takes physical panel pixels (320x480 portrait), because the glass never rotates. -Screens are laid out by `/lib/ui.lua`, so read tap targets off a `capture` rather than +Screens are laid out by `/.lua/lib/ui.lua`, so read tap targets off a `capture` rather than computing them. The status bar takes the top 44 rows; card grids use three portrait columns and four landscape columns. With the current apps, portrait Home card centres are near `(109,102)` and `(210,102)`, while Settings' first row is near x=59/160/261 at y=102. @@ -121,7 +121,9 @@ esp-emu --board e32r40t boot .pio/build/esp32-32e/firmware.bin --sdcard sdcard/ esp-emu --board e32r40t boot .pio/build/esp32-32e/firmware.bin --sdcard card.img # use image directly ``` -Apps are `/apps//main.lua`; settings persist in `/settings.lua`. Directory contents overwrite matching files in the persistent state image and other image contents remain, so use `--fresh-sd` when a test needs default settings or a known app list. +Apps are `/.lua/apps//main.lua` with data under `/.lua/data/`; settings persist in +`/settings.lua`. Run `make sdcard` first: the shared modules in `/.lua/lib` are copied from +the submodule and are not committed here. Directory contents overwrite matching files in the persistent state image and other image contents remain, so use `--fresh-sd` when a test needs default settings or a known app list. ## Lua-only Logic diff --git a/AGENTS.md b/AGENTS.md index 04a0239..2aecc81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,18 +1,22 @@ # slate32 Agent Guidelines -ESP32 firmware (C++/Arduino) hosting Lua apps off an SD card. C++ owns the panel, touch, -network and persistence; everything user-visible is Lua under `sdcard/`. +ESP32 firmware (C++/Arduino) hosting Lua apps off an SD card. The Lua platform -- bindings, +the widget tree, app loading and navigation -- is shared with crosspoint-reader and lives in +`lib/esp32-lua-api`. This repository owns the panel, touch, network, persistence and chrome; +everything user-visible is Lua under `sdcard/.lua/`. ## Build and Test ```sh -nix develop -c make test # C++ unit tests, Lua tests, and the stub check +nix develop -c make test # gfx tests, this firmware's Lua tests, then the submodule's suite nix develop -c pio run # firmware -> .pio/build/esp32-32e/firmware.bin -nix develop -c make stubs # regenerate stubs/slate32.lua after any binding change +nix develop -c make sdcard # copy shared Lua modules onto the card before flash or emulator ``` -`make test` fails if `stubs/slate32.lua` drifts from the bindings. Regenerate rather than -hand-editing it. +`src/host` implements the interfaces in `lib/esp32-lua-api/native/include/lua/providers.h` +and nothing else. A binding, a layout rule or an API declaration changes **in the submodule**, +not here; `lua/api/**` there is generated from the C++ that registers it, so run its +`make api` rather than editing a declaration. ## Binding Conventions @@ -39,7 +43,7 @@ bindings so there is one writer. ## The Widget Tree -The tree lives in `src/ui/layout.h`, not in Lua. A node is a **16 byte struct** in a flat +The tree lives in `lib/esp32-lua-api/native/include/lua/layout.h`, not in Lua. A node is a **16 byte struct** in a flat arena; the same tree as Lua tables cost roughly forty times that, and a long list could not coexist with WiFi's buffers. `sdcard/lib/ui.lua` is a wrapper: `ui.button{...}` returns an integer handle, so a node carries nothing an app puts on it. Anything an app used to hang on @@ -118,8 +122,8 @@ invalidation flags on the C++ side; the facts that drive the bar are only knowab Full instructions in `.pi/skills/test-e32r40t-firmware/SKILL.md`. Two things that cost time: -- The launcher logs `[lua] home ready`, not "launcher ready". It is `/apps/Home`, an app like - any other. +- The host logs `[lua] running ` for every app; the launcher then logs `[lua] info: home + ready`. It is `/.lua/apps/Home`, an app like any other. - `wait-frame` is an e-ink command and hangs on this board, and `sleep` is not a command at all -- a script using either stops silently. Use `wait-idle SECONDS` between captures and `wait-log` for everything semantic. diff --git a/src/host/lua_host.cpp b/src/host/lua_host.cpp index ec9677d..32fe771 100644 --- a/src/host/lua_host.cpp +++ b/src/host/lua_host.cpp @@ -103,33 +103,34 @@ void LuaHost::setFullscreen(bool on) { nextBarMs = 0; // leaving fullscreen left the bar's rows painted by the app } -bool LuaHost::begin() { return startApp("Home", ""); } +bool LuaHost::begin() { + prepareForApp(); + if (!runtime.startApp("Home")) return false; + settleNewApp(); + return true; +} -bool LuaHost::startApp(const std::string& path, const std::string& arg) { +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 last app's, which is the same module, and loadStatusBar() corrects it if that changes. + // is the last app's, which is the same module, and settleNewApp() corrects it if that changes. applyViewport(); - - Serial.printf("[lua] launching %s free=%u largest=%u\n", path.c_str(), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - lastTouched = true; // the tap that launched this app may still be down - ignoreRelease = true; // and its release is not this app's gesture + 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()); +} - if (!runtime.startApp(path, arg)) { - fail(("could not start " + path).c_str()); - return false; - } +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; - return true; + 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. @@ -232,17 +233,12 @@ void LuaHost::pollTimers() { } void LuaHost::navigate() { + prepareForApp(); if (!runtime.applyPendingNavigation()) { fail("app failed to start"); return; } - hasBack = runtime.canGoBack(); - loadStatusBar(); - applyViewport(); - if (barInset() > 0 && !barBroken) drawStatusBar(); - const uint32_t now = millis(); - nextDrawMs = now; - lastDrawMs = now; + settleNewApp(); } void LuaHost::loop() { diff --git a/src/host/lua_host.h b/src/host/lua_host.h index 0e7550b..90a9191 100644 --- a/src/host/lua_host.h +++ b/src/host/lua_host.h @@ -41,7 +41,10 @@ class LuaHost { } esp32lua::Providers wire(); - bool startApp(const std::string& path, const std::string& arg); + // Every app starts the same way, whether it is the launcher at boot or a route the + // running app asked for: prepare the panel, load, then re-clip around the new bar. + void prepareForApp(); + void settleNewApp(); void navigate(); void pollTouch(); void pollTimers();