XenneX/LLC
All posts
By Buddy Delaune

Why I Wrote My Own Firmware Instead of Shipping Open Source

The Nion belt ran on WLED, a great open source project. Here's why I replaced it with my own firmware, and the clean-room rule that kept WLED code out.

#hardware#nion lights#firmware#esp32#engineering decisions
A small bare circuit board with a coiled strip of unlit LEDs and a USB-C cable on a dark wooden workbench, next to a pencil and a closed notebook, lit by a warm brass desk lamp

The Nion Lights belt has always run on WLED. If you haven't used it, WLED is open source firmware for addressable LED strips, and it's excellent. Well over a hundred effects, a big community, and it works on cheap ESP32 boards out of the box. It's the reason the first belts lit up at all.

This month I replaced it. The belt now runs firmware I wrote from scratch, and on October 28 the signed 0.9.0 release went out on the stable update feed. Here's why I did it, and how I built it without using a single line of WLED.

What WLED Wasn't Built For

WLED is built for LED strips on a home network. The controller joins your WiFi, or hosts its own, and you control it from a browser or an app on that network. For lights on a porch or under a TV, that's exactly right.

A belt at a festival is a different problem. There's no network. Asking someone to join their belt's WiFi drops their phone off cell data, right when they're trying to find their friends. I wanted three things WLED wasn't designed around:

  1. Control from the Nion app over Bluetooth. The phone stays on its normal connection, and the app builds its controls from whatever effects the belt reports.
  2. Group sync with no phone in the loop. One belt leads, the others follow over ESP-NOW, a radio protocol that lets ESP32 chips talk to each other directly. A whole group runs the same effect in step.
  3. Signed updates over Bluetooth. The belt should only install firmware I signed, and the update should happen from the app, not a USB cable and a laptop.

You could bend WLED toward some of that. But I'd be maintaining a fork of a big project forever, and that leads to the bigger issue.

The License Question

WLED is licensed under the EUPL, a copyleft license. Ship a product built on it, and the source code for that firmware has to be available to the people who get it. That's a fair deal, and it's how a lot of good open source stays open.

It just wasn't the deal I wanted for Nion. The firmware is a big part of what makes the belt the belt, so I wanted it closed. The only honest way to get there was to build my own, from nothing.

The Clean-Room Rule

Writing firmware "inspired by" an open source project is easy to get wrong. If you have the source open in the other window, you end up copying it, even when you don't mean to. So I wrote the rule down before I wrote any code:

  • Allowed: WLED's command vocabulary (effect, speed, intensity, palette, colors, segments, presets) and how the effects look. I could watch an effect run, describe it in plain words, and build it from that description.
  • Not allowed: copying or translating WLED's effect code, its palette tables, or anything ported from them.

That second rule cost me a tool. I had an LED simulator I'd been using to preview effects, and it was itself a port of WLED's effect code. Under the rule it carries the same license, so it couldn't be a source for anything either. Even the palettes are my own set.

Keeping the command vocabulary was the part that made this practical. The belt's existing presets used 26 WLED effects. I rebuilt each one by behavior, gave them my own effect IDs, and the app keeps a map from the old WLED IDs to the new ones. Old presets import over, and nothing in the firmware came from WLED.

One fun side effect: two of those effects react to music in WLED, and the belt has no microphone. So they run on a synthetic beat instead, a generator that produces a steady pulse at the effect's speed. It's seeded, so every belt in a synced group pulses on the same beat.

The Bug in the First Real Update

The update path is where I was most careful, and it's where the first real bug showed up.

The first time I updated a real belt over Bluetooth, it failed partway through, and not in the same place twice. The cause was a single comparison. The update code checked how long it had been idle by subtracting two timestamps. Once in a while, a chunk of the update arrived with a timestamp a hair newer than the one it was being compared against. The math came out negative, the check read it as a huge number instead, and the belt decided the update had stalled and cancelled it.

The fix was a few lines and a test that recreates the timing. In 0.3.2, the belt took its first successful in-app update, to 0.3.3. That's the kind of bug that only shows up on real hardware with a real phone, which is a lesson I wrote about in crossing into hardware. On a website, a bad release is a redeploy. On a belt, a failed update in the wrong place is a support ticket.

Where It Is Now

Release builds now use the ESP32's secure boot and flash encryption, so the belt only runs firmware signed with my key. Every update gets signed, published to the release feed, and checked on a test belt before it goes to stable. On October 28, 0.9.0 passed every check and went out.

I'm not anti open source. Most of what I build sits on it, and WLED is the reason Nion had a working product to improve. But open source is a license, not just free code, and when a product's core needs to stay yours, the clean way is to write it yourself and be strict about how.

The app side of this is Ionic and Angular, the same stack I wrote about in a decade on Ionic. If you're working through a build-or-reuse decision on your own product, that's the kind of call I help with in advisory.

Working on something like this? See architecture advisory or what XenneX has built.

Keep reading