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 6d 0d 00 00 7b 22 62 6c CSIQ....m...{"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

000d79 a1 3b 03 00 00 01 04 00 00 00 d0 6f db 49 02 04 .;.........o.I..
000d89 00 00 00 e1 b9 3e 00 03 08 00 00 00 68 1d b2 26 .....>......h..&
000d99 db e1 d1 18 04 04 00 00 00 04 c1 00 00 05 03 00 ................
000da9 00 00 01 04 01 11 02 00 00 00 00 03 12 08 00 00 ................
000db9 00 5d 7c 3d 00 00 00 00 00 14 10 01 00 00 a0 01 .]|=............
000dc9 00 00 04 00 00 00 d0 6f db 49 00 00 00 00 00 00 .......o.I......
000dd9 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000de9 00 00 00 00 00 00 03 00 00 01 00 00 02 01 02 00 ................
000df9 02 03 34 00 00 00 00 00 00 00 4a 00 00 00 4f 00 ..4.......J...O.
000e09 00 00 00 00 5e 00 53 02 00 00 c8 00 00 08 00 00 ....^.S.........
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 monad04_chan-6ghz-psc5_20260903-175758 — monad04, chan-6ghz-psc5, 6 GHz channel 5 at HT20, written by csid 0.2.0. It was selected from 10 candidates in its cell, by useful yield.

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 3437

Session block

Offset 12, 3,437 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 29 B
filter 00038b 6 fields
radio 000413 12 fields
run_id 000517 39 B
run_id_generated 00053f 24 B
schema 000558 25 B
session_id 000572 61 B
started_at 0005b0 35 B
status 0005d4 19 B
summary 0005e8 8 fields
tag 000a73 85 B
timesync 000ac9 6 fields
_redaction 000b81 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:10Z", "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-1060-raspi/updates/dkms/iwlwifi.ko
firmware78.3bfdc55f.0 ty-a0-gf-a0-78.uc
hostnamemonad04
kernel6.8.0-1060-raspi
regdomaincountry SK: DFS-ETSI
filter 6
countnull
fingerprintno-filter
frame_typesnull
rate_n_flags_masknull
rate_n_flags_valnull
timeout_usnull
radio 12
achieved_center_freq_mhz5975
achieved_control_freq_mhz5975
achieved_width_mhz20
band6
center_freq_mhznull
channel5
control_freq_mhz5975
interfacewlp1s0
interval_us0
mac_filter(empty list)
monitorwlp1s0mon0
widthHT20
summary 8
ble{"adapter_errors": 0, "distinct_device_hashes": 6, "distinct_lab_participants": 0, "gap_alert_s": 0.0, "gaps_over_alert": 0, "lab_frames": 0, "malformed_log_lines": 0, "max_gap_s": 8.635, "mean_rate_hz": 0.4818977242232925, "observations": 588, "parquet_rows": 0, "rssi_unavailable": 0, "scan_restarts": 0, "status": "segment", "unparsed_events": 0}
capture_bytes116385500
empty_records0
live_dropped0
mean_rate_hz135.77442637118253
records166265
tone_counts52
transmitters{"distinct": 163, "top": [{"mac": "00:00:5e:00:53:00", "records": 136442}, {"mac": "00:00:5e:00:53:01", "records": 15195}, {"mac": "00:00:5e:00:53:02", "records": 14468}, {"mac": "00:00:5e:00:53:09", "records": 1}, {"mac": "00:00:5e:00:53:0a", "records": 1}, {"mac": "00:00:5e:00:53:0b", "records": 1}, {"mac": "00:00:5e:00:53:0c", "records": 1}, {"mac": "00:00:5e:00:53:0d", "records": 1}, {"mac": "00:00:5e:00:53:0e", "records": 1}, {"mac": "00:00:5e:00:53:0f", "records": 1}, {"mac": "00:00:5e:00:53:10", "records": 1}, {"mac": "00:00:5e:00:53:11", "records": 1}, {"mac": "00:00:5e:00:53:12", "records": 1}, {"mac": "00:00:5e:00:53:13", "records": 1}, {"mac": "00:00:5e:00:53:14", "records": 1}, {"mac": "00:00:5e:00:53:15", "records": 1}]}
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,449, 832 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 000d79 1 B 0xA1
record length 000d7a 4 B 827
FTM 000d7e 5+4 B
US 000d87 5+4 B
UNIX_TS_NS 000d90 5+8 B
RNF 000d9d 5+4 B
PHY 000da6 5+3 B
BW_ANTSEL 000dae 5+2 B
MONO_US 000db5 5+8 B
VENDOR_HDR 000dc2 5+272 B
SEQ 000ed7 5+1 B
NRX 000edd 5+1 B
NTX 000ee3 5+1 B
NTONE 000ee9 5+2 B
SRC_MAC 000ef0 5+6 B
CHANNEL 000efb 5+4 B
WIDTH 000f04 5+2 B
RSSI 000f0b 5+4 B
CSI_MATRIX 000f14 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 -74 and -79 dBm.

Chain-major, imaginary first

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

Real first

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

Read as tone-interleaved

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

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,281, 832 bytes.

Field Offset Bytes Value
record tag 0xA1 0010b9 1 B 0xA1
record length 0010ba 4 B 827
FTM 0010be 5+4 B
US 0010c7 5+4 B
UNIX_TS_NS 0010d0 5+8 B
RNF 0010dd 5+4 B
PHY 0010e6 5+3 B
BW_ANTSEL 0010ee 5+2 B
MONO_US 0010f5 5+8 B
VENDOR_HDR 001102 5+272 B
SEQ 001217 5+1 B
NRX 00121d 5+1 B
NTX 001223 5+1 B
NTONE 001229 5+2 B
SRC_MAC 001230 5+6 B
CHANNEL 00123b 5+4 B
WIDTH 001244 5+2 B
RSSI 00124b 5+4 B
CSI_MATRIX 001254 5+416 B

Record 2

Offset 5,113, 832 bytes.

Field Offset Bytes Value
record tag 0xA1 0013f9 1 B 0xA1
record length 0013fa 4 B 827
FTM 0013fe 5+4 B
US 001407 5+4 B
UNIX_TS_NS 001410 5+8 B
RNF 00141d 5+4 B
PHY 001426 5+3 B
BW_ANTSEL 00142e 5+2 B
MONO_US 001435 5+8 B
VENDOR_HDR 001442 5+272 B
SEQ 001557 5+1 B
NRX 00155d 5+1 B
NTX 001563 5+1 B
NTONE 001569 5+2 B
SRC_MAC 001570 5+6 B
CHANNEL 00157b 5+4 B
WIDTH 001584 5+2 B
RSSI 00158b 5+4 B
CSI_MATRIX 001594 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 ↗