Files
2026-08-03 17:46:14 -04:00

139 lines
7.0 KiB
Markdown

# 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/<AppId>/main.lua
apps/<AppId>/<Subapp>/main.lua
data/<AppId>/
lib/<module>.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`