Add chip temperature sensor and RGB LED control, verified live in HA
Quick end-to-end data-flow proof using only what's on the ESP32-C6- DevKitC-1 itself (no sensors on the bare devkit -- confirmed via research: BOOT button, WS2812 LED, no external sensors): - drivers::chip_temperature_* wraps ESP-IDF's built-in temperature_sensor driver (SoC die temperature). - drivers::rgb_led_* wraps the onboard WS2812 (GPIO8) via the espressif/led_strip managed component (another external registry dependency, like mdns/mqtt). - profiles::ChipTemperatureSensor is the first real implementation of the Sensor interface designed earlier, converting the driver's float reading to the fixed-point convention once at the boundary. - transport/mqtt.cpp publishes chip temperature as an HA "temperature" sensor every 30s, and exposes the LED as an HA "light" entity (rgb_command_topic) -- the first use of MQTT subscribe in this project, not just publish. Added a ChipTemperature SensorChannel (distinct from AmbientTemperature -- different meaning/range) to sensor.h. Verified live: both the chip temperature reading and LED color control work from Home Assistant against the real device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
idf_component_register()
|
||||
|
||||
idf_component_register(
|
||||
SRCS "src/chip_temperature.cpp" "src/rgb_led.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_REQUIRES esp_driver_tsens espressif__led_strip
|
||||
)
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
|
||||
Hardware abstraction: wraps ESP32-C6 peripherals (GPIO, ADC, I2C) behind a board-independent interface used by `sensors`. No dependency on other Kvida components.
|
||||
|
||||
- `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`.
|
||||
|
||||
3
components/drivers/idf_component.yml
Normal file
3
components/drivers/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
espressif/led_strip:
|
||||
version: "*"
|
||||
11
components/drivers/include/chip_temperature.h
Normal file
11
components/drivers/include/chip_temperature.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace kvida::drivers {
|
||||
|
||||
// Wraps ESP-IDF's temperature_sensor driver (die temperature, not
|
||||
// ambient -- there is no external temperature sensor on the ESP32-C6
|
||||
// DevKitC). Call init() once before read_celsius().
|
||||
void chip_temperature_init();
|
||||
float chip_temperature_read_celsius();
|
||||
|
||||
} // namespace kvida::drivers
|
||||
11
components/drivers/include/rgb_led.h
Normal file
11
components/drivers/include/rgb_led.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace kvida::drivers {
|
||||
|
||||
// Wraps the WS2812 addressable RGB LED on the ESP32-C6-DevKitC-1 (GPIO8).
|
||||
void rgb_led_init();
|
||||
void rgb_led_set_color(uint8_t red, uint8_t green, uint8_t blue);
|
||||
|
||||
} // namespace kvida::drivers
|
||||
37
components/drivers/src/chip_temperature.cpp
Normal file
37
components/drivers/src/chip_temperature.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "chip_temperature.h"
|
||||
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
namespace kvida::drivers {
|
||||
|
||||
namespace {
|
||||
constexpr const char *TAG = "chip_temperature";
|
||||
temperature_sensor_handle_t g_handle = nullptr;
|
||||
} // namespace
|
||||
|
||||
void chip_temperature_init()
|
||||
{
|
||||
if (g_handle) {
|
||||
return;
|
||||
}
|
||||
temperature_sensor_config_t cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
|
||||
ESP_ERROR_CHECK(temperature_sensor_install(&cfg, &g_handle));
|
||||
ESP_ERROR_CHECK(temperature_sensor_enable(g_handle));
|
||||
}
|
||||
|
||||
float chip_temperature_read_celsius()
|
||||
{
|
||||
if (!g_handle) {
|
||||
ESP_LOGE(TAG, "chip_temperature_read_celsius() called before chip_temperature_init()");
|
||||
return 0.0f;
|
||||
}
|
||||
float celsius = 0.0f;
|
||||
esp_err_t err = temperature_sensor_get_celsius(g_handle, &celsius);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "temperature_sensor_get_celsius failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return celsius;
|
||||
}
|
||||
|
||||
} // namespace kvida::drivers
|
||||
47
components/drivers/src/rgb_led.cpp
Normal file
47
components/drivers/src/rgb_led.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "rgb_led.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "led_strip.h"
|
||||
|
||||
namespace kvida::drivers {
|
||||
|
||||
namespace {
|
||||
constexpr const char *TAG = "rgb_led";
|
||||
constexpr int kGpio = 8; // WS2812 on ESP32-C6-DevKitC-1
|
||||
led_strip_handle_t g_strip = nullptr;
|
||||
} // namespace
|
||||
|
||||
void rgb_led_init()
|
||||
{
|
||||
if (g_strip) {
|
||||
return;
|
||||
}
|
||||
|
||||
led_strip_config_t strip_config = {};
|
||||
strip_config.strip_gpio_num = kGpio;
|
||||
strip_config.max_leds = 1;
|
||||
strip_config.led_model = LED_MODEL_WS2812;
|
||||
strip_config.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB;
|
||||
|
||||
led_strip_rmt_config_t rmt_config = {};
|
||||
rmt_config.resolution_hz = 10 * 1000 * 1000;
|
||||
|
||||
esp_err_t err = led_strip_new_rmt_device(&strip_config, &rmt_config, &g_strip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "led_strip_new_rmt_device failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
led_strip_clear(g_strip);
|
||||
}
|
||||
|
||||
void rgb_led_set_color(uint8_t red, uint8_t green, uint8_t blue)
|
||||
{
|
||||
if (!g_strip) {
|
||||
ESP_LOGE(TAG, "rgb_led_set_color() called before rgb_led_init()");
|
||||
return;
|
||||
}
|
||||
led_strip_set_pixel(g_strip, 0, red, green, blue);
|
||||
led_strip_refresh(g_strip);
|
||||
}
|
||||
|
||||
} // namespace kvida::drivers
|
||||
@@ -1,3 +1,5 @@
|
||||
idf_component_register(
|
||||
REQUIRES sensors
|
||||
SRCS "src/chip_temperature_sensor.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES sensors drivers
|
||||
)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# profiles
|
||||
|
||||
Exposes reusable sensor/actuator capabilities (reed switch, hall sensor, push button, pulse counter, analog input, DS18B20, BME280, leak sensor, PIR, generic I2C device) to the Lua runtime, backed by `sensors`. `REQUIRES sensors`.
|
||||
|
||||
Concrete `Sensor` implementations live here (not in `sensors`, which owns the shared interface/types), and reach down to `drivers` directly for the actual hardware access -- `sensors` is the shared contract, not a mandatory pass-through. `REQUIRES drivers` too.
|
||||
|
||||
- `ChipTemperatureSensor` (`include/chip_temperature_sensor.h`) -- the first real `Sensor` implementation, wrapping `drivers::chip_temperature_*` (the ESP32-C6's internal die temperature). Added to get real data flowing end-to-end (drivers -> profiles -> transport/MQTT) before more sensor types exist.
|
||||
|
||||
28
components/profiles/include/chip_temperature_sensor.h
Normal file
28
components/profiles/include/chip_temperature_sensor.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "sensor.h"
|
||||
|
||||
namespace kvida {
|
||||
|
||||
// First real implementation of the Sensor interface (see
|
||||
// components/sensors/include/sensor.h) -- wraps the ESP32-C6's internal
|
||||
// die temperature sensor (kvida::drivers::chip_temperature_*).
|
||||
class ChipTemperatureSensor : public Sensor {
|
||||
public:
|
||||
const char *id() const override { return "chip_temperature"; }
|
||||
void begin() override;
|
||||
|
||||
bool supports(SensorChannel channel) const override
|
||||
{
|
||||
return channel == SensorChannel::ChipTemperature;
|
||||
}
|
||||
|
||||
bool fetch() override;
|
||||
bool get(SensorChannel channel, SensorValue &out) override;
|
||||
|
||||
private:
|
||||
bool fetched_ = false;
|
||||
SensorValue last_{};
|
||||
};
|
||||
|
||||
} // namespace kvida
|
||||
36
components/profiles/src/chip_temperature_sensor.cpp
Normal file
36
components/profiles/src/chip_temperature_sensor.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "chip_temperature_sensor.h"
|
||||
|
||||
#include "chip_temperature.h"
|
||||
|
||||
namespace kvida {
|
||||
|
||||
void ChipTemperatureSensor::begin()
|
||||
{
|
||||
drivers::chip_temperature_init();
|
||||
}
|
||||
|
||||
bool ChipTemperatureSensor::fetch()
|
||||
{
|
||||
float celsius = drivers::chip_temperature_read_celsius();
|
||||
|
||||
// Convert to the fixed-point convention (val1 + val2 * 1e-6) once, at
|
||||
// the boundary where the underlying driver hands us a float --
|
||||
// ESP32-C6 has no hardware FPU, see sensor.h.
|
||||
auto val1 = static_cast<int32_t>(celsius);
|
||||
auto val2 = static_cast<int32_t>((celsius - static_cast<float>(val1)) * 1000000.0f);
|
||||
|
||||
last_ = SensorValue{val1, val2, "C", 0};
|
||||
fetched_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChipTemperatureSensor::get(SensorChannel channel, SensorValue &out)
|
||||
{
|
||||
if (!fetched_ || channel != SensorChannel::ChipTemperature) {
|
||||
return false;
|
||||
}
|
||||
out = last_;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace kvida
|
||||
@@ -11,6 +11,7 @@ enum class SensorChannel {
|
||||
Contact,
|
||||
Motion,
|
||||
AmbientTemperature,
|
||||
ChipTemperature, // SoC die temperature, e.g. ESP32-C6's internal sensor -- distinct from AmbientTemperature since the value range/meaning differ
|
||||
Humidity,
|
||||
Pressure,
|
||||
AccelX,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace kvida {
|
||||
|
||||
// Starts mDNS-based MQTT broker discovery (_mqtt._tcp) and, once found,
|
||||
@@ -12,4 +14,16 @@ void start_mqtt();
|
||||
|
||||
bool mqtt_connected();
|
||||
|
||||
// Publishes (retained) Home Assistant MQTT Discovery for a diagnostic
|
||||
// "Chip temperature" sensor on first call, then the reading itself.
|
||||
// Quick end-to-end proof that a real Sensor implementation's data can
|
||||
// reach MQTT/HA -- not the eventual generic publish(topic, value) API.
|
||||
void publish_chip_temperature(float celsius);
|
||||
|
||||
// Registered by main to receive "set LED color" commands from Home
|
||||
// Assistant (an RGB light entity) without transport depending on
|
||||
// `drivers` directly.
|
||||
using LedColorHandler = void (*)(uint8_t red, uint8_t green, uint8_t blue);
|
||||
void set_led_color_handler(LedColorHandler handler);
|
||||
|
||||
} // namespace kvida
|
||||
|
||||
@@ -33,10 +33,19 @@ std::atomic<bool> g_connected{false};
|
||||
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;
|
||||
|
||||
char g_availability_topic[48];
|
||||
char g_discovery_topic[80];
|
||||
char g_discovery_payload[512];
|
||||
|
||||
char g_temp_state_topic[48];
|
||||
char g_led_command_topic[48];
|
||||
char g_led_rgb_command_topic[48];
|
||||
char g_led_state_topic[48];
|
||||
char g_led_rgb_state_topic[48];
|
||||
bool g_led_discovery_published = false;
|
||||
bool g_led_on = false;
|
||||
// Remembered so a settings update can reconnect without re-running mDNS
|
||||
// discovery.
|
||||
char g_broker_uri[96]; // "mqtt://" + up to 63-char host + ":" + port + NUL
|
||||
@@ -149,18 +158,106 @@ void publish_discovery_and_birth()
|
||||
esp_mqtt_client_publish(g_client, g_availability_topic, "online", 0, 1, true);
|
||||
}
|
||||
|
||||
void mqtt_event_handler(void * /*arg*/, esp_event_base_t /*base*/, int32_t event_id, void * /*event_data*/)
|
||||
// Publishes HA MQTT Discovery for an RGB light entity (once) and
|
||||
// (re)subscribes to its command topics -- topic subscriptions don't
|
||||
// survive a client reconnect, so this runs on every MQTT_EVENT_CONNECTED,
|
||||
// not just the first.
|
||||
void publish_led_discovery_and_subscribe()
|
||||
{
|
||||
char id[16];
|
||||
device_id(id, sizeof(id));
|
||||
|
||||
snprintf(g_led_command_topic, sizeof(g_led_command_topic), "kvida/%s/led/set", id);
|
||||
snprintf(g_led_rgb_command_topic, sizeof(g_led_rgb_command_topic), "kvida/%s/led/rgb/set", id);
|
||||
snprintf(g_led_state_topic, sizeof(g_led_state_topic), "kvida/%s/led/state", id);
|
||||
snprintf(g_led_rgb_state_topic, sizeof(g_led_rgb_state_topic), "kvida/%s/led/rgb/state", id);
|
||||
|
||||
if (!g_led_discovery_published) {
|
||||
char discovery_topic[80];
|
||||
snprintf(discovery_topic, sizeof(discovery_topic), "homeassistant/light/%s/led/config", id);
|
||||
|
||||
char payload[640];
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{"
|
||||
"\"name\":\"LED\","
|
||||
"\"unique_id\":\"%s_led\","
|
||||
"\"command_topic\":\"%s\","
|
||||
"\"state_topic\":\"%s\","
|
||||
"\"rgb_command_topic\":\"%s\","
|
||||
"\"rgb_state_topic\":\"%s\","
|
||||
"\"payload_on\":\"ON\","
|
||||
"\"payload_off\":\"OFF\","
|
||||
"\"availability_topic\":\"%s\","
|
||||
"\"payload_available\":\"online\","
|
||||
"\"payload_not_available\":\"offline\","
|
||||
"\"device\":{"
|
||||
"\"identifiers\":[\"%s\"],"
|
||||
"\"name\":\"%s\","
|
||||
"\"manufacturer\":\"Xylon\","
|
||||
"\"model\":\"Kvida\""
|
||||
"}"
|
||||
"}",
|
||||
id, g_led_command_topic, g_led_state_topic, g_led_rgb_command_topic, g_led_rgb_state_topic,
|
||||
g_availability_topic, id, id);
|
||||
|
||||
esp_mqtt_client_publish(g_client, discovery_topic, payload, 0, 1, true);
|
||||
g_led_discovery_published = true;
|
||||
}
|
||||
|
||||
esp_mqtt_client_subscribe(g_client, g_led_command_topic, 1);
|
||||
esp_mqtt_client_subscribe(g_client, g_led_rgb_command_topic, 1);
|
||||
}
|
||||
|
||||
void handle_led_data(const char *topic, size_t topic_len, const char *data, size_t data_len)
|
||||
{
|
||||
char payload[32] = {0};
|
||||
size_t n = data_len < sizeof(payload) - 1 ? data_len : sizeof(payload) - 1;
|
||||
memcpy(payload, data, n);
|
||||
payload[n] = '\0';
|
||||
|
||||
bool is_power = topic_len == strlen(g_led_command_topic) && memcmp(topic, g_led_command_topic, topic_len) == 0;
|
||||
bool is_rgb =
|
||||
topic_len == strlen(g_led_rgb_command_topic) && memcmp(topic, g_led_rgb_command_topic, topic_len) == 0;
|
||||
|
||||
if (is_power) {
|
||||
g_led_on = strcmp(payload, "ON") == 0;
|
||||
if (!g_led_on && g_led_handler) {
|
||||
g_led_handler(0, 0, 0);
|
||||
}
|
||||
esp_mqtt_client_publish(g_client, g_led_state_topic, g_led_on ? "ON" : "OFF", 0, 1, true);
|
||||
} else if (is_rgb) {
|
||||
// Home Assistant's classic (non-JSON) rgb_command_topic schema
|
||||
// sends "R,G,B".
|
||||
int r = 0, g = 0, b = 0;
|
||||
if (sscanf(payload, "%d,%d,%d", &r, &g, &b) == 3) {
|
||||
g_led_on = true;
|
||||
if (g_led_handler) {
|
||||
g_led_handler(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
}
|
||||
esp_mqtt_client_publish(g_client, g_led_state_topic, "ON", 0, 1, true);
|
||||
esp_mqtt_client_publish(g_client, g_led_rgb_state_topic, payload, 0, 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mqtt_event_handler(void * /*arg*/, esp_event_base_t /*base*/, int32_t event_id, void *event_data)
|
||||
{
|
||||
switch (static_cast<esp_mqtt_event_id_t>(event_id)) {
|
||||
case MQTT_EVENT_CONNECTED:
|
||||
ESP_LOGI(TAG, "MQTT connected");
|
||||
g_connected.store(true);
|
||||
publish_discovery_and_birth();
|
||||
publish_led_discovery_and_subscribe();
|
||||
break;
|
||||
case MQTT_EVENT_DISCONNECTED:
|
||||
ESP_LOGW(TAG, "MQTT disconnected");
|
||||
g_connected.store(false);
|
||||
break;
|
||||
case MQTT_EVENT_DATA: {
|
||||
auto *event = static_cast<esp_mqtt_event_handle_t>(event_data);
|
||||
handle_led_data(event->topic, event->topic_len, event->data, event->data_len);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -411,4 +508,55 @@ bool mqtt_connected()
|
||||
return g_connected.load();
|
||||
}
|
||||
|
||||
void publish_chip_temperature(float celsius)
|
||||
{
|
||||
if (!g_client || !g_connected.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
char id[16];
|
||||
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;
|
||||
if (!discovery_published) {
|
||||
char discovery_topic[80];
|
||||
snprintf(discovery_topic, sizeof(discovery_topic),
|
||||
"homeassistant/sensor/%s/chip_temperature/config", id);
|
||||
|
||||
char payload[512];
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{"
|
||||
"\"name\":\"Chip temperature\","
|
||||
"\"device_class\":\"temperature\","
|
||||
"\"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);
|
||||
discovery_published = true;
|
||||
}
|
||||
|
||||
char value[16];
|
||||
snprintf(value, sizeof(value), "%.1f", static_cast<double>(celsius));
|
||||
esp_mqtt_client_publish(g_client, g_temp_state_topic, value, 0, 0, false);
|
||||
}
|
||||
|
||||
void set_led_color_handler(LedColorHandler handler)
|
||||
{
|
||||
g_led_handler = handler;
|
||||
}
|
||||
|
||||
} // namespace kvida
|
||||
|
||||
Reference in New Issue
Block a user