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 43 0a 00 00 7b 22 62 6c CSIQ....C...{"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

000a4f a1 12 02 00 00 01 04 00 00 00 18 89 14 57 02 04 .............W..
000a5f 00 00 00 23 17 9d c3 03 08 00 00 00 e6 ea 69 95 ...#..........i.
000a6f 9c 84 ce 18 04 04 00 00 00 00 c1 00 00 05 03 00 ................
000a7f 00 00 01 00 01 0d 01 00 00 00 10 06 01 00 00 00 ................
000a8f 02 07 01 00 00 00 01 08 02 00 00 00 34 00 09 06 ............4...
000a9f 00 00 00 00 00 5e 00 53 01 0a 04 00 00 00 24 00 .....^.S......$.
000aaf 00 00 0b 02 00 00 00 01 00 0c 04 00 00 00 c2 ff ................
000abf bb ff 10 a0 01 00 00 b1 00 da ff 8f ff b0 00 b6 ................
000acf ff b3 ff 59 00 fe ff f9 ff 93 ff 1b 00 8d ff 43 ...Y...........C
000adf 00 8d ff 6c 00 9c ff 8e 00 b6 ff b8 00 01 00 ba ...l............
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 monad05_exp-chan-36_20260823-190509 — monad05, exp-chan-36, 5 GHz channel 36 at HT20, written by csid 0.1.0. It was selected from 381 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 2627

Session block

Offset 12, 2,627 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 7 fields
experiment 0002c0 26 B
radio 0002db 9 fields
run_id 000387 41 B
run_id_generated 0003b1 24 B
schema 0003ca 25 B
session_id 0003e4 50 B
started_at 000417 35 B
status 00043b 19 B
summary 00044f 8 fields
tag 000753 75 B
timesync 00079f 6 fields
_redaction 000857 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 7
cpu_governorperformance
csid_version0.1.0
driver_module/lib/modules/6.8.0-1060-raspi/updates/dkms/iwlwifi.ko
firmware78.3bfdc55f.0 ty-a0-gf-a0-78.uc
hostnamemonad05
kernel6.8.0-1060-raspi
regdomaincountry SK: DFS-ETSI
radio 9
band5
center_freq_mhznull
channel36
control_freq_mhz5180
interfacewlp1s0
interval_us0
mac_filter(empty list)
monitorwlp1s0mon0
widthHT20
summary 8
ble{"adapter_errors": 0, "distinct_device_hashes": 10, "distinct_lab_participants": 0, "gap_alert_s": 5.0, "gaps_over_alert": 0, "lab_frames": 0, "malformed_log_lines": 0, "max_gap_s": 3.816, "mean_rate_hz": 2.5614868923963474, "observations": 4610, "parquet_rows": 4610, "rssi_unavailable": 0, "scan_restarts": 0, "status": "ok", "unparsed_events": 0}
capture_bytes621186916
empty_records8539
live_dropped0
mean_rate_hz492.9921811599342
records887407
timesync{"distinct_transmitters": 2, "frames_seen": 886330, "ftm_paired": 726503, "malformed_log_lines": 0, "mean_rate_hz": 405.36275461073, "own_transmissions": 0, "protected_frames": 40727, "rows": 726571, "rows_app": 0, "rows_csid": 726571, "rx_stamp_source": "kernel", "status": "ok", "unrecognised_frames": 46647}
tone_counts52, 56
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 2,639, 535 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 000a4f 1 B 0xA1
record length 000a50 4 B 530
FTM 000a54 5+4 B
US 000a5d 5+4 B
UNIX_TS_NS 000a66 5+8 B
RNF 000a73 5+4 B
PHY 000a7c 5+3 B
SEQ 000a84 5+1 B
NRX 000a8a 5+1 B
NTX 000a90 5+1 B
NTONE 000a96 5+2 B
SRC_MAC 000a9d 5+6 B
CHANNEL 000aa8 5+4 B
WIDTH 000ab1 5+2 B
RSSI 000ab8 5+4 B
CSI_MATRIX 000ac1 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) not recorded Separates received frames from the node's own transmissions. received() raises rather than returning an empty iterator, because an empty result would read as a claim about the radio.
BW_ANTSEL (0x11) recorded States the bandwidth directly instead of leaving it to be decoded from RNF.
VENDOR_HDR (0x14) not recorded Keeps the 272-byte driver header verbatim, so a later reader can recover a field nobody has named yet. Anything the container does not decode today is gone from this capture.
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

Not in this file.

Not recorded by this writer. That is a fact about csid's version, not about the radio.

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

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.

WIDTH

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

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 -62 and -69 dBm.

Chain-major, imaginary first

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

Real first

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

Read as tone-interleaved

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

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 3,174, 535 bytes.

Field Offset Bytes Value
record tag 0xA1 000c66 1 B 0xA1
record length 000c67 4 B 530
FTM 000c6b 5+4 B
US 000c74 5+4 B
UNIX_TS_NS 000c7d 5+8 B
RNF 000c8a 5+4 B
PHY 000c93 5+3 B
SEQ 000c9b 5+1 B
NRX 000ca1 5+1 B
NTX 000ca7 5+1 B
NTONE 000cad 5+2 B
SRC_MAC 000cb4 5+6 B
CHANNEL 000cbf 5+4 B
WIDTH 000cc8 5+2 B
RSSI 000ccf 5+4 B
CSI_MATRIX 000cd8 5+416 B

Record 2

Offset 3,709, 535 bytes.

Field Offset Bytes Value
record tag 0xA1 000e7d 1 B 0xA1
record length 000e7e 4 B 530
FTM 000e82 5+4 B
US 000e8b 5+4 B
UNIX_TS_NS 000e94 5+8 B
RNF 000ea1 5+4 B
PHY 000eaa 5+3 B
SEQ 000eb2 5+1 B
NRX 000eb8 5+1 B
NTX 000ebe 5+1 B
NTONE 000ec4 5+2 B
SRC_MAC 000ecb 5+6 B
CHANNEL 000ed6 5+4 B
WIDTH 000edf 5+2 B
RSSI 000ee6 5+4 B
CSI_MATRIX 000eef 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 ↗