From 967fecbf69df1c100059a53f46d20b5c032f1c1b Mon Sep 17 00:00:00 2001 From: Ronny Eia <3652665+eiaro@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:27:00 +0200 Subject: [PATCH] Add persistent script storage + hot-reload, verified live lua_runtime now loads the running script from a new LittleFS-backed drivers::storage (joltwallet/littlefs, the project's fifth external managed component -- mounts the "storage" partition already reserved in partitions.csv), falling back to the compiled-in default script only on first boot. transport exposes GET/POST /api/script -- raw Lua text, not JSON, since json_helpers.h can't round-trip scripts safely -- relayed to lua_runtime via the same callback-registration pattern already used for LedColorHandler, keeping transport free of a dependency on lua_runtime. lua_runtime_reload() persists and hot-swaps to a fresh Lua state immediately, no reboot. Verified on real ESP32-C6 hardware: script loads via kvida-sdk, an edited script hot-reloads within one tick, and the edit survives a power cycle (proving LittleFS persistence, not just an in-RAM change). Co-Authored-By: Claude Sonnet 5 --- components/drivers/CMakeLists.txt | 4 +- components/drivers/README.md | 3 +- components/drivers/idf_component.yml | 1 + components/drivers/include/storage.h | 21 +++++ components/drivers/src/storage.cpp | 79 ++++++++++++++++ components/lua_runtime/README.md | 13 +-- components/lua_runtime/include/lua_runtime.h | 21 ++++- components/lua_runtime/src/lua_runtime.cpp | 94 +++++++++++++++++--- components/transport/README.md | 1 + components/transport/include/mqtt.h | 14 +++ components/transport/src/mqtt.cpp | 85 +++++++++++++++++- main/main.cpp | 22 ++++- 12 files changed, 329 insertions(+), 29 deletions(-) create mode 100644 components/drivers/include/storage.h create mode 100644 components/drivers/src/storage.cpp diff --git a/components/drivers/CMakeLists.txt b/components/drivers/CMakeLists.txt index f9af15a..f1272f9 100644 --- a/components/drivers/CMakeLists.txt +++ b/components/drivers/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "src/chip_temperature.cpp" "src/rgb_led.cpp" + SRCS "src/chip_temperature.cpp" "src/rgb_led.cpp" "src/storage.cpp" INCLUDE_DIRS "include" - PRIV_REQUIRES esp_driver_tsens espressif__led_strip + PRIV_REQUIRES esp_driver_tsens espressif__led_strip joltwallet__littlefs ) diff --git a/components/drivers/README.md b/components/drivers/README.md index 0e0453e..bc2aa05 100644 --- a/components/drivers/README.md +++ b/components/drivers/README.md @@ -4,5 +4,4 @@ Hardware abstraction: wraps ESP32-C6 peripherals (GPIO, ADC, I2C) behind a board - `chip_temperature.h`/`.cpp` -- wraps ESP-IDF's built-in `temperature_sensor` driver (die temperature; there's no external ambient temperature sensor on the ESP32-C6-DevKitC-1). - `rgb_led.h`/`.cpp` -- wraps the DevKitC-1's onboard WS2812 addressable RGB LED (GPIO8) via the `espressif/led_strip` managed component (`idf_component.yml`). - -TODO: mounting the `storage` LittleFS partition (see `partitions.csv`) belongs here once a LittleFS component (e.g. `joltwallet/esp_littlefs` from the IDF Component Registry) is picked and pinned via `idf_component.yml`. +- `storage.h`/`.cpp` -- mounts the `storage` LittleFS partition (see `partitions.csv`) at `/storage` via `joltwallet/littlefs` (verified against the component's actual header, not guessed -- the real mount function is `esp_vfs_littlefs_register()`, not `esp_vfs_littlefs_mount()` despite what some docs summaries say), then exposes plain whole-file `storage_read_file()`/`storage_write_file()` helpers over the standard `fopen`/`fread`/`fwrite` VFS calls. Used by `lua_runtime` to persist the running script (`components/lua_runtime/README.md`). diff --git a/components/drivers/idf_component.yml b/components/drivers/idf_component.yml index ac4ab84..a0fd919 100644 --- a/components/drivers/idf_component.yml +++ b/components/drivers/idf_component.yml @@ -1,3 +1,4 @@ dependencies: espressif/led_strip: version: "*" + joltwallet/littlefs: "1.22.2" diff --git a/components/drivers/include/storage.h b/components/drivers/include/storage.h new file mode 100644 index 0000000..bfe6d2d --- /dev/null +++ b/components/drivers/include/storage.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace kvida::drivers { + +// Mounts the "storage" partition (see partitions.csv) as a LittleFS +// filesystem at /storage. Safe to call multiple times (no-op if already +// mounted). Formats the partition automatically if it's blank or corrupt. +void storage_init(); + +// Reads a whole file into buf (cap includes room for the implicit '\0' +// this appends). Returns false if the filesystem isn't mounted, the file +// doesn't exist, or it doesn't fit in cap. +bool storage_read_file(const char *path, char *buf, size_t cap, size_t *out_len); + +// Writes/overwrites a whole file. Returns false if the filesystem isn't +// mounted or the write fails. +bool storage_write_file(const char *path, const char *data, size_t len); + +} // namespace kvida::drivers diff --git a/components/drivers/src/storage.cpp b/components/drivers/src/storage.cpp new file mode 100644 index 0000000..0cd771f --- /dev/null +++ b/components/drivers/src/storage.cpp @@ -0,0 +1,79 @@ +#include "storage.h" + +#include + +#include "esp_log.h" +#include "esp_littlefs.h" + +namespace kvida::drivers { + +namespace { +constexpr const char *TAG = "storage"; +bool g_mounted = false; +} // namespace + +void storage_init() +{ + if (g_mounted) { + return; + } + + esp_vfs_littlefs_conf_t conf = {}; + conf.base_path = "/storage"; + conf.partition_label = "storage"; + conf.format_if_mount_failed = true; + conf.grow_on_mount = false; + + esp_err_t err = esp_vfs_littlefs_register(&conf); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_vfs_littlefs_register failed: %s", esp_err_to_name(err)); + return; + } + g_mounted = true; +} + +bool storage_read_file(const char *path, char *buf, size_t cap, size_t *out_len) +{ + if (!g_mounted || cap == 0) { + return false; + } + + FILE *f = fopen(path, "r"); + if (!f) { + return false; + } + + size_t n = fread(buf, 1, cap - 1, f); + bool truncated = !feof(f); + fclose(f); + + if (truncated) { + ESP_LOGE(TAG, "%s is larger than the %u-byte read buffer", path, static_cast(cap - 1)); + return false; + } + + buf[n] = '\0'; + if (out_len) { + *out_len = n; + } + return true; +} + +bool storage_write_file(const char *path, const char *data, size_t len) +{ + if (!g_mounted) { + return false; + } + + FILE *f = fopen(path, "w"); + if (!f) { + ESP_LOGE(TAG, "fopen(%s, \"w\") failed", path); + return false; + } + + size_t written = fwrite(data, 1, len, f); + fclose(f); + return written == len; +} + +} // namespace kvida::drivers diff --git a/components/lua_runtime/README.md b/components/lua_runtime/README.md index a0080a9..8873630 100644 --- a/components/lua_runtime/README.md +++ b/components/lua_runtime/README.md @@ -6,20 +6,21 @@ Built on `espressif/lua` (v5.5.0, pinned in `idf_component.yml`) -- Espressif's ## What's here -- `lua_runtime_init()` creates the Lua state, opens only `base`/`table`/`string`/`math` (deliberately *not* `luaL_openlibs()`, which would also expose `io`, `os`, `package`/`require`, `debug` -- see AGENTS.md's "Lua should never access ESP-IDF directly"), registers the `kvida` API table, then loads the default script (`src/default_script.h`, a compiled-in string) which just *defines* `on_tick()`. +- `lua_runtime_init()` mounts storage (`drivers::storage_init()`) and tries to read `/storage/script.lua`; if none is stored yet (first boot, or the read fails), it falls back to the compiled-in default script (`src/default_script.h`). Either way it then builds the Lua state: opens only `base`/`table`/`string`/`math` (deliberately *not* `luaL_openlibs()`, which would also expose `io`, `os`, `package`/`require`, `debug` -- see AGENTS.md's "Lua should never access ESP-IDF directly"), registers the `kvida` API table, then runs the script, which just *defines* `on_tick()`. - `lua_runtime_tick()` calls `on_tick()` -- one "wake, execute Lua, publish changes, sleep" cycle. A Lua runtime error is logged, not fatal. +- `lua_runtime_reload(script, len)` persists a new script to storage and hot-reloads it into a fresh Lua state immediately -- no reboot. Wired to `transport::set_script_change_handler()` (POST /api/script) by `main.cpp`. `lua_runtime_current_script(buf, cap)` is the read side, wired to `transport::set_script_provider()` (GET /api/script). - The `kvida` table exposed to scripts: - `kvida.chip_temperature()` -- reads the ESP32-C6's internal die temperature via `profiles::ChipTemperatureSensor` (the same `Sensor` implementation `main.cpp` used directly before this component existed). - `kvida.set_led(r, g, b)` -- sets the onboard WS2812 via `drivers::rgb_led_set_color()`. - `kvida.publish(name, value)` -- calls `transport::publish_value()`, AGENTS.md's semantic publish API, built for the first time here. - The default script reads chip temperature, maps it to a blue(cool)-to-red(hot) LED color (linear interpolation, 20-60C, clamped), and publishes the reading to Home Assistant. Verified end-to-end on real hardware. -## Known limitation, accepted for now +## Known limitations, accepted for now -The onboard LED is also controllable manually from Home Assistant (an MQTT light entity, see `components/transport/src/mqtt.cpp`). The two aren't reconciled: `on_tick()` overwrites a manually-chosen HA color every 30s. Accepted as a conflict between two "quick proof" demos rather than solved now -- a real answer (e.g. a Lua-settable "mode" toggle, or the MQTT light entity deferring to Lua) needs product input, not a guess. +- The onboard LED is also controllable manually from Home Assistant (an MQTT light entity, see `components/transport/src/mqtt.cpp`). The two aren't reconciled: `on_tick()` overwrites a manually-chosen HA color every 30s. Accepted as a conflict between two "quick proof" demos rather than solved now -- a real answer (e.g. a Lua-settable "mode" toggle, or the MQTT light entity deferring to Lua) needs product input, not a guess. +- `lua_runtime_reload()` doesn't roll back to the last-good script if the new one fails to compile/run -- it just leaves `on_tick` undefined until the next successful reload (same behavior `lua_runtime_init()` already has for a bad default script). Storage and `lua_runtime_current_script()` always reflect the most recently POSTed script either way, even a broken one, so a user editing from `kvida-sdk` can see (and fix) exactly what they submitted. ## Not yet done -- The script is compiled in, not loaded from storage -- there's no upload mechanism yet. Once `kvida-sdk` can push a script (and `drivers` mounts the `storage` LittleFS partition -- both still TODO), scripts should be loaded from there instead. -- No Blockly-to-Lua compiler exists yet (that's `kvida-sdk`'s job per AGENTS.md's "Level 2"). -- No memory/CPU/runtime limits on the Lua state beyond the restricted library set -- a script with an infinite loop would hang `lua_runtime_tick()` (and, since it currently runs on the same timer callback, block other `esp_timer` callbacks too). Not a concern for a compiled-in, developer-authored script; becomes one once arbitrary user scripts can be uploaded. +- No Blockly-to-Lua compiler exists yet (that's `kvida-sdk`'s job per AGENTS.md's "Level 2") -- for now scripts are hand-written Lua, edited as raw text in `kvida-sdk`. +- No memory/CPU/runtime limits on the Lua state beyond the restricted library set -- a script with an infinite loop would hang `lua_runtime_tick()` (and, since it currently runs on the same timer callback, block other `esp_timer` callbacks too). Was a non-issue for a compiled-in, developer-authored script; now that scripts are user-editable and persisted, this is a real gap, just not one this pass solves. diff --git a/components/lua_runtime/include/lua_runtime.h b/components/lua_runtime/include/lua_runtime.h index 04feaed..c60ad0f 100644 --- a/components/lua_runtime/include/lua_runtime.h +++ b/components/lua_runtime/include/lua_runtime.h @@ -1,11 +1,14 @@ #pragma once +#include + namespace kvida { -// Creates the sandboxed Lua state and loads the default script (see -// src/default_script.h). The script only *defines* the on_tick() -// function -- call lua_runtime_tick() to actually run it. Call once at -// boot. +// Mounts storage and loads the script from there (see +// components/drivers/include/storage.h), falling back to the compiled-in +// default (src/default_script.h) if none is stored yet. The script only +// *defines* the on_tick() function -- call lua_runtime_tick() to actually +// run it. Call once at boot. void lua_runtime_init(); // Calls the script's on_tick() global function -- one "wake, execute @@ -14,4 +17,14 @@ void lua_runtime_init(); // down the device. void lua_runtime_tick(); +// Persists `script` to storage and hot-reloads it into a fresh Lua +// state, replacing whatever's currently running -- no reboot needed. +// Wired to transport::set_script_change_handler() by main.cpp. +void lua_runtime_reload(const char *script, size_t len); + +// Copies the currently-running script into buf (capacity cap), returns +// the number of bytes written. Wired to transport::set_script_provider() +// by main.cpp. +size_t lua_runtime_current_script(char *buf, size_t cap); + } // namespace kvida diff --git a/components/lua_runtime/src/lua_runtime.cpp b/components/lua_runtime/src/lua_runtime.cpp index 443913f..198246e 100644 --- a/components/lua_runtime/src/lua_runtime.cpp +++ b/components/lua_runtime/src/lua_runtime.cpp @@ -1,10 +1,14 @@ #include "lua_runtime.h" +#include +#include + #include "default_script.h" #include "chip_temperature_sensor.h" #include "mqtt.h" #include "rgb_led.h" +#include "storage.h" #include "esp_log.h" @@ -19,10 +23,18 @@ namespace kvida { namespace { constexpr const char *TAG = "lua_runtime"; +constexpr const char *kScriptPath = "/storage/script.lua"; lua_State *g_L = nullptr; ChipTemperatureSensor g_chip_temp_sensor; +// Holds whatever script is currently loaded into g_L -- either read from +// storage at boot, the compiled-in default (no stored script yet, or the +// read failed), or the most recent successful lua_runtime_reload() body. +// This is what GET /api/script (via lua_runtime_current_script()) serves. +char g_script[8192]; +size_t g_script_len = 0; + // Deliberately not luaL_openlibs(), which would also expose `io`, `os`, // `package`/`require`, and `debug` -- AGENTS.md: "Lua should never // access ESP-IDF directly. Lua interacts only through the Kvida API." @@ -85,6 +97,50 @@ void register_kvida_api(lua_State *L) lua_setglobal(L, "kvida"); } +// Copies into g_script/g_script_len, truncating (with a logged warning) +// if it doesn't fit -- the same fixed buffer backs both what gets +// persisted to storage and what GET /api/script serves back. +void copy_script(const char *script, size_t len) +{ + size_t n = std::min(len, sizeof(g_script) - 1); + if (n < len) { + ESP_LOGW(TAG, "script truncated from %u to %u bytes to fit the buffer", static_cast(len), + static_cast(n)); + } + memcpy(g_script, script, n); + g_script[n] = '\0'; + g_script_len = n; +} + +// (Re)creates the Lua state and runs `script` in it -- shared by both +// lua_runtime_init() and lua_runtime_reload(). Uses luaL_loadbuffer() +// (explicit length) rather than luaL_dostring() (relies on strlen()) +// since callers -- e.g. the POST /api/script body -- don't guarantee +// NUL-termination. +bool load_script(const char *script, size_t len) +{ + if (g_L) { + lua_close(g_L); + g_L = nullptr; + } + + g_L = luaL_newstate(); + if (!g_L) { + ESP_LOGE(TAG, "luaL_newstate failed"); + return false; + } + + open_safe_libs(g_L); + register_kvida_api(g_L); + + if (luaL_loadbuffer(g_L, script, len, "script") != LUA_OK || lua_pcall(g_L, 0, 0, 0) != LUA_OK) { + ESP_LOGE(TAG, "Failed to load script: %s", lua_tostring(g_L, -1)); + lua_pop(g_L, 1); + return false; + } + return true; +} + } // namespace void lua_runtime_init() @@ -93,20 +149,36 @@ void lua_runtime_init() return; } - g_L = luaL_newstate(); - if (!g_L) { - ESP_LOGE(TAG, "luaL_newstate failed"); - return; - } - - open_safe_libs(g_L); + drivers::storage_init(); g_chip_temp_sensor.begin(); - register_kvida_api(g_L); - if (luaL_dostring(g_L, default_script()) != LUA_OK) { - ESP_LOGE(TAG, "Failed to load default script: %s", lua_tostring(g_L, -1)); - lua_pop(g_L, 1); + size_t stored_len = 0; + if (!drivers::storage_read_file(kScriptPath, g_script, sizeof(g_script), &stored_len)) { + copy_script(default_script(), strlen(default_script())); + } else { + g_script_len = stored_len; } + + load_script(g_script, g_script_len); +} + +// No rollback to the last-good script if the new one fails to +// compile/run -- accepted simplification for now (same as +// lua_runtime_init()'s handling of a bad default script), see the +// README. g_script/storage always reflect the most recent POST either +// way, so the user can see (and fix) what they submitted. +void lua_runtime_reload(const char *script, size_t len) +{ + copy_script(script, len); + drivers::storage_write_file(kScriptPath, g_script, g_script_len); + load_script(g_script, g_script_len); +} + +size_t lua_runtime_current_script(char *buf, size_t cap) +{ + size_t n = std::min(g_script_len, cap); + memcpy(buf, g_script, n); + return n; } void lua_runtime_tick() diff --git a/components/transport/README.md b/components/transport/README.md index 7c4ee02..551915c 100644 --- a/components/transport/README.md +++ b/components/transport/README.md @@ -34,6 +34,7 @@ Both httpd servers (the commissioning AP's and the settings server's) expose a s - `POST /api/wifi` (commissioning AP only) -- `{"ssid","password"}`, replaces the old form-urlencoded `/connect`. - `GET /api/status` (settings server) -- `{"device_id","wifi_connected","mqtt_connected"}`. - `GET /api/mqtt` / `POST /api/mqtt` (settings server) -- `{"host","port","username"[,"password"]}`; `GET` never returns the password. +- `GET /api/script` / `POST /api/script` (settings server) -- the one endpoint in this API that is **not** JSON: the body is the raw Lua source as `text/plain`, both directions. Deliberate, not an oversight -- `json_helpers.h` is explicitly a non-escaping minimal parser, and Lua scripts routinely contain `"`, `\`, and newlines it can't round-trip safely. `transport` doesn't store or interpret the script itself (would mean depending on `lua_runtime`, breaking this component's "no dependency on other Kvida components" rule) -- it just relays through two function-pointer callbacks main.cpp wires up, `set_script_change_handler`/`set_script_provider`, the same pattern already used for `LedColorHandler`. `POST` reads the body in a loop up to an 8KB cap (`413` beyond that) since, unlike the small fixed-shape JSON bodies elsewhere here, `httpd_req_recv()` isn't guaranteed to return a script-sized body in one call. No JSON library ships with ESP-IDF v6.0.2 core (verified: no `cJSON`, no bundled `json` component). Given these payloads are tiny, flat, fixed-shape objects, `src/json_helpers.h` hand-rolls minimal `strstr`-based extraction rather than adding a 4th external registry dependency (already have `mdns`, `mqtt`, `led_strip`) -- explicitly not a general parser (no nesting, arrays, or escaping beyond what these specific request bodies need). diff --git a/components/transport/include/mqtt.h b/components/transport/include/mqtt.h index 36b9b8c..b863c5b 100644 --- a/components/transport/include/mqtt.h +++ b/components/transport/include/mqtt.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace kvida { @@ -29,4 +30,17 @@ void publish_value(const char *name, double value); using LedColorHandler = void (*)(uint8_t red, uint8_t green, uint8_t blue); void set_led_color_handler(LedColorHandler handler); +// Same no-dependency-on-other-components pattern as LedColorHandler: +// registered by main so a POST to /api/script can hand the new script to +// `lua_runtime` without transport depending on it. `script` is not +// NUL-terminated by the caller's guarantee -- handlers must use `len`. +using ScriptChangeHandler = void (*)(const char *script, size_t len); +void set_script_change_handler(ScriptChangeHandler handler); + +// Registered by main so GET /api/script can pull the currently-running +// script from `lua_runtime`. Called with a buffer of size `cap`; returns +// the number of bytes written (0 if no provider is registered yet). +using ScriptProvider = size_t (*)(char *buf, size_t cap); +void set_script_provider(ScriptProvider provider); + } // namespace kvida diff --git a/components/transport/src/mqtt.cpp b/components/transport/src/mqtt.cpp index 2e5ce20..ec1b6fc 100644 --- a/components/transport/src/mqtt.cpp +++ b/components/transport/src/mqtt.cpp @@ -38,6 +38,12 @@ esp_mqtt_client_handle_t g_client = nullptr; esp_timer_handle_t g_retry_timer = nullptr; httpd_handle_t g_settings_httpd = nullptr; LedColorHandler g_led_handler = nullptr; +ScriptChangeHandler g_script_change_handler = nullptr; +ScriptProvider g_script_provider = nullptr; + +// Matches lua_runtime's g_script buffer size -- a script that doesn't fit +// in either is rejected the same way. +constexpr size_t kMaxScriptSize = 8192; char g_availability_topic[48]; char g_discovery_topic[80]; @@ -451,6 +457,64 @@ esp_err_t api_mqtt_post_handler(httpd_req_t *req) return ESP_OK; } +// GET /api/script -- the currently-running Lua script, raw text (not +// JSON: see the POST handler's comment for why). +esp_err_t api_script_get_handler(httpd_req_t *req) +{ + add_cors_headers(req); + + if (!g_script_provider) { + httpd_resp_set_status(req, "503 Service Unavailable"); + httpd_resp_send(req, "", 0); + return ESP_OK; + } + + static char buf[kMaxScriptSize]; + size_t len = g_script_provider(buf, sizeof(buf)); + + httpd_resp_set_type(req, "text/plain"); + httpd_resp_send(req, buf, len); + return ESP_OK; +} + +// POST /api/script -- body is the raw Lua source, not JSON. Scripts +// routinely contain '"', '\', and newlines that json_helpers.h's +// non-escaping parser (see its own header comment) can't round-trip +// safely, so this endpoint skips JSON entirely. httpd_req_recv() isn't +// guaranteed to return the whole body in one call once it's more than a +// few hundred bytes (unlike the small fixed-shape JSON bodies elsewhere +// in this file), so this reads in a loop until content_len bytes are in. +esp_err_t api_script_post_handler(httpd_req_t *req) +{ + add_cors_headers(req); + + if (req->content_len == 0 || req->content_len >= kMaxScriptSize) { + httpd_resp_set_status(req, "413 Payload Too Large"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"error\":\"script too large\"}", HTTPD_RESP_USE_STRLEN); + return ESP_OK; + } + + static char body[kMaxScriptSize]; + size_t received = 0; + while (received < req->content_len) { + int ret = httpd_req_recv(req, body + received, req->content_len - received); + if (ret <= 0) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + received += static_cast(ret); + } + + if (g_script_change_handler) { + g_script_change_handler(body, received); + } + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, "{\"status\":\"reloaded\"}", HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + esp_err_t api_options_handler(httpd_req_t *req) { return cors_preflight_handler(req); @@ -486,7 +550,7 @@ void start_settings_server() httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.lru_purge_enable = true; - config.max_uri_handlers = 12; + config.max_uri_handlers = 15; if (httpd_start(&g_settings_httpd, &config) != ESP_OK) { ESP_LOGE(TAG, "Failed to start settings HTTP server"); return; @@ -502,11 +566,20 @@ void start_settings_server() .uri = "/api/mqtt", .method = HTTP_POST, .handler = api_mqtt_post_handler, .user_ctx = nullptr}; static const httpd_uri_t mqtt_options_uri = { .uri = "/api/mqtt", .method = HTTP_OPTIONS, .handler = api_options_handler, .user_ctx = nullptr}; + static const httpd_uri_t script_get_uri = { + .uri = "/api/script", .method = HTTP_GET, .handler = api_script_get_handler, .user_ctx = nullptr}; + static const httpd_uri_t script_post_uri = { + .uri = "/api/script", .method = HTTP_POST, .handler = api_script_post_handler, .user_ctx = nullptr}; + static const httpd_uri_t script_options_uri = { + .uri = "/api/script", .method = HTTP_OPTIONS, .handler = api_options_handler, .user_ctx = nullptr}; httpd_register_uri_handler(g_settings_httpd, &root_uri); httpd_register_uri_handler(g_settings_httpd, &status_uri); httpd_register_uri_handler(g_settings_httpd, &mqtt_get_uri); httpd_register_uri_handler(g_settings_httpd, &mqtt_post_uri); httpd_register_uri_handler(g_settings_httpd, &mqtt_options_uri); + httpd_register_uri_handler(g_settings_httpd, &script_get_uri); + httpd_register_uri_handler(g_settings_httpd, &script_post_uri); + httpd_register_uri_handler(g_settings_httpd, &script_options_uri); } } // namespace @@ -590,4 +663,14 @@ void set_led_color_handler(LedColorHandler handler) g_led_handler = handler; } +void set_script_change_handler(ScriptChangeHandler handler) +{ + g_script_change_handler = handler; +} + +void set_script_provider(ScriptProvider provider) +{ + g_script_provider = provider; +} + } // namespace kvida diff --git a/main/main.cpp b/main/main.cpp index 498c5be..0ecf784 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,3 +1,5 @@ +#include + #include "commissioning.h" #include "esp_log.h" #include "esp_timer.h" @@ -15,19 +17,33 @@ void on_led_color(uint8_t red, uint8_t green, uint8_t blue) kvida::drivers::rgb_led_set_color(red, green, blue); } +void on_script_change(const char *script, size_t len) +{ + kvida::lua_runtime_reload(script, len); +} + +size_t on_script_provider(char *buf, size_t cap) +{ + return kvida::lua_runtime_current_script(buf, cap); +} + } // namespace extern "C" void app_main(void) { ESP_LOGI(TAG, "kvida-os starting"); - // The default Lua script (components/lua_runtime/src/default_script.h) + // Runs whatever script is in storage (uploaded via kvida-sdk's + // editor), falling back to the compiled-in default + // (components/lua_runtime/src/default_script.h) on first boot -- // reads chip temperature, drives the LED, and publishes to Home - // Assistant -- see that component's README for what's real here vs. - // still a "quick proof" using only what's on the ESP32-C6-DevKitC-1. + // Assistant. See lua_runtime's README for what's real here vs. still + // a "quick proof" using only what's on the ESP32-C6-DevKitC-1. kvida::drivers::rgb_led_init(); kvida::lua_runtime_init(); kvida::set_led_color_handler(&on_led_color); + kvida::set_script_change_handler(&on_script_change); + kvida::set_script_provider(&on_script_provider); const esp_timer_create_args_t timer_args = { .callback = [](void *) { kvida::lua_runtime_tick(); },