#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 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., 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 the entry file returned, or 0 before one // loads. int mainRef_ = 0; std::string appPath_; Pending pending_; }; } // namespace esp32lua