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 sequencetx_node_id: The ESP32 assigned to transmitduration: 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 initialSyncBeaconand starts the cycle timercomplete_slot(): RecordsTdmSlotCompletedevents withcapture_qualitymetricscurrent_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:
- Beacon Broadcast: The coordinator calls
begin_cycle(), creates aSyncBeacon, and broadcasts it via UDP to all ESP32 nodes. - Clock Synchronization: Each ESP32 receives the beacon, applies
drift_correction_us, and enters receive mode. - Slot Transmission: When a node's allocated slot arrives, it transmits a burst of Null Data Packets (NDP) containing CSI preamble sequences.
- CSI Capture: All non-transmitting nodes capture the Wi-Fi frames and extract CSI subcarriers.
- 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:
rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/tdm.rs— Core protocol logic:TdmSchedule,TdmSlot,SyncBeacon,TdmCoordinator, drift compensation algorithms, and unit tests.rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/mod.rs— Public API re-exports exposing thetdm,secure_tdm, andquic_transportmodules.rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/secure_tdm.rs— Optional encrypted variant of the TDM protocol using cryptographic beacon signatures.rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/quic_transport.rs— High-throughput transport layer for CSI payload delivery over UDP/QUIC.
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
SyncBeaconcorrections 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.rswith theTdmCoordinatormanaging cycle state andTdmScheduledefining 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →