add async manual
This commit is contained in:
parent
0a4bce5bf6
commit
05fbff0092
|
|
@ -1,10 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
ESP32 Async Batch Configuration Tool
|
ESP32 Async Batch Configuration Tool
|
||||||
|
The definitive parallel configuration tool.
|
||||||
Configures 30+ ESP32 devices concurrently using non-blocking I/O.
|
Configures 30+ ESP32 devices concurrently using non-blocking I/O.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Concurrent execution (configure 30 devices in <20 seconds)
|
- Concurrent execution (configure 30 devices in <20 seconds)
|
||||||
- Regex-based state detection (Robust against noise)
|
- Robust Regex-based state detection
|
||||||
|
- Supports verifying both Station Mode (IP check) and Monitor Mode
|
||||||
- Context-aware logging
|
- Context-aware logging
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -32,11 +35,10 @@ class DeviceLoggerAdapter(logging.LoggerAdapter):
|
||||||
def process(self, msg, kwargs):
|
def process(self, msg, kwargs):
|
||||||
return '[%s] %s' % (self.extra['connid'], msg), kwargs
|
return '[%s] %s' % (self.extra['connid'], msg), kwargs
|
||||||
|
|
||||||
# Configure clean logging format
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
|
||||||
logger = logging.getLogger("BatchConfig")
|
logger = logging.getLogger("BatchConfig")
|
||||||
|
|
||||||
class AsyncConfigurator:
|
class Esp32Configurator:
|
||||||
"""
|
"""
|
||||||
Manages the lifecycle of configuring a single ESP32 device via Async Serial.
|
Manages the lifecycle of configuring a single ESP32 device via Async Serial.
|
||||||
"""
|
"""
|
||||||
|
|
@ -46,11 +48,15 @@ class AsyncConfigurator:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
||||||
|
|
||||||
# Pre-compile Regex patterns for efficiency (Parsing logic)
|
# --- Regex Patterns ---
|
||||||
|
# Success indicators
|
||||||
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_config_saved = re.compile(r'Config saved|saved to NVS', re.IGNORECASE)
|
self.regex_monitor_success = re.compile(r'Monitor mode active', re.IGNORECASE)
|
||||||
# Prompts that indicate the device is alive and listening
|
|
||||||
|
# Prompts indicating device is booting/ready
|
||||||
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)
|
||||||
|
|
||||||
|
# Error indicators
|
||||||
self.regex_error = re.compile(r'Error:|Failed|Disconnect', re.IGNORECASE)
|
self.regex_error = re.compile(r'Error:|Failed|Disconnect', re.IGNORECASE)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
|
|
@ -62,7 +68,7 @@ class AsyncConfigurator:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Hardware Reset
|
# 1. Hardware Reset (DTR/RTS)
|
||||||
self.log.info("Resetting...")
|
self.log.info("Resetting...")
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
writer.transport.serial.rts = True
|
writer.transport.serial.rts = True
|
||||||
|
|
@ -71,9 +77,10 @@ class AsyncConfigurator:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
writer.transport.serial.dtr = True
|
writer.transport.serial.dtr = True
|
||||||
|
|
||||||
# 2. Wait for Boot (Regex detection)
|
# 2. Wait for Boot
|
||||||
|
# We assume the device is ready when we see logs or a prompt
|
||||||
if not await self._wait_for_boot(reader):
|
if not await self._wait_for_boot(reader):
|
||||||
self.log.warning("Boot prompt missed, sending config anyway...")
|
self.log.warning("Boot prompt missed, attempting config anyway...")
|
||||||
|
|
||||||
# 3. Send Configuration
|
# 3. Send Configuration
|
||||||
await self._send_config(writer)
|
await self._send_config(writer)
|
||||||
|
|
@ -95,7 +102,6 @@ class AsyncConfigurator:
|
||||||
|
|
||||||
while time.time() < timeout:
|
while time.time() < timeout:
|
||||||
try:
|
try:
|
||||||
# Read line with a short timeout to keep checking total time
|
|
||||||
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
||||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
if not line: continue
|
if not line: continue
|
||||||
|
|
@ -110,6 +116,7 @@ class AsyncConfigurator:
|
||||||
"""Builds and transmits the configuration command"""
|
"""Builds and transmits the configuration command"""
|
||||||
self.log.info(f"Sending config for IP {self.target_ip}...")
|
self.log.info(f"Sending config for IP {self.target_ip}...")
|
||||||
|
|
||||||
|
# Construct command block
|
||||||
config_str = (
|
config_str = (
|
||||||
f"CFG\n"
|
f"CFG\n"
|
||||||
f"SSID:{self.args.ssid}\n"
|
f"SSID:{self.args.ssid}\n"
|
||||||
|
|
@ -130,9 +137,9 @@ class AsyncConfigurator:
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
async def _verify_configuration(self, reader):
|
async def _verify_configuration(self, reader):
|
||||||
"""Monitors output for confirmation of IP assignment"""
|
"""Monitors output for confirmation of Success"""
|
||||||
self.log.info("Verifying configuration...")
|
self.log.info("Verifying configuration...")
|
||||||
timeout = time.time() + 15 # 15s connection timeout
|
timeout = time.time() + 15 # 15s verification timeout
|
||||||
|
|
||||||
while time.time() < timeout:
|
while time.time() < timeout:
|
||||||
try:
|
try:
|
||||||
|
|
@ -140,7 +147,7 @@ class AsyncConfigurator:
|
||||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
if not line: continue
|
if not line: continue
|
||||||
|
|
||||||
# Check for IP assignment
|
# Check for Station Mode Success (IP Address)
|
||||||
m_ip = self.regex_got_ip.search(line)
|
m_ip = self.regex_got_ip.search(line)
|
||||||
if m_ip:
|
if m_ip:
|
||||||
got_ip = m_ip.group(1)
|
got_ip = m_ip.group(1)
|
||||||
|
|
@ -150,20 +157,25 @@ class AsyncConfigurator:
|
||||||
else:
|
else:
|
||||||
self.log.warning(f"MISMATCH: Wanted {self.target_ip}, got {got_ip}")
|
self.log.warning(f"MISMATCH: Wanted {self.target_ip}, got {got_ip}")
|
||||||
|
|
||||||
|
# Check for Monitor Mode Success
|
||||||
|
if self.regex_monitor_success.search(line):
|
||||||
|
self.log.info("SUCCESS: Monitor Mode Active")
|
||||||
|
return True
|
||||||
|
|
||||||
# Check for errors
|
# Check for errors
|
||||||
if self.regex_error.search(line):
|
if self.regex_error.search(line):
|
||||||
self.log.warning(f"Device Error: {line}")
|
self.log.warning(f"Device Reported Error: {line}")
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.log.error("Timeout: Device did not report IP address.")
|
self.log.error("Timeout: Device did not confirm configuration.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def main_async():
|
async def main_async():
|
||||||
parser = argparse.ArgumentParser(description='Async ESP32 Batch Config')
|
parser = argparse.ArgumentParser(description='Async ESP32 Batch Config')
|
||||||
|
|
||||||
# Arguments matching your existing tools
|
# Arguments
|
||||||
parser.add_argument('--start-ip', required=True, help='Starting Static IP')
|
parser.add_argument('--start-ip', required=True, help='Starting Static IP')
|
||||||
parser.add_argument('-s', '--ssid', default='ClubHouse2G', help='WiFi SSID')
|
parser.add_argument('-s', '--ssid', default='ClubHouse2G', help='WiFi SSID')
|
||||||
parser.add_argument('-P', '--password', default='ez2remember', help='WiFi password')
|
parser.add_argument('-P', '--password', default='ez2remember', help='WiFi password')
|
||||||
|
|
@ -201,7 +213,7 @@ async def main_async():
|
||||||
|
|
||||||
for i, dev in enumerate(devices):
|
for i, dev in enumerate(devices):
|
||||||
current_ip = str(start_ip_obj + i)
|
current_ip = str(start_ip_obj + i)
|
||||||
configurator = AsyncConfigurator(dev.device, current_ip, args)
|
configurator = Esp32Configurator(dev.device, current_ip, args)
|
||||||
tasks.append(configurator.run())
|
tasks.append(configurator.run())
|
||||||
|
|
||||||
# Run everything at once
|
# Run everything at once
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
ESP32 Async Batch Configuration Tool (Pro Version)
|
|
||||||
Incorporates architectural patterns from flows.py:
|
|
||||||
- Regex-based state detection
|
|
||||||
- Event-driven synchronization
|
|
||||||
- Context-aware logging
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import serial_asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import ipaddress
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Ensure detection script is available
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
try:
|
|
||||||
import detect_esp32
|
|
||||||
except ImportError:
|
|
||||||
print("Error: 'detect_esp32.py' not found.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Logging Setup (Borrowed concept from flows.py) ---
|
|
||||||
class DeviceLoggerAdapter(logging.LoggerAdapter):
|
|
||||||
def process(self, msg, kwargs):
|
|
||||||
return '[%s] %s' % (self.extra['connid'], msg), kwargs
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
|
|
||||||
logger = logging.getLogger("BatchConfig")
|
|
||||||
|
|
||||||
class Esp32Configurator:
|
|
||||||
"""
|
|
||||||
Manages the lifecycle of configuring a single ESP32 device.
|
|
||||||
Uses regex patterns similar to iperf_client in flows.py.
|
|
||||||
"""
|
|
||||||
def __init__(self, port, target_ip, config_args):
|
|
||||||
self.port = port
|
|
||||||
self.target_ip = target_ip
|
|
||||||
self.args = config_args
|
|
||||||
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
|
||||||
|
|
||||||
# Regex Patterns (Inspired by flows.py lines 350+)
|
|
||||||
# We pre-compile these for efficiency
|
|
||||||
self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
|
|
||||||
self.regex_wifi_connected = re.compile(r'WiFi connected: Yes', re.IGNORECASE)
|
|
||||||
self.regex_config_saved = re.compile(r'Config saved', re.IGNORECASE)
|
|
||||||
self.regex_ready_prompt = re.compile(r'Initialization complete|GPS synced|No WiFi config found', re.IGNORECASE)
|
|
||||||
self.regex_error = re.compile(r'Error:|Failed|Disconnect', re.IGNORECASE)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""Main coroutine for this device"""
|
|
||||||
try:
|
|
||||||
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(f"Failed to open port: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. Hardware Reset via DTR/RTS
|
|
||||||
self.log.info("Resetting...")
|
|
||||||
writer.transport.serial.dtr = False
|
|
||||||
writer.transport.serial.rts = True
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
writer.transport.serial.rts = False
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
writer.transport.serial.dtr = True
|
|
||||||
|
|
||||||
# 2. Monitor Boot Stream
|
|
||||||
# We wait until the device settles or we see a prompt
|
|
||||||
await self._wait_for_boot(reader)
|
|
||||||
|
|
||||||
# 3. Send Configuration
|
|
||||||
await self._send_config(writer)
|
|
||||||
|
|
||||||
# 4. Verification Loop
|
|
||||||
success = await self._verify_connection(reader)
|
|
||||||
return success
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(f"Process Exception: {e}")
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
writer.close()
|
|
||||||
await writer.wait_closed()
|
|
||||||
|
|
||||||
async def _wait_for_boot(self, reader):
|
|
||||||
"""Consumes boot logs until device looks ready"""
|
|
||||||
self.log.info("Waiting for boot...")
|
|
||||||
# Give it a max of 5 seconds to settle or show a prompt
|
|
||||||
end_time = time.time() + 5
|
|
||||||
|
|
||||||
while time.time() < end_time:
|
|
||||||
try:
|
|
||||||
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
|
||||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
|
||||||
if not line: continue
|
|
||||||
|
|
||||||
# Check if device is ready to accept commands
|
|
||||||
if self.regex_ready_prompt.search(line):
|
|
||||||
self.log.info("Device ready detected.")
|
|
||||||
return
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
# If silence for 0.5s, it's probably waiting
|
|
||||||
continue
|
|
||||||
|
|
||||||
async def _send_config(self, writer):
|
|
||||||
"""Constructs and writes the config block"""
|
|
||||||
self.log.info(f"Sending config for IP {self.target_ip}...")
|
|
||||||
|
|
||||||
config_str = (
|
|
||||||
f"CFG\n"
|
|
||||||
f"SSID:{self.args.ssid}\n"
|
|
||||||
f"PASS:{self.args.password}\n"
|
|
||||||
f"IP:{self.target_ip}\n"
|
|
||||||
f"MASK:{self.args.netmask}\n"
|
|
||||||
f"GW:{self.args.gateway}\n"
|
|
||||||
f"DHCP:0\n"
|
|
||||||
f"BAND:{self.args.band}\n"
|
|
||||||
f"BW:{self.args.bandwidth}\n"
|
|
||||||
f"POWERSAVE:{self.args.powersave}\n"
|
|
||||||
f"MODE:{self.args.mode}\n"
|
|
||||||
f"MON_CH:{self.args.monitor_channel}\n"
|
|
||||||
f"END\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Flush input buffer before writing to ensure clean state
|
|
||||||
# (Note: asyncio streams don't have a direct flush_input, relies on OS)
|
|
||||||
writer.write(config_str.encode('utf-8'))
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
async def _verify_connection(self, reader):
|
|
||||||
"""Reads stream verifying IP assignment"""
|
|
||||||
self.log.info("Verifying configuration...")
|
|
||||||
end_time = time.time() + 15 # 15s Timeout
|
|
||||||
|
|
||||||
while time.time() < end_time:
|
|
||||||
try:
|
|
||||||
line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
|
||||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
|
||||||
if not line: continue
|
|
||||||
|
|
||||||
# Regex Checks
|
|
||||||
m_ip = self.regex_got_ip.search(line)
|
|
||||||
if m_ip:
|
|
||||||
got_ip = m_ip.group(1)
|
|
||||||
if got_ip == self.target_ip:
|
|
||||||
self.log.info(f"SUCCESS: Assigned {got_ip}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.log.warning(f"MISMATCH: Wanted {self.target_ip}, got {got_ip}")
|
|
||||||
|
|
||||||
if self.regex_error.search(line):
|
|
||||||
self.log.warning(f"Device reported error: {line}")
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.log.error("Timeout waiting for IP confirmation.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def main_async():
|
|
||||||
parser = argparse.ArgumentParser(description='Async ESP32 Batch Config (Pro)')
|
|
||||||
parser.add_argument('--start-ip', required=True, help='Start IP')
|
|
||||||
parser.add_argument('-s', '--ssid', default='ClubHouse2G')
|
|
||||||
parser.add_argument('-P', '--password', default='ez2remember')
|
|
||||||
parser.add_argument('-g', '--gateway', default='192.168.1.1')
|
|
||||||
parser.add_argument('-m', '--netmask', default='255.255.255.0')
|
|
||||||
parser.add_argument('-b', '--band', default='2.4G')
|
|
||||||
parser.add_argument('-B', '--bandwidth', default='HT20')
|
|
||||||
parser.add_argument('-ps', '--powersave', default='NONE')
|
|
||||||
parser.add_argument('-M', '--mode', default='STA')
|
|
||||||
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 1. Detect
|
|
||||||
print("Scanning devices...")
|
|
||||||
devices = detect_esp32.detect_esp32_devices()
|
|
||||||
if not devices:
|
|
||||||
print("No devices found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sort naturally
|
|
||||||
def natural_keys(d):
|
|
||||||
return [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', d.device)]
|
|
||||||
devices.sort(key=natural_keys)
|
|
||||||
|
|
||||||
# 2. Parse IP
|
|
||||||
try:
|
|
||||||
start_ip_obj = ipaddress.IPv4Address(args.start_ip)
|
|
||||||
except:
|
|
||||||
print("Invalid IP")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 3. Create Tasks
|
|
||||||
tasks = []
|
|
||||||
print(f"Configuring {len(devices)} devices concurrently...")
|
|
||||||
|
|
||||||
for i, dev in enumerate(devices):
|
|
||||||
current_ip = str(start_ip_obj + i)
|
|
||||||
configurator = Esp32Configurator(dev.device, current_ip, args)
|
|
||||||
tasks.append(configurator.run())
|
|
||||||
|
|
||||||
# 4. Run All
|
|
||||||
results = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
# 5. Summary
|
|
||||||
success_count = results.count(True)
|
|
||||||
print("\n" + "="*40)
|
|
||||||
print(f"Total: {len(devices)}")
|
|
||||||
print(f"Success: {success_count}")
|
|
||||||
print(f"Failed: {len(devices) - success_count}")
|
|
||||||
print("="*40)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
# Windows/Linux loop compatibility handling (borrowed from ssh_node.py lines 60-65)
|
|
||||||
if os.name == 'nt':
|
|
||||||
loop = asyncio.ProactorEventLoop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
|
|
||||||
asyncio.run(main_async())
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\nCancelled.")
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# ESP32 Fleet Management Tools: User Manual
|
||||||
|
|
||||||
|
This document provides instructions for using the asynchronous Python tools designed to manage, flash, and configure large fleets (30+) of ESP32 devices simultaneously.
|
||||||
|
|
||||||
|
## ⚠️ Build Target Selection (Carve Out)
|
||||||
|
**Critical Step:** Before using the mass deployment tools, you must define which chip architecture you are building for. The `async_mass_deploy.py` script relies on the project's current configuration.
|
||||||
|
|
||||||
|
Run **one** of the following commands in the project root to set the target:
|
||||||
|
|
||||||
|
| Target Hardware | Command |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Original ESP32** | `idf.py set-target esp32` |
|
||||||
|
| **ESP32-S3** | `idf.py set-target esp32s3` |
|
||||||
|
| **ESP32-C5** | `idf.py set-target esp32c5` |
|
||||||
|
|
||||||
|
*Note: Changing the target forces a full clean rebuild. Ensure you see a successful configuration message before proceeding to deployment.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NVS Behavior Summary
|
||||||
|
Understanding how Non-Volatile Storage (NVS) is handled is critical to preventing data loss or configuration mismatch.
|
||||||
|
|
||||||
|
| Script | Operation Mode | NVS Behavior | Use Case |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **async_mass_deploy.py** | Default (No credentials) | **Preserved** | Updating firmware code without changing IP/WiFi settings. |
|
||||||
|
| **async_mass_deploy.py** | With SSID/Pass | **Partially Rewritten** | Updating firmware AND forcing new WiFi credentials/IPs. |
|
||||||
|
| **async_mass_deploy.py** | With `--erase` | **WIPED Completely** | Factory reset. Deletes all settings, calibration, and code. |
|
||||||
|
| **async_batch_config.py** | Normal Run | **Partially Rewritten** | Changing WiFi/IP settings *without* touching firmware. |
|
||||||
|
| **async_find_failed.py** | Audit / Diagnostics | **Read-Only** | Checking status. No changes to NVS. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. async_mass_deploy.py
|
||||||
|
**The "Factory Floor" Tool.** Use this to flash the compiled binary (`.bin`) to the chips. It combines building, flashing (via `esptool`), and optional initial configuration.
|
||||||
|
|
||||||
|
### When to use:
|
||||||
|
* You have new, blank ESP32 chips.
|
||||||
|
* You have modified the C code (`main.c`) and need to update the firmware.
|
||||||
|
* Devices are stuck in a boot loop and need a full erase.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Usage Examples:
|
||||||
|
|
||||||
|
**A. Firmware Update Only (Preserve Settings)**
|
||||||
|
Updates the code but keeps the existing WiFi SSID, Password, and Static IP stored on the device.
|
||||||
|
```bash
|
||||||
|
python3 async_mass_deploy.py
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ESP32 Fleet Management Manual</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 { border-bottom: 2px solid #eaeaea; padding-bottom: 10px; color: #2c3e50; }
|
||||||
|
h2 { margin-top: 30px; color: #34495e; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
||||||
|
h3 { margin-top: 20px; color: #455a64; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th { background-color: #f2f2f2; color: #333; }
|
||||||
|
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #d63384;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #2d3436;
|
||||||
|
color: #dfe6e9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
color: #856404;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #e1f5fe;
|
||||||
|
border-left: 5px solid #039be5;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tag-green { color: #27ae60; font-weight: bold; }
|
||||||
|
.tag-yellow { color: #f39c12; font-weight: bold; }
|
||||||
|
.tag-red { color: #c0392b; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>ESP32 Fleet Management Tools: User Manual</h1>
|
||||||
|
<p>This document provides instructions for using the asynchronous Python tools designed to manage, flash, and configure large fleets (30+) of ESP32 devices simultaneously.</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h2>🛠️ Critical Step: Set Build Target</h2>
|
||||||
|
<p>Before running <code>async_mass_deploy.py</code>, you <strong>must</strong> configure the project for your specific chip architecture. The deployment script uses the currently configured target.</p>
|
||||||
|
<p>Run <strong>one</strong> of the following commands in your project root before deployment:</p>
|
||||||
|
|
||||||
|
<p><strong>For Original ESP32:</strong></p>
|
||||||
|
<pre><code>idf.py set-target esp32</code></pre>
|
||||||
|
|
||||||
|
<p><strong>For ESP32-S3:</strong></p>
|
||||||
|
<pre><code>idf.py set-target esp32s3</code></pre>
|
||||||
|
|
||||||
|
<p><strong>For ESP32-C5:</strong></p>
|
||||||
|
<pre><code>idf.py set-target esp32c5</code></pre>
|
||||||
|
|
||||||
|
<p><em>Note: Changing the target triggers a full re-build. Ensure the build completes successfully before attempting mass deployment.</em></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert">
|
||||||
|
<strong>⚠️ NVS Behavior Summary (Read First)</strong><br>
|
||||||
|
Understanding how Non-Volatile Storage (NVS) is handled is critical to preventing data loss or configuration mismatch.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Script</th>
|
||||||
|
<th>Operation Mode</th>
|
||||||
|
<th>NVS Behavior</th>
|
||||||
|
<th>Use Case</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>async_mass_deploy.py</code></td>
|
||||||
|
<td>Default</td>
|
||||||
|
<td><span class="tag-green">Preserved</span></td>
|
||||||
|
<td>Updating firmware code without changing IP/WiFi settings.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>async_mass_deploy.py</code></td>
|
||||||
|
<td>With SSID/Pass</td>
|
||||||
|
<td><span class="tag-yellow">Partially Rewritten</span></td>
|
||||||
|
<td>Updating firmware AND forcing new WiFi credentials/IPs.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>async_mass_deploy.py</code></td>
|
||||||
|
<td>With <code>--erase</code></td>
|
||||||
|
<td><span class="tag-red">WIPED Completely</span></td>
|
||||||
|
<td>Factory reset. Deletes all settings, calibration, and code.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>async_batch_config.py</code></td>
|
||||||
|
<td>Normal Run</td>
|
||||||
|
<td><span class="tag-yellow">Partially Rewritten</span></td>
|
||||||
|
<td>Changing WiFi/IP settings <em>without</em> touching firmware.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>async_find_failed.py</code></td>
|
||||||
|
<td>Audit / Diagnostics</td>
|
||||||
|
<td><span class="tag-green">Read-Only</span></td>
|
||||||
|
<td>Checking status. No changes to NVS.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>1. async_mass_deploy.py</h2>
|
||||||
|
<p><strong>The "Factory Floor" Tool.</strong> Use this to flash the compiled binary (<code>.bin</code>) to the chips. It combines building, flashing (via <code>esptool</code>), and optional initial configuration.</p>
|
||||||
|
|
||||||
|
<h3>When to use:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>You have new, blank ESP32 chips.</li>
|
||||||
|
<li>You have modified the C code (<code>main.c</code>) and need to update the firmware.</li>
|
||||||
|
<li>Devices are stuck in a boot loop and need a full erase.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Usage Examples:</h3>
|
||||||
|
|
||||||
|
<p><strong>A. Firmware Update Only (Preserve Settings)</strong><br>
|
||||||
|
Updates the code but keeps the existing WiFi SSID, Password, and Static IP stored on the device.</p>
|
||||||
|
<pre><code>python3 async_mass_deploy.py</code></pre>
|
||||||
|
|
||||||
|
<p><strong>B. Full Factory Deployment (Erase & Provision)</strong><br>
|
||||||
|
Wipes the chip clean, flashes new code, and sets up WiFi/IPs from scratch.</p>
|
||||||
|
<pre><code>python3 async_mass_deploy.py \
|
||||||
|
--erase \
|
||||||
|
--start-ip 192.168.1.101 \
|
||||||
|
-s ClubHouse2G \
|
||||||
|
-p ez2remember</code></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>2. async_batch_config.py</h2>
|
||||||
|
<p><strong>The "Field Update" Tool.</strong> Use this to change settings (IP, WiFi, Mode) on devices that are <em>already</em> running valid firmware. It is much faster than <code>mass_deploy</code> because it does not flash binaries.</p>
|
||||||
|
|
||||||
|
<h3>NVS Behavior:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Rewrites:</strong> SSID, Password, IP, Gateway, Netmask, Mode, Bandwidth.</li>
|
||||||
|
<li><strong>Preserves:</strong> Any other NVS keys not explicitly touched by the config command.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Usage Examples:</h3>
|
||||||
|
|
||||||
|
<p><strong>A. Configure for Station Mode (Standard)</strong><br>
|
||||||
|
Sets sequential IPs starting at .101.</p>
|
||||||
|
<pre><code>python3 async_batch_config.py \
|
||||||
|
--start-ip 192.168.1.101 \
|
||||||
|
-s ClubHouse2G \
|
||||||
|
-P ez2remember</code></pre>
|
||||||
|
|
||||||
|
<p><strong>B. Configure for Monitor Mode (Sniffer)</strong><br>
|
||||||
|
Sets devices to listen promiscuously on Channel 36.</p>
|
||||||
|
<pre><code>python3 async_batch_config.py \
|
||||||
|
--start-ip 192.168.1.101 \
|
||||||
|
-s ClubHouse2G \
|
||||||
|
-P ez2remember \
|
||||||
|
-M MONITOR \
|
||||||
|
-mc 36 \
|
||||||
|
-ps NONE</code></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>3. async_find_failed.py</h2>
|
||||||
|
<p><strong>The "Doctor" Tool.</strong> Use this to audit the status of the fleet. It does not flash or configure; it only asks the devices "Are you okay?" and displays the result.</p>
|
||||||
|
|
||||||
|
<h3>NVS Behavior:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Read-Only:</strong> Does not write to NVS during scanning.</li>
|
||||||
|
<li><strong>Recovery:</strong> The "Soft Reconnect" option sends a command to retry WiFi, but does not permanently alter the saved config.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Usage:</h3>
|
||||||
|
<pre><code>python3 async_find_failed.py</code></pre>
|
||||||
|
<p><em>Output:</em> Displays a table of Port, IP, Mode, and LED status.<br>
|
||||||
|
<em>Interactive Menu:</em> Allows you to reboot or send reconnect commands to failed devices found during the scan.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue