-
How do the badges synchronise?
05/03/2026 at 18:26 • 0 commentsThe 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:
- Time only moves forward No backwards correction = no ping-ponging
- 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.
-
The 50ms Trick: Quantisation for Mesh-Synchronised Effects
04/30/2026 at 07:12 • 0 commentsOne of the more subtle problems in the badge mesh isn't communication — it's agreement.
Each badge runs its own local clock and steps through lighting effects based solely on that value. ESP-NOW gives us low-latency broadcast, but it doesn’t give us a shared notion of time. Even tiny timing differences (a few milliseconds) quickly turn into visible phase drift in animations.
The Problem
If every node advances its animation based purely on
millis(), then:- Two badges starting “together” won’t stay together
- Packet latency introduces small offsets
- Clock drift accumulates over time
The result: effects that should look synchronised.....don't.
Don’t Fight Time — Quantise It.
Instead of trying to perfectly synchronise clocks (hard), I switched to quantising time into fixed slices.
All animation timing snaps to a global step size:
50 ms per tick (20 Hz)
Each node converts its local time into a shared “tick number”:
tick = millis() - (millis() % 50)
Now the key idea: Nodes don’t need identical clocks — they just need to agree on the same tick.
Why 50 ms?
It’s a sweet spot:
- Fast enough for reasonable animation speeds (20Hz)
- Slow enough to tolerate packet jitter
- Divides nicely into common effect durations
I experimented with smaller steps (10 ms), but network jitter started to dominate. Larger steps made animations visibly steppy.
Mesh Alignment
When a badge receives a packet, it doesn’t try to syncit's millis() value. Instead it just adds a simple offset:
alignedMillis = (millis() + offset)
Incoming messages carry the sender’s tick. The receiver nudges its offset to match.
Because everything is quantised:
- Small timing errors collapse into the same tick
- Nodes naturally “snap” back into alignment
- No complex clock sync required
The Effect
This one change made a huge visual difference. Each badge calulates it's current effect from local aligned time.
effect = alignedTime % numEffects;And each effect calculates it's current state in a similar fashion.
Before:
- Effects drift apart
- Synchronisation feels “loose”
After:
- Effects lock together cleanly after a less than a few seconds.
- The system feels intentional and coordinated
Takeaway
Perfect synchronisation is expensive.
But quantised synchronisation is cheap, robust, and good enough to look perfect.
Sometimes the trick isn’t making time continuous — it’s making everyone agree on the same discrete version of it.
-
Subtle synch fault due to ESP mounting choice.
04/28/2026 at 13:49 • 0 commentsA Strange ESP32 RF Bug (and why sockets had to go)
During early builds, I mounted the ESP32-C3 Mini modules using standard 0.1" female headers so they could be easily replaced.
This caused a wierd issue.
ESP-NOW communication was unreliable—some badges would transmit, but often fail to receive broadcasts from others. At first this looked like a software issue, but everything checked out.
Then something odd happened.
If I lifted one side of the ESP32 slightly out of the socket… it started working perfectly.
Plug it back in it was broken, lift one side it works
None of the pins on that side were even used.
What was going on?
At 2.4 GHz, small physical details matter more than expected. The most likely causes are:
- Slight ground impedance differences across multiple pins
- Parasitic capacitance introduced by the socket
- Subtle detuning of the onboard antenna due to nearby geometry
In other words, the connector itself was interfering with RF performance.
The fix
For the production badges, I removed the sockets entirely and soldered the ESP32 modules directly to the PCB.
This completely resolved the issue:
- Reliable ESP-NOW reception
- Stable synchronisation across groups
Takeaway
Sometimes the most subtle problems in a system aren’t in the code or the protocol—but in a few millimetres of hardware.
At RF frequencies, “it shouldn’t matter” often means “it definitely matters”.
Tony Goacher