ESP32 Wi-Fi Motion Sensing: Build Guide

Build an experimental Wi-Fi motion sensor with two ESP32 boards, pinned firmware, a local activity display, supplier options and repeatable tests.

Concept diagram of an ESP32 sender and receiver sensing changes in radio paths, with USB data sent to a local browser activity display.

Build an experimental Wi-Fi motion-sensing instrument with two ESP32 boards and a local browser. One board sends packets; the other records channel state information (CSI). The companion app turns changing radio-channel measurements into an activity score, with a calibrated threshold and downloadable evidence.

Release 1.0 · 18 September 2026. Experimental build: software checks do not establish sensing performance. THL has not yet flashed these boards or measured this configuration in a room. Treat the activity indicator as a hypothesis to test, not a proven occupancy sensor.

At a glance: intermediate software setup; no soldering or GPIO wiring; Windows instructions; $49.80 for the reference boards and cables before tax/shipping. Allow an afternoon for installation and first trials, with extra time for toolchain or driver troubleshooting.

What you will build

The signal path is sender → changing radio paths through the room → receiver → USB → local browser. Fix both boards in place, calibrate while the test area is still, and compare marked movement trials with quiet periods, a fan and an opening door.

You get a score graph, explicit missing-data states, trial markers, raw CSI, score exports and calibration records. No router login or cloud account is needed. The sender actively transmits Wi-Fi frames using ESP-NOW. Use your own test area and tell participants when recording trials.

The score measures channel variation. It does not identify people, locate bodies, form images, measure breathing, or establish that a room is empty. A still person may produce little activity. This guide makes no through-wall range or detection-accuracy claim. The radio foundation is Espressif’s ESP-CSI project.

Parts and two supplier options

Prices are USD listing snapshots checked on 18 September 2026, excluding tax and shipping. Confirm the exact variant, delivery eligibility and current availability before ordering. Every purchased equipment recommendation has two exact-model suppliers; Micro Center is an additional comparison where suitable.

Quantity and equipment Supplier one Supplier two Required specification
2 × Adafruit ESP32 Feather V2, product 5400 Adafruit, $19.95 each DigiKey, 1528-5400-ND, $19.95 each PCB antenna version; classic ESP32-PICO-MINI-02; USB-C. Do not substitute an ESP32-S2, S3 or C3. Both listings showed stock; delivered-board operation remains untested.
2 × USB-A to USB-C data cable, Adafruit 4474, about 1 m Adafruit, $4.95 each Mouser, 485-4474 Data-capable cable, not charge-only. Mouser offers the same part; recheck price and destination restrictions. Assumes USB-A ports on your host.

Reference subtotal: 2 × $19.95 + 2 × $4.95 = $49.80 using Adafruit listings. Reuse a Windows computer with two suitable USB ports and two stable nonmetallic supports. One port powers the sender; the other carries receiver data. No battery, Raspberry Pi, external antenna, breadboard, jumper wires or soldering iron is needed. If your computer only has USB-C, select matching data cables before buying; this basket assumes USB-A host ports.

Where Micro Center fits

Micro Center’s Inland USB-A to USB-C data cables, three-pack, SKU 550293 were listed at $24.99 with usually-ships wording. They are a differently branded alternative to the Adafruit/Mouser cables above, not the same part. Reuse existing data cables where possible.

The Elegoo ESP-32 development-board three-pack, SKU 961193, listed at $19.99, is a candidate awaiting exact module and USB-connector verification, not a confirmed drop-in replacement. Use the two exact Feather suppliers for the documented basket. Do not combine an unknown board connector with USB-C cables just because both are inexpensive.

Select Shippable Items and confirm delivery to your destination. No local store or stock quantity is assumed. These are ordinary supplier links, not a delivered quotation.

1. Download the companion and install the toolchain

Download THL Wi-Fi Channel Lab 1.0 (ZIP, 90 KB). It contains the local app, firmware preparation scripts and patch, dependency locks, test sources, validation records and an offline guide. Extract the ZIP and place its project folder at C:\esp\thl-wifi. You should see host, firmware, tests, README.md and this guide. Keep the host’s HTML, JavaScript and CSS files together.

The ZIP is 90,009 bytes. SHA-256: 93feea6bc2b67581b7b46350f68bf33461e870469b391ebf09c9c28742b931f3.

Install Git and ESP-IDF v5.4.4, including the classic ESP32 toolchain, using Espressif’s Windows setup instructions. Open its configured PowerShell environment. The SDK supplies Python and the build tools. Use a short local path without spaces and allow several gigabytes for tools and builds.

idf.py --version
git --version
python --version

The first command must report ESP-IDF v5.4.4. Stop if it reports another version.

Component Pinned value
ESP-CSI source 8633d67152db2808f141cc1595970aa9cf406045
ESP-IDF v5.4.4, commit 296b6eab9445fd720e71aecab961e2d3fbca9944
Firmware target esp32
Receiver gain component esp_csi_gain_ctrl 0.1.5
Receiver helper dependency cmake_utilities 0.5.3, retained by the supplied lockfile
Companion app THL Wi-Fi Channel Lab 1.0.0

The download contains the original THL browser app and a small patch/preparation script. It retrieves the pinned upstream firmware, verifies source hashes, installs the supplied dependency lockfiles and applies documented corrections. It does not flash a board automatically.

2. Prepare and build both firmware images

From the extracted project folder:

Set-Location C:\esp\thl-wifi
python .\firmware\prepare_firmware.py --dest C:\esp\thl-csi

Expect Ready: and the classic ESP32 settings. Use a new/empty destination or the same already-prepared checkout. The script refuses unexpected edits; preserve your experiments and choose a fresh directory if it stops.

Build sender, then receiver. Run each command separately and stop at any error.

Set-Location C:\esp\thl-csi\examples\get-started\csi_send
idf.py set-target esp32
idf.py build

Set-Location C:\esp\thl-csi\examples\get-started\csi_recv
idf.py set-target esp32
idf.py build

The download also includes firmware\build_firmware.ps1 for this sequence with error checks. Manual commands avoid needing to change PowerShell execution policy. set-target regenerates SDK configuration; keep customised experiments separately. Save each project’s sdkconfig, dependencies.lock, build log and generated firmware with your records.

What the THL patch changes

It sets MAC addresses before starting Wi-Fi, following the SDK API contract. The sender stays 1a:00:00:00:00:00; the receiver uses 1a:00:00:00:00:01 and filters for that sender. It pins the receiver component, guards short/null packet payloads, and safely copies the sequence number instead of using a potentially unaligned integer read.

The sender requests 20 packets per second, reduced from upstream’s 100 for serial bandwidth headroom. Actual received rate remains a measurement. Both boards retain channel 11, HT40, secondary channel below, MCS0 with long guard interval. Use one pair initially. Fixed MACs are lab identifiers, not authentication. Other bandwidths or chip variants need coordinated firmware and parser changes.

3. Identify ports, flash and check startup

Connect each board separately using a data cable and label it sender or receiver:

python -m serial.tools.list_ports

Record the port appearing for each board. If none appears, check the cable and USB bridge driver. Current Feather V2 boards use CH9102F; older revisions may use CP2102N. Follow Adafruit’s driver instructions for the delivered revision.

Flashing replaces the selected development board’s firmware. Replace the quoted placeholders with the ports you identified, such as COM7; do not assume an example port is yours.

$senderPort = 'REPLACE_WITH_SENDER_COM_PORT'
$receiverPort = 'REPLACE_WITH_RECEIVER_COM_PORT'

Set-Location C:\esp\thl-csi\examples\get-started\csi_send
idf.py -p $senderPort -b 460800 flash
idf.py -p $senderPort monitor

Check startup without an abort or repeating reset. Exit the monitor with Ctrl+] before continuing:

Set-Location C:\esp\thl-csi\examples\get-started\csi_recv
idf.py -p $receiverPort -b 460800 flash
idf.py -p $receiverPort monitor

With the sender powered, the receiver should eventually print CSI_DATA rows. Save an example and any error messages. Exit with Ctrl+] before opening the browser; only one program can own the serial port.

Flash speed is 460800 here. Receiver output is 921600 baud, 8-N-1; sender console is 115200. idf.py monitor reads the corresponding project’s configuration. A readable sender log does not establish the receiver baud rate.

The Feather uses automatic upload/reset. SW38 is a user button, not BOOT. Follow the Feather pinout/reset documentation if uploading fails.

4. Arrange the experiment and open the display

Use USB power only initially. There is no wire between the boards and no GPIO connection. Keep exposed boards off conductive surfaces. Fix them more than one metre apart at a recorded height and orientation, with antenna ends clear of supports and metal. Mark a walking path between them. Keep cables and computer still; moving these also changes the channel.

Run the local companion server in a separate configured terminal:

Set-Location C:\esp\thl-wifi\host
python -m http.server 8000 --bind 127.0.0.1

Open http://127.0.0.1:8000/ in desktop Chrome or Edge. Keep the terminal open. The downloaded app has no external libraries, analytics or uploads. Serial data stays in the tab until you export it. This applies to the companion, not the THL website’s normal analytics.

Select Start synthetic demo to learn the controls without boards. Its badge and exports say SYNTHETIC. Generated curves are never evidence about your room. Stop, export if wanted, and Clear session before connecting hardware; the app will not mix sources.

Choose Connect receiver and select its port. Expect usable frames, RSSI and a score after about one second. Supported frames must be classic ESP32 HT40, secondary below, MCS0, no STBC, with 384 signed CSI bytes. Other parsed formats remain in raw CSV but cannot generate scores. Resolve an unsupported-format warning before calibration.

5. Calibrate once, then evaluate separate trials

Let the boards settle for a minute. Keep the browser foreground and computer awake. Check a steady usable-frame rate and that invalid/excluded counters are not rapidly increasing. Requested 20 Hz is not a promised delivered rate.

Leave the test area still and choose Calibrate empty room · 30 s. Allow about one second of warmup plus 30 continuous seconds of score coverage. Movement during calibration becomes part of the baseline. The app then freezes the threshold and stores a numbered calibration.

Do not recalibrate between quiet and movement trials you intend to compare. A gap greater than 250 ms, detected restart, changed source/settings or disconnect invalidates the baseline. Missing data is not quiet-room evidence. Resolve interruptions, recalibrate and label a new run.

A repeatable first test

Where practical, have a second person operate markers from a fixed position away from the test path. Their movement can also affect the channel. Keypress markers provide approximate ground truth, not laboratory timing.

Trial Procedure with threshold frozen Record
Quiet evaluation Two minutes without intentional movement, after calibration Valid duration, activity episodes, gaps and environmental changes
Marked crossings Twenty crossings on the same path; let the indicator settle between them crossing 01 start / end markers, hits/misses, speed and interruptions
Still person Sit quietly in the area for two minutes Whether activity fades; below-threshold does not mean nobody is there
Confusers Separately operate an existing fan and open/close a door Mark events and elevated activity unrelated to crossings
Repeat Same marked positions another day Fresh baseline, drift, differences and failed trials

Define a scored crossing before testing: for example, elevated activity during its marked interval or within one second after its end. The extra second accommodates the score window; it is an experimental matching rule, not measured latency. Mark interrupted trials invalid, report them separately, and repeat them without hiding failures.

Notes should include trial ID, board revision, firmware/app versions, layout, separation, height, antenna orientation, path, USB/power arrangement, browser version, fan/door state and unexpected movement. Keep sessions short and export regularly.

6. Export and interpret results

Choose Disconnect / stop, then export all four files before clearing:

Export Contents and purpose
Raw CSV Original CSI row, source/session labels, receipt UTC/elapsed time and score eligibility
Scores CSV Score, threshold, calibration number, state, RSSI and selected-sample RMS
Markers CSV Your labelled events on the same receipt-time axis
Session JSON Algorithm settings, calibration records, counters, notes and interruptions

Capture stops at 20,000 parsed frames or 16 MiB of raw text, whichever comes first; earlier records are not overwritten. Downloading does not clear data. Clear session resets data, graph, counters and calibration. Refreshing or closing loses unexported records. Export after each short run.

Calculate these from evaluation data, excluding calibration and reporting invalid intervals:

  • Crossing hit rate: detected valid crossings ÷ all valid crossings. Show counts such as 17/20, not only a percentage. That is an example format, not a THL result.
  • False activity episodes/minute: count transitions into elevated activity during quiet evaluation and divide by valid minutes. Count episodes, not every high sample. State any rule for merging brief gaps.
  • Data coverage: report valid duration, invalid trials, unsupported/excluded frames and interruptions alongside hit rate. A blind interval is not a correct negative.
  • Repeatability: keep placement, score constants and the event rule fixed. Tuning after seeing results requires a new held-out evaluation set.

Browser times include USB buffering and scheduling; marker reaction time adds uncertainty. Do not claim millisecond sensing latency from these files. Latency needs an independently synchronised reference and stated uncertainty.

What the score computes

The app takes 48 fixed legacy-training-field amplitude samples from the supported layout and divides each frame by its RMS. It computes variability over a one-second window, sampling at most 20 times per second:

score = 100 × sqrt(mean of the 48 per-tone population variances)

For baseline scores, let m be the median and MAD the median absolute deviation. The threshold is:

max(1.5, m + 6 × max(1.4826 × MAD, 0.05))

The score is dimensionless, not a percentage. These constants are experimental starting choices, not validated settings or confidence levels. Normalisation reduces common amplitude scaling but cannot remove all gain, interference and multipath effects. It may also remove changes you care about. Phase is not used to measure distance.

Classic CSV stores imaginary/real pairs. When first_word=1, the first four scalar bytes are invalid. The companion’s fixed selection excludes them; raw exports retain the original record. See Espressif’s CSI layout documentation.

Troubleshooting

Symptom Next check
idf.py missing/wrong version Configured v5.4.4 terminal and classic ESP32 tools
Preparation refuses directory Preserve edits and use a fresh destination; source hashes are checked
Component/build error Network access and first error; save log/lockfile, do not silently change versions
No COM port Data cable, matching connector, power and delivered-board driver
Flash cannot connect Correct port/target, close serial tools, board-specific reset instructions
Browser cannot open port Desktop Chrome/Edge, localhost, receiver permission, no other serial monitor
No CSI rows Sender power, receiver firmware/921600 baud, matching settings and clean boot logs
Unsupported format Classic esp32, supplied patch, unchanged HT40/secondary-below settings; retain a raw row
Calibration restarts Gaps, weak/clipped data, USB stability, foreground tab, fixed mounting and no sleep
Activity stays high Cables/boards moving, fan, doors, interference or poor baseline; log cause and start a separate run
Crossing gives no response Change one placement variable, recalibrate and rerun; retain misses
Capture full Export all four files, clear, and shorten subsequent runs

Validation record and next step

Both classic ESP32 firmware builds passed with ESP-IDF v5.4.4. The companion passed 10 core tests and 9 browser checks, including synthetic calibration, all four exports, mobile layout and simulated serial disconnects. Firmware preparation is hash-checked with exact changes and dependency locks supplied. The download includes the validation record and test sources. THL has not validated physical board operation, USB transport, radio startup, packet rate or human-motion accuracy for this release.

A successful hardware run means two flashed boards, a stable supported stream, completed baseline and saved trials with honest misses and confusers. Record observations before publishing measured results, and keep photographs, firmware/lockfiles and trial evidence together.

Continue with THL’s RF engineering lessons for placement principles, engineering calculators for propagation concepts, and more guides and downloads.