Files
esp32-lua-api/native/include/lua/runtime.h
T
evan 75b3a2c490 refactor(api)!: one namespace per feature, screen split out
Namespaces were shared across features: `settings` was written by core, the
panel and touch, and `input` by touch and buttons. That made "does this
firmware implement the whole feature?" a question no pointer could answer.

Each namespace now belongs to exactly one feature or to core, so a feature is
a provider pointer and the compiler validates completeness:

  gui, node       -> screen, tree, under the screen feature
  settings        -> screen (rotation, theme), sys (timezone),
                     touch (calibration)
  input           -> touch, buttons

Runtime::open() no longer requires a GuiProvider; a firmware without one runs
with no screen/tree globals and reports sys.hasFeature("screen") false.
Rotation is one value again: GuiProvider::setRotation applies and persists, so
an app rotating the panel transiently puts the old value back itself.
2026-08-05 10:26:38 -04:00

183 lines
6.3 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;
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;
};
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; }
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