Badges that sync themselves—no pairing, no network, no master. They don't synch animations, they synch time.
Using ESP32, ESP-NOW and WS2812 LEDs I created a festival light badge that automatically synchronises effects with its peers without configuration, pairing or external control. Each badge can leave or join a peer group without issue.
I got some the the young people at the none profit I volunteer at to get involved soldering, assembling, coding and wearing the badges.
My town, Oldham (UK) has an annual festival of Light: Illuminate.
I volunteer as a tech mentor at a local none profit tech hub, Inclusive Bytes. And this year we decided
to participate.
Our design is simple. A wearable badge that sychronises lighting effects. I used an ESP32 C3 combined with a LiPo BMS and a WS2812 LED ring. Plus of course a 3D printed badge mount.
I designed a schematic and PCB in Design Spark


and CNC routed the PCB

The intention was to make about 10 of these, but I knocked a couple out initially so I could get the software working.

A quick check of the hardware showed the current varying between 100-500mA. With a 1000mAh lipo battery that's about 2 hours worst case, which is fine for this application.

Once happy with the design, we got some of the young people at Inclusive Bytes to get some soldering practice in to make the remaining badges.

Once the badges were assembled it was time to start writing some lighting effects....but how does it all work?
To synchronise lighting effects across multiple nodes, you'd normally need a controller of some kind:
- A boss node, set in software or with a hardware switch
- A controller on the network
- Bluetooth pairing
- WiFi network
None of these are used. Any badge can join or leave a badge group at any time and the effects will still run.
The badges don't synchronise effects.....they synchronise time.
Each badge transmits it millis() value every 5ms or so over ESP-now. It quantises this time to 10ms to help prevent jitter.
transmittedTime = millis() - (millis() % 10)
As another badge receives the new value, it compares it against it's own millis() value and adopts the new time if it is greater.
Effectively, the node that has been running longest since it's last reset becomes the leader in that group.
All badges run the same software, and as time become synched across the group, the badges use their local time to calculate the effect and the effect state.
Time is further quantised to 50ms to further reduce timing errors.
effectTimeSlot = 5000 // Each effect runs for 5s
effectTime = ( millis() - (millis() % 50 ))
currentEffect = (effectTime / effectTimeSlot) % NumberOfEffects
effectStage = effectTime % numberOfStages // There is a time prescaler in here too, omitted for clarity

The codes all in C++. It includes an IEffect interface class for all effects to inherit from:
class IEffect
{
public:
virtual void Run(CRGB* strip, uint64_t systime) = 0;
};
And an effect manager that allows effects to be registered and selects the current effect.
class EffectManager
{
public:
void AddEffect(IEffect* effect);
void Run(CRGB* strip, uint64_t systime);
};
This topography means that any badge can leave or enter a group at any time. Once it has received the highest time vale, it is synched with the other badges. As the time propogates across a mesh, badges that cannot 'see' the original highest time badge just get theirs from their nearby peers.
The daya is transmitted in JSON format, I did this because it's easier for our young developers to see the data and the overhead is minimal. And the JSON gives very basic error checking.
{ "timer": 123456 }
In the real world I'd pack it in a struct with a CRC.
struct
{
uint64_t timeValue;
uint16_t crc
}TimePacket;
There are a few disadvantages using this method:
- There is no security. Any rogue ESP-NOW could join the group and send receive data.
- I avoided fades, any small differences in time values would be very noticeable. The same for on off colour flashes....such differences are very noticeable (this is why progressive jackpot values in casinos always count up. By looking at values in different machine positions, it's almost impossible to notice discrepancies).
- The 50ms 'tick' time for effects limits the effect quality...but this is not an issue.
- Different effects always run in the same sequence.
- If badge software version is inconsistent, synch will be out some of the time.
- A time slot of 5s per effect means careful coding is required to avoid effect truncation.
Why it works
- Quantised time prevents oscillation
- Fast broadcast ensures convergence
- Stateless design allows instant joining
- Simple patterns hide small timing differences
A surprisingly subtle hardware issue appeared during testing… I'd designed the PCB for the ESP32 to sit in sockets like these:

Whilst debugging I happened to remove one side the ESP32 from its socket, no pins were connected on this side, but it synched up! Putting tht side back caused it to fail again. There must be some RF wizardry happening here. To keep things simple, I just removed the sockets from the design.
Some of the young people at Inclusive Bytes chose to create their own lighting effects, and these were included in the final software.
SO with all the badges working , it was time for the event itself.
Unfortunately, The festival was held in February.....and Oldham is on a hilll...in the north west of England...in the foothills of the Pennines and so inevitably the rain was torrential and our attending numbers were down.
BUT....The badges worked.....they synched up and multiple people commented on the display. Overall, a success, and we'll be taking them to EMF Camp 2026.
Tony Goacher