Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Troubleshooting & Calibration

Most CSI problems are not subtle. They are a channel mismatch, an acquisition setting, or a console that cannot carry what the radio captured. This section is ordered by how often each cause actually turns out to be the one, so working through it in order is usually faster than reasoning about it.

The second half covers calibration — the adjustments that make measurements comparable across time, across devices, and across rooms.

Start With the Counters

Before changing anything, find out which half of the system is failing. On a device running esp-csi-cli-rs:

show-stats

It reports packets transmitted and received, rates in Hz, and drop counts. One distinction resolves most cases:

  • The received count is low. The radio is not hearing the traffic. The problem is upstream: channel, band, acquisition configuration, distance, or an emitter that is not transmitting.
  • The received count is fine but fewer rows reach you. The radio heard it and the delivery path lost it. The problem is downstream: console bandwidth, the async logging queue, or a filter.

From firmware, the same numbers are available as get_total_rx_packets, get_pps_rx, get_dropped_packets_rx, and — specifically for the async logging queue — get_log_packet_drops.

Note that drops include frames rejected by set-csi-filter. That is deliberate: a filtered frame stays visible as a drop rather than vanishing, so the gap between captured and delivered is never unexplained.

No CSI at All

Check the channel first. More first-run failures trace to this than to everything else combined. Every node in a capture set must agree on the primary channel, and the defaults across this ecosystem do not agree with each other: esp-csi-cli-rs defaults to channel 1, the esp-csi-rs emitter examples to 7, and esp-csi-litegui-rs to 1 while expecting an emitter that defaults to 7.

On a C5, check the band. The channel number selects it. The default is 149, which is 5 GHz; a 2.4 GHz AP is simply invisible from there and reports only “no access point found”.

Check that the emitter is actually radiating. An emitter reports no errors when its frames are accepted by the driver, which is not the same as them reaching the air. collector_sniffer reports CSI rate per source MAC, so point a collector at the channel and look for the emitter’s MAC specifically — that distinguishes “transmitting but unheard” from “not transmitting”. If the emitter’s MAC never appears, try the associated-link arrangement (softAP collector plus station) instead, which puts energy in the channel through the ordinary association path rather than through raw injection.

Check that ambient traffic exists. A sniffer on a quiet channel correctly reports nothing. Move to a channel your own network uses, or add an emitter.

Too Few Packets

If the rate is well below the emitter’s configured frame rate, work down this list:

  1. Console bandwidth. text mode emits twenty-odd lines per packet. Switch to array-list or serialized and re-measure before concluding anything else. This is the most common cause by a wide margin.
  2. Baud rate. Fixed at build time, which is why release artifacts publish it in manifest.json. A monitor at the wrong rate produces garbage, not a slow stream — but a low build-time baud produces exactly a slow stream.
  3. Logging queue overflow. Under async-print, the callback enqueues onto a 32-slot channel. get_log_packet_drops() counts what it could not enqueue.
  4. Filters. set-csi-filter --peer-mac and --min-phy=ht are doing their job; confirm they are filtering what you meant.
  5. Emitter period. --inject-period-ms=20 is 50 frames per second, not 500.

If throughput is the binding constraint, the ESP-NOW fast simplex pair sustains a markedly higher rate than any other arrangement, because the collector stops transmitting once it hears a source and all airtime belongs to one transmitter.

The Subcarrier Count Is Wrong

csi_data_len is the most informative single number in a capture, because it tells you which PHY actually produced the measurement:

SamplesWhat it means
~53Legacy 20 MHz, L-LTF only
~56HT20, HT-LTF
~117–128HT40

If you configured HT40 and see ~53, HT40 did not engage. In order of likelihood:

  1. Legacy and ACK acquisition are still on. The default CsiConfig acquires everything, and those reports arrive at ambient rates and swamp the HT40 ones. The symptom is distinctive: the count sits at ~53 and the CSI rate tracks ambient traffic rather than the emitter’s period. Fix with set-csi --csi-legacy=off --csi-ht20=off --csi-ht40=on --dump-ack=off, or emitter::phy::ht_csi_acquisition from firmware, which does the right thing on every chip.
  2. No room in the band. Ht40Above on channel 7 occupies up to channel 11; Ht40Below occupies down to channel 3. A primary too close to the edge silently falls back to 20 MHz.
  3. The collector’s receive path is still 20 MHz. Setting a secondary channel is not the same as widening the interface bandwidth. The library does both together; hand-rolled configuration often does not.

If the count varies packet to packet, nothing is wrong — you are capturing a mix of formats. Read data_format (the RxCSIFmt classification) per packet and handle each layout, or filter with --min-phy=ht so only HT frames arrive.

Rows That Look Like Someone Else’s

A collector is promiscuous. It reports CSI for every frame it decodes, including beacons, ACKs, association exchanges, and any third-party device on the channel. Those rows are valid CSI and they look wrong next to yours in two specific ways: the leading field is the frame’s own 802.11 sequence number, which is per-transmitter and per-TID and so neither starts at zero nor shares a counter with your traffic; and legacy-rate frames carry the shorter L-LTF-only payload.

set-csi-filter --peer-mac=<emitter MAC> is the precise fix, --min-phy=ht the looser one. Filtering on the device is also cheaper than filtering on the host, because a rejected frame is dropped in the Wi-Fi callback before the packet copy and before any formatting.

Host-Side Failures

412 Precondition Failed from csi-webserver means the firmware has not been verified as esp-csi-cli-rs. Call GET /api/devices/{id}/info, then POST /api/devices/{id}/control/reset if that does not clear it.

503 Service Unavailable means the device is not connected at all — check the cable and the port.

A Parquet file that will not open was almost certainly not stopped cleanly. The footer is written when the session ends; a crash, a power loss, or an unplugged board leaves the file without one and any unflushed rows lost. Unplugging a board to end a capture is the most common way to lose a dataset.

Device ids that change between runs mean you are relying on port basenames. Pin them with --device lab1=/dev/ttyUSB0, and key per-device state on the MAC from info rather than on the path — that is what the MAC is published for.

Decoded frames that are nonsense usually mean a schema mismatch. The serialized format carries no self-describing tag and there are two CSIDataPacket schemas, one for the classic parts and one for C5/C6. A decoder built for the wrong one produces plausible-looking garbage rather than an error.

Calibration

Diagnosis gets the capture working. Calibration makes it comparable.

Amplitude

Automatic gain control moves the entire CSI vector when the receiver changes gain, and that movement is indistinguishable from the environment changing unless you account for it. Three levers:

  • Per-packet normalisation. Divide by the vector norm, or by RSSI. Removes the AGC step at the cost of any genuine absolute-power information.
  • Manual scaling. manu_scale with shift on the classic parts, and val_scale_cfg (0–3, default 2) on the C5 and C6, fix the scaling instead of letting the radio choose. Fixed scaling makes packets comparable; it also means a strong signal can clip and a weak one can quantise badly. Worth it for controlled measurements, not for a survey.
  • A static baseline. Record the empty environment and normalise against it. This is the most effective calibration available for presence and motion, and the cheapest.

Phase

Everything in Signal Processing in Rust about CFO, SFO, and packet detection delay is a calibration problem. The practical sequence: unwrap along the subcarrier axis, fit and remove a linear trend per packet, and use the antenna ratio where multiple chains are available. Raw phase without this is not a measurement of the room.

Geometry

Node placement dominates results more than any software setting.

  • Keep the geometry fixed across a measurement campaign. Moving a node by a metre changes the channel response as much as the activity you are trying to detect.
  • Put the subject between the nodes. Line-of-sight obstruction produces far stronger signal than reflection alone.
  • Separate the nodes. Boards close together see a dominant direct path that swamps the environmental component.
  • Record the geometry with every dataset. A capture without positions is very difficult to reproduce or to compare against later.

Channel Selection

Prefer a quiet channel for controlled emitter/collector work: interference appears as variance you did not cause. In 2.4 GHz, channels 1, 6, and 11 are the non-overlapping set — pick the one your neighbours are not on. On a C5, the 5 GHz band is generally quieter and its wider channels give better delay resolution for impulse-response work.

Characterising, Not Just Fixing

When you need numbers rather than a working capture, the esp-csi-rs repository carries formal measurement specifications under specs/, with the firmware in experiments/. They are methodology documents — each defines what is measured, how, and what counts as a pass — and they are worth reading before designing your own comparison, because each one exists to avoid a specific unfair comparison:

  • packet_drop_rate_test_spec.md and its ESP-NOW variant. The key insight is that comparing a receive count against an assumed transmit rate is not a loss measurement — any pacing drift or transmitter stall misreports as loss. Carrying an explicit sequence number lets the receiver compute dropped / (received + dropped) from what was actually sent.
  • cpu_utilization_test_spec.md — CPU load as a function of offered traffic, separated from the cost of the CSI callback itself.
  • heap_usage_test_spec.md — memory, with rules that make Rust’s static-BSS layout comparable against ESP-IDF’s single DRAM pool.
  • power_consumption_test_spec.md — workload power as active − idle, so the always-on radio and the USB bridge cancel out.
  • binary_footprint_test_spec.md — flash footprint by role, decomposed by memory section and by crate.

The experiments/README.md in that repository is explicit that these are characterisation harnesses and not usage examples. Do not start from them.

Summary

In this section we looked at:

  • Reading the counters first, to separate a radio problem from a delivery problem.
  • Channel and band mismatches as the dominant first-run failure, and the conflicting defaults that cause them.
  • Using csi_data_len to tell which PHY actually produced a measurement, and what to do when HT40 does not engage.
  • The host-side failure modes: the firmware gate, unfinalised Parquet files, and schema mismatches in the serialized stream.
  • Calibrating amplitude, phase, and geometry so measurements stay comparable.
  • The measurement specifications available when you need numbers rather than a fix.