ESP32-C3 Firmware

EMBER

A high-performance pattern engine, stack-based bytecode VM, and real-time audio analyzer for expressive addressable LED installations.

1. What Is Ember?

Ember grew out of WebFastLed, an earlier project that ran patterns by parsing JavaScript-like expressions on every pixel, every frame. It worked, but it was slow and brittle. Ember replaces that interpreter with a proper bytecode compiler and a stack-based virtual machine running inside its own FreeRTOS task—which is how it manages a stable, smooth framerate on a $2 chip.

Caveat first: if you want the gold standard in scriptable LED systems, go look at ElectroMage's Pixelblaze. It's more expensive hardware, but it's a battle-tested ecosystem that can drive thousands of pixels with expansion boards and a mature compiler. Zero friction, rock-solid. It's worth every dollar if that's what you need!

Ember is the scrappy DIY alternative. It's new, it's open-source, and it runs entirely on a bare ESP32-C3 module. No companion app, no proprietary cloud, just a captive portal you connect to over Wi-Fi, write a pattern, hit save, and watch it render live. The idea is that this would be perfect as a cheap solution for wearable lights, since if the controller got lost, destroyed, etc, it's just two or three dollars to replace.

Forged at a hackathon. This project got its legs during the State of Oregon | Claude Code Hackathon (Anthropic + Vibes DIY at the PSU Business Accelerator). I came in with a shaky prototype and a folder of prompts I'd prepped in my Agent Arsenal beforehand. I dropped the hackathon API key in, opened Claude Code directly in my GitHub folder. I used a freshly wiped laptop, no saved credentials, copied prompts over, and let the agents run completely unsupervised. I was bracing for AI slop. Instead, they punched clean through the two hardest problems I'd been wrestling with: the interpreter bottleneck and the FreeRTOS concurrency bugs. By the end of the day it was rendering at a steady 60 FPS!

Feature WebFastLed (Legacy) Ember
Execution Model AST tree-walking interpreter Stack-based bytecode VM
Hot-Loop Allocations Heavy heap churn (strings, vectors) Zero heap allocations
Performance (60 LEDs) Roughly 28 FPS (~190 µs/pixel) Up to 60 FPS (~14 µs/pixel)
Audio Reactivity None 256-point FFT (I2S & Analog)
Threading Single-threaded Arduino loop Isolated FreeRTOS tasks
UI Basic web sliders Live captive portal + pattern editor

Live Portal Simulator

Virtual Addressable Strip 60 FPS
Active: Rainbow Drift
connected
1

                                    
                                
ready

2. Compiler & VM

Why the old approach was slow

WebFastLed parsed your pattern expression character-by-character on every pixel of every frame. At 60 LEDs and 30 FPS, that's roughly 1,800 parse-eval cycles per second. Each one dragged in heap allocations for temporary token strings and std::map lookups. On a microcontroller, that adds up: memory fragments, the allocator stalls, frames drop.

How Ember fixes it

Ember ships a single-pass Pratt-style compiler baked directly into the firmware. When you save a pattern it compiles once into a flat bytecode array, stashes numeric literals in a de-duplicated float pool, and resolves variable names to uint8_t register indices. After that the render loop just executes opcodes against pre-allocated stack buffers, no malloc, no free, no surprises.

Source Code ──► Pratt Compiler ──► Bytecode Array ──► Tight VM Loop

3. Concurrency on a Single Core

The ESP32-C3 is a single-core chip, so we have to share nicely. Ember uses a small FreeRTOS task schedule to make sure everything stays responsive:

  • RenderTask (Priority 2) — This is the heart of the show. It stays locked to 60Hz and doesn't get interrupted by the networking layer.
  • AsyncTCP Worker (Priority 3) — This runs slightly above the renderer. It handles web requests, meaning the dashboard feels snappy, and it only steals a few microseconds to do its job.
  • Atomic pointer swap — When you update a pattern, it doesn't break the current frame. It just atomically replaces a std::atomic<Program*> pointer that the renderer grabs at the top of the next cycle.

4. Audio & FFT

Ember has two mic options depending on your build: a digital INMP441 MEMS mic over I2S (cleaner signal, better for music), or an analog MAX4466 electret mic through ADC1 (cheaper, easier to wire up). Either way, the processing pipeline is the same:

  • A 256-point Hamming-windowed FFT runs via arduinoFFT in a background task.
  • Raw frequency bins get compressed into 8 logarithmic bands that map well to human hearing: bass, mids, and treble don't all fight for the same bucket.
  • The results feed into pattern builtins you can call directly: vu(), beat(), bass(), mid(), treble(), and band(n) for per-band access.
  • The simulator above supports both a sequencer simulation and your actual browser microphone, so you can prototype audio-reactive patterns without touching hardware.