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:
@@ -4,4 +4,16 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
# The espressif/lua component's luaconf.h defaults to 64-bit
|
||||||
|
# (long long) Lua integers and errors out at compile time on this
|
||||||
|
# toolchain ("Compiler does not support 'long long'" -- LLONG_MAX isn't
|
||||||
|
# visible to it here, even though <limits.h> is included). Lua's own
|
||||||
|
# suggested fix for constrained/32-bit platforms: build with 32-bit
|
||||||
|
# integers/floats instead. Set globally (not just in lua_runtime's own
|
||||||
|
# CMakeLists.txt) so the Lua component's own .c files see the same
|
||||||
|
# definition -- otherwise its library and our calling code would
|
||||||
|
# disagree on the size of lua_Integer.
|
||||||
|
idf_build_set_property(COMPILE_DEFINITIONS "-DLUA_32BITS" APPEND)
|
||||||
|
|
||||||
project(kvida_os)
|
project(kvida_os)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
idf_component_register(
|
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
|
# 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
|
||||||
@@ -25,7 +25,7 @@ Future transports (Zigbee, Thread, Matter) should implement the same publish API
|
|||||||
- **Broker address**: the settings page has a required host/port field, used directly and given priority over mDNS auto-discovery. mDNS discovery of `_mqtt._tcp` (`mdns_query_ptr`) is attempted as a fallback only if no manual host is saved, retried every 30s -- **verified against a real Home Assistant + Mosquitto add-on setup that this mDNS discovery does not reliably find the broker** (confirmed with a from-scratch raw mDNS query test from the host machine: no PTR answer for `_mqtt._tcp.local` was ever received on that network), hence the manual field being the primary path rather than a rarely-needed fallback.
|
- **Broker address**: the settings page has a required host/port field, used directly and given priority over mDNS auto-discovery. mDNS discovery of `_mqtt._tcp` (`mdns_query_ptr`) is attempted as a fallback only if no manual host is saved, retried every 30s -- **verified against a real Home Assistant + Mosquitto add-on setup that this mDNS discovery does not reliably find the broker** (confirmed with a from-scratch raw mDNS query test from the host machine: no PTR answer for `_mqtt._tcp.local` was ever received on that network), hence the manual field being the primary path rather than a rarely-needed fallback.
|
||||||
- **Credentials**: also collected on the same settings page (blank username = anonymous; blank password on a resubmit means "keep the current one", since the password is never echoed back into the page for basic hygiene). Real brokers, including Home Assistant's Mosquitto add-on, require auth -- confirmed on real hardware.
|
- **Credentials**: also collected on the same settings page (blank username = anonymous; blank password on a resubmit means "keep the current one", since the password is never echoed back into the page for basic hygiene). Real brokers, including Home Assistant's Mosquitto add-on, require auth -- confirmed on real hardware.
|
||||||
- On `MQTT_EVENT_CONNECTED`: publishes (retained) an HA MQTT Discovery config for a diagnostic `connectivity` binary_sensor, grouped under a `device` block (`identifiers`/`name` = the same `kvida-xxxxxx` id, `manufacturer` "Xylon", `model` "Kvida"), then publishes `online` to the availability topic. MQTT's own LWT (`session.last_will`, set at client init) publishes `offline` to the same topic if the connection drops. Verified end-to-end: device appears in Home Assistant with a "Connected" Connectivity sensor.
|
- On `MQTT_EVENT_CONNECTED`: publishes (retained) an HA MQTT Discovery config for a diagnostic `connectivity` binary_sensor, grouped under a `device` block (`identifiers`/`name` = the same `kvida-xxxxxx` id, `manufacturer` "Xylon", `model` "Kvida"), then publishes `online` to the availability topic. MQTT's own LWT (`session.last_will`, set at client init) publishes `offline` to the same topic if the connection drops. Verified end-to-end: device appears in Home Assistant with a "Connected" Connectivity sensor.
|
||||||
- No generic semantic `publish(topic, value)` API yet -- deferred until `profiles` has real sensor data to publish; building it now would be speculative.
|
- **`publish_value(name, value)`**: the generic semantic publish API, finally built once there was a real caller -- `lua_runtime`'s `kvida.publish()`, not called directly by other components. Builds `kvida/<id>/<name>` (state) and `homeassistant/sensor/<id>/<name>/config` (discovery) topics from the name. No `device_class`/`unit_of_measurement` or prettified HA display name, since neither can be inferred from just a name -- the entity shows up in HA labeled with the raw name (e.g. `chip_temperature`).
|
||||||
|
|
||||||
## JSON API
|
## JSON API
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,21 @@ namespace kvida {
|
|||||||
// Starts mDNS-based MQTT broker discovery (_mqtt._tcp) and, once found,
|
// Starts mDNS-based MQTT broker discovery (_mqtt._tcp) and, once found,
|
||||||
// connects the MQTT client and publishes Home Assistant MQTT Discovery
|
// connects the MQTT client and publishes Home Assistant MQTT Discovery
|
||||||
// for a diagnostic "connectivity" entity. Safe to call multiple times
|
// for a diagnostic "connectivity" entity. Safe to call multiple times
|
||||||
// (no-op if already connecting/connected). No public-facing semantic
|
// (no-op if already connecting/connected).
|
||||||
// publish(topic, value) API yet -- deferred until `profiles` has real
|
|
||||||
// sensor data to publish (see components/transport/README.md).
|
|
||||||
void start_mqtt();
|
void start_mqtt();
|
||||||
|
|
||||||
bool mqtt_connected();
|
bool mqtt_connected();
|
||||||
|
|
||||||
// Publishes (retained) Home Assistant MQTT Discovery for a diagnostic
|
// Publishes a named numeric value as a Home Assistant MQTT Discovery
|
||||||
// "Chip temperature" sensor on first call, then the reading itself.
|
// sensor: `kvida/<id>/<name>` state topic, `homeassistant/sensor/<id>/<name>/config`
|
||||||
// Quick end-to-end proof that a real Sensor implementation's data can
|
// discovery topic. This is the semantic publish(name, value) API AGENTS.md
|
||||||
// reach MQTT/HA -- not the eventual generic publish(topic, value) API.
|
// describes -- driven by `lua_runtime`'s `kvida.publish()`, not called
|
||||||
void publish_chip_temperature(float celsius);
|
// directly by other components. Discovery is republished (retained, so
|
||||||
|
// idempotent) on every call rather than cached behind a per-name
|
||||||
|
// "already announced" flag -- simpler, at the cost of a bit of extra
|
||||||
|
// retained-message traffic each call. TODO: cache announced names if
|
||||||
|
// that traffic becomes a real concern.
|
||||||
|
void publish_value(const char *name, double value);
|
||||||
|
|
||||||
// Registered by main to receive "set LED color" commands from Home
|
// Registered by main to receive "set LED color" commands from Home
|
||||||
// Assistant (an RGB light entity) without transport depending on
|
// Assistant (an RGB light entity) without transport depending on
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ char g_availability_topic[48];
|
|||||||
char g_discovery_topic[80];
|
char g_discovery_topic[80];
|
||||||
char g_discovery_payload[512];
|
char g_discovery_payload[512];
|
||||||
|
|
||||||
char g_temp_state_topic[48];
|
|
||||||
char g_led_command_topic[48];
|
char g_led_command_topic[48];
|
||||||
char g_led_rgb_command_topic[48];
|
char g_led_rgb_command_topic[48];
|
||||||
char g_led_state_topic[48];
|
char g_led_state_topic[48];
|
||||||
@@ -537,7 +536,7 @@ bool mqtt_connected()
|
|||||||
return g_connected.load();
|
return g_connected.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void publish_chip_temperature(float celsius)
|
void publish_value(const char *name, double value)
|
||||||
{
|
{
|
||||||
if (!g_client || !g_connected.load()) {
|
if (!g_client || !g_connected.load()) {
|
||||||
return;
|
return;
|
||||||
@@ -545,42 +544,45 @@ void publish_chip_temperature(float celsius)
|
|||||||
|
|
||||||
char id[16];
|
char id[16];
|
||||||
device_id(id, sizeof(id));
|
device_id(id, sizeof(id));
|
||||||
snprintf(g_temp_state_topic, sizeof(g_temp_state_topic), "kvida/%s/chip_temperature", id);
|
|
||||||
|
|
||||||
static bool discovery_published = false;
|
char state_topic[64];
|
||||||
if (!discovery_published) {
|
snprintf(state_topic, sizeof(state_topic), "kvida/%s/%s", id, name);
|
||||||
char discovery_topic[80];
|
|
||||||
snprintf(discovery_topic, sizeof(discovery_topic),
|
|
||||||
"homeassistant/sensor/%s/chip_temperature/config", id);
|
|
||||||
|
|
||||||
char payload[512];
|
// Discovery is republished (retained, so idempotent) every call rather
|
||||||
snprintf(payload, sizeof(payload),
|
// than cached behind a per-name "already announced" flag -- see the
|
||||||
"{"
|
// comment on publish_value() in mqtt.h.
|
||||||
"\"name\":\"Chip temperature\","
|
char discovery_topic[96];
|
||||||
"\"device_class\":\"temperature\","
|
snprintf(discovery_topic, sizeof(discovery_topic), "homeassistant/sensor/%s/%s/config", id, name);
|
||||||
"\"unit_of_measurement\":\"\xc2\xb0"
|
|
||||||
"C\","
|
|
||||||
"\"unique_id\":\"%s_chip_temperature\","
|
|
||||||
"\"state_topic\":\"%s\","
|
|
||||||
"\"availability_topic\":\"%s\","
|
|
||||||
"\"payload_available\":\"online\","
|
|
||||||
"\"payload_not_available\":\"offline\","
|
|
||||||
"\"device\":{"
|
|
||||||
"\"identifiers\":[\"%s\"],"
|
|
||||||
"\"name\":\"%s\","
|
|
||||||
"\"manufacturer\":\"Xylon\","
|
|
||||||
"\"model\":\"Kvida\""
|
|
||||||
"}"
|
|
||||||
"}",
|
|
||||||
id, g_temp_state_topic, g_availability_topic, id, id);
|
|
||||||
|
|
||||||
esp_mqtt_client_publish(g_client, discovery_topic, payload, 0, 1, true);
|
// No device_class/unit_of_measurement: this is a generic named-value
|
||||||
discovery_published = true;
|
// API (Lua's kvida.publish(name, value)), so there's no reliable way
|
||||||
}
|
// to infer units from just a name. The entity's HA display name is
|
||||||
|
// also just the raw `name` (e.g. "chip_temperature") for the same
|
||||||
|
// reason -- a future version could thread a prettier label through
|
||||||
|
// once profiles pass richer metadata.
|
||||||
|
char payload[512];
|
||||||
|
snprintf(payload, sizeof(payload),
|
||||||
|
"{"
|
||||||
|
"\"name\":\"%s\","
|
||||||
|
"\"unique_id\":\"%s_%s\","
|
||||||
|
"\"state_topic\":\"%s\","
|
||||||
|
"\"availability_topic\":\"%s\","
|
||||||
|
"\"payload_available\":\"online\","
|
||||||
|
"\"payload_not_available\":\"offline\","
|
||||||
|
"\"device\":{"
|
||||||
|
"\"identifiers\":[\"%s\"],"
|
||||||
|
"\"name\":\"%s\","
|
||||||
|
"\"manufacturer\":\"Xylon\","
|
||||||
|
"\"model\":\"Kvida\""
|
||||||
|
"}"
|
||||||
|
"}",
|
||||||
|
name, id, name, state_topic, g_availability_topic, id, id);
|
||||||
|
|
||||||
char value[16];
|
esp_mqtt_client_publish(g_client, discovery_topic, payload, 0, 1, true);
|
||||||
snprintf(value, sizeof(value), "%.1f", static_cast<double>(celsius));
|
|
||||||
esp_mqtt_client_publish(g_client, g_temp_state_topic, value, 0, 0, false);
|
char value_str[32];
|
||||||
|
snprintf(value_str, sizeof(value_str), "%.2f", value);
|
||||||
|
esp_mqtt_client_publish(g_client, state_topic, value_str, 0, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_led_color_handler(LedColorHandler handler)
|
void set_led_color_handler(LedColorHandler handler)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.cpp"
|
SRCS "main.cpp"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES drivers sensors profiles transport lua_runtime esp_timer
|
REQUIRES drivers transport lua_runtime esp_timer
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
#include "chip_temperature_sensor.h"
|
|
||||||
#include "commissioning.h"
|
#include "commissioning.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
#include "lua_runtime.h"
|
||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "rgb_led.h"
|
#include "rgb_led.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char *TAG = "kvida_os";
|
constexpr const char *TAG = "kvida_os";
|
||||||
|
|
||||||
kvida::ChipTemperatureSensor g_chip_temp_sensor;
|
esp_timer_handle_t g_lua_tick_timer = nullptr;
|
||||||
esp_timer_handle_t g_chip_temp_timer = nullptr;
|
|
||||||
|
|
||||||
void publish_chip_temperature_tick(void * /*arg*/)
|
|
||||||
{
|
|
||||||
if (!g_chip_temp_sensor.fetch()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
kvida::SensorValue value{};
|
|
||||||
if (g_chip_temp_sensor.get(kvida::SensorChannel::ChipTemperature, value)) {
|
|
||||||
float celsius = static_cast<float>(value.val1) + static_cast<float>(value.val2) * 1e-6f;
|
|
||||||
kvida::publish_chip_temperature(celsius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_led_color(uint8_t red, uint8_t green, uint8_t blue)
|
void on_led_color(uint8_t red, uint8_t green, uint8_t blue)
|
||||||
{
|
{
|
||||||
@@ -34,22 +21,23 @@ extern "C" void app_main(void)
|
|||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "kvida-os starting");
|
ESP_LOGI(TAG, "kvida-os starting");
|
||||||
|
|
||||||
// Quick end-to-end data-flow proof (real Sensor impl -> MQTT/HA, and
|
// The default Lua script (components/lua_runtime/src/default_script.h)
|
||||||
// HA -> MQTT -> actuator) using only what's on the ESP32-C6-DevKitC-1
|
// reads chip temperature, drives the LED, and publishes to Home
|
||||||
// itself: no sensor profiles or hardware wiring exist yet.
|
// Assistant -- see that component's README for what's real here vs.
|
||||||
g_chip_temp_sensor.begin();
|
// still a "quick proof" using only what's on the ESP32-C6-DevKitC-1.
|
||||||
kvida::drivers::rgb_led_init();
|
kvida::drivers::rgb_led_init();
|
||||||
|
kvida::lua_runtime_init();
|
||||||
kvida::set_led_color_handler(&on_led_color);
|
kvida::set_led_color_handler(&on_led_color);
|
||||||
|
|
||||||
const esp_timer_create_args_t timer_args = {
|
const esp_timer_create_args_t timer_args = {
|
||||||
.callback = &publish_chip_temperature_tick,
|
.callback = [](void *) { kvida::lua_runtime_tick(); },
|
||||||
.arg = nullptr,
|
.arg = nullptr,
|
||||||
.dispatch_method = ESP_TIMER_TASK,
|
.dispatch_method = ESP_TIMER_TASK,
|
||||||
.name = "chip_temp_publish",
|
.name = "lua_tick",
|
||||||
.skip_unhandled_events = true,
|
.skip_unhandled_events = true,
|
||||||
};
|
};
|
||||||
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &g_chip_temp_timer));
|
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &g_lua_tick_timer));
|
||||||
ESP_ERROR_CHECK(esp_timer_start_periodic(g_chip_temp_timer, 30LL * 1000000));
|
ESP_ERROR_CHECK(esp_timer_start_periodic(g_lua_tick_timer, 30LL * 1000000));
|
||||||
|
|
||||||
// First boot / no stored Wi-Fi credentials -> fall straight into
|
// First boot / no stored Wi-Fi credentials -> fall straight into
|
||||||
// commissioning (nothing to protect yet, so no physical-interaction
|
// commissioning (nothing to protect yet, so no physical-interaction
|
||||||
|
|||||||
Reference in New Issue
Block a user