mass deploy

This commit is contained in:
Bob 2025-11-15 12:07:35 -08:00
parent 60c1d49d3e
commit f2abcf450b
2 changed files with 129 additions and 100 deletions

View File

@ -89,7 +89,7 @@ static void led_task(void *arg)
case LED_STATE_CONNECTED: case LED_STATE_CONNECTED:
// Blue solid - connected successfully // Blue solid - connected successfully
set_led_color(0, 0, 255); // Blue set_led_color(0, 255, 0); // Green
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
break; break;

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
ESP32 Mass Deployment Tool ESP32 Mass Deployment Tool
Parallel or sequential flashing and WiFi configuration for multiple ESP32 devices Parallel flashing and WiFi configuration for multiple ESP32 devices
""" """
import os import os
@ -46,6 +46,7 @@ class DeviceDeployer:
self.devices = [] self.devices = []
self.results = {} self.results = {}
self.log_dir = Path('/tmp')
def print_banner(self): def print_banner(self):
"""Print deployment configuration""" """Print deployment configuration"""
@ -119,6 +120,7 @@ class DeviceDeployer:
def flash_and_configure(self, index, device): def flash_and_configure(self, index, device):
"""Flash and configure a single device""" """Flash and configure a single device"""
ip_addr = self.get_ip_for_index(index) ip_addr = self.get_ip_for_index(index)
log_file = self.log_dir / f"esp32_deploy_{index}.log"
log_lines = [] log_lines = []
def log(msg): def log(msg):
@ -133,17 +135,20 @@ class DeviceDeployer:
# Flash firmware # Flash firmware
log("Flashing...") log("Flashing...")
try: try:
subprocess.run( result = subprocess.run(
['idf.py', '-p', device, '-b', str(self.baud_rate), 'flash'], ['idf.py', '-p', device, '-b', str(self.baud_rate), 'flash'],
cwd=self.project_dir, cwd=self.project_dir,
check=True, check=True,
capture_output=not (self.verbose or not self.parallel), capture_output=True,
timeout=300 # 5 minute timeout timeout=300 # 5 minute timeout
) )
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 on attempt {attempt}")
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))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -156,6 +161,8 @@ class DeviceDeployer:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
log(f"✗ Flash timeout on attempt {attempt}") log(f"✗ Flash timeout on attempt {attempt}")
if attempt == self.max_retries: if attempt == self.max_retries:
with open(log_file, 'w') as f:
f.write('\n'.join(log_lines))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -206,6 +213,9 @@ class DeviceDeployer:
if result.returncode == 0: if result.returncode == 0:
log("✓ Ping successful") log("✓ Ping successful")
# Write log file
with open(log_file, 'w') as f:
f.write('\n'.join(log_lines))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -216,6 +226,8 @@ class DeviceDeployer:
else: else:
log(f"✗ Ping failed on attempt {attempt}") log(f"✗ Ping failed on attempt {attempt}")
if attempt == self.max_retries: if attempt == self.max_retries:
with open(log_file, 'w') as f:
f.write('\n'.join(log_lines))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -226,6 +238,9 @@ class DeviceDeployer:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
log(f"✗ Ping timeout on attempt {attempt}") log(f"✗ Ping timeout on attempt {attempt}")
else: else:
# Write log file
with open(log_file, 'w') as f:
f.write('\n'.join(log_lines))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -237,6 +252,8 @@ class DeviceDeployer:
time.sleep(2) time.sleep(2)
# If we get here, all retries failed # If we get here, all retries failed
with open(log_file, 'w') as f:
f.write('\n'.join(log_lines))
return { return {
'index': index, 'index': index,
'device': device, 'device': device,
@ -251,6 +268,10 @@ class DeviceDeployer:
print(f"{Colors.YELLOW}[3/4] Flashing and configuring (parallel)...{Colors.NC}") print(f"{Colors.YELLOW}[3/4] Flashing and configuring (parallel)...{Colors.NC}")
print() print()
# Clean old log files
for f in self.log_dir.glob('esp32_deploy_*.log'):
f.unlink()
# Use ThreadPoolExecutor for parallel execution # Use ThreadPoolExecutor for parallel execution
max_workers = min(32, len(self.devices)) max_workers = min(32, len(self.devices))
@ -275,6 +296,10 @@ class DeviceDeployer:
print(f"{Colors.YELLOW}[3/4] Flashing and configuring (sequential)...{Colors.NC}") print(f"{Colors.YELLOW}[3/4] Flashing and configuring (sequential)...{Colors.NC}")
print() 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)
@ -352,19 +377,20 @@ class DeviceDeployer:
print(f"{Colors.BLUE}{'='*70}{Colors.NC}") print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
# Print log location
print()
print(f"Logs: /tmp/esp32_deploy_*.log")
# Print test commands # Print test commands
print() print()
print("Test commands:") print("Test commands:")
print(f" # Test first device")
print(f" iperf -c {self.get_ip_for_index(0)}")
print()
print(f" # Ping all devices") print(f" # Ping all devices")
ip_range = f"{self.ip_start}..{self.ip_start + len(self.devices) - 1}" 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(f" for i in {{{ip_range}}}; do ping -c 1 {self.ip_base}.$i & done; wait")
print() print()
print(f" # Test first device with iperf")
print(f" iperf -c {self.get_ip_for_index(0)}")
print()
print(f" # Check all device status")
print(f" ./check_device_status.py --reset")
print()
return failed_count return failed_count
@ -374,55 +400,58 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
# Deploy to first 15 devices (parallel, default) # Deploy to first 30 devices (default)
%(prog)s -s ClubHouse2G -p mypassword %(prog)s -s ClubHouse2G -p mypassword
# Deploy sequentially (easier debugging) # Deploy sequentially (easier debugging)
%(prog)s -s ClubHouse2G -p mypassword --sequential %(prog)s -s ClubHouse2G -p mypassword --sequential
# Deploy to 31 devices in parallel # Deploy to all connected devices
%(prog)s -s ClubHouse2G -p mypassword -n 31 %(prog)s -s ClubHouse2G -p mypassword -n 0
# Custom IP range # Custom IP range
%(prog)s -s ClubHouse2G -p mypassword --start-ip 10.0.0.100 %(prog)s -s ClubHouse2G -p mypassword --start-ip 10.0.0.100
# From environment variables # From environment variables
export WIFI_SSID="MyNetwork" export SSID="MyNetwork"
export WIFI_PASSWORD="secret123" export PASSWORD="secret123"
export START_IP="192.168.1.51"
%(prog)s %(prog)s
# Verbose sequential mode (see everything) # Verbose sequential mode (see everything)
%(prog)s -s ClubHouse2G -p mypassword --sequential -v %(prog)s -s ClubHouse2G -p mypassword --sequential -v
# Skip ping verification (faster)
%(prog)s -s ClubHouse2G -p mypassword --no-verify
""" """
) )
parser.add_argument('-d', '--dir', default=os.getcwd(), parser.add_argument('-d', '--dir', default=os.getcwd(),
help='ESP-IDF project directory (default: current dir)') help='ESP-IDF project directory (default: current dir)')
parser.add_argument('-s', '--ssid', parser.add_argument('-s', '--ssid',
default=os.environ.get('WIFI_SSID', 'ClubHouse2G'), default=os.environ.get('SSID', 'ClubHouse2G'),
help='WiFi SSID (default: ClubHouse2G or $WIFI_SSID)') help='WiFi SSID (default: ClubHouse2G or $SSID)')
parser.add_argument('-p', '--password', parser.add_argument('-p', '--password',
default=os.environ.get('WIFI_PASSWORD', ''), default=os.environ.get('PASSWORD', ''),
help='WiFi password (default: $WIFI_PASSWORD)') help='WiFi password (default: $PASSWORD)')
parser.add_argument('--start-ip', default='192.168.1.51', parser.add_argument('--start-ip',
help='Starting IP address (default: 192.168.1.51)') default=os.environ.get('START_IP', '192.168.1.51'),
parser.add_argument('-n', '--num-devices', type=int, default=15, help='Starting IP address (default: 192.168.1.51 or $START_IP)')
help='Number of devices to deploy (default: 15, use 0 for all)') parser.add_argument('-n', '--num-devices', type=int, default=30,
parser.add_argument('-g', '--gateway', default='192.168.1.1', help='Number of devices to deploy (default: 30, use 0 for all)')
help='Gateway IP (default: 192.168.1.1)') parser.add_argument('-g', '--gateway',
parser.add_argument('-m', '--netmask', default='255.255.255.0', default=os.environ.get('GATEWAY', '192.168.1.1'),
help='Network mask (default: 255.255.255.0)') help='Gateway IP (default: 192.168.1.1 or $GATEWAY)')
parser.add_argument('-b', '--baud', type=int, default=460800, parser.add_argument('-m', '--netmask',
help='Baud rate for flashing (default: 460800)') default=os.environ.get('NETMASK', '255.255.255.0'),
parser.add_argument('-r', '--retries', type=int, default=2, help='Network mask (default: 255.255.255.0 or $NETMASK)')
help='Max retries per device (default: 2)') 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', parser.add_argument('--no-verify', action='store_true',
help='Skip ping verification') help='Skip ping verification')
parser.add_argument('--sequential', action='store_true', parser.add_argument('--sequential', action='store_true',
help='Flash devices sequentially instead of parallel (easier debugging)') help='Flash devices sequentially instead of parallel')
parser.add_argument('-v', '--verbose', action='store_true', parser.add_argument('-v', '--verbose', action='store_true',
help='Verbose output') help='Verbose output')
@ -434,7 +463,7 @@ Examples:
print() print()
print("Provide password via:") print("Provide password via:")
print(" 1. Command line: -p 'your_password'") print(" 1. Command line: -p 'your_password'")
print(" 2. Environment: export WIFI_PASSWORD='your_password'") print(" 2. Environment: export PASSWORD='your_password'")
print() print()
print("Example:") print("Example:")
print(f" {sys.argv[0]} -s MyWiFi -p mypassword") print(f" {sys.argv[0]} -s MyWiFi -p mypassword")