139 lines
7.0 KiB
Markdown
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`
|