Add Lua runtime: chip temp -> LED color -> HA, verified live
Sandboxed Lua state (espressif/lua v5.5.0, base/table/string/math only, no io/os/package/debug) exposes a kvida API table (chip_temperature, set_led, publish) to a compiled-in default script that reads chip temperature, maps it to an LED color, and publishes the reading to Home Assistant. Generalizes transport's publish_chip_temperature() into publish_value(name, value), the first real use of AGENTS.md's semantic publish API. Requires -DLUA_32BITS globally since this toolchain's long long support isn't visible to Lua's default build. Verified on real ESP32-C6 hardware: no crashes/errors across multiple serial monitor windows, LED changes color, chip_temperature sensor appears in Home Assistant via MQTT Discovery. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
idf_component_register(
|
||||
REQUIRES profiles transport
|
||||
SRCS "src/lua_runtime.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_INCLUDE_DIRS "src"
|
||||
REQUIRES drivers profiles transport
|
||||
PRIV_REQUIRES espressif__lua
|
||||
)
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# 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 profiles transport`.
|
||||
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`.
|
||||
|
||||
TODO: needs an actual Lua interpreter dependency, pinned via `idf_component.yml` once a specific IDF Component Registry package/version is confirmed (not guessed here, same caution as pinning `west.yml`'s NCS revision previously).
|
||||
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()` 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_tick()` calls `on_tick()` -- one "wake, execute Lua, publish changes, sleep" cycle. A Lua runtime error is logged, not fatal.
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
2
components/lua_runtime/idf_component.yml
Normal file
2
components/lua_runtime/idf_component.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/lua: "==5.5.0"
|
||||
17
components/lua_runtime/include/lua_runtime.h
Normal file
17
components/lua_runtime/include/lua_runtime.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
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.
|
||||
void lua_runtime_init();
|
||||
|
||||
// Calls the script's on_tick() global function -- one "wake, execute
|
||||
// Lua, publish changes, sleep" cycle, per AGENTS.md's power philosophy.
|
||||
// A Lua runtime error is logged, not fatal: a script bug shouldn't take
|
||||
// down the device.
|
||||
void lua_runtime_tick();
|
||||
|
||||
} // namespace kvida
|
||||
28
components/lua_runtime/src/default_script.h
Normal file
28
components/lua_runtime/src/default_script.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
namespace kvida {
|
||||
|
||||
// Default on-device script, compiled in for now -- there's no upload
|
||||
// mechanism yet (that's a kvida-sdk + LittleFS concern for later), so a
|
||||
// real filesystem-backed script store would be premature. Demonstrates
|
||||
// the whole point of lua_runtime: read a sensor, drive an actuator, and
|
||||
// publish a value to Home Assistant, entirely from Lua, with no
|
||||
// firmware rebuild for behavior changes.
|
||||
inline const char *default_script()
|
||||
{
|
||||
return R"lua(
|
||||
function on_tick()
|
||||
local temp = kvida.chip_temperature()
|
||||
|
||||
local min_temp, max_temp = 20, 60
|
||||
local t = (temp - min_temp) / (max_temp - min_temp)
|
||||
if t < 0 then t = 0 end
|
||||
if t > 1 then t = 1 end
|
||||
|
||||
kvida.set_led(math.floor(t * 255), 0, math.floor((1 - t) * 255))
|
||||
kvida.publish("chip_temperature", temp)
|
||||
end
|
||||
)lua";
|
||||
}
|
||||
|
||||
} // namespace kvida
|
||||
130
components/lua_runtime/src/lua_runtime.cpp
Normal file
130
components/lua_runtime/src/lua_runtime.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "lua_runtime.h"
|
||||
|
||||
#include "default_script.h"
|
||||
|
||||
#include "chip_temperature_sensor.h"
|
||||
#include "mqtt.h"
|
||||
#include "rgb_led.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
extern "C" {
|
||||
#include "lauxlib.h"
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
}
|
||||
|
||||
namespace kvida {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *TAG = "lua_runtime";
|
||||
|
||||
lua_State *g_L = nullptr;
|
||||
ChipTemperatureSensor g_chip_temp_sensor;
|
||||
|
||||
// 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."
|
||||
void open_safe_libs(lua_State *L)
|
||||
{
|
||||
luaL_requiref(L, LUA_GNAME, luaopen_base, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1);
|
||||
lua_pop(L, 1);
|
||||
luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
int l_chip_temperature(lua_State *L)
|
||||
{
|
||||
if (!g_chip_temp_sensor.fetch()) {
|
||||
return luaL_error(L, "failed to read chip temperature");
|
||||
}
|
||||
SensorValue value{};
|
||||
if (!g_chip_temp_sensor.get(SensorChannel::ChipTemperature, value)) {
|
||||
return luaL_error(L, "chip temperature channel unavailable");
|
||||
}
|
||||
double celsius = static_cast<double>(value.val1) + static_cast<double>(value.val2) * 1e-6;
|
||||
lua_pushnumber(L, celsius);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int l_set_led(lua_State *L)
|
||||
{
|
||||
auto r = static_cast<uint8_t>(luaL_checkinteger(L, 1));
|
||||
auto g = static_cast<uint8_t>(luaL_checkinteger(L, 2));
|
||||
auto b = static_cast<uint8_t>(luaL_checkinteger(L, 3));
|
||||
drivers::rgb_led_set_color(r, g, b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int l_publish(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
double value = luaL_checknumber(L, 2);
|
||||
publish_value(name, value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void register_kvida_api(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, l_chip_temperature);
|
||||
lua_setfield(L, -2, "chip_temperature");
|
||||
|
||||
lua_pushcfunction(L, l_set_led);
|
||||
lua_setfield(L, -2, "set_led");
|
||||
|
||||
lua_pushcfunction(L, l_publish);
|
||||
lua_setfield(L, -2, "publish");
|
||||
|
||||
lua_setglobal(L, "kvida");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void lua_runtime_init()
|
||||
{
|
||||
if (g_L) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_L = luaL_newstate();
|
||||
if (!g_L) {
|
||||
ESP_LOGE(TAG, "luaL_newstate failed");
|
||||
return;
|
||||
}
|
||||
|
||||
open_safe_libs(g_L);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void lua_runtime_tick()
|
||||
{
|
||||
if (!g_L) {
|
||||
return;
|
||||
}
|
||||
|
||||
lua_getglobal(g_L, "on_tick");
|
||||
if (!lua_isfunction(g_L, -1)) {
|
||||
lua_pop(g_L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lua_pcall(g_L, 0, 0, 0) != LUA_OK) {
|
||||
ESP_LOGE(TAG, "Lua error in on_tick: %s", lua_tostring(g_L, -1));
|
||||
lua_pop(g_L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kvida
|
||||
Reference in New Issue
Block a user