/* * wifi_controller.c * * Copyright (c) 2025 Umber Networks & Robert McMahon * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #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" // Dependencies #include "iperf.h" #include "status_led.h" #include "wifi_monitor.h" #include "gps_sync.h" #ifdef CONFIG_ESP_WIFI_CSI_ENABLED #include "csi_manager.h" #endif #include "mcs_telemetry.h" #include "sd_card.h" #define FIWI_TELEMETRY_FILE "fiwi-telemetry" #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 wifi_ctl_mode_t s_current_mode = WIFI_CTL_MODE_STA; static uint8_t s_monitor_channel_active = 6; 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}; 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 --- static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ESP_LOGI(TAG, "Got IP -> LED Connected"); status_led_set_state(LED_STATE_CONNECTED); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_current_mode == WIFI_CTL_MODE_STA) { status_led_set_state(LED_STATE_NO_CONFIG); } } } // ... [Log Collapse / Monitor Callback Logic] ... 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(); 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) { (void)payload; (void)len; s_monitor_frame_count++; status_led_set_capture_active(true); if (frame->retry && frame->duration_id > 5000) { log_collapse_event((float)frame->duration_id, frame->rssi, frame->retry); } /* MCS telemetry: feed frames to fiwi-telemetry (default on monitor start) */ 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[TELEMETRY_JSON_BUF_SIZE]; uint32_t flush_count = 0; uint32_t last_frame_count = 0; 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; 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) { status_led_set_capture_active(false); } /* Update frame rate tracker */ uint32_t frames_delta = s_monitor_frame_count - s_last_frame_count_for_rate; uint32_t frame_interval_ms = (uint32_t)(now_ms - s_frame_rate.last_update_ms); if (frame_interval_ms > 0) { 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; } 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); if (len > 0) { json_buf[len] = '\n'; /* NDJSON: one object per line */ 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 != 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); esp_netif_set_ip_info(netif, &info); ESP_LOGI(TAG, "Static IP applied: %s", ip); } } } } // ============================================================================ // PUBLIC API IMPLEMENTATION // ============================================================================ void wifi_ctl_init(void) { s_current_mode = WIFI_CTL_MODE_STA; s_monitor_enabled = false; s_monitor_frame_count = 0; // 1. Initialize Network Interface esp_netif_create_default_wifi_sta(); // 2. Apply IP Settings (Static vs DHCP) apply_ip_settings(); // 3. Initialize Wi-Fi Driver wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // 4. Register Events ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL)); ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifi_event_handler, NULL, NULL)); // 5. Configure Storage & Mode ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_start()); // 6. Apply Saved Config if (!wifi_cfg_apply_from_nvs()) { ESP_LOGW(TAG, "No saved WiFi config found, driver initialized in defaults."); status_led_set_state(LED_STATE_NO_CONFIG); } else { ESP_LOGI(TAG, "WiFi driver initialized from NVS."); status_led_set_state(LED_STATE_WAITING); esp_wifi_connect(); } // Load Staging and Active Params from NVS char mode_ignored[16]; wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging); 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) --- esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) { 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) { ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel); result = ESP_OK; } else { ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel); iperf_stop(); vTaskDelay(pdMS_TO_TICKS(500)); #ifdef CONFIG_ESP_WIFI_CSI_ENABLED csi_mgr_disable(); #endif 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"); 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; /* Save active channel to NVS so it persists across reboots */ wifi_cfg_set_monitor_channel(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)); 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); } } 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 result = ESP_OK; if (s_current_mode == WIFI_CTL_MODE_STA) { ESP_LOGI(TAG, "Already in STA mode"); result = ESP_OK; } else { 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_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)); } 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; } return result; } // --- Wrappers for cmd_monitor.c --- void wifi_ctl_monitor_start(int channel) { wifi_ctl_switch_to_monitor((uint8_t)channel, WIFI_BW_HT20); } void wifi_ctl_stop(void) { wifi_ctl_switch_to_sta(); } void wifi_ctl_start_station(void) { wifi_ctl_switch_to_sta(); } void wifi_ctl_start_ap(void) { ESP_LOGW(TAG, "AP Mode not fully implemented, using STA"); wifi_ctl_switch_to_sta(); } // --- Settings --- void wifi_ctl_set_channel(int channel) { if (channel < 1 || channel > 14) { ESP_LOGE(TAG, "Invalid channel %d", channel); return; } s_monitor_channel_staging = (uint8_t)channel; if (s_current_mode == WIFI_CTL_MODE_MONITOR) { ESP_LOGI(TAG, "Switching live channel to %d", channel); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); 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) { const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" : (s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION"; printf("WiFi Status:\n"); printf(" Mode: %s\n", mode_str); if (s_current_mode == WIFI_CTL_MODE_MONITOR) { uint8_t channel = s_monitor_channel_active; uint32_t freq_mhz = wifi_channel_to_frequency(channel); /* Display channel info: channel N (FREQ MHz), width: 20 MHz, center1: FREQ MHz */ /* Note: Monitor mode currently uses single channel (20 MHz width) */ if (freq_mhz > 0) { printf(" Channel: %d (%" PRIu32 " MHz), width: 20 MHz, center1: %" PRIu32 " MHz\n", 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 frame type breakdown */ wifi_collapse_stats_t monitor_stats; if (wifi_monitor_get_stats(&monitor_stats) == ESP_OK) { printf(" Frame Types: Data=%lu, Mgmt=%lu, Ctrl=%lu (RTS=%lu, CTS=%lu, ACK=%lu)\n", (unsigned long)monitor_stats.data_frames, (unsigned long)monitor_stats.mgmt_frames, (unsigned long)(monitor_stats.rts_frames + monitor_stats.cts_frames + monitor_stats.ack_frames), (unsigned long)monitor_stats.rts_frames, (unsigned long)monitor_stats.cts_frames, (unsigned long)monitor_stats.ack_frames); /* Show histograms */ // AMPDU histogram uint32_t total_ampdu = 0; for (int i = 0; i < 8; i++) { total_ampdu += monitor_stats.ampdu_hist[i]; } if (total_ampdu > 0) { printf(" AMPDU Aggregation: "); const char *ampdu_labels[] = {"1", "2-4", "5-8", "9-16", "17-32", "33-48", "49-64", "65+"}; for (int i = 0; i < 8; i++) { if (monitor_stats.ampdu_hist[i] > 0) { printf("%s=%lu ", ampdu_labels[i], (unsigned long)monitor_stats.ampdu_hist[i]); } } printf("\n"); } // MCS histogram (show non-zero entries) uint32_t total_mcs = 0; for (int i = 0; i < 32; i++) { total_mcs += monitor_stats.mcs_hist[i]; } if (total_mcs > 0) { printf(" MCS Distribution: "); int printed = 0; for (int i = 0; i < 32; i++) { if (monitor_stats.mcs_hist[i] > 0) { if (printed > 0) printf(", "); printf("MCS%d=%lu", i, (unsigned long)monitor_stats.mcs_hist[i]); printed++; if (printed >= 10) { // Limit output length printf(" ..."); break; } } } printf("\n"); } // Spatial streams histogram uint32_t total_ss = 0; for (int i = 0; i < 8; i++) { total_ss += monitor_stats.ss_hist[i]; } if (total_ss > 0) { printf(" Spatial Streams: "); for (int i = 0; i < 8; i++) { if (monitor_stats.ss_hist[i] > 0) { printf("%dSS=%lu ", i + 1, (unsigned long)monitor_stats.ss_hist[i]); } } printf("\n"); } } /* Show debug and filter status */ bool debug_enabled = wifi_ctl_get_monitor_debug(); uint8_t filter_mac[6]; bool filter_enabled = wifi_ctl_get_monitor_debug_filter(filter_mac); printf(" Debug: %s\n", debug_enabled ? "enabled" : "disabled"); if (filter_enabled) { printf(" Filter: %02x:%02x:%02x:%02x:%02x:%02x\n", filter_mac[0], filter_mac[1], filter_mac[2], filter_mac[3], filter_mac[4], filter_mac[5]); } else { printf(" Filter: disabled (showing all frames)\n"); } /* Show telemetry rate statistics */ uint64_t gen_bytes = 0, write_bytes = 0; double gen_rate = 0.0, write_rate = 0.0; 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); } } } } // --- Params (NVS) --- bool wifi_ctl_param_is_unsaved(void) { return wifi_cfg_monitor_channel_is_unsaved(s_monitor_channel_staging); } void wifi_ctl_param_save(const char *dummy) { (void)dummy; /* If monitor mode is running, save the active channel; otherwise save staging */ uint8_t channel_to_save = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? 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 { ESP_LOGI(TAG, "No changes to save."); } } void wifi_ctl_param_init(void) { char mode_ignored[16]; uint8_t ch = 0; wifi_cfg_get_mode(mode_ignored, &ch); if (ch > 0) s_monitor_channel_staging = ch; // 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) { wifi_cfg_clear_monitor_channel(); s_monitor_channel_staging = 6; ESP_LOGI(TAG, "Monitor config cleared (Defaulting to Ch 6)."); } // --- Getters --- 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; } esp_err_t wifi_ctl_set_monitor_debug_filter(const uint8_t *mac) { return wifi_monitor_set_debug_filter(mac); } bool wifi_ctl_get_monitor_debug_filter(uint8_t *mac_out) { return wifi_monitor_get_debug_filter(mac_out); } // --- Deprecated --- static void auto_monitor_task_func(void *arg) { uint8_t channel = (uint8_t)(uintptr_t)arg; ESP_LOGI(TAG, "Waiting for WiFi connection before switching to monitor mode..."); while (status_led_get_state() != LED_STATE_CONNECTED) { vTaskDelay(pdMS_TO_TICKS(500)); } ESP_LOGI(TAG, "WiFi connected, waiting for GPS sync (2s)..."); vTaskDelay(pdMS_TO_TICKS(2000)); wifi_ctl_switch_to_monitor(channel, WIFI_BW_HT20); vTaskDelete(NULL); } void wifi_ctl_auto_monitor_start(uint8_t channel) { xTaskCreate(auto_monitor_task_func, "auto_monitor", 4096, (void*)(uintptr_t)channel, 5, NULL); }