# ESP32 TDM Protocol: How RuView Orchestrates Multistatic CSI Streaming

> Learn how RuView leverages the ESP32 TDM protocol for synchronized multistatic CSI streaming. Achieve precise TX/RX scheduling and drift compensation with distributed radios.

- Repository: [rUv/RuView](https://github.com/ruvnet/RuView)
- Tags: deep-dive
- Published: 2026-03-08

---

**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](https://github.com/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`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/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:

```rust
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:

```rust
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:

```rust
// 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:

```rust
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:

```rust
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`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/mod.rs)** — Public API re-exports exposing the `tdm`, `secure_tdm`, and `quic_transport` modules.
- **[`rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/secure_tdm.rs`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/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 `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`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/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.