diff --git a/esp32_deploy.py b/esp32_deploy.py index ee78acb..8a20521 100755 --- a/esp32_deploy.py +++ b/esp32_deploy.py @@ -1,17 +1,50 @@ #!/usr/bin/env python3 -""" -ESP32 Unified Deployment Tool (esp32_deploy) -Combines firmware flashing and device configuration with full control. -Updates: - - ADDED: --ip-device-based support (IP = Start_IP + Port_Number) - - FIXED: Robust IP calculation logic - - PRESERVED: Existing flash/monitor/config workflow -""" +import sys +import os + +# --- AUTO-BOOTSTRAP ESP-IDF ENVIRONMENT --- +# This block detects if the script is running without 'get_idf' +# and automatically re-launches it using the ESP-IDF Python environment. +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 serial_asyncio -import sys -import os import argparse import ipaddress import re @@ -86,24 +119,26 @@ class UnifiedDeployWorker: self.log = DeviceLoggerAdapter(logger, {'connid': port}) 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_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_version = re.compile(r'APP_VERSION:\s*([0-9\.]+)', re.IGNORECASE) async def run(self): try: + if self.args.check_version: + ver = await self._query_version() + return ver + if not self.args.config_only: async with self.flash_sem: if self.args.flash_erase: if not await self._erase_flash(): return False if not await self._flash_firmware(): return False - # Give it a moment to stabilize after flash reset await asyncio.sleep(2.0) if not self.args.flash_only: if self.args.ssid and self.args.password: - # Retry logic success = False for attempt in range(1, 4): self.log.info(f"Configuring (Attempt {attempt}/3)...") @@ -125,6 +160,39 @@ class UnifiedDeployWorker: self.log.error(f"Worker Exception: {e}") 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): cmd = ['esptool.py', '-p', self.port, 'chip_id'] 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) firmware_dir = self.project_dir / "firmware" - unique_app = None if firmware_dir.exists(): 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"): unique_app = f break - if not unique_app: self.log.error(f"Binary for config '{suffix}' not found in firmware/.") return False @@ -172,9 +238,8 @@ class UnifiedDeployWorker: unique_boot = f"bootloader_{suffix}.bin" unique_part = f"partition-table_{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(): self.log.error(f"flash_args for {suffix} not found") return False @@ -182,34 +247,23 @@ class UnifiedDeployWorker: try: with open(flash_args_path, 'r') as f: content = f.read().replace('\n', ' ').strip() - raw_args = [x for x in content.split(' ') if x] final_args = [] - for arg in raw_args: - if arg.endswith('bootloader.bin'): - final_args.append(str(firmware_dir / unique_boot)) - elif arg.endswith('partition-table.bin'): - final_args.append(str(firmware_dir / unique_part)) + if arg.endswith('bootloader.bin'): 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('ota_data_initial.bin'): - if (firmware_dir / unique_ota).exists(): - final_args.append(str(firmware_dir / unique_ota)) - else: - continue - elif arg.endswith('phy_init_data.bin'): - final_args.append(arg) - elif arg.endswith('.bin'): - final_args.append(str(firmware_dir / unique_app)) - else: - final_args.append(arg) + if (firmware_dir / unique_ota).exists(): final_args.append(str(firmware_dir / unique_ota)) + else: continue + elif arg.endswith('phy_init_data.bin'): 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), '--before', 'default_reset', '--after', 'hard_reset', 'write_flash'] + final_args - full_path = firmware_dir / unique_app - self.log.info(f"Flashing {full_path}...") - + self.log.info(f"Flashing {firmware_dir / unique_app}...") proc = await asyncio.create_subprocess_exec(*cmd, cwd=self.project_dir, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300) @@ -220,7 +274,6 @@ class UnifiedDeployWorker: if proc.returncode == 0: return True self.log.error(f"Flash failed: {stderr.decode()}") return False - except Exception as e: self.log.error(f"Flash Prep Error: {e}") return False @@ -230,29 +283,21 @@ class UnifiedDeployWorker: reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200) except Exception as e: return False - try: - # 1. Reset writer.transport.serial.dtr = False writer.transport.serial.rts = True await asyncio.sleep(0.1) writer.transport.serial.rts = False - # FIX: DTR Must be False to allow Booting (True=Low=Bootloader Mode) writer.transport.serial.dtr = False - # 2. Robust Wait (with Poke) if not await self._wait_for_boot(reader, writer): self.log.warning("Boot prompt missed (sending blindly)...") - # 3. Send await self._send_config(writer) - - # 4. Verify is_configured = await self._verify_configuration(reader) if is_configured: 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.rts = True await asyncio.sleep(0.1) @@ -261,7 +306,6 @@ class UnifiedDeployWorker: else: self.log.error(f"{Colors.RED}Config verification failed.{Colors.RESET}") return False - except Exception as e: self.log.error(f"Config Error: {e}") return False @@ -270,80 +314,51 @@ class UnifiedDeployWorker: await writer.wait_closed() async def _wait_for_boot(self, reader, writer): - # Timeout covers GPS delay (~3.5s) + boot overhead end_time = time.time() + 12 last_poke = time.time() - while time.time() < end_time: try: - # Poke every 1.5 seconds if we haven't seen the prompt if time.time() - last_poke > 1.5: writer.write(b'\n') await writer.drain() last_poke = time.time() - try: - # Short timeout to allow polling loop line_bytes = await asyncio.wait_for(reader.readline(), timeout=0.1) line = line_bytes.decode('utf-8', errors='ignore').strip() if not line: continue - - if self.regex_ready.search(line): - return True - except asyncio.TimeoutError: - continue - + if self.regex_ready.search(line): return True + except asyncio.TimeoutError: continue except Exception as e: self.log.error(f"Read error: {e}") return False return False async def _send_config(self, writer): - # Wait a moment for any last boot logs to clear await asyncio.sleep(0.5) - - # Wake up console writer.write(b'\n') await writer.drain() await asyncio.sleep(0.2) - csi_val = '1' if self.args.csi_enable else '0' role_str = "SERVER" if self.args.iperf_server else "CLIENT" iperf_enable_val = '0' if self.args.no_iperf else '1' period_us = int(self.args.iperf_period * 1000000) - - # Build list of commands using args (which have robust defaults) config_lines = [ "CFG", - f"SSID:{self.args.ssid}", - f"PASS:{self.args.password}", - f"IP:{self.target_ip}", - f"MASK:{self.args.netmask}", - f"GW:{self.args.gateway}", - f"DHCP:0", - f"BAND:{self.args.band}", - f"BW:{self.args.bandwidth}", - f"POWERSAVE:{self.args.powersave}", - 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}", + f"SSID:{self.args.ssid}", f"PASS:{self.args.password}", + f"IP:{self.target_ip}", f"MASK:{self.args.netmask}", f"GW:{self.args.gateway}", + f"DHCP:0", f"BAND:{self.args.band}", f"BW:{self.args.bandwidth}", + f"POWERSAVE:{self.args.powersave}", 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" ] - - # CHANGED: Send line-by-line with a delay to prevent UART FIFO overflow for line in config_lines: cmd = line + "\r\n" writer.write(cmd.encode('utf-8')) await writer.drain() - # 50ms delay allows the ESP32 (running at 115200 baud) to process the line await asyncio.sleep(0.1) async def _verify_configuration(self, reader): @@ -353,7 +368,6 @@ class UnifiedDeployWorker: line_bytes = await asyncio.wait_for(reader.readline(), timeout=1.0) line = line_bytes.decode('utf-8', errors='ignore').strip() if not line: continue - if self.regex_csi_saved.search(line): return True m = self.regex_got_ip.search(line) 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('--flash-only', 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('-b', '--baud', type=int, default=460800) parser.add_argument('--devices', type=str) parser.add_argument('--max-concurrent', type=int, default=None) - - # --- IP Configuration --- - parser.add_argument('--start-ip', help='Start IP (Required unless --target all)') + parser.add_argument('--start-ip', help='Start IP (Required unless --target all or --check-version)') 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('-P', '--password', default='ez2remember') 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('-B', '--bandwidth', default='HT20') parser.add_argument('-ps', '--powersave', default='NONE') - - # --- Iperf Defaults (Robustness) --- parser.add_argument('--iperf-period', type=float, default=0.01) parser.add_argument('--iperf-burst', type=int, default=1) 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('--iperf-client', action='store_true') parser.add_argument('--iperf-server', action='store_true') - - # --- Monitor Mode Defaults --- parser.add_argument('-M', '--mode', default='STA') parser.add_argument('-mc', '--monitor-channel', type=int, default=36) parser.add_argument('--csi', dest='csi_enable', action='store_true') - args = parser.parse_args() - - if args.target != 'all' and not args.start_ip: + if args.target != 'all' and not args.start_ip and not args.check_version: parser.error("the following arguments are required: --start-ip") 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 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) 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'}" prefix = f"[{current_step}/{total_steps}] " if current_step else "" print(f" {prefix}Building [{desc}] ... ", end='', flush=True) - try: output_dir = project_dir / "firmware" output_dir.mkdir(exist_ok=True) - sdkconfig_path = project_dir / "sdkconfig" build_path = project_dir / "build" if sdkconfig_path.exists(): os.remove(sdkconfig_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) _, stderr = await proc.communicate() if proc.returncode != 0: print(f"{Colors.RED}FAIL (Set Target){Colors.RESET}") return False, f"Set Target Failed", 0 - start_time = time.time() 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) @@ -476,33 +469,24 @@ async def build_task(project_dir, target, csi, ampdu, current_step=None, total_s if proc.returncode != 0: print(f"{Colors.RED}FAIL{Colors.RESET}") return False, f"Build Failed", duration - build_dir = project_dir / 'build' suffix = generate_config_suffix(target, csi, ampdu) unique_app_name = "Unknown" - project_bin = get_project_binary_name(build_dir) if project_bin: unique_app_name = f"{os.path.splitext(project_bin)[0]}_{suffix}.bin" shutil.copy2(build_dir / project_bin, output_dir / unique_app_name) - boot_src = build_dir / "bootloader" / "bootloader.bin" if boot_src.exists(): shutil.copy2(boot_src, output_dir / f"bootloader_{suffix}.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") - - # Fix: Save OTA data binary if it exists ota_src = build_dir / "ota_data_initial.bin" if ota_src.exists(): shutil.copy2(ota_src, output_dir / f"ota_data_initial_{suffix}.bin") - flash_src = build_dir / "flash_args" if flash_src.exists(): shutil.copy2(flash_src, output_dir / f"flash_args_{suffix}") - full_path = output_dir / unique_app_name print(f"{Colors.GREEN}OK ({duration:.1f}s) -> {full_path}{Colors.RESET}") return True, "Success", duration - except Exception as e: print(f"{Colors.RED}ERROR: {e}{Colors.RESET}") 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): 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() - - # --- Target 'ALL' Mode --- if args.target == 'all': 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" if firmware_dir.exists(): - try: - shutil.rmtree(firmware_dir) - 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 + try: shutil.rmtree(firmware_dir) + except Exception as e: return firmware_dir.mkdir(exist_ok=True) - print("") # Spacer - targets = ['esp32', 'esp32s3', 'esp32c5'] booleans = [False, True] results = [] - total_steps = len(targets) * len(booleans) * len(booleans) current_step = 0 - for target in targets: for csi in booleans: for ampdu in booleans: current_step += 1 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}) - print(f"\n{Colors.BLUE}Batch Summary:{Colors.RESET}") for r in results: 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)") return - # --- Single Build Configuration --- - # Skip build if we are in AUTO mode (we assume binaries exist in firmware/) - if not args.config_only and args.target != 'auto': + if not args.config_only and args.target != 'auto' and not args.check_version: target = args.target if args.target else 'esp32s3' csi = args.csi_enable ampdu = args.ampdu - if args.interactive: print(f"\n{Colors.YELLOW}--- Build Configuration ---{Colors.RESET}") 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.target = target args.ampdu = ampdu - success, msg, _ = await build_task(project_dir, target, csi, ampdu, 1, 1) if not success: print(f"{Colors.RED}{msg}{Colors.RESET}") 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}") - # --- Device Detection & Flash --- - if args.devices: + if args.devices and args.devices.lower() != 'all': devs = [type('obj', (object,), {'device': d.strip()}) for d in args.devices.split(',')] else: - # Use AUTO DETECT first (for static names), then standard fallback devs = auto_detect_devices() 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)]) 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) flash_sem = asyncio.Semaphore(max_c) - - tasks = [] - 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) - # FIX: Subtract 1 from the raw port number to make the offset zero-based. - # This ensures esp_port_1 gets the exact --start-ip (offset 0). - offset = raw_port_number - 1 + for i, dev in enumerate(devs): + raw_port_number = extract_device_number(dev.device) + + # --- NEW LOGIC: Handling Zero-Based (ttyUSB) vs One-Based (esp_port) --- + if args.ip_device_based: + if "esp_port" in dev.device: + # 1-based naming (esp_port_1 -> Offset 0) + offset = raw_port_number - 1 + else: + # 0-based naming (ttyUSB0 -> Offset 0) + offset = raw_port_number target_ip = str(start_ip + offset) - - # Display the result using the clear 'DEVICE IP' label - print(f" [{dev.device}] Using device-based IP offset: +{offset} (Raw Port: {raw_port_number}). DEVICE IP: {target_ip}") + if not args.check_version: + print(f" [{dev.device}] Device-based IP: {target_ip} (Raw: {raw_port_number}, Offset: {offset})") else: - # Mode B: Sequential offset based on loop index (zero-based) offset = i target_ip = str(start_ip + offset) - - # Display the result using the clear 'DEVICE IP' label - print(f" [{dev.device}] Using sequential IP offset: +{offset}. DEVICE IP: {target_ip}") + if not args.check_version: + print(f" [{dev.device}] Sequential IP: {target_ip} (Offset: +{offset})") tasks.append(UnifiedDeployWorker(dev.device, target_ip, args, project_dir, flash_sem).run()) 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) print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}") diff --git a/main/main.c b/main/main.c index 77dcff9..25d51ae 100644 --- a/main/main.c +++ b/main/main.c @@ -7,7 +7,6 @@ #include "esp_event.h" #include "esp_log.h" #include "esp_console.h" -// REMOVED: #include "linenoise/linenoise.h" <-- CAUSE OF CONFLICT #include "nvs_flash.h" #include "esp_netif.h" #include "lwip/inet.h" @@ -27,8 +26,17 @@ #include "csi_manager.h" #endif +// --- VERSION DEFINITION --- +#define APP_VERSION "1.1.0" + 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 ------------------------------------------------- static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { 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) { if (wifi_ctl_get_mode() != WIFI_CTL_MODE_STA) return; @@ -74,8 +80,6 @@ void app_main(void) { // 1. System Init esp_err_t ret = nvs_flash_init(); 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()); ret = nvs_flash_init(); } @@ -102,12 +106,9 @@ void app_main(void) { csi_mgr_init(); #endif wifi_ctl_init(); - - // THIS call starts the cmd_transport (UART listener task) - // which effectively replaces the manual console loop below. wifi_cfg_init(); - // 4. Console Registry Init (Still needed for registering commands) + // 4. Console Registry Init esp_console_config_t console_config = { .max_cmdline_args = 8, .max_cmdline_length = 256, @@ -118,6 +119,15 @@ void app_main(void) { // Register App 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 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)); @@ -142,12 +152,10 @@ void app_main(void) { } // 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, "Firmware Version: %s", APP_VERSION); // Log version on boot while (true) { - // Just sleep forever. cmd_transport task handles input. - // main event loop task handles wifi events. vTaskDelay(pdMS_TO_TICKS(1000)); } }