Wi-Fi respiratory monitoring and heart rate from CSI: what works
By Wu Jun · Updated 2026-11-02 · 8 min read
Chest movement and heartbeats can change multipath, and that modulation may appear in CSI amplitude or phase [1]. A peak in a spectrum is not proof of a physiological signal, however. Fans, HVAC, walking, chain switching and hardware phase drift can all create a plausible-looking number.
This page gives a reproducible engineering route: calculate the true independent measurement rate, choose a relatively stable signal, resample by timestamp, use 30-second windows, gate unusable outputs and calibrate against an external reference. It is not medical advice, and the open dataset is not evidence of clinical accuracy.
0. Bottom line
| Task | Relative feasibility | Main reason |
|---|---|---|
| Respiration rate | Medium | Slow motion and usually larger displacement, but still vulnerable to body motion and HVAC cycles |
| Heart rate | Low to medium | Tiny displacement; respiration harmonics, phase noise and multipath can dominate |
| Multiple people at once | Low | Reflections overlap and a single spectral peak cannot usually be assigned to one person |
| Through-wall or cross-room | Not assumed | Walls and range change SNR; every environment needs revalidation |
| Medical diagnosis | Not applicable | Consumer CSI and this project do not replace compliant medical equipment |
The defensible design is not "print a bpm for every window." It is to emit a value together with quality
metadata, or return Unavailable. Accuracy must be measured against a synchronised chest band, impedance
device, pulse oximeter or manual count.
1. Record rate is not independent measurement rate
Our 10-minute capture contains 57,293 chain records, or about 95.5 records/s. One CSI measurement
normally contains four or eight chain records that share the same sampling instant. Splitting on chain_info
bit 15 gives 7,609 complete measurement groups, so the independent measurement rate is:
7,609 / 599.924 s ≈ 12.68 measurements/s
The full analysis script also removes 25 zero-heavy groups, using 7,583 measurements and obtaining about 12.64 Hz. This is above the 4 Hz Nyquist minimum for a 2 Hz heart-rate component, but the margin is not enormous. Spectral resolution, packet loss, timestamp jitter and window length still determine whether a result is usable.
Before plotting a spectrum:
- close measurements on
chain_infobit 15; - select one valid chain per group, or construct a two-antenna ratio;
- sort by the actual timestamp instead of inventing time from a record index and a fixed frame rate;
- inspect missing chains, 64-subcarrier records, all-zero I/Q and gaps longer than one second.
2. Which signal should you use?
Amplitude is easy to consume, but automatic gain, rate control and gross motion affect it. Phase contains finer displacement information, yet commodity NIC phase includes carrier and sampling frequency offsets plus hardware drift. Unwrapping one chain does not automatically remove those errors.
PhaseBeat uses CSI phase for respiration and heart-rate monitoring [2]. FarSense uses the CSI ratio of two antennas to suppress common-mode noise and extend respiration range [3]. A practical order to test is:
- phase difference or complex ratio between two antennas in the same measurement;
- phase difference between adjacent subcarriers;
- first principal component of multi-subcarrier amplitude changes;
- a single-chain amplitude baseline.
For every candidate, collect three classes first: empty room, still person and moving person. Check whether the feature tracks respiratory cycles rather than only detecting motion.
3. A deliberately conservative estimator
The functions below assume the input is a real-valued series produced by selecting one chain for each measurement. The estimator removes a linear trend, applies a Hann window and reports peak frequency plus two quality markers. Resample on the real timestamp grid first:
import numpy as np
from numpy.fft import rfft, rfftfreq
def resample_by_time(x, t, fs=20.0):
x = np.asarray(x, dtype=float)
t = np.asarray(t, dtype=float)
order = np.argsort(t)
t, x = t[order], x[order]
tu = np.arange(t[0], t[-1], 1.0 / fs)
xu = np.interp(tu, t, x)
return xu, tu
def estimate_bpm(x, fs, band):
x = np.asarray(x, dtype=float)
n = len(x)
if n < int(fs * 20):
return None
idx = np.arange(n, dtype=float)
slope, intercept = np.polyfit(idx, x, 1)
y = x - (slope * idx + intercept)
y = (y - y.mean()) * np.hanning(n)
power = np.abs(rfft(y)) ** 2
freqs = rfftfreq(n, 1.0 / fs)
mask = (freqs >= band[0]) & (freqs <= band[1])
if not mask.any():
return None
band_power = power[mask]
band_freqs = freqs[mask]
k = int(np.argmax(band_power))
peak_freq = band_freqs[k]
floor = float(np.median(band_power)) + 1e-12
prominence_db = 10.0 * np.log10((band_power[k] + 1e-12) / floor)
band_ratio = float(band_power.sum() / (power.sum() + 1e-12))
return {
"bpm": float(peak_freq * 60.0),
"prominence_db": float(prominence_db),
"band_power_ratio": band_ratio,
}
Example bands:
# Interpolation regularises time; it does not add physiological information.
xu, tu = resample_by_time(signal, timestamps, fs=20.0)
breath = estimate_bpm(xu, fs=20.0, band=(0.10, 0.50)) # 6–30 bpm
heart = estimate_bpm(xu, fs=20.0, band=(0.80, 2.00)) # 48–120 bpm
A 30-second window has a theoretical bin spacing of 1 / 30 = 0.033 Hz, or about 2 bpm. That is not a promise
of 2 bpm error. SNR, window leakage and non-stationarity still matter.
4. Thirty-second windows and availability gating
Start with a 30-second window and a 5-second hop. Respiration is covered by at least three cycles, while heart rate contains more. The tradeoff is at least 30 seconds of warm-up. Store quality fields with every estimate:
window = {
"start_s": start,
"end_s": end,
"valid_sample_ratio": valid_count / expected_count,
"max_gap_s": max_gap,
"motion_score": motion_score,
"band_power_ratio": result["band_power_ratio"],
"prominence_db": result["prominence_db"],
"bpm": result["bpm"],
}
An example gate:
def vitals_status(w, limits):
if w["valid_sample_ratio"] < limits["min_valid_ratio"]:
return "Unavailable: data coverage"
if w["max_gap_s"] > limits["max_gap_s"]:
return "Unavailable: gap"
if w["motion_score"] > limits["max_motion"]:
return "Unavailable: motion"
if w["prominence_db"] < limits["min_prominence_db"]:
return "Unavailable: weak peak"
if w["band_power_ratio"] < limits["min_band_ratio"]:
return "Unavailable: broadband noise"
return "OK"
Do not copy these thresholds as universal constants. Collect labelled empty-room, still, talking, walking and posture-change data on the actual firmware, channel, link and installation. Choose thresholds on that data and report both false-positive rate and coverage.
5. A reproducible calibration protocol
This is an experimental design, not a completed accuracy result. Participants must give informed consent and be told that the work is for research, not diagnosis.
5.1 Setup
- fix router channel, bandwidth, transmit power and antenna orientation;
- record distance, subject orientation, wall materials and furniture layout;
- use a dedicated AP interface to produce steady traffic;
- place a reference device where its clock can be synchronised: chest band, pulse oximeter or an independent breathing-count application;
- record two minutes of an empty room to find fan, HVAC and power-supply cycles.
5.2 Capture sequence
- empty room for two minutes;
- seated and still for two minutes, without talking or changing posture;
- paced breathing at 10, 15 and 20 breaths/min, at least one minute per segment;
- normal breathing for two minutes with a synchronised reference;
- small in-place motion for 30 seconds, followed by 60 seconds still;
- repeat after changing distance or orientation, at least three times.
Do not use "I feel like my heart rate is 70" as ground truth. Use a time-synchronised pulse oximeter or another validated reference for research comparison only.
5.3 Store with every run
| Field | Purpose |
|---|---|
| UTC start time and monotonic clock | align CSI with the reference device |
| device, firmware, channel, bandwidth | reproduce the capture |
| distance, orientation, posture | interpret multipath changes |
| reference bpm and update time | compute error rather than judging waveform appearance |
| RSSI, SNR, selected chain | detect weak links or chain switching |
| loss, maximum gap, valid-sample ratio | explain Unavailable windows |
| empty/still/motion labels | estimate misses and false positives |
6. How to report metrics
Reporting only the window with the smallest error against a reference overstates performance. At minimum report:
- coverage: fraction of all windows that pass the quality gate;
- conditional MAE/RMSE: computed only on
OKwindows with a valid reference; - empty-room false-positive rate: how often a normal rate appears with nobody present;
- recovery time: delay before a stable estimate returns after motion;
- latency: window length plus hop delay;
- results by scenario: distance, orientation, subject and device separately.
When comparing two algorithms, use the same windows, reference timeline and gate. If one method reports only 20% of windows and has a lower MAE, show both the error and the coverage.
7. Common failure modes
| Symptom | Likely cause | Response |
|---|---|---|
| Empty room repeatedly reports 15 bpm | fan, HVAC or periodic router load | capture an empty-room baseline and investigate periodic sources |
| Talking causes a jump to high frequency | speech and jaw motion dominate chest micro-motion | gate the window as unavailable during motion |
| Heart rate is always 3–4× respiration | respiratory harmonics or nonlinear mixing | inspect peak separability; do not relabel the strongest harmonic as heart rate |
| Selected chain changes every window | some chains are empty or rate control changes | fix a valid chain or use a two-antenna ratio |
| Uniform-looking time axis creates fake peaks | using record index instead of timestamps | resample on timestamps and inspect large gaps |
| Performance collapses in another room | different multipath, range, attenuation and gain | recalibrate; do not reuse old thresholds |
| Deep-learning accuracy cannot be reproduced | same-subject leakage or overlapping train/test windows | split by subject and date |
8. What the public dataset can and cannot prove
The public 10-minute capture is useful for:
- parsing the 24-byte CSI header and I/Q;
- grouping records on bit 15;
- building amplitude heatmaps and motion metrics;
- testing timestamps, loss and chain-selection code.
It has no synchronised chest-band or heart-rate reference, so it cannot support claims such as "2 bpm respiration error" or "5 bpm heart-rate error." Those results require a new consented capture with a reference device.
9. Minimum implementation checklist
- select one chain per measurement; do not treat 95.5 records/s as a 95.5 Hz physiological signal;
- resample on timestamps and record the maximum gap;
- establish empty-room and still-person baselines before trying respiration;
- repeat each subject, posture and distance at least three times;
- keep raw CSI, reference values, labels and quality metrics;
- prove respiration before attempting heart rate;
- return
Unavailablewhen the gate fails; - make no diagnosis, disease-monitoring or guaranteed-accuracy claim.
10. Related implementation
- Phase processing and spectral estimation: this page plus the Python pipeline;
- Real data: 10-second sample and 10-minute dataset;
- Dataset parsing: from eight parts to a heatmap;
- Published methods: PhaseBeat [2], FarSense [3] and the CSI sensing survey [1].
References and data sources
Every external claim carries a source and an evidence grade (grading scheme). Grade E marks first-party, reproducible measurements from this project.
- Grade A · standard/review
WiFi Sensing with Channel State Information: A Survey
W. Ma, W. Zhou, X. Wang · ACM Computing Surveys · 2019
doi:10.1145/3310194 · citations 857 · verified: CrossRef API, 2026-10-11 · The field's most-cited CSI sensing survey (857 citations, verified via CrossRef). - Grade B · peer-reviewed
PhaseBeat: Exploiting CSI Phase Data for Vital Sign Monitoring with Commodity WiFi Devices
X. Wang, C. Yang, S. Mao · IEEE ICDCS 2017 · 2017
doi:10.1109/icdcs.2017.206 · citations 325 · verified: CrossRef API, 2026-10-11 · A representative work on respiration/heart-rate monitoring from CSI phase (325 citations). - Grade B · peer-reviewed
FarSense: Pushing the Range Limit of WiFi-based Respiration Sensing with CSI Ratio of Two Antennas
Y. Zeng, D. Wu, J. Xiong, et al. · Proceedings of the ACM on Interactive, Mobile, Wearable and Ubiquitous Technologies (IMWUT) · 2019
doi:10.1145/3351279 · citations 461 · verified: CrossRef API, 2026-10-11 · Range limits for respiration sensing and the two-antenna CSI ratio method (461 citations).