Files
esp32-lua-api/native/include/lua/runtime.h
T
evan 26f3fdb6e6 feat(runtime)!: sys.startApp replaces routing, history and app identity
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.
2026-08-05 17:04:35 -04:00

169 lines
5.9 KiB
C++

#pragma once
#include <map>
#include <string>
#include <vector>
#include <lua/layout.h>
#include <lua/providers.h>
struct lua_State;
namespace esp32lua {
// The version sys.getAPIVersion() reports: this contract, not the firmware's
// build.
constexpr int32_t API_VERSION = 1;
// The path a firmware boots. Nothing else here knows it: startApp() takes
// whatever path it is given, and where apps live, where their data goes and
// what chrome surrounds them are decided by the Lua it loads.
constexpr const char* MAIN_PATH = "/.lua/main.lua";
// Firmware supplies every core provider; a null feature provider is how
// sys.hasFeature() answers false, and its namespace additions are simply never
// registered.
struct Providers {
LogProvider* log = nullptr;
SysProvider* sys = nullptr;
FsProvider* fs = nullptr;
HttpProvider* http = nullptr;
TimerProvider* timer = nullptr;
WifiProvider* wifi = nullptr;
BleProvider* ble = nullptr;
GuiProvider* gui = nullptr;
TouchProvider* touch = nullptr;
ButtonsProvider* buttons = nullptr;
};
// The arguments a launch carries cross the teardown as JSON, because the table
// they came from dies with the state that built it. Encoding raises, so an app
// that passes a function sees the error at its own sys.startApp() call;
// decoding cannot, because by then there is no app to report it to.
std::string encodeJson(lua_State* state, int index);
bool decodeJson(lua_State* state, const std::string& json);
class Runtime {
public:
explicit Runtime(const Providers& providers);
~Runtime();
Runtime(const Runtime&) = delete;
Runtime& operator=(const Runtime&) = delete;
// Fails when a core provider is missing, leaving no Lua state behind.
bool open();
void close();
lua_State* state() const { return state_; }
// Replaces the running app with a fresh lua_State, loads the path, and hands
// the table it returns its arguments through start(args). A failure leaves no
// app running rather than a half-built one. `argsJson` is the JSON a previous
// state encoded, and is the only thing that crosses the teardown.
bool startApp(const std::string& path,
const std::string& argsJson = std::string());
bool hasApp() const { return !appPath_.empty(); }
// The path that was loaded, which is all the runtime knows about an app.
const std::string& appPath() const { return appPath_; }
bool hasFeature(const std::string& feature) const;
// sys.startApp records intent and returns; swapping the lua_State inside a
// callback would free the VM that is still executing. The firmware applies it
// between batches.
void requestStart(const std::string& path, const std::string& argsJson);
bool hasPendingNavigation() const { return pending_.pending; }
// Loads whatever was requested. False means the app failed to start, in which
// case no app is running.
bool applyPendingNavigation();
LogProvider& log() const { return *providers_.log; }
SysProvider& sys() const { return *providers_.sys; }
FsProvider& fs() const { return *providers_.fs; }
GuiProvider& gui() const { return *providers_.gui; }
HttpProvider& http() const { return *providers_.http; }
TimerProvider& timer() const { return *providers_.timer; }
WifiProvider& wifi() const { return *providers_.wifi; }
BleProvider& ble() const { return *providers_.ble; }
TouchProvider& touch() const { return *providers_.touch; }
ButtonsProvider& buttons() const { return *providers_.buttons; }
ui::Tree& tree() { return tree_; }
// Entry points into main.lua, which forwards whatever the app it mounted
// defines. The firmware decides whether an event happens at all -- jitter and
// debouncing are its business -- and main.lua decides who sees it. Only a
// failed start() stops an app; every other callback logs and carries on.
bool callStart(const std::string& argsJson);
void callDraw(int32_t deltaMs);
// An Up phase also fires the on_touch tap alias, in that order.
void callTouch(TouchPhase phase, int32_t x, int32_t y);
// A release also fires the on_button tap alias, in that order.
void callButton(const std::string& button, bool pressed);
// Timer identity and callback retention are the runtime's; deadlines are the
// firmware's.
TimerId addTimer(int callbackRef, int32_t intervalMs, bool repeating);
bool cancelTimer(TimerId id);
// Called on the Lua thread when a scheduled deadline elapses.
void callTimer(TimerId id);
static Runtime* from(lua_State* state);
private:
struct Timer {
int callbackRef;
bool repeating;
};
struct Pending {
bool pending = false;
std::string path;
std::string argsJson;
};
bool loadScript(const std::string& path);
// Runs the app's entry file and keeps the table it returns; the app is
// mounted by that table, not by the runtime.
bool loadMain(const std::string& path);
void installLoader();
static int searchModule(lua_State* state);
static int searchEmbedded(lua_State* state);
static int loadFile(lua_State* state);
// A batch is one visit to the app, however many callbacks it fans out into: a
// tap fires on_touch_up and then the on_touch alias, and a timer can fire
// inside draw. Committing per callback would refresh an e-ink panel twice for
// one visible change, so the display is committed when the outermost call
// returns.
class Batch {
public:
explicit Batch(Runtime& runtime);
~Batch();
private:
Runtime& runtime_;
};
// Pushes main.<name>, or returns false when main.lua defines no such handler.
bool beginCall(const char* name);
bool finishCall(const char* name, int argc);
bool finishCallValue(const char* name);
void cancelAllTimers();
Providers providers_;
lua_State* state_ = nullptr;
ui::Tree tree_;
std::map<TimerId, Timer> timers_;
TimerId nextTimerId_ = 1;
int batchDepth_ = 0;
// Registry reference to the table the entry file returned, or 0 before one
// loads.
int mainRef_ = 0;
std::string appPath_;
Pending pending_;
};
} // namespace esp32lua