Wi-Fi CSI in Python: parsing, phase sanitising, heatmaps and motion metrics
2026-10-11 · 2 min read
Getting CSI is step one; parse → sanitise → features decides whether your data is usable. Here is a minimal runnable pipeline.
1. Parsing: two containers
| Container | Layout |
|---|---|
| UDP payload | 24-byte header + interleaved int16 I/Q |
| Capture file | each record prefixed by 10 bytes (double ts + uint16 len) |
import struct
REC = struct.Struct("<dH")
HDR = struct.Struct("<I6sbBBBBBHHHH")
def iter_records(path):
with open(path, "rb") as f:
while True:
head = f.read(REC.size)
if len(head) < REC.size:
return
_ts, length = REC.unpack(head)
buf = f.read(length)
if len(buf) < HDR.size:
return
ts, ta, rssi, snr, bw, ch, mode, _pad, n, tx, rx, chain = HDR.unpack_from(buf)
iq = struct.unpack_from(f"<{n*2}h", buf, HDR.size)
yield dict(ts=ts, rssi=rssi, snr=snr, n=n, tx=tx, rx=rx, chain=chain,
i=iq[0::2], q=iq[1::2])
Sample data: 10 s / 256 subcarriers — use it to test the parser above [4].
Ad slot (AdSense/Ezoic once live)
2. Phase sanitising
Raw phase contains the true channel phase plus linear drift from CFO/SFO and hardware offsets:
import numpy as np
def sanitize_phase(phase):
p = np.unwrap(phase)
idx = np.arange(len(p))
slope, intercept = np.polyfit(idx, p, 1)
return p - (slope * idx + intercept)
3. Features
amp = np.abs(i + 1j * q) # per-subcarrier amplitude
heatmap = np.vstack(amplitude_rows) # (measurements, subcarriers)
motion = np.abs(np.diff(heatmap, axis=0)).mean(axis=1) # 1 s rolling motion metric

The figure is drawn from the project's 10-minute raw capture [5].
4. Common traps
| Trap | Symptom | Fix |
|---|---|---|
| sampling jitter | fake spectral peaks | resample by timestamp, not by index |
| using raw phase | pure noise | CFO/SFO correction first |
| wrong measurement grouping | chains split apart | group by (ts, ta), end on chain_info bit15 |
| timestamp unit | time axis off by 1000× | this project's ts is microseconds |
5. Next
- Tools · Dataset
- Algorithm zoos:
SenseFi[3] · fast parsing:csiread[2] · multi-platform parsing:CSIKit[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 C · official docs
Gi-z/CSIKit — Python CSI parsing and visualisation
G. Zhang
github.com/Gi-z/CSIKit · verified: GitHub API 2026-10-11 (stars 307, MIT, last push 2025-02-19) · Parses many formats: Atheros / Intel / Nexmon / ESP32 / FeitCSI / PicoScenes. - Grade C · official docs
citysu/csiread — fast Cython CSI parser
S. Zhu
github.com/citysu/csiread · verified: GitHub API 2026-10-11 (stars 142, MIT, last push 2024-10-12) - Grade C · official docs
xyanchen/WiFi-CSI-Sensing-Benchmark (SenseFi) — deep-learning benchmark
X. Chen, et al.
github.com/xyanchen/WiFi-CSI-Sensing-Benchmark · verified: GitHub API 2026-10-11 (stars 638, MIT, last push 2023-11-06) - Grade E · first-party measurement
Router CSI Lab — GL-MT6000 (MT7986) Wi-Fi CSI dataset, 10 s sample + 10 min capture
Router CSI Lab · 2026
csi.okwujun.pw/zh/datasets/ · verified: CC BY 4.0, SHA-256 published; DOI (Zenodo) pending - Grade E · first-party measurement
This project's 10-minute raw capture (57,293 records / 600.0 s, SHA-256 00373f83…95e2)
Router CSI Lab · 2026
csi.okwujun.pw/zh/datasets/ · verified: Reassembled chunks verified byte-identical (2026-10-11)