#pragma once #include #include #include #include #include struct lua_State; namespace esp32lua { // The version sys.getAPIVersion() reports: this contract, not the firmware's build. constexpr int32_t API_VERSION = 1; // Where the runtime looks for apps, their data, and shared modules. struct Paths { std::string apps = "/.lua/apps"; std::string data = "/.lua/data"; std::string lib = "/.lua/lib"; // Where sys.back() lands once history is empty. It is an app like any other. std::string home = "Home"; }; // 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, const Paths& paths = Paths()); ~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 calls // init(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 the app. The firmware decides whether an event reaches the app at all -- // jitter, chrome and debouncing are its business -- and the runtime decides what the app sees. // Only a failed init() stops an app; every other callback logs and carries on. bool callInit(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); void installLoader(const std::string& appDir); static int searchModule(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 the named global, or returns false when the app does not define it. bool beginCall(const char* name); bool finishCall(const char* name, int argc); void cancelAllTimers(); Providers providers_; lua_State* state_ = nullptr; ui::Tree tree_; std::map timers_; TimerId nextTimerId_ = 1; int batchDepth_ = 0; Paths paths_; std::string appPath_; std::string appTitle_; std::vector history_; Pending pending_; }; } // namespace esp32lua