Close

How do the badges synchronise?

A project log for Self-synchronising LED badges using ESP-NOW

Light Festival Badges automatically sync their effects with ESP-NOW. No pairing, No controller, No WiFi

tony-goacherTony Goacher 05/03/2026 at 18:260 Comments

The badges don't really synchronise, they converge on time.

At first glance these badges look synchronised but there’s no master and no pairing.

And yet the animations line up.

It works because they don't actually synchronise time, they converge on it.

The Rule

Each badge keeps its own local time:

t = millis() + offset

…and periodically broadcasts it over ESP-NOW, quantised to 10ms.

When a badge hears another:

if (t_rx > t_local)   
    set offset so that millis() + offset matches new time

That’s the entire “protocol”.

There is no averaging, no correction loops. And of particular importance, the local time never goes backwards.

Inevitably, one badges millis() value will be the highest in a group. When its broadcasts it time to other badges, they adjust their time offset so their local timestamp matches the new value.

The new time is then rebroadcast. So the highest time propogates through the group. Even if not all badges are directly in range, the update spreads hop-by-hop until everyone catches up.

Normally this kind of thing would oscillate or fight itself.

Two things stop that:

  1. Time only moves forward No backwards correction = no ping-ponging
  2. Time is quantised Small differences collapse into the same value

So instead of chasing each other endlessly, nodes quickly “snap” to the same tick. Inevitably there will always be small differences in each badges time, but this is handled with another quantisation.

It looks perfect because the LED effect rendering is stateless.

Rendering  uses 50ms quantisation of the effet time to further absorb any timing differences:

MILLIS_PER_EFFECT = 5000 // 5 seconds for each effect

effectTime = t - (t % 50)
effectId = (effectTime / MILLIS_PER_EFFECT)  % numEffects
effectStage = t % numEffectStages

So as soon as two badges agree on time, they produce identical output.

There’s no history to repair, no frames to catch up — just the current tick.

As a result, nodes can join (or leaver) the group at any time. They instantly sync to the effect state determined by 'global' time.
Packet loss is irrelevant. Each badge runs off it's local millis() which it updates peridodically from the broadcast time, and because there is no central coordinator, the effects just continue even if comms fail (though they will drift out of sync until a new broadcast is received)

What looks like tight synchronisation is really just:

a network agreeing on the largest timestamp, over and over again

In conclusion, 
If you're willing to sacrifice time resolution, perfect time syn isn't needed.

It’s enough to let time “spread” through the system, and design everything else so small errors disappear.

And in practice that ends up looking indistinguishable from perfect sync.

Discussions