more control
This commit is contained in:
parent
cf33a8b19a
commit
1b6e61bdb4
293
esp32_deploy.py
293
esp32_deploy.py
|
|
@ -1,17 +1,50 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
import sys
|
||||||
ESP32 Unified Deployment Tool (esp32_deploy)
|
import os
|
||||||
Combines firmware flashing and device configuration with full control.
|
|
||||||
Updates:
|
# --- AUTO-BOOTSTRAP ESP-IDF ENVIRONMENT ---
|
||||||
- ADDED: --ip-device-based support (IP = Start_IP + Port_Number)
|
# This block detects if the script is running without 'get_idf'
|
||||||
- FIXED: Robust IP calculation logic
|
# and automatically re-launches it using the ESP-IDF Python environment.
|
||||||
- PRESERVED: Existing flash/monitor/config workflow
|
try:
|
||||||
"""
|
import serial_asyncio
|
||||||
|
except ImportError:
|
||||||
|
# 1. Define the location of your ESP-IDF Python Env (from your .bashrc)
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
base_env_dir = os.path.join(home, ".espressif", "python_env")
|
||||||
|
idf_python_path = None
|
||||||
|
|
||||||
|
# Search for the v6.0 environment
|
||||||
|
if os.path.exists(base_env_dir):
|
||||||
|
for d in os.listdir(base_env_dir):
|
||||||
|
if d.startswith("idf6.0") and "py3" in d:
|
||||||
|
candidate = os.path.join(base_env_dir, d, "bin", "python")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
idf_python_path = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if idf_python_path:
|
||||||
|
print(f"\033[93m[Bootstrap] Missing libraries. Switching to ESP-IDF Python: {idf_python_path}\033[0m")
|
||||||
|
|
||||||
|
# 2. Update PATH to include 'esptool.py' (which lives in the venv bin)
|
||||||
|
venv_bin = os.path.dirname(idf_python_path)
|
||||||
|
os.environ["PATH"] = venv_bin + os.pathsep + os.environ["PATH"]
|
||||||
|
|
||||||
|
# 3. Optional: Set IDF_PATH if missing
|
||||||
|
if "IDF_PATH" not in os.environ:
|
||||||
|
default_idf = os.path.join(home, "Code", "esp32", "esp-idf-v6")
|
||||||
|
if os.path.exists(default_idf):
|
||||||
|
os.environ["IDF_PATH"] = default_idf
|
||||||
|
|
||||||
|
# 4. Re-execute the script using the correct interpreter
|
||||||
|
os.execv(idf_python_path, [idf_python_path] + sys.argv)
|
||||||
|
else:
|
||||||
|
print("\033[91mError: Could not find 'serial_asyncio' or the ESP-IDF environment.\033[0m")
|
||||||
|
print("Please run 'get_idf' manually.")
|
||||||
|
sys.exit(1)
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
|
|
@ -86,24 +119,26 @@ class UnifiedDeployWorker:
|
||||||
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
self.log = DeviceLoggerAdapter(logger, {'connid': port})
|
||||||
|
|
||||||
self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)')
|
self.regex_chip_type = re.compile(r'Detecting chip type... (ESP32\S*)')
|
||||||
# Matches the log from your updated main.c
|
|
||||||
self.regex_ready = re.compile(r'Entering idle loop|esp32>', re.IGNORECASE)
|
self.regex_ready = re.compile(r'Entering idle loop|esp32>', 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_csi_saved = re.compile(r'CSI enable state saved|Config saved', re.IGNORECASE)
|
self.regex_csi_saved = re.compile(r'CSI enable state saved|Config saved', re.IGNORECASE)
|
||||||
|
self.regex_version = re.compile(r'APP_VERSION:\s*([0-9\.]+)', re.IGNORECASE)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
|
if self.args.check_version:
|
||||||
|
ver = await self._query_version()
|
||||||
|
return ver
|
||||||
|
|
||||||
if not self.args.config_only:
|
if not self.args.config_only:
|
||||||
async with self.flash_sem:
|
async with self.flash_sem:
|
||||||
if self.args.flash_erase:
|
if self.args.flash_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
|
||||||
# Give it a moment to stabilize after flash reset
|
|
||||||
await asyncio.sleep(2.0)
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
if not self.args.flash_only:
|
if not self.args.flash_only:
|
||||||
if self.args.ssid and self.args.password:
|
if self.args.ssid and self.args.password:
|
||||||
# Retry logic
|
|
||||||
success = False
|
success = False
|
||||||
for attempt in range(1, 4):
|
for attempt in range(1, 4):
|
||||||
self.log.info(f"Configuring (Attempt {attempt}/3)...")
|
self.log.info(f"Configuring (Attempt {attempt}/3)...")
|
||||||
|
|
@ -125,6 +160,39 @@ class UnifiedDeployWorker:
|
||||||
self.log.error(f"Worker Exception: {e}")
|
self.log.error(f"Worker Exception: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _query_version(self):
|
||||||
|
try:
|
||||||
|
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
|
||||||
|
writer.transport.serial.dtr = False
|
||||||
|
writer.transport.serial.rts = False
|
||||||
|
writer.write(b'\n')
|
||||||
|
await writer.drain()
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
writer.write(b'version\n')
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
found_version = "Unknown"
|
||||||
|
timeout = time.time() + 2.0
|
||||||
|
|
||||||
|
while time.time() < timeout:
|
||||||
|
try:
|
||||||
|
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.5)
|
||||||
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
|
m = self.regex_version.search(line)
|
||||||
|
if m:
|
||||||
|
found_version = m.group(1)
|
||||||
|
break
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
return found_version
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f"Version Check Error: {e}")
|
||||||
|
return "Error"
|
||||||
|
|
||||||
async def _identify_chip(self):
|
async def _identify_chip(self):
|
||||||
cmd = ['esptool.py', '-p', self.port, 'chip_id']
|
cmd = ['esptool.py', '-p', self.port, 'chip_id']
|
||||||
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
|
|
@ -157,14 +225,12 @@ class UnifiedDeployWorker:
|
||||||
|
|
||||||
suffix = generate_config_suffix(target_to_use, self.args.csi_enable, self.args.ampdu)
|
suffix = generate_config_suffix(target_to_use, self.args.csi_enable, self.args.ampdu)
|
||||||
firmware_dir = self.project_dir / "firmware"
|
firmware_dir = self.project_dir / "firmware"
|
||||||
|
|
||||||
unique_app = None
|
unique_app = None
|
||||||
if firmware_dir.exists():
|
if firmware_dir.exists():
|
||||||
for f in os.listdir(firmware_dir):
|
for f in os.listdir(firmware_dir):
|
||||||
if f.endswith(f"_{suffix}.bin") and not f.startswith("bootloader") and not f.startswith("partition") and not f.startswith("ota_data") and not f.startswith("phy_init"):
|
if f.endswith(f"_{suffix}.bin") and not f.startswith("bootloader") and not f.startswith("partition") and not f.startswith("ota_data") and not f.startswith("phy_init"):
|
||||||
unique_app = f
|
unique_app = f
|
||||||
break
|
break
|
||||||
|
|
||||||
if not unique_app:
|
if not unique_app:
|
||||||
self.log.error(f"Binary for config '{suffix}' not found in firmware/.")
|
self.log.error(f"Binary for config '{suffix}' not found in firmware/.")
|
||||||
return False
|
return False
|
||||||
|
|
@ -172,9 +238,8 @@ class UnifiedDeployWorker:
|
||||||
unique_boot = f"bootloader_{suffix}.bin"
|
unique_boot = f"bootloader_{suffix}.bin"
|
||||||
unique_part = f"partition-table_{suffix}.bin"
|
unique_part = f"partition-table_{suffix}.bin"
|
||||||
unique_ota = f"ota_data_initial_{suffix}.bin"
|
unique_ota = f"ota_data_initial_{suffix}.bin"
|
||||||
unique_args_file = f"flash_args_{suffix}"
|
flash_args_path = firmware_dir / f"flash_args_{suffix}"
|
||||||
|
|
||||||
flash_args_path = firmware_dir / unique_args_file
|
|
||||||
if not flash_args_path.exists():
|
if not flash_args_path.exists():
|
||||||
self.log.error(f"flash_args for {suffix} not found")
|
self.log.error(f"flash_args for {suffix} not found")
|
||||||
return False
|
return False
|
||||||
|
|
@ -182,34 +247,23 @@ class UnifiedDeployWorker:
|
||||||
try:
|
try:
|
||||||
with open(flash_args_path, 'r') as f:
|
with open(flash_args_path, 'r') as f:
|
||||||
content = f.read().replace('\n', ' ').strip()
|
content = f.read().replace('\n', ' ').strip()
|
||||||
|
|
||||||
raw_args = [x for x in content.split(' ') if x]
|
raw_args = [x for x in content.split(' ') if x]
|
||||||
final_args = []
|
final_args = []
|
||||||
|
|
||||||
for arg in raw_args:
|
for arg in raw_args:
|
||||||
if arg.endswith('bootloader.bin'):
|
if arg.endswith('bootloader.bin'): final_args.append(str(firmware_dir / unique_boot))
|
||||||
final_args.append(str(firmware_dir / unique_boot))
|
elif arg.endswith('partition-table.bin'): final_args.append(str(firmware_dir / unique_part))
|
||||||
elif arg.endswith('partition-table.bin'):
|
|
||||||
final_args.append(str(firmware_dir / unique_part))
|
|
||||||
elif arg.endswith('ota_data_initial.bin'):
|
elif arg.endswith('ota_data_initial.bin'):
|
||||||
if (firmware_dir / unique_ota).exists():
|
if (firmware_dir / unique_ota).exists(): final_args.append(str(firmware_dir / unique_ota))
|
||||||
final_args.append(str(firmware_dir / unique_ota))
|
else: continue
|
||||||
else:
|
elif arg.endswith('phy_init_data.bin'): final_args.append(arg)
|
||||||
continue
|
elif arg.endswith('.bin'): final_args.append(str(firmware_dir / unique_app))
|
||||||
elif arg.endswith('phy_init_data.bin'):
|
else: final_args.append(arg)
|
||||||
final_args.append(arg)
|
|
||||||
elif arg.endswith('.bin'):
|
|
||||||
final_args.append(str(firmware_dir / unique_app))
|
|
||||||
else:
|
|
||||||
final_args.append(arg)
|
|
||||||
|
|
||||||
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'] + final_args
|
'write_flash'] + final_args
|
||||||
|
|
||||||
full_path = firmware_dir / unique_app
|
self.log.info(f"Flashing {firmware_dir / unique_app}...")
|
||||||
self.log.info(f"Flashing {full_path}...")
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec(*cmd, cwd=self.project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
proc = await asyncio.create_subprocess_exec(*cmd, cwd=self.project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
try:
|
try:
|
||||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
|
||||||
|
|
@ -220,7 +274,6 @@ class UnifiedDeployWorker:
|
||||||
if proc.returncode == 0: return True
|
if proc.returncode == 0: return True
|
||||||
self.log.error(f"Flash failed: {stderr.decode()}")
|
self.log.error(f"Flash failed: {stderr.decode()}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Flash Prep Error: {e}")
|
self.log.error(f"Flash Prep Error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
@ -230,29 +283,21 @@ class UnifiedDeployWorker:
|
||||||
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
|
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Reset
|
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
writer.transport.serial.rts = True
|
writer.transport.serial.rts = True
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
writer.transport.serial.rts = False
|
writer.transport.serial.rts = False
|
||||||
# FIX: DTR Must be False to allow Booting (True=Low=Bootloader Mode)
|
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
|
|
||||||
# 2. Robust Wait (with Poke)
|
|
||||||
if not await self._wait_for_boot(reader, writer):
|
if not await self._wait_for_boot(reader, writer):
|
||||||
self.log.warning("Boot prompt missed (sending blindly)...")
|
self.log.warning("Boot prompt missed (sending blindly)...")
|
||||||
|
|
||||||
# 3. Send
|
|
||||||
await self._send_config(writer)
|
await self._send_config(writer)
|
||||||
|
|
||||||
# 4. Verify
|
|
||||||
is_configured = await self._verify_configuration(reader)
|
is_configured = await self._verify_configuration(reader)
|
||||||
|
|
||||||
if is_configured:
|
if is_configured:
|
||||||
self.log.info(f"{Colors.GREEN}Config verified. IP: {self.target_ip}{Colors.RESET}")
|
self.log.info(f"{Colors.GREEN}Config verified. IP: {self.target_ip}{Colors.RESET}")
|
||||||
# Final Reset to apply
|
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
writer.transport.serial.rts = True
|
writer.transport.serial.rts = True
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
@ -261,7 +306,6 @@ class UnifiedDeployWorker:
|
||||||
else:
|
else:
|
||||||
self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}")
|
self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}")
|
||||||
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
|
||||||
|
|
@ -270,80 +314,51 @@ class UnifiedDeployWorker:
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
async def _wait_for_boot(self, reader, writer):
|
async def _wait_for_boot(self, reader, writer):
|
||||||
# Timeout covers GPS delay (~3.5s) + boot overhead
|
|
||||||
end_time = time.time() + 12
|
end_time = time.time() + 12
|
||||||
last_poke = time.time()
|
last_poke = time.time()
|
||||||
|
|
||||||
while time.time() < end_time:
|
while time.time() < end_time:
|
||||||
try:
|
try:
|
||||||
# Poke every 1.5 seconds if we haven't seen the prompt
|
|
||||||
if time.time() - last_poke > 1.5:
|
if time.time() - last_poke > 1.5:
|
||||||
writer.write(b'\n')
|
writer.write(b'\n')
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
last_poke = time.time()
|
last_poke = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Short timeout to allow polling loop
|
|
||||||
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.1)
|
line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.1)
|
||||||
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
|
||||||
|
if self.regex_ready.search(line): return True
|
||||||
if self.regex_ready.search(line):
|
except asyncio.TimeoutError: continue
|
||||||
return True
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Read error: {e}")
|
self.log.error(f"Read error: {e}")
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _send_config(self, writer):
|
async def _send_config(self, writer):
|
||||||
# Wait a moment for any last boot logs to clear
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# Wake up console
|
|
||||||
writer.write(b'\n')
|
writer.write(b'\n')
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
csi_val = '1' if self.args.csi_enable else '0'
|
csi_val = '1' if self.args.csi_enable else '0'
|
||||||
role_str = "SERVER" if self.args.iperf_server else "CLIENT"
|
role_str = "SERVER" if self.args.iperf_server else "CLIENT"
|
||||||
iperf_enable_val = '0' if self.args.no_iperf else '1'
|
iperf_enable_val = '0' if self.args.no_iperf else '1'
|
||||||
period_us = int(self.args.iperf_period * 1000000)
|
period_us = int(self.args.iperf_period * 1000000)
|
||||||
|
|
||||||
# Build list of commands using args (which have robust defaults)
|
|
||||||
config_lines = [
|
config_lines = [
|
||||||
"CFG",
|
"CFG",
|
||||||
f"SSID:{self.args.ssid}",
|
f"SSID:{self.args.ssid}", f"PASS:{self.args.password}",
|
||||||
f"PASS:{self.args.password}",
|
f"IP:{self.target_ip}", f"MASK:{self.args.netmask}", f"GW:{self.args.gateway}",
|
||||||
f"IP:{self.target_ip}",
|
f"DHCP:0", f"BAND:{self.args.band}", f"BW:{self.args.bandwidth}",
|
||||||
f"MASK:{self.args.netmask}",
|
f"POWERSAVE:{self.args.powersave}", f"MODE:{self.args.mode}",
|
||||||
f"GW:{self.args.gateway}",
|
f"MON_CH:{self.args.monitor_channel}", f"CSI:{csi_val}",
|
||||||
f"DHCP:0",
|
f"IPERF_PERIOD_US:{period_us}", f"IPERF_ROLE:{role_str}",
|
||||||
f"BAND:{self.args.band}",
|
f"IPERF_PROTO:{self.args.iperf_proto}", f"IPERF_DST_IP:{self.args.iperf_dest_ip}",
|
||||||
f"BW:{self.args.bandwidth}",
|
f"IPERF_PORT:{self.args.iperf_port}", f"IPERF_BURST:{self.args.iperf_burst}",
|
||||||
f"POWERSAVE:{self.args.powersave}",
|
f"IPERF_LEN:{self.args.iperf_len}", f"IPERF_ENABLED:{iperf_enable_val}",
|
||||||
f"MODE:{self.args.mode}",
|
|
||||||
f"MON_CH:{self.args.monitor_channel}",
|
|
||||||
f"CSI:{csi_val}",
|
|
||||||
f"IPERF_PERIOD_US:{period_us}",
|
|
||||||
f"IPERF_ROLE:{role_str}",
|
|
||||||
f"IPERF_PROTO:{self.args.iperf_proto}",
|
|
||||||
f"IPERF_DST_IP:{self.args.iperf_dest_ip}",
|
|
||||||
f"IPERF_PORT:{self.args.iperf_port}",
|
|
||||||
f"IPERF_BURST:{self.args.iperf_burst}",
|
|
||||||
f"IPERF_LEN:{self.args.iperf_len}",
|
|
||||||
f"IPERF_ENABLED:{iperf_enable_val}",
|
|
||||||
"END"
|
"END"
|
||||||
]
|
]
|
||||||
|
|
||||||
# CHANGED: Send line-by-line with a delay to prevent UART FIFO overflow
|
|
||||||
for line in config_lines:
|
for line in config_lines:
|
||||||
cmd = line + "\r\n"
|
cmd = line + "\r\n"
|
||||||
writer.write(cmd.encode('utf-8'))
|
writer.write(cmd.encode('utf-8'))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
# 50ms delay allows the ESP32 (running at 115200 baud) to process the line
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
async def _verify_configuration(self, reader):
|
async def _verify_configuration(self, reader):
|
||||||
|
|
@ -353,7 +368,6 @@ class UnifiedDeployWorker:
|
||||||
line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0)
|
||||||
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
|
||||||
|
|
||||||
if self.regex_csi_saved.search(line): return True
|
if self.regex_csi_saved.search(line): return True
|
||||||
m = self.regex_got_ip.search(line)
|
m = self.regex_got_ip.search(line)
|
||||||
if m and m.group(1) == self.target_ip: return True
|
if m and m.group(1) == self.target_ip: return True
|
||||||
|
|
@ -370,16 +384,13 @@ def parse_args():
|
||||||
parser.add_argument('--config-only', action='store_true')
|
parser.add_argument('--config-only', action='store_true')
|
||||||
parser.add_argument('--flash-only', action='store_true')
|
parser.add_argument('--flash-only', action='store_true')
|
||||||
parser.add_argument('--flash-erase', action='store_true')
|
parser.add_argument('--flash-erase', action='store_true')
|
||||||
|
parser.add_argument('--check-version', action='store_true', help='Check version of connected devices')
|
||||||
parser.add_argument('-d', '--dir', default=os.getcwd())
|
parser.add_argument('-d', '--dir', default=os.getcwd())
|
||||||
parser.add_argument('-b', '--baud', type=int, default=460800)
|
parser.add_argument('-b', '--baud', type=int, default=460800)
|
||||||
parser.add_argument('--devices', type=str)
|
parser.add_argument('--devices', type=str)
|
||||||
parser.add_argument('--max-concurrent', type=int, default=None)
|
parser.add_argument('--max-concurrent', type=int, default=None)
|
||||||
|
parser.add_argument('--start-ip', help='Start IP (Required unless --target all or --check-version)')
|
||||||
# --- IP Configuration ---
|
|
||||||
parser.add_argument('--start-ip', help='Start IP (Required unless --target all)')
|
|
||||||
parser.add_argument('--ip-device-based', action='store_true', help="Use /dev/ttyUSBx number as IP offset")
|
parser.add_argument('--ip-device-based', action='store_true', help="Use /dev/ttyUSBx number as IP offset")
|
||||||
|
|
||||||
# --- Network Defaults (Robustness) ---
|
|
||||||
parser.add_argument('-s', '--ssid', default='ClubHouse2G')
|
parser.add_argument('-s', '--ssid', default='ClubHouse2G')
|
||||||
parser.add_argument('-P', '--password', default='ez2remember')
|
parser.add_argument('-P', '--password', default='ez2remember')
|
||||||
parser.add_argument('-g', '--gateway', default='192.168.1.1')
|
parser.add_argument('-g', '--gateway', default='192.168.1.1')
|
||||||
|
|
@ -387,8 +398,6 @@ def parse_args():
|
||||||
parser.add_argument('--band', default='2.4G')
|
parser.add_argument('--band', default='2.4G')
|
||||||
parser.add_argument('-B', '--bandwidth', default='HT20')
|
parser.add_argument('-B', '--bandwidth', default='HT20')
|
||||||
parser.add_argument('-ps', '--powersave', default='NONE')
|
parser.add_argument('-ps', '--powersave', default='NONE')
|
||||||
|
|
||||||
# --- Iperf Defaults (Robustness) ---
|
|
||||||
parser.add_argument('--iperf-period', type=float, default=0.01)
|
parser.add_argument('--iperf-period', type=float, default=0.01)
|
||||||
parser.add_argument('--iperf-burst', type=int, default=1)
|
parser.add_argument('--iperf-burst', type=int, default=1)
|
||||||
parser.add_argument('--iperf-len', type=int, default=1470)
|
parser.add_argument('--iperf-len', type=int, default=1470)
|
||||||
|
|
@ -398,28 +407,16 @@ def parse_args():
|
||||||
parser.add_argument('--no-iperf', action='store_true')
|
parser.add_argument('--no-iperf', action='store_true')
|
||||||
parser.add_argument('--iperf-client', action='store_true')
|
parser.add_argument('--iperf-client', action='store_true')
|
||||||
parser.add_argument('--iperf-server', action='store_true')
|
parser.add_argument('--iperf-server', action='store_true')
|
||||||
|
|
||||||
# --- Monitor Mode Defaults ---
|
|
||||||
parser.add_argument('-M', '--mode', default='STA')
|
parser.add_argument('-M', '--mode', default='STA')
|
||||||
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
||||||
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.target != 'all' and not args.start_ip and not args.check_version:
|
||||||
if args.target != 'all' and not args.start_ip:
|
|
||||||
parser.error("the following arguments are required: --start-ip")
|
parser.error("the following arguments are required: --start-ip")
|
||||||
if args.config_only and args.flash_only: parser.error("Conflicting modes")
|
if args.config_only and args.flash_only: parser.error("Conflicting modes")
|
||||||
if not args.config_only and not args.flash_only and args.target != 'all':
|
|
||||||
if not args.ssid or not args.password:
|
|
||||||
parser.error("SSID/PASS required")
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def extract_device_number(device_path):
|
def extract_device_number(device_path):
|
||||||
"""
|
|
||||||
Extracts the integer number from a device path.
|
|
||||||
e.g. /dev/ttyUSB14 -> 14
|
|
||||||
/dev/esp_port_14 -> 14
|
|
||||||
"""
|
|
||||||
match = re.search(r'(\d+)$', device_path)
|
match = re.search(r'(\d+)$', device_path)
|
||||||
return int(match.group(1)) if match else 0
|
return int(match.group(1)) if match else 0
|
||||||
|
|
||||||
|
|
@ -452,22 +449,18 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
|
||||||
desc = f"Target={target}, CSI={'ON' if csi else 'OFF'}, AMPDU={'ON' if ampdu else 'OFF'}"
|
desc = f"Target={target}, CSI={'ON' if csi else 'OFF'}, AMPDU={'ON' if ampdu else 'OFF'}"
|
||||||
prefix = f"[{current_step}/{total_steps}] " if current_step else ""
|
prefix = f"[{current_step}/{total_steps}] " if current_step else ""
|
||||||
print(f" {prefix}Building [{desc}] ... ", end='', flush=True)
|
print(f" {prefix}Building [{desc}] ... ", end='', flush=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output_dir = project_dir / "firmware"
|
output_dir = project_dir / "firmware"
|
||||||
output_dir.mkdir(exist_ok=True)
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
sdkconfig_path = project_dir / "sdkconfig"
|
sdkconfig_path = project_dir / "sdkconfig"
|
||||||
build_path = project_dir / "build"
|
build_path = project_dir / "build"
|
||||||
if sdkconfig_path.exists(): os.remove(sdkconfig_path)
|
if sdkconfig_path.exists(): os.remove(sdkconfig_path)
|
||||||
if build_path.exists(): shutil.rmtree(build_path)
|
if build_path.exists(): shutil.rmtree(build_path)
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_exec('idf.py', 'set-target', target, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
proc = await asyncio.create_subprocess_exec('idf.py', 'set-target', target, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
_, stderr = await proc.communicate()
|
_, stderr = await proc.communicate()
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}")
|
print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}")
|
||||||
return False, f"Set Target Failed", 0
|
return False, f"Set Target Failed", 0
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
build_cmd = ['idf.py', '-D', f'SDKCONFIG_DEFAULTS={defaults_str}', 'build']
|
build_cmd = ['idf.py', '-D', f'SDKCONFIG_DEFAULTS={defaults_str}', 'build']
|
||||||
proc = await asyncio.create_subprocess_exec(*build_cmd, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
proc = await asyncio.create_subprocess_exec(*build_cmd, cwd=project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
|
|
@ -476,33 +469,24 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
print(f"{Colors.RED}FAIL{Colors.RESET}")
|
print(f"{Colors.RED}FAIL{Colors.RESET}")
|
||||||
return False, f"Build Failed", duration
|
return False, f"Build Failed", duration
|
||||||
|
|
||||||
build_dir = project_dir / 'build'
|
build_dir = project_dir / 'build'
|
||||||
suffix = generate_config_suffix(target, csi, ampdu)
|
suffix = generate_config_suffix(target, csi, ampdu)
|
||||||
unique_app_name = "Unknown"
|
unique_app_name = "Unknown"
|
||||||
|
|
||||||
project_bin = get_project_binary_name(build_dir)
|
project_bin = get_project_binary_name(build_dir)
|
||||||
if project_bin:
|
if project_bin:
|
||||||
unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin"
|
unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin"
|
||||||
shutil.copy2(build_dir / project_bin, output_dir / unique_app_name)
|
shutil.copy2(build_dir / project_bin, output_dir / unique_app_name)
|
||||||
|
|
||||||
boot_src = build_dir / "bootloader" / "bootloader.bin"
|
boot_src = build_dir / "bootloader" / "bootloader.bin"
|
||||||
if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.bin")
|
if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.bin")
|
||||||
|
|
||||||
part_src = build_dir / "partition_table" / "partition-table.bin"
|
part_src = build_dir / "partition_table" / "partition-table.bin"
|
||||||
if part_src.exists(): shutil.copy2(part_src, output_dir / f"partition-table_{suffix}.bin")
|
if part_src.exists(): shutil.copy2(part_src, output_dir / f"partition-table_{suffix}.bin")
|
||||||
|
|
||||||
# Fix: Save OTA data binary if it exists
|
|
||||||
ota_src = build_dir / "ota_data_initial.bin"
|
ota_src = build_dir / "ota_data_initial.bin"
|
||||||
if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin")
|
if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin")
|
||||||
|
|
||||||
flash_src = build_dir / "flash_args"
|
flash_src = build_dir / "flash_args"
|
||||||
if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}")
|
if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}")
|
||||||
|
|
||||||
full_path = output_dir / unique_app_name
|
full_path = output_dir / unique_app_name
|
||||||
print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}")
|
print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}")
|
||||||
return True, "Success", duration
|
return True, "Success", duration
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{Colors.RED}ERROR: {e}{Colors.RESET}")
|
print(f"{Colors.RED}ERROR: {e}{Colors.RESET}")
|
||||||
return False, str(e), 0
|
return False, str(e), 0
|
||||||
|
|
@ -510,52 +494,34 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s
|
||||||
async def run_deployment(args):
|
async def run_deployment(args):
|
||||||
print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}\n ESP32 Unified Deployment Tool\n{Colors.BLUE}{'='*60}{Colors.RESET}")
|
print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}\n ESP32 Unified Deployment Tool\n{Colors.BLUE}{'='*60}{Colors.RESET}")
|
||||||
project_dir = Path(args.dir).resolve()
|
project_dir = Path(args.dir).resolve()
|
||||||
|
|
||||||
# --- Target 'ALL' Mode ---
|
|
||||||
if args.target == 'all':
|
if args.target == 'all':
|
||||||
print(f"{Colors.YELLOW}Starting Batch Build Verification (12 Combinations){Colors.RESET}")
|
print(f"{Colors.YELLOW}Starting Batch Build Verification (12 Combinations){Colors.RESET}")
|
||||||
|
|
||||||
# SAFETY: Wipe firmware dir to ensure no stale binaries exist
|
|
||||||
firmware_dir = project_dir / "firmware"
|
firmware_dir = project_dir / "firmware"
|
||||||
if firmware_dir.exists():
|
if firmware_dir.exists():
|
||||||
try:
|
try: shutil.rmtree(firmware_dir)
|
||||||
shutil.rmtree(firmware_dir)
|
except Exception as e: return
|
||||||
print(f"{Colors.YELLOW} [Clean] Removed old firmware/ directory.{Colors.RESET}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"{Colors.RED} [Error] Could not clean firmware dir: {e}{Colors.RESET}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Re-create it fresh
|
|
||||||
firmware_dir.mkdir(exist_ok=True)
|
firmware_dir.mkdir(exist_ok=True)
|
||||||
print("") # Spacer
|
|
||||||
|
|
||||||
targets = ['esp32', 'esp32s3', 'esp32c5']
|
targets = ['esp32', 'esp32s3', 'esp32c5']
|
||||||
booleans = [False, True]
|
booleans = [False, True]
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
total_steps = len(targets) * len(booleans) * len(booleans)
|
total_steps = len(targets) * len(booleans) * len(booleans)
|
||||||
current_step = 0
|
current_step = 0
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
for csi in booleans:
|
for csi in booleans:
|
||||||
for ampdu in booleans:
|
for ampdu in booleans:
|
||||||
current_step += 1
|
current_step += 1
|
||||||
success, msg, dur = await build_task(project_dir, target, csi, ampdu, current_step, total_steps)
|
success, msg, dur = await build_task(project_dir, target, csi, ampdu, current_step, total_steps)
|
||||||
results.append({"cfg": f"{target.ljust(9)} CSI:{'ON ' if csi else 'OFF'} AMPDU:{'ON ' if ampdu else 'OFF'}", "ok": success, "dur": dur})
|
results.append({"cfg": f"{target.ljust(9)} CSI:{'ON ' if csi else 'OFF'} AMPDU:{'ON ' if ampdu else 'OFF'}", "ok": success, "dur": dur})
|
||||||
|
|
||||||
print(f"\n{Colors.BLUE}Batch Summary:{Colors.RESET}")
|
print(f"\n{Colors.BLUE}Batch Summary:{Colors.RESET}")
|
||||||
for r in results:
|
for r in results:
|
||||||
status = f"{Colors.GREEN}PASS{Colors.RESET}" if r['ok'] else f"{Colors.RED}FAIL{Colors.RESET}"
|
status = f"{Colors.GREEN}PASS{Colors.RESET}" if r['ok'] else f"{Colors.RED}FAIL{Colors.RESET}"
|
||||||
print(f" {r['cfg']} : {status} ({r['dur']:.1f}s)")
|
print(f" {r['cfg']} : {status} ({r['dur']:.1f}s)")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Single Build Configuration ---
|
if not args.config_only and args.target != 'auto' and not args.check_version:
|
||||||
# Skip build if we are in AUTO mode (we assume binaries exist in firmware/)
|
|
||||||
if not args.config_only and args.target != 'auto':
|
|
||||||
target = args.target if args.target else 'esp32s3'
|
target = args.target if args.target else 'esp32s3'
|
||||||
csi = args.csi_enable
|
csi = args.csi_enable
|
||||||
ampdu = args.ampdu
|
ampdu = args.ampdu
|
||||||
|
|
||||||
if args.interactive:
|
if args.interactive:
|
||||||
print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}")
|
print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}")
|
||||||
target = ask_user("Target Chip", default=target, choices=['esp32', 'esp32s3', 'esp32c5'])
|
target = ask_user("Target Chip", default=target, choices=['esp32', 'esp32s3', 'esp32c5'])
|
||||||
|
|
@ -564,57 +530,58 @@ async def run_deployment(args):
|
||||||
args.csi_enable = csi
|
args.csi_enable = csi
|
||||||
args.target = target
|
args.target = target
|
||||||
args.ampdu = ampdu
|
args.ampdu = ampdu
|
||||||
|
|
||||||
success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1)
|
success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1)
|
||||||
if not success:
|
if not success:
|
||||||
print(f"{Colors.RED}{msg}{Colors.RESET}")
|
print(f"{Colors.RED}{msg}{Colors.RESET}")
|
||||||
return
|
return
|
||||||
elif args.target == 'auto' and not args.config_only:
|
elif args.target == 'auto' and not args.config_only and not args.check_version:
|
||||||
print(f"{Colors.YELLOW}Target 'auto' selected. Skipping build step (assuming artifacts in firmware/).{Colors.RESET}")
|
print(f"{Colors.YELLOW}Target 'auto' selected. Skipping build step (assuming artifacts in firmware/).{Colors.RESET}")
|
||||||
|
|
||||||
# --- Device Detection & Flash ---
|
if args.devices and args.devices.lower() != 'all':
|
||||||
if args.devices:
|
|
||||||
devs = [type('obj', (object,), {'device': d.strip()}) for d in args.devices.split(',')]
|
devs = [type('obj', (object,), {'device': d.strip()}) for d in args.devices.split(',')]
|
||||||
else:
|
else:
|
||||||
# Use AUTO DETECT first (for static names), then standard fallback
|
|
||||||
devs = auto_detect_devices()
|
devs = auto_detect_devices()
|
||||||
if not devs: print("No devices found"); return
|
if not devs: print("No devices found"); return
|
||||||
|
|
||||||
# Sort naturally (esp_port_01 before esp_port_10)
|
|
||||||
devs.sort(key=lambda d: [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', d.device)])
|
devs.sort(key=lambda d: [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', d.device)])
|
||||||
|
|
||||||
print(f"\n{Colors.GREEN}Found {len(devs)} devices{Colors.RESET}")
|
print(f"\n{Colors.GREEN}Found {len(devs)} devices{Colors.RESET}")
|
||||||
start_ip = ipaddress.IPv4Address(args.start_ip)
|
start_ip = ipaddress.IPv4Address(args.start_ip) if args.start_ip else ipaddress.IPv4Address('0.0.0.0')
|
||||||
max_c = args.max_concurrent if args.max_concurrent else (1 if args.devices and not args.config_only else DEFAULT_MAX_CONCURRENT_FLASH)
|
max_c = args.max_concurrent if args.max_concurrent else (1 if args.devices and not args.config_only else DEFAULT_MAX_CONCURRENT_FLASH)
|
||||||
flash_sem = asyncio.Semaphore(max_c)
|
flash_sem = asyncio.Semaphore(max_c)
|
||||||
|
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
for i, dev in enumerate(devs):
|
for i, dev in enumerate(devs):
|
||||||
# --- ROBUST IP CALCULATION LOGIC ---
|
|
||||||
if args.ip_device_based:
|
|
||||||
# Mode A: Offset based on physical port number (e.g. 14 for esp_port_14)
|
|
||||||
raw_port_number = extract_device_number(dev.device)
|
raw_port_number = extract_device_number(dev.device)
|
||||||
|
|
||||||
# FIX: Subtract 1 from the raw port number to make the offset zero-based.
|
# --- NEW LOGIC: Handling Zero-Based (ttyUSB) vs One-Based (esp_port) ---
|
||||||
# This ensures esp_port_1 gets the exact --start-ip (offset 0).
|
if args.ip_device_based:
|
||||||
|
if "esp_port" in dev.device:
|
||||||
|
# 1-based naming (esp_port_1 -> Offset 0)
|
||||||
offset = raw_port_number - 1
|
offset = raw_port_number - 1
|
||||||
|
else:
|
||||||
|
# 0-based naming (ttyUSB0 -> Offset 0)
|
||||||
|
offset = raw_port_number
|
||||||
|
|
||||||
target_ip = str(start_ip + offset)
|
target_ip = str(start_ip + offset)
|
||||||
|
if not args.check_version:
|
||||||
# Display the result using the clear 'DEVICE IP' label
|
print(f" [{dev.device}] Device-based IP: {target_ip} (Raw: {raw_port_number}, Offset: {offset})")
|
||||||
print(f" [{dev.device}] Using device-based IP offset: +{offset} (Raw Port: {raw_port_number}). DEVICE IP: {target_ip}")
|
|
||||||
else:
|
else:
|
||||||
# Mode B: Sequential offset based on loop index (zero-based)
|
|
||||||
offset = i
|
offset = i
|
||||||
target_ip = str(start_ip + offset)
|
target_ip = str(start_ip + offset)
|
||||||
|
if not args.check_version:
|
||||||
# Display the result using the clear 'DEVICE IP' label
|
print(f" [{dev.device}] Sequential IP: {target_ip} (Offset: +{offset})")
|
||||||
print(f" [{dev.device}] Using sequential IP offset: +{offset}. DEVICE IP: {target_ip}")
|
|
||||||
|
|
||||||
tasks.append(UnifiedDeployWorker(dev.device, target_ip, args, project_dir, flash_sem).run())
|
tasks.append(UnifiedDeployWorker(dev.device, target_ip, args, project_dir, flash_sem).run())
|
||||||
|
|
||||||
results = await asyncio.gather(*tasks)
|
results = await asyncio.gather(*tasks)
|
||||||
|
if args.check_version:
|
||||||
|
print(f"\n{Colors.BLUE}--- FIRMWARE VERSION AUDIT ---{Colors.RESET}")
|
||||||
|
print(f"{'Device':<20} | {'Version':<15}")
|
||||||
|
print("-" * 40)
|
||||||
|
for dev, res in zip(devs, results):
|
||||||
|
ver_color = Colors.GREEN if res != "Unknown" and res != "Error" else Colors.RED
|
||||||
|
print(f"{dev.device:<20} | {ver_color}{res:<15}{Colors.RESET}")
|
||||||
|
return
|
||||||
success = results.count(True)
|
success = results.count(True)
|
||||||
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
||||||
|
|
||||||
|
|
|
||||||
32
main/main.c
32
main/main.c
|
|
@ -7,7 +7,6 @@
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
// REMOVED: #include "linenoise/linenoise.h" <-- CAUSE OF CONFLICT
|
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "lwip/inet.h"
|
#include "lwip/inet.h"
|
||||||
|
|
@ -27,8 +26,17 @@
|
||||||
#include "csi_manager.h"
|
#include "csi_manager.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// --- VERSION DEFINITION ---
|
||||||
|
#define APP_VERSION "1.1.0"
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
static const char *TAG = "MAIN";
|
||||||
|
|
||||||
|
// --- Version Command -----------------------------------------------
|
||||||
|
static int cmd_version(int argc, char **argv) {
|
||||||
|
printf("APP_VERSION: %s\n", APP_VERSION);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Event Handler -------------------------------------------------
|
// --- Event Handler -------------------------------------------------
|
||||||
static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||||
if (event_base == WIFI_EVENT) {
|
if (event_base == WIFI_EVENT) {
|
||||||
|
|
@ -43,8 +51,6 @@ static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// In event_handler function inside main.c
|
|
||||||
|
|
||||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||||
if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) return;
|
if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) return;
|
||||||
|
|
||||||
|
|
@ -74,8 +80,6 @@ void app_main(void) {
|
||||||
// 1. System Init
|
// 1. System Init
|
||||||
esp_err_t ret = nvs_flash_init();
|
esp_err_t ret = nvs_flash_init();
|
||||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
// NVS partition was truncated and needs to be erased
|
|
||||||
// Retry nvs_flash_init
|
|
||||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
ret = nvs_flash_init();
|
ret = nvs_flash_init();
|
||||||
}
|
}
|
||||||
|
|
@ -102,12 +106,9 @@ void app_main(void) {
|
||||||
csi_mgr_init();
|
csi_mgr_init();
|
||||||
#endif
|
#endif
|
||||||
wifi_ctl_init();
|
wifi_ctl_init();
|
||||||
|
|
||||||
// THIS call starts the cmd_transport (UART listener task)
|
|
||||||
// which effectively replaces the manual console loop below.
|
|
||||||
wifi_cfg_init();
|
wifi_cfg_init();
|
||||||
|
|
||||||
// 4. Console Registry Init (Still needed for registering commands)
|
// 4. Console Registry Init
|
||||||
esp_console_config_t console_config = {
|
esp_console_config_t console_config = {
|
||||||
.max_cmdline_args = 8,
|
.max_cmdline_args = 8,
|
||||||
.max_cmdline_length = 256,
|
.max_cmdline_length = 256,
|
||||||
|
|
@ -118,6 +119,15 @@ void app_main(void) {
|
||||||
// Register App Commands
|
// Register App Commands
|
||||||
app_console_register_commands();
|
app_console_register_commands();
|
||||||
|
|
||||||
|
// Register Version Command (Built-in)
|
||||||
|
const esp_console_cmd_t version_cmd_def = {
|
||||||
|
.command = "version",
|
||||||
|
.help = "Get firmware version",
|
||||||
|
.hint = NULL,
|
||||||
|
.func = &cmd_version,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(esp_console_cmd_register(&version_cmd_def));
|
||||||
|
|
||||||
// 5. Register Events
|
// 5. Register Events
|
||||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
|
||||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
|
||||||
|
|
@ -142,12 +152,10 @@ void app_main(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Keep Main Task Alive
|
// 7. Keep Main Task Alive
|
||||||
// We removed linenoise because cmd_transport.c is already reading UART.
|
|
||||||
ESP_LOGI(TAG, "Initialization complete. Entering idle loop.");
|
ESP_LOGI(TAG, "Initialization complete. Entering idle loop.");
|
||||||
|
ESP_LOGI(TAG, "Firmware Version: %s", APP_VERSION); // Log version on boot
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// Just sleep forever. cmd_transport task handles input.
|
|
||||||
// main event loop task handles wifi events.
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue