26f3fdb6e6
The runtime kept a back stack, a launcher fallback, an app id and a title because a teardown destroys the Lua that would otherwise hold them. Only the first of those is true: everything about where an app came from can ride in the arguments, and the arguments are the one value that has to outlive the VM. So the runtime now does four things -- close the state, load a path, hand the next state its arguments, defer the swap to a batch boundary -- and sys.startApp(path, args) is the whole of navigation. Routing, history, titles and data directories move to the Lua file a firmware boots, where they can differ per product without a flag on Runtime. Arguments cross as JSON, encoded while the sending state still holds the table, so a function or a cycle raises at the call rather than stranding a launch. start(args) receives the decoded table, or nil at boot, which is how the entry file knows to open its own launcher. Removes launch, replace, back, canGoBack, getAppID, getAppTitle, setAppTitle and getAppDataPath, along with the home and data fields. LANDSCAPE.md goes with them: it recorded a divergence from firmwares that have since migrated.
152 lines
8.0 KiB
Markdown
152 lines
8.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/`. 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 or
|
|
directory. Features compose, so a device may expose both `touch` and `buttons`. A panel is the
|
|
`screen` feature -- the `screen` and `tree` namespaces, including the saved rotation and theme --
|
|
because a headless firmware supplies no `GuiProvider`. Every namespace belongs to exactly one
|
|
feature or to core, which is why touch calibration is `touch.setCalibration()` rather than a
|
|
shared settings namespace three features write to. Display technology is still not a feature:
|
|
an e-ink `GuiProvider` flattens a gradient the way `screen.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.
|
|
|
|
Callbacks are fields on the table an app returns, not globals, so each `@lua-app` block generates a
|
|
class rather than loose functions: `App` for the core contract, `TouchHandlers` and `ButtonHandlers`
|
|
alongside the namespaces they belong to. An app composes the ones it implements
|
|
(`---@class PaintApp : App, TouchHandlers`), which is as close to per-device stubs as static
|
|
declarations get -- what a firmware actually provides is still `sys.hasFeature()` at runtime.
|
|
|
|
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, app
|
|
navigation, module loading, and `ble` are core even where a firmware does not implement them yet.
|
|
Every app may use `require`; the entry file points `package.path` wherever it keeps apps and
|
|
modules. This repository owns portable shared modules such as `ui.lua`; firmware-specific modules
|
|
stay with their firmware. The tree below is a convention of the Lua that boots, not something the
|
|
runtime knows -- it loads the one path it is given.
|
|
|
|
```text
|
|
/.lua/
|
|
main.lua the entry file a firmware boots
|
|
apps/<AppId>/main.lua
|
|
data/<AppId>/
|
|
lib/<module>.lua
|
|
```
|
|
|
|
The runtime owns the teardown and nothing above it. `sys.startApp(path, args)` closes the
|
|
`lua_State`, opens a fresh one, loads a path, and calls `start(args)` on the table it returns.
|
|
The arguments cross as JSON, because the table they came from dies with the state that built it;
|
|
encoding happens while that state still lives, so an argument JSON cannot carry raises at the call
|
|
rather than stranding a launch.
|
|
|
|
That is the whole of navigation. Routing, history, titles and data directories are decided by the
|
|
Lua file a firmware boots, since the only thing that structurally cannot live there is a value
|
|
that has to outlive the VM -- and the arguments are that value. `sys.startApp` records intent and
|
|
returns, because swapping the state inside a callback would free the VM still executing it; the
|
|
firmware calls `applyPendingNavigation()` between batches. With app identity gone from C++,
|
|
`SysProvider` is down to `millis`, `memory`, and `isClockSynced`.
|
|
|
|
`draw(deltaMs)` is an optional frame loop called once after `start()` 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 `start()` 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 application belongs to shared `ui.lua`; `screen.setTheme()` only stores the name.
|
|
|
|
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; `tree` 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`
|