fixes to mass_deploy for concurrency
This commit is contained in:
parent
0e2cea8da7
commit
bb0e1814b6
|
|
@ -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.")
|
||||
471
mass_deploy.py
471
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__':
|
||||
|
|
|
|||
Loading…
Reference in New Issue