Note · evergreen
ESP32 CSI stores the imaginary part first
- Stage
- evergreen
- Links
- 1 out, 2 in
It took Aquila three decisions to get the CSI payload right, and the last one was a single sentence in Espressif’s documentation: “For each subcarrier, the imaginary part is stored first, followed by the real part.”
The first design had each node convert CSI to amplitude and phase as 32-bit floats before sending it. That contradicted the hardware. The radio produces signed 8-bit I/Q pairs, so the conversion put a square root and an arctangent per subcarrier on a microcontroller, made every sample 8 bytes where the source was 2, and ran through code that was never tested against a real capture.
The second design sent the raw bytes. The payload shrank to a quarter, and the limit on subcarriers per UDP datagram went from 178 to 714. Amplitude and phase became the core’s job, where they can be computed deterministically and tested.
The second design also assumed the pairs were real first. They are not. With the order swapped, every phase the core derived was the conjugate of the true one: atan2(Q, I) with the arguments exchanged. Amplitude is unaffected by that mistake, which is why nothing looked broken. Two decoders in the codebase disagreed about the order, and each had tests pinning its own behaviour.
The fix was to state the order once, imaginary first, in the wire format and derive both decoders from it.
What I took from it: send what the hardware produces and do the arithmetic where it can be tested, and when two pieces of code disagree and both have passing tests, the tests are recording an assumption that nobody checked against the source.
Projects
- Aquila Presence, occupancy and vital signs for Home Assistant, sensed from WiFi by ESP32-S3 nodes.
Projects
- Aquila Presence, occupancy and vital signs for Home Assistant, sensed from WiFi by ESP32-S3 nodes.