Skip to content

⭐ Tutorial: ESP32-S3 CSI Pipeline End-to-End Setup (ADR-018) #34

@ruvnet

Description

@ruvnet

ESP32-S3 CSI Pipeline: Step-by-Step Tutorial (ADR-018)

Complete end-to-end guide for building, flashing, and running the WiFi CSI human presence detection pipeline using an ESP32-S3 and the Rust aggregator.

Verified hardware: ESP32-S3-DevKitC-1 (CP2102 UART, MAC 3C:0F:02:EC:C2:28)
Verified performance: ~20 Hz CSI streaming, 64/128/192 subcarrier frames, RSSI -47 to -88 dBm


Prerequisites

Component Version Install
Docker Desktop 28.x+ https://www.docker.com/products/docker-desktop
esptool 5.x+ pip install esptool
Rust toolchain stable https://rustup.rs
CP210x driver latest https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers
ESP32-S3 board any DevKitC-1, WROOM, or similar

Step 1: Clone the repository

git clone https://github.com/ruvnet/wifi-densepose.git
cd wifi-densepose

Step 2: Build the Rust aggregator

cd rust-port/wifi-densepose-rs
cargo build -p wifi-densepose-hardware --bin aggregator --release

The binary will be at target/release/aggregator (or aggregator.exe on Windows).

Step 3: Configure WiFi credentials

Create or edit firmware/esp32-csi-node/sdkconfig.defaults:

CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESP_WIFI_CSI_ENABLED=y
CONFIG_CSI_NODE_ID=1
CONFIG_CSI_WIFI_SSID="YOUR_WIFI_SSID"
CONFIG_CSI_WIFI_PASSWORD="YOUR_WIFI_PASSWORD"
CONFIG_CSI_TARGET_IP="192.168.1.20"
CONFIG_CSI_TARGET_PORT=5005
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

Replace:

  • YOUR_WIFI_SSID with your WiFi network name
  • YOUR_WIFI_PASSWORD with your WiFi password
  • 192.168.1.20 with the IP of the machine running the aggregator

Security: sdkconfig.defaults is gitignored. Never commit real credentials.

Step 4: Build firmware with Docker

cd firmware/esp32-csi-node

# Linux/macOS:
docker run --rm -v "$(pwd):/project" -w /project \
  espressif/idf:v5.2 bash -c "idf.py set-target esp32s3 && idf.py build"

# Windows (Git Bash):
MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd -W)://project" -w //project \
  espressif/idf:v5.2 bash -c "idf.py set-target esp32s3 && idf.py build"

Build output appears in build/ (~960 compilation steps, takes 3-5 minutes first time).

Step 5: Find your serial port

OS Command Typical Port
Windows Device Manager > Ports COM7
Linux ls /dev/ttyUSB* /dev/ttyUSB0
macOS ls /dev/cu.usbserial* /dev/cu.usbserial-0001

Step 6: Flash firmware to ESP32-S3

cd firmware/esp32-csi-node/build

python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
  --before default-reset --after hard-reset \
  write-flash --flash-mode dio --flash-freq 80m --flash-size 4MB \
  0x0 bootloader/bootloader.bin \
  0x8000 partition_table/partition-table.bin \
  0x10000 esp32-csi-node.bin

Replace COM7 with your serial port. Expected output:

Hash of data verified.
Leaving...
Hard resetting via RTS pin...

Step 7: Open firewall (Windows only)

Run in elevated PowerShell:

netsh advfirewall firewall add rule name="ESP32 CSI" dir=in action=allow protocol=UDP localport=5005

Step 8: Run the aggregator

cargo run -p wifi-densepose-hardware --bin aggregator -- --bind 0.0.0.0:5005 --verbose

Expected output when ESP32 connects:

Listening on 0.0.0.0:5005...
  [148 bytes from 192.168.1.71:60764]
[node:1 seq:0] sc=64 rssi=-49 amp=9.5
  [276 bytes from 192.168.1.71:60764]
[node:1 seq:1] sc=128 rssi=-64 amp=16.0

Step 9: Verify presence detection

Walk near the ESP32 and observe changes in amplitude and RSSI values. Expect:

  • ~20 frames/sec streaming rate
  • Amplitude variance increases when someone is moving near the antenna
  • RSSI fluctuates with proximity changes
  • Frame types: 64 sc (148 B), 128 sc (276 B), 192 sc (404 B)

Step 10: Multi-node setup (optional)

For multi-node coverage, repeat Steps 3-6 for additional ESP32-S3 boards with different CONFIG_CSI_NODE_ID values (2, 3, ...). All nodes stream to the same aggregator.


Architecture

ESP32-S3 Node(s)                     Host Machine
+-------------------+               +-------------------+
| WiFi CSI callback |  UDP/5005     | aggregator binary |
| (promiscuous mode)|  ---------->  | (Rust, clap CLI)  |
| ADR-018 serialize |  ADR-018      | Esp32CsiParser    |
| stream_sender.c   |  binary       | CsiFrame output   |
+-------------------+               +-------------------+

ADR-018 Binary Frame Format

Offset  Size  Field
0       4     Magic: 0xC5110001 (LE)
4       1     Node ID
5       1     Number of antennas
6       2     Number of subcarriers (LE u16)
8       4     Frequency MHz (LE u32)
12      4     Sequence number (LE u32)
16      1     RSSI (i8)
17      1     Noise floor (i8)
18      2     Reserved
20      N*2   I/Q pairs (n_antennas * n_subcarriers * 2 bytes)

Troubleshooting

Symptom Cause Fix
No serial output Wrong baud rate Use 115200 for ESP32 monitor
WiFi connection fails Wrong SSID/password Check sdkconfig.defaults
No UDP frames arrive Firewall blocking Add UDP 5005 inbound rule (Step 7)
CSI callback not firing Promiscuous mode issue Verify esp_wifi_set_promiscuous(true) in csi_collector.c
Parse errors in aggregator Firmware/parser version mismatch Rebuild both from same commit
Docker path errors (Windows) MSYS path conversion Use MSYS_NO_PATHCONV=1 prefix
esptool not found Not installed pip install esptool

Verified Test Results

  • 20 Rust tests pass (cargo test -p wifi-densepose-hardware)
  • 6 Python tests pass (pytest v1/tests/)
  • 693 frames captured in 18 seconds (~21.6 fps)
  • Sequence numbers contiguous (zero frame loss)
  • Presence detection confirmed with live motion scoring

Related

  • ADR-018: ESP32 Development Implementation
  • Commit 92a5182: feat(adr-018) ESP32-S3 firmware, Rust aggregator, and live CSI pipeline
  • firmware/esp32-csi-node/README.md: Firmware-specific documentation

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions