# ESP32 Lua API A future shared contract for Lua applications running on the ESP32 firmwares in `../slate32` and `../crosspoint-reader`. Lua declarations and shared modules live under `lua/`; the vendored interpreter and shared C/C++ runtime live under `native/`. [`LANDSCAPE.md`](LANDSCAPE.md) records how the contract differs from the current firmwares. Runtime bindings remain authoritative until this repository is wired into their tests. ## Layout ```text lua/ api/core/ Required namespace contracts, generated api/features/ Optional touch and buttons contracts, generated lib/ Executable modules shipped to /.lua/lib, handwritten test/ Lua host tests native/ include/lua/ Public shared-runtime C++ headers src/bindings/ Annotated Lua bindings, one file per namespace src/node/ Shared widget-tree painting over GuiProvider src/runtime/ Lua state, provider wiring, and timer dispatch src/vendor/lua/ Vendored Lua interpreter implemented in C test/ Native host tests ``` Every firmware implements all files under `lua/api/core/`. `sys.hasFeature(name)` declares optional features; claiming one guarantees every API and behavior in its matching file. Features compose, so a device may expose both `touch` and `buttons`. Display technology is not a feature: an e-ink `GuiProvider` flattens a gradient the way `gui.color()` quantizes to grayscale, and the firmware owns publication and waveform policy on every panel. The contract is the app-facing Lua API, not the provider C++ interface. Shared binding registrations carry LuaLS annotations; `tools/gen_api.py` mirrors them into `lua/api/`. Generated files are committed for editors and checked for drift by `make test`, so a namespace is documented by the code that registers it, including the callbacks in `core/runtime.lua` and the feature files, which are generated from the `Runtime::call*` sites that fire them. Nothing under `lua/api/` ever runs: it is `---@meta` for editors and the drift check. `lua/lib/` is the opposite -- real modules that ship to the SD card, so composition like `ui.lua` and `hints.lua` changes without a reflash. Firmware supplies the interfaces in `native/include/lua/providers.h` and nothing else: the bindings, argument validation, timer identity, and the node tree are shared. A null feature provider is how `sys.hasFeature()` answers false, and its namespace additions are simply never registered. The declarations are a clean target, not the intersection of today's APIs. Existing apps and firmwares migrate to it without compatibility aliases. Safe filesystem mutation, `node`, app navigation, module loading, and `ble` are core even where a firmware does not implement them yet. Every app may use `require`; modules resolve from its app directory and `/.lua/lib`. This repository owns portable shared modules such as `ui.lua`; firmware-specific modules stay with their firmware. ```text /.lua/ apps//main.lua apps///main.lua data// lib/.lua ``` The runtime owns app loading: `Runtime::startApp()` opens a fresh `lua_State`, points `require` at the app directory and `/.lua/lib`, runs the chunk, and calls `init(arg)`. `sys.launch`/`replace`/ `back` only record intent, because swapping the state inside a callback would free the VM that is still executing; the firmware calls `applyPendingNavigation()` between batches. History, the launcher fallback, app identity, and `sys.hasFeature()` all live there too, which is why `SysProvider` is down to `millis`, `memory`, and `isClockSynced`. `sys.launch("Settings/Calibration")` resolves the nested `main.lua`; only top-level apps appear in the launcher. The first path component is the immutable app ID, so every Settings route shares `sys.getAppDataPath()` and `/.lua/data/Settings`. Package updates do not touch persistent data. `draw(deltaMs)` is an optional frame loop called once after `init()` and then at most 30 FPS, best effort. The host passes monotonic elapsed milliseconds (`0` on the first frame). Timers take Lua callbacks and return cancellation handles. The firmware decides whether an event reaches an app at all -- jitter filtering, chrome, and debouncing are its business -- and `Runtime::call*` decides what the app sees. A release fires `on_touch_up` then the `on_touch` alias, and a button release fires `on_button_up` then `on_button`, so the ordering is identical on every device. Only a failed `init()` stops an app; every other callback logs through `LogProvider` and carries on. Calling a feature callback without its provider is a wiring bug and says so. The firmware commits dirty display content after callback batches and owns e-ink waveform policy; apps do not refresh the panel manually. Apps are fully trusted with the complete core API. Theme persistence and application belong to shared `ui.lua`, not the firmware `settings` binding. The native library vendors Lua 5.4.8 from GitHub tag `v5.4.8` and compiles it with `LUA_32BITS`. Command-line and upstream test entry points are excluded; `luaconf.h` carries one documented guard that lets the build flag select 32-bit number mode. ## Shared UI Portable apps normally use the declarative `ui.lua` toolkit; `node` remains the low-level escape hatch. The baseline constructors are `screen`, `box`, `spacer`, `text`, `label`, `button`, `custom`, and `confirm`. A screen accepts both touch and physical-button input. Widgets expose three input-agnostic callbacks: ```lua ui.button{ on_enter = function(id, x, y) end, on_exit = function(id, x, y) end, on_click = function(id, x, y) end, } ``` Touch-down enters; moving outside exits and moving back enters again; release exits before a release-inside click. Directional focus enters and exits nodes; confirmation clicks. Coordinates are present only for touch. There is no hover API. Built-in buttons usually need only `on_click`; enter/exit exist for custom visuals. Apps forward input through `screen:down(x, y)`, `move(x, y)`, `up(x, y)`, and `button(name, pressed)`. These return whether the toolkit handled the event, leaving back and page buttons available to the app. The selected theme name is global in `/.lua/theme`. Shared `ui.lua` atomically persists and applies `getTheme`, `setTheme`, and `themeNames`; it also owns palette application, focus handling, and input dispatch. Hardware-specific widgets stay outside core UI. ## Development ```sh nix develop make api # regenerate LuaLS declarations from shared binding annotations make test # generated-file, Lua module, interpreter, and runtime checks ``` The shell provides Lua 5.4 for module tests; native checks compile the vendored Lua 5.4.8 with the embedded 32-bit number configuration. `native/test/runtime_test.cpp` drives every namespace against the fakes in `native/test/fake_providers.h`, so a binding's marshalling is asserted without a board. ## Current Sources - LCD: `../slate32/stubs/slate32.lua` - E-ink: `../crosspoint-reader/data/lua/crosspoint.lua` - Existing comparison: `../slate32/docs/lua-api-parity.md`