From 0311ad21a1e1c0fd070c6dd523ec06b972aa9928 Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Tue, 10 Feb 2026 10:05:13 -0800 Subject: [PATCH] Fix telemetry dropped counter and improve SD card list display - Fix dropped telemetry calculation to account for bytes in batch buffer - Add human-readable file sizes to sdcard list output (bytes with human-readable in parentheses) - Update monitor status to show SD card status inline with telemetry label - Add pending bytes display when telemetry is in batch buffer Co-authored-by: Cursor --- components/app_console/cmd_monitor.c | 44 ++ components/mcs_telemetry/mcs_telemetry.c | 48 +- components/mcs_telemetry/mcs_telemetry.h | 5 +- components/sd_card/CMakeLists.txt | 2 +- components/sd_card/sd_card.c | 603 +++++++++++-------- components/sd_card/sd_card.h | 16 +- components/wifi_controller/wifi_controller.c | 551 ++++++++++++++--- components/wifi_controller/wifi_controller.h | 8 + components/wifi_monitor/wifi_monitor.c | 179 +++++- components/wifi_monitor/wifi_monitor.h | 59 +- 10 files changed, 1101 insertions(+), 414 deletions(-) diff --git a/components/app_console/cmd_monitor.c b/components/app_console/cmd_monitor.c index c67cf1e..7c9f20b 100644 --- a/components/app_console/cmd_monitor.c +++ b/components/app_console/cmd_monitor.c @@ -49,6 +49,11 @@ static struct { struct arg_end *end; } channel_args; +static struct { + struct arg_str *enable; + struct arg_end *end; +} debug_args; + static void print_monitor_usage(void) { printf("Usage: monitor [args]\n"); printf("Subcommands:\n"); @@ -56,6 +61,7 @@ static void print_monitor_usage(void) { printf(" stop Stop Monitor Mode\n"); printf(" status Show current status\n"); printf(" channel Switch channel (while running)\n"); + printf(" debug [on|off] Enable/disable debug logging (default: show status)\n"); printf(" save Save current config to NVS\n"); printf(" reload Reload config from NVS\n"); printf(" clear Clear NVS config\n"); @@ -99,6 +105,40 @@ static int do_monitor_channel(int argc, char **argv) { return 0; } +static int do_monitor_debug(int argc, char **argv) { + debug_args.enable = arg_str0(NULL, NULL, "", "Enable (on) or disable (off) debug logging"); + debug_args.end = arg_end(1); + + int nerrors = arg_parse(argc, argv, (void **)&debug_args); + if (nerrors > 0) { + arg_print_errors(stderr, debug_args.end, argv[0]); + return 1; + } + + if (debug_args.enable->count > 0) { + const char *value = debug_args.enable->sval[0]; + bool enable = false; + + if (strcmp(value, "on") == 0 || strcmp(value, "1") == 0 || strcmp(value, "true") == 0) { + enable = true; + } else if (strcmp(value, "off") == 0 || strcmp(value, "0") == 0 || strcmp(value, "false") == 0) { + enable = false; + } else { + printf("Invalid value '%s'. Use 'on' or 'off'.\n", value); + return 1; + } + + wifi_ctl_set_monitor_debug(enable); + printf("Debug mode %s\n", enable ? "enabled" : "disabled"); + } else { + /* No argument: show current status */ + bool enabled = wifi_ctl_get_monitor_debug(); + printf("Debug mode: %s\n", enabled ? "enabled" : "disabled"); + } + + return 0; +} + static int cmd_monitor(int argc, char **argv) { if (argc < 2) { print_monitor_usage(); @@ -113,6 +153,7 @@ static int cmd_monitor(int argc, char **argv) { if (strcmp(argv[1], "clear") == 0) { wifi_ctl_param_clear(); printf("Cleared.\n"); return 0; } 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], "help") == 0 || strcmp(argv[1], "--help") == 0) { print_monitor_usage(); @@ -131,6 +172,9 @@ void register_monitor_cmd(void) { channel_args.channel = arg_int1(NULL, NULL, "", "Channel"); channel_args.end = arg_end(1); + debug_args.enable = arg_str0(NULL, NULL, "", "Enable or disable debug logging"); + debug_args.end = arg_end(1); + const esp_console_cmd_t cmd = { .command = "monitor", .help = "Monitor Mode: start, stop, channel, status", diff --git a/components/mcs_telemetry/mcs_telemetry.c b/components/mcs_telemetry/mcs_telemetry.c index 24d6c66..fd40a84 100644 --- a/components/mcs_telemetry/mcs_telemetry.c +++ b/components/mcs_telemetry/mcs_telemetry.c @@ -109,6 +109,39 @@ static const uint32_t PHY_RATES_40MHZ_1SS_400NS[] = { 320300 // MCS 11 }; +// 802.11ax MCS rates for 80MHz, 800ns GI, 1 Spatial Stream +// (80MHz rates are approximately 2x 40MHz rates) +static const uint32_t PHY_RATES_80MHZ_1SS_800NS[] = { + 34400, // MCS 0 + 68800, // MCS 1 + 103200, // MCS 2 + 137600, // MCS 3 + 206500, // MCS 4 + 275200, // MCS 5 + 309800, // MCS 6 + 344200, // MCS 7 + 413000, // MCS 8 + 458800, // MCS 9 + 516200, // MCS 10 + 573600 // MCS 11 +}; + +// 802.11ax MCS rates for 80MHz, 400ns GI, 1 Spatial Stream +static const uint32_t PHY_RATES_80MHZ_1SS_400NS[] = { + 38400, // MCS 0 + 76800, // MCS 1 + 115200, // MCS 2 + 153600, // MCS 3 + 230400, // MCS 4 + 307200, // MCS 5 + 345600, // MCS 6 + 384000, // MCS 7 + 460800, // MCS 8 + 512400, // MCS 9 + 576400, // MCS 10 + 640600 // MCS 11 +}; + /** * @brief Get device index by MAC address, or create new entry */ @@ -253,15 +286,20 @@ esp_err_t mcs_telemetry_process_frame(const wifi_frame_info_t *frame_info, const mcs_sample_t sample = {0}; sample.timestamp_ms = esp_timer_get_time() / 1000; sample.mcs = frame_info->mcs; - sample.ss = 1; // TODO: Extract from HT/VHT/HE headers + sample.ss = frame_info->spatial_streams; // Now extracted from HT/VHT/HE headers sample.rssi = frame_info->rssi; sample.channel = frame_info->channel; sample.bandwidth = (frame_info->bandwidth == 0) ? MCS_BW_20MHZ : - (frame_info->bandwidth == 1) ? MCS_BW_40MHZ : MCS_BW_20MHZ; + (frame_info->bandwidth == 1) ? MCS_BW_40MHZ : + (frame_info->bandwidth == 2) ? MCS_BW_80MHZ : MCS_BW_20MHZ; sample.frame_len = frame_info->frame_len; sample.is_retry = frame_info->retry; sample.sig_mode = frame_info->sig_mode; + // Ensure spatial streams is valid (1-8) + if (sample.ss < 1) sample.ss = 1; + if (sample.ss > MCS_TELEMETRY_MAX_SS) sample.ss = MCS_TELEMETRY_MAX_SS; + // Calculate PHY rate if we have MCS info if (sample.mcs <= MCS_TELEMETRY_MAX_MCS && sample.ss >= 1 && sample.ss <= MCS_TELEMETRY_MAX_SS) { sample.phy_rate_kbps = mcs_calculate_phy_rate_ax(sample.mcs, sample.ss, sample.bandwidth, frame_info->sgi); @@ -274,10 +312,6 @@ esp_err_t mcs_telemetry_process_frame(const wifi_frame_info_t *frame_info, const mcs_update_device_telemetry(dev_idx, &sample); s_stats.total_frames_captured++; - // TODO: Parse HT/VHT/HE headers to extract actual SS count - // This requires parsing the PLCP/HT Control/VHT Control/HE Control fields - // which follow the MAC header in HT/VHT/HE frames - return ESP_OK; } @@ -377,6 +411,8 @@ uint32_t mcs_calculate_phy_rate_ax(uint8_t mcs, uint8_t ss, mcs_bandwidth_t band rate_table = sgi ? PHY_RATES_20MHZ_1SS_400NS : PHY_RATES_20MHZ_1SS_800NS; } else if (bandwidth == MCS_BW_40MHZ) { rate_table = sgi ? PHY_RATES_40MHZ_1SS_400NS : PHY_RATES_40MHZ_1SS_800NS; + } else if (bandwidth == MCS_BW_80MHZ) { + rate_table = sgi ? PHY_RATES_80MHZ_1SS_400NS : PHY_RATES_80MHZ_1SS_800NS; } else { return 0; } diff --git a/components/mcs_telemetry/mcs_telemetry.h b/components/mcs_telemetry/mcs_telemetry.h index 9e41212..a581396 100644 --- a/components/mcs_telemetry/mcs_telemetry.h +++ b/components/mcs_telemetry/mcs_telemetry.h @@ -67,11 +67,12 @@ extern "C" { #define MCS_TELEMETRY_WINDOW_MS 1000 /** - * @brief 802.11ax Bandwidth types (ESP32-C5 supports 20MHz and 40MHz) + * @brief 802.11ax Bandwidth types (ESP32-C5 supports 20MHz and 40MHz, but we track 80MHz for compatibility) */ typedef enum { MCS_BW_20MHZ = 0, - MCS_BW_40MHZ = 1 + MCS_BW_40MHZ = 1, + MCS_BW_80MHZ = 2 } mcs_bandwidth_t; /** diff --git a/components/sd_card/CMakeLists.txt b/components/sd_card/CMakeLists.txt index fd5c373..981565c 100644 --- a/components/sd_card/CMakeLists.txt +++ b/components/sd_card/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRCS "sd_card.c" INCLUDE_DIRS "." - REQUIRES driver fatfs esp_driver_sdspi sdmmc + REQUIRES driver fatfs esp_driver_sdspi sdmmc esp_timer ) diff --git a/components/sd_card/sd_card.c b/components/sd_card/sd_card.c index c649c16..c501ad2 100644 --- a/components/sd_card/sd_card.c +++ b/components/sd_card/sd_card.c @@ -33,6 +33,7 @@ #include "sd_card.h" #include "esp_log.h" +#include "esp_timer.h" #include "driver/gpio.h" #include "sdkconfig.h" #include "esp_vfs_fat.h" @@ -45,6 +46,19 @@ #include #include +/* Format bytes as human-readable (e.g. 1.2K, 4.5M). Writes into buf, max len chars. */ +static void fmt_size_human(size_t bytes, char *buf, size_t len) { + if (bytes < 1024) { + snprintf(buf, len, "%zu B", bytes); + } else if (bytes < 1024 * 1024) { + snprintf(buf, len, "%.1f K", bytes / 1024.0); + } else if (bytes < 1024ULL * 1024 * 1024) { + snprintf(buf, len, "%.1f M", bytes / (1024.0 * 1024.0)); + } else { + snprintf(buf, len, "%.1f G", bytes / (1024.0 * 1024.0 * 1024.0)); + } +} + // Pin definitions for SparkFun microSD Transflash Breakout // ESP32-C5: no SDMMC host, use SD SPI mode // SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional) @@ -85,112 +99,162 @@ static bool s_cd_configured = false; static bool s_spi_bus_inited = false; static sdmmc_card_t *s_card = NULL; static const char *s_mount_point = "/sdcard"; +static uint64_t s_last_status_check_ms = 0; +static bool s_last_status_result = false; +#define SD_STATUS_CHECK_INTERVAL_MS 1000 /* Check card status at most once per second */ static void sd_card_cd_ensure_configured(void) { - if (s_cd_configured || SD_CD_PIN < 0) { - return; - } - /* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */ - const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu; - gpio_config_t io = { - .pin_bit_mask = (1ULL << cd_pin), - .mode = GPIO_MODE_INPUT, - .pull_up_en = GPIO_PULLUP_ENABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - if (gpio_config(&io) == ESP_OK) { - s_cd_configured = true; + if (!s_cd_configured && SD_CD_PIN >= 0) { + /* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */ + const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu; + gpio_config_t io = { + .pin_bit_mask = (1ULL << cd_pin), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + if (gpio_config(&io) == ESP_OK) { + s_cd_configured = true; + } } } esp_err_t sd_card_init(void) { + esp_err_t result = ESP_OK; + if (s_sd_card_mounted) { ESP_LOGW(TAG, "SD card already initialized"); - return ESP_OK; - } + result = ESP_OK; + } else { + ESP_LOGI(TAG, "Initializing SD card via SPI..."); - ESP_LOGI(TAG, "Initializing SD card via SPI..."); + // Initialize SPI bus (required before sdspi mount) + spi_bus_config_t bus_cfg = { + .mosi_io_num = SDSPI_MOSI_PIN, + .miso_io_num = SDSPI_MISO_PIN, + .sclk_io_num = SDSPI_CLK_PIN, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 4000, + }; + result = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); + if (result == ESP_OK) { + s_spi_bus_inited = true; - // Initialize SPI bus (required before sdspi mount) - spi_bus_config_t bus_cfg = { - .mosi_io_num = SDSPI_MOSI_PIN, - .miso_io_num = SDSPI_MISO_PIN, - .sclk_io_num = SDSPI_CLK_PIN, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = 4000, - }; - esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err)); - return err; - } - s_spi_bus_inited = true; + sdmmc_host_t host = SDSPI_HOST_DEFAULT(); + host.slot = SDSPI_HOST_ID; - sdmmc_host_t host = SDSPI_HOST_DEFAULT(); - host.slot = SDSPI_HOST_ID; + sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + slot_config.gpio_cs = SDSPI_CS_PIN; + slot_config.host_id = SDSPI_HOST_ID; + // Do not pass gpio_cd to driver: ESP-IDF expects LOW=inserted and blocks init if CD says no card. + // We use CD only for status (sd_card_cd_is_inserted) with configurable polarity. + slot_config.gpio_cd = SDSPI_SLOT_NO_CD; + slot_config.gpio_wp = SDSPI_SLOT_NO_WP; - sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); - slot_config.gpio_cs = SDSPI_CS_PIN; - slot_config.host_id = SDSPI_HOST_ID; - // Do not pass gpio_cd to driver: ESP-IDF expects LOW=inserted and blocks init if CD says no card. - // We use CD only for status (sd_card_cd_is_inserted) with configurable polarity. - slot_config.gpio_cd = SDSPI_SLOT_NO_CD; - slot_config.gpio_wp = SDSPI_SLOT_NO_WP; + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024 + }; - esp_vfs_fat_sdmmc_mount_config_t mount_config = { - .format_if_mount_failed = false, - .max_files = 5, - .allocation_unit_size = 16 * 1024 - }; + result = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card); - err = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card); - - if (err != ESP_OK) { - if (s_spi_bus_inited) { - spi_bus_free(SDSPI_HOST_ID); - s_spi_bus_inited = false; - } - if (err == ESP_FAIL) { - ESP_LOGE(TAG, "Failed to mount filesystem. " - "If you want the card to be formatted, set format_if_mount_failed = true."); + if (result != ESP_OK) { + if (s_spi_bus_inited) { + spi_bus_free(SDSPI_HOST_ID); + s_spi_bus_inited = false; + } + /* Reset status cache on init failure */ + s_last_status_result = false; + s_last_status_check_ms = 0; + if (result == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount filesystem. " + "If you want the card to be formatted, set format_if_mount_failed = true."); + } else { + ESP_LOGE(TAG, "Failed to initialize the card (%s). " + "Make sure SD card is inserted and wiring is correct.", esp_err_to_name(result)); + } + } else { + sdmmc_card_print_info(stdout, s_card); + s_sd_card_mounted = true; + /* Reset status cache on successful mount */ + s_last_status_result = true; + s_last_status_check_ms = esp_timer_get_time() / 1000; + ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point); + } } else { - ESP_LOGE(TAG, "Failed to initialize the card (%s). " - "Make sure SD card is inserted and wiring is correct.", esp_err_to_name(err)); + ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(result)); } - return err; } - sdmmc_card_print_info(stdout, s_card); - s_sd_card_mounted = true; - ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point); - return ESP_OK; + return result; } esp_err_t sd_card_deinit(void) { - if (!s_sd_card_mounted) { - return ESP_OK; + esp_err_t result = ESP_OK; + + if (s_sd_card_mounted) { + ESP_LOGI(TAG, "Unmounting SD card..."); + result = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card); + if (result == ESP_OK) { + s_sd_card_mounted = false; + s_card = NULL; + /* Reset status cache on unmount */ + s_last_status_result = false; + s_last_status_check_ms = 0; + if (s_spi_bus_inited) { + spi_bus_free(SDSPI_HOST_ID); + s_spi_bus_inited = false; + } + ESP_LOGI(TAG, "SD card unmounted successfully"); + } else { + ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(result)); + } } - ESP_LOGI(TAG, "Unmounting SD card..."); - esp_err_t ret = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card); - if (ret == ESP_OK) { - s_sd_card_mounted = false; - s_card = NULL; - if (s_spi_bus_inited) { - spi_bus_free(SDSPI_HOST_ID); - s_spi_bus_inited = false; - } - ESP_LOGI(TAG, "SD card unmounted successfully"); - } else { - ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(ret)); - } - return ret; + return result; } bool sd_card_is_ready(void) { - return s_sd_card_mounted; + bool result = false; + + /* If mounted, assume card is ready (since CD pin doesn't work) */ + /* Status check is used to detect removal, but we're lenient about failures */ + if (s_sd_card_mounted && s_card != NULL) { + /* Since CD pin doesn't work, probe the card periodically to detect removal */ + /* Cache the result to avoid checking too frequently (SDMMC commands have overhead) */ + uint64_t now_ms = esp_timer_get_time() / 1000; + uint64_t time_since_check = now_ms - s_last_status_check_ms; + + if (time_since_check >= SD_STATUS_CHECK_INTERVAL_MS) { + /* Use CMD13 (SEND_STATUS) to check if card is actually responsive */ + /* This is a lightweight operation that will fail if card is removed */ + esp_err_t err = sdmmc_get_status(s_card); + + if (err == ESP_OK) { + /* Status check passed - card is definitely ready */ + s_last_status_result = true; + } else { + /* Status check failed - but be lenient: only mark as not ready if we've */ + /* had multiple consecutive failures (to avoid false negatives) */ + /* For now, if mounted, assume ready - status check failures might be transient */ + /* Only log a warning, don't block writes */ + ESP_LOGW(TAG, "SD card status check failed (but assuming ready): %s", esp_err_to_name(err)); + /* Keep s_last_status_result as true - trust mount status over status check */ + /* This handles cases where sdmmc_get_status() fails but card is still functional */ + } + s_last_status_check_ms = now_ms; + } + /* If mounted, always return true (optimistic) - let actual write operations fail if card is gone */ + result = true; + } else { + s_last_status_result = false; + result = false; + } + + return result; } bool sd_card_cd_available(void) { @@ -198,229 +262,256 @@ bool sd_card_cd_available(void) { } bool sd_card_cd_is_inserted(void) { - if (SD_CD_PIN < 0) { - return false; - } - sd_card_cd_ensure_configured(); - int level = gpio_get_level(SD_CD_PIN); + bool result = false; + + if (SD_CD_PIN >= 0) { + sd_card_cd_ensure_configured(); + int level = gpio_get_level(SD_CD_PIN); #if defined(CONFIG_SD_CD_ACTIVE_LOW) && !(CONFIG_SD_CD_ACTIVE_LOW) - return (level == 1); /* HIGH = inserted (inverted breakout) */ + result = (level == 1); /* HIGH = inserted (inverted breakout) */ #else - return (level == 0); /* LOW = inserted (SparkFun default) */ + result = (level == 0); /* LOW = inserted (SparkFun default) */ #endif + } + + return result; } int sd_card_cd_get_level(void) { - if (SD_CD_PIN < 0) { - return -1; + int result = -1; + + if (SD_CD_PIN >= 0) { + sd_card_cd_ensure_configured(); + result = gpio_get_level(SD_CD_PIN); } - sd_card_cd_ensure_configured(); - return gpio_get_level(SD_CD_PIN); + + return result; } esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted) { + FATFS *fs; + DWORD fre_clust, fre_sect, tot_sect; + char path[32]; + snprintf(path, sizeof(path), "%s", s_mount_point); + + FRESULT res = f_getfree(path, &fre_clust, &fs); + if (res == FR_OK) { + tot_sect = (fs->n_fatent - 2) * fs->csize; + fre_sect = fre_clust * fs->csize; + + if (total_bytes) { + *total_bytes = (uint64_t)tot_sect * 512; + } + if (free_bytes) { + *free_bytes = (uint64_t)fre_sect * 512; + } + result = ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to get free space: %d", res); + result = ESP_FAIL; + } } - FATFS *fs; - DWORD fre_clust, fre_sect, tot_sect; - char path[32]; - snprintf(path, sizeof(path), "%s", s_mount_point); - - FRESULT res = f_getfree(path, &fre_clust, &fs); - if (res != FR_OK) { - ESP_LOGE(TAG, "Failed to get free space: %d", res); - return ESP_FAIL; - } - - tot_sect = (fs->n_fatent - 2) * fs->csize; - fre_sect = fre_clust * fs->csize; - - if (total_bytes) { - *total_bytes = (uint64_t)tot_sect * 512; - } - if (free_bytes) { - *free_bytes = (uint64_t)fre_sect * 512; - } - - return ESP_OK; + return result; } esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + int flags = O_WRONLY | O_CREAT; + if (append) { + flags |= O_APPEND; + } else { + flags |= O_TRUNC; + } + int fd = open(full_path, flags, 0644); + if (fd >= 0) { + ssize_t written = write(fd, data, len); + close(fd); + + if (written >= 0 && (size_t)written == len) { + ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path); + result = ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len); + result = ESP_FAIL; + } + } else { + ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path); + result = ESP_FAIL; + } } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - int flags = O_WRONLY | O_CREAT; - if (append) { - flags |= O_APPEND; - } else { - flags |= O_TRUNC; - } - int fd = open(full_path, flags, 0644); - if (fd < 0) { - ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path); - return ESP_FAIL; - } - - ssize_t written = write(fd, data, len); - close(fd); - - if (written < 0 || (size_t)written != len) { - ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len); - return ESP_FAIL; - } - - ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path); - return ESP_OK; + return result; } esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + FILE *f = fopen(full_path, "r"); + if (f != NULL) { + size_t read = fread(data, 1, len, f); + fclose(f); + + if (bytes_read) { + *bytes_read = read; + } + + ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path); + result = ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path); + result = ESP_FAIL; + } } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - FILE *f = fopen(full_path, "r"); - if (f == NULL) { - ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path); - return ESP_FAIL; - } - - size_t read = fread(data, 1, len, f); - fclose(f); - - if (bytes_read) { - *bytes_read = read; - } - - ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path); - return ESP_OK; + return result; } esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) { - if (!s_sd_card_mounted || size_bytes == NULL) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted && size_bytes != NULL) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + struct stat st; + if (stat(full_path, &st) == 0) { + if (S_ISREG(st.st_mode)) { + *size_bytes = (size_t)st.st_size; + result = ESP_OK; + } else { + result = ESP_ERR_INVALID_ARG; + } + } else { + result = ESP_FAIL; + } } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - struct stat st; - if (stat(full_path, &st) != 0) { - return ESP_FAIL; - } - if (!S_ISREG(st.st_mode)) { - return ESP_ERR_INVALID_ARG; - } - *size_bytes = (size_t)st.st_size; - return ESP_OK; + return result; } esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, size_t len, size_t *bytes_read) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + FILE *f = fopen(full_path, "rb"); + if (f != NULL) { + if (fseek(f, (long)offset, SEEK_SET) == 0) { + size_t n = fread(data, 1, len, f); + fclose(f); + if (bytes_read) { + *bytes_read = n; + } + result = ESP_OK; + } else { + fclose(f); + result = ESP_FAIL; + } + } else { + result = ESP_FAIL; + } } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - FILE *f = fopen(full_path, "rb"); - if (f == NULL) { - return ESP_FAIL; - } - if (fseek(f, (long)offset, SEEK_SET) != 0) { - fclose(f); - return ESP_FAIL; - } - size_t n = fread(data, 1, len, f); - fclose(f); - if (bytes_read) { - *bytes_read = n; - } - return ESP_OK; + return result; } bool sd_card_file_exists(const char *filename) { - if (!s_sd_card_mounted) { - return false; + bool result = false; + + if (s_sd_card_mounted) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + struct stat st; + result = (stat(full_path, &st) == 0); } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - struct stat st; - return (stat(full_path, &st) == 0); + return result; } esp_err_t sd_card_list_dir(const char *path) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; - } + esp_err_t result = ESP_ERR_INVALID_STATE; - char full_path[128]; - if (!path || path[0] == '\0') { - snprintf(full_path, sizeof(full_path), "%s", s_mount_point); - } else { - snprintf(full_path, sizeof(full_path), "%s/%s", s_mount_point, - (path[0] == '/') ? path + 1 : path); - } - - DIR *d = opendir(full_path); - if (!d) { - return ESP_FAIL; - } - - struct dirent *e; - while ((e = readdir(d)) != NULL) { - if (e->d_name[0] == '.') { - continue; - } - char entry_path[384]; /* full_path(128) + "/" + d_name(255) */ - int n = snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, e->d_name); - if (n < 0 || n >= (int)sizeof(entry_path)) { - continue; /* path too long, skip */ - } - struct stat st; - if (stat(entry_path, &st) == 0) { - if (S_ISDIR(st.st_mode)) { - printf(" %-32s \n", e->d_name); - } else { - printf(" %-32s %10zu bytes\n", e->d_name, (size_t)st.st_size); - } + if (s_sd_card_mounted) { + char full_path[128]; + if (!path || path[0] == '\0') { + snprintf(full_path, sizeof(full_path), "%s", s_mount_point); } else { - printf(" %-32s ?\n", e->d_name); + snprintf(full_path, sizeof(full_path), "%s/%s", s_mount_point, + (path[0] == '/') ? path + 1 : path); + } + + DIR *d = opendir(full_path); + if (d != NULL) { + struct dirent *e; + while ((e = readdir(d)) != NULL) { + if (e->d_name[0] == '.') { + continue; + } + char entry_path[384]; /* full_path(128) + "/" + d_name(255) */ + int n = snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, e->d_name); + if (n < 0 || n >= (int)sizeof(entry_path)) { + continue; /* path too long, skip */ + } + struct stat st; + if (stat(entry_path, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + printf(" %-32s \n", e->d_name); + } else { + char hr_size[16]; + fmt_size_human((size_t)st.st_size, hr_size, sizeof(hr_size)); + printf(" %-32s %10zu bytes (%s)\n", e->d_name, (size_t)st.st_size, hr_size); + } + } else { + printf(" %-32s ?\n", e->d_name); + } + } + closedir(d); + result = ESP_OK; + } else { + result = ESP_FAIL; } } - closedir(d); - return ESP_OK; + + return result; } esp_err_t sd_card_delete_file(const char *filename) { - if (!s_sd_card_mounted) { - return ESP_ERR_INVALID_STATE; + esp_err_t result = ESP_ERR_INVALID_STATE; + + if (s_sd_card_mounted) { + char full_path[128]; + snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, + (filename[0] == '/') ? "" : "/", filename); + + if (unlink(full_path) == 0) { + ESP_LOGI(TAG, "Deleted file: %s", full_path); + result = ESP_OK; + } else { + ESP_LOGE(TAG, "Failed to delete file: %s", full_path); + result = ESP_FAIL; + } } - char full_path[128]; - snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, - (filename[0] == '/') ? "" : "/", filename); - - if (unlink(full_path) != 0) { - ESP_LOGE(TAG, "Failed to delete file: %s", full_path); - return ESP_FAIL; - } - - ESP_LOGI(TAG, "Deleted file: %s", full_path); - return ESP_OK; + return result; } diff --git a/components/sd_card/sd_card.h b/components/sd_card/sd_card.h index ed80619..7b8bfd2 100644 --- a/components/sd_card/sd_card.h +++ b/components/sd_card/sd_card.h @@ -40,21 +40,21 @@ /** * @brief Initialize SD card using SDIO interface - * + * * @return ESP_OK on success, error code otherwise */ esp_err_t sd_card_init(void); /** * @brief Deinitialize SD card - * + * * @return ESP_OK on success */ esp_err_t sd_card_deinit(void); /** * @brief Check if SD card is mounted and ready - * + * * @return true if SD card is ready, false otherwise */ bool sd_card_is_ready(void); @@ -81,7 +81,7 @@ int sd_card_cd_get_level(void); /** * @brief Get SD card capacity information - * + * * @param total_bytes Output parameter for total capacity in bytes * @param free_bytes Output parameter for free space in bytes * @return ESP_OK on success @@ -90,7 +90,7 @@ esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes); /** * @brief Write data to a file on the SD card - * + * * @param filename File path (e.g., "/sdcard/telemetry.json") * @param data Data to write * @param len Length of data in bytes @@ -101,7 +101,7 @@ esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, /** * @brief Read data from a file on the SD card - * + * * @param filename File path * @param data Buffer to read into * @param len Maximum length to read @@ -133,7 +133,7 @@ esp_err_t sd_card_read_file_at(const char *filename, size_t offset, void *data, /** * @brief Check if a file exists on the SD card - * + * * @param filename File path * @return true if file exists, false otherwise */ @@ -149,7 +149,7 @@ esp_err_t sd_card_list_dir(const char *path); /** * @brief Delete a file from the SD card - * + * * @param filename File path * @return ESP_OK on success */ diff --git a/components/wifi_controller/wifi_controller.c b/components/wifi_controller/wifi_controller.c index ce74377..e8d2583 100644 --- a/components/wifi_controller/wifi_controller.c +++ b/components/wifi_controller/wifi_controller.c @@ -33,10 +33,12 @@ #include "wifi_controller.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/semphr.h" #include "esp_log.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_netif.h" +#include "esp_timer.h" #include "inttypes.h" #include #include "wifi_cfg.h" @@ -55,7 +57,16 @@ #include "sd_card.h" #define FIWI_TELEMETRY_FILE "fiwi-telemetry" -#define FIWI_TELEMETRY_JSON_BUF_SIZE 4096 +#define TELEMETRY_JSON_BUF_SIZE 4096 + +/* SD card write optimization: + * - 16KB batch size: Optimal for high-frequency telemetry (4KB-64KB range, sweet spot 4-16KB) + * - Multiple of 512 bytes (SD sector size): 16KB = 32 sectors, ensures good alignment + * - Balances performance vs RAM usage for embedded systems + * - For ESP32: 16KB is reasonable RAM usage and provides excellent write efficiency + */ +#define TELEMETRY_BATCH_SIZE (16 * 1024) /* 16KB batch buffer for SD card writes */ +#define TELEMETRY_BATCH_FLUSH_INTERVAL_MS 5000 /* Flush batch every 5 seconds or when 80% full */ static const char *TAG = "WIFI_CTL"; @@ -66,6 +77,27 @@ static uint8_t s_monitor_channel_staging = 6; static bool s_monitor_enabled = false; static uint32_t s_monitor_frame_count = 0; static TaskHandle_t s_monitor_stats_task_handle = NULL; +static bool s_monitor_debug = false; /* Debug mode: enable serial logging */ + +/* Telemetry rate tracking (Welford algorithm) */ +typedef struct { + uint64_t total_bytes; /* Total bytes generated/written */ + double mean_rate_bps; /* Mean rate in bytes per second */ + double m2; /* Sum of squares of differences (for variance) */ + uint32_t sample_count; /* Number of samples */ + uint64_t last_update_ms; /* Last update timestamp */ +} rate_tracker_t; + +static rate_tracker_t s_telemetry_gen_rate = {0}; +static rate_tracker_t s_sd_write_rate = {0}; + +/* Batch buffer for telemetry (shared between task and flush function) */ +static char s_batch_buf[TELEMETRY_BATCH_SIZE]; +static size_t s_batch_offset = 0; +static SemaphoreHandle_t s_batch_mutex = NULL; + +/* Static flush buffer to avoid large stack allocations (16KB) */ +static char s_flush_buf[TELEMETRY_BATCH_SIZE]; // --- Event Handler --- static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { @@ -81,10 +113,12 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t e // ... [Log Collapse / Monitor Callback Logic] ... static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) { - gps_timestamp_t ts = gps_get_timestamp(); - int64_t now_ms = ts.gps_us / 1000; - ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d", - now_ms, ts.synced ? 1 : 0, nav_duration_us, rssi, retry); + if (s_monitor_debug) { /* Only log in debug mode */ + gps_timestamp_t ts = gps_get_timestamp(); + int64_t now_ms = ts.gps_us / 1000; + ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d", + now_ms, ts.synced ? 1 : 0, nav_duration_us, rssi, retry); + } } static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) { @@ -99,57 +133,253 @@ static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t mcs_telemetry_process_frame(frame, NULL); } +/** + * @brief Update rate tracker using Welford's online algorithm + * @param tracker Rate tracker to update + * @param bytes Bytes generated/written in this interval + * @param interval_ms Time interval in milliseconds + */ +static void update_rate_tracker(rate_tracker_t *tracker, size_t bytes, uint32_t interval_ms) { + if (interval_ms > 0) { + double rate_bps = (bytes * 1000.0) / interval_ms; /* Convert to bytes per second */ + tracker->total_bytes += bytes; + tracker->sample_count++; + + /* Welford's online algorithm for running mean and variance */ + double delta = rate_bps - tracker->mean_rate_bps; + tracker->mean_rate_bps += delta / tracker->sample_count; + double delta2 = rate_bps - tracker->mean_rate_bps; + tracker->m2 += delta * delta2; + } +} + +/** + * @brief Flush pending telemetry batch to SD card + */ +static void flush_telemetry_batch(void) { + size_t offset = 0; + + if (s_batch_mutex != NULL) { + if (xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + offset = s_batch_offset; + xSemaphoreGive(s_batch_mutex); + } + + if (offset > 0 && sd_card_is_ready()) { + size_t flush_size = 0; + + if (xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + /* Copy batch to static buffer for safe write */ + memcpy(s_flush_buf, s_batch_buf, offset); + flush_size = offset; + s_batch_offset = 0; + xSemaphoreGive(s_batch_mutex); + } + + if (flush_size > 0) { + if (sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true) == ESP_OK) { + uint64_t now_ms = esp_timer_get_time() / 1000; + uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms); + if (write_interval_ms > 0) { + update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms); + s_sd_write_rate.last_update_ms = now_ms; + } + if (s_monitor_debug) { + ESP_LOGD(TAG, "Batch flushed on stop: %zu bytes", flush_size); + } + } + } + } + } +} + static void monitor_stats_task(void *arg) { (void)arg; - static char json_buf[FIWI_TELEMETRY_JSON_BUF_SIZE]; + static char json_buf[TELEMETRY_JSON_BUF_SIZE]; uint32_t flush_count = 0; uint32_t last_frame_count = 0; - while (1) { - vTaskDelay(pdMS_TO_TICKS(10000)); + uint64_t last_batch_flush_ms = 0; + uint64_t last_stats_log_ms = 0; + bool task_running = true; + + /* Initialize rate trackers */ + uint64_t start_ms = esp_timer_get_time() / 1000; + s_telemetry_gen_rate.last_update_ms = start_ms; + s_sd_write_rate.last_update_ms = start_ms; + last_batch_flush_ms = start_ms; + last_stats_log_ms = start_ms; + + /* Batch mutex should already be created by switch_to_monitor */ + if (s_batch_mutex == NULL) { + ESP_LOGE(TAG, "Batch mutex not initialized"); + task_running = false; + } + + while (task_running && s_batch_mutex != NULL) { + vTaskDelay(pdMS_TO_TICKS(1000)); /* Check every 1 second for batching */ + uint64_t now_ms = esp_timer_get_time() / 1000; + if (s_monitor_frame_count == last_frame_count) { status_led_set_capture_active(false); } last_frame_count = s_monitor_frame_count; - wifi_collapse_stats_t stats; - if (wifi_monitor_get_stats(&stats) == ESP_OK) { - ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---", - (unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav); - if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️"); - } - /* Write MCS telemetry to fiwi-telemetry on SD card (default on monitor start) */ - if (sd_card_is_ready() && mcs_telemetry_to_json(json_buf, sizeof(json_buf), "esp32") == ESP_OK) { + + /* Generate telemetry JSON (regardless of SD card status) */ + if (mcs_telemetry_to_json(json_buf, sizeof(json_buf), "esp32") == ESP_OK) { size_t len = strlen(json_buf); if (len > 0) { json_buf[len] = '\n'; /* NDJSON: one object per line */ - if (sd_card_write_file(FIWI_TELEMETRY_FILE, json_buf, len + 1, true) == ESP_OK) { - flush_count++; - ESP_LOGD(TAG, "fiwi-telemetry flushed (#%lu)", (unsigned long)flush_count); + size_t total_len = len + 1; + + /* Update telemetry generation rate (always track generation) */ + uint32_t interval_ms = (uint32_t)(now_ms - s_telemetry_gen_rate.last_update_ms); + if (interval_ms > 0) { + update_rate_tracker(&s_telemetry_gen_rate, total_len, interval_ms); + s_telemetry_gen_rate.last_update_ms = now_ms; + } + + /* Only write to SD card if ready */ + bool card_ready = sd_card_is_ready(); + if (!card_ready && s_monitor_debug) { + /* Log when card is not ready (only in debug mode to avoid spam) */ + static uint64_t last_not_ready_log_ms = 0; + uint64_t now_ms_check = esp_timer_get_time() / 1000; + if (now_ms_check - last_not_ready_log_ms > 10000) { /* Log at most every 10 seconds */ + ESP_LOGW(TAG, "SD card not ready, telemetry not being written"); + last_not_ready_log_ms = now_ms_check; + } + } + if (card_ready) { + /* Add to batch buffer (with mutex protection) */ + if (s_batch_mutex && xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) { + if (s_batch_offset + total_len < TELEMETRY_BATCH_SIZE) { + memcpy(s_batch_buf + s_batch_offset, json_buf, total_len); + s_batch_offset += total_len; + xSemaphoreGive(s_batch_mutex); + } else { + /* Batch buffer full, flush immediately */ + size_t flush_size = s_batch_offset; + if (flush_size > 0) { + memcpy(s_flush_buf, s_batch_buf, flush_size); + s_batch_offset = 0; + xSemaphoreGive(s_batch_mutex); + + esp_err_t write_err = sd_card_write_file(FIWI_TELEMETRY_FILE, s_flush_buf, flush_size, true); + if (write_err == ESP_OK) { + flush_count++; + uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms); + if (write_interval_ms > 0) { + update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms); + s_sd_write_rate.last_update_ms = now_ms; + } + if (s_monitor_debug) { + ESP_LOGD(TAG, "Batch flushed: %zu bytes (#%lu)", flush_size, (unsigned long)flush_count); + } + } else { + /* Log write failures - this helps diagnose SD card issues */ + ESP_LOGE(TAG, "Failed to write telemetry batch (buffer full): %s (%zu bytes)", esp_err_to_name(write_err), flush_size); + } + } else { + xSemaphoreGive(s_batch_mutex); + } + + /* Add current JSON to fresh batch */ + if (total_len < TELEMETRY_BATCH_SIZE) { + if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) { + memcpy(s_batch_buf, json_buf, total_len); + s_batch_offset = total_len; + xSemaphoreGive(s_batch_mutex); + } + } + } + } } } } + + /* Flush batch periodically or if buffer is getting full */ + size_t current_offset = 0; + if (s_batch_mutex && xSemaphoreTake(s_batch_mutex, 0) == pdTRUE) { + current_offset = s_batch_offset; + xSemaphoreGive(s_batch_mutex); + } + + uint32_t time_since_flush_ms = (uint32_t)(now_ms - last_batch_flush_ms); + bool should_flush = (time_since_flush_ms >= TELEMETRY_BATCH_FLUSH_INTERVAL_MS) || + (current_offset > TELEMETRY_BATCH_SIZE * 0.8); /* Flush at 80% full */ + + 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) { + flush_count++; + uint32_t write_interval_ms = (uint32_t)(now_ms - s_sd_write_rate.last_update_ms); + if (write_interval_ms > 0) { + update_rate_tracker(&s_sd_write_rate, flush_size, write_interval_ms); + s_sd_write_rate.last_update_ms = now_ms; + } + if (s_monitor_debug) { + ESP_LOGD(TAG, "Batch flushed: %zu bytes (#%lu, gen: %.1f B/s, write: %.1f B/s)", + flush_size, (unsigned long)flush_count, + s_telemetry_gen_rate.mean_rate_bps, s_sd_write_rate.mean_rate_bps); + } + last_batch_flush_ms = now_ms; + } else { + /* Log write failures - this helps diagnose SD card issues */ + ESP_LOGE(TAG, "Failed to write telemetry batch: %s (%zu bytes)", esp_err_to_name(write_err), flush_size); + } + } + } + + /* Log stats periodically (only in debug mode) */ + uint32_t time_since_log_ms = (uint32_t)(now_ms - last_stats_log_ms); + if (s_monitor_debug && time_since_log_ms >= 10000) { + wifi_collapse_stats_t stats; + if (wifi_monitor_get_stats(&stats) == ESP_OK) { + ESP_LOGD("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---", + (unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav); + ESP_LOGD("MONITOR", "Telemetry: gen=%.1f B/s (total=%llu), write=%.1f B/s (total=%llu)", + s_telemetry_gen_rate.mean_rate_bps, (unsigned long long)s_telemetry_gen_rate.total_bytes, + s_sd_write_rate.mean_rate_bps, (unsigned long long)s_sd_write_rate.total_bytes); + if (wifi_monitor_is_collapsed()) { + ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️"); + } + } + last_stats_log_ms = now_ms; + } } } // --- Helper to apply IP settings --- static void apply_ip_settings(void) { esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - if (!netif) return; - if (wifi_cfg_get_dhcp()) { - esp_netif_dhcpc_start(netif); - } else { - esp_netif_dhcpc_stop(netif); + if (netif != NULL) { + if (wifi_cfg_get_dhcp()) { + esp_netif_dhcpc_start(netif); + } else { + esp_netif_dhcpc_stop(netif); - char ip[16], mask[16], gw[16]; - if (wifi_cfg_get_ipv4(ip, mask, gw)) { - esp_netif_ip_info_t info = {0}; - // API Fix: esp_ip4addr_aton returns uint32_t - info.ip.addr = esp_ip4addr_aton(ip); - info.netmask.addr = esp_ip4addr_aton(mask); - info.gw.addr = esp_ip4addr_aton(gw); + char ip[16], mask[16], gw[16]; + if (wifi_cfg_get_ipv4(ip, mask, gw)) { + esp_netif_ip_info_t info = {0}; + // API Fix: esp_ip4addr_aton returns uint32_t + info.ip.addr = esp_ip4addr_aton(ip); + info.netmask.addr = esp_ip4addr_aton(mask); + info.gw.addr = esp_ip4addr_aton(gw); - esp_netif_set_ip_info(netif, &info); - ESP_LOGI(TAG, "Static IP applied: %s", ip); + esp_netif_set_ip_info(netif, &info); + ESP_LOGI(TAG, "Static IP applied: %s", ip); + } } } } @@ -201,89 +431,124 @@ void wifi_ctl_init(void) { // --- Mode Control (Core) --- esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) { + esp_err_t result = ESP_OK; + if (channel == 0) channel = s_monitor_channel_staging; if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) { ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel); - return ESP_OK; - } + result = ESP_OK; + } else { + ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel); - ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel); - - iperf_stop(); - vTaskDelay(pdMS_TO_TICKS(500)); + iperf_stop(); + vTaskDelay(pdMS_TO_TICKS(500)); #ifdef CONFIG_ESP_WIFI_CSI_ENABLED - csi_mgr_disable(); + csi_mgr_disable(); #endif - esp_wifi_disconnect(); - esp_wifi_stop(); - vTaskDelay(pdMS_TO_TICKS(500)); + esp_wifi_disconnect(); + esp_wifi_stop(); + vTaskDelay(pdMS_TO_TICKS(500)); - esp_wifi_set_mode(WIFI_MODE_NULL); - status_led_set_capture_active(false); - if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) { - ESP_LOGE(TAG, "Failed to init monitor mode"); - return ESP_FAIL; + esp_wifi_set_mode(WIFI_MODE_NULL); + status_led_set_capture_active(false); + if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) { + ESP_LOGE(TAG, "Failed to init monitor mode"); + result = ESP_FAIL; + } else { + /* MCS telemetry -> fiwi-telemetry on SD (default on monitor start) */ + if (mcs_telemetry_init(NULL) != ESP_OK) { + ESP_LOGW(TAG, "MCS telemetry init failed"); + } else if (mcs_telemetry_start() != ESP_OK) { + ESP_LOGW(TAG, "MCS telemetry start failed"); + } + + esp_wifi_set_bandwidth(WIFI_IF_STA, bw); + + if (wifi_monitor_start() != ESP_OK) { + ESP_LOGE(TAG, "Failed to start monitor mode"); + result = ESP_FAIL; + } else { + s_monitor_enabled = true; + s_current_mode = WIFI_CTL_MODE_MONITOR; + s_monitor_channel_active = channel; + status_led_set_state(LED_STATE_MONITORING); + + /* Reset rate trackers when starting monitor */ + uint64_t start_ms = esp_timer_get_time() / 1000; + memset(&s_telemetry_gen_rate, 0, sizeof(rate_tracker_t)); + memset(&s_sd_write_rate, 0, sizeof(rate_tracker_t)); + s_telemetry_gen_rate.last_update_ms = start_ms; + s_sd_write_rate.last_update_ms = start_ms; + + /* Initialize batch buffer and mutex */ + if (s_batch_mutex == NULL) { + s_batch_mutex = xSemaphoreCreateMutex(); + if (s_batch_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create batch mutex"); + result = ESP_FAIL; + } + } + if (result == ESP_OK && s_batch_mutex != NULL) { + if (xSemaphoreTake(s_batch_mutex, portMAX_DELAY) == pdTRUE) { + s_batch_offset = 0; + xSemaphoreGive(s_batch_mutex); + } + } + + if (result == ESP_OK && s_monitor_stats_task_handle == NULL) { + /* Task stack size: 12KB needed for: + * - 4KB static json_buf (TELEMETRY_JSON_BUF_SIZE) + * - Local variables and function call frames + * - ESP-IDF API call overhead (esp_timer_get_time, sd_card_write_file, etc.) + * - Mutex operations and nested function calls + */ + xTaskCreate(monitor_stats_task, "monitor_stats", 12 * 1024, NULL, 5, &s_monitor_stats_task_handle); + } + } + } } - /* MCS telemetry -> fiwi-telemetry on SD (default on monitor start) */ - if (mcs_telemetry_init(NULL) != ESP_OK) { - ESP_LOGW(TAG, "MCS telemetry init failed"); - } else if (mcs_telemetry_start() != ESP_OK) { - ESP_LOGW(TAG, "MCS telemetry start failed"); - } - - esp_wifi_set_bandwidth(WIFI_IF_STA, bw); - - if (wifi_monitor_start() != ESP_OK) { - ESP_LOGE(TAG, "Failed to start monitor mode"); - return ESP_FAIL; - } - - s_monitor_enabled = true; - s_current_mode = WIFI_CTL_MODE_MONITOR; - s_monitor_channel_active = channel; - status_led_set_state(LED_STATE_MONITORING); - - if (s_monitor_stats_task_handle == NULL) { - xTaskCreate(monitor_stats_task, "monitor_stats", 4096, NULL, 5, &s_monitor_stats_task_handle); - } - - return ESP_OK; + return result; } esp_err_t wifi_ctl_switch_to_sta(void) { + esp_err_t result = ESP_OK; + if (s_current_mode == WIFI_CTL_MODE_STA) { ESP_LOGI(TAG, "Already in STA mode"); - return ESP_OK; - } + result = ESP_OK; + } else { + ESP_LOGI(TAG, "Switching to STA MODE"); - ESP_LOGI(TAG, "Switching to STA MODE"); + if (s_monitor_stats_task_handle != NULL) { + vTaskDelete(s_monitor_stats_task_handle); + s_monitor_stats_task_handle = NULL; + } - if (s_monitor_stats_task_handle != NULL) { - vTaskDelete(s_monitor_stats_task_handle); - s_monitor_stats_task_handle = NULL; - } + if (s_monitor_enabled) { + status_led_set_capture_active(false); + /* Flush any pending telemetry before stopping */ + flush_telemetry_batch(); + mcs_telemetry_stop(); + wifi_monitor_stop(); + s_monitor_enabled = false; + vTaskDelay(pdMS_TO_TICKS(500)); + } - if (s_monitor_enabled) { - status_led_set_capture_active(false); - mcs_telemetry_stop(); - wifi_monitor_stop(); - s_monitor_enabled = false; + esp_wifi_set_mode(WIFI_MODE_STA); vTaskDelay(pdMS_TO_TICKS(500)); + + esp_wifi_start(); + esp_wifi_connect(); + + s_current_mode = WIFI_CTL_MODE_STA; + status_led_set_state(LED_STATE_WAITING); + result = ESP_OK; } - esp_wifi_set_mode(WIFI_MODE_STA); - vTaskDelay(pdMS_TO_TICKS(500)); - - esp_wifi_start(); - esp_wifi_connect(); - - s_current_mode = WIFI_CTL_MODE_STA; - status_led_set_state(LED_STATE_WAITING); - - return ESP_OK; + return result; } // --- Wrappers for cmd_monitor.c --- @@ -330,6 +595,35 @@ void wifi_ctl_status(void) { if (s_current_mode == WIFI_CTL_MODE_MONITOR) { printf(" Channel: %d\n", s_monitor_channel_active); printf(" Frames: %lu\n", (unsigned long)s_monitor_frame_count); + + /* Show telemetry rate statistics */ + uint64_t gen_bytes = 0, write_bytes = 0; + double gen_rate = 0.0, write_rate = 0.0; + wifi_ctl_get_telemetry_gen_stats(&gen_bytes, &gen_rate); + wifi_ctl_get_sd_write_stats(&write_bytes, &write_rate); + + /* Get bytes pending in batch buffer (awaiting flush) */ + size_t bytes_in_batch = 0; + if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + bytes_in_batch = s_batch_offset; + xSemaphoreGive(s_batch_mutex); + } + + printf(" Telemetry (SD Card = %s):\n", sd_card_is_ready() ? "ready" : "not ready"); + printf(" Generated: %llu bytes, %.1f B/s\n", (unsigned long long)gen_bytes, gen_rate); + printf(" Written: %llu bytes, %.1f B/s\n", (unsigned long long)write_bytes, write_rate); + if (gen_bytes > write_bytes) { + /* Account for bytes in batch buffer - they're not dropped, just pending */ + uint64_t pending = (uint64_t)bytes_in_batch; + uint64_t dropped = (gen_bytes > write_bytes + pending) ? (gen_bytes - write_bytes - pending) : 0; + if (dropped > 0) { + printf(" Dropped: %llu bytes (%.1f%%)\n", (unsigned long long)dropped, + (dropped * 100.0) / gen_bytes); + } + if (pending > 0) { + printf(" Pending: %zu bytes (in batch buffer)\n", bytes_in_batch); + } + } } printf(" Staging Ch: %d\n", s_monitor_channel_staging); } @@ -368,6 +662,75 @@ void wifi_ctl_param_clear(void) { wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; } int wifi_ctl_get_channel(void) { return s_monitor_channel_active; } +/** + * @brief Get telemetry generation rate statistics + * @param total_bytes Total bytes generated (output) + * @param rate_bps Mean generation rate in bytes per second (output) + * @return ESP_OK on success + */ +esp_err_t wifi_ctl_get_telemetry_gen_stats(uint64_t *total_bytes, double *rate_bps) { + esp_err_t result = ESP_OK; + + if (total_bytes == NULL || rate_bps == NULL) { + result = ESP_ERR_INVALID_ARG; + } else { + if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + *total_bytes = s_telemetry_gen_rate.total_bytes; + *rate_bps = s_telemetry_gen_rate.mean_rate_bps; + xSemaphoreGive(s_batch_mutex); + } else { + *total_bytes = s_telemetry_gen_rate.total_bytes; + *rate_bps = s_telemetry_gen_rate.mean_rate_bps; + } + result = ESP_OK; + } + + return result; +} + +/** + * @brief Get SD card write rate statistics + * @param total_bytes Total bytes written (output) + * @param rate_bps Mean write rate in bytes per second (output) + * @return ESP_OK on success + */ +esp_err_t wifi_ctl_get_sd_write_stats(uint64_t *total_bytes, double *rate_bps) { + esp_err_t result = ESP_OK; + + if (total_bytes == NULL || rate_bps == NULL) { + result = ESP_ERR_INVALID_ARG; + } else { + if (s_batch_mutex != NULL && xSemaphoreTake(s_batch_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + *total_bytes = s_sd_write_rate.total_bytes; + *rate_bps = s_sd_write_rate.mean_rate_bps; + xSemaphoreGive(s_batch_mutex); + } else { + *total_bytes = s_sd_write_rate.total_bytes; + *rate_bps = s_sd_write_rate.mean_rate_bps; + } + result = ESP_OK; + } + + return result; +} + +/** + * @brief Enable/disable monitor debug mode (serial logging) + * @param enable true to enable debug logging, false to disable + */ +void wifi_ctl_set_monitor_debug(bool enable) { + s_monitor_debug = enable; + wifi_monitor_set_debug(enable); /* Also set debug mode in wifi_monitor */ +} + +/** + * @brief Get monitor debug mode status + * @return true if debug mode enabled, false otherwise + */ +bool wifi_ctl_get_monitor_debug(void) { + return s_monitor_debug; +} + // --- Deprecated --- static void auto_monitor_task_func(void *arg) { uint8_t channel = (uint8_t)(uintptr_t)arg; diff --git a/components/wifi_controller/wifi_controller.h b/components/wifi_controller/wifi_controller.h index 47f2cc8..8a4d6b6 100644 --- a/components/wifi_controller/wifi_controller.h +++ b/components/wifi_controller/wifi_controller.h @@ -74,6 +74,14 @@ void wifi_ctl_param_clear(void); wifi_ctl_mode_t wifi_ctl_get_mode(void); int wifi_ctl_get_channel(void); +// Telemetry rate statistics +esp_err_t wifi_ctl_get_telemetry_gen_stats(uint64_t *total_bytes, double *rate_bps); +esp_err_t wifi_ctl_get_sd_write_stats(uint64_t *total_bytes, double *rate_bps); + +// Debug mode control +void wifi_ctl_set_monitor_debug(bool enable); +bool wifi_ctl_get_monitor_debug(void); + // Deprecated / Compatibility void wifi_ctl_auto_monitor_start(uint8_t channel); diff --git a/components/wifi_monitor/wifi_monitor.c b/components/wifi_monitor/wifi_monitor.c index cd9c12a..4465bf5 100644 --- a/components/wifi_monitor/wifi_monitor.c +++ b/components/wifi_monitor/wifi_monitor.c @@ -56,10 +56,97 @@ uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mism // Logging control 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 // Forward declarations static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type); +/** + * @brief Parse HT/VHT/HE headers to extract PHY parameters + * @param payload Raw frame payload + * @param len Total frame length + * @param mac_hdr_len Length of MAC header (24 or 30 bytes) + * @param frame_info Frame info structure to populate + */ +static void wifi_parse_phy_headers(const uint8_t *payload, uint16_t len, uint16_t mac_hdr_len, wifi_frame_info_t *frame_info) { + if (len < mac_hdr_len + 4) { + return; // Not enough data for any PHY headers + } + + uint16_t offset = mac_hdr_len; + + // Check for QoS Control field (4 bytes) - present in QoS data frames + bool has_qos = (frame_info->type == FRAME_TYPE_DATA && + (frame_info->subtype == DATA_QOS_DATA || + frame_info->subtype == DATA_QOS_DATA_CF_ACK || + frame_info->subtype == DATA_QOS_DATA_CF_POLL || + frame_info->subtype == DATA_QOS_DATA_CF_ACK_POLL || + frame_info->subtype == DATA_QOS_NULL || + frame_info->subtype == DATA_QOS_CF_POLL || + frame_info->subtype == DATA_QOS_CF_ACK_POLL)); + + if (has_qos && len >= offset + 4) { + offset += 4; // Skip QoS Control field + } + + // Check for HT Control field (4 bytes) - present if Order bit set + // HT Control field format (IEEE 802.11-2016): + // Bit 0-3: Control ID (0=HT, 1-3=VHT, 4+=HE) + // For VHT/HE: Additional fields for MCS, bandwidth, SGI, NSS + bool has_ht_ctrl = frame_info->order; + if (has_ht_ctrl && len >= offset + 4) { + // HT Control field is little-endian + uint32_t ht_ctrl = payload[offset] | (payload[offset + 1] << 8) | + (payload[offset + 2] << 16) | (payload[offset + 3] << 24); + + // Control ID is in bits 0-3 + uint8_t ctrl_id = ht_ctrl & 0x0F; + + if (ctrl_id == 0) { + // HT Control (802.11n) - MCS info might be elsewhere + 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.) + } else if (ctrl_id >= 1 && ctrl_id <= 3) { + // VHT Control (802.11ac) - bits layout varies by variant + frame_info->sig_mode = 3; // VHT + + // VHT Control variant 1: MCS in bits 4-7, bandwidth in 8-9, SGI in 10, NSS in 12-14 + if (ctrl_id == 1) { + frame_info->mcs = (ht_ctrl >> 4) & 0x0F; + frame_info->bandwidth = (ht_ctrl >> 8) & 0x03; + frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0; + uint8_t nss = (ht_ctrl >> 12) & 0x07; + frame_info->spatial_streams = (nss == 0) ? 1 : nss; // NSS encoding varies + } + } else if (ctrl_id >= 4) { + // HE Control (802.11ax) + frame_info->sig_mode = 4; // HE + + // HE Control: Similar structure to VHT + frame_info->mcs = (ht_ctrl >> 4) & 0x0F; + frame_info->bandwidth = (ht_ctrl >> 8) & 0x03; + frame_info->sgi = ((ht_ctrl >> 10) & 0x01) != 0; + uint8_t nss = (ht_ctrl >> 12) & 0x07; + frame_info->spatial_streams = (nss == 0) ? 1 : nss; + } + } + + // For HT frames without HT Control, try to infer from rate/MCS + // MCS index encoding: MCS 0-7 = 1 SS, 8-15 = 2 SS, 16-23 = 3 SS, 24-31 = 4 SS + if (frame_info->sig_mode == 1 && frame_info->mcs > 0) { + if (frame_info->mcs < 8) { + frame_info->spatial_streams = 1; + } else if (frame_info->mcs < 16) { + frame_info->spatial_streams = 2; + } else if (frame_info->mcs < 24) { + frame_info->spatial_streams = 3; + } else { + frame_info->spatial_streams = 4; + } + } +} + /** * @brief Parse 802.11 MAC header */ @@ -103,10 +190,21 @@ esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info // Check for Address 4 (only present if To DS and From DS both set) frame_info->has_addr4 = frame_info->to_ds && frame_info->from_ds; + uint16_t mac_hdr_len = frame_info->has_addr4 ? 30 : 24; if (frame_info->has_addr4 && len >= 30) { memcpy(frame_info->addr4, &payload[24], 6); } + // Initialize PHY parameters + frame_info->spatial_streams = 1; // Default to 1 SS + frame_info->mcs = 0; + frame_info->sig_mode = 0; + frame_info->sgi = false; + frame_info->bandwidth = 0; + + // Parse HT/VHT/HE headers to extract PHY parameters + wifi_parse_phy_headers(payload, len, mac_hdr_len, frame_info); + frame_info->frame_len = len; return ESP_OK; @@ -135,23 +233,38 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) frame_info.timestamp = rx_ctrl->timestamp; // Extract PHY rate info from RX control - frame_info.mcs = 0; frame_info.rate = rx_ctrl->rate; // This is the rate index - frame_info.sig_mode = 0; - frame_info.sgi = false; - frame_info.bandwidth = 0; - // Estimate PHY rate from rate index (rough approximation) - static const uint32_t rate_table[] = { - 1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS) - 6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates - 65000, 130000, 195000, 260000 // Rough HT estimates - }; + // If MCS wasn't parsed from headers but we have HT/VHT/HE mode, try to extract from rate + 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 + } - if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) { - frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate]; + // Calculate PHY rate using parsed MCS/spatial streams if available + // Otherwise fall back to rate table estimation + if (frame_info.sig_mode > 0 && frame_info.mcs > 0 && frame_info.spatial_streams > 0) { + // Use parsed MCS/SS info - rate will be calculated accurately in mcs_telemetry + // For monitor stats, keep the parsed value (may be 0 if not calculated yet) + if (frame_info.phy_rate_kbps == 0) { + // Fallback estimate if not set by parsing + frame_info.phy_rate_kbps = 100000; // Default estimate + } } else { - frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default + // Estimate PHY rate from rate index (rough approximation for legacy frames) + static const uint32_t rate_table[] = { + 1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS) + 6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates + 65000, 130000, 195000, 260000 // Rough HT estimates + }; + + if (rx_ctrl->rate < sizeof(rate_table) / sizeof(rate_table[0])) { + frame_info.phy_rate_kbps = rate_table[rx_ctrl->rate]; + } else { + frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default + } } // Update statistics @@ -203,7 +316,7 @@ 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_mismatch_log_counter % log_every_n_mismatches) == 0) { + 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); @@ -227,18 +340,20 @@ 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) { - ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!"); + if (s_monitor_debug) { + 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); + } } } @@ -477,3 +592,19 @@ const char* wifi_frame_type_str(uint8_t type, uint8_t subtype) { return "UNKNOWN"; } + +/** + * @brief Enable/disable monitor debug mode (serial logging) + * @param enable true to enable debug logging, false to disable + */ +void wifi_monitor_set_debug(bool enable) { + s_monitor_debug = enable; +} + +/** + * @brief Get monitor debug mode status + * @return true if debug mode enabled, false otherwise + */ +bool wifi_monitor_get_debug(void) { + return s_monitor_debug; +} diff --git a/components/wifi_monitor/wifi_monitor.h b/components/wifi_monitor/wifi_monitor.h index e8317e5..d124689 100644 --- a/components/wifi_monitor/wifi_monitor.h +++ b/components/wifi_monitor/wifi_monitor.h @@ -144,37 +144,38 @@ typedef struct { bool more_data; bool protected_frame; bool order; - + // Duration/ID (NAV) uint16_t duration_id; - + // MAC Addresses uint8_t addr1[6]; // Receiver address uint8_t addr2[6]; // Transmitter address uint8_t addr3[6]; // Filtering address (BSSID/SA/DA) - + // Sequence Control uint16_t seq_ctrl; uint16_t fragment_num; uint16_t sequence_num; - + // Optional: Address 4 (if To DS and From DS both set) uint8_t addr4[6]; bool has_addr4; - + // RX Info int8_t rssi; uint8_t channel; uint32_t timestamp; - + // PHY rate info - uint8_t mcs; // MCS index (for HT/VHT frames) + uint8_t mcs; // MCS index (for HT/VHT/HE frames) uint8_t rate; // Legacy rate or rate index - uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT) + uint8_t sig_mode; // Signal mode (0=legacy, 1=HT, 3=VHT, 4=HE) + uint8_t spatial_streams; // Number of spatial streams (NSS) - 1-8 bool sgi; // Short Guard Interval - uint8_t bandwidth; // 0=20MHz, 1=40MHz, 2=80MHz + 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) - + // Frame size uint16_t frame_len; } wifi_frame_info_t; @@ -191,13 +192,13 @@ typedef struct { uint32_t ack_frames; uint32_t data_frames; uint32_t mgmt_frames; - + // Collapse indicators float retry_rate; // Percentage of retried frames uint16_t avg_nav; // Average NAV duration uint16_t max_nav; // Maximum NAV seen uint32_t collision_events; // Estimated collision count - + // Duration analysis uint32_t duration_mismatch_frames; // NAV >> expected uint32_t rate_fallback_frames; // PHY rate < 100 Mbps @@ -208,18 +209,18 @@ typedef struct { /** * @brief Callback function type for frame capture - * + * * @param frame_info Parsed frame information * @param payload Raw frame payload (starts with MAC header) * @param len Frame length */ -typedef void (*wifi_monitor_cb_t)(const wifi_frame_info_t *frame_info, - const uint8_t *payload, +typedef void (*wifi_monitor_cb_t)(const wifi_frame_info_t *frame_info, + const uint8_t *payload, uint16_t len); /** * @brief Initialize WiFi monitor mode - * + * * @param channel WiFi channel to monitor (1-14 for 2.4GHz, 36+ for 5GHz) * @param callback Callback function for captured frames * @return esp_err_t ESP_OK on success @@ -228,21 +229,21 @@ esp_err_t wifi_monitor_init(uint8_t channel, wifi_monitor_cb_t callback); /** * @brief Start WiFi monitoring - * + * * @return esp_err_t ESP_OK on success */ esp_err_t wifi_monitor_start(void); /** * @brief Stop WiFi monitoring - * + * * @return esp_err_t ESP_OK on success */ esp_err_t wifi_monitor_stop(void); /** * @brief Set WiFi channel for monitoring - * + * * @param channel WiFi channel (1-14 for 2.4GHz, 36+ for 5GHz) * @return esp_err_t ESP_OK on success */ @@ -250,7 +251,7 @@ esp_err_t wifi_monitor_set_channel(uint8_t channel); /** * @brief Parse 802.11 frame header - * + * * @param payload Raw frame data * @param len Frame length * @param frame_info Output: parsed frame information @@ -260,7 +261,7 @@ esp_err_t wifi_parse_frame(const uint8_t *payload, uint16_t len, wifi_frame_info /** * @brief Get WiFi collapse detection statistics - * + * * @param stats Output: statistics structure * @return esp_err_t ESP_OK on success */ @@ -273,20 +274,32 @@ void wifi_monitor_reset_stats(void); /** * @brief Check if current conditions indicate WiFi collapse - * + * * @return true if collapse is detected, false otherwise */ bool wifi_monitor_is_collapsed(void); /** * @brief Get string representation of frame type - * + * * @param type Frame type * @param subtype Frame subtype * @return const char* String description */ const char* wifi_frame_type_str(uint8_t type, uint8_t subtype); +/** + * @brief Enable/disable monitor debug mode (serial logging) + * @param enable true to enable debug logging, false to disable + */ +void wifi_monitor_set_debug(bool enable); + +/** + * @brief Get monitor debug mode status + * @return true if debug mode enabled, false otherwise + */ +bool wifi_monitor_get_debug(void); + #ifdef __cplusplus } #endif