forked from ruvnet/RuView
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcsi_collector.c
More file actions
368 lines (307 loc) · 11.4 KB
/
csi_collector.c
File metadata and controls
368 lines (307 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
/**
* @file csi_collector.c
* @brief CSI data collection and ADR-018 binary frame serialization.
*
* Registers the ESP-IDF WiFi CSI callback and serializes incoming CSI data
* into the ADR-018 binary frame format for UDP transmission.
*
* ADR-029 extensions:
* - Channel-hop table for multi-band sensing (channels 1/6/11 by default)
* - Timer-driven channel hopping at configurable dwell intervals
* - NDP frame injection stub for sensing-first TX
*/
#include "csi_collector.h"
#include "stream_sender.h"
#include "edge_processing.h"
#include <string.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "sdkconfig.h"
static const char *TAG = "csi_collector";
static uint32_t s_sequence = 0;
static uint32_t s_cb_count = 0;
static uint32_t s_send_ok = 0;
static uint32_t s_send_fail = 0;
static uint32_t s_rate_skip = 0;
/**
* Minimum interval between UDP sends in microseconds.
* CSI callbacks can fire hundreds of times per second in promiscuous mode.
* We cap the send rate to avoid exhausting lwIP packet buffers (ENOMEM).
* Default: 20 ms = 50 Hz max send rate.
*/
#define CSI_MIN_SEND_INTERVAL_US (20 * 1000)
static int64_t s_last_send_us = 0;
/* ---- ADR-029: Channel-hop state ---- */
/** Channel hop table (populated from NVS at boot or via set_hop_table). */
static uint8_t s_hop_channels[CSI_HOP_CHANNELS_MAX] = {1, 6, 11, 36, 40, 44};
/** Number of active channels in the hop table. 1 = single-channel (no hop). */
static uint8_t s_hop_count = 1;
/** Dwell time per channel in milliseconds. */
static uint32_t s_dwell_ms = 50;
/** Current index into s_hop_channels. */
static uint8_t s_hop_index = 0;
/** Handle for the periodic hop timer. NULL when timer is not running. */
static esp_timer_handle_t s_hop_timer = NULL;
/**
* Serialize CSI data into ADR-018 binary frame format.
*
* Layout:
* [0..3] Magic: 0xC5110001 (LE)
* [4] Node ID
* [5] Number of antennas (rx_ctrl.rx_ant + 1 if available, else 1)
* [6..7] Number of subcarriers (LE u16) = len / (2 * n_antennas)
* [8..11] Frequency MHz (LE u32) — derived from channel
* [12..15] Sequence number (LE u32)
* [16] RSSI (i8)
* [17] Noise floor (i8)
* [18..19] Reserved
* [20..] I/Q data (raw bytes from ESP-IDF callback)
*/
size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf_len)
{
if (info == NULL || buf == NULL || info->buf == NULL) {
return 0;
}
uint8_t n_antennas = 1; /* ESP32-S3 typically reports 1 antenna for CSI */
uint16_t iq_len = (uint16_t)info->len;
uint16_t n_subcarriers = iq_len / (2 * n_antennas);
size_t frame_size = CSI_HEADER_SIZE + iq_len;
if (frame_size > buf_len) {
ESP_LOGW(TAG, "Buffer too small: need %u, have %u", (unsigned)frame_size, (unsigned)buf_len);
return 0;
}
/* Derive frequency from channel number */
uint8_t channel = info->rx_ctrl.channel;
uint32_t freq_mhz;
if (channel >= 1 && channel <= 13) {
freq_mhz = 2412 + (channel - 1) * 5;
} else if (channel == 14) {
freq_mhz = 2484;
} else if (channel >= 36 && channel <= 177) {
freq_mhz = 5000 + channel * 5;
} else {
freq_mhz = 0;
}
/* Magic (LE) */
uint32_t magic = CSI_MAGIC;
memcpy(&buf[0], &magic, 4);
/* Node ID */
buf[4] = (uint8_t)CONFIG_CSI_NODE_ID;
/* Number of antennas */
buf[5] = n_antennas;
/* Number of subcarriers (LE u16) */
memcpy(&buf[6], &n_subcarriers, 2);
/* Frequency MHz (LE u32) */
memcpy(&buf[8], &freq_mhz, 4);
/* Sequence number (LE u32) */
uint32_t seq = s_sequence++;
memcpy(&buf[12], &seq, 4);
/* RSSI (i8) */
buf[16] = (uint8_t)(int8_t)info->rx_ctrl.rssi;
/* Noise floor (i8) */
buf[17] = (uint8_t)(int8_t)info->rx_ctrl.noise_floor;
/* Reserved */
buf[18] = 0;
buf[19] = 0;
/* I/Q data */
memcpy(&buf[CSI_HEADER_SIZE], info->buf, iq_len);
return frame_size;
}
/**
* WiFi CSI callback — invoked by ESP-IDF when CSI data is available.
*/
static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
{
(void)ctx;
s_cb_count++;
if (s_cb_count <= 3 || (s_cb_count % 100) == 0) {
ESP_LOGI(TAG, "CSI cb #%lu: len=%d rssi=%d ch=%d",
(unsigned long)s_cb_count, info->len,
info->rx_ctrl.rssi, info->rx_ctrl.channel);
}
uint8_t frame_buf[CSI_MAX_FRAME_SIZE];
size_t frame_len = csi_serialize_frame(info, frame_buf, sizeof(frame_buf));
if (frame_len > 0) {
/* Rate-limit UDP sends to avoid ENOMEM from lwIP pbuf exhaustion.
* In promiscuous mode, CSI callbacks can fire 100-500+ times/sec.
* We only need 20-50 Hz for the sensing pipeline. */
int64_t now = esp_timer_get_time();
if ((now - s_last_send_us) >= CSI_MIN_SEND_INTERVAL_US) {
int ret = stream_sender_send(frame_buf, frame_len);
if (ret > 0) {
s_send_ok++;
s_last_send_us = now;
} else {
s_send_fail++;
if (s_send_fail <= 5) {
ESP_LOGW(TAG, "sendto failed (fail #%lu)", (unsigned long)s_send_fail);
}
}
} else {
s_rate_skip++;
}
}
/* ADR-039: Enqueue raw I/Q into edge processing ring buffer. */
if (info->buf && info->len > 0) {
edge_enqueue_csi((const uint8_t *)info->buf, (uint16_t)info->len,
(int8_t)info->rx_ctrl.rssi, info->rx_ctrl.channel);
}
}
/**
* Promiscuous mode callback — required for CSI to fire on all received frames.
* We don't need the packet content, just the CSI triggered by reception.
*/
static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
{
/* No-op: CSI callback is registered separately and fires in parallel. */
(void)buf;
(void)type;
}
void csi_collector_init(void)
{
/* Enable promiscuous mode — required for reliable CSI callbacks.
* Without this, CSI only fires on frames destined to this station,
* which may be very infrequent on a quiet network. */
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(wifi_promiscuous_cb));
wifi_promiscuous_filter_t filt = {
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA,
};
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filt));
ESP_LOGI(TAG, "Promiscuous mode enabled for CSI capture");
wifi_csi_config_t csi_config = {
.lltf_en = true,
.htltf_en = true,
.stbc_htltf2_en = true,
.ltf_merge_en = true,
.channel_filter_en = false,
.manu_scale = false,
.shift = false,
};
ESP_ERROR_CHECK(esp_wifi_set_csi_config(&csi_config));
ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_callback, NULL));
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%d)",
CONFIG_CSI_NODE_ID, CONFIG_CSI_WIFI_CHANNEL);
}
/* ---- ADR-029: Channel hopping ---- */
void csi_collector_set_hop_table(const uint8_t *channels, uint8_t hop_count, uint32_t dwell_ms)
{
if (channels == NULL) {
ESP_LOGW(TAG, "csi_collector_set_hop_table: channels is NULL");
return;
}
if (hop_count == 0 || hop_count > CSI_HOP_CHANNELS_MAX) {
ESP_LOGW(TAG, "csi_collector_set_hop_table: invalid hop_count=%u (max=%u)",
(unsigned)hop_count, (unsigned)CSI_HOP_CHANNELS_MAX);
return;
}
if (dwell_ms < 10) {
ESP_LOGW(TAG, "csi_collector_set_hop_table: dwell_ms=%lu too small, clamping to 10",
(unsigned long)dwell_ms);
dwell_ms = 10;
}
memcpy(s_hop_channels, channels, hop_count);
s_hop_count = hop_count;
s_dwell_ms = dwell_ms;
s_hop_index = 0;
ESP_LOGI(TAG, "Hop table set: %u channels, dwell=%lu ms", (unsigned)hop_count,
(unsigned long)dwell_ms);
for (uint8_t i = 0; i < hop_count; i++) {
ESP_LOGI(TAG, " hop[%u] = channel %u", (unsigned)i, (unsigned)channels[i]);
}
}
void csi_hop_next_channel(void)
{
if (s_hop_count <= 1) {
/* Single-channel mode: no-op for backward compatibility. */
return;
}
s_hop_index = (s_hop_index + 1) % s_hop_count;
uint8_t channel = s_hop_channels[s_hop_index];
/*
* esp_wifi_set_channel() changes the primary channel.
* The second parameter is the secondary channel offset for HT40;
* we use HT20 (no secondary) for sensing.
*/
esp_err_t err = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Channel hop to %u failed: %s", (unsigned)channel, esp_err_to_name(err));
} else if ((s_cb_count % 200) == 0) {
/* Periodic log to confirm hopping is working (not every hop). */
ESP_LOGI(TAG, "Hopped to channel %u (index %u/%u)",
(unsigned)channel, (unsigned)s_hop_index, (unsigned)s_hop_count);
}
}
/**
* Timer callback for channel hopping.
* Called every s_dwell_ms milliseconds from the esp_timer context.
*/
static void hop_timer_cb(void *arg)
{
(void)arg;
csi_hop_next_channel();
}
void csi_collector_start_hop_timer(void)
{
if (s_hop_count <= 1) {
ESP_LOGI(TAG, "Single-channel mode: hop timer not started");
return;
}
if (s_hop_timer != NULL) {
ESP_LOGW(TAG, "Hop timer already running");
return;
}
esp_timer_create_args_t timer_args = {
.callback = hop_timer_cb,
.arg = NULL,
.name = "csi_hop",
};
esp_err_t err = esp_timer_create(&timer_args, &s_hop_timer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to create hop timer: %s", esp_err_to_name(err));
return;
}
uint64_t period_us = (uint64_t)s_dwell_ms * 1000;
err = esp_timer_start_periodic(s_hop_timer, period_us);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start hop timer: %s", esp_err_to_name(err));
esp_timer_delete(s_hop_timer);
s_hop_timer = NULL;
return;
}
ESP_LOGI(TAG, "Hop timer started: period=%lu ms, channels=%u",
(unsigned long)s_dwell_ms, (unsigned)s_hop_count);
}
/* ---- ADR-029: NDP frame injection stub ---- */
esp_err_t csi_inject_ndp_frame(void)
{
/*
* TODO: Construct a proper 802.11 Null Data Packet frame.
*
* A real NDP is preamble-only (~24 us airtime, no payload) and is the
* sensing-first TX mechanism described in ADR-029. For now we send a
* minimal null-data frame as a placeholder so the API is wired up.
*
* Frame structure (IEEE 802.11 Null Data):
* FC (2) | Duration (2) | Addr1 (6) | Addr2 (6) | Addr3 (6) | SeqCtl (2)
* = 24 bytes total, no body, no FCS (hardware appends FCS).
*/
uint8_t ndp_frame[24];
memset(ndp_frame, 0, sizeof(ndp_frame));
/* Frame Control: Type=Data (0x02), Subtype=Null (0x04) -> 0x0048 */
ndp_frame[0] = 0x48;
ndp_frame[1] = 0x00;
/* Duration: 0 (let hardware fill) */
/* Addr1 (destination): broadcast */
memset(&ndp_frame[4], 0xFF, 6);
/* Addr2 (source): will be overwritten by hardware with own MAC */
/* Addr3 (BSSID): broadcast */
memset(&ndp_frame[16], 0xFF, 6);
esp_err_t err = esp_wifi_80211_tx(WIFI_IF_STA, ndp_frame, sizeof(ndp_frame), false);
if (err != ESP_OK) {
ESP_LOGW(TAG, "NDP inject failed: %s", esp_err_to_name(err));
}
return err;
}