diff --git a/docs/adr/ADR-183-onboard-led-gamma-stimulus-csi-colormap.md b/docs/adr/ADR-183-onboard-led-gamma-stimulus-csi-colormap.md index 4e0b2574..329be5e1 100644 --- a/docs/adr/ADR-183-onboard-led-gamma-stimulus-csi-colormap.md +++ b/docs/adr/ADR-183-onboard-led-gamma-stimulus-csi-colormap.md @@ -74,11 +74,13 @@ therefore provably `ruv-neural-viz`'s, and the motion is provably real. - Confirms #962's GPIO-48 fix visually (the LED lights on N16R8). **Negative / risks** -- Changes *default* firmware behaviour: the onboard LED now animates instead of staying - off (minor power + a visible flicker some may not want). Gate behind a Kconfig - (`CONFIG_LED_GAMMA_VIZ`) if a dark default is preferred — follow-up. -- A 40 Hz flicker can be an issue for photosensitive users; document on the enclosure. -- `LED_MOTION_FULLSCALE` (0.25) is hand-tuned, not calibrated per-environment. +- Changes the *default* firmware behaviour: the onboard LED animates instead of staying + off. Now **gated by `CONFIG_LED_GAMMA_VIZ`** (default `y`); set it `n` for a dark, + lower-power boot (the LED is just cleared) — no source change needed. +- A 40 Hz flicker can be an issue for photosensitive users; document on the enclosure + and disable `CONFIG_LED_GAMMA_VIZ` in those deployments. +- The saturation point is now `CONFIG_LED_MOTION_FULLSCALE_MILLI` (default 250 = 0.25), + operator-tunable; still not auto-calibrated per-environment. - The colour uses a baked LUT, not the live Rust `ColorMap` (FFI path deferred — needs the ESP Rust/xtensa toolchain, not yet in CI). diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 18c32ebf..ee1dff16 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -468,3 +468,29 @@ menu "Mock CSI (QEMU Testing)" depends on CSI_MOCK_ENABLED default n endmenu + +menu "Onboard LED (ADR-183)" + + config LED_GAMMA_VIZ + bool "Onboard WS2812: 40 Hz gamma flicker + CSI-motion colour" + default y + help + Drive the onboard WS2812 as a GENUS-style 40 Hz gamma square wave + (12.5 ms on / 12.5 ms off, 50% duty). The ON-phase colour is live + CSI motion (edge motion_energy) mapped through the ruv-neural-viz + viridis colormap (still=purple, moving=yellow). + + Disable to leave the LED off at boot — lower power, no flicker. + NOTE: a 40 Hz flicker can affect photosensitive users; disable or + shield the LED in those environments. Not a medical device. + + config LED_MOTION_FULLSCALE_MILLI + int "Motion value (x1000) that saturates the colormap to yellow" + depends on LED_GAMMA_VIZ + default 250 + range 1 100000 + help + edge motion_energy that maps to the top (yellow) of the viridis + colormap, in milli-units (250 = 0.25). Lower = more sensitive + (reaches yellow with less motion). +endmenu diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index 7cf32ffb..67973cee 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -144,6 +144,7 @@ static void wifi_init_sta(void) } } +#if CONFIG_LED_GAMMA_VIZ /* Viridis colormap (60 steps), generated from ruv-neural-viz::ColorMap::viridis() * — the rUv-Neural brain-topology colormap, now no_std (ruvnet/ruv-neural#3 / * RuView#1126). Used as the ON-phase colour of the 40 Hz gamma flicker below: @@ -164,8 +165,8 @@ static const uint8_t VIRIDIS_LUT[60][3] = { }; static led_strip_handle_t s_viz_led; -/* motion_energy that saturates the colormap to yellow (tunable). */ -#define LED_MOTION_FULLSCALE 0.25f +/* motion_energy that saturates the colormap to yellow (CONFIG, milli-units). */ +#define LED_MOTION_FULLSCALE ((float)CONFIG_LED_MOTION_FULLSCALE_MILLI / 1000.0f) /* GENUS-style 40 Hz gamma flicker: full on/off square wave, 50% duty (toggled * every 12.5 ms → 40 Hz). The ON colour is live CSI motion (edge motion_energy) @@ -189,6 +190,7 @@ static void led_gamma_40hz_cb(void *arg) } led_strip_refresh(s_viz_led); } +#endif /* CONFIG_LED_GAMMA_VIZ */ void app_main(void) { @@ -219,10 +221,11 @@ void app_main(void) ESP_LOGI(TAG, "%s CSI Node (ADR-018 / ADR-110) — v%s — Node ID: %d", target_name, app_desc->version, g_nvs_config.node_id); - /* Onboard WS2812: sweep the ruv-neural-viz viridis colormap at 40 Hz. - * C6 dev boards wire the LED to GPIO 8; S3 boards to GPIO 38 (DevKitC-1 v1.0) + /* Onboard WS2812. C6 wires the LED to GPIO 8; S3 to GPIO 38 (DevKitC-1 v1.0) * or GPIO 48 (DevKitC-1 v1.1 / N16R8 — see #962). On S3 we drive 48 (the - * common module). On C6, GPIO 38/48 don't exist (only 0-30) — gate by target. */ + * common module). On C6, GPIO 38/48 don't exist (only 0-30) — gate by target. + * Behaviour is set by CONFIG_LED_GAMMA_VIZ (ADR-183): on = 40 Hz gamma flicker + * coloured by CSI motion; off = clear the LED at boot. */ #if defined(CONFIG_IDF_TARGET_ESP32C6) const int led_gpio = 8; #else @@ -239,6 +242,7 @@ void app_main(void) .resolution_hz = 10 * 1000 * 1000, // 10MHz .flags.with_dma = false, }; +#if CONFIG_LED_GAMMA_VIZ if (led_strip_new_rmt_device(&strip_config, &rmt_config, &s_viz_led) == ESP_OK) { const esp_timer_create_args_t viz_args = { .callback = &led_gamma_40hz_cb, @@ -250,6 +254,14 @@ void app_main(void) ESP_LOGI(TAG, "Onboard WS2812: 40 Hz gamma flicker (GENUS), colour=CSI motion via ruv-neural-viz, GPIO %d", led_gpio); } } +#else + /* Viz disabled — clear the onboard LED at boot and release the RMT channel. */ + led_strip_handle_t led_strip; + if (led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip) == ESP_OK) { + led_strip_clear(led_strip); + led_strip_del(led_strip); + } +#endif /* CONFIG_LED_GAMMA_VIZ */ /* ADR-110 P4: 802.15.4 mesh time-sync (C6 only). * Initialized BEFORE WiFi so it's available even when WiFi STA can't