diff --git a/Kconfig b/Kconfig index 1f053b4..d805ec4 100644 --- a/Kconfig +++ b/Kconfig @@ -35,6 +35,28 @@ config WIFI_WATCHDOG_CHECK_INTERVAL_SECONDS help Interval in seconds between WiFi watchdog checks. +choice HEARTBEAT_LED_TYPE + prompt "Select heartbeat LED variant" + default HEARTBEAT_LED_STRIP if $(dt_alias_enabled,led-strip) + default HEARTBEAT_LED_GPIO if $(dt_alias_enabled,heartbeat-led) + +config HEARTBEAT_LED_STRIP + bool "WS2812 RGB LED strip" + depends on LED_STRIP + help + Enable heartbeat LED using a WS2812 RGB LED strip. + GREEN when WiFi is connected, RED otherwise. + Blinks at a 1-second interval. + +config HEARTBEAT_LED_GPIO + bool "GPIO LED" + depends on GPIO + help + Enable heartbeat LED using a simple GPIO-driven LED. + Blinks at a 1-second interval unconditionally. + +endchoice + endmenu config MEMFAULT_BUILTIN_DEVICE_INFO_SOFTWARE_VERSION diff --git a/boards/esp32c3_devkitm.overlay b/boards/esp32c3_devkitm.overlay index 4fb2ab8..f6c86a2 100644 --- a/boards/esp32c3_devkitm.overlay +++ b/boards/esp32c3_devkitm.overlay @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + &wifi { status = "okay"; }; @@ -11,3 +13,39 @@ &coretemp { status = "okay"; }; + +/ { + aliases { + led-strip = &led_strip; + }; +}; + +/* WS2812 RGB LED on GPIO8 via SPI2 */ +&spi2 { + /* Workaround to support WS2812 driver */ + line-idle-low; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + + /* SPI */ + reg = <0>; + spi-max-frequency = <6400000>; + + /* WS2812 */ + chain-length = <1>; + spi-cpha; + spi-one-frame = <0xf0>; + spi-zero-frame = <0xc0>; + color-mapping = ; + }; +}; + +&pinctrl { + spim2_default: spim2_default { + group2 { + /* WS2812 data on GPIO8 */ + pinmux = ; + }; + }; +}; diff --git a/boards/esp32c6_devkitc_esp32c6_hpcore.conf b/boards/esp32c6_devkitc_esp32c6_hpcore.conf new file mode 100644 index 0000000..268fc1f --- /dev/null +++ b/boards/esp32c6_devkitc_esp32c6_hpcore.conf @@ -0,0 +1,3 @@ +# Enable DMA for heartbeat LED +CONFIG_DMA=y +CONFIG_LED_STRIP=y diff --git a/boards/esp32c6_devkitc_esp32c6_hpcore.overlay b/boards/esp32c6_devkitc_esp32c6_hpcore.overlay index b417db0..a2bf341 100644 --- a/boards/esp32c6_devkitc_esp32c6_hpcore.overlay +++ b/boards/esp32c6_devkitc_esp32c6_hpcore.overlay @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + &wifi { status = "okay"; }; @@ -11,3 +13,36 @@ &coretemp { status = "okay"; }; + +/ { + aliases { + led-strip = &led_strip; + }; +}; + +/* WS2812 RGB LED on GPIO8 via I2S */ +&i2s_default { + group1 { + pinmux = ; + }; +}; + +i2s_led: &i2s { + status = "okay"; + + dmas = <&dma 3>; + dma-names = "tx"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-i2s"; + + reg = <0>; + chain-length = <1>; + color-mapping = ; + reset-delay = <500>; + }; +}; + +&dma { + status = "okay"; +}; diff --git a/boards/esp32s3_devkitc_esp32s3_procpu.conf b/boards/esp32s3_devkitc_esp32s3_procpu.conf index 56fd230..2e1a5e8 100644 --- a/boards/esp32s3_devkitc_esp32s3_procpu.conf +++ b/boards/esp32s3_devkitc_esp32s3_procpu.conf @@ -1,3 +1,7 @@ # Enable external SPI RAM CONFIG_SPI=y CONFIG_ESP_SPIRAM=y + +# Enable DMA for heartbeat LED +CONFIG_DMA=y +CONFIG_LED_STRIP=y diff --git a/boards/esp32s3_devkitc_esp32s3_procpu.overlay b/boards/esp32s3_devkitc_esp32s3_procpu.overlay index 44eee6f..b54d578 100644 --- a/boards/esp32s3_devkitc_esp32s3_procpu.overlay +++ b/boards/esp32s3_devkitc_esp32s3_procpu.overlay @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + &wifi { status = "okay"; }; @@ -16,3 +18,36 @@ &psram0 { size = ; }; + +/ { + aliases { + led-strip = &led_strip; + }; +}; + +/* WS2812 RGB LED on GPIO38 via I2S0 */ +&i2s0_default { + group1 { + pinmux = ; + }; +}; + +i2s_led: &i2s0 { + status = "okay"; + + dmas = <&dma 3>; + dma-names = "tx"; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-i2s"; + + reg = <0>; + chain-length = <1>; + color-mapping = ; + reset-delay = <500>; + }; +}; + +&dma { + status = "okay"; +}; diff --git a/boards/esp32s3_devkitm_esp32s3_procpu.conf b/boards/esp32s3_devkitm_esp32s3_procpu.conf new file mode 100644 index 0000000..86c2a91 --- /dev/null +++ b/boards/esp32s3_devkitm_esp32s3_procpu.conf @@ -0,0 +1,3 @@ +# Enable DMA + LED_STRIP for heartbeat LED +CONFIG_DMA=y +CONFIG_LED_STRIP=y diff --git a/boards/esp32s3_devkitm_esp32s3_procpu.overlay b/boards/esp32s3_devkitm_esp32s3_procpu.overlay index b417db0..a880816 100644 --- a/boards/esp32s3_devkitm_esp32s3_procpu.overlay +++ b/boards/esp32s3_devkitm_esp32s3_procpu.overlay @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include + &wifi { status = "okay"; }; @@ -11,3 +13,39 @@ &coretemp { status = "okay"; }; + +/ { + aliases { + led-strip = &led_strip; + }; +}; + +/* WS2812 RGB LED on GPIO48 via SPI3 */ +&spi3 { + /* Workaround to support WS2812 driver */ + line-idle-low; + + led_strip: ws2812@0 { + compatible = "worldsemi,ws2812-spi"; + + /* SPI */ + reg = <0>; + spi-max-frequency = <6400000>; + + /* WS2812 */ + chain-length = <1>; + spi-cpha; + spi-one-frame = <0xf0>; + spi-zero-frame = <0xc0>; + color-mapping = ; + }; +}; + +&pinctrl { + spim3_default: spim3_default { + group2 { + /* WS2812 data on GPIO48 */ + pinmux = ; + }; + }; +}; diff --git a/boards/esp_wrover_kit_procpu.conf b/boards/esp_wrover_kit_procpu.conf index e69de29..9d2d341 100644 --- a/boards/esp_wrover_kit_procpu.conf +++ b/boards/esp_wrover_kit_procpu.conf @@ -0,0 +1 @@ +CONFIG_HEARTBEAT_LED_GPIO=y diff --git a/boards/esp_wrover_kit_procpu.overlay b/boards/esp_wrover_kit_procpu.overlay index d342be1..6cc515d 100644 --- a/boards/esp_wrover_kit_procpu.overlay +++ b/boards/esp_wrover_kit_procpu.overlay @@ -1,3 +1,9 @@ &wifi { status = "okay"; }; + +/ { + aliases { + heartbeat-led = &red_led; + }; +}; diff --git a/src/heartbeat_led.c b/src/heartbeat_led.c new file mode 100644 index 0000000..a45ac5a --- /dev/null +++ b/src/heartbeat_led.c @@ -0,0 +1,130 @@ +//! @file +//! +//! @brief Heartbeat LED module. +//! +//! Blinks an LED at a 1-second interval to indicate the device is running. +//! +//! On boards with a WS2812 RGB LED (CONFIG_HEARTBEAT_LED_STRIP): +//! - GREEN when WiFi is connected +//! - RED when WiFi is not connected +//! +//! On boards with a simple GPIO LED (CONFIG_HEARTBEAT_LED_GPIO): +//! - Blinks unconditionally at a 1-second interval + +#include +#include + +LOG_MODULE_REGISTER(heartbeat_led, LOG_LEVEL_DBG); + +// Blink half-period: 500ms on + 500ms off = 1 second total +#define HEARTBEAT_BLINK_HALF_PERIOD_MS 500 + +#define HEARTBEAT_LED_STACK_SIZE 1024 +#define HEARTBEAT_LED_PRIORITY 7 + +#if defined(CONFIG_HEARTBEAT_LED_STRIP) + +#include +#include +#include + +static const struct device* s_led_strip = DEVICE_DT_GET(DT_ALIAS(led_strip)); + +static bool s_wifi_connected = false; +static struct net_mgmt_event_callback s_wifi_led_mgmt_cb; + +// WiFi event handler to track connection state +static void prv_wifi_event_handler(struct net_mgmt_event_callback* cb, + uint64_t mgmt_event, struct net_if* iface) { + ARG_UNUSED(iface); + if (mgmt_event == NET_EVENT_WIFI_CONNECT_RESULT) { + const struct wifi_status* status = (const struct wifi_status*)cb->info; + s_wifi_connected = (status->status == 0); + LOG_DBG("Heartbeat LED: WiFi %s", + s_wifi_connected ? "connected" : "failed"); + } else if (mgmt_event == NET_EVENT_WIFI_DISCONNECT_RESULT) { + s_wifi_connected = false; + LOG_DBG("Heartbeat LED: WiFi disconnected"); + } +} + +static void prv_heartbeat_led_thread(void* arg1, void* arg2, void* arg3) { + ARG_UNUSED(arg1); + ARG_UNUSED(arg2); + ARG_UNUSED(arg3); + + net_mgmt_init_event_callback( + &s_wifi_led_mgmt_cb, prv_wifi_event_handler, + NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_WIFI_DISCONNECT_RESULT); + net_mgmt_add_event_callback(&s_wifi_led_mgmt_cb); + + if (!device_is_ready(s_led_strip)) { + LOG_ERR("LED strip device not ready"); + return; + } + + bool led_on = false; + while (1) { + led_on = !led_on; + + struct led_rgb pixel; + if (led_on) { + if (s_wifi_connected) { + // Green: WiFi connected + pixel = (struct led_rgb){.r = 0, .g = 32, .b = 0}; + } else { + // Red: WiFi not connected + pixel = (struct led_rgb){.r = 32, .g = 0, .b = 0}; + } + } else { + // Off + pixel = (struct led_rgb){.r = 0, .g = 0, .b = 0}; + } + + int rc = led_strip_update_rgb(s_led_strip, &pixel, 1); + if (rc != 0) { + LOG_ERR("LED strip update failed: %d", rc); + } + + k_sleep(K_MSEC(HEARTBEAT_BLINK_HALF_PERIOD_MS)); + } +} + +#elif defined(CONFIG_HEARTBEAT_LED_GPIO) + +#include + +static const struct gpio_dt_spec s_led = + GPIO_DT_SPEC_GET(DT_ALIAS(heartbeat_led), gpios); + +static void prv_heartbeat_led_thread(void* arg1, void* arg2, void* arg3) { + ARG_UNUSED(arg1); + ARG_UNUSED(arg2); + ARG_UNUSED(arg3); + + if (!gpio_is_ready_dt(&s_led)) { + LOG_ERR("Heartbeat LED GPIO not ready"); + return; + } + + int rc = gpio_pin_configure_dt(&s_led, GPIO_OUTPUT_INACTIVE); + if (rc != 0) { + LOG_ERR("Failed to configure heartbeat LED GPIO: %d", rc); + return; + } + + while (1) { + gpio_pin_toggle_dt(&s_led); + k_sleep(K_MSEC(HEARTBEAT_BLINK_HALF_PERIOD_MS)); + } +} + +#endif /* CONFIG_HEARTBEAT_LED_STRIP / CONFIG_HEARTBEAT_LED_GPIO */ + +#if defined(CONFIG_HEARTBEAT_LED_STRIP) || defined(CONFIG_HEARTBEAT_LED_GPIO) + +K_THREAD_DEFINE(heartbeat_led_thread, HEARTBEAT_LED_STACK_SIZE, + prv_heartbeat_led_thread, NULL, NULL, NULL, + HEARTBEAT_LED_PRIORITY, 0, 0); + +#endif /* CONFIG_HEARTBEAT_LED_STRIP || CONFIG_HEARTBEAT_LED_GPIO */