From f41f10320250d59dbf12f768c520739d35432f6b Mon Sep 17 00:00:00 2001 From: Mathew Saju Date: Mon, 16 Mar 2026 16:31:43 +0530 Subject: [PATCH 1/2] feat: implement modular configurable RGB LED status indicators --- firmware/esp32-csi-node/main/CMakeLists.txt | 2 +- .../esp32-csi-node/main/Kconfig.projbuild | 19 +++ .../esp32-csi-node/main/idf_component.yml | 3 + firmware/esp32-csi-node/main/led_indicator.c | 131 ++++++++++++++++++ firmware/esp32-csi-node/main/led_indicator.h | 38 +++++ firmware/esp32-csi-node/main/main.c | 9 ++ firmware/esp32-csi-node/main/nvs_config.c | 10 ++ firmware/esp32-csi-node/main/nvs_config.h | 3 + firmware/esp32-csi-node/provision.py | 23 ++- 9 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 firmware/esp32-csi-node/main/led_indicator.c create mode 100644 firmware/esp32-csi-node/main/led_indicator.h diff --git a/firmware/esp32-csi-node/main/CMakeLists.txt b/firmware/esp32-csi-node/main/CMakeLists.txt index acf2a111..3f7f1e19 100644 --- a/firmware/esp32-csi-node/main/CMakeLists.txt +++ b/firmware/esp32-csi-node/main/CMakeLists.txt @@ -2,7 +2,7 @@ set(SRCS "main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c" "edge_processing.c" "ota_update.c" "power_mgmt.c" "wasm_runtime.c" "wasm_upload.c" "rvf_parser.c" - "mmwave_sensor.c" + "mmwave_sensor.c" "led_indicator.c" ) set(REQUIRES "") diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 899b6b4d..2785fb56 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -205,6 +205,25 @@ menu "WASM Programmable Sensing (ADR-040)" endmenu +menu "RGB Status Indicator LED" + + config RGB_LED_ENABLED + bool "Enable onboard WS2812 status LED" + default y + help + Compiles the LED state machine to indicate boot, wifi, + and streaming status using the ESP32's onboard RGB LED. + + config RGB_LED_GPIO + int "RGB LED GPIO Pin" + depends on RGB_LED_ENABLED + default 38 + help + The GPIO pin connected to the NeoPixel (WS2812/SK6812). + Commonly 38 on generic S3 boards or 48 on older boards. + +endmenu + menu "Mock CSI (QEMU Testing)" config CSI_MOCK_ENABLED bool "Enable mock CSI generator (for QEMU testing)" diff --git a/firmware/esp32-csi-node/main/idf_component.yml b/firmware/esp32-csi-node/main/idf_component.yml index 7c52a6f4..fc9b341c 100644 --- a/firmware/esp32-csi-node/main/idf_component.yml +++ b/firmware/esp32-csi-node/main/idf_component.yml @@ -8,3 +8,6 @@ dependencies: ## LCD touch abstraction espressif/esp_lcd_touch: "^1.0" + + ## WS2812 LED Strip Driver + espressif/led_strip: "^3.0.0" diff --git a/firmware/esp32-csi-node/main/led_indicator.c b/firmware/esp32-csi-node/main/led_indicator.c new file mode 100644 index 00000000..ae1b80da --- /dev/null +++ b/firmware/esp32-csi-node/main/led_indicator.c @@ -0,0 +1,131 @@ +/** + * @file led_indicator.c + * @brief Configurable RGB LED Status Indicator for ESP32 CSI Node + */ + +#include "led_indicator.h" +#include "sdkconfig.h" +#include "nvs_config.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#ifdef CONFIG_RGB_LED_ENABLED +#include "led_strip.h" + +static const char *TAG = "led_indicator"; +extern nvs_config_t g_nvs_config; + +static led_strip_handle_t s_led_strip = NULL; +static led_indicator_state_t s_current_state = LED_STATE_BOOTING; +static TaskHandle_t s_led_task = NULL; + +static void led_task(void *arg) +{ + uint8_t pulse = 0; + int8_t dir = 5; + bool toggle = false; + + while (1) { + if (!s_led_strip) { + vTaskDelay(pdMS_TO_TICKS(100)); + continue; + } + + switch (s_current_state) { + case LED_STATE_BOOTING: + /* Solid White */ + led_strip_set_pixel(s_led_strip, 0, 50, 50, 50); + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(100)); + break; + + case LED_STATE_WIFI_CONNECTING: + /* Fast Blinking Blue */ + toggle = !toggle; + if (toggle) { + led_strip_set_pixel(s_led_strip, 0, 0, 0, 100); + } else { + led_strip_clear(s_led_strip); + } + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(200)); + break; + + case LED_STATE_WIFI_ERROR: + /* Solid Red */ + led_strip_set_pixel(s_led_strip, 0, 100, 0, 0); + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(100)); + break; + + case LED_STATE_CONNECTED: + /* Slow Pulsing Green */ + pulse += dir; + if (pulse >= 100 || pulse <= 0) { + dir = -dir; + } + led_strip_set_pixel(s_led_strip, 0, 0, pulse, 0); + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(50)); + break; + + case LED_STATE_MOCK_MODE: + /* Blinking Yellow */ + toggle = !toggle; + if (toggle) { + led_strip_set_pixel(s_led_strip, 0, 100, 100, 0); + } else { + led_strip_clear(s_led_strip); + } + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(500)); + break; + } + } +} + +void led_indicator_init(void) +{ + led_strip_config_t strip_config = { + .strip_gpio_num = CONFIG_RGB_LED_GPIO, + .max_leds = 1, + .led_model = LED_MODEL_WS2812, + .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB, + .flags.invert_out = false, + }; + led_strip_rmt_config_t rmt_config = { + .resolution_hz = 10 * 1000 * 1000, /* 10MHz */ + .flags.with_dma = false, + }; + + if (led_strip_new_rmt_device(&strip_config, &rmt_config, &s_led_strip) == ESP_OK) { + led_strip_clear(s_led_strip); + + if (!g_nvs_config.status_led) { + ESP_LOGI(TAG, "Status LED disabled by NVS configuration. Cleared and stopped."); + return; + } + + xTaskCreate(led_task, "led_indicator_task", 2048, NULL, 5, &s_led_task); + ESP_LOGI(TAG, "RGB LED Indicator initialized on GPIO %d", CONFIG_RGB_LED_GPIO); + } else { + ESP_LOGE(TAG, "Failed to initialize RGB LED on GPIO %d", CONFIG_RGB_LED_GPIO); + } +} + +void led_indicator_set_state(led_indicator_state_t state) +{ + if (!g_nvs_config.status_led || !s_led_strip) { + return; + } + s_current_state = state; +} + +#else + +/* Stubs when disabled via Kconfig */ +void led_indicator_init(void) {} +void led_indicator_set_state(led_indicator_state_t state) {} + +#endif diff --git a/firmware/esp32-csi-node/main/led_indicator.h b/firmware/esp32-csi-node/main/led_indicator.h new file mode 100644 index 00000000..d868086f --- /dev/null +++ b/firmware/esp32-csi-node/main/led_indicator.h @@ -0,0 +1,38 @@ +/** + * @file led_indicator.h + * @brief Configurable RGB LED Status Indicator for ESP32 CSI Node + */ + +#ifndef LED_INDICATOR_H +#define LED_INDICATOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** State of the system to indicate via the LED */ +typedef enum { + LED_STATE_BOOTING = 0, + LED_STATE_WIFI_CONNECTING, + LED_STATE_CONNECTED, + LED_STATE_WIFI_ERROR, + LED_STATE_MOCK_MODE, +} led_indicator_state_t; + +/** + * Initializes the LED indicator system if enabled via NVS and Kconfig. + * Starts the background FreeRTOS task to drive the NeoPixel animations. + */ +void led_indicator_init(void); + +/** + * Updates the current system state, changing the LED animation. + * @param state The new system state to indicate. + */ +void led_indicator_set_state(led_indicator_state_t state); + +#ifdef __cplusplus +} +#endif + +#endif /* LED_INDICATOR_H */ diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index b4270943..185cf960 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -32,6 +32,7 @@ #include "mock_csi.h" #endif +#include "led_indicator.h" #include "esp_timer.h" static const char *TAG = "main"; @@ -55,19 +56,23 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + led_indicator_set_state(LED_STATE_WIFI_CONNECTING); esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < MAX_RETRY) { + led_indicator_set_state(LED_STATE_WIFI_CONNECTING); esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "Retrying WiFi connection (%d/%d)", s_retry_num, MAX_RETRY); } else { + led_indicator_set_state(LED_STATE_WIFI_ERROR); xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip)); s_retry_num = 0; + led_indicator_set_state(LED_STATE_CONNECTED); xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } @@ -136,6 +141,9 @@ void app_main(void) /* Load runtime config (NVS overrides Kconfig defaults) */ nvs_config_load(&g_nvs_config); + /* Initialize visual status indicator subsystem */ + led_indicator_init(); + ESP_LOGI(TAG, "ESP32-S3 CSI Node (ADR-018) — Node ID: %d", g_nvs_config.node_id); /* Initialize WiFi STA (skip entirely under QEMU mock — no RF hardware) */ @@ -158,6 +166,7 @@ void app_main(void) /* Initialize CSI collection */ #ifdef CONFIG_CSI_MOCK_ENABLED /* ADR-061: Start mock CSI generator (replaces real WiFi CSI in QEMU) */ + led_indicator_set_state(LED_STATE_MOCK_MODE); esp_err_t mock_ret = mock_csi_init(CONFIG_CSI_MOCK_SCENARIO); if (mock_ret != ESP_OK) { ESP_LOGE(TAG, "Mock CSI init failed: %s", esp_err_to_name(mock_ret)); diff --git a/firmware/esp32-csi-node/main/nvs_config.c b/firmware/esp32-csi-node/main/nvs_config.c index 3c85e4a5..5e6ab797 100644 --- a/firmware/esp32-csi-node/main/nvs_config.c +++ b/firmware/esp32-csi-node/main/nvs_config.c @@ -96,6 +96,9 @@ void nvs_config_load(nvs_config_t *cfg) cfg->filter_mac_set = 0; memset(cfg->filter_mac, 0, 6); + /* Indicator defaults */ + cfg->status_led = 1; + /* Try to override from NVS */ nvs_handle_t handle; esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle); @@ -302,6 +305,13 @@ void nvs_config_load(nvs_config_t *cfg) cfg->filter_mac[3], cfg->filter_mac[4], cfg->filter_mac[5]); } + /* Indicator LED override */ + uint8_t status_led_val; + if (nvs_get_u8(handle, "status_led", &status_led_val) == ESP_OK) { + cfg->status_led = status_led_val ? 1 : 0; + ESP_LOGI(TAG, "NVS override: status_led=%u", (unsigned)cfg->status_led); + } + /* Validate tdm_slot_index < tdm_node_count */ if (cfg->tdm_slot_index >= cfg->tdm_node_count) { ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0", diff --git a/firmware/esp32-csi-node/main/nvs_config.h b/firmware/esp32-csi-node/main/nvs_config.h index 1a49efaa..6c2d0fe7 100644 --- a/firmware/esp32-csi-node/main/nvs_config.h +++ b/firmware/esp32-csi-node/main/nvs_config.h @@ -55,6 +55,9 @@ typedef struct { uint8_t csi_channel; /**< Explicit CSI channel override (0 = auto-detect). */ uint8_t filter_mac[6]; /**< MAC address to filter CSI frames. */ uint8_t filter_mac_set; /**< 1 if filter_mac was loaded from NVS. */ + + /* Generic Utility Settings */ + uint8_t status_led; /**< 1 to enable the RGB status LED, 0 to disable. */ } nvs_config_t; /** diff --git a/firmware/esp32-csi-node/provision.py b/firmware/esp32-csi-node/provision.py index bbe4e21e..8efd0523 100644 --- a/firmware/esp32-csi-node/provision.py +++ b/firmware/esp32-csi-node/provision.py @@ -64,11 +64,17 @@ def build_nvs_csv(args): writer.writerow(["vital_int", "data", "u16", str(args.vital_int)]) if args.subk_count is not None: writer.writerow(["subk_count", "data", "u8", str(args.subk_count)]) + + # Generic Utility Settings + if args.status_led is not None: + writer.writerow(["status_led", "data", "u8", str(args.status_led)]) + # ADR-060: Channel override and MAC filter if args.channel is not None: writer.writerow(["csi_channel", "data", "u8", str(args.channel)]) if args.filter_mac is not None: - mac_bytes = bytes(int(b, 16) for b in args.filter_mac.split(":")) + mac_str = str(args.filter_mac) + mac_bytes = bytes(int(b, 16) for b in mac_str.split(":")) # pyre-ignore # NVS blob: write as hex-encoded string for CSV compatibility writer.writerow(["filter_mac", "data", "hex2bin", mac_bytes.hex()]) return buf.getvalue() @@ -170,6 +176,10 @@ def main(): parser.add_argument("--channel", type=int, help="CSI channel (1-14 for 2.4GHz, 36-177 for 5GHz). " "Overrides auto-detection from connected AP.") parser.add_argument("--filter-mac", type=str, help="MAC address to filter CSI frames (AA:BB:CC:DD:EE:FF)") + + # Generic Utility + parser.add_argument("--status-led", type=int, choices=[0, 1], help="Enable (1) or disable (0) RGB status indicator (default: 1)") + parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash") args = parser.parse_args() @@ -182,6 +192,7 @@ def main(): args.fall_thresh is not None, args.vital_win is not None, args.vital_int is not None, args.subk_count is not None, args.channel is not None, args.filter_mac is not None, + args.status_led is not None, ]) if not has_value: parser.error("At least one config value must be specified") @@ -202,7 +213,7 @@ def main(): parser.error(f"--filter-mac must be in AA:BB:CC:DD:EE:FF format, got '{args.filter_mac}'") try: for p in parts: - val = int(p, 16) + val = int(p, 16) # pyre-ignore if val < 0 or val > 255: raise ValueError except ValueError: @@ -238,6 +249,8 @@ def main(): print(f" CSI Channel: {args.channel}") if args.filter_mac is not None: print(f" Filter MAC: {args.filter_mac}") + if args.status_led is not None: + print(f" Status LED: {'On (1)' if args.status_led else 'Off (0)'}") csv_content = build_nvs_csv(args) @@ -255,6 +268,12 @@ def main(): f"{fallback_path} nvs.bin 0x6000", file=sys.stderr) sys.exit(1) + if not nvs_bin: + print("Failed to generate NVS binary", file=sys.stderr) + sys.exit(1) + + assert isinstance(nvs_bin, bytes) + if args.dry_run: out = "nvs_provision.bin" with open(out, "wb") as f: From 9dfd80b00140769a696659144e145e41b9258756 Mon Sep 17 00:00:00 2001 From: Mathew Saju Date: Mon, 23 Mar 2026 11:32:54 +0530 Subject: [PATCH 2/2] feat(firmware): add mmWave and Swarm Bridge status to LED indicators - Added LED_STATE_MMWAVE_ERROR, LED_STATE_SWARM_ERROR, and LED_STATE_SWARM_ACTIVE. - Integrated mmWave initialization health check into LED status. - Added Swarm Bridge registration, heartbeat, and ingest feedback (Magenta pulse). - Improved overall system status robustness by skipping errors for unconfigured features. --- firmware/esp32-csi-node/main/led_indicator.c | 34 ++++++++++++++++++++ firmware/esp32-csi-node/main/led_indicator.h | 3 ++ firmware/esp32-csi-node/main/main.c | 5 ++- firmware/esp32-csi-node/main/swarm_bridge.c | 13 ++++++-- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/firmware/esp32-csi-node/main/led_indicator.c b/firmware/esp32-csi-node/main/led_indicator.c index ae1b80da..e50741d5 100644 --- a/firmware/esp32-csi-node/main/led_indicator.c +++ b/firmware/esp32-csi-node/main/led_indicator.c @@ -81,6 +81,40 @@ static void led_task(void *arg) led_strip_refresh(s_led_strip); vTaskDelay(pdMS_TO_TICKS(500)); break; + + case LED_STATE_MMWAVE_ERROR: + /* Slow Blinking Yellow */ + toggle = !toggle; + if (toggle) { + led_strip_set_pixel(s_led_strip, 0, 100, 100, 0); + } else { + led_strip_clear(s_led_strip); + } + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(1000)); + break; + + case LED_STATE_SWARM_ERROR: + /* Slow Blinking Magenta */ + toggle = !toggle; + if (toggle) { + led_strip_set_pixel(s_led_strip, 0, 100, 0, 100); + } else { + led_strip_clear(s_led_strip); + } + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(1000)); + break; + + case LED_STATE_SWARM_ACTIVE: + /* Quick Blip Magenta */ + led_strip_set_pixel(s_led_strip, 0, 100, 0, 100); + led_strip_refresh(s_led_strip); + vTaskDelay(pdMS_TO_TICKS(100)); + led_strip_clear(s_led_strip); + led_strip_refresh(s_led_strip); + s_current_state = LED_STATE_CONNECTED; + break; } } } diff --git a/firmware/esp32-csi-node/main/led_indicator.h b/firmware/esp32-csi-node/main/led_indicator.h index d868086f..dcedb6d1 100644 --- a/firmware/esp32-csi-node/main/led_indicator.h +++ b/firmware/esp32-csi-node/main/led_indicator.h @@ -17,6 +17,9 @@ typedef enum { LED_STATE_CONNECTED, LED_STATE_WIFI_ERROR, LED_STATE_MOCK_MODE, + LED_STATE_MMWAVE_ERROR, + LED_STATE_SWARM_ERROR, + LED_STATE_SWARM_ACTIVE, } led_indicator_state_t; /** diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index eb0e85bb..8a2b964e 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -246,8 +246,11 @@ void app_main(void) ESP_LOGI(TAG, "mmWave sensor: %s (caps=0x%04x)", mmwave_type_name(mw.type), mw.capabilities); } - } else { + } else if (mmwave_ret == ESP_ERR_NOT_FOUND) { ESP_LOGI(TAG, "No mmWave sensor detected (CSI-only mode)"); + } else { + ESP_LOGE(TAG, "mmWave sensor init error: %s", esp_err_to_name(mmwave_ret)); + led_indicator_set_state(LED_STATE_MMWAVE_ERROR); } /* ADR-066: Initialize swarm bridge to Cognitum Seed (if configured). */ diff --git a/firmware/esp32-csi-node/main/swarm_bridge.c b/firmware/esp32-csi-node/main/swarm_bridge.c index b6b485b2..deeb5d68 100644 --- a/firmware/esp32-csi-node/main/swarm_bridge.c +++ b/firmware/esp32-csi-node/main/swarm_bridge.c @@ -19,6 +19,7 @@ #include "esp_app_desc.h" #include "esp_netif.h" #include "esp_http_client.h" +#include "led_indicator.h" static const char *TAG = "swarm"; @@ -233,6 +234,7 @@ static void swarm_task(void *arg) /* Get firmware version string. */ const esp_app_desc_t *app = esp_app_get_description(); const char *fw_ver = app ? app->version : "unknown"; + ESP_LOGI(TAG, "Firmware version: %s", fw_ver); /* Get local IP. */ char ip_str[16]; @@ -251,8 +253,10 @@ static void swarm_task(void *arg) if (swarm_post_json(client, json, len) == ESP_OK) { s_cnt_regs++; ESP_LOGI(TAG, "registered node %u with seed (id=%lu)", s_node_id, (unsigned long)reg_id); + led_indicator_set_state(LED_STATE_SWARM_ACTIVE); } else { ESP_LOGW(TAG, "registration failed — will retry on next heartbeat"); + led_indicator_set_state(LED_STATE_SWARM_ERROR); } } @@ -278,15 +282,12 @@ static void swarm_task(void *arg) xSemaphoreGive(s_mutex); uint32_t uptime_s = (uint32_t)(esp_timer_get_time() / 1000000ULL); - uint32_t free_heap = esp_get_free_heap_size(); uint32_t ts = (uint32_t)(esp_timer_get_time() / 1000ULL); /* ---- Heartbeat ---- */ if ((now - last_heartbeat) >= pdMS_TO_TICKS(s_cfg.heartbeat_sec * 1000U)) { last_heartbeat = now; - bool presence = vit_valid && (vit.flags & 0x01); - /* Heartbeat ID: node_id * 1000000 + 100000 + ts_sec */ uint32_t hb_id = (uint32_t)s_node_id * 1000000U + 100000U + (uptime_s % 100000U); char json[SWARM_JSON_BUF]; @@ -297,6 +298,9 @@ static void swarm_task(void *arg) if (swarm_post_json(client, json, len) == ESP_OK) { s_cnt_heartbeats++; + led_indicator_set_state(LED_STATE_SWARM_ACTIVE); + } else { + led_indicator_set_state(LED_STATE_SWARM_ERROR); } } @@ -316,6 +320,9 @@ static void swarm_task(void *arg) if (swarm_post_json(client, json, len) == ESP_OK) { s_cnt_ingests++; + led_indicator_set_state(LED_STATE_SWARM_ACTIVE); + } else { + led_indicator_set_state(LED_STATE_SWARM_ERROR); } } }