spaxel/firmware/main/csi.c
jedarden d7c1adc260 Fix ESP-IDF 5.x firmware compilation for ESP32-S3 build
Port firmware/main to ESP-IDF 5.2 API:
- Add idf_component.yml with mdns and esp_websocket_client managed deps
- Rename esp_ota → app_update, esp_wifi_csi_info_t → wifi_csi_info_t
- Fix freertos/semphr.h include path rename
- Add missing headers: esp_mac.h, esp_netif.h, driver/temperature_sensor.h, string.h, math.h
- Add led.c to SRCS, fix xTaskCreate arg count (7→6)
- Use IDF 5.x temperature_sensor API in place of stub
- Fix mdns_query_ptr signature (add max_results arg)
- Fix url_decode isxdigit unsigned char cast
- Add flash size config (16MB) to sdkconfig.defaults
- Pin managed component versions in dependencies.lock
- Add sdkconfig (generated) to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 15:35:03 -04:00

306 lines
8.6 KiB
C

#include "csi.h"
#include "spaxel.h"
#include "websocket.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include <string.h>
#include <math.h>
static const char *TAG = "csi";
// Global variance threshold for motion hints
float g_variance_threshold = 0.02f;
// CSI statistics
static csi_stats_t s_stats = {0};
// TX task handle
static TaskHandle_t s_tx_task = NULL;
static volatile bool s_tx_running = false;
// CSI data queue
static QueueHandle_t s_csi_queue = NULL;
// Amplitude history for on-device variance check
#define VARIANCE_WINDOW 100
static float s_amplitude_history[VARIANCE_WINDOW] = {0};
static int s_amplitude_idx = 0;
static int s_amplitude_count = 0;
// Passive mode BSSID filter
static uint8_t s_passive_bssid[6] = {0};
static bool s_passive_filter_enabled = false;
// Forward declarations
static void csi_rx_task(void *arg);
static void csi_tx_task(void *arg);
static float compute_amplitude_variance(float new_amp);
static void wifi_csi_cb(void *ctx, wifi_csi_info_t *info);
esp_err_t csi_init(void) {
// Create CSI queue
s_csi_queue = xQueueCreate(SPAXEL_CSI_QUEUE_SIZE, sizeof(wifi_csi_info_t *));
if (!s_csi_queue) {
ESP_LOGE(TAG, "Failed to create CSI queue");
return ESP_ERR_NO_MEM;
}
// Configure CSI
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 = 0,
};
esp_err_t err = esp_wifi_set_csi_config(&csi_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set CSI config: %s", esp_err_to_name(err));
return err;
}
// Register CSI callback
err = esp_wifi_set_csi_rx_cb(wifi_csi_cb, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register CSI callback: %s", esp_err_to_name(err));
return err;
}
// Enable CSI
err = esp_wifi_set_csi(true);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to enable CSI: %s", esp_err_to_name(err));
return err;
}
// Start RX task
xTaskCreatePinnedToCore(csi_rx_task, "csi_rx", 8192, NULL, 5, NULL, 1);
ESP_LOGI(TAG, "CSI initialized");
return ESP_OK;
}
static void wifi_csi_cb(void *ctx, wifi_csi_info_t *info) {
s_stats.frames_received++;
// Apply passive BSSID filter if enabled
if (s_passive_filter_enabled) {
if (memcmp(info->mac, s_passive_bssid, 6) != 0) {
// Not from the AP we're tracking, skip
s_stats.frames_dropped++;
return;
}
}
// Copy CSI info to queue (pointer to heap-allocated copy)
wifi_csi_info_t *copy = malloc(sizeof(wifi_csi_info_t));
if (copy) {
memcpy(copy, info, sizeof(wifi_csi_info_t));
if (xQueueSend(s_csi_queue, &copy, 0) != pdPASS) {
free(copy);
s_stats.frames_dropped++;
}
} else {
s_stats.frames_dropped++;
}
}
static void csi_rx_task(void *arg) {
wifi_csi_info_t *info;
while (1) {
if (xQueueReceive(s_csi_queue, &info, portMAX_DELAY) == pdPASS) {
// Process CSI frame
int8_t *csi_data = info->buf;
uint8_t n_sub = info->len / 2; // I/Q pairs
if (n_sub > SPAXEL_CSI_MAX_SUBCARRIERS) {
n_sub = SPAXEL_CSI_MAX_SUBCARRIERS;
}
// Compute average amplitude for variance tracking
float amp_sum = 0;
for (int i = 0; i < n_sub; i++) {
int8_t i_val = csi_data[i * 2];
int8_t q_val = csi_data[i * 2 + 1];
amp_sum += sqrtf((float)i_val * i_val + (float)q_val * q_val);
}
float avg_amp = amp_sum / n_sub;
float variance = compute_amplitude_variance(avg_amp);
// Check for motion hint (if variance exceeds threshold at low rate)
if (variance > g_variance_threshold && g_state.packet_rate < 20) {
ESP_LOGD(TAG, "On-device motion hint: variance=%.4f", variance);
websocket_send_motion_hint(variance);
}
// Send to mothership if connected
if (websocket_is_connected()) {
uint64_t timestamp = (uint64_t)esp_timer_get_time();
// Determine peer MAC
uint8_t peer_mac[6];
if (g_state.role == NODE_ROLE_TX) {
// In TX mode, peer is ourselves
memcpy(peer_mac, g_state.mac, 6);
} else {
// In RX/TX_RX mode, peer is the transmitter
memcpy(peer_mac, info->mac, 6);
}
esp_err_t err = websocket_send_csi(
peer_mac,
timestamp,
info->rx_ctrl.rssi,
info->rx_ctrl.noise_floor,
info->rx_ctrl.channel,
csi_data,
n_sub
);
if (err == ESP_OK) {
s_stats.frames_sent++;
} else {
s_stats.frames_dropped++;
}
}
free(info);
}
}
}
static float compute_amplitude_variance(float new_amp) {
// Welford's online algorithm for variance
s_amplitude_history[s_amplitude_idx] = new_amp;
s_amplitude_idx = (s_amplitude_idx + 1) % VARIANCE_WINDOW;
if (s_amplitude_count < VARIANCE_WINDOW) {
s_amplitude_count++;
}
if (s_amplitude_count < 10) {
return 0; // Not enough samples
}
// Compute variance over window
float mean = 0;
for (int i = 0; i < s_amplitude_count; i++) {
mean += s_amplitude_history[i];
}
mean /= s_amplitude_count;
float var_sum = 0;
for (int i = 0; i < s_amplitude_count; i++) {
float diff = s_amplitude_history[i] - mean;
var_sum += diff * diff;
}
return var_sum / s_amplitude_count;
}
esp_err_t csi_set_role(node_role_t role, const uint8_t *passive_bssid) {
ESP_LOGI(TAG, "Setting role: %s", node_role_str(role));
// Handle passive mode filter
if (role == NODE_ROLE_PASSIVE && passive_bssid) {
memcpy(s_passive_bssid, passive_bssid, 6);
s_passive_filter_enabled = true;
ESP_LOGI(TAG, "Passive mode BSSID: %02X:%02X:%02X:%02X:%02X:%02X",
passive_bssid[0], passive_bssid[1], passive_bssid[2],
passive_bssid[3], passive_bssid[4], passive_bssid[5]);
} else {
s_passive_filter_enabled = false;
}
// Handle TX mode
if (role == NODE_ROLE_TX || role == NODE_ROLE_TX_RX) {
if (!s_tx_running) {
csi_start_tx();
}
} else {
if (s_tx_running) {
csi_stop_tx();
}
}
// Handle promiscuous mode for RX
if (role == NODE_ROLE_RX || role == NODE_ROLE_TX_RX || role == NODE_ROLE_PASSIVE) {
esp_wifi_set_promiscuous(true);
} else {
esp_wifi_set_promiscuous(false);
}
return ESP_OK;
}
esp_err_t csi_set_rate(uint8_t rate_hz) {
if (rate_hz < 1 || rate_hz > 100) {
return ESP_ERR_INVALID_ARG;
}
g_state.packet_rate = rate_hz;
ESP_LOGI(TAG, "Setting rate: %d Hz", rate_hz);
return ESP_OK;
}
esp_err_t csi_start_tx(void) {
if (s_tx_running) {
return ESP_OK;
}
s_tx_running = true;
xTaskCreate(csi_tx_task, "csi_tx", 4096, NULL, 5, &s_tx_task);
ESP_LOGI(TAG, "TX started");
return ESP_OK;
}
esp_err_t csi_stop_tx(void) {
s_tx_running = false;
if (s_tx_task) {
vTaskDelete(s_tx_task);
s_tx_task = NULL;
}
ESP_LOGI(TAG, "TX stopped");
return ESP_OK;
}
static void csi_tx_task(void *arg) {
// TX task sends null data packets that other nodes can receive CSI from
// Using ESP-NOW or custom packets
uint8_t broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
while (s_tx_running) {
uint32_t interval_ms = 1000 / g_state.packet_rate;
// Send a packet that other nodes can capture CSI from
// ESP32-S3 doesn't have direct TX CSI, but we can send packets
// that trigger CSI capture on receivers
// For now, use a simple approach: send a null data frame
// This requires being in station mode and associated
// The actual implementation would use esp_wifi_80211_tx
s_stats.tx_packets++;
vTaskDelay(pdMS_TO_TICKS(interval_ms));
}
vTaskDelete(NULL);
}
void csi_get_stats(csi_stats_t *stats) {
if (stats) {
memcpy(stats, &s_stats, sizeof(csi_stats_t));
}
}