fixes to mass_deploy for concurrency

This commit is contained in:
Bob 2025-12-07 20:18:36 -08:00
parent 0e2cea8da7
commit bb0e1814b6
2 changed files with 308 additions and 321 deletions

158
batch_config.py Executable file
View File

@ -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.")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
ESP32 Mass Deployment Tool ESP32 Mass Deployment Tool (Fixed for Parallel Flashing)
Parallel flashing and WiFi configuration for multiple ESP32 devices Uses esptool.py directly to bypass CMake locking issues.
""" """
import os import os
@ -10,6 +10,7 @@ import subprocess
import glob import glob
import time import time
import argparse import argparse
import serial
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@ -18,7 +19,7 @@ class Colors:
GREEN = '\033[0;32m' GREEN = '\033[0;32m'
YELLOW = '\033[1;33m' YELLOW = '\033[1;33m'
BLUE = '\033[0;34m' BLUE = '\033[0;34m'
NC = '\033[0m' # No Color NC = '\033[0m'
class DeviceDeployer: class DeviceDeployer:
def __init__(self, project_dir, ssid, password, start_ip="192.168.1.51", def __init__(self, project_dir, ssid, password, start_ip="192.168.1.51",
@ -39,89 +40,96 @@ class DeviceDeployer:
self.verbose = verbose self.verbose = verbose
self.parallel = parallel self.parallel = parallel
# Parse IP address # 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('.') ip_parts = start_ip.split('.')
self.ip_base = '.'.join(ip_parts[:3]) self.ip_base = '.'.join(ip_parts[:3])
self.ip_start = int(ip_parts[3]) self.ip_start = int(ip_parts[3])
else:
self.ip_base = "192.168.1"
self.ip_start = 51
self.devices = [] self.devices = []
self.results = {} self.results = {}
self.log_dir = Path('/tmp') self.log_dir = Path('/tmp')
def print_banner(self): def print_banner(self):
"""Print deployment configuration"""
print() print()
print(f"{Colors.BLUE}{'='*70}") print(f"{Colors.BLUE}{'='*70}")
print("ESP32 Mass Deployment Tool") print("ESP32 Mass Deployment Tool")
print(f"{'='*70}{Colors.NC}") print(f"{'='*70}{Colors.NC}")
print(f"Project: {self.project_dir}") print(f"Project: {self.project_dir}")
if self.config_mode:
print(f"Mode: {Colors.YELLOW}FLASH + CONFIGURE{Colors.NC}")
print(f"SSID: {self.ssid}") print(f"SSID: {self.ssid}")
print(f"Password: {'*' * len(self.password)}") print(f"Password: {'*' * len(self.password)}")
print(f"Start IP: {self.start_ip}") print(f"Start IP: {self.start_ip}")
print(f"Gateway: {self.gateway}") else:
print(f"Netmask: {self.netmask}") print(f"Mode: {Colors.GREEN}FLASH ONLY (Preserve NVS){Colors.NC}")
print(f"Baud Rate: {self.baud_rate}")
print(f"Max Retries: {self.max_retries}") print(f"Flash Baud: {self.baud_rate}")
print(f"Verify Ping: {self.verify_ping}") print(f"Parallel: {self.parallel}")
print(f"Mode: {'Parallel' if self.parallel else 'Sequential'}")
if self.num_devices: if self.num_devices:
print(f"Max Devices: {self.num_devices}") print(f"Max Devices: {self.num_devices}")
print(f"{Colors.BLUE}{'='*70}{Colors.NC}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
def build_firmware(self): def build_firmware(self):
"""Build the firmware using idf.py""" """Build firmware once to generate flash_args"""
print() print()
print(f"{Colors.YELLOW}[1/4] Building firmware...{Colors.NC}") print(f"{Colors.YELLOW}[1/4] Building firmware...{Colors.NC}")
try: try:
result = subprocess.run( # We run build to ensure binary and flash_args exist
subprocess.run(
['idf.py', 'build'], ['idf.py', 'build'],
cwd=self.project_dir, cwd=self.project_dir,
check=True, check=True,
capture_output=not self.verbose 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 return True
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"{Colors.RED}✗ Build failed!{Colors.NC}") print(f"{Colors.RED}✗ Build failed!{Colors.NC}")
if self.verbose: if self.verbose: print(e.stderr.decode() if e.stderr else "")
print(e.stderr.decode() if e.stderr else "")
return False return False
def detect_devices(self): def detect_devices(self):
"""Detect connected ESP32 devices"""
print() print()
print(f"{Colors.YELLOW}[2/4] Detecting ESP32 devices...{Colors.NC}") 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*')) self.devices = sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*'))
if not self.devices: if not self.devices:
print(f"{Colors.RED}ERROR: No devices found!{Colors.NC}") print(f"{Colors.RED}ERROR: No devices found!{Colors.NC}")
print("Connect ESP32 devices via USB and try again.")
return False return False
# Limit to num_devices if specified
if self.num_devices and len(self.devices) > self.num_devices: if self.num_devices and len(self.devices) > self.num_devices:
print(f"Limiting to first {self.num_devices} devices") print(f"Limiting to first {self.num_devices} devices")
self.devices = self.devices[:self.num_devices] self.devices = self.devices[:self.num_devices]
print(f"{Colors.GREEN}Found {len(self.devices)} device(s):{Colors.NC}") print(f"{Colors.GREEN}Found {len(self.devices)} device(s):{Colors.NC}")
for i, device in enumerate(self.devices): for i, device in enumerate(self.devices):
ip = self.get_ip_for_index(i) if self.config_mode:
print(f" [{i:2d}] {device:14s}{ip}") print(f" [{i:2d}] {device:14s}{self.get_ip_for_index(i)}")
else:
print(f" [{i:2d}] {device:14s} → (Existing IP)")
return True return True
def get_ip_for_index(self, index): def get_ip_for_index(self, index):
"""Calculate IP address for device index"""
return f"{self.ip_base}.{self.ip_start + index}" return f"{self.ip_base}.{self.ip_start + index}"
def flash_and_configure(self, index, device): def flash_and_configure(self, index, device):
"""Flash and configure a single device""" target_ip = self.get_ip_for_index(index) if self.config_mode else "Existing IP"
ip_addr = self.get_ip_for_index(index)
log_file = self.log_dir / f"esp32_deploy_{index}.log" log_file = self.log_dir / f"esp32_deploy_{index}.log"
log_lines = [] log_lines = []
flash_args_file = self.project_dir / 'build' / 'flash_args'
def log(msg): def log(msg):
log_lines.append(msg) log_lines.append(msg)
@ -130,373 +138,194 @@ class DeviceDeployer:
for attempt in range(1, self.max_retries + 1): for attempt in range(1, self.max_retries + 1):
log(f"=== Device {index}: {device} (Attempt {attempt}/{self.max_retries}) ===") log(f"=== Device {index}: {device} (Attempt {attempt}/{self.max_retries}) ===")
log(f"Target IP: {ip_addr}")
# Flash firmware # --- FLASHING (Using esptool.py directly) ---
log("Flashing...") log("Flashing via esptool...")
try: 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( result = subprocess.run(
['idf.py', '-p', device, '-b', str(self.baud_rate), 'flash'], cmd,
cwd=self.project_dir, cwd=self.project_dir, # Run from project dir so relative paths in flash_args work
check=True, check=True,
capture_output=True, capture_output=True,
timeout=300 # 5 minute timeout timeout=300
) )
log("✓ Flash successful") log("✓ Flash successful")
except subprocess.CalledProcessError as e: 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: if attempt == self.max_retries:
# Write log file with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
with open(log_file, 'w') as f: return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines}
f.write('\n'.join(log_lines))
return {
'index': index,
'device': device,
'ip': ip_addr,
'status': 'FAILED',
'log': log_lines
}
time.sleep(2) time.sleep(2)
continue continue
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
log(f"✗ Flash timeout on attempt {attempt}") log("✗ Flash timeout")
if attempt == self.max_retries: if attempt == self.max_retries:
with open(log_file, 'w') as f: with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
f.write('\n'.join(log_lines)) return {'index': index, 'device': device, 'ip': target_ip, 'status': 'TIMEOUT', 'log': log_lines}
return {
'index': index,
'device': device,
'ip': ip_addr,
'status': 'TIMEOUT',
'log': log_lines
}
continue continue
# Wait for device to boot # --- CONFIGURATION ---
log("Waiting for boot...") log("Waiting for boot (3s)...")
time.sleep(3) time.sleep(3)
# Configure WiFi if self.config_mode:
log("Configuring WiFi...") log(f"Configuring WiFi ({target_ip})...")
try: try:
config = ( config = (
f"CFG\n" f"CFG\n"
f"SSID:{self.ssid}\n" f"SSID:{self.ssid}\n"
f"PASS:{self.password}\n" f"PASS:{self.password}\n"
f"IP:{ip_addr}\n" f"IP:{target_ip}\n"
f"MASK:{self.netmask}\n" f"MASK:{self.netmask}\n"
f"GW:{self.gateway}\n" f"GW:{self.gateway}\n"
f"DHCP:0\n" f"DHCP:0\n"
f"END\n" f"END\n"
) )
with open(device, 'w') as f: with serial.Serial(device, 115200, timeout=2, write_timeout=2) as ser:
f.write(config) ser.reset_input_buffer()
ser.write(config.encode('utf-8'))
ser.flush()
log("✓ Config sent") 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:
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: except Exception as e:
log(f"✗ Config error: {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...")
try:
result = subprocess.run(
['ping', '-c', '2', '-W', '3', ip_addr],
capture_output=True,
timeout=10
)
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
}
else: else:
log(f"✗ Ping failed on attempt {attempt}") log("Configuration skipped (Preserving NVS)")
if attempt == self.max_retries: with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
with open(log_file, 'w') as f: return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines}
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}")
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
}
time.sleep(2) time.sleep(2)
# If we get here, all retries failed # Final failure return
with open(log_file, 'w') as f: with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
f.write('\n'.join(log_lines)) return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines}
return {
'index': index,
'device': device,
'ip': ip_addr,
'status': 'FAILED',
'log': log_lines
}
def deploy_all_parallel(self): 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()
print(f"{Colors.YELLOW}[3/4] Flashing (parallel)...{Colors.NC}")
# Clean old log files # 10 workers is a safe limit for USB hubs
for f in self.log_dir.glob('esp32_deploy_*.log'): max_workers = min(10, len(self.devices))
f.unlink()
# Use ThreadPoolExecutor for parallel execution
max_workers = min(32, len(self.devices))
with ThreadPoolExecutor(max_workers=max_workers) as executor: with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all jobs
futures = { futures = {
executor.submit(self.flash_and_configure, i, device): (i, device) executor.submit(self.flash_and_configure, i, device): (i, device)
for i, device in enumerate(self.devices) for i, device in enumerate(self.devices)
} }
# Collect results as they complete
for future in as_completed(futures): for future in as_completed(futures):
result = future.result() result = future.result()
self.results[result['index']] = result self.results[result['index']] = result
# Print immediate status
self.print_device_status(result) self.print_device_status(result)
def deploy_all_sequential(self): def deploy_all_sequential(self):
"""Deploy to devices one at a time (sequential)"""
print() print()
print(f"{Colors.YELLOW}[3/4] Flashing and configuring (sequential)...{Colors.NC}") print(f"{Colors.YELLOW}[3/4] Flashing (sequential)...{Colors.NC}")
print()
# Clean old log files
for f in self.log_dir.glob('esp32_deploy_*.log'):
f.unlink()
for i, device in enumerate(self.devices): for i, device in enumerate(self.devices):
print(f"\n{Colors.BLUE}--- Device {i+1}/{len(self.devices)} ---{Colors.NC}") print(f"\n{Colors.BLUE}--- Device {i+1}/{len(self.devices)} ---{Colors.NC}")
result = self.flash_and_configure(i, device) result = self.flash_and_configure(i, device)
self.results[result['index']] = result self.results[result['index']] = result
# Print status after each device
self.print_device_status(result) self.print_device_status(result)
print()
def print_device_status(self, result): def print_device_status(self, result):
"""Print status for a single device"""
status_color = { status_color = {
'SUCCESS': Colors.GREEN, 'SUCCESS': Colors.GREEN, 'NO_PING': Colors.YELLOW,
'NO_PING': Colors.YELLOW, 'FAILED': Colors.RED, 'TIMEOUT': Colors.RED
'FAILED': Colors.RED,
'TIMEOUT': Colors.RED
}.get(result['status'], 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}" 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): def deploy_all(self):
"""Deploy to all devices (parallel or sequential)""" if self.parallel: self.deploy_all_parallel()
if self.parallel: else: self.deploy_all_sequential()
self.deploy_all_parallel()
else:
self.deploy_all_sequential()
def print_summary(self): def print_summary(self):
"""Print deployment summary"""
print() print()
print(f"{Colors.YELLOW}[4/4] Deployment Summary{Colors.NC}") print(f"{Colors.YELLOW}[4/4] Deployment Summary{Colors.NC}")
print(f"{Colors.BLUE}{'='*70}{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)): for i in range(len(self.devices)):
if i in self.results: if i in self.results:
result = self.results[i] r = self.results[i]
status_icon = { icon = f"{Colors.GREEN}{Colors.NC}" if r['status'] == 'SUCCESS' else f"{Colors.RED}{Colors.NC}"
'SUCCESS': f"{Colors.GREEN}{Colors.NC}", print(f"{icon} {r['device']:14s}{r['ip']}")
'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}")
print(f"{Colors.BLUE}{'='*70}{Colors.NC}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
print(f"Total: {len(self.devices)} devices") print(f"Total: {len(self.devices)}")
print(f"{Colors.GREEN}Success: {success_count}{Colors.NC}") print(f"Success: {success}")
print(f"Failed: {failed}")
if no_ping_count > 0: return failed
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
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description='ESP32 Mass Deployment Tool')
description='ESP32 Mass Deployment Tool', parser.add_argument('-d', '--dir', default=os.getcwd(), help='ESP-IDF project dir')
formatter_class=argparse.RawDescriptionHelpFormatter, parser.add_argument('-s', '--ssid', help='WiFi SSID (Optional)')
epilog=""" parser.add_argument('-p', '--password', help='WiFi Password (Optional)')
Examples: parser.add_argument('--start-ip', default='192.168.1.51', help='Starting IP')
# Deploy to first 30 devices (default) parser.add_argument('-n', '--num-devices', type=int, default=30, help='Max devices')
%(prog)s -s ClubHouse2G -p mypassword 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')
# Deploy sequentially (easier debugging) parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud rate')
%(prog)s -s ClubHouse2G -p mypassword --sequential parser.add_argument('-r', '--retries', type=int, default=2, help='Retries')
parser.add_argument('--no-verify', action='store_true', help='Skip ping check')
# Deploy to all connected devices parser.add_argument('--sequential', action='store_true', help='Run sequentially')
%(prog)s -s ClubHouse2G -p mypassword -n 0 parser.add_argument('-v', '--verbose', action='store_true', help='Verbose')
# 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')
args = parser.parse_args() args = parser.parse_args()
# Validate password # Validate arguments: Must have both SSID+Pass or neither
if not args.password: if (args.ssid and not args.password) or (args.password and not args.ssid):
print(f"{Colors.RED}ERROR: WiFi password not set!{Colors.NC}") print(f"{Colors.RED}ERROR: Provide both SSID and Password for config, or neither for flash-only.{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")
sys.exit(1) sys.exit(1)
# Create deployer
deployer = DeviceDeployer( deployer = DeviceDeployer(
project_dir=args.dir, project_dir=args.dir, ssid=args.ssid, password=args.password,
ssid=args.ssid, start_ip=args.start_ip, netmask=args.netmask, gateway=args.gateway,
password=args.password, baud_rate=args.baud, max_retries=args.retries, verify_ping=not args.no_verify,
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, num_devices=args.num_devices if args.num_devices > 0 else None,
verbose=args.verbose, verbose=args.verbose, parallel=not args.sequential
parallel=not args.sequential
) )
# Run deployment
deployer.print_banner() deployer.print_banner()
if not deployer.build_firmware(): sys.exit(1)
if not deployer.build_firmware(): if not deployer.detect_devices(): sys.exit(1)
sys.exit(1)
if not deployer.detect_devices():
sys.exit(1)
deployer.deploy_all() deployer.deploy_all()
failed_count = deployer.print_summary() failed_count = deployer.print_summary()
sys.exit(failed_count) sys.exit(failed_count)
if __name__ == '__main__': if __name__ == '__main__':