diff --git a/README.md b/README.md index cd34bc1..4fca358 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,15 @@ docs/ lua-api-parity.md, the crosspoint-reader comparison ``` Copy `sdcard/` to the SD card root: apps live in `/apps//main.lua` and shared -Lua modules in `/lib`. The launcher is itself an app (`/apps/launcher/main.lua`); the -firmware only draws a fallback screen if it cannot start. +Lua modules in `/lib`. Home is itself an app (`/apps/home/main.lua`); the firmware only +draws a fallback screen if it cannot start. ## Lua API Apps define `init()`, which is required, plus optional `draw()` (~30fps cap), `on_touch_down(x, y)`, `on_touch_up(x, y)`, `on_touch(x, y)` (tap alias, fired on release), and `on_tick()` -(enabled by `app.setTickInterval(ms)`). Call `sys.exit()` to return to the launcher. +(enabled by `sys.setTickInterval(ms)`). Call `sys.exit()` to return home, which the back +button in the status bar does too. `require` reads from the SD card: `/apps//?.lua` first, then `/lib/?.lua`. @@ -85,7 +86,7 @@ card like any other device containing a saved password. Rotation never needs a recalibration: calibration is stored in the panel's rotation-0 frame (320x480 raw ADC space) and the current rotation is applied afterwards, so `sys.setRotation()` is safe at any time. `gui.setRotation(0-3)` -changes only the current frame; the launcher restores the saved rotation when an +changes only the current frame; the firmware restores the saved rotation when an app exits. The glass itself is always portrait, so a rotated UI is drawn sideways on it. diff --git a/docs/lua-api-parity.md b/docs/lua-api-parity.md index f140806..91ae927 100644 --- a/docs/lua-api-parity.md +++ b/docs/lua-api-parity.md @@ -34,7 +34,7 @@ strength a status screen wants. Its `state` vocabulary is a subset: crosspoint r | Themes | none | `sys.getTheme/setTheme`, `/lib/theme.lua` | Colour panel. | | Rotation | `gui.setOrientation("portrait")` | `sys.setRotation(degrees)` persisted, `gui.setRotation(0-3)` for one frame | This device stores rotation in settings and remaps touch to match. | | Clock | nothing exposed; UTC offset is a C++ setting | `sys.clockSynced`, `sys.getTimezone/setTimezone` with POSIX TZ rules | Timezone here is a stored rule, so `os.date()` returns local time with DST handled by libc. | -| Launching | launcher is C++ | `sys.launch(path)`, launcher is a Lua app | The launcher is just another app here. | +| Launching | launcher is C++ | `sys.launch(path)`, home is a Lua app | The launcher is just another app here, named `home`. | | Modules | single-file apps; `require` unusable | `require` works, `package.searchers` reads the SD card, `/lib` on the path | Shared code such as `ui.lua` needs it. **crosspoint should adopt this.** | | BLE | `ble.*` | none | No BLE use case here yet. | | TLS memory | `TlsScratchLoan` lends the framebuffer to wolfSSL | none | crosspoint is heap-starved; this device has ~280KB free. | diff --git a/sdcard/apps/launcher/main.lua b/sdcard/apps/home/main.lua similarity index 78% rename from sdcard/apps/launcher/main.lua rename to sdcard/apps/home/main.lua index 9559b68..d3c47cd 100644 --- a/sdcard/apps/launcher/main.lua +++ b/sdcard/apps/home/main.lua @@ -17,7 +17,7 @@ end function init() local names = {} for _, name in ipairs(fs.listDirs("/apps")) do - if name ~= "launcher" then names[#names + 1] = name end + if name ~= "home" then names[#names + 1] = name end end table.sort(names) @@ -35,15 +35,18 @@ function init() row[#row + 1] = card(name, side) end - local items = {pad = PAD, gap = GAP} + -- Centred left to right as a unit, still top aligned: the gaps inside the grid are + -- fixed, so the leftover width belongs beside the block, not to its last column. + local items = {pad = PAD, gap = GAP, w = "fill", align = "center"} for _, row in ipairs(rows) do row.row, row.gap = true, GAP + row.w = #row * side + (#row - 1) * GAP items[#items + 1] = ui.box(row) end if #rows == 0 then items[#items + 1] = ui.text("no apps in /apps", {color = ui.theme.muted}) end screen = ui.screen(ui.box(items)) - log.info("launcher ready") + log.info("home ready") end function draw() screen:draw() end diff --git a/sdcard/apps/settings/main.lua b/sdcard/apps/settings/main.lua index 6434748..df73ccd 100644 --- a/sdcard/apps/settings/main.lua +++ b/sdcard/apps/settings/main.lua @@ -141,10 +141,12 @@ function buildMenu() card(side, "exit", nil, sys.exit), } - local items = {pad = MENU_PAD, gap = MENU_GAP} + -- Centred left to right as a unit, like the home grid, and still top aligned. + local items = {pad = MENU_PAD, gap = MENU_GAP, w = "fill", align = "center"} for index = 1, #cards, cols do local row = {row = true, gap = MENU_GAP} for column = index, math.min(index + cols - 1, #cards) do row[#row + 1] = cards[column] end + row.w = #row * side + (#row - 1) * MENU_GAP items[#items + 1] = ui.box(row) end if message then items[#items + 1] = ui.text(message) end diff --git a/sdcard/lib/statusbar.lua b/sdcard/lib/statusbar.lua index 3e7d721..409dcd0 100644 --- a/sdcard/lib/statusbar.lua +++ b/sdcard/lib/statusbar.lua @@ -28,7 +28,21 @@ local function drawSignal(x, y, color, muted) end end -function M.draw() +-- Fills the leading square of the bar, which is the rect the firmware treats as home. +-- Drawn as a button rather than left as a hidden hit region: a target nobody can see is +-- one nobody finds. +local function drawHome(theme) + local inset = 3 + local side = BAR_H - 1 - inset * 2 + gui.roundRect(inset, inset, side, side, 4, theme.bg, theme.face[1], theme.face[2], theme.muted) + local x, y = inset + side // 2 + 1, inset + side // 2 + for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck + gui.drawLine(x + offset, y - 4, x + offset - 4, y, theme.fg) + gui.drawLine(x + offset - 4, y, x + offset, y + 4, theme.fg) + end +end + +function M.draw(home) local theme = ui.theme gui.setTextSize(1) -- panel state: the app may have left it scaled up local w = gui.width() @@ -36,7 +50,12 @@ function M.draw() gui.fillRect(0, BAR_H - 1, w, 1, theme.muted) -- a rule, so the bar reads as chrome local textY = math.floor((BAR_H - 1 - gui.fontHeight()) / 2) - gui.drawText(sys.appName(), PAD, textY, theme.fg, theme.bg) + local nameX = PAD + if home then + drawHome(theme) + nameX = BAR_H + PAD + end + gui.drawText(sys.appName(), nameX, textY, theme.fg, theme.bg) local time = clock() gui.drawText(time, w - PAD - gui.textWidth(time), textY, theme.muted, theme.bg) diff --git a/src/lua/bindings/sys.cpp b/src/lua/bindings/sys.cpp index 1ed8de2..1aac526 100644 --- a/src/lua/bindings/sys.cpp +++ b/src/lua/bindings/sys.cpp @@ -141,7 +141,7 @@ void registerSys(lua_State* L) { // --- Blocks for the given time. // @param ms integer {"delay", l_sys_delay}, - // --- Ends this app and returns to the launcher. + // --- Ends this app and returns home. {"exit", l_sys_exit}, // --- Directory name of the running app, for example "settings". // @return string diff --git a/src/lua/lua_app.cpp b/src/lua/lua_app.cpp index f2bdb03..d480808 100644 --- a/src/lua/lua_app.cpp +++ b/src/lua/lua_app.cpp @@ -101,12 +101,14 @@ bool LuaApp::takeFailure() { return value; } -bool LuaApp::load(const char* path) { +bool LuaApp::load(const char* path, bool isHome) { 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 launcher tap may still be down; swallow that gesture's release. + // The tap that launched this app may still be down; swallow that gesture's release. lastTouched = true; ignoreRelease = true; state = luaL_newstate(); @@ -177,8 +179,9 @@ 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, 0, 0, 0) != LUA_OK) { + 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; @@ -229,7 +232,19 @@ void LuaApp::loop() { if (touched) { TS_Point p = touch.getPoint(); mapTouch(p, lastX, lastY); - if (lastY < 0) touched = false; // the bar is the host's, and it takes no input yet + } + + // 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); diff --git a/src/lua/lua_app.h b/src/lua/lua_app.h index b6a990f..cca914d 100644 --- a/src/lua/lua_app.h +++ b/src/lua/lua_app.h @@ -15,10 +15,11 @@ class LuaApp { LuaApp(TFT_eSPI& tft, XPT2046_Touchscreen& touch); ~LuaApp(); - bool load(const char* path); + // `isHome` marks the app the host falls back to, which needs no way back to itself. + bool load(const char* path, bool isHome = false); void loop(); - // Directory name of the running app ("launcher"), for the status bar. + // Directory name of the running app ("settings"), for the status bar. const String& name() const { return appName; } // Lets an app own the whole panel: no viewport, no status bar. The touch calibration @@ -64,6 +65,8 @@ class LuaApp { bool lastTouched = false; bool ignoreRelease = false; bool fullscreen = false; + bool isHome = false; + bool homeArmed = false; String appName; // One strike: a bar that failed once fails identically every second, and the serial // log is the only place anyone would see it. @@ -92,6 +95,12 @@ class LuaApp { // and the touch offset. int16_t barInset() const { return (fullscreen || barBroken) ? 0 : barHeight; } + // The home button is the leading square of the bar, which is what /lib/statusbar.lua + // paints into. Touches are in app space, so the bar is above y = 0. + bool inHomeButton() const { + return !isHome && lastY < 0 && lastY >= -barInset() && lastX >= 0 && lastX < barInset(); + } + void registerBindings(); bool callGlobal(const char* name, int nargs = 0); bool hasGlobal(const char* name); diff --git a/src/main.cpp b/src/main.cpp index 7f634c7..b4dbb27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,7 @@ static constexpr int SD_MOSI = 23; static constexpr int SD_MISO = 19; static constexpr int TOUCH_CS = 33; -static const char* LAUNCHER = "/apps/launcher/main.lua"; +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 @@ -42,7 +42,7 @@ void fallbackScreen(const char* message) { tft.setTextColor(TFT_RED, TFT_WHITE); tft.drawString(message, 10, 10); tft.setTextColor(TFT_BLACK, TFT_WHITE); - tft.drawString("expected /apps/launcher/main.lua", 10, 30); + tft.drawString("expected /apps/home/main.lua", 10, 30); Serial.printf("halted: %s\n", message); halted = true; } @@ -52,8 +52,8 @@ void startApp(const String& path) { tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame statusbar::apply(tft, 0); Serial.printf("launching %s\n", path.c_str()); - if (app.load(path.c_str())) return; - if (path == LAUNCHER) fallbackScreen("launcher failed to start"); + if (app.load(path.c_str(), path == HOME)) return; + if (path == HOME) fallbackScreen("home failed to start"); } void setup() { @@ -71,7 +71,7 @@ void setup() { } settings.load(); // absent file keeps the built-in defaults net::begin(); - startApp(LAUNCHER); + startApp(HOME); } void loop() { @@ -86,11 +86,11 @@ void loop() { app.loop(); if (!app.running()) nextApp = app.takePendingLaunch(); } else { - // An app that died left its message on screen, and the launcher is about to paint - // over it. Serial alone is no help to anyone holding the device. + // An app that died left its message on screen, and home is about to paint over it. + // Serial alone is no help to anyone holding the device. if (app.takeFailure()) delay(ERROR_HOLD_MS); - String path = nextApp.length() > 0 ? nextApp : String(LAUNCHER); + String path = nextApp.length() > 0 ? nextApp : String(HOME); nextApp = ""; startApp(path); } diff --git a/stubs/slate32.lua b/stubs/slate32.lua index b4d2b25..710c98e 100644 --- a/stubs/slate32.lua +++ b/stubs/slate32.lua @@ -225,7 +225,7 @@ function sys.millis() end ---@param ms integer function sys.delay(ms) end ---- Ends this app and returns to the launcher. +--- Ends this app and returns home. function sys.exit() end --- Directory name of the running app, for example "settings".