diff --git a/.gitignore b/.gitignore index 93c41db2..197fc73a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Local machine configuration (not shared) +CLAUDE.local.md + # ESP32 firmware build artifacts and local config (contains WiFi credentials) firmware/esp32-csi-node/build/ firmware/esp32-csi-node/sdkconfig diff --git a/firmware/esp32-csi-node/main/Kconfig.projbuild b/firmware/esp32-csi-node/main/Kconfig.projbuild index 609dc2d4..245d023d 100644 --- a/firmware/esp32-csi-node/main/Kconfig.projbuild +++ b/firmware/esp32-csi-node/main/Kconfig.projbuild @@ -39,4 +39,18 @@ menu "CSI Node Configuration" help WiFi channel to listen on for CSI data. + config CSI_FILTER_MAC + string "CSI source MAC filter (AA:BB:CC:DD:EE:FF or empty)" + default "" + help + When set to a valid MAC address (e.g. "AA:BB:CC:DD:EE:FF"), + only CSI frames from that transmitter are processed. All + other frames are silently dropped. This prevents signal + mixing in multi-AP environments. + + Leave empty to accept CSI from all transmitters. + + Can be overridden at runtime via NVS key "filter_mac" + (6-byte blob) without reflashing. + endmenu diff --git a/firmware/esp32-csi-node/main/csi_collector.c b/firmware/esp32-csi-node/main/csi_collector.c index 4ce95b53..aaed5d92 100644 --- a/firmware/esp32-csi-node/main/csi_collector.c +++ b/firmware/esp32-csi-node/main/csi_collector.c @@ -26,6 +26,15 @@ 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_filtered = 0; + +/* ---- MAC address filter (Issue #98) ---- */ + +/** When non-zero, only CSI from s_filter_mac is accepted. */ +static uint8_t s_filter_enabled = 0; + +/** The accepted transmitter MAC address (6 bytes). */ +static uint8_t s_filter_mac[6] = {0}; /* ---- ADR-029: Channel-hop state ---- */ @@ -124,18 +133,52 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf return frame_size; } +void csi_collector_set_filter_mac(const uint8_t *mac) +{ + if (mac == NULL) { + s_filter_enabled = 0; + memset(s_filter_mac, 0, 6); + ESP_LOGI(TAG, "MAC filter disabled — accepting CSI from all transmitters"); + } else { + memcpy(s_filter_mac, mac, 6); + s_filter_enabled = 1; + ESP_LOGI(TAG, "MAC filter enabled: only accepting %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + s_filtered = 0; +} + /** * WiFi CSI callback — invoked by ESP-IDF when CSI data is available. + * + * When a MAC filter is active, frames from non-matching transmitters are + * silently dropped to prevent signal mixing in multi-AP environments. */ static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info) { (void)ctx; s_cb_count++; + /* ---- MAC address filter (Issue #98) ---- */ + if (s_filter_enabled) { + if (memcmp(info->mac, s_filter_mac, 6) != 0) { + s_filtered++; + if (s_filtered <= 3 || (s_filtered % 500) == 0) { + ESP_LOGD(TAG, "Filtered CSI from %02X:%02X:%02X:%02X:%02X:%02X (dropped %lu)", + info->mac[0], info->mac[1], info->mac[2], + info->mac[3], info->mac[4], info->mac[5], + (unsigned long)s_filtered); + } + return; + } + } + if (s_cb_count <= 3 || (s_cb_count % 100) == 0) { - ESP_LOGI(TAG, "CSI cb #%lu: len=%d rssi=%d ch=%d", + ESP_LOGI(TAG, "CSI cb #%lu: len=%d rssi=%d ch=%d mac=%02X:%02X:%02X:%02X:%02X:%02X", (unsigned long)s_cb_count, info->len, - info->rx_ctrl.rssi, info->rx_ctrl.channel); + info->rx_ctrl.rssi, info->rx_ctrl.channel, + info->mac[0], info->mac[1], info->mac[2], + info->mac[3], info->mac[4], info->mac[5]); } uint8_t frame_buf[CSI_MAX_FRAME_SIZE]; diff --git a/firmware/esp32-csi-node/main/csi_collector.h b/firmware/esp32-csi-node/main/csi_collector.h index 47c8ef13..5aeef1bc 100644 --- a/firmware/esp32-csi-node/main/csi_collector.h +++ b/firmware/esp32-csi-node/main/csi_collector.h @@ -22,12 +22,28 @@ /** Maximum number of channels in the hop table (ADR-029). */ #define CSI_HOP_CHANNELS_MAX 6 +/** Length of a MAC address in bytes. */ +#define CSI_MAC_LEN 6 + /** * Initialize CSI collection. * Registers the WiFi CSI callback. */ void csi_collector_init(void); +/** + * Set a MAC address filter for CSI collection. + * + * When set, only CSI frames from the specified transmitter MAC are processed; + * all others are silently dropped. This prevents signal mixing in multi-AP + * environments. + * + * Pass NULL to disable filtering (accept CSI from all transmitters). + * + * @param mac 6-byte MAC address to accept, or NULL to disable filtering. + */ +void csi_collector_set_filter_mac(const uint8_t *mac); + /** * Serialize CSI data into ADR-018 binary frame format. * diff --git a/firmware/esp32-csi-node/main/main.c b/firmware/esp32-csi-node/main/main.c index 23c2867a..5652fd9b 100644 --- a/firmware/esp32-csi-node/main/main.c +++ b/firmware/esp32-csi-node/main/main.c @@ -134,6 +134,13 @@ void app_main(void) /* Initialize CSI collection */ csi_collector_init(); + /* Apply MAC address filter if configured (Issue #98) */ + if (s_cfg.filter_mac_enabled) { + csi_collector_set_filter_mac(s_cfg.filter_mac); + } else { + ESP_LOGI(TAG, "No MAC filter — accepting CSI from all transmitters"); + } + ESP_LOGI(TAG, "CSI streaming active → %s:%d", s_cfg.target_ip, s_cfg.target_port); diff --git a/firmware/esp32-csi-node/main/nvs_config.c b/firmware/esp32-csi-node/main/nvs_config.c index dd549f6a..a8452cf3 100644 --- a/firmware/esp32-csi-node/main/nvs_config.c +++ b/firmware/esp32-csi-node/main/nvs_config.c @@ -9,6 +9,7 @@ #include "nvs_config.h" #include +#include #include "esp_log.h" #include "nvs_flash.h" #include "nvs.h" @@ -51,6 +52,29 @@ void nvs_config_load(nvs_config_t *cfg) cfg->tdm_slot_index = 0; cfg->tdm_node_count = 1; + /* MAC filter: default disabled (all zeros) */ + memset(cfg->filter_mac, 0, 6); + cfg->filter_mac_enabled = 0; + + /* Parse compile-time Kconfig MAC filter if set (format: "AA:BB:CC:DD:EE:FF") */ +#ifdef CONFIG_CSI_FILTER_MAC + { + const char *mac_str = CONFIG_CSI_FILTER_MAC; + unsigned int m[6]; + if (mac_str[0] != '\0' && + sscanf(mac_str, "%x:%x:%x:%x:%x:%x", + &m[0], &m[1], &m[2], &m[3], &m[4], &m[5]) == 6) { + for (int i = 0; i < 6; i++) { + cfg->filter_mac[i] = (uint8_t)m[i]; + } + cfg->filter_mac_enabled = 1; + ESP_LOGI(TAG, "Kconfig MAC filter: %02X:%02X:%02X:%02X:%02X:%02X", + cfg->filter_mac[0], cfg->filter_mac[1], cfg->filter_mac[2], + cfg->filter_mac[3], cfg->filter_mac[4], cfg->filter_mac[5]); + } + } +#endif + /* Try to override from NVS */ nvs_handle_t handle; esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle); @@ -152,6 +176,27 @@ void nvs_config_load(nvs_config_t *cfg) } } + /* MAC filter (stored as a 6-byte blob in NVS key "filter_mac") */ + uint8_t mac_blob[6]; + size_t mac_len = 6; + if (nvs_get_blob(handle, "filter_mac", mac_blob, &mac_len) == ESP_OK && mac_len == 6) { + /* Check it's not all zeros (which would mean "no filter") */ + uint8_t is_zero = 1; + for (int i = 0; i < 6; i++) { + if (mac_blob[i] != 0) { is_zero = 0; break; } + } + if (!is_zero) { + memcpy(cfg->filter_mac, mac_blob, 6); + cfg->filter_mac_enabled = 1; + ESP_LOGI(TAG, "NVS override: filter_mac=%02X:%02X:%02X:%02X:%02X:%02X", + mac_blob[0], mac_blob[1], mac_blob[2], + mac_blob[3], mac_blob[4], mac_blob[5]); + } else { + cfg->filter_mac_enabled = 0; + ESP_LOGI(TAG, "NVS override: filter_mac disabled (all zeros)"); + } + } + /* Validate tdm_slot_index < tdm_node_count */ if (cfg->tdm_slot_index >= cfg->tdm_node_count) { ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0", diff --git a/firmware/esp32-csi-node/main/nvs_config.h b/firmware/esp32-csi-node/main/nvs_config.h index 87e0a0c3..8779585d 100644 --- a/firmware/esp32-csi-node/main/nvs_config.h +++ b/firmware/esp32-csi-node/main/nvs_config.h @@ -35,6 +35,10 @@ typedef struct { uint32_t dwell_ms; /**< Dwell time per channel in ms. */ uint8_t tdm_slot_index; /**< This node's TDM slot index (0-based). */ uint8_t tdm_node_count; /**< Total nodes in the TDM schedule. */ + + /* MAC address filter for CSI source selection (Issue #98) */ + uint8_t filter_mac[6]; /**< Transmitter MAC to accept (all zeros = no filter). */ + uint8_t filter_mac_enabled; /**< 1 = filter active, 0 = accept all. */ } nvs_config_t; /**