Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 14 additions & 0 deletions firmware/esp32-csi-node/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 45 additions & 2 deletions firmware/esp32-csi-node/main/csi_collector.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---- */

Expand Down Expand Up @@ -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];
Expand Down
16 changes: 16 additions & 0 deletions firmware/esp32-csi-node/main/csi_collector.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
7 changes: 7 additions & 0 deletions firmware/esp32-csi-node/main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
45 changes: 45 additions & 0 deletions firmware/esp32-csi-node/main/nvs_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "nvs_config.h"

#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions firmware/esp32-csi-node/main/nvs_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down