Files
kvida-os/components/lua_runtime
Ronny Eia 967fecbf69 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>
2026-07-12 10:27:00 +02:00
..

lua_runtime

Executes the sandboxed Lua program (compiled from Blockly) that defines device behaviour. Exposes the Kvida API (e.g. publish("temperature", 21.3)) to Lua scripts; Lua never touches ESP-IDF or transport directly. REQUIRES drivers profiles transport.

Built on espressif/lua (v5.5.0, pinned in idf_component.yml) -- Espressif's own official IDF Component Registry package, MIT licensed, built specifically for embedding Lua in ESP-IDF apps. No JS/exotic bindings: standard lua_State/luaL_newstate/lua_pushcfunction C API. The project's fourth external managed component (after mdns, mqtt, led_strip).

What's here

  • 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 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.
  • 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

  • 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.