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...
Tony Goacher
Raunaq Bose
Bruce Land