Asking the fleet what it is doing…
monad-knowledge Wi-Fi sensing lab · FIIT STU

The capture format

A binary format you can read.

A capture is a binary file, and a binary file is readable exactly when you can put a finger on a byte and be told what it is. Below is one real capture from this project's archive, decoded in front of you by the same reader the specification documents. Pick a field on the left, or click any byte, and the two panes point at each other.

In plain words A capture is a binary file, and a binary file is readable exactly when you can put a finger on a byte and be told what it is. This is that finger: a real file from the archive on the left, its bytes in the middle, and the meaning of what you point at on the right. Point your own reader at the same file and compare.

This is the workbench without its interactive layer. Every byte, table, figure and note the app carries is below, in file order — what is missing is the ability to select a field and see its bytes light up.

The file head

000000 43 53 49 51 01 00 01 00 38 0d 00 00 7b 22 62 6c CSIQ....8...{"bl
000010 65 22 3a 7b 22 61 64 61 70 74 65 72 22 3a 22 68 e":{"adapter":"h
000020 63 69 30 22 2c 22 61 72 74 65 66 61 63 74 22 3a ci0","artefact":
000030 22 62 6c 65 5f 72 73 73 69 2e 70 61 72 71 75 65 "ble_rssi.parque
Twelve bytes of header, then the session block. A reader that does not recognise the magic must stop here rather than guess.

The first record

000d44 a1 2e 03 00 00 01 04 00 00 00 a8 b3 80 86 02 04 ................
000d54 00 00 00 dd 57 5f b1 03 08 00 00 00 66 2a 32 18 ....W_......f*2.
000d64 8a b5 cf 18 04 04 00 00 00 04 c1 00 00 05 03 00 ................
000d74 00 00 01 04 01 11 02 00 00 00 00 03 14 10 01 00 ................
000d84 00 a0 01 00 00 04 00 00 00 a8 b3 80 86 00 00 00 ................
000d94 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000da4 00 00 00 00 00 00 00 00 00 03 00 00 01 00 00 02 ................
000db4 01 02 00 02 03 34 00 00 00 00 00 00 00 47 00 00 .....4.......G..
000dc4 00 48 00 00 00 00 00 5e 00 53 00 00 00 fb 00 00 .H.....^.S......
000dd4 08 00 00 00 00 00 00 00 00 dd 57 5f b1 04 c1 00 ..........W_....
One tag byte, a four-byte length, then a chain of type-length-value fields. A reader skips what it does not know, which is why adding a type code is not a version bump.

The capture

A binary format you can read

CSIQ is what this project's nine radios write to disk. Its design goal is that a capture should be interpretable by somebody holding only the file and the specification. This page is that claim, tested: a real capture out of the archive, decoded in front of you.

Every number here came out of the file beside it. Nothing is written into the copy. The page decodes the committed capture with the same reader the specification documents, so a figure cannot drift from the bytes you can download.

This capture is the archive's session monad09_walk-abba-48-night_20260827-161301 — monad09, walk-abba-48-night, 5 GHz channel 48 at HT20, written by csid 0.2.0. Its own numbers disagree with each other, so it is excluded from every corpus total. It is in the catalogue deliberately, so a reader can see what one looks like.

The conformance kit

Every sample here is downloadable as a valid, self-contained capture, with the decode this project's reader produces for its first records as JSON. Point your own reader at the file and diff against the answer key. Where the two disagree, the specification wins and both readers are suspect — that is the document's own rule, and it is why there is a published fixture at all.

Capture Records The file The expected decode
2.4 GHz · HT20 · csid 0.2.0 2,000 band-2.4__condition-clean__width-HT20.csiq expected decode
5 GHz · HT20 · csid 0.2.0 2,000 band-5__condition-clean__width-HT20.csiq expected decode
5 GHz · HT20 · quarantined · csid 0.2.0 2,000 band-5__condition-quarantined__width-HT20.csiq expected decode
5 GHz · HT40+ · csid 0.2.0 2,000 band-5__condition-clean__width-HT40+.csiq expected decode
5 GHz · 80MHz · csid 0.2.0 2,000 band-5__condition-clean__width-80MHz.csiq expected decode
5 GHz · HT20 · csid 0.2.0 2,000 envelope-csiq.csiq expected decode
5 GHz · HT20 · csid 0.2.0 2,000 envelope-csiq.zst.csiq expected decode
5 GHz · HT20 · csid 0.2.0 2,000 writer-modern.csiq expected decode
5 GHz · HT20 · csid 0.1.0 2,000 writer-legacy.csiq expected decode
6 GHz · HT20 · csid 0.2.0 2,000 band-6.csiq expected decode

Defects the format admits

  • SEQ is a driver record counter. Not the 802.11 sequence number its name suggests. Corrected 2026-08-24; the name is kept because changing it would break every existing reader.
  • Two reference readers rendered an absent width differently. One as an unknown code, one as its own default. Two sentinels for one fact, found only when both were decoded side by side. Neither is right: absence should be reported as absence.
  • The header version cannot say which fields a file carries. Adding a type code is not a version bump, so two captures both stamping version 1 can differ by five codes. The capability probe is the answer instead.

What the catalogue does not hold

The catalogue is declared by axis rather than by session, so a cell the archive does not populate is reported rather than quietly dropped. 7 declared cells are empty:

  • band=2.4,condition=quarantined,width=HT20
  • band=2.4,condition=clean,width=HT40+
  • band=2.4,condition=quarantined,width=HT40+
  • band=2.4,condition=clean,width=80MHz
  • band=2.4,condition=quarantined,width=80MHz
  • band=5,condition=quarantined,width=HT40+
  • band=5,condition=quarantined,width=80MHz

The 2.4 GHz wide-channel cells are empty because this deployment runs 2.4 GHz on channel 11 at HT20 only. That is a fact about the fleet.

Offset 0, 12 bytes.

Twelve bytes, then everything else

Four bytes of magic, a version, a flags word, and the length of the block that follows. A reader that does not recognise the magic must stop here rather than guess: everything after these twelve bytes is only meaningful if they said what they were supposed to say.

The header version cannot say which fields a file carries. Adding a type code is not a version bump, so two captures both stamping version 1 can differ by five codes. The capability probe answers that instead.

Field Offset Bytes Value
magic 000000 4 B CSIQ
version 000004 2 B 1
flags 000006 2 B 0x0001
session_len 000008 4 B 3384

Session block

Offset 12, 3,384 bytes.

Provenance travels with the data

One JSON object at the head of every capture, carrying how the measurement was taken: the radio's channel and width, the build of the daemon that wrote it, the host it ran on, and the filter that decided which frames were kept.

The block is opaque by design. A writer adds a group without a format version bump, so a reader that dropped what it did not recognise would silently lose the newest thing anybody recorded. Every group this capture carries is listed, known or not.

Field Offset Bytes Value
ble 00000d 13 fields
ended_at 000191 33 B
environment 0001b3 8 fields
experiment 00036d 33 B
filter 00038f 6 fields
radio 000417 9 fields
run_id 0004c3 38 B
run_id_generated 0004ea 24 B
schema 000503 25 B
session_id 00051d 65 B
started_at 00055f 35 B
status 000583 19 B
summary 000597 8 fields
tag 000a2e 101 B
timesync 000a94 6 fields
_redaction 000b4c 3 fields
ble 13
adapterhci0
artefactble_rssi.parquet
durable_logble_scan.jsonl
hash_algorithmsha256(salt || addr_type || addr)[:hash_bytes], hex
hash_bytes8
lab_namespace_uuid6d6f6e61-6461-4076-b100-000000000000
parquet_schemable-rssi/2
requiredFalse
salt_bits256
salt_persistedFalse
scan_interval_ms100.0
scan_typepassive
scan_window_ms100.0
environment 8
build{"built_at": "2026-08-24T19:32:09Z", "csiq_format_version": 1, "profile": "release", "revision": "", "revision_source": "none", "rustc": "rustc 1.97.1 (8bab26f4f 2026-07-14)"}
cpu_governorperformance
csid_version0.2.0
driver_module/lib/modules/6.8.0-1061-raspi/updates/dkms/iwlwifi.ko
firmware78.3bfdc55f.0 ty-a0-gf-a0-78.uc
hostnamemonad09
kernel6.8.0-1061-raspi
regdomaincountry SK: DFS-ETSI
filter 6
countnull
fingerprintno-filter
frame_typesnull
rate_n_flags_masknull
rate_n_flags_valnull
timeout_usnull
radio 9
band5
center_freq_mhznull
channel48
control_freq_mhz5240
interfacewlp1s0
interval_us0
mac_filter(empty list)
monitorwlp1s0mon0
widthHT20
summary 8
ble{"adapter_errors": 0, "distinct_device_hashes": 27, "distinct_lab_participants": 0, "gap_alert_s": 0.0, "gaps_over_alert": 0, "lab_frames": 0, "malformed_log_lines": 0, "max_gap_s": 4.997, "mean_rate_hz": 3.165408611602167, "observations": 5696, "parquet_rows": 0, "rssi_unavailable": 0, "scan_restarts": 0, "status": "segment", "unparsed_events": 0}
capture_bytes620985820
empty_records0
live_dropped0
mean_rate_hz492.8456495842563
records887121
tone_counts52, 56
transmitters{"distinct": 83, "top": [{"mac": "00:00:5e:00:53:04", "records": 434734}, {"mac": "00:00:5e:00:53:00", "records": 391535}, {"mac": "00:00:5e:00:53:03", "records": 20461}, {"mac": "00:00:5e:00:53:01", "records": 20385}, {"mac": "00:00:5e:00:53:02", "records": 19865}, {"mac": "00:00:5e:00:53:05", "records": 48}, {"mac": "00:00:5e:00:53:06", "records": 2}, {"mac": "00:00:5e:00:53:07", "records": 2}, {"mac": "00:00:5e:00:53:08", "records": 2}, {"mac": "00:00:5e:00:53:09", "records": 2}, {"mac": "00:00:5e:00:53:0a", "records": 2}, {"mac": "00:00:5e:00:53:0b", "records": 2}, {"mac": "00:00:5e:00:53:0c", "records": 2}, {"mac": "00:00:5e:00:53:0d", "records": 2}, {"mac": "00:00:5e:00:53:0e", "records": 2}, {"mac": "00:00:5e:00:53:0f", "records": 2}]}
timesync 6
artefacttime_transfer.parquet
durable_logtime_transfer.jsonl
ftm_tolerance_us2000
one_way_floor_us5000
parquet_schematime-transfer/1
requiredFalse
_redaction 3
schemerfc7042-documentation-range
fieldsSRC_MAC (0x09), the src_mac inside VENDOR_HDR (0x14), at Appendix A offset 68, every MAC-shaped string anywhere in the session block
noteSource addresses were relabelled into 00:00:5E:00:53:xx, a range reserved for documentation. Distinct transmitters remain distinct. Hostnames are unchanged: the site publishes node names already, and removing them would strip real provenance from a file that exists to demonstrate provenance.

Record 0

Offset 3,396, 819 bytes.

A self-describing record

A tag byte, a four-byte length, then a chain of type-length-value fields. Each field costs five bytes of header, and that is the price of the property below it: a reader skips any type code it does not know, so a writer can add one without breaking every existing reader. Adding a type code is therefore not a version bump.

A field this file does not carry is a fact about the writer, not a gap in the measurement. It changes which questions can be asked of the capture at all, so each absence below is printed with what it costs.

Field Offset Bytes Value
record tag 0xA1 000d44 1 B 0xA1
record length 000d45 4 B 814
FTM 000d49 5+4 B
US 000d52 5+4 B
UNIX_TS_NS 000d5b 5+8 B
RNF 000d68 5+4 B
PHY 000d71 5+3 B
BW_ANTSEL 000d79 5+2 B
VENDOR_HDR 000d80 5+272 B
SEQ 000e95 5+1 B
NRX 000e9b 5+1 B
NTX 000ea1 5+1 B
NTONE 000ea7 5+2 B
SRC_MAC 000eae 5+6 B
CHANNEL 000eb9 5+4 B
WIDTH 000ec2 5+2 B
RSSI 000ec9 5+4 B
CSI_MATRIX 000ed2 5+416 B

What this writer does and does not record

Presence was established by probing 2,000 records. Absence is only ever evidence over the records looked at.

Field In this file What it gives What its absence costs
MONO_US (0x12) recorded Separates received frames from the node's own transmissions.
BW_ANTSEL (0x11) recorded States the bandwidth directly instead of leaving it to be decoded from RNF.
VENDOR_HDR (0x14) recorded Keeps the 272-byte driver header verbatim, so a later reader can recover a field nobody has named yet.
NODE_* (0x40–0x43) not recorded Core temperature, throttling, spool headroom and load, per record. The conditions the measurement was taken under are not in the file.
NODE_NIC_TEMP_C (0x44) not recorded The radio's own temperature — a different sensor in a different unit from the core. Thermal phase drift cannot be attributed to the card.
RSSI (0x0C) recorded Per-chain received power, and the −127 stale-block sentinel.
PHY (0x05) recorded Modulation, MCS and spatial streams.
SRC_MAC (0x09) recorded Which transmitter each record came from.

FTM

Why one timestamp is not enough

A record carries up to five timestamps, and no one of them answers every question. Each series is drawn as seconds since its own first reading, because they have different epochs and one absolute axis would say nothing.

The FTM counter is 32 bits at 320 MHz, so it wraps. Differencing the stored form across a wrap gives a large negative interval that looks like a clock stepping backwards.

FTM, as stored

A 32-bit 320 MHz counter. It wraps every 13.42 s — 0 times in these records.

FTM, unwrapped

The same counter through Capture.clocked(). Monotonic, and the only form worth differencing.

US — the writer's own clock

Stamped by csid when it took the record, not by the radio when it heard the frame.

UNIX_TS_NS — wall clock

Steppable. These nodes carry no real-time clock, so it moves when chrony corrects it.

MONO_US — CLOCK_MONOTONIC on the receive path

Present on every received frame and absent on the node's own transmissions.

The FTM counter is 32 bits at 320 MHz, so it wraps every 13.42 s — 0 times in the 400 records above.

BW_ANTSEL

242 tones is HE20 or VHT80

A record says how many tones it carries. That number alone does not name the numerology: 242 tones is HE20 at 78.125 kHz spacing, or VHT80 at 312.5 kHz. Deriving the grid needs the bandwidth too.

The bandwidth is the field the reader rule exists for — prefer the explicit code, fall back to decoding the rate-and-flags word, and report genuine absence as absence rather than as 20 MHz.

Tones Bandwidth Spacing Numerology
52 20 MHz 312.5 kHz Legacy OFDM / HT20
56 20 MHz 312.5 kHz HT20
114 40 MHz 312.5 kHz HT40
242 20 MHz 78.125 kHz HE20
242 80 MHz 312.5 kHz VHT80
484 40 MHz 78.125 kHz HE40
996 80 MHz 78.125 kHz HE80

What the catalogue's records carry

Capture Declared width Width in the record Tones Bandwidth Spacing
2.4 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT20 · quarantined · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT40+ · csid 0.2.0 HT40+ HT40+ 52 40 MHz 312.5 kHz
5 GHz · 80MHz · csid 0.2.0 80MHz 80MHz 52 40 MHz 312.5 kHz
5 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz
5 GHz · HT20 · csid 0.1.0 HT20 HT20 52 20 MHz 312.5 kHz
6 GHz · HT20 · csid 0.2.0 HT20 HT20 52 20 MHz 312.5 kHz

SEQ

A name the format is stuck with

SEQ is a driver record counter, not the 802.11 sequence number its name suggests. Corrected 2026-08-24. The name is kept because changing it would break every existing reader.

SRC_MAC

The one thing that is not the archive's

Record framing and lengths are bit-identical to the original capture. The only bytes that changed are the six-byte MAC addresses, relabelled into the RFC 7042 documentation range — here, in the session block, and inside the 272-byte driver header that keeps its own copy of the source address.

Hostnames are not redacted: this site publishes node names already, and stripping them would remove the provenance these files exist to demonstrate.

RSSI

A sentinel is not a weak measurement

Received power is stored per chain in dBm. One value is not a measurement: −127 marks a stale block — the driver handed back the previous record's bytes unchanged.

Averaged in, it becomes the weakest signal the fleet ever recorded, tens of decibels below anything real, and it drags every mean it touches. It is counted separately everywhere it appears on this page.

-96 dBm -33 dBm

40,000 chain readings across the whole catalogue. The sentinel is not a bar in this chart. It is counted separately — 0 of them — and sits 31 dB below the weakest real reading.
Capture Chain readings Measured range Sentinels
2.4 GHz · HT20 · csid 0.2.0 4,000 -70 to -57 dBm 0
5 GHz · HT20 · csid 0.2.0 4,000 -96 to -58 dBm 0
5 GHz · HT20 · quarantined · csid 0.2.0 4,000 -82 to -65 dBm 0
5 GHz · HT40+ · csid 0.2.0 4,000 -91 to -54 dBm 0
5 GHz · 80MHz · csid 0.2.0 4,000 -90 to -54 dBm 0
5 GHz · HT20 · csid 0.2.0 4,000 -80 to -58 dBm 0
5 GHz · HT20 · csid 0.2.0 4,000 -82 to -33 dBm 0
5 GHz · HT20 · csid 0.2.0 4,000 -85 to -57 dBm 0
5 GHz · HT20 · csid 0.1.0 4,000 -96 to -58 dBm 0
6 GHz · HT20 · csid 0.2.0 4,000 -95 to -38 dBm 0

CSI_MATRIX

The CSI matrix, and the two ways to read it wrong

Complex coefficients across the tone grid, one block per antenna chain — the shape the channel imposed on a frame as it crossed the room.

Amplitude here is AGC-normalised, so it is channel shape only. The correlation between the squared amplitude and received power is about 0.01 on this hardware. The absolute scale is in the RSSI field, not in this one.

Two properties of the stored matrix are easy to get wrong, and getting either wrong is silent: the result looks healthy and is not. Both are visible in the impulse response — the same data seen in delay rather than in frequency — and in nothing else.

  1. Coefficients are imaginary first, then real. Reading them the other way round yields i·conj(H). Amplitude is untouched, so every amplitude plot stays byte-identical, and every phase is mirrored.
  2. The payload is chain-major. Chains are contiguous blocks of tones, not interleaved per tone. Read the other way, the impulse response smears instead of concentrating.
Amplitude per tone, 2 chains, 52 tones across 2×1 chains. The vertical scale carries no absolute level. The absolute scale is in the RSSI field, which this record gives as -71 and -72 dBm.

Chain-major, imaginary first

Energy concentrated at early delays. This is a channel. Early-delay energy over late: 0.05. Energy in the four strongest taps: 0.743.

Real first

Amplitude is byte-identical to the correct reading. The response is anti-causal. Early-delay energy over late: 18.26. Energy in the four strongest taps: 0.743.

Read as tone-interleaved

The response smears across the delay axis instead of concentrating. Early-delay energy over late: 0.18. Energy in the four strongest taps: 0.533.

This capture inverts the specification's result, and we cannot yet say why. The specification measures the correct reading at 21.5× more early-delay energy than late across a population of real captures. On this file the ratio runs the other way. Four of the ten catalogue samples do this, and all three of their sessions are walk-* capture profiles. The page prints what it measured rather than what it expected, because a plot that contradicts the number printed beside it teaches a reader to distrust the plot.

The same measurement, across the catalogue

Capture Profile Records Imaginary first Real first Verdict
2.4 GHz · HT20 · csid 0.2.0 rate-ladder-sweep 400 14.74 0.069 as specified
5 GHz · HT20 · csid 0.2.0 exp-chan-36 400 26.59 0.058 as specified
5 GHz · HT20 · quarantined · csid 0.2.0 walk-abba-48-night 400 0.14 7.238 inverted
5 GHz · HT40+ · csid 0.2.0 explore-vht-rx-40 400 16.30 0.178 as specified
5 GHz · 80MHz · csid 0.2.0 explore-vht-rx 400 16.38 0.117 as specified
5 GHz · HT20 · csid 0.2.0 walk-raster-48 400 0.18 5.949 inverted
5 GHz · HT20 · csid 0.2.0 exp-chan-36 400 15.89 0.089 as specified
5 GHz · HT20 · csid 0.2.0 exp-chan-36 400 9.82 0.107 as specified
5 GHz · HT20 · csid 0.1.0 exp-chan-36 400 26.82 0.052 as specified
6 GHz · HT20 · csid 0.2.0 chan-6ghz-psc5 400 0.19 6.678 inverted

Record 1

Offset 4,215, 819 bytes.

Field Offset Bytes Value
record tag 0xA1 001077 1 B 0xA1
record length 001078 4 B 814
FTM 00107c 5+4 B
US 001085 5+4 B
UNIX_TS_NS 00108e 5+8 B
RNF 00109b 5+4 B
PHY 0010a4 5+3 B
BW_ANTSEL 0010ac 5+2 B
VENDOR_HDR 0010b3 5+272 B
SEQ 0011c8 5+1 B
NRX 0011ce 5+1 B
NTX 0011d4 5+1 B
NTONE 0011da 5+2 B
SRC_MAC 0011e1 5+6 B
CHANNEL 0011ec 5+4 B
WIDTH 0011f5 5+2 B
RSSI 0011fc 5+4 B
CSI_MATRIX 001205 5+416 B

Record 2

Offset 5,034, 819 bytes.

Field Offset Bytes Value
record tag 0xA1 0013aa 1 B 0xA1
record length 0013ab 4 B 814
FTM 0013af 5+4 B
US 0013b8 5+4 B
UNIX_TS_NS 0013c1 5+8 B
RNF 0013ce 5+4 B
PHY 0013d7 5+3 B
BW_ANTSEL 0013df 5+2 B
VENDOR_HDR 0013e6 5+272 B
SEQ 0014fb 5+1 B
NRX 001501 5+1 B
NTX 001507 5+1 B
NTONE 00150d 5+2 B
SRC_MAC 001514 5+6 B
CHANNEL 00151f 5+4 B
WIDTH 001528 5+2 B
RSSI 00152f 5+4 B
CSI_MATRIX 001538 5+416 B

What changed in these files, and what did not

Every sample is a byte-preserving slice of a real capture from this project's archive. Record framing and lengths are bit-identical to the original. The only bytes that changed are the six-byte MAC addresses, relabelled into the RFC 7042 documentation range — in the SRC_MAC field, in the session block, and inside the 272-byte driver header that keeps its own copy of the source address. Hostnames are not redacted: this site publishes node names already, and stripping them would remove the provenance these files exist to demonstrate.

Compare two captures side by side · This capture (.csiq) · Its expected decode · The reader and the specification ↗