Real-Time Wi-Fi Zonal Localization Using ESP32 Channel State Information
High-accuracy, privacy-preserving human tracking through walls and in total darkness β using a single $4 microcontroller and invisible Wi-Fi waves.
Project PRISM is a passive indoor localization system that detects human presence and predicts spatial position in real-time without cameras, microphones, or wearable devices. It exploits Channel State Information (CSI) β the fine-grained amplitude and phase data embedded in every Wi-Fi packet β to sense how human bodies perturb the electromagnetic field in a room.
By deploying a custom Digital Signal Processing (DSP) pipeline and a 135-dimensional machine learning feature engine on data from a single ESP32 antenna, PRISM divides indoor spaces into discrete zones and classifies a person's location at ~100 Hz.
| Capability | Detail |
|---|---|
| Zones | Up to 4 (Empty, Zone A, Zone B, Zone C) |
| Accuracy | 73.3% generalized (4-zone room), 81.4% CV (2-zone corridor) |
| Latency | Real-time (~10ms per inference cycle) |
| Hardware | Single ESP32 NodeMCU ($4) |
| Privacy | Zero visual/audio data captured |
| Conditions | Works through walls, in complete darkness |
| Component | Purpose |
|---|---|
| 1Γ ESP32 NodeMCU | Flashed with ESP-IDF CSI extraction firmware β operates as a Wi-Fi sniffer |
| 1Γ Laptop/PC | Runs the Python ML backend; connected via Serial USB (/dev/ttyUSB0) |
| Ambient Wi-Fi | Any standard 2.4GHz 802.11n router or device within range |
| USB Cable | Micro-USB for ESP32 serial communication at 115200 baud |
No additional sensors, cameras, or wearable devices are required.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PRISM Architecture β
β β
β ββββββββββββ Serial ββββββββββββββββ βββββββββββββββββ β
β β ESP32 ββββ115200bdβββ β CSI Parser ββββ β Ring Buffer β β
β β (Sniffer)β /dev/USB0 β (I/Q β Amp) β β (100 pkts) β β
β ββββββββββββ ββββββββββββββββ βββββββββ¬ββββββββ β
β β β
β ββββββββββββββββββββββββββββΌβββββββββ β
β β DSP Pipeline β β
β β 1. Hampel Filter (outlier kill) β β
β β 2. Background Subtraction β β
β β 3. Butterworth Bandpass (0.1-3Hz)β β
β ββββββββββββββββββββ¬βββββββββββββββββ β
β β β
β ββββββββββββββββββββΌβββββββββββββββββ β
β β Feature Engine (135-dim) β β
β β β’ Multi-Lag Autocorrelation β β
β β β’ Variance Ratios β β
β β β’ Spectral Band Energy β β
β β β’ Covariance Eigenvalues β β
β β β’ Subcarrier Profile Gradients β β
β ββββββββββββββββββββ¬βββββββββββββββββ β
β β β
β βββββββββββββββββ βββββββββββββββββββββββββββΌβββββββββββββββββ β
β β Live GUI ββββ β Random Forest Classifier β β
β β (Matplotlib) β β + Confidence Thresholding (>50%) β β
β β Zone Display β β + 3-Vote Exponential Smoothing Queue β β
β βββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
wifi_localization/
β
βββ README.md # This file
βββ walkthrough.md # Detailed technical report
βββ final_submission_materials.md # Presentation script, slide layout, writeup
β
βββ data/ # CSI amplitude logs β Corridor environment
β βββ empty_area.csv # Empty corridor (3 recordings)
β βββ zone_a.csv # Zone A occupancy (3 recordings)
β βββ zone_b.csv # Zone B occupancy (3 recordings)
β
βββ data_room/ # CSI amplitude logs β Room environment
β βββ empty_room.csv # Empty room
β βββ zone_a.csv # Zone A occupancy
β βββ zone_b.csv # Zone B occupancy
β βββ zone_c.csv # Zone C occupancy
β
βββ exp_data/ # Experimental data from multiple environments
β βββ sparkonics_lab_*.csv # Sparkonics Lab captures
β βββ stc_*.csv # STC building captures
β βββ stairs_*.csv # Stairwell captures
β βββ tl_*.csv # TL environment captures
β
βββ images/ # Generated visualizations
β βββ heatmap_room_raw.png # Raw CSI amplitude heatmaps
β βββ heatmap_room_clean.png # Filtered CSI heatmaps
β βββ dsp_comparison.png # Raw vs cleaned signal comparison
β βββ pca_room.png # PCA scatter plot (room features)
β βββ pca_corridor.png # PCA scatter plot (corridor features)
β βββ feature_importance_*.png # Random Forest feature importances
β βββ zone_*.png # Per-zone signal plots
β
βββ wifi_localization/ # Source code
β βββ pyproject.toml # Python dependencies (uv managed)
β βββ uv.lock # Locked dependency versions
β β
β βββ prism.py # β‘ Core DSP filter library
β β # Hampel, Background Sub, Butterworth
β β
β βββ prism_debug.py # π§ ESP32 serial debug tool
β β # Raw packet inspection for 10 seconds
β β
β βββ csi_logger.py # π Data harvesting script
β β # Records CSI from live ESP32 to CSV
β β
β βββ prism_ai.py # π€ v1 ML training (SVM-RBF, 4-class)
β βββ prism_ai_prev.py # π€ v0 ML training (RF, basic variance)
β βββ prism_ai_v2.py # π€ v2 ML training (corridor, 87-dim)
β βββ prism_ai_room.py # π€ v3 ML training (room, 135-dim) β BEST
β β
β βββ prism_live_room.py # π΄ Live inference (v1 model, 4-zone)
β βββ prism_live_room_v2.py # π΄ Live inference (v2 corridor, 2-zone)
β βββ prism_live_room_room.py # π΄ Live inference (room model, 4-zone) β BEST
β β
β βββ prism_model.pkl # Serialized v1 model
β βββ prism_model_v2.pkl # Serialized corridor model
β βββ prism_model_room.pkl # Serialized room model β BEST
β β
β βββ generate_visualizations.py # π Heatmap, PCA, DSP comparison generator
β βββ create_pptx.py # π Auto-generates presentation slides
β β
β βββ corridor/ # Organized corridor environment copies
β β βββ prism_ai_v2.py
β β βββ prism_live_room_v2.py
β β βββ prism_model_v2.pkl
β β
β βββ room/ # Organized room environment copies
β βββ prism_ai_room.py
β βββ prism_live_room_room.py
β βββ prism_model_room.pkl
β
βββ PRISM_Zonal_Localization.pptx # Generated presentation
βββ RF_PRISM*.mp4 # Demo videos
The raw CSI amplitude from the ESP32 is devastatingly noisy. PRISM applies a three-stage Digital Signal Processing pipeline (implemented in prism.py) to isolate the human-induced perturbations:
Bluetooth, microwaves, and other RF sources cause massive random spikes. A rolling median window (size=15) replaces any value exceeding 3Ο (via Median Absolute Deviation) with the local median.
Static room geometry (walls, desks) dominates the raw signal. A trailing 100-packet moving average is subtracted to zero out the static environment, isolating only dynamic (human-induced) changes.
A 3rd-order Butterworth bandpass at 0.1β3.0 Hz eliminates low-frequency drift and high-frequency electronic noise, isolating the Doppler frequencies of human breathing (~0.1β0.5 Hz) and walking (~1.0β3.0 Hz).
- The ESP32 outputs a 128-element array per packet:
[Realβ, Imagβ, Realβ, Imagβ, ...] - Amplitude is computed as: A = β(IΒ² + QΒ²) (phase discarded due to single-antenna clock drift)
- Null subcarriers 27β37 are dropped per IEEE 802.11n β 53 active subcarriers
The critical breakthrough was moving from naive per-subcarrier statistics to domain-aware time-frequency features. For each 100-packet (1-second) window, we extract:
| Feature Group | Dimensions | Scientific Justification |
|---|---|---|
| Basic Statistics | 40 | Variance, std, energy, diff-variance, skewness, kurtosis, IQR, range (5-number summary each) |
| Multi-Lag Autocorrelation | 15 | Lags 1, 5, 10 capture signal persistence β separates erratic noise from rhythmic walking |
| Temporal Derivatives | 10 | 1st and 2nd order temporal diff-variance detects acceleration patterns |
| Multi-Scale Variance Ratios | 10 | Half/quarter window variance ratios detect subjects crossing zone boundaries |
| Spectral Features | 20 | FFT peak frequency, spectral centroid, spectral bandwidth, band energy ratios (breathing vs walking) |
| Subcarrier Profile Gradients | 10 | 1st and 2nd derivatives of the mean amplitude profile capture frequency-selective fading |
| Covariance Eigenvalues | 5 | Top-5 eigenvalues of the 53Γ53 subcarrier covariance matrix map multipath complexity |
| Correlation Statistics | 3 | Mean, std, median of upper-triangle cross-subcarrier correlations |
| Global Metrics | 2 | Total energy, subcarrier entropy |
| Top-10 Subcarrier Features | 20 | Variance and energy of the 10 most variable subcarriers |
| Version | Script | Model | Features | Classes | Accuracy | Notes |
|---|---|---|---|---|---|---|
| v0 | prism_ai_prev.py |
Random Forest | ~53 (variance only) | 4 | ~65% | Basic per-subcarrier variance |
| v1 | prism_ai.py |
SVM-RBF | ~212 (var+std+energy+diff) | 4 | Variable | Leave-One-Chunk-Out CV |
| v2 | prism_ai_v2.py |
GradientBoosting | 87 | 3 (corridor) | 81.4% | Best corridor model |
| v3 | prism_ai_room.py |
RandomForest | 135 | 4 (room) | 73.3% | Production model |
- SVM-RBF requires
StandardScalingwhich destroys the relative magnitude physics between subcarriers - SVMs scale poorly in high-dimensional (135+), highly-correlated feature spaces
- Random Forest implicitly feature-selects, carves non-linear decision boundaries, and needs no normalization
The Bug: Initial models reported 96.3% accuracy but failed completely in live inference.
Root Cause: A sliding window step of 10 (on a 100-packet window) created 90% overlap. K-Fold CV leaked near-identical frames across train/test splits. The model memorized local noise patterns, not physical zone signatures.
The Fix:
- Reduced overlap to 50% (
step=50) for truly independent windows- 8Γ Gaussian noise augmentation (scaled per-subcarrier std) simulating dynamic multipath changes
- Cross-validation runs only on real (non-augmented) samples
| Metric | Score |
|---|---|
| Overall Accuracy | 73.3% |
| Empty Detection Recall | >83% |
| Zone B Recall | >86% |
| False Positive Rate | Low β confusions largely between neighboring physical zones |
The real-time system (prism_live_room_room.py) streams from the ESP32 at 115200 baud and runs inference on every incoming packet:
A circular NumPy buffer maintains the latest 100 packets. Old data rolls out, new data rolls in β numpy matrix operations execute without memory reallocation.
Even an 86% accurate model will misclassify ~1/10 packets, causing UI flicker. PRISM solves this with:
-
Confidence Thresholding:
predict_proba()must exceed 50% for the dominant class. Below threshold β fallback to previous stable state. -
Exponential Vote Queue: A 3-vote sliding window requires unanimous agreement before switching zones. A 1.0-second release timeout prevents zone "sticking" when the target leaves.
The Matplotlib-based dashboard renders zone rectangles that light up in real-time as the model classifies human position:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STATUS: TARGET IN ZONE B β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
β β β β ββββββββ β β β β
β β Zone A β β ZONE B β β Zone C β β
β β β β ββββββββ β β β β
β ββββββββββββ ββββββββββββ ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
We use uv to manage Python dependencies.
cd wifi_localization/
uv syncVerify your ESP32 is streaming CSI data correctly:
uv run prism_debug.pyThis prints raw serial lines for 10 seconds and reports packet rate.
Record CSI data from the live ESP32 to CSV files:
uv run csi_logger.pyRetrain the model with new data or modified hyperparameters:
uv run prism_ai_room.pyThis automatically handles:
- 50% non-overlapping sliding windows
- 8Γ Gaussian noise augmentation
- 5-fold Stratified CV on real data only
- Model comparison (SVM-RBF, RandomForest, HistGBM)
- Feature importance plots
uv run prism_ai_v2.pyFire up the real-time dashboard with the room model:
uv run prism_live_room_room.py
β οΈ Note: Ensure your ESP32 is plugged into/dev/ttyUSB0andidf.py monitoris not running. If your port differs (e.g.,COM3on Windows), modify theSERIAL_PORTvariable at the top of the script.
uv run prism_live_room_v2.pyCreate heatmaps, PCA plots, and DSP comparison images:
uv run generate_visualizations.pyAuto-generate the PowerPoint deck:
uv run --with python-pptx create_pptx.py- Classes: Empty, Zone A, Zone B
- Challenge: Symmetric geometry created near-identical multipath signatures
- Fisher Separability Score: 0.070 (extremely low)
- Data: 9 CSV files across 3 recording sessions per class
- Classes: Empty, Zone A, Zone B, Zone C
- Advantage: Enclosed walls create distinct multipath reflections per zone
- Data: 1,500 continuous packets (~15 seconds steady recording) per zone
- Result: Substantially better spatial discrimination
Additional captures from diverse environments (STC building, Sparkonics Lab, stairwells) are stored in exp_data/ for extended analysis.
| Package | Version | Purpose |
|---|---|---|
numpy |
β₯2.4.4 | Matrix operations, ring buffers |
pandas |
β₯3.0.2 | Data loading, rolling window calculations |
scipy |
β₯1.17.1 | Butterworth filter, signal processing |
scikit-learn |
β₯1.8.0 | Random Forest, SVM, PCA, cross-validation |
matplotlib |
β₯3.10.8 | Live GUI dashboard, visualization generation |
pyserial |
β₯3.5 | ESP32 serial communication |
python-pptx |
(optional) | PowerPoint slide generation |
Python: β₯ 3.11
- CSI Extraction: ESP-IDF Wi-Fi CSI firmware for ESP32
- Hampel Filter: Friedrich R. Hampel's robust outlier detection via MAD
- Butterworth Filter: 3rd-order Infinite Impulse Response (IIR) bandpass
- Feature Engineering: Inspired by radar micro-Doppler signature analysis
- Validation: Stratified K-Fold with temporal de-correlation (50% non-overlapping windows)
| Script | Role | Input | Output |
|---|---|---|---|
prism.py |
Core DSP library | Raw CSV | Cleaned signal + plots |
prism_debug.py |
ESP32 serial debugger | /dev/ttyUSB0 |
Terminal diagnostics |
prism_ai_room.py |
Room model trainer | data_room/*.csv |
prism_model_room.pkl |
prism_ai_v2.py |
Corridor model trainer | data/*.csv |
prism_model_v2.pkl |
prism_live_room_room.py |
Live radar (room) | Serial + .pkl |
Real-time GUI |
prism_live_room_v2.py |
Live radar (corridor) | Serial + .pkl |
Real-time GUI |
generate_visualizations.py |
Plot generator | data/, data_room/ |
images/*.png |
create_pptx.py |
Slide generator | images/ |
.pptx |
Built as part of the Vinayabrhami AI OS Architecture.
Project PRISM β Seeing through walls with invisible waves. π‘