For my seminar this term I put two tiny chips in my living room. One does nothing but shout the same Wi-Fi frame a hundred times a second. The other one listens, and forwards what it hears to my laptop over USB. No camera, no microphone, no motion sensor. Two microcontrollers you can buy for a few euros each.
With that, a completely ordinary machine-learning classifier could tell whether the room was empty, whether someone was walking near the line between the two chips, or whether someone was walking in the far corner. In the clean case it got that right every single time. Then I moved the listener behind a drywall partition so it could no longer “see” the room directly, and it still worked, just not that precisely.
That sounded like magic to me when I started, and the point of this post is that it is not. It is a fairly clean tour through how radio actually works, so it got long. Every section builds up the concept it needs before using it, and you can stop after any of them and walk away with a correct, just less detailed, picture.
The short answer
Here is the whole idea in one paragraph, and if you read nothing else, this is the honest version.
Your Wi-Fi devices cannot decode a packet without first figuring out what the wireless path did to the signal on its way over. A radio signal does not travel in a single straight line from sender to receiver. It bounces off walls, furniture, and people, and arrives as a sum of many delayed, weakened copies of itself. To reconstruct the bits, the receiver has to estimate this distortion for every packet and mathematically undo it. That estimate is a surprisingly detailed description of every path the signal took through the room, and it has a name: Channel State Information, or CSI. It is computed purely as a means to an end, decoding data, and then normally thrown away. It usually never leaves the RF-chip.
The trick of Wi-Fi sensing is to keep it. When you move through the room, you change which paths exist and how long they are, so the CSI changes with you. Record how it wiggles over time, hand those wiggles to a classifier, and you get “someone is walking in the kitchen” out of a signal that was only ever meant to carry your Netflix stream.
Everything below is just making each piece of that paragraph real: what “the room did to the signal” means precisely, why the receiver computes it, and why it turns out to be such a good fingerprint of what is moving.
How do bits even ride on a radio wave?
To understand what CSI is, I first need to build up what “the signal” is in the first place, because CSI is a statement about how one specific kind of signal got mangled.
Start with the wave itself. An antenna is just a piece of metal in which we push electrons back and forth. Wiggle them at a steady rate and they radiate an electromagnetic wave at that frequency: a clean sine wave, oscillating billions of times per second for Wi-Fi (2.4 or 5 GHz). On its own, a pure sine wave carries no information. It is the same forever, so there is nothing to read off it. This bare, information-free wave is called the carrier.
To send data, you have to disturb the carrier in a way the receiver can measure. That disturbance is modulation. You can vary the wave’s amplitude (how strong it is), its phase (where in its up-down cycle it is at a given instant), or both at once. The receiver compares what arrives against the steady carrier it expects and reads the data out of the difference.
The clever part is that you do not send one bit at a time. You define a small set of allowed (amplitude, phase) combinations, and each one stands for a specific bit pattern. One such combination is a symbol. If you allow 16 distinct amplitude-and-phase points, each symbol picks one of 16 possibilities, which is 4 bits. This scheme is called QAM (quadrature amplitude modulation), and you can picture its symbols as dots arranged in a grid on a 2D plane: the horizontal axis is “how much cosine”, the vertical axis is “how much sine”, and together they encode amplitude and phase as a single point. 16-QAM is a 4x4 grid, 256-QAM is a 16x16 grid packing 8 bits per symbol. The receiver’s job for each symbol is to look at where the incoming point landed and decide which grid dot was meant.
You send these symbols one after another, at some symbol rate. Send them faster and you get more bits per second, but you also occupy more bandwidth (a wider slice of frequencies). That tension, cramming more symbols per second through a limited chunk of spectrum, is where the trouble starts.
The enemy: what the room does to the signal
Here is the problem that shapes everything about modern Wi-Fi, and it is also exactly the effect that makes sensing possible, so it is worth getting right.
In free space a signal would travel in a straight line and arrive as a single clean copy. A room is not free space. The signal reflects off the walls, the floor, the ceiling, that metal shelf, and your body. So the receiver does not get one copy of each symbol. It gets many: the direct one, plus an echo off the left wall a few nanoseconds later, plus an echo off the far wall later still, each one weaker and phase-shifted differently. This is multipath, and the spread between the earliest and latest meaningful copy is the delay spread.
Multipath causes two distinct kinds of damage.
The first is between symbols. If the delay spread is comparable to how long one symbol lasts, then the echoes of symbol number 5 are still arriving while symbol number 6 is being sent, and they pile on top of each other. The receiver can no longer cleanly tell one symbol from the next. This smearing is inter-symbol interference (ISI), and it gets worse the faster you send. That is the catch from the last section made concrete: the higher your symbol rate, the shorter each symbol, the more easily multipath echoes bleed across symbol boundaries.
The second kind of damage is across frequencies. When a delayed copy of the wave lands on top of the original, the two can add up or cancel out depending on how far the phases have drifted, and that depends on the exact frequency. So some frequencies come through strong and others get deep notches where the copies nearly cancel. The channel is not uniform across your band. This is frequency-selective fading, and crucially it is a fingerprint of the specific geometry of the room, because it is set by the exact set of path lengths.
Both problems get worse with wider bandwidth and faster symbols, which is precisely what you want for speed. For decades this was the central headache of wireless. The elegant way out is the reason every modern Wi-Fi, LTE, and 5G link works the way it does.
OFDM: many slow carriers instead of one fast one
The instinct is to send one very fast stream of symbols on a single carrier. OFDM (orthogonal frequency-division multiplexing) does the opposite, and this inversion is the key idea.
Instead of one carrier running at a blistering symbol rate, OFDM splits the band into many narrow subcarriers, each running slowly, and sends a little piece of the data on each one in parallel. Wi-Fi in a 20 MHz channel uses 64 such subcarriers. The total throughput is the same or better, because you have many streams going at once, but each individual stream is now slow, which means each symbol spans over a longer time period.
That long symbol is the whole point. If each symbol lasts much longer than the delay spread, then the multipath echoes of a symbol all arrive within that same symbol’s own time window instead of smearing into the next one. ISI essentially goes away. (Real OFDM nails this shut with a “cyclic prefix”, a short guard copy at the start of each symbol that absorbs the leftover echoes, but the long-symbol intuition is the core of it.)
There is a second gift, and it is the one that matters for sensing. Because each subcarrier is so narrow in the frequency band, the frequency-selective fading from the last section is essentially constant across that one subcarrier. Over a slice that thin, the channel does not have room to vary. So instead of some horribly complicated distortion, each subcarrier experiences just one simple thing: the wave that comes out is the wave that went in, multiplied by a single complex number that captures “how much this particular frequency was attenuated and phase-shifted by the room.”
That per-subcarrier complex number is the channel frequency response, which I will write as for subcarrier . If you know , undoing the channel on that subcarrier is simple: you just divide the received symbol by and you are back to what was sent. Dividing out the channel like this is called equalization. A nasty smeared mess turned into 64 independent “multiply by a number, then divide it back out” problems. That is why OFDM won.
But it hides a question I have been dodging: the receiver only gets to divide by if it knows . Nobody handed it those 64 numbers. It has to measure them, and that measurement is the thing this whole post is really about.
Measuring the channel: pilots, estimation, and synchronization
The receiver’s problem is that it sees the received symbol but wants the sent symbol , and they are related by , where is noise. Two unknowns ( and ), one equation. Unsolvable, unless you cheat by making known in advance.
That is exactly what a pilot is: a symbol whose value the receiver already knows because the standard fixed it ahead of time. On a subcarrier carrying a known pilot, the receiver can just rearrange: since is known, an estimate of the channel is . (This is the least-squares estimate, and it is as simple as it looks: the noise comes along for the ride, but it gives you a usable .) Sprinkle known pilots through the signal, measure where you have them, and you have estimated the channel. This whole procedure is channel estimation.
Now, where do you put the pilots? There are two classic arrangements, and the choice is a genuine engineering tradeoff about what kind of channel you expect.
- Block-type: every so often, send one OFDM symbol where all subcarriers are pilots. That gives you the full channel across every frequency at one instant, and you reuse it for the data symbols that follow, interpolating across time until the next pilot block. This works when the channel barely changes over that stretch of time, that is, a slowly-varying channel.
- Comb-type: in every symbol, make a fixed subset of subcarriers pilots, like the teeth of a comb spread across frequency. You measure only at the teeth and interpolate across frequency to fill in the data subcarriers between them. Because you get a fresh measurement every symbol, this tracks a channel that changes quickly in time, at the cost of some frequency resolution.
The mental shortcut I use: block-type spreads its pilots along time and interpolates over time; comb-type spreads them along frequency and interpolates over frequency. Real Wi-Fi uses a mix, with a full training field at the start of each frame plus scattered pilots to track drift.
One more thing has to be right before any of this works, and it quietly does a lot of the heavy lifting: synchronization. The receiver has to know where each symbol starts (timing), and its local oscillator has to run at almost exactly the transmitter’s frequency (carrier frequency). If the timing is off, it slices the symbols at the wrong boundaries. If the two oscillators differ even slightly, the whole constellation slowly rotates and every phase reading drifts. Getting the timing and frequency locked is its own hard problem, and I mostly want to flag it because it comes back to bite the sensing side: cheap hardware never gets the phase truly clean, which is why, as you will see, I end up throwing the phase away.
Put it together. For every packet, on every one of its 64 subcarriers, the receiver estimates a complex number describing how the room treated that frequency, and it does this only so it can equalize and recover the bits. Collect those 64 complex numbers and you have one CSI measurement. That is the whole object. A snapshot of the room, written in 64 numbers, computed a hundred times a second, and normally discarded the instant the packet is decoded.
The waste product is a fingerprint
That vector of values was computed to undo the channel. But it is the channel. It is a direct measurement of the combined effect of every path the signal took through the room, which means it encodes where the walls are, where the furniture is, and where you are.
Stand still and the CSI is basically constant: the same paths, the same lengths, the same 64 numbers packet after packet. Take a step, and you move a reflector. Some path lengths change, some notches in the frequency response shift, the copies add up differently, and the CSI moves. Wave your arm and you get a smaller wiggle; walk across the room and you get a big one. The signal your router computes purely as bookkeeping is, for free, a motion sensor for the whole space it covers.
A single CSI measurement from my setup is 64 subcarriers, each a complex number delivered as an in-phase and a quadrature part, an pair. From that I take the amplitude and, in principle, the phase:
import numpy as np
# One CSI frame from an ESP32-C6: 64 subcarriers as interleaved I/Q
iq = raw_frame.reshape(-1, 2) # rows of [I, Q]
h = iq[:, 0] + 1j * iq[:, 1] # complex channel estimate H_k
amplitude = np.abs(h) # |H_k|: what I actually use
phase = np.angle(h) # noisy on cheap hardware, mostly unusable That comment on the last line is where the low-cost reality bites. Remember the synchronization problem: on a cheap chip, the transmitter and receiver clocks are never perfectly locked, so the phase of carries a random, drifting offset that has nothing to do with the room. The amplitude does not care about that offset, so it stays meaningful, but the raw phase is mostly garbage without careful calibration I did not attempt. So like most ESP32 sensing work, I use amplitude only. That is a real limitation, and I will come back to what it costs.
Collect a few hundred of those amplitude vectors per second, watch how they move, and movement in the room is plainly visible as ripples in the amplitude over time. The rest is turning those ripples into a label.
My experiment: telling apart three zones
The concrete setup was deliberately minimal. Both the transmitter and receiver are ESP32-C6 microcontrollers, the kind of chip you can get from around five euros, with nothing but their onboard PCB antennas. The transmitter broadcasts roughly 100 frames per second on a 20 MHz Wi-Fi channel, which fixes my sampling rate at about 100 Hz, and the receiver pulls the CSI out through Espressif’s ESP-CSI interface and streams it to my laptop. Access to CSI here is not part of the Wi-Fi standard at all: it is a vendor-specific hook that exposes the channel estimates the chip already computes internally. That detail matters later.
I defined three classes for the living room: empty, one person walking in the near zone (right along the line between the two chips), and one person walking in the far zone. So the question is not just “is someone there” but “roughly where are they”, which is a harder ask.
To label the data, my toolkit lets me press number keys while recording, so as I walk I tag each moment with the zone I am actually in. That gives clean ground-truth labels without any painful post-hoc annotation. I then chop the stream into non-overlapping windows of 100 samples (about one second each), and from each window I compute a set of summary features: the mean amplitude, how much it varied, its spread, and similar statistics, plus a few features that look across several consecutive windows to capture how the signal is evolving. A window becomes one labelled example, a vector of 27 numbers, and a whole recording becomes a table of those. I tried a few standard classifiers and a plain random forest came out best, so that is what I used.
The whole path, collecting CSI over serial with live labeling, turning it into windowed features, training a model, and even running live predictions as new data streams in, is the pipeline I built for the seminar and released as the open-source CSI-Toolkit. It is not the first tool to pull CSI off an ESP32: Steven Hernandez’s ESP32-CSI-Tool is the well-known reference for the collection step. What I wanted was the whole workflow, from raw serial bytes to a trained classifier with ground-truth labeling built in, in one place.
In the clean line-of-sight case, the result was as good as it can get: 100% accuracy across the test windows, all three zones told apart without a single mistake. As a sanity check I also trained a version to notice a static change, an empty line of sight versus one with a chair placed in the direct path, and it nailed that too. So the amplitude of CSI on a five-euro chip is enough to detect not just motion but a rearranged room.
That number, 100%, is where I have to be careful, because on its own it is almost misleading.
What 100% does not mean
A perfect score on a clean test set is the kind of result that should make you suspicious, not proud, and the failures I found next are the actually interesting part.
The first thing I checked was whether a model I trained in one situation still works in another. I took the classifier trained in line of sight and applied it, unchanged, to data recorded with the receiver moved behind a drywall partition. It scored 32.8%, which for a three-way choice is essentially worse than guessing. And it failed in a revealing way: it labeled almost everything as the same single class, regardless of what was actually happening. The decision boundaries it had learned were pinned to the exact amplitude patterns of the first setup, and once the wall reshaped the multipath, those patterns meant nothing.
This is not me having built a flimsy model. It is a well-known and still largely open problem in the field: CSI-based models generalize poorly across environments, device positions, and even hardware, precisely because CSI is a fingerprint of one specific geometry, and that is a feature for sensing but a curse for portability. Making these models transfer across rooms, so-called cross-domain generalization, is an active research area, not a solved one. The honest reading of my 100% is “100% in the exact conditions it was trained in, and do not expect a euro of that to carry over to your living room.”
So I trained a fresh model directly on the through-wall data. That recovered a lot: 83.6% accuracy, down about 16 points from the clean case. Digging into where it went wrong, the near zone stayed easy (that path is short and strong even through the wall), while the far zone got confused with the empty room. That makes physical sense: the wall attenuates everything, and activity in the far corner perturbs the already-weak signal so little that it starts to look like nobody is there. So the coarse question “is someone here” survives the wall, but the finer question “which zone” degrades.
And underneath both failures sits the amplitude-only limitation from earlier. By throwing away the phase, I threw away the information that would let you estimate the angle a reflection arrives from or the precise distance it traveled. Systems that do the fancy things, tracking a person’s position, sensing breathing, need that stable phase across multiple antennas. On a single cheap chip with amplitude only, coarse zone-level motion is about the ceiling. Which is a fine place for a baseline to sit, as long as you are honest that it is the ceiling.
Where this is going: Wi-Fi that senses on purpose
Everything above is a hack. I am reading a vendor-specific debug output that was never meant to leave the chip, throwing away half the information because the hardware cannot keep it clean, and getting something that works in one room until I move a wall. The obvious question is what this looks like when it is designed in from the start instead of scavenged.
That is essentially what the IEEE just standardized. 802.11bf-2025, formally “Amendment 4: Enhancements for Wireless LAN Sensing”, is the first Wi-Fi amendment whose primary job is sensing rather than throughput. It was approved in May 2025 and published that September. A good technical overview, if you want to go deeper than I do here, is Du et al.’s survey. The parts that map directly onto my pain points:
- Standardized CSI, not a vendor hack. My whole demonstrator hangs on Espressif choosing to expose channel estimates that the standard never promised. 802.11bf defines CSI feedback as a first-class part of the protocol, so any compliant device can produce sensing data without a chip-specific backdoor.
- Dedicated sounding, on a schedule. Rather than piggybacking on whatever traffic happens to exist, 802.11bf uses purpose-built sounding exchanges (a short “null data packet” whose only job is to let the receiver measure the channel), scheduled in recurring windows. Clean, regular measurements by design.
- Many devices instead of my two. My single transmitter-receiver pair sees the room from exactly one angle, which is a big part of why moving the receiver broke everything. 802.11bf coordinates multiple devices so one can transmit while several others measure the reflections, covering the room from several viewpoints at once and filling in the blind spots a single link leaves. This is the direct answer to my through-wall degradation.
- Wider bandwidth for finer motion. My 20 MHz channel is enough to see walking. Detecting something as subtle as breathing benefits from the much wider channels modern Wi-Fi supports, which 802.11bf can use.
I want to be careful not to oversell it. A published standard defines the machinery; it does not automatically make sensing accurate, and how well 802.11bf performs in practice, along with how much its sensing traffic eats into normal data throughput, is still being measured. It is also worth noting that in the sub-7 GHz bands it targets, the standard covers the two-device and multi-device cases (like my setup) but not single-device radar-style sensing. So this is the beginning of Wi-Fi sensing as a real feature, not the finished article.
For context, people were already pushing commodity ESP32 hardware surprisingly far before any of this: Strohmayer and Kampel, for instance, did long-range through-wall activity recognition with an ESP32 and a directional antenna across several rooms. My contribution is smaller and more boring: a clean, reproducible baseline and the tooling to get anyone from two bare chips to a trained classifier, so the barrier to trying this yourself is a weekend and about ten euros of hardware.
What I keep coming back to
The thing that stuck with me is that none of this required new physics or special hardware. The information was always there. Every Wi-Fi receiver on earth computes a detailed picture of its surroundings a hundred times a second, uses it for a fraction of a millisecond to clean up your data, and throws it away. Sensing is just the decision to keep the receipt.
What still nags at me is the tension baked into the whole thing. The reason CSI is such a good motion sensor, that it is exquisitely tied to the exact geometry of one room, is the very reason my model fell apart the moment I changed that geometry. A fingerprint is only useful if it does not transfer. Making Wi-Fi sensing both sensitive and portable is, as far as I can tell, still genuinely unsolved, and that is the part I would like to understand better next. There is also a quieter thought I have not shaken: there are already more than 21 billion Wi-Fi devices in use, and every one of them is, in principle, already watching the room it sits in. What we choose to do with that, and what we choose not to, is going to be the more interesting question than whether it works.