Compare commits

...

2 Commits

Author SHA1 Message Date
Robert McMahon bb6bd568ce Add channel frequency display to monitor status
- Display channel number, frequency (MHz), width, and center frequency
- Format: 'Channel: 11 (2462 MHz), width: 20 MHz, center1: 2462 MHz'
- Supports both 2.4GHz (channels 1-14) and 5GHz (channels 36-177)
- Use PRIu32 format specifier for portable uint32_t formatting

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 10:50:16 -08:00
Robert McMahon 0311ad21a1 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 <cursoragent@cursor.com>
2026-02-10 10:05:13 -08:00
12 changed files with 1223 additions and 422 deletions

View File

@ -49,6 +49,11 @@ static struct {
struct arg_end *end; struct arg_end *end;
} channel_args; } channel_args;
static struct {
struct arg_str *enable;
struct arg_end *end;
} debug_args;
static void print_monitor_usage(void) { static void print_monitor_usage(void) {
printf("Usage: monitor <subcommand> [args]\n"); printf("Usage: monitor <subcommand> [args]\n");
printf("Subcommands:\n"); printf("Subcommands:\n");
@ -56,6 +61,7 @@ static void print_monitor_usage(void) {
printf(" stop Stop Monitor Mode\n"); printf(" stop Stop Monitor Mode\n");
printf(" status Show current status\n"); printf(" status Show current status\n");
printf(" channel <n> Switch channel (while running)\n"); printf(" channel <n> 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(" save Save current config to NVS\n");
printf(" reload Reload config from NVS\n"); printf(" reload Reload config from NVS\n");
printf(" clear Clear NVS config\n"); printf(" clear Clear NVS config\n");
@ -99,6 +105,40 @@ static int do_monitor_channel(int argc, char **argv) {
return 0; return 0;
} }
static int do_monitor_debug(int argc, char **argv) {
debug_args.enable = arg_str0(NULL, NULL, "<on|off>", "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) { static int cmd_monitor(int argc, char **argv) {
if (argc < 2) { if (argc < 2) {
print_monitor_usage(); 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], "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], "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) { if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) {
print_monitor_usage(); print_monitor_usage();
@ -131,6 +172,9 @@ void register_monitor_cmd(void) {
channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel"); channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel");
channel_args.end = arg_end(1); channel_args.end = arg_end(1);
debug_args.enable = arg_str0(NULL, NULL, "<on|off>", "Enable or disable debug logging");
debug_args.end = arg_end(1);
const esp_console_cmd_t cmd = { const esp_console_cmd_t cmd = {
.command = "monitor", .command = "monitor",
.help = "Monitor Mode: start, stop, channel, status", .help = "Monitor Mode: start, stop, channel, status",

View File

@ -109,6 +109,39 @@ static const uint32_t PHY_RATES_40MHZ_1SS_400NS[] = {
320300 // MCS 11 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 * @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}; mcs_sample_t sample = {0};
sample.timestamp_ms = esp_timer_get_time() / 1000; sample.timestamp_ms = esp_timer_get_time() / 1000;
sample.mcs = frame_info->mcs; 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.rssi = frame_info->rssi;
sample.channel = frame_info->channel; sample.channel = frame_info->channel;
sample.bandwidth = (frame_info->bandwidth == 0) ? MCS_BW_20MHZ : 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.frame_len = frame_info->frame_len;
sample.is_retry = frame_info->retry; sample.is_retry = frame_info->retry;
sample.sig_mode = frame_info->sig_mode; 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 // Calculate PHY rate if we have MCS info
if (sample.mcs <= MCS_TELEMETRY_MAX_MCS && sample.ss >= 1 && sample.ss <= MCS_TELEMETRY_MAX_SS) { 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); 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); mcs_update_device_telemetry(dev_idx, &sample);
s_stats.total_frames_captured++; 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; 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; rate_table = sgi ? PHY_RATES_20MHZ_1SS_400NS : PHY_RATES_20MHZ_1SS_800NS;
} else if (bandwidth == MCS_BW_40MHZ) { } else if (bandwidth == MCS_BW_40MHZ) {
rate_table = sgi ? PHY_RATES_40MHZ_1SS_400NS : PHY_RATES_40MHZ_1SS_800NS; 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 { } else {
return 0; return 0;
} }

View File

@ -67,11 +67,12 @@ extern "C" {
#define MCS_TELEMETRY_WINDOW_MS 1000 #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 { typedef enum {
MCS_BW_20MHZ = 0, MCS_BW_20MHZ = 0,
MCS_BW_40MHZ = 1 MCS_BW_40MHZ = 1,
MCS_BW_80MHZ = 2
} mcs_bandwidth_t; } mcs_bandwidth_t;
/** /**

View File

@ -1,5 +1,5 @@
idf_component_register( idf_component_register(
SRCS "sd_card.c" SRCS "sd_card.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES driver fatfs esp_driver_sdspi sdmmc REQUIRES driver fatfs esp_driver_sdspi sdmmc esp_timer
) )

View File

@ -33,6 +33,7 @@
#include "sd_card.h" #include "sd_card.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#include "esp_vfs_fat.h" #include "esp_vfs_fat.h"
@ -45,6 +46,19 @@
#include <fcntl.h> #include <fcntl.h>
#include <dirent.h> #include <dirent.h>
/* 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 // Pin definitions for SparkFun microSD Transflash Breakout
// ESP32-C5: no SDMMC host, use SD SPI mode // ESP32-C5: no SDMMC host, use SD SPI mode
// SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional) // SparkFun in SPI: CLK, MOSI(DI), MISO(DO), CS, CD(optional)
@ -85,11 +99,12 @@ static bool s_cd_configured = false;
static bool s_spi_bus_inited = false; static bool s_spi_bus_inited = false;
static sdmmc_card_t *s_card = NULL; static sdmmc_card_t *s_card = NULL;
static const char *s_mount_point = "/sdcard"; 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) { static void sd_card_cd_ensure_configured(void) {
if (s_cd_configured || SD_CD_PIN < 0) { if (!s_cd_configured && SD_CD_PIN >= 0) {
return;
}
/* Limit shift to 0..63 so compiler does not warn; valid GPIOs are 0..48 */ /* 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; const unsigned int cd_pin = (unsigned int)SD_CD_PIN & 0x3Fu;
gpio_config_t io = { gpio_config_t io = {
@ -102,14 +117,16 @@ static void sd_card_cd_ensure_configured(void) {
if (gpio_config(&io) == ESP_OK) { if (gpio_config(&io) == ESP_OK) {
s_cd_configured = true; s_cd_configured = true;
} }
}
} }
esp_err_t sd_card_init(void) { esp_err_t sd_card_init(void) {
esp_err_t result = ESP_OK;
if (s_sd_card_mounted) { if (s_sd_card_mounted) {
ESP_LOGW(TAG, "SD card already initialized"); 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) // Initialize SPI bus (required before sdspi mount)
@ -121,11 +138,8 @@ esp_err_t sd_card_init(void) {
.quadhd_io_num = -1, .quadhd_io_num = -1,
.max_transfer_sz = 4000, .max_transfer_sz = 4000,
}; };
esp_err_t err = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); result = spi_bus_initialize(SDSPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK) { if (result == ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err));
return err;
}
s_spi_bus_inited = true; s_spi_bus_inited = true;
sdmmc_host_t host = SDSPI_HOST_DEFAULT(); sdmmc_host_t host = SDSPI_HOST_DEFAULT();
@ -145,52 +159,102 @@ esp_err_t sd_card_init(void) {
.allocation_unit_size = 16 * 1024 .allocation_unit_size = 16 * 1024
}; };
err = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card); result = esp_vfs_fat_sdspi_mount(s_mount_point, &host, &slot_config, &mount_config, &s_card);
if (err != ESP_OK) { if (result != ESP_OK) {
if (s_spi_bus_inited) { if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID); spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false; s_spi_bus_inited = false;
} }
if (err == ESP_FAIL) { /* 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. " ESP_LOGE(TAG, "Failed to mount filesystem. "
"If you want the card to be formatted, set format_if_mount_failed = true."); "If you want the card to be formatted, set format_if_mount_failed = true.");
} else { } else {
ESP_LOGE(TAG, "Failed to initialize the card (%s). " ESP_LOGE(TAG, "Failed to initialize the card (%s). "
"Make sure SD card is inserted and wiring is correct.", esp_err_to_name(err)); "Make sure SD card is inserted and wiring is correct.", esp_err_to_name(result));
} }
return err; } else {
}
sdmmc_card_print_info(stdout, s_card); sdmmc_card_print_info(stdout, s_card);
s_sd_card_mounted = true; 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); ESP_LOGI(TAG, "SD card mounted successfully at %s", s_mount_point);
return ESP_OK; }
} else {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(result));
}
}
return result;
} }
esp_err_t sd_card_deinit(void) { esp_err_t sd_card_deinit(void) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_OK;
return ESP_OK;
}
if (s_sd_card_mounted) {
ESP_LOGI(TAG, "Unmounting SD card..."); ESP_LOGI(TAG, "Unmounting SD card...");
esp_err_t ret = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card); result = esp_vfs_fat_sdcard_unmount(s_mount_point, s_card);
if (ret == ESP_OK) { if (result == ESP_OK) {
s_sd_card_mounted = false; s_sd_card_mounted = false;
s_card = NULL; s_card = NULL;
/* Reset status cache on unmount */
s_last_status_result = false;
s_last_status_check_ms = 0;
if (s_spi_bus_inited) { if (s_spi_bus_inited) {
spi_bus_free(SDSPI_HOST_ID); spi_bus_free(SDSPI_HOST_ID);
s_spi_bus_inited = false; s_spi_bus_inited = false;
} }
ESP_LOGI(TAG, "SD card unmounted successfully"); ESP_LOGI(TAG, "SD card unmounted successfully");
} else { } else {
ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "Failed to unmount SD card: %s", esp_err_to_name(result));
} }
return ret; }
return result;
} }
bool sd_card_is_ready(void) { 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) { bool sd_card_cd_available(void) {
@ -198,42 +262,43 @@ bool sd_card_cd_available(void) {
} }
bool sd_card_cd_is_inserted(void) { bool sd_card_cd_is_inserted(void) {
if (SD_CD_PIN < 0) { bool result = false;
return false;
} if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured(); sd_card_cd_ensure_configured();
int level = gpio_get_level(SD_CD_PIN); int level = gpio_get_level(SD_CD_PIN);
#if defined(CONFIG_SD_CD_ACTIVE_LOW) && !(CONFIG_SD_CD_ACTIVE_LOW) #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 #else
return (level == 0); /* LOW = inserted (SparkFun default) */ result = (level == 0); /* LOW = inserted (SparkFun default) */
#endif #endif
}
return result;
} }
int sd_card_cd_get_level(void) { int sd_card_cd_get_level(void) {
if (SD_CD_PIN < 0) { int result = -1;
return -1;
} if (SD_CD_PIN >= 0) {
sd_card_cd_ensure_configured(); sd_card_cd_ensure_configured();
return gpio_get_level(SD_CD_PIN); result = gpio_get_level(SD_CD_PIN);
}
return result;
} }
esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) { esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
FATFS *fs; FATFS *fs;
DWORD fre_clust, fre_sect, tot_sect; DWORD fre_clust, fre_sect, tot_sect;
char path[32]; char path[32];
snprintf(path, sizeof(path), "%s", s_mount_point); snprintf(path, sizeof(path), "%s", s_mount_point);
FRESULT res = f_getfree(path, &fre_clust, &fs); FRESULT res = f_getfree(path, &fre_clust, &fs);
if (res != FR_OK) { 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; tot_sect = (fs->n_fatent - 2) * fs->csize;
fre_sect = fre_clust * fs->csize; fre_sect = fre_clust * fs->csize;
@ -243,15 +308,20 @@ esp_err_t sd_card_get_info(uint64_t *total_bytes, uint64_t *free_bytes) {
if (free_bytes) { if (free_bytes) {
*free_bytes = (uint64_t)fre_sect * 512; *free_bytes = (uint64_t)fre_sect * 512;
} }
result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to get free space: %d", res);
result = ESP_FAIL;
}
}
return ESP_OK; return result;
} }
esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) { esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len, bool append) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
@ -263,38 +333,36 @@ esp_err_t sd_card_write_file(const char *filename, const void *data, size_t len,
flags |= O_TRUNC; flags |= O_TRUNC;
} }
int fd = open(full_path, flags, 0644); int fd = open(full_path, flags, 0644);
if (fd < 0) { 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); ssize_t written = write(fd, data, len);
close(fd); close(fd);
if (written < 0 || (size_t)written != len) { 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); ESP_LOGE(TAG, "Failed to write all data: wrote %zd of %zu bytes", (ssize_t)written, len);
return ESP_FAIL; result = ESP_FAIL;
}
} else {
ESP_LOGE(TAG, "Failed to open file for writing: %s", full_path);
result = ESP_FAIL;
}
} }
ESP_LOGD(TAG, "Wrote %zu bytes to %s", (size_t)written, full_path); return result;
return ESP_OK;
} }
esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) { esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t *bytes_read) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "r"); FILE *f = fopen(full_path, "r");
if (f == NULL) { 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); size_t read = fread(data, 1, len, f);
fclose(f); fclose(f);
@ -303,72 +371,88 @@ esp_err_t sd_card_read_file(const char *filename, void *data, size_t len, size_t
} }
ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path); ESP_LOGD(TAG, "Read %zu bytes from %s", read, full_path);
return ESP_OK; result = ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to open file for reading: %s", full_path);
result = ESP_FAIL;
}
}
return result;
} }
esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) { esp_err_t sd_card_get_file_size(const char *filename, size_t *size_bytes) {
if (!s_sd_card_mounted || size_bytes == NULL) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted && size_bytes != NULL) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
struct stat st; struct stat st;
if (stat(full_path, &st) != 0) { if (stat(full_path, &st) == 0) {
return ESP_FAIL; if (S_ISREG(st.st_mode)) {
}
if (!S_ISREG(st.st_mode)) {
return ESP_ERR_INVALID_ARG;
}
*size_bytes = (size_t)st.st_size; *size_bytes = (size_t)st.st_size;
return ESP_OK; result = ESP_OK;
} else {
result = ESP_ERR_INVALID_ARG;
}
} else {
result = ESP_FAIL;
}
}
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) { 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) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
FILE *f = fopen(full_path, "rb"); FILE *f = fopen(full_path, "rb");
if (f == NULL) { if (f != NULL) {
return ESP_FAIL; if (fseek(f, (long)offset, SEEK_SET) == 0) {
}
if (fseek(f, (long)offset, SEEK_SET) != 0) {
fclose(f);
return ESP_FAIL;
}
size_t n = fread(data, 1, len, f); size_t n = fread(data, 1, len, f);
fclose(f); fclose(f);
if (bytes_read) { if (bytes_read) {
*bytes_read = n; *bytes_read = n;
} }
return ESP_OK; result = ESP_OK;
} else {
fclose(f);
result = ESP_FAIL;
}
} else {
result = ESP_FAIL;
}
}
return result;
} }
bool sd_card_file_exists(const char *filename) { bool sd_card_file_exists(const char *filename) {
if (!s_sd_card_mounted) { bool result = false;
return false;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
struct stat st; struct stat st;
return (stat(full_path, &st) == 0); result = (stat(full_path, &st) == 0);
}
return result;
} }
esp_err_t sd_card_list_dir(const char *path) { esp_err_t sd_card_list_dir(const char *path) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
if (!path || path[0] == '\0') { if (!path || path[0] == '\0') {
snprintf(full_path, sizeof(full_path), "%s", s_mount_point); snprintf(full_path, sizeof(full_path), "%s", s_mount_point);
@ -378,10 +462,7 @@ esp_err_t sd_card_list_dir(const char *path) {
} }
DIR *d = opendir(full_path); DIR *d = opendir(full_path);
if (!d) { if (d != NULL) {
return ESP_FAIL;
}
struct dirent *e; struct dirent *e;
while ((e = readdir(d)) != NULL) { while ((e = readdir(d)) != NULL) {
if (e->d_name[0] == '.') { if (e->d_name[0] == '.') {
@ -397,30 +478,40 @@ esp_err_t sd_card_list_dir(const char *path) {
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
printf(" %-32s <DIR>\n", e->d_name); printf(" %-32s <DIR>\n", e->d_name);
} else { } else {
printf(" %-32s %10zu bytes\n", e->d_name, (size_t)st.st_size); 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 { } else {
printf(" %-32s ?\n", e->d_name); printf(" %-32s ?\n", e->d_name);
} }
} }
closedir(d); closedir(d);
return ESP_OK; result = ESP_OK;
} else {
result = ESP_FAIL;
}
}
return result;
} }
esp_err_t sd_card_delete_file(const char *filename) { esp_err_t sd_card_delete_file(const char *filename) {
if (!s_sd_card_mounted) { esp_err_t result = ESP_ERR_INVALID_STATE;
return ESP_ERR_INVALID_STATE;
}
if (s_sd_card_mounted) {
char full_path[128]; char full_path[128];
snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point, snprintf(full_path, sizeof(full_path), "%s%s%s", s_mount_point,
(filename[0] == '/') ? "" : "/", filename); (filename[0] == '/') ? "" : "/", filename);
if (unlink(full_path) != 0) { 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); ESP_LOGE(TAG, "Failed to delete file: %s", full_path);
return ESP_FAIL; result = ESP_FAIL;
}
} }
ESP_LOGI(TAG, "Deleted file: %s", full_path); return result;
return ESP_OK;
} }

View File

@ -141,6 +141,26 @@ bool wifi_cfg_set_monitor_channel(uint8_t channel) {
return (err == ESP_OK); return (err == ESP_OK);
} }
bool wifi_cfg_get_monitor_channel(uint8_t *channel_out) {
bool result = false;
if (channel_out != NULL) {
nvs_handle_t h;
uint8_t saved_val = DEFAULT_MONITOR_CHANNEL;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
esp_err_t err = nvs_get_u8(h, "mon_chan", &saved_val);
nvs_close(h);
if (err == ESP_OK || err == ESP_ERR_NVS_NOT_FOUND) {
*channel_out = saved_val;
result = true;
}
}
}
return result;
}
void wifi_cfg_clear_monitor_channel(void) { void wifi_cfg_clear_monitor_channel(void) {
nvs_handle_t h; nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) { if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) {

View File

@ -55,6 +55,7 @@ bool wifi_cfg_set_password(const char *password);
// Monitor Specific // Monitor Specific
bool wifi_cfg_set_monitor_channel(uint8_t channel); bool wifi_cfg_set_monitor_channel(uint8_t channel);
bool wifi_cfg_get_monitor_channel(uint8_t *channel_out);
void wifi_cfg_clear_monitor_channel(void); void wifi_cfg_clear_monitor_channel(void);
bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val); bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val);

View File

@ -33,10 +33,12 @@
#include "wifi_controller.h" #include "wifi_controller.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "esp_timer.h"
#include "inttypes.h" #include "inttypes.h"
#include <string.h> #include <string.h>
#include "wifi_cfg.h" #include "wifi_cfg.h"
@ -55,7 +57,16 @@
#include "sd_card.h" #include "sd_card.h"
#define FIWI_TELEMETRY_FILE "fiwi-telemetry" #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"; static const char *TAG = "WIFI_CTL";
@ -66,6 +77,29 @@ static uint8_t s_monitor_channel_staging = 6;
static bool s_monitor_enabled = false; static bool s_monitor_enabled = false;
static uint32_t s_monitor_frame_count = 0; static uint32_t s_monitor_frame_count = 0;
static TaskHandle_t s_monitor_stats_task_handle = NULL; 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};
static rate_tracker_t s_frame_rate = {0};
static uint32_t s_last_frame_count_for_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 --- // --- Event Handler ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
@ -81,10 +115,12 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t e
// ... [Log Collapse / Monitor Callback Logic] ... // ... [Log Collapse / Monitor Callback Logic] ...
static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) { static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) {
if (s_monitor_debug) { /* Only log in debug mode */
gps_timestamp_t ts = gps_get_timestamp(); gps_timestamp_t ts = gps_get_timestamp();
int64_t now_ms = ts.gps_us / 1000; int64_t now_ms = ts.gps_us / 1000;
ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d", 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); 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) { static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) {
@ -99,42 +135,249 @@ static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t
mcs_telemetry_process_frame(frame, NULL); 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) { static void monitor_stats_task(void *arg) {
(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 flush_count = 0;
uint32_t last_frame_count = 0; uint32_t last_frame_count = 0;
while (1) { uint64_t last_batch_flush_ms = 0;
vTaskDelay(pdMS_TO_TICKS(10000)); 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;
s_frame_rate.last_update_ms = start_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
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) { if (s_monitor_frame_count == last_frame_count) {
status_led_set_capture_active(false); status_led_set_capture_active(false);
} }
last_frame_count = s_monitor_frame_count;
wifi_collapse_stats_t stats; /* Update frame rate tracker */
if (wifi_monitor_get_stats(&stats) == ESP_OK) { uint32_t frames_delta = s_monitor_frame_count - s_last_frame_count_for_rate;
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---", uint32_t frame_interval_ms = (uint32_t)(now_ms - s_frame_rate.last_update_ms);
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav); if (frame_interval_ms > 0) {
if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️"); update_rate_tracker(&s_frame_rate, frames_delta, frame_interval_ms);
s_frame_rate.last_update_ms = now_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
} }
/* 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) { last_frame_count = s_monitor_frame_count;
/* 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); size_t len = strlen(json_buf);
if (len > 0) { if (len > 0) {
json_buf[len] = '\n'; /* NDJSON: one object per line */ json_buf[len] = '\n'; /* NDJSON: one object per line */
if (sd_card_write_file(FIWI_TELEMETRY_FILE, json_buf, len + 1, true) == ESP_OK) { 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++; flush_count++;
ESP_LOGD(TAG, "fiwi-telemetry flushed (#%lu)", (unsigned long)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 --- // --- Helper to apply IP settings ---
static void apply_ip_settings(void) { static void apply_ip_settings(void) {
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) return;
if (netif != NULL) {
if (wifi_cfg_get_dhcp()) { if (wifi_cfg_get_dhcp()) {
esp_netif_dhcpc_start(netif); esp_netif_dhcpc_start(netif);
} else { } else {
@ -152,6 +395,7 @@ static void apply_ip_settings(void) {
ESP_LOGI(TAG, "Static IP applied: %s", ip); ESP_LOGI(TAG, "Static IP applied: %s", ip);
} }
} }
}
} }
// ============================================================================ // ============================================================================
@ -192,22 +436,36 @@ void wifi_ctl_init(void) {
esp_wifi_connect(); esp_wifi_connect();
} }
// Load Staging Params // Load Staging and Active Params from NVS
char mode_ignored[16]; char mode_ignored[16];
wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging); wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging);
if (s_monitor_channel_staging == 0) s_monitor_channel_staging = 6; if (s_monitor_channel_staging == 0) s_monitor_channel_staging = 6;
// Load active channel from NVS (persists across reboots)
uint8_t saved_active_channel = 0;
if (wifi_cfg_get_monitor_channel(&saved_active_channel) && saved_active_channel > 0 && saved_active_channel <= 14) {
s_monitor_channel_active = saved_active_channel;
// Also update staging to match active if staging wasn't set
if (s_monitor_channel_staging == 6) {
s_monitor_channel_staging = saved_active_channel;
}
}
} }
// --- Mode Control (Core) --- // --- Mode Control (Core) ---
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) { esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
if (channel == 0) channel = s_monitor_channel_staging; esp_err_t result = ESP_OK;
if (channel == 0) {
// Use active channel if set, otherwise fall back to staging
channel = (s_monitor_channel_active > 0) ? s_monitor_channel_active : s_monitor_channel_staging;
}
if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) { if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) {
ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", 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(); iperf_stop();
@ -224,9 +482,8 @@ esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
status_led_set_capture_active(false); status_led_set_capture_active(false);
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) { if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
ESP_LOGE(TAG, "Failed to init monitor mode"); ESP_LOGE(TAG, "Failed to init monitor mode");
return ESP_FAIL; result = ESP_FAIL;
} } else {
/* MCS telemetry -> fiwi-telemetry on SD (default on monitor start) */ /* MCS telemetry -> fiwi-telemetry on SD (default on monitor start) */
if (mcs_telemetry_init(NULL) != ESP_OK) { if (mcs_telemetry_init(NULL) != ESP_OK) {
ESP_LOGW(TAG, "MCS telemetry init failed"); ESP_LOGW(TAG, "MCS telemetry init failed");
@ -238,27 +495,63 @@ esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
if (wifi_monitor_start() != ESP_OK) { if (wifi_monitor_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start monitor mode"); ESP_LOGE(TAG, "Failed to start monitor mode");
return ESP_FAIL; result = ESP_FAIL;
} } else {
s_monitor_enabled = true; s_monitor_enabled = true;
s_current_mode = WIFI_CTL_MODE_MONITOR; s_current_mode = WIFI_CTL_MODE_MONITOR;
s_monitor_channel_active = channel; s_monitor_channel_active = channel;
/* Save active channel to NVS so it persists across reboots */
wifi_cfg_set_monitor_channel(channel);
status_led_set_state(LED_STATE_MONITORING); status_led_set_state(LED_STATE_MONITORING);
if (s_monitor_stats_task_handle == NULL) { /* Reset rate trackers when starting monitor */
xTaskCreate(monitor_stats_task, "monitor_stats", 4096, NULL, 5, &s_monitor_stats_task_handle); 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));
memset(&s_frame_rate, 0, sizeof(rate_tracker_t));
s_telemetry_gen_rate.last_update_ms = start_ms;
s_sd_write_rate.last_update_ms = start_ms;
s_frame_rate.last_update_ms = start_ms;
s_last_frame_count_for_rate = s_monitor_frame_count;
/* 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);
}
} }
return ESP_OK; 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);
}
}
}
}
return result;
} }
esp_err_t wifi_ctl_switch_to_sta(void) { esp_err_t wifi_ctl_switch_to_sta(void) {
esp_err_t result = ESP_OK;
if (s_current_mode == WIFI_CTL_MODE_STA) { if (s_current_mode == WIFI_CTL_MODE_STA) {
ESP_LOGI(TAG, "Already in STA mode"); 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) { if (s_monitor_stats_task_handle != NULL) {
@ -268,6 +561,8 @@ esp_err_t wifi_ctl_switch_to_sta(void) {
if (s_monitor_enabled) { if (s_monitor_enabled) {
status_led_set_capture_active(false); status_led_set_capture_active(false);
/* Flush any pending telemetry before stopping */
flush_telemetry_batch();
mcs_telemetry_stop(); mcs_telemetry_stop();
wifi_monitor_stop(); wifi_monitor_stop();
s_monitor_enabled = false; s_monitor_enabled = false;
@ -282,8 +577,10 @@ esp_err_t wifi_ctl_switch_to_sta(void) {
s_current_mode = WIFI_CTL_MODE_STA; s_current_mode = WIFI_CTL_MODE_STA;
status_led_set_state(LED_STATE_WAITING); status_led_set_state(LED_STATE_WAITING);
result = ESP_OK;
}
return ESP_OK; return result;
} }
// --- Wrappers for cmd_monitor.c --- // --- Wrappers for cmd_monitor.c ---
@ -318,9 +615,39 @@ void wifi_ctl_set_channel(int channel) {
ESP_LOGI(TAG, "Switching live channel to %d", channel); ESP_LOGI(TAG, "Switching live channel to %d", channel);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
s_monitor_channel_active = (uint8_t)channel; s_monitor_channel_active = (uint8_t)channel;
/* Save active channel to NVS so it persists across reboots */
wifi_cfg_set_monitor_channel(channel);
} }
} }
/**
* @brief Calculate WiFi channel frequency in MHz
* @param channel Channel number (1-14 for 2.4GHz, 36+ for 5GHz)
* @return Frequency in MHz, or 0 if invalid 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);
} else if (channel >= 36 && channel <= 64) {
/* 5 GHz UNII-1/2A: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 100 && channel <= 144) {
/* 5 GHz UNII-2C: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 149 && channel <= 165) {
/* 5 GHz UNII-3: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
} else if (channel >= 169 && channel <= 177) {
/* 5 GHz UNII-4: 5000 + (channel * 5) MHz */
freq = 5000 + (channel * 5);
}
return freq;
}
void wifi_ctl_status(void) { void wifi_ctl_status(void) {
const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" : const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" :
(s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION"; (s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION";
@ -328,10 +655,53 @@ void wifi_ctl_status(void) {
printf("WiFi Status:\n"); printf("WiFi Status:\n");
printf(" Mode: %s\n", mode_str); printf(" Mode: %s\n", mode_str);
if (s_current_mode == WIFI_CTL_MODE_MONITOR) { if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
printf(" Channel: %d\n", s_monitor_channel_active); uint8_t channel = s_monitor_channel_active;
printf(" Frames: %lu\n", (unsigned long)s_monitor_frame_count); 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",
channel, freq_mhz, freq_mhz);
} else {
printf(" Channel: %d\n", channel);
}
/* Get frame rate (frames per second) */
double frame_rate_fps = 0.0;
if (s_frame_rate.sample_count > 0) {
frame_rate_fps = s_frame_rate.mean_rate_bps; /* Already in per-second units for frames */
}
printf(" Frames: %lu, %.1f fps\n", (unsigned long)s_monitor_frame_count, frame_rate_fps);
/* 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);
} }
// --- Params (NVS) --- // --- Params (NVS) ---
@ -342,8 +712,15 @@ bool wifi_ctl_param_is_unsaved(void) {
void wifi_ctl_param_save(const char *dummy) { void wifi_ctl_param_save(const char *dummy) {
(void)dummy; (void)dummy;
if (wifi_cfg_set_monitor_channel(s_monitor_channel_staging)) { /* If monitor mode is running, save the active channel; otherwise save staging */
ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", s_monitor_channel_staging); 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);
/* Update staging to match if we saved active */
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
s_monitor_channel_staging = channel_to_save;
}
} else { } else {
ESP_LOGI(TAG, "No changes to save."); ESP_LOGI(TAG, "No changes to save.");
} }
@ -354,7 +731,17 @@ void wifi_ctl_param_init(void) {
uint8_t ch = 0; uint8_t ch = 0;
wifi_cfg_get_mode(mode_ignored, &ch); wifi_cfg_get_mode(mode_ignored, &ch);
if (ch > 0) s_monitor_channel_staging = ch; if (ch > 0) s_monitor_channel_staging = ch;
ESP_LOGI(TAG, "Reloaded monitor channel: %d", s_monitor_channel_staging);
// Reload active channel from NVS
uint8_t saved_active_channel = 0;
if (wifi_cfg_get_monitor_channel(&saved_active_channel) && saved_active_channel > 0 && saved_active_channel <= 14) {
s_monitor_channel_active = saved_active_channel;
// Update staging to match active if staging wasn't set
if (s_monitor_channel_staging == 6 && ch == 0) {
s_monitor_channel_staging = saved_active_channel;
}
}
ESP_LOGI(TAG, "Reloaded monitor channel: active=%d, staging=%d", s_monitor_channel_active, s_monitor_channel_staging);
} }
void wifi_ctl_param_clear(void) { void wifi_ctl_param_clear(void) {
@ -368,6 +755,75 @@ void wifi_ctl_param_clear(void) {
wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; } wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; }
int wifi_ctl_get_channel(void) { return s_monitor_channel_active; } 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 --- // --- Deprecated ---
static void auto_monitor_task_func(void *arg) { static void auto_monitor_task_func(void *arg) {
uint8_t channel = (uint8_t)(uintptr_t)arg; uint8_t channel = (uint8_t)(uintptr_t)arg;

View File

@ -74,6 +74,14 @@ void wifi_ctl_param_clear(void);
wifi_ctl_mode_t wifi_ctl_get_mode(void); wifi_ctl_mode_t wifi_ctl_get_mode(void);
int wifi_ctl_get_channel(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 // Deprecated / Compatibility
void wifi_ctl_auto_monitor_start(uint8_t channel); void wifi_ctl_auto_monitor_start(uint8_t channel);

View File

@ -56,10 +56,97 @@ uint32_t threshold_duration_multiplier = 2; // NAV > expected * this = mism
// Logging control // Logging control
uint32_t log_every_n_mismatches = 1; // Log every Nth mismatch (1 = all, 10 = every 10th) 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 uint32_t s_mismatch_log_counter = 0;
static bool s_monitor_debug = false; // Debug mode: enable serial logging
// Forward declarations // Forward declarations
static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type); 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 * @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) // 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; 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) { if (frame_info->has_addr4 && len >= 30) {
memcpy(frame_info->addr4, &payload[24], 6); 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; frame_info->frame_len = len;
return ESP_OK; return ESP_OK;
@ -135,13 +233,27 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
frame_info.timestamp = rx_ctrl->timestamp; frame_info.timestamp = rx_ctrl->timestamp;
// Extract PHY rate info from RX control // Extract PHY rate info from RX control
frame_info.mcs = 0;
frame_info.rate = rx_ctrl->rate; // This is the rate index 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) // 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
}
// 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 {
// Estimate PHY rate from rate index (rough approximation for legacy frames)
static const uint32_t rate_table[] = { static const uint32_t rate_table[] = {
1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS) 1000, 2000, 5500, 11000, // 1, 2, 5.5, 11 Mbps (DSSS)
6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates 6000, 9000, 12000, 18000, 24000, 36000, 48000, 54000, // OFDM rates
@ -153,6 +265,7 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
} else { } else {
frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default frame_info.phy_rate_kbps = 100000; // Assume 100 Mbps default
} }
}
// Update statistics // Update statistics
stats.total_frames++; stats.total_frames++;
@ -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) { if (frame_info.duration_id > threshold_duration_mismatch_us) {
s_mismatch_log_counter++; 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", ESP_LOGW("MONITOR", "Duration mismatch: %s frame, %u bytes @ %u Mbps",
wifi_frame_type_str(frame_info.type, frame_info.subtype), wifi_frame_type_str(frame_info.type, frame_info.subtype),
frame_info.frame_len, phy_rate_mbps); frame_info.frame_len, phy_rate_mbps);
@ -227,6 +340,7 @@ 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 && if (frame_info.retry && frame_info.duration_id > threshold_high_nav_us &&
phy_rate_mbps < threshold_phy_rate_fallback_mbps) { phy_rate_mbps < threshold_phy_rate_fallback_mbps) {
if (s_monitor_debug) {
ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!"); ESP_LOGW("MONITOR", "⚠⚠⚠ COLLISION DETECTED!");
// NEW: Log the Attacker MAC // NEW: Log the Attacker MAC
@ -241,6 +355,7 @@ static void wifi_promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type)
frame_info.duration_id, expected_duration); frame_info.duration_id, expected_duration);
} }
} }
}
// Count frame types // Count frame types
switch (frame_info.type) { switch (frame_info.type) {
@ -477,3 +592,19 @@ const char* wifi_frame_type_str(uint8_t type, uint8_t subtype) {
return "UNKNOWN"; 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;
}

View File

@ -168,11 +168,12 @@ typedef struct {
uint32_t timestamp; uint32_t timestamp;
// PHY rate info // 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 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 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) uint32_t phy_rate_kbps; // Calculated PHY rate in Kbps (uint32_t to handle >65 Mbps)
// Frame size // Frame size
@ -287,6 +288,18 @@ bool wifi_monitor_is_collapsed(void);
*/ */
const char* wifi_frame_type_str(uint8_t type, uint8_t subtype); 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 #ifdef __cplusplus
} }
#endif #endif