Add MAC filter, histograms, and enhanced debug logging to ESP32 monitor

- Add MAC address filter for debug logging (monitor filter command)
- Add histograms for AMPDU aggregation, MCS distribution, and spatial streams
- Display RA (Receiver Address) in data frame debug logs alongside TA
- Add periodic diagnostic summary (every 10s) showing data frame stats
- Add filtered frame diagnostic logging when filter is active
- Fix MCS extraction to properly cap values based on HT/VHT/HE standards
- Add esp_timer dependency to wifi_monitor component
- Display frame type breakdown and histograms in monitor status
- Show debug and filter status in monitor status output

These changes help debug WiFi traffic by filtering on specific MAC addresses
and providing detailed statistics about frame characteristics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Robert McMahon 2026-02-10 16:38:21 -08:00
parent 8ba4bff040
commit c73e694b9a
6 changed files with 490 additions and 37 deletions

View File

@ -54,6 +54,11 @@ static struct {
struct arg_end *end;
} debug_args;
static struct {
struct arg_str *mac;
struct arg_end *end;
} filter_args;
static void print_monitor_usage(void) {
printf("Usage: monitor <subcommand> [args]\n");
printf("Subcommands:\n");
@ -62,6 +67,7 @@ static void print_monitor_usage(void) {
printf(" status Show current status\n");
printf(" channel <n> Switch channel (while running)\n");
printf(" debug [on|off] Enable/disable debug logging (default: show status)\n");
printf(" filter [mac] Set MAC address filter for debug (e.g., 80:84:89:93:c4:b6), or 'clear' to disable\n");
printf(" save Save current config to NVS\n");
printf(" reload Reload config from NVS\n");
printf(" clear Clear NVS config\n");
@ -139,6 +145,73 @@ static int do_monitor_debug(int argc, char **argv) {
return 0;
}
static int do_monitor_filter(int argc, char **argv) {
filter_args.mac = arg_str0(NULL, NULL, "<mac|clear>", "MAC address (XX:XX:XX:XX:XX:XX) or 'clear' to disable filter");
filter_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&filter_args);
if (nerrors > 0) {
arg_print_errors(stderr, filter_args.end, argv[0]);
return 1;
}
if (filter_args.mac->count > 0) {
const char *mac_str = filter_args.mac->sval[0];
if (strcmp(mac_str, "clear") == 0) {
wifi_ctl_set_monitor_debug_filter(NULL);
printf("Debug filter cleared\n");
} else {
/* Parse MAC address string (format: XX:XX:XX:XX:XX:XX) */
uint8_t mac[6];
int values[6];
int count = sscanf(mac_str, "%x:%x:%x:%x:%x:%x",
&values[0], &values[1], &values[2],
&values[3], &values[4], &values[5]);
if (count == 6) {
/* Validate values are in range */
bool valid = true;
for (int i = 0; i < 6; i++) {
if (values[i] < 0 || values[i] > 255) {
valid = false;
break;
}
mac[i] = (uint8_t)values[i];
}
if (valid) {
if (wifi_ctl_set_monitor_debug_filter(mac) == ESP_OK) {
printf("Debug filter set to: %02x:%02x:%02x:%02x:%02x:%02x\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
printf("Failed to set debug filter\n");
return 1;
}
} else {
printf("Invalid MAC address: values must be 0-255\n");
return 1;
}
} else {
printf("Invalid MAC address format. Use XX:XX:XX:XX:XX:XX or 'clear'\n");
return 1;
}
}
} else {
/* No argument: show current filter */
uint8_t mac[6];
bool enabled = wifi_ctl_get_monitor_debug_filter(mac);
if (enabled) {
printf("Debug filter: %02x:%02x:%02x:%02x:%02x:%02x\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
} else {
printf("Debug filter: disabled\n");
}
}
return 0;
}
static int cmd_monitor(int argc, char **argv) {
if (argc < 2) {
print_monitor_usage();
@ -154,6 +227,7 @@ static int cmd_monitor(int argc, char **argv) {
if (strcmp(argv[1], "channel") == 0) return do_monitor_channel(argc - 1, &argv[1]);
if (strcmp(argv[1], "debug") == 0) return do_monitor_debug(argc - 1, &argv[1]);
if (strcmp(argv[1], "filter") == 0) return do_monitor_filter(argc - 1, &argv[1]);
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) {
print_monitor_usage();
@ -175,6 +249,9 @@ void register_monitor_cmd(void) {
debug_args.enable = arg_str0(NULL, NULL, "<on|off>", "Enable or disable debug logging");
debug_args.end = arg_end(1);
filter_args.mac = arg_str0(NULL, NULL, "<mac|clear>", "MAC address filter or 'clear'");
filter_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
.command = "monitor",
.help = "Monitor Mode: start, stop, channel, status",

View File

@ -324,14 +324,14 @@ static void monitor_stats_task(void *arg) {
if (should_flush && current_offset > 0 && s_batch_mutex && sd_card_is_ready()) {
size_t flush_size = 0;
if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) {
memcpy(s_flush_buf, s_batch_buf, current_offset);
flush_size = current_offset;
s_batch_offset = 0;
xSemaphoreGive(s_batch_mutex);
}
if (flush_size > 0) {
esp_err_t write_err = sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true);
if (write_err == ESP_OK) {
@ -627,7 +627,7 @@ void wifi_ctl_set_channel(int channel) {
*/
static uint32_t wifi_channel_to_frequency(uint8_t channel) {
uint32_t freq = 0;
if (channel >= 1 && channel <= 14) {
/* 2.4 GHz band: 2407 + (channel * 5) MHz */
freq = 2407 + (channel * 5);
@ -644,7 +644,7 @@ static uint32_t wifi_channel_to_frequency(uint8_t channel) {
/* 5 GHz UNII-4: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
}
return freq;
}
@ -657,11 +657,11 @@ void wifi_ctl_status(void) {
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
uint8_t channel = s_monitor_channel_active;
uint32_t freq_mhz = wifi_channel_to_frequency(channel);
/* Display channel info: channel N (FREQ MHz), width: 20 MHz, center1: FREQ MHz */
/* Note: Monitor mode currently uses single channel (20 MHz width) */
if (freq_mhz > 0) {
printf(" Channel: %d (%" PRIu32 " MHz), width: 20 MHz, center1: %" PRIu32 " MHz\n",
printf(" Channel: %d (%" PRIu32 " MHz), width: 20 MHz, center1: %" PRIu32 " MHz\n",
channel, freq_mhz, freq_mhz);
} else {
printf(" Channel: %d\n", channel);
@ -673,6 +673,85 @@ void wifi_ctl_status(void) {
}
printf(" Frames: %lu, %.1f fps\n", (unsigned long)s_monitor_frame_count, frame_rate_fps);
/* Show frame type breakdown */
wifi_collapse_stats_t monitor_stats;
if (wifi_monitor_get_stats(&monitor_stats) == ESP_OK) {
printf(" Frame Types: Data=%lu, Mgmt=%lu, Ctrl=%lu (RTS=%lu, CTS=%lu, ACK=%lu)\n",
(unsigned long)monitor_stats.data_frames,
(unsigned long)monitor_stats.mgmt_frames,
(unsigned long)(monitor_stats.rts_frames + monitor_stats.cts_frames + monitor_stats.ack_frames),
(unsigned long)monitor_stats.rts_frames,
(unsigned long)monitor_stats.cts_frames,
(unsigned long)monitor_stats.ack_frames);
/* Show histograms */
// AMPDU histogram
uint32_t total_ampdu = 0;
for (int i = 0; i < 8; i++) {
total_ampdu += monitor_stats.ampdu_hist[i];
}
if (total_ampdu > 0) {
printf(" AMPDU Aggregation: ");
const char *ampdu_labels[] = {"1", "2-4", "5-8", "9-16", "17-32", "33-48", "49-64", "65+"};
for (int i = 0; i < 8; i++) {
if (monitor_stats.ampdu_hist[i] > 0) {
printf("%s=%lu ", ampdu_labels[i], (unsigned long)monitor_stats.ampdu_hist[i]);
}
}
printf("\n");
}
// MCS histogram (show non-zero entries)
uint32_t total_mcs = 0;
for (int i = 0; i < 32; i++) {
total_mcs += monitor_stats.mcs_hist[i];
}
if (total_mcs > 0) {
printf(" MCS Distribution: ");
int printed = 0;
for (int i = 0; i < 32; i++) {
if (monitor_stats.mcs_hist[i] > 0) {
if (printed > 0) printf(", ");
printf("MCS%d=%lu", i, (unsigned long)monitor_stats.mcs_hist[i]);
printed++;
if (printed >= 10) { // Limit output length
printf(" ...");
break;
}
}
}
printf("\n");
}
// Spatial streams histogram
uint32_t total_ss = 0;
for (int i = 0; i < 8; i++) {
total_ss += monitor_stats.ss_hist[i];
}
if (total_ss > 0) {
printf(" Spatial Streams: ");
for (int i = 0; i < 8; i++) {
if (monitor_stats.ss_hist[i] > 0) {
printf("%dSS=%lu ", i + 1, (unsigned long)monitor_stats.ss_hist[i]);
}
}
printf("\n");
}
}
/* Show debug and filter status */
bool debug_enabled = wifi_ctl_get_monitor_debug();
uint8_t filter_mac[6];
bool filter_enabled = wifi_ctl_get_monitor_debug_filter(filter_mac);
printf(" Debug: %s\n", debug_enabled ? "enabled" : "disabled");
if (filter_enabled) {
printf(" Filter: %02x:%02x:%02x:%02x:%02x:%02x\n",
filter_mac[0], filter_mac[1], filter_mac[2],
filter_mac[3], filter_mac[4], filter_mac[5]);
} else {
printf(" Filter: disabled (showing all frames)\n");
}
/* Show telemetry rate statistics */
uint64_t gen_bytes = 0, write_bytes = 0;
double gen_rate = 0.0, write_rate = 0.0;
@ -713,7 +792,7 @@ bool wifi_ctl_param_is_unsaved(void) {
void wifi_ctl_param_save(const char *dummy) {
(void)dummy;
/* If monitor mode is running, save the active channel; otherwise save staging */
uint8_t channel_to_save = (s_current_mode == WIFI_CTL_MODE_MONITOR) ?
uint8_t channel_to_save = (s_current_mode == WIFI_CTL_MODE_MONITOR) ?
s_monitor_channel_active : s_monitor_channel_staging;
if (wifi_cfg_set_monitor_channel(channel_to_save)) {
ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", channel_to_save);
@ -824,6 +903,14 @@ bool wifi_ctl_get_monitor_debug(void) {
return s_monitor_debug;
}
esp_err_t wifi_ctl_set_monitor_debug_filter(const uint8_t *mac) {
return wifi_monitor_set_debug_filter(mac);
}
bool wifi_ctl_get_monitor_debug_filter(uint8_t *mac_out) {
return wifi_monitor_get_debug_filter(mac_out);
}
// --- Deprecated ---
static void auto_monitor_task_func(void *arg) {
uint8_t channel = (uint8_t)(uintptr_t)arg;

View File

@ -82,6 +82,10 @@ esp_err_t wifi_ctl_get_sd_write_stats(uint64_t *total_bytes, double *rate_bps);
void wifi_ctl_set_monitor_debug(bool enable);
bool wifi_ctl_get_monitor_debug(void);
// Debug MAC filter control
esp_err_t wifi_ctl_set_monitor_debug_filter(const uint8_t *mac);
bool wifi_ctl_get_monitor_debug_filter(uint8_t *mac_out);
// Deprecated / Compatibility
void wifi_ctl_auto_monitor_start(uint8_t channel);

View File

@ -1,5 +1,5 @@
idf_component_register(
SRCS "wifi_monitor.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi nvs_flash
REQUIRES esp_wifi nvs_flash esp_timer
)

View File

@ -34,6 +34,7 @@
#include "wifi_monitor.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "string.h"
static const char *TAG = "WiFi_Monitor";
@ -57,9 +58,12 @@ uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mism
uint32_t log_every_n_mismatches = 1; // Log every Nth mismatch (1 = all, 10 = every 10th)
static uint32_t s_mismatch_log_counter = 0;
static bool s_monitor_debug = false; // Debug mode: enable serial logging
static uint8_t s_monitor_debug_filter_mac[6] = {0}; // MAC address filter (all zeros = no filter)
static bool s_monitor_debug_filter_enabled = false; // Whether MAC filtering is enabled
// Forward declarations
static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type);
static bool wifi_monitor_debug_filter_match(const uint8_t *mac);
/**
* @brief Parse HT/VHT/HE headers to extract PHY parameters
@ -103,10 +107,11 @@ static void wifi_parse_phy_headers(const uint8_t *payload, uint16_t len, uint16_
uint8_t ctrl_id = ht_ctrl & 0x0F;
if (ctrl_id == 0) {
// HT Control (802.11n) - MCS info might be elsewhere
// HT Control (802.11n) - MCS info is typically in PLCP header (HT-SIG)
// which ESP-IDF provides in rx_ctrl->rate, not in HT Control field
frame_info->sig_mode = 1; // HT
// Note: For HT, spatial streams are typically in HT Capabilities element
// or can be inferred from MCS index (MCS 0-7 = 1 SS, 8-15 = 2 SS, etc.)
// Note: MCS will be extracted from rx_ctrl->rate in the callback
// For HT, spatial streams can be inferred from MCS index (MCS 0-7 = 1 SS, 8-15 = 2 SS, etc.)
} else if (ctrl_id >= 1 && ctrl_id <= 3) {
// VHT Control (802.11ac) - bits layout varies by variant
frame_info->sig_mode = 3; // VHT
@ -147,6 +152,101 @@ static void wifi_parse_phy_headers(const uint8_t *payload, uint16_t len, uint16_
}
}
/**
* @brief Parse A-MPDU aggregation count from frame payload
* @param payload Raw frame payload
* @param len Total frame length
* @param mac_hdr_len Length of MAC header (24 or 30 bytes)
* @return Number of aggregated MPDUs (1 if not aggregated)
*/
static uint8_t wifi_parse_ampdu_count(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len) {
uint8_t count = 1; // Default: not aggregated
uint16_t offset = mac_hdr_len;
// Check if this is a QoS data frame (required for A-MPDU)
uint8_t frame_type = (payload[0] >> 2) & 0x03;
uint8_t frame_subtype = (payload[0] >> 4) & 0x0F;
if (frame_type != FRAME_TYPE_DATA) {
return count; // Not a data frame
}
// Check if it's a QoS subtype
bool is_qos = (frame_subtype == DATA_QOS_DATA ||
frame_subtype == DATA_QOS_DATA_CF_ACK ||
frame_subtype == DATA_QOS_DATA_CF_POLL ||
frame_subtype == DATA_QOS_DATA_CF_ACK_POLL ||
frame_subtype == DATA_QOS_NULL ||
frame_subtype == DATA_QOS_CF_POLL ||
frame_subtype == DATA_QOS_CF_ACK_POLL);
if (!is_qos || len < offset + 4) {
return count; // Not QoS or too short
}
// Skip QoS Control field (4 bytes)
offset += 4;
// Check for HT Control field (4 bytes) - present if Order bit set
// ORDER bit is 0x8000 (bit 15), which is in payload[1], bit 7
bool has_ht_ctrl = (payload[1] & 0x80) != 0;
if (has_ht_ctrl && len >= offset + 4) {
offset += 4; // Skip HT Control field
}
// A-MPDU delimiter format (IEEE 802.11-2016):
// Each delimiter is 4 bytes: [Reserved(4) | MPDU Length(12) | CRC(8) | Delimiter Signature(8)]
// Delimiter signature is 0x4E (ASCII 'N')
// We count delimiters until we find the end delimiter (length = 0) or run out of data
uint16_t remaining = len - offset;
if (remaining < 4) {
return count; // Not enough data for even one delimiter
}
// Count A-MPDU subframes by parsing delimiters
uint16_t pos = offset;
uint16_t delimiter_count = 0; // Use uint16_t to support up to 256 delimiters
while (pos + 4 <= len && delimiter_count < 256) { // Support up to 256 for HE (though typically 64)
// Read delimiter (little-endian)
uint16_t delimiter = payload[pos] | (payload[pos + 1] << 8);
uint8_t sig = payload[pos + 3];
// Check delimiter signature (should be 0x4E)
if (sig != 0x4E) {
break; // Not a valid delimiter
}
// Extract MPDU length (bits 4-15)
uint16_t mpdu_len = (delimiter >> 4) & 0x0FFF;
if (mpdu_len == 0) {
break; // End delimiter
}
delimiter_count++;
// Move to next delimiter (skip this MPDU)
pos += 4 + mpdu_len; // Delimiter (4) + MPDU length
// Align to 4-byte boundary
pos = (pos + 3) & ~3;
if (pos >= len) {
break; // Reached end of frame
}
}
// If we found multiple delimiters, this is an A-MPDU
// Cap count at 255 (max value for uint8_t return type)
if (delimiter_count > 1) {
count = (delimiter_count > 255) ? 255 : (uint8_t)delimiter_count;
}
return count;
}
/**
* @brief Parse 802.11 MAC header
*/
@ -205,6 +305,9 @@ esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info
// Parse HT/VHT/HE headers to extract PHY parameters
wifi_parse_phy_headers(payload, len, mac_hdr_len, frame_info);
// Parse A-MPDU aggregation count
frame_info->ampdu_count = wifi_parse_ampdu_count(payload, len, mac_hdr_len);
frame_info->frame_len = len;
return ESP_OK;
@ -236,11 +339,29 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
frame_info.rate = rx_ctrl->rate; // This is the rate index
// If MCS wasn't parsed from headers but we have HT/VHT/HE mode, try to extract from rate
// ESP-IDF encoding: rate >= 128 encodes MCS for HT/VHT/HE frames
// HT: MCS 0-31, VHT: MCS 0-9, HE: MCS 0-11
if (frame_info.sig_mode > 0 && frame_info.mcs == 0 && rx_ctrl->rate >= 128) {
// For HT/VHT/HE, rate >= 128 might encode MCS
// ESP-IDF encoding: rate 128+ might be MCS index
frame_info.mcs = rx_ctrl->rate - 128;
if (frame_info.mcs > 11) frame_info.mcs = 11; // Cap at max MCS
uint8_t extracted_mcs = rx_ctrl->rate - 128;
// Cap MCS based on signal mode
if (frame_info.sig_mode == 1) {
// HT (802.11n): MCS 0-31
if (extracted_mcs > 31) extracted_mcs = 31;
frame_info.mcs = extracted_mcs;
} else if (frame_info.sig_mode == 3) {
// VHT (802.11ac): MCS 0-9
if (extracted_mcs > 9) extracted_mcs = 9;
frame_info.mcs = extracted_mcs;
} else if (frame_info.sig_mode == 4) {
// HE (802.11ax): MCS 0-11
if (extracted_mcs > 11) extracted_mcs = 11;
frame_info.mcs = extracted_mcs;
} else {
// Unknown mode, use extracted value but cap at 31 (max HT)
if (extracted_mcs > 31) extracted_mcs = 31;
frame_info.mcs = extracted_mcs;
}
}
// Calculate PHY rate using parsed MCS/spatial streams if available
@ -317,20 +438,23 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
if (frame_info.duration_id > threshold_duration_mismatch_us) {
s_mismatch_log_counter++;
if (s_monitor_debug && (s_mismatch_log_counter % log_every_n_mismatches) == 0) {
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps);
/* Check MAC filter before logging */
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps);
// NEW: Log the Source MAC (Addr2)
ESP_LOGW("MONITOR", " Source MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
// NEW: Log the Source MAC (Addr2)
ESP_LOGW("MONITOR", " Source MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
expected_duration, frame_info.duration_id,
frame_info.duration_id - expected_duration);
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
frame_info.retry ? "YES" : "no", frame_info.rssi);
ESP_LOGW("MONITOR", " Expected: %lu us, Actual NAV: %u us (+%ld us)",
expected_duration, frame_info.duration_id,
frame_info.duration_id - expected_duration);
ESP_LOGW("MONITOR", " Retry: %s, RSSI: %d dBm",
frame_info.retry ? "YES" : "no", frame_info.rssi);
}
}
}
}
@ -341,18 +465,21 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us &&
phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
if (s_monitor_debug) {
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
/* Check MAC filter before logging */
if (wifi_monitor_debug_filter_match(frame_info.addr2)) {
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
// NEW: Log the Attacker MAC
ESP_LOGW("MONITOR", " Attacker MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
// NEW: Log the Attacker MAC
ESP_LOGW("MONITOR", " Attacker MAC: %02x:%02x:%02x:%02x:%02x:%02x",
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5]);
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps);
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
frame_info.duration_id, expected_duration);
ESP_LOGW("MONITOR", " Type: %s, Size: %u bytes, Rate: %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps);
ESP_LOGW("MONITOR", " NAV: %u us (expected %lu us), Retry: YES",
frame_info.duration_id, expected_duration);
}
}
}
}
@ -375,6 +502,94 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
case FRAME_TYPE_DATA:
stats.data_frames++;
/* Periodic diagnostic summary (even when debug is off) */
static uint32_t data_frame_diag_counter = 0;
static uint64_t last_diag_log_ms = 0;
data_frame_diag_counter++;
uint64_t now_ms = esp_timer_get_time() / 1000;
if (now_ms - last_diag_log_ms > 10000) { /* Log every 10 seconds */
ESP_LOGI("MONITOR", "Data frames: %lu total, last TA=%02x:%02x:%02x:%02x:%02x:%02x, "
"debug=%s, filter=%s",
(unsigned long)stats.data_frames,
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
s_monitor_debug ? "on" : "off",
s_monitor_debug_filter_enabled ? "on" : "off");
data_frame_diag_counter = 0;
last_diag_log_ms = now_ms;
}
/* Update histograms for data frames */
// AMPDU histogram: [1, 2-4, 5-8, 9-16, 17-32, 33-48, 49-64, 65+]
uint8_t ampdu_count = frame_info.ampdu_count;
if (ampdu_count == 1) {
stats.ampdu_hist[0]++;
} else if (ampdu_count >= 2 && ampdu_count <= 4) {
stats.ampdu_hist[1]++;
} else if (ampdu_count >= 5 && ampdu_count <= 8) {
stats.ampdu_hist[2]++;
} else if (ampdu_count >= 9 && ampdu_count <= 16) {
stats.ampdu_hist[3]++;
} else if (ampdu_count >= 17 && ampdu_count <= 32) {
stats.ampdu_hist[4]++;
} else if (ampdu_count >= 33 && ampdu_count <= 48) {
stats.ampdu_hist[5]++;
} else if (ampdu_count >= 49 && ampdu_count <= 64) {
stats.ampdu_hist[6]++;
} else if (ampdu_count >= 65) {
stats.ampdu_hist[7]++;
}
// MCS histogram: [0-31]
if (frame_info.mcs < 32) {
stats.mcs_hist[frame_info.mcs]++;
}
// Spatial streams histogram: [1-8]
uint8_t ss = frame_info.spatial_streams;
if (ss >= 1 && ss <= 8) {
stats.ss_hist[ss - 1]++; // Convert to 0-indexed
}
/* Debug logging for data frames (especially QoS data used by iperf) */
if (s_monitor_debug) {
bool filter_match = wifi_monitor_debug_filter_match(frame_info.addr2);
uint16_t phy_rate_mbps = frame_info.phy_rate_kbps / 1000;
const char *frame_type_name = wifi_frame_type_str(frame_info.type, frame_info.subtype);
if (filter_match) {
/* Log all data frames (QoS and non-QoS) when filter matches or filter is disabled */
ESP_LOGI("MONITOR", "DATA: %s, TA=%02x:%02x:%02x:%02x:%02x:%02x, RA=%02x:%02x:%02x:%02x:%02x:%02x, "
"Size=%u bytes, Rate=%u Mbps, MCS=%u, SS=%u, BW=%u MHz, RSSI=%d dBm, Retry=%s",
frame_type_name,
frame_info.addr2[0], frame_info.addr2[1], frame_info.addr2[2],
frame_info.addr2[3], frame_info.addr2[4], frame_info.addr2[5],
frame_info.addr1[0], frame_info.addr1[1], frame_info.addr1[2],
frame_info.addr1[3], frame_info.addr1[4], frame_info.addr1[5],
frame_info.frame_len, phy_rate_mbps, frame_info.mcs,
frame_info.spatial_streams,
(frame_info.bandwidth == 0) ? 20 : (frame_info.bandwidth == 1) ? 40 : 80,
frame_info.rssi, frame_info.retry ? "YES" : "no");
} else {
/* Diagnostic: log when data frames are filtered out (throttled, but more visible) */
static uint32_t filtered_data_count = 0;
static uint64_t last_filtered_log_ms = 0;
static uint8_t last_filtered_ta[6] = {0};
filtered_data_count++;
uint64_t now_ms = esp_timer_get_time() / 1000;
memcpy(last_filtered_ta, frame_info.addr2, 6);
if (now_ms - last_filtered_log_ms > 5000) { /* Log every 5 seconds */
ESP_LOGW("MONITOR", "Filtered out %lu data frames (last TA=%02x:%02x:%02x:%02x:%02x:%02x, filter=%s)",
(unsigned long)filtered_data_count,
last_filtered_ta[0], last_filtered_ta[1], last_filtered_ta[2],
last_filtered_ta[3], last_filtered_ta[4], last_filtered_ta[5],
s_monitor_debug_filter_enabled ? "enabled" : "disabled");
filtered_data_count = 0;
last_filtered_log_ms = now_ms;
}
}
}
break;
}
@ -608,3 +823,51 @@ void wifi_monitor_set_debug(bool enable) {
bool wifi_monitor_get_debug(void) {
return s_monitor_debug;
}
/**
* @brief Check if a MAC address matches the debug filter
* @param mac MAC address to check (6 bytes)
* @return true if filter is disabled or MAC matches filter, false otherwise
*/
static bool wifi_monitor_debug_filter_match(const uint8_t *mac) {
if (!s_monitor_debug_filter_enabled || mac == NULL) {
return true; /* No filter or invalid MAC - allow all */
}
/* Compare MAC addresses byte by byte */
for (int i = 0; i < 6; i++) {
if (mac[i] != s_monitor_debug_filter_mac[i]) {
return false; /* MAC doesn't match filter */
}
}
return true; /* MAC matches filter */
}
/**
* @brief Set MAC address filter for debug logging
* @param mac MAC address to filter on (6 bytes), or NULL to disable filter
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if mac is invalid
*/
esp_err_t wifi_monitor_set_debug_filter(const uint8_t *mac) {
if (mac == NULL) {
/* Disable filter */
s_monitor_debug_filter_enabled = false;
memset(s_monitor_debug_filter_mac, 0, 6);
return ESP_OK;
}
/* Enable filter and copy MAC address */
memcpy(s_monitor_debug_filter_mac, mac, 6);
s_monitor_debug_filter_enabled = true;
return ESP_OK;
}
/**
* @brief Get current MAC address filter for debug logging
* @param mac_out Buffer to store MAC address (6 bytes), or NULL to just check if filter is enabled
* @return true if filter is enabled, false otherwise
*/
bool wifi_monitor_get_debug_filter(uint8_t *mac_out) {
if (mac_out != NULL && s_monitor_debug_filter_enabled) {
memcpy(mac_out, s_monitor_debug_filter_mac, 6);
}
return s_monitor_debug_filter_enabled;
}

View File

@ -176,6 +176,9 @@ typedef struct {
uint8_t bandwidth; // 0=20MHz, 1=40MHz, 2=80MHz, 3=160MHz
uint32_t phy_rate_kbps; // Calculated PHY rate in Kbps (uint32_t to handle >65 Mbps)
// Aggregation info
uint8_t ampdu_count; // Number of aggregated MPDUs in A-MPDU (1 = not aggregated)
// Frame size
uint16_t frame_len;
} wifi_frame_info_t;
@ -205,6 +208,11 @@ typedef struct {
uint16_t avg_phy_rate_mbps; // Average PHY rate
uint16_t min_phy_rate_mbps; // Minimum PHY rate seen
uint16_t max_phy_rate_mbps; // Maximum PHY rate seen
// Histograms
uint32_t ampdu_hist[8]; // AMPDU aggregation: [1, 2-4, 5-8, 9-16, 17-32, 33-48, 49-64, 65+]
uint32_t mcs_hist[32]; // MCS index: [0-31]
uint32_t ss_hist[8]; // Spatial streams: [1-8]
} wifi_collapse_stats_t;
/**
@ -300,6 +308,20 @@ void wifi_monitor_set_debug(bool enable);
*/
bool wifi_monitor_get_debug(void);
/**
* @brief Set MAC address filter for debug logging
* @param mac MAC address to filter on (6 bytes), or NULL to disable filter
* @return ESP_OK on success, ESP_ERR_INVALID_ARG if mac is invalid
*/
esp_err_t wifi_monitor_set_debug_filter(const uint8_t *mac);
/**
* @brief Get current MAC address filter for debug logging
* @param mac_out Buffer to store MAC address (6 bytes), or NULL to just check if filter is enabled
* @return true if filter is enabled, false otherwise
*/
bool wifi_monitor_get_debug_filter(uint8_t *mac_out);
#ifdef __cplusplus
}
#endif