Files
esp32-lua-api/native/include/lua/runtime.h
T
evan e3153f57c5 feat(runtime): hand the whole app contract to /.lua/main.lua
The firmware knew four paths and called four globals, so the card could
not change its own layout or put anything around an app. It now loads one
file, and the table that file returns owns the rest: start() mounts the
route, home and data name the tree, and every callback is a field on it
rather than a global the app and its chrome would have to share.
2026-08-04 20:51:25 -04:00

185 lines
6.4 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 one path the firmware knows. Everything below it -- where apps live,
// where their data goes, what chrome surrounds them -- is decided by the table
// this file returns.
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;
SettingsProvider* settings = nullptr;
SysProvider* sys = nullptr;
FsProvider* fs = nullptr;
GuiProvider* gui = nullptr;
HttpProvider* http = nullptr;
TimerProvider* timer = nullptr;
WifiProvider* wifi = nullptr;
BleProvider* ble = nullptr;
TouchProvider* touch = nullptr;
ButtonsProvider* buttons = nullptr;
};
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 main.lua, and hands
// it the route through start(route, arg). A failure leaves no app running
// rather than a half-built one.
bool startApp(const std::string& path,
const std::string& arg = std::string());
bool hasApp() const { return !appPath_.empty(); }
// The app-relative route, its immutable first component, and the title the
// app chose.
const std::string& appPath() const { return appPath_; }
std::string appId() const;
std::string appDataPath() const;
const std::string& appTitle() const { return appTitle_; }
void setAppTitle(const std::string& title) { appTitle_ = title; }
bool hasFeature(const std::string& feature) const;
// sys.launch/replace/back record intent and return; swapping the lua_State
// inside a callback would free the VM that is still executing. The firmware
// applies it between batches.
void requestLaunch(const std::string& path, const std::string& arg,
bool replace);
void requestBack();
bool hasPendingNavigation() const { return pending_.kind != Pending::None; }
// Whether sys.back() would return somewhere rather than land on the launcher,
// which is what firmware chrome needs to decide whether to offer a back
// control.
bool canGoBack() const { return !history_.empty(); }
// Loads whatever was requested. False means the app failed to start or
// history ran out at the launcher, in which case no app is running.
bool applyPendingNavigation();
LogProvider& log() const { return *providers_.log; }
SettingsProvider& settings() const { return *providers_.settings; }
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& route, const std::string& arg);
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 Route {
std::string path;
std::string arg;
};
struct Pending {
enum Kind { None, Launch, Replace, Back } kind = None;
Route route;
};
bool loadScript(const std::string& path);
// Runs main.lua and keeps the table it returns; the app is mounted by it, not
// by the runtime.
bool loadMain();
void installLoader();
// A field of the main table, or the fallback when main.lua names none.
std::string mainField(const char* key, const char* fallback);
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 main.lua returned, or 0 before one loads.
int mainRef_ = 0;
// Read from main.lua once per load, because sys.back() out of the last app
// needs the route after that app's state is gone.
std::string home_;
std::string dataTemplate_;
std::string appPath_;
std::string appTitle_;
std::vector<Route> history_;
Pending pending_;
};
} // namespace esp32lua