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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies:
|
||||
espressif/led_strip:
|
||||
version: "*"
|
||||
joltwallet/littlefs: "1.22.2"
|
||||
|
||||
21
components/drivers/include/storage.h
Normal file
21
components/drivers/include/storage.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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
|
||||
79
components/drivers/src/storage.cpp
Normal file
79
components/drivers/src/storage.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "storage.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#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<unsigned>(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
|
||||
@@ -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.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
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
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#include "lua_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#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<unsigned>(len),
|
||||
static_cast<unsigned>(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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
|
||||
@@ -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<size_t>(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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <cstddef>
|
||||
|
||||
#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(); },
|
||||
|
||||
Reference in New Issue
Block a user