/** * @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. */ #include "csi_collector.h" #include "stream_sender.h" #include #include "esp_log.h" #include "esp_wifi.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; /** * 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) { int ret = stream_sender_send(frame_buf, frame_len); if (ret > 0) { s_send_ok++; } else { s_send_fail++; if (s_send_fail <= 5) { ESP_LOGW(TAG, "sendto failed (fail #%lu)", (unsigned long)s_send_fail); } } } } /** * 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); }