DIY Sunrise Alarm: ESP32 + LED Strip Lessons
Yes, you can build a genuinely good sunrise alarm from an ESP32 and an addressable LED strip. I did, and I’ve woken up to mine every morning for months, daily at 4:34. But the difference between “LEDs that get brighter on a timer” and a fade that actually feels like dawn comes down to a handful of details that almost every DIY build (including my early ones) gets wrong. This is the builder-to-builder version: the mistakes, the fixes, and the reliability engineering nobody blogs about because it’s boring: right up until your alarm doesn’t fire.
The hardware that actually works
My recommendation after a lot of iteration: SK6812 RGBW, not plain WS2812B RGB. The dedicated white channel matters more than you’d think. Faking warm white by mixing R+G+B looks fine at full brightness and terrible at 2%. You get a muddy, greenish glow instead of that clean candle-warm light a real dawn fade needs to pass through. The W channel gives you a genuine phosphor-converted warm white you can blend against the color channels, and the fourth channel is where most of the “this looks like light, not like LEDs” quality lives.
On the controller side, an ESP32 is the right tool, but put rendering on its own core. I run the animation engine as a dedicated task on one core at 60fps, with WiFi, the web server, and scheduling on the other. If your render loop shares a core with the network stack, you’ll see it: micro-stutters during the fade every time a client polls the device. At normal LED-project speeds nobody notices; during a 30-minute glacially slow fade in a pitch-black room, a dropped frame is a visible pop.
One hard-won note: PSRAM silently claims GPIO pins. On classic ESP32-WROVER modules it’s GPIO 16 and 17; on ESP32-S3 modules with octal PSRAM (the R8/R16 parts) it’s GPIO 33–37. Check your exact module’s datasheet before assigning strip pins. I lost time to this one. I lost time to that one.
Gamma before brightness scaling: the #1 reason DIY ramps look wrong
If you take away one thing, take this. LEDs are linear; your eyes are not. A pixel at value 128 does not look half as bright as 255. It looks maybe 75% as bright. So a linear ramp from 0 to 255 burns through most of its perceptible change early and then seems to stall, by the halfway point it already looks roughly three-quarters as bright as full, leaving the entire second half of your fade with almost nothing visible left to give.
The standard fix is gamma correction (I use the standard 2.2 curve), and most builders know that part. The subtle bug is the order of operations: you must apply gamma correction before brightness scaling, not after. In my renderer, every channel goes gamma8(color) * brightnessScale, gamma first, then the master brightness multiplier. Do it the other way, scale the raw value first, then gamma-correct, and the gamma curve’s steep low end flattens every already-small result down to zero once it’s quantized to 8 bits. The fade’s first ten minutes, the most important part, the part your sleeping brain is supposed to register, becomes a black screen followed by a jump. I’ve seen this exact ordering bug in Home Assistant automation threads and WLED preset discussions more times than I can count, usually described as “my sunrise does nothing for a while and then suddenly turns on.” That’s the symptom. This is the cause.
The curve shape: linear is wrong, and parabolic isn’t enough
Even with gamma handled, a linear brightness-over-time ramp doesn’t feel like sunrise. Real dawn brightens slowly, then accelerates. A parabolic curve is closer, but I ended up wanting control over the shape, not just the exponent, so my brightness curve is defined by four tunable control points, which lets you hold a long dim smolder early and steepen the climb near wake time. Where the curve should top out is its own question. I wrote up how bright a sunrise alarm actually needs to be in a separate guide, but the short version is that peak brightness matters less than how you get there.
Start before the fade: the pre-dawn phase
Real dawn doesn’t begin with light. It begins with the darkness getting slightly less absolute. I added a distinct pre-dawn glow phase before the main fade: a barely-there deep glow that runs before the sunrise proper starts. It sounds like a garnish. It isn’t. Waking up during pre-dawn versus getting slammed by mid-fade brightness is the difference between drifting up and being startled, and whether any of this gently-waking business works at all is a fair question. I dug into the research on that here.
What color is dawn, actually?
Most DIY sunrises fade from black to warm white, maybe through orange. Go watch an actual dawn (I live under the Wasatch range in Ogden, Utah, which is what my signature scene is modeled on). The real trajectory is stranger: deep violet, into magenta, then a single point of deep-red ember that brightens and spreads into amber, and only then into anything you’d call white. My renderer’s sun model literally starts as “first ember: deep red” and grows from there while the sky layer runs its own violet-to-magenta track behind it. Two layers, two color journeys. It’s the single biggest visual upgrade over the fade-to-warm-white default, and it costs nothing but a better color table.
The part nobody blogs about: it has to fire
Here’s where a fun weekend project becomes an appliance. A decorative lamp that glitches is a shrug. An alarm that fails is a missed obligation. The reliability lessons cost me actual missed alarms:
- One command path into the renderer. Early on I had HTTP handlers poking scene state directly while the render task read it, a classic cross-core race that produced rare, unreproducible failures. Now every state change goes through a single command queue, and nothing else is allowed to touch animation state. If you take one architecture rule from this post, take that one.
- Network time, local execution. The device syncs via NTP but needs the internet for nothing else, if WiFi dies at 3 a.m., the alarm still fires from the on-board clock and schedule. (This local-first stance is also why there’s no app or cloud account anywhere in the design: more on that philosophy here.)
- Power-loss catch-up. If power blips mid-fade, the device reboots, notices an alarm should be in progress, and rejoins the fade at the correct point instead of restarting from black or silently skipping.
- Log every decision. The device keeps an on-device log of every alarm decision, fired, skipped, why. When something looks wrong at 4:34 a.m., you cannot debug from memory. The log is how I found half the bugs above.
The Home Assistant and WLED communities deserve real credit here, the long forum threads on sunrise automations wrestle honestly with the gamma problem, the curve-shape problem, and the “my automation didn’t trigger” problem. If you’re building on those platforms, those threads are worth your time. The gap I kept hitting was that general-purpose platforms treat the alarm as one automation among many, and alarm-grade reliability wants the whole device organized around it.
Build it, or don’t
If this post made you want to solder something: genuinely, do it. It’s a great project and you’ll learn more about perception and embedded reliability than any tutorial can teach. And if you want the tuned curve, the real dawn colors, and the alarm-grade reliability engineering without the months of debugging. That’s exactly what I turned this project into. It’s called Horizn, it’s local-first with no app and no subscription, and it’s the device I wake up to every morning.