WSN Time Sync — FTSP¶
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 (NODEROLEGATEWAY): Connects to Enterprise WiFi, syncs via NTP, broadcasts FTSP beacons.
- Leaf Node (NODEROLELEAFNODE): 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 (localrx, reftime_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 (unixus0, rxus0) |
| 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