Compare commits

...

4 Commits

Author SHA1 Message Date
rUv 4bf88e1283 feat(firmware): gate LED gamma viz behind CONFIG_LED_GAMMA_VIZ (ADR-183 follow-up) (#1129)
The 40 Hz gamma flicker is now Kconfig-gated (default y, unchanged
behaviour). Set CONFIG_LED_GAMMA_VIZ=n for a dark, lower-power boot (the
LED is simply cleared) — important for photosensitive deployments, no
source edit needed. The colormap saturation point is now operator-tunable
via CONFIG_LED_MOTION_FULLSCALE_MILLI (default 250 = 0.25).

Build + flash confirmed on ESP32-S3 N16R8 (COM8): default y still arms the
gamma timer, CSI flows. ADR-183 updated (gate moved from follow-up to done).
2026-06-17 22:22:20 -04:00
rUv a4c2935a2f feat(firmware): onboard LED 40 Hz gamma stimulus + CSI-motion colour (ADR-183) (#1127)
* chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32

Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(firmware): onboard LED as 40 Hz gamma stimulus, colour from live CSI motion (ADR-183)

The S3 onboard WS2812 (GPIO 48, #962) now runs a GENUS-style 40 Hz gamma
square wave (12.5 ms on/off, 50% duty). The ON-phase colour is live CSI
motion (edge motion_energy) mapped through a 60-step viridis LUT generated
from ruv-neural-viz::ColorMap::viridis() — still=purple, moving=yellow.

Uses the now-no_std ColorMap (ruvnet/ruv-neural#3 / #1126). Hardware-
confirmed on ESP32-S3 N16R8 (COM8): boot log shows the timer armed, CSI
keeps flowing (27-38 pps). Honesty + photosensitivity notes + a Kconfig-gate
follow-up are in ADR-183.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-17 21:04:02 -04:00
rUv 315d7df09e chore(deps): bump ruv-neural submodule — ColorMap no_std for ESP32 (#1126)
Points to ruvnet/ruv-neural#3 (c9638fa): ruv-neural-viz::ColorMap now
builds no_std, so it can run on the ESP32. Unblocks driving the onboard
WS2812 from the viridis/cool-warm colormap.
2026-06-17 20:18:35 -04:00
rUv bdd1eaf927 chore: untrack ruvector.db runtime artifacts + gitignore at any depth (#1124)
These are hook/runtime-generated databases (ruvector/intelligence store)
that kept showing as dirty and don't belong in version control. Removed
from the index (files kept on disk) and ignored globally.
2026-06-17 17:49:47 -04:00
10 changed files with 200 additions and 6 deletions
+4
View File
@@ -287,3 +287,7 @@ harness/**/*.tgz
harness/**/package-lock.json
harness/**/.claude-flow/
harness/**/ruvector.db
# ruvector runtime/hook DB — never tracked (any depth)
ruvector.db
**/ruvector.db
Binary file not shown.
@@ -0,0 +1,98 @@
# ADR-183: Onboard LED as a 40 Hz Gamma Stimulus, Colour-Mapped from Live CSI via `ruv-neural-viz`
| Field | Value |
|-------|-------|
| **Status** | Accepted — implemented & hardware-confirmed on ESP32-S3 N16R8 (COM8) |
| **Date** | 2026-06-17 |
| **Deciders** | ruv |
| **Codename** | **GAMMA-VIZ** |
| **Builds on** | `ruv-neural-viz::ColorMap` (now `no_std` — ruvnet/ruv-neural#3 / RuView#1126), the ESP32 edge `motion_energy` metric (`edge_processing.c`), PR #962 (WS2812 on GPIO 48) |
## Context
Two threads converged. (1) `ruv-neural-viz::ColorMap` — the viridis/cool-warm
palette the rUv-Neural stack uses to render brain-topology graphs — was `std`-only,
so it couldn't run on the ESP32. (2) The onboard WS2812 on the S3 CSI node was dead
weight: the firmware only cleared it on boot (and on the wrong pin for N16R8 — GPIO
38 vs the actual 48, see #962).
The ask: make the LED do something real and honest, using the project's own visual
capability — not a decorative blink. The natural fit is a **40 Hz gamma stimulus**
(the GENUS gamma-entrainment frequency from Alzheimer's light-therapy research)
whose **colour is driven by live sensed motion**, so the node's front panel is both
a known bio-stimulus waveform and a truthful readout of what the CSI is detecting.
## Decision
### Part A — make `ColorMap` `no_std`
`colormap.rs` is self-contained (no cross-crate deps), so expose it on `no_std`
targets. The only blockers were two `std`-only `f64` ops:
- `f64::round` / `f64::abs` → replaced with `core`+`alloc`-safe helpers `fround`
(round via `f64 as i64` truncation — a `core` cast, no `libm`) and `fabs`.
- `Vec`/`String`/`format!` → from `alloc`.
The graph-bound modules (`animation`/`ascii`/`export`/`layout`) and their heavy deps
move behind a default `std` feature; `--no-default-features` builds the crate `no_std`
and exposes only `colormap`. Output is **byte-identical** (8/8 colormap tests pass with
the same RGB values), so this is a pure portability change.
### Part B — the LED stimulus (firmware)
`firmware/esp32-csi-node/main/main.c`, on boot:
- WS2812 on **GPIO 48** (N16R8 / DevKitC-1 v1.1; GPIO 8 on C6).
- An `esp_timer` periodic at **12 500 µs toggles a square wave → 40 Hz, 50 % duty**
(full-on / full-off — a *perceptible* gamma flicker, not a colour drift).
- **ON-phase colour = live CSI motion.** Each ON phase reads `edge_get_vitals().motion_energy`,
normalises it (`/ LED_MOTION_FULLSCALE`, clamped `[0,1]`), and indexes a **60-step
viridis LUT generated from `ColorMap::viridis().map()`** — still = dark purple,
strong motion = yellow.
The LUT is baked from the real crate (Part A makes the same `ColorMap` embeddable
for a future direct FFI path once the ESP Rust toolchain is in CI). The colours are
therefore provably `ruv-neural-viz`'s, and the motion is provably real.
## Honesty (what it is and is not)
- **40 Hz is a real square-wave stimulus** (12.5 ms on / 12.5 ms off), not a label on
a colour sweep. It is *not* tied to any measured 40 Hz brain rhythm — it is an
*output* stimulus at the gamma frequency, not a readout of neural gamma.
- **Colour is a real CSI readout** — `motion_energy` is the on-device phase-variance
motion metric the node already computes; no fabrication. At rest the LED sits at the
purple (low) end and flickers there.
- No therapeutic claim is made. 40 Hz GENUS entrainment is cited as the *origin of the
frequency choice*, not as a validated medical effect of this device.
## Consequences
**Positive**
- The LED is now an honest front-panel: gamma-frequency flicker + a live motion readout.
- `ColorMap` is embeddable (`no_std`), unblocking on-device use of the rUv-Neural
palette beyond this LED.
- Confirms #962's GPIO-48 fix visually (the LED lights on N16R8).
**Negative / risks**
- 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).
## Validation
- `ruv-neural-viz`: `cargo build` (std) ✓, `cargo test colormap` 8/8 ✓ (identical RGB),
`cargo build --no-default-features` compiles `no_std` ✓.
- Firmware: built (1.13 MB), flashed to ESP32-S3 N16R8 (COM8). Boot log:
`Onboard WS2812: 40 Hz gamma flicker (GENUS), colour=CSI motion via ruv-neural-viz, GPIO 48`;
CSI continues (2738 pps), `motion=0.00` at rest → purple flicker as designed.
- Full on-device (xtensa) Rust build of `ColorMap` not run — ESP Rust toolchain absent.
## References
- ruvnet/ruv-neural#3 (ColorMap no_std), RuView#1126 (submodule bump), #962 (GPIO 48).
- Singer/Tsai GENUS 40 Hz gamma entrainment (origin of the frequency, not a device claim).
@@ -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
+71 -5
View File
@@ -144,6 +144,54 @@ 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:
* dark-purple (still) -> teal -> green -> yellow (strong motion). */
static const uint8_t VIRIDIS_LUT[60][3] = {
{ 68, 1, 84},{ 67, 6, 88},{ 67, 12, 91},{ 66, 17, 95},{ 66, 23, 99},
{ 65, 28,103},{ 64, 34,106},{ 64, 39,110},{ 63, 45,114},{ 63, 50,118},
{ 62, 56,121},{ 61, 61,125},{ 61, 67,129},{ 60, 72,132},{ 59, 78,136},
{ 59, 83,139},{ 57, 87,139},{ 55, 92,139},{ 53, 96,139},{ 52,100,139},
{ 50,104,139},{ 48,109,139},{ 46,113,139},{ 44,117,140},{ 43,122,140},
{ 41,126,140},{ 39,130,140},{ 37,134,140},{ 36,139,140},{ 34,143,140},
{ 35,147,139},{ 39,151,136},{ 43,154,133},{ 47,158,130},{ 52,162,127},
{ 56,166,124},{ 60,170,121},{ 64,173,119},{ 68,177,116},{ 72,181,113},
{ 76,185,110},{ 81,189,107},{ 85,192,104},{ 89,196,102},{ 93,200, 99},
{102,203, 95},{113,205, 91},{124,207, 87},{134,209, 82},{145,211, 78},
{156,213, 74},{167,215, 70},{178,217, 66},{188,219, 62},{199,221, 58},
{210,223, 54},{221,225, 49},{231,227, 45},{242,229, 41},{253,231, 37},
};
static led_strip_handle_t s_viz_led;
/* 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)
* mapped through the ruv-neural-viz viridis LUT — still=purple, moving=yellow.
* So the LED is a real 40 Hz gamma stimulus whose hue tracks sensed motion. */
static void led_gamma_40hz_cb(void *arg)
{
static bool on = false;
on = !on;
if (on) {
edge_vitals_pkt_t v;
float m = edge_get_vitals(&v) ? v.motion_energy : 0.0f;
float norm = m / LED_MOTION_FULLSCALE;
if (norm < 0.0f) norm = 0.0f;
if (norm > 1.0f) norm = 1.0f;
int idx = (int)(norm * 59.0f + 0.5f);
const uint8_t *c = VIRIDIS_LUT[idx];
led_strip_set_pixel(s_viz_led, 0, c[0], c[1], c[2]); /* R,G,B (driver maps to GRB) */
} else {
led_strip_set_pixel(s_viz_led, 0, 0, 0, 0); /* off phase */
}
led_strip_refresh(s_viz_led);
}
#endif /* CONFIG_LED_GAMMA_VIZ */
void app_main(void)
{
/* Initialize NVS */
@@ -173,15 +221,16 @@ 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);
/* Turn off onboard WS2812 LED.
* S3 dev boards put the LED on GPIO 38; C6 dev boards on GPIO 8.
* On C6, GPIO 38 doesn't exist (only 0-30) — gate the init by target. */
/* 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.
* 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
const int led_gpio = 38;
const int led_gpio = 48;
#endif
led_strip_handle_t led_strip;
led_strip_config_t strip_config = {
.strip_gpio_num = led_gpio,
.max_leds = 1,
@@ -193,9 +242,26 @@ 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,
.name = "led_gamma_40hz",
};
esp_timer_handle_t viz_timer;
if (esp_timer_create(&viz_args, &viz_timer) == ESP_OK) {
esp_timer_start_periodic(viz_timer, 12500); // 12.5 ms toggle → 40 Hz square wave
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
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.