# How to Build a Multistatic Mesh with ESP32 for 360-Degree Room Coverage Using RuView

> Create a 360 room multistatic mesh with ESP32 and RuView. Deploy ESP32-S3 nodes and fuse CSI data for full room pose estimates. Learn how to build your own.

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

---

**You can build a 360-degree multistatic sensing mesh by deploying 4–6 ESP32-S3 nodes running RuView's CSI firmware, provisioning them with unique node IDs, and running the Rust-based RuView server to fuse multistatic Channel State Information (CSI) into full-room pose estimates.**

RuView is an open-source Wi-Fi sensing stack that transforms inexpensive ESP32-S3 hardware into a professional-grade multistatic radar. By following the **ADR-029 – RuvSense multistatic sensing mode** specification, you can achieve sub-inch pose jitter, vital-sign monitoring, and persistent-field modeling across an entire room using only Wi-Fi signals.

## Hardware Layout for 360-Degree Coverage

For complete room coverage, position nodes to maximize TX-RX link diversity. According to ADR-029, the baseline configuration uses **4 nodes** placed at wall midpoints approximately 2 meters apart, creating roughly 12 unique links. For production-grade robustness, deploy **6 nodes** to generate 30 redundant links (N·(N-1)), improving SNR and enabling multi-person separation.

Each node runs identical firmware from [`firmware/esp32-csi-node/main/csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/firmware/esp32-csi-node/main/csi_collector.c), which handles UDP CSI serialization, channel hopping, and rate-limited output to prevent lwIP buffer exhaustion.

## Flashing the ESP32 CSI Firmware

Build the firmware using the provided install script, or flash the pre-built binary directly.

To build from source:

```bash
./install.sh --profile iot --yes

```

This generates `firmware/esp32-csi-node/build/esp32_csi_node.bin`.

To flash a node (replace port as needed):

```bash
esptool.py --chip esp32s3 --port /dev/ttyUSB0 \
  --baud 460800 write_flash 0x1000 \
  firmware/esp32-csi-node/build/esp32_csi_node.bin

```

The firmware implements **channel hopping** across 2.4 GHz channels 1, 6, and 11 (configurable in [`csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/csi_collector.c) lines 66-73) and a **TDM schedule** where each node transmits null-data-packets in assigned slots while others listen, as defined 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).

## Provisioning Each Node with Wi-Fi Credentials

Use [`scripts/provision.py`](https://github.com/ruvnet/RuView/blob/main/scripts/provision.py) to write Wi-Fi credentials, node IDs, and sensing parameters into the ESP32 NVS partition. Each node requires a unique `--node-id` (0-255) so the server can differentiate streams.

```bash
python scripts/provision.py \
  --port /dev/ttyUSB0 \
  --ssid "MyWiFi" \
  --password "SuperSecret" \
  --target-ip 192.168.1.42 \
  --target-port 5005 \
  --node-id 0 \
  --edge-tier 2 \
  --pres-thresh 0.6 \
  --fall-thresh 2.0 \
  --vital-window 64 \
  --vital-interval 200 \
  --subk-count 16

```

Key parameters include:
- `--edge-tier 2`: Enables full AI processing on the device (tier 0 = raw only)
- `--pres-thresh` and `--fall-thresh`: Presence and fall detection thresholds used by edge security modules
- `--target-ip` and `--target-port`: The server address (default UDP port 5005)

The provisioner generates a CSV NVS map and calls [`nvs_partition_gen.py`](https://github.com/ruvnet/RuView/blob/main/nvs_partition_gen.py) to create the binary blob, then flashes it to the device (implementation in [`provision.py`](https://github.com/ruvnet/RuView/blob/main/provision.py) lines 33-71 for CSV creation, 74-104 for binary generation, and 118-135 for flashing).

## Running the RuView Multistatic Server

The Rust server ingests UDP CSI frames from all nodes, performs multi-band fusion, multistatic viewpoint fusion, coherence gating, and Kalman tracking.

Launch the server:

```bash
cd rust-port/wifi-densepose-rs
cargo run -p wifi-densepose-sensing-server --release \
  --listen-udp 0.0.0.0:5005 \
  --listen-http 0.0.0.0:3000

```

The server pipeline includes:
- **Multi-Band Fusion** ([`wifi-densepose-signal/src/ruvsense/multiband.rs`](https://github.com/ruvnet/RuView/blob/main/wifi-densepose-signal/src/ruvsense/multiband.rs)): Merges per-channel CSI into `MultiBandCsiFrame` (lines 39-55)
- **Phase Alignment** ([`wifi-densepose-signal/src/ruvsense/phase_align.rs`](https://github.com/ruvnet/RuView/blob/main/wifi-densepose-signal/src/ruvsense/phase_align.rs)): Removes LO phase offsets using a Neumann solver
- **Multistatic Fusion** ([`wifi-densepose-signal/src/ruvsense/multistatic.rs`](https://github.com/ruvnet/RuView/blob/main/wifi-densepose-signal/src/ruvsense/multistatic.rs)): Cross-node attention with `ruvector-attn-mincut` (ADR-029-p4)
- **Coherence Gating** ([`wifi-densepose-signal/src/ruvsense/coherence.rs`](https://github.com/ruvnet/RuView/blob/main/wifi-densepose-signal/src/ruvsense/coherence.rs) and [`coherence_gate.rs`](https://github.com/ruvnet/RuView/blob/main/coherence_gate.rs)): Computes Z-score coherence and decides Accept/PredictOnly/Reject/Recalibrate (enum `GateDecision` lines 58-67)
- **Pose Tracking** ([`wifi-densepose-signal/src/ruvsense/pose_tracker.rs`](https://github.com/ruvnet/RuView/blob/main/wifi-densepose-signal/src/ruvsense/pose_tracker.rs)): 17-keypoint Kalman filter with Re-ID (cost function lines 94-98)

## Verifying and Visualizing Results

Connect to `http://localhost:3000` to access the **Observatory UI**, which displays:
- 3-D skeleton overlay with 17 keypoints
- Real-time vital-sign plots (breathing rate, heart rate)
- Presence heat-maps and persistent field fingerprints

The UI consumes WebSocket data from `/ws`. For custom integrations, query the REST API at `/api/v1/pose`, which returns JSON matching the internal `Pose` struct defined in [`pose_tracker.rs`](https://github.com/ruvnet/RuView/blob/main/pose_tracker.rs) (lines 77-84).

## Summary

- **Deploy 4-6 ESP32-S3 nodes** around the room perimeter to create a multistatic mesh with 360-degree coverage.
- **Flash the firmware** from [`firmware/esp32-csi-node/main/csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/firmware/esp32-csi-node/main/csi_collector.c) using [`esptool.py`](https://github.com/ruvnet/RuView/blob/main/esptool.py) or the [`./install.sh`](https://github.com/ruvnet/RuView/blob/main/./install.sh) script.
- **Provision each node** with [`scripts/provision.py`](https://github.com/ruvnet/RuView/blob/main/scripts/provision.py) to set unique node IDs, Wi-Fi credentials, and sensing thresholds.
- **Run the Rust server** (`wifi-densepose-sensing-server`) to fuse multi-band CSI, perform multistatic fusion, and track 17-keypoint poses.
- **Visualize results** via the Observatory UI at port 3000 or consume the REST/WebSocket API for custom applications.

## Frequently Asked Questions

### How many ESP32 nodes are required for 360-degree coverage?

You need **at least 4 nodes** for baseline coverage, placed at wall midpoints approximately 2 meters apart. This configuration generates roughly 12 unique TX-RX links. For production deployments requiring higher SNR and multi-person separation, use **6 nodes** to create 30 redundant links (N·(N-1)), as specified in ADR-029.

### What is the difference between edge-tier 0 and edge-tier 2?

**Edge-tier 0** streams raw CSI frames without on-device processing, requiring the server to handle all computation. **Edge-tier 2** enables full AI processing on the ESP32, including presence detection, fall detection, and vital-sign extraction, reducing bandwidth and server load. Configure this via the `--edge-tier` flag in [`scripts/provision.py`](https://github.com/ruvnet/RuView/blob/main/scripts/provision.py).

### How does the server handle synchronization between multiple nodes?

The server implements **Time Division Multiplexing (TDM)** scheduling defined 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). Each node transmits null-data-packets in assigned slots while others listen, creating deterministic TX/RX pairs. The firmware in [`csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/csi_collector.c) handles the slot timing, while the server performs **multistatic fusion** using `ruvector-attn-mincut` attention mechanisms to align and fuse data from all viewpoints.

### Can I use 5 GHz Wi-Fi instead of 2.4 GHz?

Yes. While the default configuration uses 2.4 GHz channels 1, 6, and 11 for channel hopping (configured in [`csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/csi_collector.c) lines 66-73), the firmware supports 5 GHz operation. You must modify the channel list in [`csi_collector.c`](https://github.com/ruvnet/RuView/blob/main/csi_collector.c) and ensure your ESP32-S3 variant supports the 5 GHz band (note: standard ESP32-S3 is 2.4 GHz only; you may need ESP32-C6 or ESP32-C5 for 5 GHz). The server handles multi-band fusion automatically via the `MultiBandCsiFrame` structure in [`multiband.rs`](https://github.com/ruvnet/RuView/blob/main/multiband.rs).