#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; // 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., 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 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 history_; Pending pending_; }; } // namespace esp32lua