From bb0e1814b642c3717605634c91f7102d2834ba61 Mon Sep 17 00:00:00 2001 From: Bob Date: Sun, 7 Dec 2025 20:18:36 -0800 Subject: [PATCH] fixes to mass_deploy for concurrency --- batch_config.py | 158 ++++++++++++++++ mass_deploy.py | 471 +++++++++++++++--------------------------------- 2 files changed, 308 insertions(+), 321 deletions(-) create mode 100755 batch_config.py diff --git a/batch_config.py b/batch_config.py new file mode 100755 index 0000000..0694172 --- /dev/null +++ b/batch_config.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +ESP32 Batch Configuration Tool +Detects all connected ESP32s and configures them with sequential Static IPs. +Requires: detect_esp32.py and config_device.py in the same directory. +""" + +import sys +import os +import argparse +import time +import ipaddress +import re + +# Ensure we can import the other scripts +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +try: + import detect_esp32 + import config_device +except ImportError as e: + print(f"Error: Could not import required modules ({e}).") + print("Make sure 'detect_esp32.py' and 'config_device.py' are in the same folder.") + sys.exit(1) + +def natural_sort_key(device_obj): + """ + Sorts ports naturally (ttyUSB2 comes before ttyUSB10) + """ + s = device_obj.device + # Split string into a list of integers and non-integers + return [int(text) if text.isdigit() else text.lower() + for text in re.split('([0-9]+)', s)] + +def main(): + parser = argparse.ArgumentParser( + description='Batch Config: Detects all ESP32s and configures sequential IPs', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + # Arguments matching config_device.py options + parser.add_argument('--start-ip', required=True, + help='Starting Static IP (e.g., 192.168.1.101). Will increment for each device.') + parser.add_argument('-s', '--ssid', default='ClubHouse2G', + help='WiFi SSID') + parser.add_argument('-P', '--password', default='ez2remember', + help='WiFi password') + parser.add_argument('-g', '--gateway', default='192.168.1.1', + help='Gateway IP') + parser.add_argument('-m', '--netmask', default='255.255.255.0', + help='Netmask') + parser.add_argument('-b', '--band', default='2.4G', choices=['2.4G', '5G'], + help='WiFi band') + parser.add_argument('-B', '--bandwidth', default='HT20', choices=['HT20', 'HT40', 'VHT80'], + help='Channel bandwidth') + parser.add_argument('-ps', '--powersave', default='NONE', + choices=['NONE', 'MIN', 'MIN_MODEM', 'MAX', 'MAX_MODEM'], + help='Power save mode') + parser.add_argument('-M', '--mode', default='STA', choices=['STA', 'MONITOR'], + help='Operating mode') + parser.add_argument('-mc', '--monitor-channel', type=int, default=36, + help='Monitor mode channel') + parser.add_argument('-r', '--no-reboot', action='store_true', + help='Do NOT reboot devices after configuration') + parser.add_argument('-v', '--verbose', action='store_true', + help='Enable verbose output') + + args = parser.parse_args() + + # 1. Detect Devices + print(f"{'='*60}") + print("Step 1: Detecting ESP32 Devices...") + print(f"{'='*60}") + + devices = detect_esp32.detect_esp32_devices() + + if not devices: + print("No ESP32 devices found! Check USB connections.") + sys.exit(1) + + # Sort devices naturally so IPs are assigned in order (USB0, USB1, USB2...) + devices.sort(key=natural_sort_key) + + print(f"Found {len(devices)} devices:") + for d in devices: + print(f" - {d.device} ({d.description})") + print() + + # 2. Parse Starting IP + try: + start_ip_obj = ipaddress.IPv4Address(args.start_ip) + except ipaddress.AddressValueError: + print(f"Error: Invalid IP address format: {args.start_ip}") + sys.exit(1) + + # 3. Configure Each Device + print(f"{'='*60}") + print("Step 2: Configuring Devices Sequentially") + print(f"{'='*60}") + + success_count = 0 + fail_count = 0 + failed_devices = [] + + for index, device in enumerate(devices): + # Calculate current IP + current_ip = str(start_ip_obj + index) + port = device.device + + print(f"\n[{index+1}/{len(devices)}] Configuring {port} with IP {current_ip}...") + + # Call the config function from your existing script + result = config_device.config_device( + port=port, + ip=current_ip, + ssid=args.ssid, + password=args.password, + gateway=args.gateway, + netmask=args.netmask, + band=args.band, + bandwidth=args.bandwidth, + powersave=args.powersave, + mode=args.mode, + monitor_channel=args.monitor_channel, + reboot=not args.no_reboot, + verbose=args.verbose + ) + + if result: + print(f"✓ Success: {port} -> {current_ip}") + success_count += 1 + else: + print(f"✗ Failed: {port}") + fail_count += 1 + failed_devices.append(port) + + # Small delay to prevent USB power spikes if multiple devices reboot simultaneously + if not args.no_reboot and index < len(devices) - 1: + time.sleep(1.0) + + # 4. Summary + print(f"\n{'='*60}") + print("Batch Configuration Complete") + print(f"{'='*60}") + print(f"Total Devices: {len(devices)}") + print(f"Successful: {success_count}") + print(f"Failed: {fail_count}") + + if failed_devices: + print("\nFailed Ports:") + for p in failed_devices: + print(f" - {p}") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nBatch process interrupted by user.") diff --git a/mass_deploy.py b/mass_deploy.py index 7e326dc..e5abe39 100755 --- a/mass_deploy.py +++ b/mass_deploy.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -ESP32 Mass Deployment Tool -Parallel flashing and WiFi configuration for multiple ESP32 devices +ESP32 Mass Deployment Tool (Fixed for Parallel Flashing) +Uses esptool.py directly to bypass CMake locking issues. """ import os @@ -10,6 +10,7 @@ import subprocess import glob import time import argparse +import serial from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path @@ -18,7 +19,7 @@ class Colors: GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' - NC = '\033[0m' # No Color + NC = '\033[0m' class DeviceDeployer: def __init__(self, project_dir, ssid, password, start_ip="192.168.1.51", @@ -39,89 +40,96 @@ class DeviceDeployer: self.verbose = verbose self.parallel = parallel - # Parse IP address - ip_parts = start_ip.split('.') - self.ip_base = '.'.join(ip_parts[:3]) - self.ip_start = int(ip_parts[3]) + # Mode detection + self.config_mode = (self.ssid is not None and self.password is not None) + + if self.start_ip: + ip_parts = start_ip.split('.') + self.ip_base = '.'.join(ip_parts[:3]) + self.ip_start = int(ip_parts[3]) + else: + self.ip_base = "192.168.1" + self.ip_start = 51 self.devices = [] self.results = {} self.log_dir = Path('/tmp') def print_banner(self): - """Print deployment configuration""" print() print(f"{Colors.BLUE}{'='*70}") print("ESP32 Mass Deployment Tool") print(f"{'='*70}{Colors.NC}") print(f"Project: {self.project_dir}") - print(f"SSID: {self.ssid}") - print(f"Password: {'*' * len(self.password)}") - print(f"Start IP: {self.start_ip}") - print(f"Gateway: {self.gateway}") - print(f"Netmask: {self.netmask}") - print(f"Baud Rate: {self.baud_rate}") - print(f"Max Retries: {self.max_retries}") - print(f"Verify Ping: {self.verify_ping}") - print(f"Mode: {'Parallel' if self.parallel else 'Sequential'}") + if self.config_mode: + print(f"Mode: {Colors.YELLOW}FLASH + CONFIGURE{Colors.NC}") + print(f"SSID: {self.ssid}") + print(f"Password: {'*' * len(self.password)}") + print(f"Start IP: {self.start_ip}") + else: + print(f"Mode: {Colors.GREEN}FLASH ONLY (Preserve NVS){Colors.NC}") + + print(f"Flash Baud: {self.baud_rate}") + print(f"Parallel: {self.parallel}") if self.num_devices: print(f"Max Devices: {self.num_devices}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}") def build_firmware(self): - """Build the firmware using idf.py""" + """Build firmware once to generate flash_args""" print() print(f"{Colors.YELLOW}[1/4] Building firmware...{Colors.NC}") - try: - result = subprocess.run( + # We run build to ensure binary and flash_args exist + subprocess.run( ['idf.py', 'build'], cwd=self.project_dir, check=True, capture_output=not self.verbose ) - print(f"{Colors.GREEN}✓ Build complete{Colors.NC}") + + # Verify flash_args exists (critical for parallel flashing) + flash_args_path = self.project_dir / 'build' / 'flash_args' + if not flash_args_path.exists(): + print(f"{Colors.RED}Error: build/flash_args not found.{Colors.NC}") + return False + + print(f"{Colors.GREEN}✓ Build complete (ready for parallel flash){Colors.NC}") return True except subprocess.CalledProcessError as e: print(f"{Colors.RED}✗ Build failed!{Colors.NC}") - if self.verbose: - print(e.stderr.decode() if e.stderr else "") + if self.verbose: print(e.stderr.decode() if e.stderr else "") return False def detect_devices(self): - """Detect connected ESP32 devices""" print() print(f"{Colors.YELLOW}[2/4] Detecting ESP32 devices...{Colors.NC}") - - # Find all USB serial devices self.devices = sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')) if not self.devices: print(f"{Colors.RED}ERROR: No devices found!{Colors.NC}") - print("Connect ESP32 devices via USB and try again.") return False - # Limit to num_devices if specified if self.num_devices and len(self.devices) > self.num_devices: print(f"Limiting to first {self.num_devices} devices") self.devices = self.devices[:self.num_devices] print(f"{Colors.GREEN}Found {len(self.devices)} device(s):{Colors.NC}") for i, device in enumerate(self.devices): - ip = self.get_ip_for_index(i) - print(f" [{i:2d}] {device:14s} → {ip}") - + if self.config_mode: + print(f" [{i:2d}] {device:14s} → {self.get_ip_for_index(i)}") + else: + print(f" [{i:2d}] {device:14s} → (Existing IP)") return True def get_ip_for_index(self, index): - """Calculate IP address for device index""" return f"{self.ip_base}.{self.ip_start + index}" def flash_and_configure(self, index, device): - """Flash and configure a single device""" - ip_addr = self.get_ip_for_index(index) + target_ip = self.get_ip_for_index(index) if self.config_mode else "Existing IP" log_file = self.log_dir / f"esp32_deploy_{index}.log" log_lines = [] + flash_args_file = self.project_dir / 'build' / 'flash_args' def log(msg): log_lines.append(msg) @@ -130,373 +138,194 @@ class DeviceDeployer: for attempt in range(1, self.max_retries + 1): log(f"=== Device {index}: {device} (Attempt {attempt}/{self.max_retries}) ===") - log(f"Target IP: {ip_addr}") - # Flash firmware - log("Flashing...") + # --- FLASHING (Using esptool.py directly) --- + log("Flashing via esptool...") try: + # Construct command: esptool.py -p PORT -b BAUD --before default_reset --after hard_reset write_flash @build/flash_args + cmd = [ + 'esptool.py', + '-p', device, + '-b', str(self.baud_rate), + '--before', 'default_reset', + '--after', 'hard_reset', + 'write_flash', + f"@{flash_args_file}" + ] + result = subprocess.run( - ['idf.py', '-p', device, '-b', str(self.baud_rate), 'flash'], - cwd=self.project_dir, + cmd, + cwd=self.project_dir, # Run from project dir so relative paths in flash_args work check=True, capture_output=True, - timeout=300 # 5 minute timeout + timeout=300 ) log("✓ Flash successful") except subprocess.CalledProcessError as e: - log(f"✗ Flash failed on attempt {attempt}") + log(f"✗ Flash failed: {e.stderr.decode() if e.stderr else 'Unknown error'}") if attempt == self.max_retries: - # Write log file - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'FAILED', - 'log': log_lines - } + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines} time.sleep(2) continue except subprocess.TimeoutExpired: - log(f"✗ Flash timeout on attempt {attempt}") + log("✗ Flash timeout") if attempt == self.max_retries: - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'TIMEOUT', - 'log': log_lines - } + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'TIMEOUT', 'log': log_lines} continue - # Wait for device to boot - log("Waiting for boot...") + # --- CONFIGURATION --- + log("Waiting for boot (3s)...") time.sleep(3) - # Configure WiFi - log("Configuring WiFi...") - try: - config = ( - f"CFG\n" - f"SSID:{self.ssid}\n" - f"PASS:{self.password}\n" - f"IP:{ip_addr}\n" - f"MASK:{self.netmask}\n" - f"GW:{self.gateway}\n" - f"DHCP:0\n" - f"END\n" - ) - - with open(device, 'w') as f: - f.write(config) - - log("✓ Config sent") - except Exception as e: - log(f"✗ Config error: {e}") - - # Wait for network to initialize - log("Waiting for network...") - time.sleep(5) - - # Verify connectivity - if self.verify_ping: - log("Verifying connectivity...") + if self.config_mode: + log(f"Configuring WiFi ({target_ip})...") try: - result = subprocess.run( - ['ping', '-c', '2', '-W', '3', ip_addr], - capture_output=True, - timeout=10 + config = ( + f"CFG\n" + f"SSID:{self.ssid}\n" + f"PASS:{self.password}\n" + f"IP:{target_ip}\n" + f"MASK:{self.netmask}\n" + f"GW:{self.gateway}\n" + f"DHCP:0\n" + f"END\n" ) - if result.returncode == 0: - log("✓ Ping successful") - # Write log file - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'SUCCESS', - 'log': log_lines - } + with serial.Serial(device, 115200, timeout=2, write_timeout=2) as ser: + ser.reset_input_buffer() + ser.write(config.encode('utf-8')) + ser.flush() + + log("✓ Config sent") + + if self.verify_ping: + log("Waiting for network (6s)...") + time.sleep(6) + log(f"Pinging {target_ip}...") + try: + res = subprocess.run(['ping', '-c', '2', '-W', '3', target_ip], capture_output=True, timeout=10) + if res.returncode == 0: + log("✓ Ping successful") + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines} + else: + log("✗ Ping failed") + # Fall through to retry loop or fail + except: + log("✗ Ping error") else: - log(f"✗ Ping failed on attempt {attempt}") - if attempt == self.max_retries: - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'NO_PING', - 'log': log_lines - } - except subprocess.TimeoutExpired: - log(f"✗ Ping timeout on attempt {attempt}") + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines} + + except Exception as e: + log(f"✗ Config error: {e}") else: - # Write log file - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'SUCCESS', - 'log': log_lines - } + log("Configuration skipped (Preserving NVS)") + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines} time.sleep(2) - # If we get here, all retries failed - with open(log_file, 'w') as f: - f.write('\n'.join(log_lines)) - return { - 'index': index, - 'device': device, - 'ip': ip_addr, - 'status': 'FAILED', - 'log': log_lines - } + # Final failure return + with open(log_file, 'w') as f: f.write('\n'.join(log_lines)) + return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines} def deploy_all_parallel(self): - """Deploy to all devices in parallel""" - print() - print(f"{Colors.YELLOW}[3/4] Flashing and configuring (parallel)...{Colors.NC}") print() + print(f"{Colors.YELLOW}[3/4] Flashing (parallel)...{Colors.NC}") - # Clean old log files - for f in self.log_dir.glob('esp32_deploy_*.log'): - f.unlink() - - # Use ThreadPoolExecutor for parallel execution - max_workers = min(32, len(self.devices)) + # 10 workers is a safe limit for USB hubs + max_workers = min(10, len(self.devices)) with ThreadPoolExecutor(max_workers=max_workers) as executor: - # Submit all jobs futures = { executor.submit(self.flash_and_configure, i, device): (i, device) for i, device in enumerate(self.devices) } - # Collect results as they complete for future in as_completed(futures): result = future.result() self.results[result['index']] = result - - # Print immediate status self.print_device_status(result) def deploy_all_sequential(self): - """Deploy to devices one at a time (sequential)""" print() - print(f"{Colors.YELLOW}[3/4] Flashing and configuring (sequential)...{Colors.NC}") - print() - - # Clean old log files - for f in self.log_dir.glob('esp32_deploy_*.log'): - f.unlink() - + print(f"{Colors.YELLOW}[3/4] Flashing (sequential)...{Colors.NC}") for i, device in enumerate(self.devices): print(f"\n{Colors.BLUE}--- Device {i+1}/{len(self.devices)} ---{Colors.NC}") result = self.flash_and_configure(i, device) self.results[result['index']] = result - - # Print status after each device self.print_device_status(result) - print() def print_device_status(self, result): - """Print status for a single device""" status_color = { - 'SUCCESS': Colors.GREEN, - 'NO_PING': Colors.YELLOW, - 'FAILED': Colors.RED, - 'TIMEOUT': Colors.RED + 'SUCCESS': Colors.GREEN, 'NO_PING': Colors.YELLOW, + 'FAILED': Colors.RED, 'TIMEOUT': Colors.RED }.get(result['status'], Colors.RED) - status_text = { - 'SUCCESS': 'OK', - 'NO_PING': 'FLASHED, NO PING', - 'FAILED': 'FAILED', - 'TIMEOUT': 'TIMEOUT' - }.get(result['status'], 'ERROR') - print(f"{status_color}[Device {result['index']:2d}] {result['device']:14s} → " - f"{result['ip']:15s} [{status_text}]{Colors.NC}") + f"{result['ip']:15s} [{result['status']}]{Colors.NC}") def deploy_all(self): - """Deploy to all devices (parallel or sequential)""" - if self.parallel: - self.deploy_all_parallel() - else: - self.deploy_all_sequential() + if self.parallel: self.deploy_all_parallel() + else: self.deploy_all_sequential() def print_summary(self): - """Print deployment summary""" print() print(f"{Colors.YELLOW}[4/4] Deployment Summary{Colors.NC}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}") + success = sum(1 for r in self.results.values() if r['status'] == 'SUCCESS') + failed = sum(1 for r in self.results.values() if r['status'] in ['FAILED', 'TIMEOUT']) - # Count statuses - success_count = sum(1 for r in self.results.values() if r['status'] == 'SUCCESS') - no_ping_count = sum(1 for r in self.results.values() if r['status'] == 'NO_PING') - failed_count = sum(1 for r in self.results.values() if r['status'] in ['FAILED', 'TIMEOUT']) - - # Print all devices for i in range(len(self.devices)): if i in self.results: - result = self.results[i] - status_icon = { - 'SUCCESS': f"{Colors.GREEN}✓{Colors.NC}", - 'NO_PING': f"{Colors.YELLOW}⚠{Colors.NC}", - 'FAILED': f"{Colors.RED}✗{Colors.NC}", - 'TIMEOUT': f"{Colors.RED}✗{Colors.NC}" - }.get(result['status'], f"{Colors.RED}?{Colors.NC}") - - status_msg = { - 'NO_PING': " (no ping response)", - 'FAILED': " (failed)", - 'TIMEOUT': " (timeout)" - }.get(result['status'], "") - - print(f"{status_icon} {result['device']:14s} → {result['ip']}{status_msg}") + r = self.results[i] + icon = f"{Colors.GREEN}✓{Colors.NC}" if r['status'] == 'SUCCESS' else f"{Colors.RED}✗{Colors.NC}" + print(f"{icon} {r['device']:14s} → {r['ip']}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}") - print(f"Total: {len(self.devices)} devices") - print(f"{Colors.GREEN}Success: {success_count}{Colors.NC}") - - if no_ping_count > 0: - print(f"{Colors.YELLOW}Warning: {no_ping_count} (flashed but no ping){Colors.NC}") - - if failed_count > 0: - print(f"{Colors.RED}Failed: {failed_count}{Colors.NC}") - - print(f"{Colors.BLUE}{'='*70}{Colors.NC}") - - # Print log location - print() - print(f"Logs: /tmp/esp32_deploy_*.log") - - # Print test commands - print() - print("Test commands:") - print(f" # Test first device") - print(f" iperf -c {self.get_ip_for_index(0)}") - print() - print(f" # Ping all devices") - ip_range = f"{self.ip_start}..{self.ip_start + len(self.devices) - 1}" - print(f" for i in {{{ip_range}}}; do ping -c 1 {self.ip_base}.$i & done; wait") - print() - - return failed_count + print(f"Total: {len(self.devices)}") + print(f"Success: {success}") + print(f"Failed: {failed}") + return failed def main(): - parser = argparse.ArgumentParser( - description='ESP32 Mass Deployment Tool', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Deploy to first 30 devices (default) - %(prog)s -s ClubHouse2G -p mypassword - - # Deploy sequentially (easier debugging) - %(prog)s -s ClubHouse2G -p mypassword --sequential - - # Deploy to all connected devices - %(prog)s -s ClubHouse2G -p mypassword -n 0 - - # Custom IP range - %(prog)s -s ClubHouse2G -p mypassword --start-ip 10.0.0.100 - - # From environment variables - export SSID="MyNetwork" - export PASSWORD="secret123" - export START_IP="192.168.1.51" - %(prog)s - - # Verbose sequential mode (see everything) - %(prog)s -s ClubHouse2G -p mypassword --sequential -v - """ - ) - - parser.add_argument('-d', '--dir', default=os.getcwd(), - help='ESP-IDF project directory (default: current dir)') - parser.add_argument('-s', '--ssid', - default=os.environ.get('SSID', 'ClubHouse2G'), - help='WiFi SSID (default: ClubHouse2G or $SSID)') - parser.add_argument('-p', '--password', - default=os.environ.get('PASSWORD', ''), - help='WiFi password (default: $PASSWORD)') - parser.add_argument('--start-ip', - default=os.environ.get('START_IP', '192.168.1.51'), - help='Starting IP address (default: 192.168.1.51 or $START_IP)') - parser.add_argument('-n', '--num-devices', type=int, default=30, - help='Number of devices to deploy (default: 30, use 0 for all)') - parser.add_argument('-g', '--gateway', - default=os.environ.get('GATEWAY', '192.168.1.1'), - help='Gateway IP (default: 192.168.1.1 or $GATEWAY)') - parser.add_argument('-m', '--netmask', - default=os.environ.get('NETMASK', '255.255.255.0'), - help='Network mask (default: 255.255.255.0 or $NETMASK)') - parser.add_argument('-b', '--baud', type=int, - default=int(os.environ.get('BAUD_RATE', 460800)), - help='Baud rate for flashing (default: 460800 or $BAUD_RATE)') - parser.add_argument('-r', '--retries', type=int, - default=int(os.environ.get('MAX_RETRIES', 2)), - help='Max retries per device (default: 2 or $MAX_RETRIES)') - parser.add_argument('--no-verify', action='store_true', - help='Skip ping verification') - parser.add_argument('--sequential', action='store_true', - help='Flash devices sequentially instead of parallel') - parser.add_argument('-v', '--verbose', action='store_true', - help='Verbose output') + parser = argparse.ArgumentParser(description='ESP32 Mass Deployment Tool') + parser.add_argument('-d', '--dir', default=os.getcwd(), help='ESP-IDF project dir') + parser.add_argument('-s', '--ssid', help='WiFi SSID (Optional)') + parser.add_argument('-p', '--password', help='WiFi Password (Optional)') + parser.add_argument('--start-ip', default='192.168.1.51', help='Starting IP') + parser.add_argument('-n', '--num-devices', type=int, default=30, help='Max devices') + parser.add_argument('-g', '--gateway', default='192.168.1.1', help='Gateway IP') + parser.add_argument('-m', '--netmask', default='255.255.255.0', help='Netmask') + parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud rate') + parser.add_argument('-r', '--retries', type=int, default=2, help='Retries') + parser.add_argument('--no-verify', action='store_true', help='Skip ping check') + parser.add_argument('--sequential', action='store_true', help='Run sequentially') + parser.add_argument('-v', '--verbose', action='store_true', help='Verbose') args = parser.parse_args() - # Validate password - if not args.password: - print(f"{Colors.RED}ERROR: WiFi password not set!{Colors.NC}") - print() - print("Provide password via:") - print(" 1. Command line: -p 'your_password'") - print(" 2. Environment: export PASSWORD='your_password'") - print() - print("Example:") - print(f" {sys.argv[0]} -s MyWiFi -p mypassword") + # Validate arguments: Must have both SSID+Pass or neither + if (args.ssid and not args.password) or (args.password and not args.ssid): + print(f"{Colors.RED}ERROR: Provide both SSID and Password for config, or neither for flash-only.{Colors.NC}") sys.exit(1) - # Create deployer deployer = DeviceDeployer( - project_dir=args.dir, - ssid=args.ssid, - password=args.password, - start_ip=args.start_ip, - netmask=args.netmask, - gateway=args.gateway, - baud_rate=args.baud, - max_retries=args.retries, - verify_ping=not args.no_verify, + project_dir=args.dir, ssid=args.ssid, password=args.password, + start_ip=args.start_ip, netmask=args.netmask, gateway=args.gateway, + baud_rate=args.baud, max_retries=args.retries, verify_ping=not args.no_verify, num_devices=args.num_devices if args.num_devices > 0 else None, - verbose=args.verbose, - parallel=not args.sequential + verbose=args.verbose, parallel=not args.sequential ) - # Run deployment deployer.print_banner() - - if not deployer.build_firmware(): - sys.exit(1) - - if not deployer.detect_devices(): - sys.exit(1) - + if not deployer.build_firmware(): sys.exit(1) + if not deployer.detect_devices(): sys.exit(1) deployer.deploy_all() failed_count = deployer.print_summary() - sys.exit(failed_count) if __name__ == '__main__':