more on mass_deploy
This commit is contained in:
parent
67de552327
commit
1288eabd10
|
|
@ -23,12 +23,6 @@ except ImportError:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
# MAX_CONCURRENT_FLASH is limited to 8 to prevent USB bus brownouts.
|
|
||||||
# Flashing an ESP32 causes high current draw (spikes >300mA).
|
|
||||||
# Triggering 30+ flashes simultaneously would draw >10A, overwhelming
|
|
||||||
# standard powered USB hubs and causing devices to reset or disconnect.
|
|
||||||
# This semaphore queues the flashing step while allowing other steps (boot/config)
|
|
||||||
# to run fully parallel.
|
|
||||||
MAX_CONCURRENT_FLASH = 8
|
MAX_CONCURRENT_FLASH = 8
|
||||||
|
|
||||||
class Colors:
|
class Colors:
|
||||||
|
|
@ -53,20 +47,22 @@ class DeployWorker:
|
||||||
self.build_dir = build_dir
|
self.build_dir = build_dir
|
||||||
self.flash_sem = flash_sem
|
self.flash_sem = flash_sem
|
||||||
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
||||||
|
|
||||||
|
# Regex Patterns
|
||||||
self.regex_ready = re.compile(r'Initialization complete|GPS synced|No WiFi config found', re.IGNORECASE)
|
self.regex_ready = re.compile(r'Initialization complete|GPS synced|No WiFi config found', re.IGNORECASE)
|
||||||
self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
|
self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
|
||||||
|
self.regex_error = re.compile(r'Error:|Failed|Disconnect|Auth Expire', re.IGNORECASE)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
# 1. Flash Phase (Semaphore Limited)
|
# 1. Flash Phase
|
||||||
async with self.flash_sem:
|
async with self.flash_sem:
|
||||||
if self.args.erase:
|
if self.args.erase:
|
||||||
if not await self._erase_flash(): return False
|
if not await self._erase_flash(): return False
|
||||||
if not await self._flash_firmware(): return False
|
if not await self._flash_firmware(): return False
|
||||||
|
|
||||||
# 2. Config Phase (Fully Parallel)
|
# 2. Config Phase
|
||||||
# Short sleep to let the port release after esptool closes
|
await asyncio.sleep(1.0) # Wait for port to stabilize after flash reset
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
if self.args.ssid and self.args.password:
|
if self.args.ssid and self.args.password:
|
||||||
if not await self._configure_device(): return False
|
if not await self._configure_device(): return False
|
||||||
|
|
@ -90,7 +86,6 @@ class DeployWorker:
|
||||||
|
|
||||||
async def _flash_firmware(self):
|
async def _flash_firmware(self):
|
||||||
self.log.info("Flashing firmware...")
|
self.log.info("Flashing firmware...")
|
||||||
# Note: flash_args contains relative paths, so we must run from build_dir
|
|
||||||
cmd = ['esptool.py', '-p', self.port, '-b', str(self.args.baud),
|
cmd = ['esptool.py', '-p', self.port, '-b', str(self.args.baud),
|
||||||
'--before', 'default_reset', '--after', 'hard_reset',
|
'--before', 'default_reset', '--after', 'hard_reset',
|
||||||
'write_flash', '@flash_args']
|
'write_flash', '@flash_args']
|
||||||
|
|
@ -125,7 +120,8 @@ class DeployWorker:
|
||||||
try:
|
try:
|
||||||
self.log.info("Waiting for boot...")
|
self.log.info("Waiting for boot...")
|
||||||
booted = False
|
booted = False
|
||||||
end_time = time.time() + 8
|
# Increased boot timeout slightly
|
||||||
|
end_time = time.time() + 10
|
||||||
while time.time() < end_time:
|
while time.time() < end_time:
|
||||||
try:
|
try:
|
||||||
line_b = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
line_b = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
||||||
|
|
@ -136,6 +132,9 @@ class DeployWorker:
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not booted:
|
||||||
|
self.log.warning("Boot prompt missed, sending config blind...")
|
||||||
|
|
||||||
self.log.info(f"Sending config for {self.target_ip}...")
|
self.log.info(f"Sending config for {self.target_ip}...")
|
||||||
config_str = (f"CFG\nSSID:{self.args.ssid}\nPASS:{self.args.password}\n"
|
config_str = (f"CFG\nSSID:{self.args.ssid}\nPASS:{self.args.password}\n"
|
||||||
f"IP:{self.target_ip}\nMASK:{self.args.netmask}\nGW:{self.args.gateway}\n"
|
f"IP:{self.target_ip}\nMASK:{self.args.netmask}\nGW:{self.args.gateway}\n"
|
||||||
|
|
@ -143,21 +142,34 @@ class DeployWorker:
|
||||||
writer.write(config_str.encode('utf-8'))
|
writer.write(config_str.encode('utf-8'))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
self.log.info("Verifying IP...")
|
self.log.info("Verifying IP (Timeout: 30s)...")
|
||||||
start_verify = time.time()
|
start_verify = time.time()
|
||||||
while time.time() < start_verify + 10:
|
|
||||||
|
# INCREASED TIMEOUT to 30s
|
||||||
|
while time.time() < start_verify + 30:
|
||||||
try:
|
try:
|
||||||
line_b = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
line_b = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
||||||
line = line_b.decode('utf-8', errors='ignore')
|
line = line_b.decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
|
# Success Check
|
||||||
m = self.regex_got_ip.search(line)
|
m = self.regex_got_ip.search(line)
|
||||||
if m:
|
if m:
|
||||||
if m.group(1) == self.target_ip:
|
if m.group(1) == self.target_ip:
|
||||||
self.log.info(f"{Colors.GREEN}SUCCESS: Configured & Connected{Colors.RESET}")
|
self.log.info(f"{Colors.GREEN}SUCCESS: Configured & Connected{Colors.RESET}")
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
self.log.warning(f"IP Mismatch: Got {m.group(1)}, Wanted {self.target_ip}")
|
||||||
|
|
||||||
|
# Failure Check
|
||||||
|
if self.regex_error.search(line):
|
||||||
|
self.log.warning(f"Device Reported Error: {line}")
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
self.log.error("Config sent, but no IP confirmation received.")
|
|
||||||
|
self.log.error("Timeout: Config sent, but device did not connect.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Config error: {e}")
|
self.log.error(f"Config error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
@ -169,7 +181,7 @@ def parse_args():
|
||||||
parser = argparse.ArgumentParser(description='Async ESP32 Mass Deployment')
|
parser = argparse.ArgumentParser(description='Async ESP32 Mass Deployment')
|
||||||
parser.add_argument('-d', '--dir', default=os.getcwd(), help='Project dir')
|
parser.add_argument('-d', '--dir', default=os.getcwd(), help='Project dir')
|
||||||
parser.add_argument('-s', '--ssid', help='WiFi SSID')
|
parser.add_argument('-s', '--ssid', help='WiFi SSID')
|
||||||
parser.add_argument('-P', '--password', help='WiFi Password')
|
parser.add_argument('-P', '--password', help='WiFi Password') # Standardized -P
|
||||||
parser.add_argument('--start-ip', default='192.168.1.51', help='Start IP')
|
parser.add_argument('--start-ip', default='192.168.1.51', help='Start IP')
|
||||||
parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud')
|
parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud')
|
||||||
parser.add_argument('--erase', action='store_true', help='Full erase first')
|
parser.add_argument('--erase', action='store_true', help='Full erase first')
|
||||||
|
|
@ -181,7 +193,7 @@ async def run_deployment(args):
|
||||||
project_dir = Path(args.dir).resolve()
|
project_dir = Path(args.dir).resolve()
|
||||||
build_dir = project_dir / 'build'
|
build_dir = project_dir / 'build'
|
||||||
|
|
||||||
# 1. Build Firmware (Sync/Blocking is fine here as it's a single pre-step)
|
# 1. Build Firmware
|
||||||
print(f"{Colors.YELLOW}[1/3] Building Firmware...{Colors.RESET}")
|
print(f"{Colors.YELLOW}[1/3] Building Firmware...{Colors.RESET}")
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
'idf.py', 'build',
|
'idf.py', 'build',
|
||||||
|
|
@ -200,7 +212,7 @@ async def run_deployment(args):
|
||||||
return
|
return
|
||||||
print(f"{Colors.GREEN}Build Complete.{Colors.RESET}")
|
print(f"{Colors.GREEN}Build Complete.{Colors.RESET}")
|
||||||
|
|
||||||
# 2. Detect Devices (Sync call to helper)
|
# 2. Detect Devices
|
||||||
print(f"{Colors.YELLOW}[2/3] Scanning Devices...{Colors.RESET}")
|
print(f"{Colors.YELLOW}[2/3] Scanning Devices...{Colors.RESET}")
|
||||||
devices = detect_esp32.detect_esp32_devices()
|
devices = detect_esp32.detect_esp32_devices()
|
||||||
if not devices:
|
if not devices:
|
||||||
|
|
@ -220,7 +232,6 @@ async def run_deployment(args):
|
||||||
print("Invalid Start IP")
|
print("Invalid Start IP")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialize shared semaphore
|
|
||||||
flash_sem = asyncio.Semaphore(MAX_CONCURRENT_FLASH)
|
flash_sem = asyncio.Semaphore(MAX_CONCURRENT_FLASH)
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
|
|
@ -229,7 +240,6 @@ async def run_deployment(args):
|
||||||
worker = DeployWorker(dev.device, target_ip, args, build_dir, flash_sem)
|
worker = DeployWorker(dev.device, target_ip, args, build_dir, flash_sem)
|
||||||
tasks.append(worker.run())
|
tasks.append(worker.run())
|
||||||
|
|
||||||
# Wait for all workers
|
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# 4. Summary
|
# 4. Summary
|
||||||
|
|
@ -241,14 +251,9 @@ async def run_deployment(args):
|
||||||
print(f"{Colors.BLUE}{'='*40}{Colors.RESET}")
|
print(f"{Colors.BLUE}{'='*40}{Colors.RESET}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 1. Parse Arguments (Sync)
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
# 2. Configure Event Loop (Platform specific)
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
||||||
|
|
||||||
# 3. Run Async Deployment
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(run_deployment(args))
|
asyncio.run(run_deployment(args))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue