Real-time, device-free human motion detection from Wi-Fi Channel State Information (CSI). A Raspberry Pi 4B running Nexmon CSI firmware captures CSI from a commodity 802.11ac radio (BCM43455); a logistic-regression classifier on the host turns frame-to-frame signal variation into a live motion / no-motion decision. No cameras, no wearables.
Movement in a room perturbs the multipath of Wi-Fi signals. By reading the per-subcarrier amplitude of each received frame and measuring how much it changes over time, the system separates "motion" from "no motion".
Pi (Nexmon CSI capture) -> UDP relay -> CSIKit decode -> preprocess -> features -> logistic regression -> smoothed live decision
- Capture. The Pi captures CSI frames into
.pcapfiles (offline) or streams them live. - Relay. A Pi-side relay (
forward.py, documented in the setup doc) forwards raw Nexmon UDP CSI frames to the host on port5500. - Decode.
CSIKit'sNEXBeamformReaderparses captures; amplitude is read directly fromframe.csi_matrixto avoid a NumPy compatibility issue in the helper API. - Preprocess. Drop 10 guard subcarriers each side (256 -> 236), clip outliers to the 1st/99th percentile, then apply a 5-frame moving-average smooth per subcarrier.
- Features. Per window of 150 frames (about 3 s at 50 Hz): per-subcarrier variance over time and mean frame-to-frame difference, concatenated.
- Classify.
StandardScaler+LogisticRegressionwith balanced class weights. Sessions are split 70/15/15 by session, not by window, to avoid leakage between overlapping windows. - Live decision. Probability is smoothed with an exponential moving average (alpha 0.15) and gated with hysteresis (on above 0.5, off below 0.3) for a stable readout.
.
|-- README.md
|-- requirements.txt
|-- .gitignore
|-- src/
| |-- _paths.py resolves project paths from any working directory
| |-- preprocess.py pcap -> processed .npz
| |-- train.py trains logistic regression, writes models/model.pkl
| |-- realtime.py terminal live inference
| |-- visualize.py PyQtGraph live waterfall + probability GUI
| `-- tools/
| |-- compare.py motion vs no-motion energy comparison
| |-- decode_test.py sanity-check CSI decoding
| |-- plot_waterfall.py render a CSI waterfall from a capture
| |-- sample_rate.py measure capture rate (Hz)
| |-- receive_test.py raw UDP receive test
| `-- debug_stats.py inspect features and model probabilities
|-- models/
| `-- model.pkl trained classifier (clf + scaler)
|-- data/
| |-- samples/ small demo captures (committed)
| `-- (raw session captures, gitignored)
|-- processed/ preprocessed .npz (gitignored)
|-- assets/ figures used in this README
`-- docs/
|-- setup_flashing_the_pi.docx
|-- POTENTIAL_IMPACT_ASSESSMENT.md
`-- project_proposal.docx
- Raspberry Pi 4B with the BCM43455 radio, running Nexmon CSI firmware
- A host machine (Windows, Linux or macOS) on the same network as the Pi
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux / macOS
source .venv/bin/activate
pip install -r requirements.txtAll scripts resolve paths from the project root, so run them from anywhere.
Offline training pipeline:
# 1. Place captures in data/ named motion_*.pcap and no_motion_*.pcap
# 2. Decode and preprocess every capture into processed/*.npz
python src/preprocess.py
# 3. Train the classifier and write models/model.pkl
python src/train.pyLive detection (Pi capturing, forward.py relaying to UDP 5500):
python src/visualize.py # GUI: live waterfall + probability bar
python src/realtime.py # terminal: probability bar in the consoleUtility scripts:
python src/tools/decode_test.py # confirm CSI frames decode
python src/tools/plot_waterfall.py # save a waterfall image from a sample capture
python src/tools/compare.py # compare motion vs no-motion energy
python src/tools/sample_rate.py # report capture rate in Hz
python src/tools/receive_test.py # confirm UDP frames arrive on 5500
python src/tools/debug_stats.py # print features and P(motion) for samplesThe logistic-regression classifier reaches roughly 97% test accuracy on held-out sessions. Run train.py to reproduce; it prints validation and test accuracy, a classification report and a confusion matrix. Because the split is by session, the figure reflects generalisation to unseen recordings rather than to neighbouring windows of the same recording.
data/ and processed/ are gitignored because the raw and preprocessed captures are large. Small demo captures live in data/samples/ (committed) so the utility scripts run on a fresh clone.
docs/ contains the Pi setup walkthrough (flashing the image and the per-reboot startup sequence), the potential impact assessment, and the project proposal.
- Single transmit/receive link in a single environment; the model is environment-specific and should be retrained per deployment.
- The host scripts expect Nexmon CSI frames on UDP
5500; the Pi-side relay must be running. - The Pi needs a short startup sequence after every reboot (see the setup doc).

