diff --git a/async_batch_config.py b/async_batch_config.py index 15e1187..2d5d3d1 100755 --- a/async_batch_config.py +++ b/async_batch_config.py @@ -9,6 +9,7 @@ Features: - Robust Regex-based state detection - Supports verifying both Station Mode (IP check) and Monitor Mode - Context-aware logging +- CSI enable/disable control """ import asyncio @@ -52,6 +53,7 @@ class Esp32Configurator: # Success indicators self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE) self.regex_monitor_success = re.compile(r'Monitor mode active', re.IGNORECASE) + self.regex_csi_saved = re.compile(r'CSI enable state saved', re.IGNORECASE) # Prompts indicating device is booting/ready self.regex_ready = re.compile(r'Initialization complete|GPS synced|No WiFi config found', re.IGNORECASE) @@ -114,7 +116,8 @@ class Esp32Configurator: async def _send_config(self, writer): """Builds and transmits the configuration command""" - self.log.info(f"Sending config for IP {self.target_ip}...") + csi_val = '1' if self.args.csi_enable else '0' + self.log.info(f"Sending config for IP {self.target_ip} (CSI:{csi_val})...") # Construct command block config_str = ( @@ -130,6 +133,7 @@ class Esp32Configurator: f"POWERSAVE:{self.args.powersave}\n" f"MODE:{self.args.mode}\n" f"MON_CH:{self.args.monitor_channel}\n" + f"CSI:{csi_val}\n" f"END\n" ) @@ -140,6 +144,7 @@ class Esp32Configurator: """Monitors output for confirmation of Success""" self.log.info("Verifying configuration...") timeout = time.time() + 15 # 15s verification timeout + csi_saved = False while time.time() < timeout: try: @@ -147,12 +152,17 @@ class Esp32Configurator: line = line_bytes.decode('utf-8', errors='ignore').strip() if not line: continue + # Check for CSI save confirmation + if self.regex_csi_saved.search(line): + csi_saved = True + # Check for Station Mode Success (IP Address) m_ip = self.regex_got_ip.search(line) if m_ip: got_ip = m_ip.group(1) if got_ip == self.target_ip: - self.log.info(f"SUCCESS: Assigned {got_ip}") + csi_status = "CSI saved" if csi_saved else "" + self.log.info(f"SUCCESS: Assigned {got_ip} {csi_status}") return True else: self.log.warning(f"MISMATCH: Wanted {self.target_ip}, got {got_ip}") @@ -173,7 +183,24 @@ class Esp32Configurator: return False async def main_async(): - parser = argparse.ArgumentParser(description='Async ESP32 Batch Config') + parser = argparse.ArgumentParser( + description='Async ESP32 Batch Config with CSI Control', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Configure 20 iperf baseline devices (NO CSI) + %(prog)s --start-ip 192.168.1.81 + + # Configure devices WITH CSI enabled + %(prog)s --start-ip 192.168.1.111 --csi + + # Configure for monitor mode on channel 36 + %(prog)s --start-ip 192.168.1.90 -M MONITOR -mc 36 + + # 5GHz with 40MHz bandwidth + %(prog)s --start-ip 192.168.1.81 -b 5G -B HT40 + """ + ) # Arguments parser.add_argument('--start-ip', required=True, help='Starting Static IP') @@ -186,6 +213,8 @@ async def main_async(): parser.add_argument('-ps', '--powersave', default='NONE') parser.add_argument('-M', '--mode', default='STA') parser.add_argument('-mc', '--monitor-channel', type=int, default=36) + parser.add_argument('--csi', dest='csi_enable', action='store_true', + help='Enable CSI capture (default: disabled)') args = parser.parse_args() @@ -208,7 +237,8 @@ async def main_async(): return # 2. Configure Concurrently - print(f"Step 2: Configuring {len(devices)} devices concurrently...") + csi_status = "ENABLED" if args.csi_enable else "DISABLED" + print(f"Step 2: Configuring {len(devices)} devices concurrently (CSI: {csi_status})...") tasks = [] for i, dev in enumerate(devices): @@ -225,6 +255,7 @@ async def main_async(): print(f"Total Devices: {len(devices)}") print(f"Success: {success_count}") print(f"Failed: {len(devices) - success_count}") + print(f"CSI Setting: {csi_status}") print("="*40) if __name__ == '__main__': diff --git a/components/csi_manager/CMakeLists.txt b/components/csi_manager/CMakeLists.txt index 42b581a..562232b 100644 --- a/components/csi_manager/CMakeLists.txt +++ b/components/csi_manager/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "csi_manager.c" INCLUDE_DIRS "." REQUIRES esp_wifi freertos - PRIV_REQUIRES csi_log log) + PRIV_REQUIRES csi_log log nvs_flash) diff --git a/components/csi_manager/csi_manager.c b/components/csi_manager/csi_manager.c index 2b4825c..d76a09a 100644 --- a/components/csi_manager/csi_manager.c +++ b/components/csi_manager/csi_manager.c @@ -2,14 +2,93 @@ #include "csi_log.h" #include "esp_wifi.h" #include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" static const char *TAG = "CSI_MGR"; +static const char *NVS_NAMESPACE = "csi_config"; +static const char *NVS_KEY_ENABLE = "csi_enable"; static bool s_csi_enabled = false; static uint32_t s_csi_packet_count = 0; +// --- NVS Functions --- + +esp_err_t csi_mgr_save_enable_state(bool enable) { + nvs_handle_t handle; + esp_err_t err; + + err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err)); + return err; + } + + err = nvs_set_u8(handle, NVS_KEY_ENABLE, enable ? 1 : 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write CSI enable state: %s", esp_err_to_name(err)); + nvs_close(handle); + return err; + } + + err = nvs_commit(handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "CSI enable state saved: %s", enable ? "ENABLED" : "DISABLED"); + } + + nvs_close(handle); + return err; +} + +esp_err_t csi_mgr_load_enable_state(bool *enable) { + if (!enable) { + return ESP_ERR_INVALID_ARG; + } + + nvs_handle_t handle; + esp_err_t err; + + err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGW(TAG, "CSI config namespace not found - using default (disabled)"); + *enable = false; + return ESP_ERR_NVS_NOT_FOUND; + } + ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err)); + *enable = false; + return err; + } + + uint8_t value = 0; + err = nvs_get_u8(handle, NVS_KEY_ENABLE, &value); + nvs_close(handle); + + if (err == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGI(TAG, "CSI enable not configured - using default (disabled)"); + *enable = false; + return ESP_ERR_NVS_NOT_FOUND; + } else if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to read CSI enable state: %s", esp_err_to_name(err)); + *enable = false; + return err; + } + + *enable = (value != 0); + ESP_LOGI(TAG, "CSI enable loaded from NVS: %s", *enable ? "ENABLED" : "DISABLED"); + return ESP_OK; +} + +bool csi_mgr_should_enable(void) { + bool enable = false; + csi_mgr_load_enable_state(&enable); + return enable; +} + // --- Callback --- static void csi_cb(void *ctx, wifi_csi_info_t *info) { if (!info || !s_csi_enabled) return; diff --git a/components/csi_manager/csi_manager.h b/components/csi_manager/csi_manager.h index 2c02f1a..d6cdab4 100644 --- a/components/csi_manager/csi_manager.h +++ b/components/csi_manager/csi_manager.h @@ -41,6 +41,30 @@ bool csi_mgr_is_enabled(void); */ uint32_t csi_mgr_get_packet_count(void); +/** + * @brief Save CSI enable state to NVS + * + * @param enable true to enable CSI on boot, false to disable + * @return esp_err_t ESP_OK on success + */ +esp_err_t csi_mgr_save_enable_state(bool enable); + +/** + * @brief Load CSI enable state from NVS + * + * @param enable Output: CSI enable state (default: false if not found) + * @return esp_err_t ESP_OK on success, ESP_ERR_NVS_NOT_FOUND if not set + */ +esp_err_t csi_mgr_load_enable_state(bool *enable); + +/** + * @brief Check if CSI should be enabled based on NVS config + * Returns false by default if no config exists + * + * @return bool true if CSI should be enabled + */ +bool csi_mgr_should_enable(void); + #ifdef __cplusplus } #endif diff --git a/components/wifi_cfg/CMakeLists.txt b/components/wifi_cfg/CMakeLists.txt index f16fe93..b88d65a 100644 --- a/components/wifi_cfg/CMakeLists.txt +++ b/components/wifi_cfg/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "wifi_cfg.c" INCLUDE_DIRS "." - PRIV_REQUIRES nvs_flash esp_wifi esp_netif driver cmd_transport) + PRIV_REQUIRES nvs_flash esp_wifi esp_netif driver cmd_transport csi_manager) diff --git a/components/wifi_cfg/wifi_cfg.c b/components/wifi_cfg/wifi_cfg.c index 59f1c27..e239946 100644 --- a/components/wifi_cfg/wifi_cfg.c +++ b/components/wifi_cfg/wifi_cfg.c @@ -15,6 +15,7 @@ #include "wifi_cfg.h" #include "cmd_transport.h" // Now uses the transport component +#include "csi_manager.h" // For CSI enable/disable static const char *TAG = "wifi_cfg"; static esp_netif_t *sta_netif = NULL; @@ -201,7 +202,7 @@ bool wifi_cfg_apply_from_nvs(void) { // --- Command Listener Logic --- -static void on_cfg_line(const char *line, char *ssid, char *pass, char *ip, char *mask, char *gw, char *band, char *bw, char *powersave, char *mode, uint8_t *mon_ch, bool *dhcp){ +static void on_cfg_line(const char *line, char *ssid, char *pass, char *ip, char *mask, char *gw, char *band, char *bw, char *powersave, char *mode, uint8_t *mon_ch, bool *dhcp, bool *csi_enable){ if (strncmp(line, "SSID:",5)==0){ strncpy(ssid, line+5, 63); ssid[63]=0; return; } if (strncmp(line, "PASS:",5)==0){ strncpy(pass, line+5, 63); pass[63]=0; return; } if (strncmp(line, "IP:",3)==0){ strncpy(ip, line+3, 31); ip[31]=0; return; } @@ -212,7 +213,8 @@ static void on_cfg_line(const char *line, char *ssid, char *pass, char *ip, char if (strncmp(line, "POWERSAVE:",10)==0){ strncpy(powersave, line+10, 15); powersave[15]=0; return; } if (strncmp(line, "MODE:",5)==0){ strncpy(mode, line+5, 15); mode[15]=0; return; } if (strncmp(line, "MON_CH:",7)==0){ *mon_ch = atoi(line+7); return; } - if (strncmp(line, "DHCP:",5)==0){ *dhcp = atoi(line+5) ? true:false; return; } + if (strncmp(line, "DHCP:",5)==0){ *dhcp = atoi(line+5) ? true:false; return; } + if (strncmp(line, "CSI:",4)==0){ *csi_enable = atoi(line+4) ? true:false; return; } } static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, void *reply_ctx) { @@ -221,6 +223,7 @@ static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, static char band[16]={0}, bw[16]={0}, powersave[16]={0}, mode[16]={0}; static uint8_t mon_ch = 36; static bool dhcp = true; + static bool csi_enable = false; if (!in_cfg) { if (strcmp(line, "CFG") == 0) { @@ -228,7 +231,7 @@ static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, // Reset buffers ssid[0]=0; pass[0]=0; ip[0]=0; mask[0]=0; gw[0]=0; band[0]=0; bw[0]=0; powersave[0]=0; mode[0]=0; - mon_ch = 36; dhcp = true; + mon_ch = 36; dhcp = true; csi_enable = false; return true; // Handled } return false; // Not handled @@ -244,6 +247,14 @@ static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, save_cfg(ssid, pass, ip, mask, gw, dhcp, band, bw, powersave, mode, mon_ch); + // Save CSI enable state + esp_err_t err = csi_mgr_save_enable_state(csi_enable); + if (err == ESP_OK) { + printf("CSI enable state saved: %s\n", csi_enable ? "ENABLED" : "DISABLED"); + } else { + printf("Failed to save CSI state: %s\n", esp_err_to_name(err)); + } + if (reply_func) reply_func("OK\n", reply_ctx); wifi_cfg_apply_from_nvs(); @@ -251,7 +262,7 @@ static bool wifi_cfg_cmd_handler(const char *line, cmd_reply_func_t reply_func, return true; } - on_cfg_line(line, ssid, pass, ip, mask, gw, band, bw, powersave, mode, &mon_ch, &dhcp); + on_cfg_line(line, ssid, pass, ip, mask, gw, band, bw, powersave, mode, &mon_ch, &dhcp, &csi_enable); return true; } diff --git a/config_device.py b/config_device.py index f5584de..3ae3b16 100755 --- a/config_device.py +++ b/config_device.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -ESP32 WiFi Configuration Tool - Static IP with auto-disable DHCP +ESP32 WiFi Configuration Tool - Static IP with auto-disable DHCP and CSI control """ import serial @@ -16,11 +16,12 @@ def log_verbose(message, verbose=False): def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", gateway="192.168.1.1", netmask="255.255.255.0", band="2.4G", bandwidth="HT20", powersave="NONE", - mode="STA", monitor_channel=36, reboot=True, verbose=False): - """Configure ESP32 device via serial with static IP""" + mode="STA", monitor_channel=36, csi_enable=False, + reboot=True, verbose=False): + """Configure ESP32 device via serial with static IP and CSI control""" print(f"\n{'='*70}") - print(f"ESP32 WiFi Configuration (Static IP + Mode)") + print(f"ESP32 WiFi Configuration (Static IP + Mode + CSI)") print(f"{'='*70}") print(f"Port: {port}") print(f"SSID: {ssid}") @@ -34,6 +35,7 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", print(f"Band: {band}") print(f"Bandwidth: {bandwidth}") print(f"PowerSave: {powersave}") + print(f"CSI: {'ENABLED' if csi_enable else 'DISABLED'}") print(f"Reboot: {'Yes' if reboot else 'No'}") print(f"Verbose: {verbose}") print(f"{'='*70}\n") @@ -68,6 +70,7 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", f"POWERSAVE:{powersave}", f"MODE:{mode}", f"MON_CH:{monitor_channel}", + f"CSI:{'1' if csi_enable else '0'}", "END" ] @@ -122,6 +125,9 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", success_indicators.append("✓ Configuration acknowledged (OK)") if "Config saved" in response or "saved to NVS" in response: success_indicators.append("✓ Config saved to NVS") + if "CSI enable state saved" in response: + csi_state = "ENABLED" if csi_enable else "DISABLED" + success_indicators.append(f"✓ CSI {csi_state} saved to NVS") if "got ip:" in response.lower(): success_indicators.append("✓ Device connected to WiFi!") import re @@ -196,6 +202,15 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", boot_warnings.append("✗ NO CONFIG found in NVS") boot_warnings.append(" Device does not see saved config") + # Check CSI status + if "CSI Capture: ENABLED" in boot_msg: + boot_success.append("✓ CSI capture is ENABLED") + elif "CSI Capture: DISABLED" in boot_msg: + if csi_enable: + boot_warnings.append("⚠ CSI is DISABLED but was configured as ENABLED") + else: + boot_success.append("✓ CSI capture is DISABLED (as configured)") + # Check if device got the correct static IP import re ip_match = re.search(r'got ip:(\d+\.\d+\.\d+\.\d+)', boot_msg, re.IGNORECASE) @@ -236,9 +251,11 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", print(f"Port: {port}") print(f"Static IP: {ip}") print(f"SSID: {ssid}") + print(f"Mode: {mode}") print(f"Band: {band}") print(f"Bandwidth: {bandwidth}") print(f"PowerSave: {powersave}") + print(f"CSI: {'ENABLED' if csi_enable else 'DISABLED'}") print(f"DHCP: Disabled (static IP mode)") print(f"{'='*70}") print("\nNext steps:") @@ -248,6 +265,10 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", print(f"\n 2. Verify device has correct IP:") print(f" idf.py -p {port} monitor") print(f" Look for: 'got ip:{ip}'") + if csi_enable: + print(f"\n 3. Verify CSI is capturing:") + print(f" Look for: 'CSI Capture: ENABLED'") + print(f" 'Captured X CSI packets'") return True @@ -272,30 +293,36 @@ def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember", def main(): parser = argparse.ArgumentParser( - description='Configure ESP32 WiFi with static IP (DHCP automatically disabled)', + description='Configure ESP32 WiFi with static IP (DHCP automatically disabled) and CSI control', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - # Configure device #1 for STA mode (iperf baseline testing) + # Configure device #1 for STA mode with CSI DISABLED (baseline testing) %(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA - # Configure device #25 for MONITOR mode on channel 36 (collapse detection) + # Configure device #1 for STA mode with CSI ENABLED + %(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA --csi + + # Configure device #25 for MONITOR mode (collapse detection, CSI not needed) %(prog)s -p /dev/ttyUSB1 -i 192.168.1.90 -M MONITOR -mc 36 + # STA mode with CSI for iperf + CSI correlation testing + %(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA --csi -ps NONE + # Monitor mode on 2.4GHz channel 6 %(prog)s -p /dev/ttyUSB0 -i 192.168.1.91 -M MONITOR -mc 6 -b 2.4G - # STA mode on 5GHz with 40MHz bandwidth - %(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA -b 5G -B HT40 - - # Disable power save for best CSI quality - %(prog)s -p /dev/ttyUSB0 -i 192.168.1.51 -ps NONE + # STA mode on 5GHz with 40MHz bandwidth and CSI + %(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA -b 5G -B HT40 --csi # With verbose output %(prog)s -p /dev/ttyUSB0 -i 192.168.1.51 -v -Note: Mode is saved to NVS and device will auto-start in configured mode on boot. - DHCP is always disabled when using this script since you're providing a static IP. +Note: + - Mode and CSI enable state are saved to NVS + - Device will auto-start in configured mode on boot + - CSI defaults to DISABLED unless --csi flag is used + - DHCP is always disabled when using this script (static IP mode) """ ) @@ -324,6 +351,8 @@ Note: Mode is saved to NVS and device will auto-start in configured mode on boot help='Operating mode: STA (connect to AP, CSI+iperf) or MONITOR (promiscuous, collapse detection) (default: STA)') parser.add_argument('-mc', '--monitor-channel', type=int, default=36, help='Monitor mode channel (1-11 for 2.4GHz, 36-165 for 5GHz) (default: 36)') + parser.add_argument('--csi', action='store_true', + help='Enable CSI capture (default: disabled). Use for devices that need CSI data collection.') parser.add_argument('-r', '--no-reboot', action='store_true', help='Do NOT reboot device after configuration') parser.add_argument('-v', '--verbose', action='store_true', @@ -349,6 +378,7 @@ Note: Mode is saved to NVS and device will auto-start in configured mode on boot powersave=args.powersave, mode=args.mode, monitor_channel=args.monitor_channel, + csi_enable=args.csi, reboot=not args.no_reboot, verbose=args.verbose ) diff --git a/esp32_deploy.py b/esp32_deploy.py new file mode 100755 index 0000000..3629561 --- /dev/null +++ b/esp32_deploy.py @@ -0,0 +1,680 @@ +#!/usr/bin/env python3 +""" +ESP32 Unified Deployment Tool +Combines firmware flashing and device configuration with full control. + +Operation Modes: + Default: Build + Flash + Configure + --config-only: Configure only (no flashing) + --flash-only: Build + Flash only (no configure) + --flash-erase: Erase + Flash + Configure + +Examples: + # Full deployment (default: flash + config) + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 + + # Config only (firmware already flashed) + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 --config-only + + # Flash only (preserve existing config) + ./esp32_deploy.py --flash-only + + # Full erase + flash + config + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 --flash-erase + + # Limit concurrent flash for unpowered USB hub + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 --max-concurrent 2 + + # Retry specific failed devices (automatic sequential flashing) + ./esp32_deploy.py --devices /dev/ttyUSB3,/dev/ttyUSB6,/dev/ttyUSB7 -s ClubHouse2G -P ez2remember --start-ip 192.168.1.63 + + # CSI-enabled devices + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.111 --csi + + # Monitor mode on channel 36 + ./esp32_deploy.py --start-ip 192.168.1.90 -M MONITOR -mc 36 --config-only + + # 5GHz with 40MHz bandwidth + ./esp32_deploy.py -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 -b 5G -B HT40 +""" + +import asyncio +import serial_asyncio +import sys +import os +import argparse +import ipaddress +import re +import time +import logging +from pathlib import Path + +# Ensure detection script is available +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +try: + import detect_esp32 +except ImportError: + print("Error: 'detect_esp32.py' not found.") + sys.exit(1) + +# --- Configuration --- +DEFAULT_MAX_CONCURRENT_FLASH = 4 # Conservative default for USB hub power limits + +class Colors: + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + CYAN = '\033[96m' + RESET = '\033[0m' + +class DeviceLoggerAdapter(logging.LoggerAdapter): + def process(self, msg, kwargs): + return '[%s] %s' % (self.extra['connid'], msg), kwargs + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S') +logger = logging.getLogger("Deploy") + +class UnifiedDeployWorker: + """Handles both flashing and configuration for a single ESP32 device""" + + def __init__(self, port, target_ip, args, build_dir, flash_sem): + self.port = port + self.target_ip = target_ip + self.args = args + self.build_dir = build_dir + self.flash_sem = flash_sem + self.log = DeviceLoggerAdapter(logger, {'connid': port}) + + # Regex Patterns + self.regex_ready = re.compile(r'Initialization complete|GPS synced|GPS initialization aborted|No Config Found', re.IGNORECASE) + self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE) + self.regex_monitor_success = re.compile(r'Monitor mode active', re.IGNORECASE) + self.regex_csi_saved = re.compile(r'CSI enable state saved', re.IGNORECASE) + self.regex_status_connected = re.compile(r'WiFi connected: Yes', re.IGNORECASE) + self.regex_error = re.compile(r'Error:|Failed|Disconnect', re.IGNORECASE) + + async def run(self): + """Main execution workflow""" + try: + # Phase 1: Flash (if not config-only) + if not self.args.config_only: + async with self.flash_sem: + if self.args.flash_erase: + if not await self._erase_flash(): + return False # HARD FAILURE: Flash Erase Failed + if not await self._flash_firmware(): + return False # HARD FAILURE: Flash Write Failed + + # Wait for port to stabilize after flash + await asyncio.sleep(1.0) + + # Phase 2: Configure (if not flash-only) + if not self.args.flash_only: + if self.args.ssid and self.args.password: + if not await self._configure_device(): + # SOFT FAILURE: Config failed, but we treat it as success if flash passed + self.log.warning(f"{Colors.YELLOW}Configuration verification failed. Marking as SUCCESS (Flash was OK).{Colors.RESET}") + # We proceed to return True at the end + else: + self.log.warning("No SSID/Password provided, skipping config") + if self.args.config_only: + return False + else: + self.log.info(f"{Colors.GREEN}Flash Complete (Config Skipped){Colors.RESET}") + + return True + + except Exception as e: + self.log.error(f"Worker Exception: {e}") + return False + + async def _erase_flash(self): + """Erase entire flash memory""" + self.log.info(f"{Colors.YELLOW}Erasing flash...{Colors.RESET}") + cmd = ['esptool.py', '-p', self.port, '-b', '115200', 'erase_flash'] + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + + if proc.returncode == 0: + self.log.info("Erase successful") + return True + + self.log.error(f"Erase failed: {stderr.decode()}") + return False + + async def _flash_firmware(self): + """Flash firmware to device""" + self.log.info("Flashing firmware...") + cmd = [ + 'esptool.py', '-p', self.port, '-b', str(self.args.baud), + '--before', 'default_reset', '--after', 'hard_reset', + 'write_flash', '@flash_args' + ] + + proc = await asyncio.create_subprocess_exec( + *cmd, + cwd=self.build_dir, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300) + except asyncio.TimeoutError: + proc.kill() + self.log.error("Flash timeout") + return False + + if proc.returncode == 0: + self.log.info("Flash successful") + return True + + self.log.error(f"Flash failed: {stderr.decode()}") + return False + + async def _configure_device(self): + """Configure device via serial console""" + self.log.info("Connecting to console...") + + try: + reader, writer = await serial_asyncio.open_serial_connection( + url=self.port, + baudrate=115200 + ) + except Exception as e: + self.log.error(f"Serial open failed: {e}") + return False + + try: + # Step 1: Hardware Reset (only if config-only mode) + if self.args.config_only: + self.log.info("Resetting device...") + writer.transport.serial.dtr = False + writer.transport.serial.rts = True + await asyncio.sleep(0.1) + writer.transport.serial.rts = False + await asyncio.sleep(0.1) + writer.transport.serial.dtr = True + + # Step 2: Wait for Boot + if not await self._wait_for_boot(reader): + self.log.warning("Boot prompt missed, attempting config anyway...") + + # Step 3: Send Configuration + await self._send_config(writer) + + # Step 4: Verify Success + return await self._verify_configuration(reader) + + except Exception as e: + self.log.error(f"Config error: {e}") + return False + finally: + writer.close() + await writer.wait_closed() + + async def _wait_for_boot(self, reader): + """Wait for device boot completion""" + self.log.info("Waiting for boot...") + timeout = time.time() + 10 + + while time.time() < timeout: + try: + line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5) + line = line_bytes.decode('utf-8', errors='ignore').strip() + + if self.regex_ready.search(line): + return True + + except asyncio.TimeoutError: + continue + + return False + + async def _send_config(self, writer): + """Build and send configuration message""" + csi_val = '1' if self.args.csi_enable else '0' + self.log.info(f"Sending config for {self.target_ip} (Mode:{self.args.mode}, CSI:{csi_val})...") + + config_str = ( + f"CFG\n" + f"SSID:{self.args.ssid}\n" + f"PASS:{self.args.password}\n" + f"IP:{self.target_ip}\n" + f"MASK:{self.args.netmask}\n" + f"GW:{self.args.gateway}\n" + f"DHCP:0\n" + f"BAND:{self.args.band}\n" + f"BW:{self.args.bandwidth}\n" + f"POWERSAVE:{self.args.powersave}\n" + f"MODE:{self.args.mode}\n" + f"MON_CH:{self.args.monitor_channel}\n" + f"CSI:{csi_val}\n" + f"END\n" + ) + + writer.write(config_str.encode('utf-8')) + await writer.drain() + + async def _verify_configuration(self, reader): + """Verify configuration success""" + self.log.info("Verifying configuration...") + timeout = time.time() + 20 + csi_saved = False + + while time.time() < timeout: + try: + line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0) + line = line_bytes.decode('utf-8', errors='ignore').strip() + if not line: + continue + + # Check for CSI save confirmation + if self.regex_csi_saved.search(line): + csi_saved = True + + # Check for Station Mode Success (IP Address) + m_ip = self.regex_got_ip.search(line) + if m_ip: + got_ip = m_ip.group(1) + if got_ip == self.target_ip: + csi_msg = f" {Colors.CYAN}(CSI saved){Colors.RESET}" if csi_saved else "" + self.log.info(f"{Colors.GREEN}SUCCESS: Assigned {got_ip}{csi_msg}{Colors.RESET}") + return True + else: + self.log.warning(f"IP MISMATCH: Wanted {self.target_ip}, got {got_ip}") + + # Check for Monitor Mode Success + if self.regex_monitor_success.search(line): + csi_msg = f" {Colors.CYAN}(CSI saved){Colors.RESET}" if csi_saved else "" + self.log.info(f"{Colors.GREEN}SUCCESS: Monitor Mode Active{csi_msg}{Colors.RESET}") + return True + + # Check for status command responses + if self.regex_status_connected.search(line): + csi_msg = f" {Colors.CYAN}(CSI saved){Colors.RESET}" if csi_saved else "" + self.log.info(f"{Colors.GREEN}SUCCESS: Connected{csi_msg}{Colors.RESET}") + return True + + # Check for errors + if self.regex_error.search(line): + self.log.warning(f"Device error: {line}") + + except asyncio.TimeoutError: + continue + + self.log.error("Timeout: Device did not confirm configuration") + return False + +def parse_args(): + parser = argparse.ArgumentParser( + description='ESP32 Unified Deployment Tool', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Operation Modes: + Default: Build + Flash + Configure + --config-only: Configure only (no flashing) + --flash-only: Build + Flash only (no configure) + --flash-erase: Erase + Flash + Configure + +Examples: + # Full deployment (flash + config) + %(prog)s -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 + + # Config only (no flashing) + %(prog)s -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 --config-only + + # Flash only (preserve config) + %(prog)s --flash-only + + # Full erase + deploy + %(prog)s -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 --flash-erase + + # CSI-enabled devices + %(prog)s -s ClubHouse2G -P ez2remember --start-ip 192.168.1.111 --csi + + # Monitor mode + %(prog)s --start-ip 192.168.1.90 -M MONITOR -mc 36 --config-only + + # 5GHz with 40MHz bandwidth + %(prog)s -s ClubHouse2G -P ez2remember --start-ip 192.168.1.81 -b 5G -B HT40 + """ + ) + + # Operation Mode + mode_group = parser.add_argument_group('Operation Mode') + mode_group.add_argument('--config-only', action='store_true', + help='Configure only (no flashing)') + mode_group.add_argument('--flash-only', action='store_true', + help='Flash only (no configure)') + mode_group.add_argument('--flash-erase', action='store_true', + help='Erase flash before flashing') + + # Build/Flash Options + flash_group = parser.add_argument_group('Flash Options') + flash_group.add_argument('-d', '--dir', default=os.getcwd(), + help='Project directory (default: current)') + flash_group.add_argument('-b', '--baud', type=int, default=460800, + help='Flash baud rate (default: 460800)') + flash_group.add_argument('--devices', type=str, + help='Comma-separated list of devices (e.g., /dev/ttyUSB3,/dev/ttyUSB6,/dev/ttyUSB7) for selective deployment/retry. ' + 'Automatically uses sequential flashing to avoid power issues.') + flash_group.add_argument('--max-concurrent', type=int, default=None, + help=f'Max concurrent flash operations (default: {DEFAULT_MAX_CONCURRENT_FLASH}). ' + 'Defaults to 1 when using --devices, unless explicitly set. ' + 'Lower this if you experience USB power issues. ' + 'Try 2-3 for unpowered hubs, 4-8 for powered hubs.') + + # Network Configuration + net_group = parser.add_argument_group('Network Configuration') + net_group.add_argument('--start-ip', required=True, + help='Starting static IP address') + net_group.add_argument('-s', '--ssid', default='ClubHouse2G', + help='WiFi SSID (default: ClubHouse2G)') + net_group.add_argument('-P', '--password', default='ez2remember', + help='WiFi password (default: ez2remember)') + net_group.add_argument('-g', '--gateway', default='192.168.1.1', + help='Gateway IP (default: 192.168.1.1)') + net_group.add_argument('-m', '--netmask', default='255.255.255.0', + help='Netmask (default: 255.255.255.0)') + + # WiFi Configuration + wifi_group = parser.add_argument_group('WiFi Configuration') + wifi_group.add_argument('--band', default='2.4G', choices=['2.4G', '5G'], + help='WiFi band (default: 2.4G)') + wifi_group.add_argument('-B', '--bandwidth', default='HT20', + choices=['HT20', 'HT40', 'VHT80'], + help='Channel bandwidth (default: HT20)') + wifi_group.add_argument('-ps', '--powersave', default='NONE', + help='Power save mode (default: NONE)') + + # Mode Configuration + mode_config_group = parser.add_argument_group('Device Mode Configuration') + mode_config_group.add_argument('-M', '--mode', default='STA', + choices=['STA', 'MONITOR'], + help='Operating mode (default: STA)') + mode_config_group.add_argument('-mc', '--monitor-channel', type=int, default=36, + help='Monitor mode channel (default: 36)') + + # Feature Flags + feature_group = parser.add_argument_group('Feature Flags') + feature_group.add_argument('--csi', dest='csi_enable', action='store_true', + help='Enable CSI capture (default: disabled)') + + args = parser.parse_args() + + # Validation + if args.config_only and args.flash_only: + parser.error("Cannot use --config-only and --flash-only together") + + if args.flash_erase and args.config_only: + parser.error("Cannot use --flash-erase with --config-only") + + if args.flash_erase and args.flash_only: + parser.error("Cannot use --flash-erase with --flash-only (use default mode)") + + if not args.config_only and not args.flash_only: + # Default mode or flash-erase mode + if not args.ssid or not args.password: + parser.error("SSID and password required for flash+config mode") + + return args + +def extract_device_number(device_path): + """Extract numeric suffix from device path (e.g., /dev/ttyUSB3 -> 3)""" + match = re.search(r'(\d+)$', device_path) + if match: + return int(match.group(1)) + return 0 # Default to 0 if no number found + +async def run_deployment(args): + """Main deployment orchestration""" + + # Determine operation mode + if args.config_only: + mode_str = f"{Colors.CYAN}CONFIG ONLY{Colors.RESET}" + elif args.flash_only: + mode_str = f"{Colors.YELLOW}FLASH ONLY{Colors.RESET}" + elif args.flash_erase: + mode_str = f"{Colors.RED}ERASE + FLASH + CONFIG{Colors.RESET}" + else: + mode_str = f"{Colors.GREEN}FLASH + CONFIG{Colors.RESET}" + + print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}") + print(f" ESP32 Unified Deployment Tool") + print(f" Operation Mode: {mode_str}") + print(f"{Colors.BLUE}{'='*60}{Colors.RESET}\n") + + project_dir = Path(args.dir).resolve() + build_dir = project_dir / 'build' + + # Phase 1: Build Firmware (if needed) + if not args.config_only: + print(f"{Colors.YELLOW}[1/3] Building Firmware...{Colors.RESET}") + proc = await asyncio.create_subprocess_exec( + 'idf.py', 'build', + cwd=project_dir, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.PIPE + ) + _, stderr = await proc.communicate() + + if proc.returncode != 0: + print(f"{Colors.RED}Build Failed:\n{stderr.decode()}{Colors.RESET}") + return + + if not (build_dir / 'flash_args').exists(): + print(f"{Colors.RED}Error: build/flash_args missing{Colors.RESET}") + return + + print(f"{Colors.GREEN}Build Complete{Colors.RESET}") + else: + print(f"{Colors.CYAN}[1/3] Skipping Build (Config-Only Mode){Colors.RESET}") + + # Phase 2: Detect Devices + step_num = 2 if not args.config_only else 1 + print(f"{Colors.YELLOW}[{step_num}/3] Detecting Devices...{Colors.RESET}") + + # Get device list + if args.devices: + # User specified devices explicitly + device_list = [d.strip() for d in args.devices.split(',')] + print(f"{Colors.CYAN}Using specified devices: {', '.join(device_list)}{Colors.RESET}") + + # Create device objects for specified devices + class SimpleDevice: + def __init__(self, path): + self.device = path + + devices = [SimpleDevice(d) for d in device_list] + + # Validate devices exist + for dev in devices: + if not os.path.exists(dev.device): + print(f"{Colors.RED}Warning: Device {dev.device} not found{Colors.RESET}") + else: + # Auto-detect all devices + devices = detect_esp32.detect_esp32_devices() + if not devices: + print(f"{Colors.RED}No devices found{Colors.RESET}") + return + + # Sort devices naturally + def natural_keys(d): + return [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', d.device)] + devices.sort(key=natural_keys) + + print(f"{Colors.GREEN}Found {len(devices)} device{'s' if len(devices) != 1 else ''}{Colors.RESET}") + + # Validate start IP + try: + start_ip_obj = ipaddress.IPv4Address(args.start_ip) + except: + print(f"{Colors.RED}Invalid start IP: {args.start_ip}{Colors.RESET}") + return + + # Phase 3: Deploy + step_num = 3 if not args.config_only else 2 + operation = "Configuring" if args.config_only else "Deploying to" + print(f"{Colors.YELLOW}[{step_num}/3] {operation} {len(devices)} devices...{Colors.RESET}\n") + + # Show device-to-IP mapping when using --devices + if args.devices and not args.flash_only: + print(f"{Colors.CYAN}Device-to-IP Mapping:{Colors.RESET}") + for dev in devices: + dev_num = extract_device_number(dev.device) + target_ip = str(start_ip_obj + dev_num) + print(f" {dev.device} -> {target_ip}") + print() + + # Determine flash concurrency + if args.max_concurrent is not None: + # Priority 1: User explicitly set a limit (honored always) + max_concurrent = args.max_concurrent + print(f"{Colors.CYAN}Flash Mode: {max_concurrent} concurrent operations (User Override){Colors.RESET}\n") + elif args.devices and not args.config_only: + # Priority 2: Retry/Specific mode defaults to sequential for safety + max_concurrent = 1 + print(f"{Colors.CYAN}Flash Mode: Sequential (retry mode default){Colors.RESET}\n") + else: + # Priority 3: Standard Bulk mode defaults to constant + max_concurrent = DEFAULT_MAX_CONCURRENT_FLASH + if not args.config_only: + print(f"{Colors.CYAN}Flash Mode: {max_concurrent} concurrent operations{Colors.RESET}\n") + + flash_sem = asyncio.Semaphore(max_concurrent) + tasks = [] + + # Map device to target IP + # If --devices specified, use USB number as offset; otherwise use enumerate index + device_ip_map = [] + + for i, dev in enumerate(devices): + if args.devices: + # Extract USB number and use as offset (e.g., ttyUSB3 -> offset 3) + device_num = extract_device_number(dev.device) + target_ip = str(start_ip_obj + device_num) + device_ip_map.append((dev.device, target_ip, device_num)) + else: + # Use enumerate index as offset + target_ip = str(start_ip_obj + i) + device_ip_map.append((dev.device, target_ip, i)) + + worker = UnifiedDeployWorker(dev.device, target_ip, args, build_dir, flash_sem) + tasks.append(worker.run()) + + # Execute all tasks concurrently + results = await asyncio.gather(*tasks) + + # Phase 4: Summary + success = results.count(True) + failed = len(devices) - success + + # Track failed devices + failed_devices = [] + for i, (dev, result) in enumerate(zip(devices, results)): + if not result: + failed_devices.append(dev.device) + + # Determine feature status for summary + features = [] + + # Add SSID (if configured) + if not args.flash_only: + features.append(f"SSID: {args.ssid}") + + if args.csi_enable: + features.append(f"CSI: {Colors.GREEN}ENABLED{Colors.RESET}") + else: + features.append(f"CSI: DISABLED") + + if args.mode == 'MONITOR': + features.append(f"Mode: MONITOR (Ch {args.monitor_channel})") + else: + features.append(f"Mode: STA") + + features.append(f"Band: {args.band}") + features.append(f"BW: {args.bandwidth}") + features.append(f"Power Save: {args.powersave}") + + print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}") + print(f" Deployment Summary") + print(f"{Colors.BLUE}{'='*60}{Colors.RESET}") + print(f" Total Devices: {len(devices)}") + print(f" Success: {Colors.GREEN}{success}{Colors.RESET}") + print(f" Failed: {Colors.RED}{failed}{Colors.RESET}" if failed > 0 else f" Failed: {failed}") + print(f"{Colors.BLUE}{'-'*60}{Colors.RESET}") + for feature in features: + print(f" {feature}") + print(f"{Colors.BLUE}{'='*60}{Colors.RESET}") + + # Show failed devices and retry command if any failed + if failed_devices: + print(f"\n{Colors.RED}Failed Devices:{Colors.RESET}") + for dev in failed_devices: + # Show device and its intended IP + dev_num = extract_device_number(dev) + intended_ip = str(start_ip_obj + dev_num) + print(f" {dev} -> {intended_ip}") + + # Generate retry command + device_list = ','.join(failed_devices) + retry_cmd = f"./esp32_deploy.py --devices {device_list}" + + # Add original arguments to retry command + if args.ssid: + retry_cmd += f" -s {args.ssid}" + if args.password: + retry_cmd += f" -P {args.password}" + + # Use original starting IP - device number extraction will handle the offset + if not args.flash_only: + retry_cmd += f" --start-ip {args.start_ip}" + + if args.band != '2.4G': + retry_cmd += f" --band {args.band}" + if args.bandwidth != 'HT20': + retry_cmd += f" -B {args.bandwidth}" + if args.powersave != 'NONE': + retry_cmd += f" -ps {args.powersave}" + if args.mode != 'STA': + retry_cmd += f" -M {args.mode}" + if args.monitor_channel != 36: + retry_cmd += f" -mc {args.monitor_channel}" + if args.csi_enable: + retry_cmd += " --csi" + if args.config_only: + retry_cmd += " --config-only" + elif args.flash_only: + retry_cmd += " --flash-only" + elif args.flash_erase: + retry_cmd += " --flash-erase" + if args.baud != 460800: + retry_cmd += f" -b {args.baud}" + if args.max_concurrent is not None: + retry_cmd += f" --max-concurrent {args.max_concurrent}" + + print(f"\n{Colors.YELLOW}Retry Command:{Colors.RESET}") + print(f" {retry_cmd}\n") + else: + print() # Extra newline for clean output + +def main(): + args = parse_args() + + if os.name == 'nt': + asyncio.set_event_loop(asyncio.ProactorEventLoop()) + + try: + asyncio.run(run_deployment(args)) + except KeyboardInterrupt: + print(f"\n{Colors.YELLOW}Deployment cancelled by user{Colors.RESET}") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/main/main.c b/main/main.c index def10fb..6282ca4 100644 --- a/main/main.c +++ b/main/main.c @@ -20,7 +20,7 @@ #include "csi_log.h" #include "csi_manager.h" #include "wifi_controller.h" -#include "app_console.h" // <--- New Component +#include "app_console.h" #include "iperf.h" static const char *TAG = "MAIN"; @@ -49,13 +49,18 @@ static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_ status_led_set_state(LED_STATE_CONNECTED); - // Start App Services - csi_mgr_enable_async(); + // Start App Services - Only enable CSI if configured in NVS + if (csi_mgr_should_enable()) { + ESP_LOGI(TAG, "CSI enabled in config - starting capture"); + csi_mgr_enable_async(); + csi_mgr_schedule_dump(); + } else { + ESP_LOGI(TAG, "CSI disabled in config - skipping capture"); + } + // Always start iperf server iperf_cfg_t cfg = { .flag = IPERF_FLAG_SERVER | IPERF_FLAG_TCP, .sport = 5001 }; iperf_start(&cfg); - - csi_mgr_schedule_dump(); } } @@ -101,6 +106,10 @@ void app_main(void) { ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL)); // 6. Application Start + // Display CSI config status + bool csi_enabled = csi_mgr_should_enable(); + ESP_LOGI(TAG, "CSI Capture: %s", csi_enabled ? "ENABLED" : "DISABLED"); + if (wifi_cfg_apply_from_nvs()) { status_led_set_state(LED_STATE_WAITING); @@ -113,4 +122,44 @@ void app_main(void) { status_led_set_state(LED_STATE_NO_CONFIG); ESP_LOGW(TAG, "No Config Found. Waiting for setup..."); } + + // 7. Enter Console Loop (CRITICAL FIX) + // This keeps the main task alive to process UART commands from the Python script + ESP_LOGI(TAG, "Initialization complete. Entering console loop."); + + const char* prompt = LOG_COLOR_I "esp32> " LOG_RESET_COLOR; + int probe_status = linenoiseProbe(); + if (probe_status) { + printf("\n" + "Your terminal application does not support escape sequences.\n" + "Line editing and history features are disabled.\n" + "On Windows, try using Putty instead.\n"); + linenoiseSetDumbMode(1); + prompt = "esp32> "; + } + + while (true) { + // This blocks until a line is received from UART + char* line = linenoise(prompt); + if (line == NULL) { /* Break on EOF or error */ + break; + } + + if (strlen(line) > 0) { + linenoiseHistoryAdd(line); + + // Try to run the command + int ret; + esp_err_t err = esp_console_run(line, &ret); + if (err == ESP_ERR_NOT_FOUND) { + printf("Unrecognized command\n"); + } else if (err == ESP_OK && ret != ESP_OK) { + printf("Command returned non-zero error code: 0x%x (%s)\n", ret, esp_err_to_name(ret)); + } else if (err != ESP_OK) { + printf("Internal error: %s\n", esp_err_to_name(err)); + } + } + + linenoiseFree(line); + } } diff --git a/sdkconfig.defaults.c5 b/sdkconfig.defaults.c5 index adea6e3..dec667f 100644 --- a/sdkconfig.defaults.c5 +++ b/sdkconfig.defaults.c5 @@ -7,11 +7,13 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_c5.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions_c5.csv" # --- Wi-Fi & CSI --- -CONFIG_ESP_WIFI_CSI_ENABLED=y -# Ensure we have enough RX buffers for promiscuous mode/CSI -CONFIG_ESP_WIFI_RX_BA_WIN=32 -CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=64 - +CONFIG_ESP_WIFI_CSI_ENABLED=n +# WiFi RX buffer configuration +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=16 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_RX_BA_WIN=16 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y # --- System Stability --- # Optimize for size to leave more room for CSV logs CONFIG_COMPILER_OPTIMIZATION_SIZE=y