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.
2.6 KiB
ESP32 Lua API Guidelines
This is a clean contract for repositories under the same owner's control. Choose the best shared API without preserving old names, signatures, or behavior; consumers migrate to the contract.
Every firmware implements all declarations under lua/api/core/. Optional hardware contracts
live under lua/api/features/, as one file or one directory per feature; sys.hasFeature(name)
guarantees the complete matching contract. Every namespace belongs to exactly one feature or to
core, so a feature is a provider pointer rather than a claim to validate: a panel is the screen
feature (screen and tree, including the saved rotation and theme), registered only when the
firmware supplies a GuiProvider, and calibration is touch.setCalibration().
The runtime owns what survives a teardown and nothing else: sys.startApp(path, args) closes the
lua_State, loads a path, and hands the next one its arguments as JSON. Routing, history, titles
and data directories are that Lua file's, because a back stack that cannot outlive the VM is not a
back stack, and everything else about where an app came from can. Encoding happens in the state
that still holds the table, so unencodable arguments raise at the call rather than stranding a
launch.
A global namespace is a provider contract a firmware implements; anything this library provides
itself is a module instead, so require and globals divide by who supplies the code. Those live in
native/src/bindings/lib/ and register into package.preload, which puts them ahead of the SD-card
searcher so nothing shadows an implementation. Pure-Lua modules stay in lua/lib/, where shadowing
is allowed because a module there is self-contained: it belongs in lua/lib/ only if replacing it
can break nothing but itself.
Lua-language sources stay under lua/; C/C++ and the vendored interpreter stay under native/.
Apps are fully trusted; keep permissions and sandboxing out of scope.
Firmware commits dirty display content and owns panel refresh policy. Binding annotations under
native/src/bindings/ generate matching lua/api/ files; regenerate instead of editing files
marked generated. Callbacks are fields on the table an app returns, so a @lua-app block generates
a class an app composes (---@class PaintApp : App, TouchHandlers) rather than global functions. Each @lua-module/@lua-augment directive sits immediately above the
luaL_Reg table it describes, which is how one source declares several namespaces. This repository owns portable lua/lib/ modules; shared UI owns theme application and persistence. Use ble for BLE/GATT
and reserve bt for a future Classic Bluetooth contract.