docs/adr/ADR-183-onboard-led-gamma-stimulus-csi-colormap.md
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) |
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.
ColorMap no_stdcolormap.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.
firmware/esp32-csi-node/main/main.c, on boot:
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).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.
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.Positive
ColorMap is embeddable (no_std), unblocking on-device use of the rUv-Neural
palette beyond this LED.Negative / risks
CONFIG_LED_GAMMA_VIZ (default y); set it n for a dark,
lower-power boot (the LED is just cleared) — no source change needed.CONFIG_LED_GAMMA_VIZ in those deployments.CONFIG_LED_MOTION_FULLSCALE_MILLI (default 250 = 0.25),
operator-tunable; still not auto-calibrated per-environment.ColorMap (FFI path deferred — needs
the ESP Rust/xtensa toolchain, not yet in CI).ruv-neural-viz: cargo build (std) ✓, cargo test colormap 8/8 ✓ (identical RGB),
cargo build --no-default-features compiles no_std ✓.Onboard WS2812: 40 Hz gamma flicker (GENUS), colour=CSI motion via ruv-neural-viz, GPIO 48;
CSI continues (27–38 pps), motion=0.00 at rest → purple flicker as designed.ColorMap not run — ESP Rust toolchain absent.