WSN Time Sync — FTSP¶
Timestamp units, synchronization state and accuracy¶
FTSP estimates clock mappings using beacons and regression. Microsecond units specify representation; they do not establish 1 µs end-to-end synchronization error. RGB synchronization videos illustrate visible timing alignment, not an error distribution. Record timestamp locations, regression window, synchronization age, packet loss, drift and measurement equipment, then report bias, quantiles and worst case.
Gateway NTP anchoring and leaf FTSP estimates contribute separate errors. Before using multi-node data for mode shapes or phase comparisons, evaluate alignment error relative to the sample interval.
Flooding Time Synchronization Protocol over ESP-NOW
The FTSP module provides microsecond-precision time synchronization across a wireless sensor network (WSN). One node acts as the gateway (root/master), obtaining wall-clock time via NTP and broadcasting it over ESP-NOW. All other nodes operate as leaf nodes (slaves), receiving the broadcasts and correcting their local clock via linear regression.
Demo: Multi-node FTSP time synchronization — all nodes cycle through the same marquee colors simultaneously
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ GATEWAY (root) │
│ │
│ NTP (Internet) ──► tiny_ntp_record_sync() │
│ │ │
│ ▼ │
│ ftsp_gateway_task() ──► esp_now_send() broadcast │
│ │ every FTSP_BEACON_INTERVAL_MS │
│ │ (default 1000 ms = 1 Hz) │
│ ▼ │
│ Beacon packet (26 bytes extended): │
│ ref_time_us – gateway local time @ TX (µs) │
│ unix_us – NTP world time @ TX (µs, Unix epoch) │
│ seq – rolling sequence number │
│ node_id – gateway identifier │
│ flags – world_time_valid, sync_sequence │
│ ntp_quality – NTP sync quality (0-100%) │
└───────────────────────┬─────────────────────────────────────┘
│ ESP-NOW (broadcast)
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ LEAF NODE (slave) │
│ │
│ node_espnow_recv_cb() ──► ftsp_sniffer_cb() │
│ │ (captures rx_us with <1 ms jitter)│
│ ▼ │
│ Circular buffer (FTSP_TABLE_SIZE pairs): │
│ (local_us, ref_us) │
│ │ │
│ ▼ │
│ Least-squares linear regression: │
│ slope = Σ(x-̄x)(y-ȳ) / Σ(x-̄x)² │
│ offset = ȳ − slope · ̄x │
│ global_us = slope × local_us + offset │
│ │ │
│ ▼ │
│ World time (µs precision): │
│ world_us = global_us + unix_offset_us │
│ unix_offset_us = beacon.unix_us - beacon.ref_time_us │
│ │
│ Direct-offset fallback (drift resets per beacon): │
│ world_us = unix_us_last + (now - local_rx) │
└──────────────────────────────────────────────────────────────┘
Key Concepts¶
Node Roles¶
The node role is selected at compile time via NODE_ROLE in node_config.h:
#define NODE_ROLE_GATEWAY 0
#define NODE_ROLE_LEAFNODE 1
#ifndef NODE_ROLE
#define NODE_ROLE NODE_ROLE_GATEWAY
#endif
- Gateway (NODE_ROLE_GATEWAY): Connects to Enterprise WiFi, syncs via NTP, broadcasts FTSP beacons.
- Leaf Node (NODE_ROLE_LEAFNODE): Listens for FTSP beacons, computes clock correction, provides world time.
Time Sources¶
| Source | Macro | Description |
|---|---|---|
| Boot time | FTSP_TIME_BOOT (0) | esp_timer_get_time() — µs since MCU boot. Always available. |
| World time | FTSP_TIME_WORLD (1) | Unix epoch µs. Requires prior NTP sync on gateway. Falls back to boot time automatically. |
Beacon Packet Format¶
Basic beacon (16 bytes): Contains magic, sequence number, gateway reference time.
Extended beacon (26 bytes): Adds NTP world time (unix_us with full µs precision), flags, and NTP quality — used when world time is available.
The gateway captures ref_time_us immediately before esp_now_send() to minimise TX jitter (typically < 100 µs).
Clock Drift Compensation¶
The leaf node maintains a circular buffer of (local_rx, ref_time_us) pairs. After FTSP_MIN_PAIRS_FOR_SYNC (default 4) valid pairs, a least-squares linear regression is solved:
The slope corrects for relative crystal frequency error (typically < 40 ppm). The ftsp_precision module tracks drift independently using an exponential moving average.
Direct-Offset Mode (Zero Regression)¶
The ftsp_get_world_time_direct_us() function uses the most recent beacon's (rx_us, unix_us) pair directly:
This eliminates drift accumulation entirely — error resets to zero with every new beacon. Maximum error between beacons: crystal_drift_ppm × beacon_interval ≈ 40 µs at 1 Hz.
Compile-Time Configuration¶
| Macro | Default | Description |
|---|---|---|
FTSP_BEACON_INTERVAL_MS | 1000 | Beacon interval (ms). Shorter → faster convergence, higher RF load. |
FTSP_TABLE_SIZE | 10 | Number of (local, ref) timestamp pairs in circular buffer. |
FTSP_MIN_PAIRS_FOR_SYNC | 4 | Minimum pairs required before declaring "synced". |
FTSP_USE_WORLD_TIME | 0 | 0 = boot time, 1 = world time (requires NTP). |
FTSP_FAKE_EPOCH_US | 1577836800000000 | Fake epoch (2020-01-01) used when NTP is unavailable. |
FTSP_GATEWAY_TASK_STACK | 2048 | Gateway task stack size (bytes). |
FTSP_GATEWAY_TASK_PRIORITY | 6 | Gateway task priority. |
API Reference¶
Gateway¶
esp_err_t ftsp_gateway_start(void); // Start beacon broadcast
esp_err_t ftsp_gateway_stop(void); // Stop beacon broadcast
bool ftsp_gateway_is_running(void);
Leaf Node¶
esp_err_t ftsp_leafnode_start(void); // Start beacon receiver
esp_err_t ftsp_leafnode_stop(void); // Stop beacon receiver
bool ftsp_is_synced(void); // Check if FTSP synced
esp_err_t ftsp_get_global_time(int64_t *global); // Get FTSP-corrected global time
// World time API (requires gateway NTP)
esp_err_t ftsp_get_world_time_us(uint64_t *world_us); // FTSP regression
esp_err_t ftsp_get_world_time_direct_us(uint64_t *world_us); // Direct offset (no drift)
esp_err_t ftsp_get_world_time_sec(uint32_t *world_sec); // Seconds since epoch
esp_err_t ftsp_get_world_datetime_us(TinyDateTime_t *dt); // Calendar time + µs
esp_err_t ftsp_get_gateway_ntp_info(uint64_t *unix_us, uint8_t *ntp_quality);
Sync Visualization¶
esp_err_t ftsp_sync_viz_init(void); // Start visualization task
ftsp_sync_viz_state_t ftsp_sync_viz_get_state(void); // Get current sync state
void ftsp_sync_viz_get_marquee_color(uint64_t world_time_us,
uint8_t *r, uint8_t *g, uint8_t *b);
esp_err_t ftsp_sync_viz_gateway_start_sync(void); // Gateway: start sync sequence
bool ftsp_sync_viz_gateway_is_syncing(void);
Sync Visualization States¶
| State | LED Color | Description |
|---|---|---|
INIT | Off | Module started, not yet active |
WAITING | White | Gateway: waiting for NTP; Leaf: waiting for beacons |
SYNC_SEQ | — | Gateway: broadcasting 6-beacon sync sequence |
COLLECTING | — | Leaf: collecting sync beacons |
SYNCED | Green | Time synchronized successfully |
MARQUEE | 7-color cycle | All nodes show same color simultaneously |
Marquee Colors¶
The color is derived from world_time_sec % 7, ensuring all nodes show the same color at the same time — a visual proof of synchronization.
Sync Sequence Protocol¶
After NTP sync, the gateway broadcasts a burst of 6 special beacons with precise timing:
| Beacon | Time (s) | Purpose |
|---|---|---|
| 0 | t=0 | Reference (unix_us_0, rx_us_0) |
| 1 | t=8 | First timing pair |
| 2 | t=9 | Second timing pair |
| 3 | t=10 | Third timing pair |
| 4 | t=11 | Fourth timing pair |
| 5 | t=12 | Fifth timing pair |
The leaf collects all 6, computes per-pair slopes, averages them, and enters SYNCED state. Both gateway and leaf target the same marquee start time (last_beacon_unix_sec + HOLD_SEC), ensuring color-synchronized LED cycling across all nodes.
Integration with tiny_toolbox¶
The FTSP module uses tiny_time's NTP sync hook to receive NTP sync notifications without a circular CMake dependency:
// In tiny_toolbox (tiny_time.c):
// - Fires s_ntp_sync_hook((uint32_t)tv->tv_sec) immediately on SNTP response
// In tiny_measurement (ftsp_gateway.c):
// - Registers hook: tiny_ntp_set_sync_hook(ftsp_ntp_record_sync);
This design keeps layers clean: - tiny_toolbox — lowest layer, provides tiny_ntp_set_sync_hook() as a function pointer - tiny_measurement — higher layer, implements ftsp_ntp_record_sync() and registers it as the hook