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].

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

CSI amplitude heatmap

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

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.

  1. 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.
  2. 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)
  3. 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)
  4. 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
  5. 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)