# Lua apps — plan + tracker Goal: non-core apps become **Lua scripts** installed from a store catalog on firmware.wadamesh.com — installable, uninstallable and updatable without cutting a beta, one app artifact for every board, and writable by the community without forking the firmware. Runtime target: **Lua 5.4** (vanilla, no JIT — smallest serious VM, trivial to sandbox, portable Xtensa/RISC-V). Non-goals (v1): native dynamic loading (ELF), Lua access to LoRa TX or protocol frames, multi-file app packages, app signing (the curated catalog is the trust layer, same as firmware bins today). ## Execution model — the decision that keeps this safe Lua runs **on the UI task**, event-driven. No app thread, no preemption: - The host calls the app's callbacks: `on_open(w, h)`, `on_tick(dt_ms)`, `on_input(ev)`, `on_close()`. All LVGL work stays on the LVGL thread by construction (LVGL is not thread-safe). - Every callback runs under a `lua_sethook(LUA_MASKCOUNT)` instruction budget (~100k instructions ≈ low single-digit ms). Budget exceeded → error → app closed with a toast. A buggy app can never stall the mesh loop or trip the WDT. - Every entry is wrapped in `lua_pcall`. A Lua error NEVER reboots the device: toast "App error: ", close the app, free the state. Crash containment is the whole point. - `lua_newstate` gets a custom allocator on **PSRAM** (`heap_caps_malloc` SPIRAM) with a hard per-app cap (256 KB to start). The V4's tight internal DRAM (#192) is never touched by app heaps. - One app at a time (matches today's overlay model). The app root is one `k_popup_registry` row — red-X/Esc/status-bar close come free. ## The `wada.*` API (v1) — deliberately small We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own (`manifest.min_api` gates compatibility). ~30 functions total: - `wada.ui` — the app's root container + widgets: `canvas(w,h)` (draw_rect, draw_line, draw_text, draw_circle, fill, blit-from-table), `label`, `chart` (the Airtime/Monitor primitive), `button`, `list`. Theme colours provided (`wada.ui.colors`), fonts by size class only (12/14/16 → g_font_*). - `wada.input` — events into `on_input`: touch x/y down/up, key codes, trackball dirs. (Snake's swipe steering maps here; the existing SnakeGame gesture path shows the shape.) - `wada.mesh` (READ-ONLY v1) — `contacts()` snapshot (name/type/last_seen/ rssi), `rx_log()` ring (the RF Monitor feed), `stats()` (airtime used, duty budget, tx/rx counters, noise floor, current rssi), `self()` (name, lat/lon, battery_mv). No TX of any kind in v1 — mesh safety line. - `wada.net` — `http_get(url, cb, max_bytes)` async on the existing tile-fetch worker; plain HTTP (V4 cannot TLS); response size capped (64 KB default). - `wada.store` — per-app KV: `get/set(key, value)` under an `app..` prefix in the file-mode prefs. Quota 2 KB/app (respects the SdNvsPrefs blob cap). - `wada.sys` — `millis()`, `board()` (id, screen w/h, caps: keyboard, trackball, gps), `version()`, `toast(msg)`. - `wada.timer` — `every(ms)` drives `on_tick` cadence (min 33 ms). Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks; whitelisted stdlib: `math`, `string`, `table`, `pairs/ipairs/select/pcall/ tostring/tonumber`). API-level bounds are the second fence, the curated catalog is the third. ## App format + store - On device: `/apps/.lua` + `/apps/.json` (manifest: name, version, min_api, boards allow-list, icon glyph, description) on the active storage root (SD where adopted, internal FS otherwise) — same resolution as chat history. - Catalog: `https://firmware.wadamesh.com/apps/apps.json` + one `.lua` + `.json` per app in `apps///`. Served like firmware: VPS rsync, immutable per-version paths, mutable index. Device store page = fetch index → list → install (download 2 files) → appears in the drawer. Uninstall = delete 2 files. Update = version compare against the index. - The Store page also lists the **built-in removable apps** (Snake, Airtime, RF Monitor, later more) with the same install/uninstall verbs — for built-ins that just toggles drawer visibility + services (MQTT bridge gets this too). One UX for both worlds from day one. ## Budgets + board gates (measure in Phase 0, numbers are targets) - Flash: Lua core + bindings ≤ **250 KB** (-Os, no coroutines trimmed? keep coroutines, trim io/os libs at compile). Gate: `CAP_LUA_APPS` in device_caps.h. V4 (77.6%) and T-Deck (81.8%) are fine. **The Pager is the risk board** (84.3% now, 87.7% if #198 fonts land) — if Lua + fonts don't both fit, the Pager ships CAP_LUA_APPS=0 first and we revisit; measure before deciding. P4/Tanmatsu: plenty of room, plain C portability. - RAM: state base ≤ 40 KB PSRAM, Snake running ≤ 100 KB PSRAM, ZERO new internal-DRAM allocations attributable to apps. - Latency: worst on_tick under budget ≤ 4 ms on the V4 @ 240 MHz (measure with the STALL_SCOPE harness already in loop()). ## Conversion inventory Tier 1 — converts first (proves the API): - **Snake** (SnakeGame.h — already a self-contained module): canvas + input + timer + store (high score). The reference app; native version compiled out once parity is confirmed on all boards. - **Airtime** (ChannelUtil, jacobpretorius): chart + `wada.mesh.stats()`. Invite @jacobpretorius to do this port against the SDK — the first external Lua app author, with credit. Tier 2 — converts once the API stabilises: - **RF Monitor**: rx_log + stats + chart + recolor-safe labels. - **Telemetry charts** (Sensors view): needs a `wada.mesh.telemetry()` read. - **LOS analyzer**: http_get (SRTM), contacts picker, canvas plot. Stays native, with reasons (do not creep): - **Spectrum** — owns the raw SX1262 with ms-level timing; a Lua radio API is a different (dangerous) project. - **Discover** — transmits protocol frames; revisit only if a consent-gated TX API ever exists. - **Web browser, VNC/Remote/web mirror, Terminal, Files** — infrastructure, not apps. - **MQTT bridge** — background service woven into the main loop; gets the catalog uninstall-toggle only. ## Delivery: ALL of it ships in ONE beta — beta_59 (Kaj, 2026-08-04) The phases below are the build order inside the beta_59 branch of work, not separate releases. Ship list for beta_59: runtime + host + wada.* v1 + the Store page + VPS catalog + Snake/Airtime/RF Monitor converted to Lua (seeded to storage at first boot so they work offline, native versions compiled out under CAP_LUA_APPS) + built-in hide/show toggles + an SDK docs page. The Pager go/no-go measurement happens mid-flight and only decides that board's CAP_LUA_APPS value, not the project. ## Phases (build order within beta_59) - **Phase 0 — spike + go/no-go (behind an experimental flag, T-Deck only).** Vendor Lua 5.4 under `lib/lua/` (PIO) + an IDF component wrapper for P4/Tanmatsu (the ed25519/shared-src pattern). PSRAM allocator, sandbox env, instruction hook, pcall containment. A 20-line hello app drawing to a canvas. Deliverable: measured flash/RAM/latency vs the budgets above. STOP here if the Pager numbers say the whole idea needs rethinking. - **Phase 1 — API v1 + Snake.** The app host (lifecycle, popup-registry row, error toasts), wada.ui/input/timer/sys/store, Snake ported + shipped as a bundled Lua app (still pre-store: file baked into the FS image or seeded). Beta-tested by the community like any feature. - **Phase 2 — the store.** /apps scan, drawer integration, manifest checks, the Store page (browse/install/uninstall/update), VPS catalog + curation flow (a PR to a catalog repo dir = submission, we review = curation). Built-in-app hide/show toggles land here too. - **Phase 3 — mesh/net APIs + Tier-1/2 conversions.** wada.mesh + wada.net, Airtime and RF Monitor converted, their native versions compiled out where CAP_LUA_APPS=1. Flash reclaimed is a bonus, not the goal. - **Phase 4 — SDK launch.** Docs page on the site (existing docs workflow), an annotated example app, an "apps wanted" note in the release body. PixPMusic and jacobpretorius get first pings. Parked for later: multi-file apps, app signing, TX-capable APIs with on-device consent prompts, a desktop simulator for app development (lv_port_pc + the same wada.* bindings — nice for contributors, not needed to start). ## Risks, named - **Pager flash ceiling** — measured in Phase 0, gated per-board; worst case the Pager waits. - **API regret** — once apps exist in the wild, wada.* is an ABI. Mitigation: v1 stays tiny, everything else waits until two real apps demand it; `min_api` versioning from day one. - **V4 PSRAM pressure** — app heaps compete with the tile pool; the 256 KB cap + tile-pool priority keeps maps working. Watch #192's findings. - **Store curation load** — catalog PRs need review; that is deliberate (curation = the trust model) but it is maintainer time. ## Status log - 2026-08-04: plan written. - 2026-08-04: Phase 0 GO — measured on the T-Deck: flash +96 KB (budget 250), state+libs 7.5 KB PSRAM / 2 ms, busy-script peak 31 KB, hostile while-true contained in 30 ms, zero leak after close. Hook cadence: 100k instructions (10k costs +111% on call-heavy code; 100k is ~free and still contains). Pager fits (+2.4%, ~87% even with #198 fonts). - 2026-08-04: Phase 1 DONE — LuaAppHost (AppPage overlay, guarded pcall + 100k-instruction budget, deferred close from inside callbacks), wada.ui (canvas/label/button + theme colors) + input (hw swipes, trackball, touch) + timer + sys + store (per-app KV file, root-prefix aware), Snake ported to Lua as the embedded+seedable reference app. Verified on the T-Deck: open at 19 KB PSRAM, plays via hw-swipe steering, an early bug (pre-layout size read) was contained exactly as designed (toast + clean close, 0 leak, 4 retries, no crash). Host layer costs 13 KB on top of the VM (85.1%% T-Deck total). Next: Phase 2 store page + catalog. **2026-08-04 — Store v2: three tabs + languages-as-files.** The store page is now segmented: Apps (catalog + sideloads), Built-in (show/hide switches), Languages (new). Store opens instantly — the card re-listing moved to the net worker (s_luascan_* flags) and pre-warms at boot. Card text is width-capped clear of the action button. Languages: gen-lang-files.py exports every i18n column to apps/lang/.lang + langs.json (serving on the VPS); the device downloads to /lang/, prefs v48 stores the active code, and at boot the file loads into PSRAM and overlays TR() (bsearch; file → built-in column → EN). Users can sideload/edit their own .lang files. Built-in table stays as fallback this beta; dropping its columns for the flash win is the follow-up once files are field-proven.