ESPectre SDK 2.8.0-280-gac7af68
Wi-Fi CSI motion sensing for ESP32 firmware
Loading...
Searching...
No Matches
csi_format.h
Go to the documentation of this file.
1/*
2 * ESPectre - CSI Format
3 *
4 * HT20 CSI layout constants, subcarrier band selection, and helpers that
5 * extract amplitudes and turbulence directly from raw CSI payloads.
6 * Keep aligned with src/python/micro_espectre/config.py and utils.py.
7 *
8 * Author: Francesco Pace <francesco.pace@gmail.com>
9 * SPDX-License-Identifier: GPL-3.0-only
10 * Commercial licensing available under separate agreement; see LICENSING.md.
11 */
12#pragma once
13
14#include <array>
15#include <cmath>
16#include <cstddef>
17#include <cstdint>
18#include <cstring>
19
20#include "csi_types.h"
21#include "utils.h"
22
23namespace espectre {
24
25// =============================================================================
26// HT20 Constants (64 subcarriers - do not change)
27// =============================================================================
28// Subcarriers +/-4, +/-9, +/-14, +/-19, +/-24, +/-28. Spans the full usable range
29// because the motion perturbation stays coherent over ~10 subcarriers (3.1 MHz)
30// while quiet noise is nearly per-tone independent, so span is what buys
31// independent looks. Stops short of |sc| <= 3, where relative jitter rises ~10%.
32// See docs/adr/2026-07-25-select-the-classic-band-from-channel-coherence.md.
33// =============================================================================
34// HT20 Bin Layout
35// =============================================================================
36// Wi-Fi 6 parts deliver HT20 CSI centered on DC (bin = subcarrier + 32), while
37// classic-MAC parts deliver Espressif's native "0~31, -32~-1" order with DC in
38// bin 0. `DEFAULT_SUBCARRIERS` and `HT20_DC_SUBCARRIER` assume the centered
39// convention, so classic payloads must be rotated before the band means the same
40// physical subcarriers on every chip.
41//
42// The two layouts are told apart by their guard nulls, which the radio reports as
43// exactly zero. Bins 0 and 32 are null under both conventions and carry no
44// information; these are the bins that are null under exactly one of them.
45constexpr uint8_t HT20_CLASSIC_ONLY_NULL_BINS[] = {29, 30, 31, 33, 34, 35};
46constexpr uint8_t HT20_CENTERED_ONLY_NULL_BINS[] = {1, 2, 3, 61, 62, 63};
47
48enum class Ht20BinLayout : uint8_t {
50 CENTERED, // bin = subcarrier + 32, DC at bin 32
51 CLASSIC, // bin = subcarrier mod 64, DC at bin 0
52};
53
54inline uint8_t ht20_bins_with_energy(const int8_t* csi_data, const uint8_t* bins, uint8_t count) {
55 uint8_t populated = 0;
56 for (uint8_t i = 0; i < count; ++i) {
57 const uint16_t byte_index = static_cast<uint16_t>(bins[i]) * 2U;
58 if (csi_data[byte_index] != 0 || csi_data[byte_index + 1] != 0) {
59 populated++;
60 }
61 }
62 return populated;
63}
64
65/**
66 * Identify which HT20 bin ordering a 64-subcarrier payload uses.
67 *
68 * Requires positive evidence in both directions: one guard set must be entirely
69 * null and the other entirely populated. Absence of energy alone is not enough,
70 * because a sparse or degenerate payload is null under both conventions.
71 *
72 * @param csi_data Raw CSI payload (interleaved I/Q pairs)
73 * @param csi_len Payload length in bytes (must be HT20_CSI_LEN)
74 * @return The detected layout, or UNKNOWN when the evidence is inconclusive
75 */
76inline Ht20BinLayout detect_ht20_bin_layout(const int8_t* csi_data, size_t csi_len) {
77 if (csi_data == nullptr || csi_len != HT20_CSI_LEN) {
79 }
80
81 constexpr uint8_t kNullBinCount =
82 static_cast<uint8_t>(sizeof(HT20_CLASSIC_ONLY_NULL_BINS));
83 const uint8_t classic_energy =
84 ht20_bins_with_energy(csi_data, HT20_CLASSIC_ONLY_NULL_BINS, kNullBinCount);
85 const uint8_t centered_energy =
87
88 if (classic_energy == 0U && centered_energy == kNullBinCount) {
90 }
91 if (centered_energy == 0U && classic_energy == kNullBinCount) {
93 }
95}
96
97/**
98 * Rotate a classic-order HT20 payload into the centered convention.
99 *
100 * Rotating by half the FFT size is its own inverse, so one swap of the payload
101 * halves maps `0~31, -32~-1` onto `-32~+31`.
102 *
103 * @param csi_data Source payload of HT20_CSI_LEN bytes
104 * @param out Destination buffer of HT20_CSI_LEN bytes (must not alias the source)
105 */
106inline void rotate_ht20_classic_to_centered(const int8_t* csi_data, int8_t* out) {
107 if (csi_data == nullptr || out == nullptr) {
108 return;
109 }
110 constexpr uint16_t kHalf = HT20_CSI_LEN / 2U;
111 std::memcpy(out, csi_data + kHalf, kHalf);
112 std::memcpy(out + kHalf, csi_data, kHalf);
113}
114
115/**
116 * Calculate spatial turbulence from pre-calculated magnitudes
117 *
118 * Spatial turbulence is the standard deviation of magnitudes across
119 * selected subcarriers. It measures the spatial variability of the
120 * Wi-Fi channel - higher values indicate motion/disturbance.
121 *
122 * @param magnitudes Array of magnitude values (one per subcarrier)
123 * @param subcarriers Array of selected subcarrier indices
124 * @param num_subcarriers Number of selected subcarriers (max 12)
125 * @param max_subcarrier Maximum valid subcarrier index (default: 64 for HT20)
126 * @return Turbulence value
127 */
128inline float calculate_spatial_turbulence(const float* magnitudes,
129 const uint8_t* subcarriers,
130 uint8_t num_subcarriers,
131 uint16_t max_subcarrier = 64) {
132 if (num_subcarriers == 0 || !magnitudes || !subcarriers) {
133 return 0.0f;
134 }
135
136 // Collect valid magnitudes (max 12 subcarriers for band selection)
137 float valid_mags[12];
138 uint8_t valid_count = 0;
139
140 for (uint8_t i = 0; i < num_subcarriers && valid_count < 12; i++) {
141 if (subcarriers[i] < max_subcarrier) {
142 valid_mags[valid_count++] = magnitudes[subcarriers[i]];
143 }
144 }
145
146 if (valid_count == 0) {
147 return 0.0f;
148 }
149
150 const MeanVariance stats = calculate_mean_variance_two_pass(valid_mags, valid_count);
151 return apply_cv_normalization(std::sqrt(stats.variance), stats.mean);
152}
153
154/**
155 * Extract subcarrier amplitudes from raw CSI data (I/Q pairs)
156 *
157 * Mirrors the Python `SegmentationContext._fill_amplitude_buffer` helper.
158 *
159 * @param csi_data Raw CSI data (interleaved I/Q pairs, Espressif format)
160 * @param csi_len Length of CSI data in bytes
161 * @param subcarriers Array of selected subcarrier indices
162 * @param num_subcarriers Number of selected subcarriers
163 * @param out Output amplitude buffer
164 * @param out_capacity Capacity of the output buffer
165 * @return Number of amplitudes written
166 */
167inline uint8_t extract_subcarrier_amplitudes(const int8_t* csi_data,
168 size_t csi_len,
169 const uint8_t* subcarriers,
170 uint8_t num_subcarriers,
171 float* out,
172 uint8_t out_capacity) {
173 if (!csi_data || csi_len < 2 || num_subcarriers == 0 || !subcarriers || !out) {
174 return 0;
175 }
176
177 int total_subcarriers = static_cast<int>(csi_len / 2);
178 uint8_t valid_count = 0;
179
180 for (int i = 0; i < num_subcarriers && valid_count < out_capacity; i++) {
181 int sc_idx = subcarriers[i];
182 if (sc_idx >= total_subcarriers) {
183 continue;
184 }
185
186 // Espressif CSI format: [Imaginary, Real, ...] per subcarrier
187 out[valid_count++] = calculate_magnitude(csi_data[sc_idx * 2 + 1],
188 csi_data[sc_idx * 2]);
189 }
190 return valid_count;
191}
192
193/** Extract one packet-wide amplitude frame for reuse by multiple feature paths. */
194inline uint8_t extract_packet_subcarrier_amplitudes(const int8_t* csi_data,
195 size_t csi_len,
196 float* out,
197 uint8_t out_capacity) {
198 if (csi_data == nullptr || out == nullptr) return 0U;
199 const uint8_t count = static_cast<uint8_t>(std::min<size_t>(
200 HT20_NUM_SUBCARRIERS, std::min<size_t>(out_capacity, csi_len / 2U)));
201 for (uint8_t i = 0U; i < count; ++i) {
202 out[i] = calculate_magnitude(csi_data[i * 2U + 1U], csi_data[i * 2U]);
203 }
204 return count;
205}
206
207/** Fill one packet-wide squared-magnitude frame for energy-domain consumers. */
208inline uint8_t fill_packet_subcarrier_energies(const int8_t* csi_data,
209 size_t csi_len,
210 float* out,
211 uint8_t out_capacity) {
212 if (csi_data == nullptr || out == nullptr) return 0U;
213 const uint8_t count = static_cast<uint8_t>(std::min<size_t>(
214 HT20_NUM_SUBCARRIERS, std::min<size_t>(out_capacity, csi_len / 2U)));
215 for (uint8_t i = 0U; i < count; ++i) {
216 const float imag = static_cast<float>(csi_data[i * 2U]);
217 const float real = static_cast<float>(csi_data[i * 2U + 1U]);
218 out[i] = real * real + imag * imag;
219 }
220 return count;
221}
222
223inline void energies_to_amplitudes_in_place(float* values, uint8_t count) {
224 if (values == nullptr) return;
225 for (uint8_t i = 0U; i < count; ++i) values[i] = std::sqrt(values[i]);
226}
227
228/** Select the configured tones from a packet-wide amplitude frame. */
229inline uint8_t select_subcarrier_amplitudes(const float* packet_amplitudes,
230 uint8_t packet_count,
231 const uint8_t* subcarriers,
232 uint8_t num_subcarriers,
233 float* out,
234 uint8_t out_capacity) {
235 if (packet_amplitudes == nullptr || subcarriers == nullptr || out == nullptr) return 0U;
236 uint8_t written = 0U;
237 for (uint8_t i = 0U; i < num_subcarriers && written < out_capacity; ++i) {
238 if (subcarriers[i] < packet_count) out[written++] = packet_amplitudes[subcarriers[i]];
239 }
240 return written;
241}
242
243/** Select adjacent-bin mean amplitudes from a packet-wide amplitude frame. */
245 const float* packet_amplitudes, uint8_t packet_count,
246 const uint8_t* subcarriers, uint8_t num_subcarriers, uint8_t width,
247 float* out, uint8_t out_capacity) {
248 if (packet_amplitudes == nullptr || subcarriers == nullptr || width == 0U || out == nullptr) return 0U;
249 const int half = static_cast<int>((width - 1U) / 2U);
250 uint8_t written = 0U;
251 for (uint8_t i = 0U; i < num_subcarriers && written < out_capacity; ++i) {
252 int low = static_cast<int>(subcarriers[i]) - half;
253 int high = static_cast<int>(subcarriers[i]) + static_cast<int>(width - 1U) - half;
254 if (low < HT20_GUARD_BAND_LOW) {
256 high = HT20_GUARD_BAND_LOW + width - 1U;
257 }
258 if (high > HT20_GUARD_BAND_HIGH) {
259 low = HT20_GUARD_BAND_HIGH - width + 1U;
261 }
262 float total = 0.0f;
263 uint8_t count = 0U;
264 for (int bin = low; bin <= high; ++bin) {
265 if (bin == HT20_DC_SUBCARRIER || bin < 0 || bin >= packet_count) continue;
266 total += packet_amplitudes[bin];
267 ++count;
268 }
269 if (count > 0U) out[written++] = total / static_cast<float>(count);
270 }
271 return written;
272}
273
274/**
275 * Extract one mean magnitude per selected tone from adjacent live HT20 bins.
276 *
277 * Windows are clamped to bins 4..60 and skip the DC null at bin 32. This is
278 * the production counterpart of
279 * `SegmentationContext._fill_adjacent_aggregated_amplitude_buffer`.
280 */
282 const int8_t* csi_data,
283 size_t csi_len,
284 const uint8_t* subcarriers,
285 uint8_t num_subcarriers,
286 uint8_t width,
287 float* out,
288 uint8_t out_capacity) {
289 if (csi_data == nullptr || csi_len < 2U || subcarriers == nullptr ||
290 num_subcarriers == 0U || width == 0U || out == nullptr) {
291 return 0U;
292 }
293
294 const int total_subcarriers = static_cast<int>(csi_len / 2U);
295 const int half = static_cast<int>((width - 1U) / 2U);
296 uint8_t written = 0U;
297 for (uint8_t i = 0U; i < num_subcarriers && written < out_capacity; ++i) {
298 int low = static_cast<int>(subcarriers[i]) - half;
299 int high = static_cast<int>(subcarriers[i]) +
300 static_cast<int>(width - 1U) - half;
301 if (low < HT20_GUARD_BAND_LOW) {
303 high = HT20_GUARD_BAND_LOW + width - 1U;
304 }
305 if (high > HT20_GUARD_BAND_HIGH) {
306 low = HT20_GUARD_BAND_HIGH - width + 1U;
308 }
309
310 float magnitude_sum = 0.0f;
311 uint8_t count = 0U;
312 for (int bin = low; bin <= high; ++bin) {
313 if (bin == HT20_DC_SUBCARRIER || bin < 0 ||
314 bin >= total_subcarriers) {
315 continue;
316 }
317 magnitude_sum += calculate_magnitude(
318 csi_data[bin * 2 + 1], csi_data[bin * 2]);
319 ++count;
320 }
321 if (count > 0U) {
322 out[written++] = magnitude_sum / static_cast<float>(count);
323 }
324 }
325 return written;
326}
327
328inline float calculate_spatial_turbulence_from_amplitudes(const float* amplitudes,
329 uint8_t count) {
330 if (amplitudes == nullptr || count == 0) {
331 return 0.0f;
332 }
333 const MeanVariance stats = calculate_mean_variance_two_pass(amplitudes, count);
334 return apply_cv_normalization(std::sqrt(stats.variance), stats.mean);
335}
336
337/**
338 * Calculate spatial turbulence directly from raw CSI data (I/Q pairs)
339 *
340 * This is a convenience wrapper that calculates magnitudes internally
341 * before computing spatial turbulence.
342 *
343 * HT20 only: 64 subcarriers, 128 bytes CSI data.
344 *
345 * @param csi_data Raw CSI data (interleaved I/Q pairs)
346 * @param csi_len Length of CSI data in bytes (expected: 128 for HT20)
347 * @param subcarriers Array of selected subcarrier indices
348 * @param num_subcarriers Number of selected subcarriers (max 12)
349 * @return Turbulence value
350 */
351inline float calculate_spatial_turbulence_from_csi(const int8_t* csi_data,
352 size_t csi_len,
353 const uint8_t* subcarriers,
354 uint8_t num_subcarriers) {
355 float amplitudes[HT20_SELECTED_BAND_SIZE];
356 uint8_t valid_count = extract_subcarrier_amplitudes(
357 csi_data, csi_len, subcarriers, num_subcarriers,
358 amplitudes, HT20_SELECTED_BAND_SIZE);
359 return calculate_spatial_turbulence_from_amplitudes(amplitudes, valid_count);
360}
361
362} // namespace espectre
uint8_t select_adjacent_aggregated_subcarrier_amplitudes(const float *packet_amplitudes, uint8_t packet_count, const uint8_t *subcarriers, uint8_t num_subcarriers, uint8_t width, float *out, uint8_t out_capacity)
Select adjacent-bin mean amplitudes from a packet-wide amplitude frame.
Definition csi_format.h:244
constexpr uint16_t HT20_NUM_SUBCARRIERS
Definition csi_types.h:18
uint8_t ht20_bins_with_energy(const int8_t *csi_data, const uint8_t *bins, uint8_t count)
Definition csi_format.h:54
float calculate_spatial_turbulence(const float *magnitudes, const uint8_t *subcarriers, uint8_t num_subcarriers, uint16_t max_subcarrier=64)
Calculate spatial turbulence from pre-calculated magnitudes.
Definition csi_format.h:128
float calculate_spatial_turbulence_from_csi(const int8_t *csi_data, size_t csi_len, const uint8_t *subcarriers, uint8_t num_subcarriers)
Calculate spatial turbulence directly from raw CSI data (I/Q pairs).
Definition csi_format.h:351
MeanVariance calculate_mean_variance_two_pass(const float *values, size_t n)
Calculate mean and variance in one two-pass sweep (numerically stable).
Definition utils.h:108
constexpr uint8_t HT20_CENTERED_ONLY_NULL_BINS[]
Definition csi_format.h:46
float calculate_spatial_turbulence_from_amplitudes(const float *amplitudes, uint8_t count)
Definition csi_format.h:328
uint8_t fill_packet_subcarrier_energies(const int8_t *csi_data, size_t csi_len, float *out, uint8_t out_capacity)
Fill one packet-wide squared-magnitude frame for energy-domain consumers.
Definition csi_format.h:208
constexpr uint8_t HT20_SELECTED_BAND_SIZE
Definition csi_types.h:27
constexpr uint8_t HT20_CLASSIC_ONLY_NULL_BINS[]
Definition csi_format.h:45
uint8_t select_subcarrier_amplitudes(const float *packet_amplitudes, uint8_t packet_count, const uint8_t *subcarriers, uint8_t num_subcarriers, float *out, uint8_t out_capacity)
Select the configured tones from a packet-wide amplitude frame.
Definition csi_format.h:229
void rotate_ht20_classic_to_centered(const int8_t *csi_data, int8_t *out)
Rotate a classic-order HT20 payload into the centered convention.
Definition csi_format.h:106
void energies_to_amplitudes_in_place(float *values, uint8_t count)
Definition csi_format.h:223
float calculate_magnitude(int8_t i, int8_t q)
Calculate magnitude (amplitude) from I/Q components.
Definition utils.h:152
uint8_t extract_adjacent_aggregated_subcarrier_amplitudes(const int8_t *csi_data, size_t csi_len, const uint8_t *subcarriers, uint8_t num_subcarriers, uint8_t width, float *out, uint8_t out_capacity)
Extract one mean magnitude per selected tone from adjacent live HT20 bins.
Definition csi_format.h:281
constexpr uint8_t HT20_GUARD_BAND_HIGH
Definition csi_types.h:25
uint8_t extract_packet_subcarrier_amplitudes(const int8_t *csi_data, size_t csi_len, float *out, uint8_t out_capacity)
Extract one packet-wide amplitude frame for reuse by multiple feature paths.
Definition csi_format.h:194
constexpr uint8_t HT20_GUARD_BAND_LOW
Definition csi_types.h:24
constexpr uint16_t HT20_CSI_LEN
Definition csi_types.h:19
uint8_t extract_subcarrier_amplitudes(const int8_t *csi_data, size_t csi_len, const uint8_t *subcarriers, uint8_t num_subcarriers, float *out, uint8_t out_capacity)
Extract subcarrier amplitudes from raw CSI data (I/Q pairs).
Definition csi_format.h:167
float apply_cv_normalization(float std_dev, float mean)
Apply gain-invariant normalization to standard deviation.
Definition utils.h:66
constexpr uint8_t HT20_DC_SUBCARRIER
Definition csi_types.h:26
Ht20BinLayout detect_ht20_bin_layout(const int8_t *csi_data, size_t csi_len)
Identify which HT20 bin ordering a 64-subcarrier payload uses.
Definition csi_format.h:76