- C++ 95.5%
- Nix 4.5%
The INMP441's L/R pin is left unwired (floating), so its channel select is electrically undefined. It had been sitting on the LEFT channel (which the sketch read via I2S_STD_SLOT_LEFT) but drifted to RIGHT, leaving the LEFT slot reading pure zeros -> silent audio animations. Confirmed by dumping raw I2S swing: LEFT = 0, RIGHT = live signal. Read the RIGHT slot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| blink-test | ||
| demoreel100 | ||
| led-animations | ||
| led-count | ||
| led-strip | ||
| led-test | ||
| mic-spectrum | ||
| mic-test | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
arduino
Arduino sketches for the addressable LED desk-backlight project (ELEGOO UNO R3 +
5V WS2812B strip). Project notes and build log live in the zettelkasten:
Inside/Projects/addressable LED strip behind desk project/.
Dev shell
arduino-cli is provided by the flake — it is not installed system-wide, which is
why a bare arduino-cli fails. Enter the shell first:
nix develop
On a fresh machine, install the core and library once (they persist in
~/.arduino15 / ~/Arduino, outside this repo):
arduino-cli core update-index
arduino-cli core install arduino:avr
arduino-cli lib install FastLED
arduino-cli lib install arduinoFFT # for the audio-reactive / mic-test FFT
Build & upload
cd <sketch-dir> # e.g. led-animations
arduino-cli compile --fqbn arduino:avr:uno .
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:uno .
Upload needs write access to /dev/ttyACM0; the user is in the dialout group via
the NixOS config (log in again after that change took effect, or newgrp dialout).
Sketches
| Sketch | What it does |
|---|---|
blink-test/ |
onboard LED blink — proves the board runs |
led-test/ |
10-LED red/green/blue/white cycle — first-light validation |
led-count/ |
chase + decade-marker pattern to count strip length (300 LEDs) |
led-strip/ |
full 300-LED scrolling rainbow |
demoreel100/ |
FastLED DemoReel100, 6 auto-rotating patterns |
led-animations/ |
all animations in one sketch; ESP32 = Wi-Fi web control, UNO = startup default |
mic-test/ |
INMP441 over I2S → FFT → 8 frequency bands printed to serial (audio pipeline bring-up, with noise gate + auto-gain) |
mic-spectrum/ |
audio-reactive: the FFT bands drive the strip as colored zones, bass→treble (standalone, pre-merge) |
All strip sketches set NUM_LEDS 300 and a 4A current cap
(setMaxPowerInVoltsAndMilliamps(5, 4000)) for safety on the 5A supply. Data pin
is auto-selected: GPIO5 on the ESP32, ~6 on the UNO.
ESP32 (current controller)
The ESP32 core ships prebuilt binaries that run on NixOS via the flake's nix-ld
setup. One-time core install (persists in ~/.arduino15):
arduino-cli core update-index
arduino-cli core install esp32:esp32
Compile and USB upload (board enumerates as /dev/ttyUSB0):
arduino-cli compile --fqbn esp32:esp32:esp32 led-animations
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:esp32 led-animations
Wi-Fi credentials (secrets.h)
led-animations/secrets.h holds the Wi-Fi SSID/password and the mDNS name. It is
gitignored because this repo mirrors to a public Forgejo — never commit it.
On a fresh checkout, copy the template and fill it in:
cp led-animations/secrets.h.example led-animations/secrets.h
$EDITOR led-animations/secrets.h
Wi-Fi web control
Once flashed, the ESP32 joins the network and serves a one-page UI at
http://desk-leds.local/ (or the IP printed to serial at 115200 baud — mDNS
can be flaky, the raw IP always works). The page has:
- a button per animation (demoreel, rainbow, cylon, fire, pacifica, pride, scratch),
- a brightness slider (0–255; the 4A cap still auto-limits it),
- a speed slider (1–10; the shared hue-advance rate),
- a glitter dropdown — a fading white overlay applied over any animation: Off / Twinkle / Drizzle / Rain / Snow / Thunder.
Each animation keeps its own brightness/speed/glitter (changing one doesn't touch the others), and all of it is persisted to NVS flash, so the strip powers back up exactly how you left it.
OTA (wireless reflashing)
After the first USB flash, the firmware runs ArduinoOTA, so subsequent uploads
go over Wi-Fi. The flake provides an ota helper:
ota # compile led-animations + push to 10.0.50.127 over Wi-Fi
ota <sketch> <ip> # override either
Turn audio mode OFF before an OTA push. This was the single biggest cause of
failed/aborted uploads ("No response from device", or "Broken pipe" partway in).
While audio mode is on, the loop does a blocking I²S mic read (~32 ms/frame) that
starves ArduinoOTA.handle(), so the board can't service the push reliably. Drop
audio mode over HTTP first, push, then turn it back on (it auto-returns to the audio
animation):
curl "http://10.0.50.127/set?mic=0" # stop the blocking mic read
ota
curl "http://10.0.50.127/set?mic=1" # back to audio mode after it reboots
With the mic off, OTA typically completes on the first try.
Firewall: espota has the board connect back to the laptop on a random high port, which the NixOS firewall blocks. Drop it for the push, then restore:
sudo systemctl stop firewall
ota
sudo systemctl start firewall
(A permanent fix is to trust inbound from the LAN subnet in the NixOS config.)
A large OTA image needs a stable link; the firmware sets WiFi.setSleep(false)
and retries the join on boot to keep the marginal signal usable.
mDNS gotcha:
desk-leds.localcan get hijacked by the router's DNS pointing at another LAN host (you'll get an nginx 404, not the board). The board is reliably at its IP (10.0.50.127) — use that, or set a DHCP reservation.
Animation dev workflow
The fun loop, from tweaking a number to inventing a pattern:
- Add an animation — write a
myThing_run()that draws one frame + callsFastLED.show(), then add it to theAnimenum,animName(), andrunCurrentAnim(). Its button appears on the web page automatically. - Iterate on hardware — edit →
ota→ look at the strip → repeat (~20–40 s per cycle). Use thescratchanimation as a sandbox so you don't disturb the working patterns. - Tune live, no reflash — the brightness/speed sliders change the strip instantly over HTTP; only code changes need a reflash.
- Prototype without hardware — Wokwi simulates an
ESP32 + WS2812 strip in the browser. Set
NUM_LEDSsmall (~30) and get the pattern math right before burningotacycles. - Debug — plug in USB and
arduino-cli monitor -p /dev/ttyUSB0 -c baudrate=115200to read serial (boot log, IP, crash traces). This is the one job that still needs the cable.
Audio-reactive (INMP441 mic)
An INMP441 I²S MEMS mic makes the strip react to sound. Wiring (mic → ESP32):
| Mic pin | ESP32 | note |
|---|---|---|
| VDD | 3V3 | not 5V — it's a 3.3 V part |
| GND | GND | shared common ground |
| L/R | GND | ties the mic to the LEFT I²S slot (which the code reads) |
| WS | GPIO25 | word select (LRCLK) |
| SCK | GPIO26 | bit clock (BCLK) |
| SD | GPIO33 | data out of the mic |
The audio pipeline was built in stages, each testable on its own:
mic-test— I²S read →arduinoFFT→ 8 log-spaced bands (so the spacing matches how we hear pitch). Printed to serial as bars.- Noise gate + AGC (also
mic-test) — a gate that learns each band's floor only while the room is quiet, plus auto-gain that scales the loudest band to full, so it self-calibrates to any volume / mic placement. Attack/decay smoothing makes the bars move musically. mic-spectrum— the same pipeline now drives the LEDs: 8 colored zones, bass (red) → treble (violet), each zone's length tracking its band. (Standalone bring-up sketch; the merged in-firmware version moved past this look.)
In led-animations, audio mode is toggled at the top of the web page and swaps in
the audio animation set. The live one is swell: a slow drifting rainbow
baseline with broad "mountain" swells that rise at random spots on each musical
onset (red = low pitch → blue = high), widen out like a wave, then settle. It
keys off transients (gOnset[]), not raw loudness, and runs at full brightness so
the peaks read as mountains on the wall. The early band-as-bars looks (spectrum /
glow / pulse) were removed — they read as vertical VU meters on a horizontal strip.
Tip: tune the audio feel over serial first, but judge motion on the LEDs — a scrolling terminal can't show smooth motion and will mislead you.
Lessons learned (the hard way)
Things this project taught me, kept here for the next person (or next me):
- A "common ground" is the data signal's voltage reference, not just a power
return. WS2812 data is a voltage relative to ground; the controller and the
strip must share the same ground or the data garbles. Two diagnostic
fingerprints that saved hours:
- Strip lit but showing garbage / spasming = lost signal ground (power's fine, the reference isn't).
- Only the first LED glows, rest dark = the strip's 5 V isn't connected — LED #1 is sipping parasitic power through the data line.
- Bare jumpers on a header are mechanically fragile, and crowding makes it worse. A cramped row of ground jumpers gave bizarre, logic-defying symptoms — an unrelated wire appearing to "fix" the LEDs — because inserting a neighbor was unseating the one ground that mattered. Minimize crowding, and solder the permanent build (especially the ground bridge).
- Isolate with a known-good control. Re-flashing the known-working LED sketch proved instantly that a failure was wiring, not the new mic/code — stop debugging the thing you just changed until you've ruled out the rest.
- Verify grounds with a multimeter (continuity, and wiggle while testing) — a pin can be "ground" and still make intermittent contact.
- OTA gotcha:
espotahas the board connect back to the host on a random port, so a host firewall blocks it; drop the firewall for the push. AndWiFi.setSleep(false)is what keeps a marginal link responsive enough for OTA. - Classic ESP32 (WROOM-32) is 2.4 GHz only — no 5 GHz networks.