ESP32 TDM Protocol: How RuView Orchestrates Multistatic CSI Streaming

RuView uses a custom Time-Division-Multiplexed (TDM) sensing protocol implemented on ESP32 nodes to coordinate multistatic Wi-Fi Channel State Information (CSI) streaming, enabling deterministic TX/RX slot scheduling and microsecond-precision drift compensation across distributed radios.

The ESP32 TDM protocol is the synchronization backbone of the RuView sensing pipeline, defined in the Rust module wifi_densepose_hardware::esp32::tdm within the ruvnet/RuView repository. This protocol transforms multiple ESP32-based Wi-Fi nodes into a coherent multistatic radar array, where each radio transmits during exclusive time slots while all others capture CSI measurements.

Core Components of the ESP32 TDM Protocol

TdmSchedule and Slot Allocation

The TdmSchedule struct defines the deterministic timing plan that assigns every ESP32 node a unique transmission window. In rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/tdm.rs, schedules are constructed via TdmSchedule::uniform() or pre-defined configurations like TdmSchedule::default_4node().

Each TdmSlot contains:

  • index: Position in the cycle sequence
  • tx_node_id: The ESP32 assigned to transmit
  • duration: TX window length (typically 4 ms)
  • guard_interval: Safety buffer (default 1 ms) to prevent overlap

The default 4-node, 20 Hz configuration creates a 50 ms cycle: four slots of 4 ms TX plus 1 ms guard intervals, followed by a 30 ms processing window. This guarantees N × (N − 1) bistatic CSI links per cycle, as every node transmits while all others receive.

SyncBeacon and Network Synchronization

Clock synchronization relies on a 16-byte UDP SyncBeacon broadcast at each cycle start. The beacon structure defined in tdm.rs uses little-endian encoding:

  • Bytes 0–7: cycle_id (u64)
  • Bytes 8–11: cycle_period_us (u32)
  • Bytes 12–13: drift_correction_us (i16)
  • Bytes 14–15: Reserved

Nodes call SyncBeacon::from_bytes() to parse incoming beacons and adjust local timers by the embedded drift_correction_us value. The 1 ms guard interval accommodates the worst-case crystal drift of 0.5 µs per 50 ms cycle.

TdmCoordinator and Cycle Management

The TdmCoordinator struct runs on the aggregator node to drive the sensing cycle. Key methods include:

  • begin_cycle(): Generates the initial SyncBeacon and starts the cycle timer
  • complete_slot(): Records TdmSlotCompleted events with capture_quality metrics
  • current_beacon(): Retrieves the current beacon for re-transmission if nodes miss the initial broadcast

How the ESP32 TDM Protocol Streams CSI Data

The CSI streaming flow follows a strict state machine orchestrated by the coordinator:

  1. Beacon Broadcast: The coordinator calls begin_cycle(), creates a SyncBeacon, and broadcasts it via UDP to all ESP32 nodes.
  2. Clock Synchronization: Each ESP32 receives the beacon, applies drift_correction_us, and enters receive mode.
  3. Slot Transmission: When a node's allocated slot arrives, it transmits a burst of Null Data Packets (NDP) containing CSI preamble sequences.
  4. CSI Capture: All non-transmitting nodes capture the Wi-Fi frames and extract CSI subcarriers.
  5. Aggregation: During the processing window, the coordinator collects CSI from all slots and fuses the multistatic measurements.

This time-division approach eliminates packet collisions and ensures phase-coherent measurements across the distributed array.

Clock Drift Compensation in Distributed ESP32 Networks

ESP32 crystals exhibit drift rates up to ±10 ppm (parts per million). Over a 50 ms cycle, this accumulates to approximately 0.5 µs—enough to corrupt slot alignment without correction.

The coordinator measures the elapsed time between successive begin_cycle() calls and compares it against the expected cycle_period. The deviation is accumulated in cumulative_drift_us, negated, clamped to an i16 range, and embedded in the next beacon's drift_correction_us field. Nodes apply this offset locally, maintaining network-wide synchronization without hardware-level time-keeping or external GPS.

Implementing the ESP32 TDM Protocol in Rust

Creating a Custom Schedule

Define a 6-node, 10 Hz configuration with 5 ms TX slots:

use wifi_densepose_hardware::esp32::{TdmSchedule, TdmCoordinator};
use std::time::Duration;

let node_ids = &[0u8, 1, 2, 3, 4, 5];

// 5 ms TX, 1 ms guard, 50 ms processing → 100 ms cycle (10 Hz)
let schedule = TdmSchedule::uniform(
    node_ids,
    Duration::from_millis(5),
    Duration::from_micros(1_000),
    Duration::from_millis(50),
).expect("valid schedule");

println!("Cycle period = {} ms", schedule.cycle_period().as_millis());

Starting a Sensing Cycle

Initialize the coordinator and broadcast the synchronization beacon:

let mut coordinator = TdmCoordinator::new(schedule);
let beacon = coordinator.begin_cycle();

// Serialize to UDP payload
let payload = beacon.to_bytes();
// udp_socket.send_to(&payload, "255.255.255.255:4242")?;

Marking Slot Completion

Record completion after receiving CSI from a specific slot:

// Slot 2 completed with 92% of expected frames captured
let event = coordinator.complete_slot(2, 0.92);
println!(
    "Slot {} (node {}) completed, quality {:.0}%",
    event.slot_index,
    event.tx_node_id,
    event.capture_quality * 100.0
);

Detecting Cycle Completion and Retrieving CSI

Check for cycle end and trigger data fusion:

if coordinator.is_cycle_complete() {
    println!("Cycle {} complete – {} slots processed.",
             coordinator.cycle_id(),
             coordinator.completed_slot_count());
    // Fuse CSI data during the processing window
}

Handling Missed Beacons

Re-broadcast the current beacon if network packet loss occurs:

let repeat_beacon = coordinator.current_beacon();
let payload = repeat_beacon.to_bytes();
// udp_socket.send_to(&payload, "255.255.255.255:4242")?;

Source Code Reference

The ESP32 TDM protocol implementation spans the following files in the ruvnet/RuView repository:

Summary

  • The ESP32 TDM protocol uses time-division multiplexing to coordinate TX/RX slots across distributed Wi-Fi nodes, preventing collisions in multistatic CSI capture.
  • Clock drift compensation via SyncBeacon corrections maintains sub-millisecond synchronization despite ESP32 crystal tolerances of ±10 ppm.
  • The 4-node default configuration achieves 20 Hz update rates with 50 ms cycles, generating 12 bistatic links per cycle (N × (N − 1)).
  • Implementation resides in tdm.rs with the TdmCoordinator managing cycle state and TdmSchedule defining deterministic slot assignments.
  • Secure variants and QUIC transport layers extend the protocol for production deployments requiring encryption or high-throughput streaming.

Frequently Asked Questions

What is the ESP32 TDM protocol in RuView?

The ESP32 TDM protocol is a custom Time-Division-Multiplexed sensing framework that synchronizes multiple ESP32 Wi-Fi radios to transmit and receive in deterministic slots. According to the ruvnet/RuView source code in wifi-densepose-hardware/src/esp32/tdm.rs, it enables multistatic CSI streaming by ensuring only one node transmits at a time while all others capture channel measurements.

How does the ESP32 TDM protocol handle clock drift?

The protocol measures timing deviations in the TdmCoordinator and embeds correction values in the SyncBeacon. Because ESP32 crystals drift up to ±10 ppm, the coordinator accumulates cumulative_drift_us, negates it, and transmits the value as drift_correction_us (a 16-bit signed integer) in the next beacon. Nodes adjust their local timers by this offset to maintain alignment.

What is the default timing configuration for ESP32 TDM in RuView?

The default configuration uses TdmSchedule::default_4node() with four ESP32 nodes operating at 20 Hz. Each cycle lasts 50 ms: four transmission slots of 4 ms each, separated by 1 ms guard intervals, followed by a 30 ms processing window. This produces 12 unique bistatic CSI measurements per cycle.

How are CSI packets synchronized across multiple ESP32 nodes?

Synchronization occurs via UDP SyncBeacon broadcasts at the start of every cycle. The coordinator generates the beacon using begin_cycle(), and all nodes parse it via SyncBeacon::from_bytes() to extract the cycle_id, cycle_period_us, and drift_correction_us. Nodes then wait for their specific slot index before transmitting NDP frames, ensuring phase-coherent CSI capture across the distributed array.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →