Skip to content

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:

slope  = Σ(xi − x̄)(yi − ȳ) / Σ(xi − x̄)²
offset = ȳ − slope · x̄

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:

world_us = unix_us_last + (esp_timer_get_time() - last_rx_us)

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

Index  0   1     2      3     4     5     6
Color  Red  Orange Yellow Green Cyan  Blue  Purple

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