Compare commits

..

30 Commits

Author SHA1 Message Date
Robert McMahon c58e70a658 Fix strncpy warnings and remove duplicate license headers
- Fix stringop-truncation warnings in wifi_cfg.c and cmd_wifi.c
- Remove duplicate license headers from multiple source files
- Update dependencies.lock
2025-12-27 19:38:23 -08:00
Robert McMahon a303b7171a Enhance error handling and logging in WiFi connection process
- Added error checks in wifi_do_connect to handle potential failures during connection attempts.
- Improved logging to provide clearer insights into connection status and errors for better debugging.
2025-12-27 19:35:25 -08:00
Robert McMahon 1eddb8e84f Refactor file headers and clean up comments in multiple source files
- Updated file headers in iperf.c to include trip-time support in the brief description.
- Removed unnecessary comment blocks in cmd_ip.c, cmd_nvs.c, app_console.c, and wifi_cfg.c to streamline the codebase.
2025-12-27 19:34:21 -08:00
Robert McMahon 56ea987f75 Improve WiFi configuration handling by ensuring proper string termination
- Updated strncpy calls in wifi_cfg_apply_from_nvs and wifi_do_connect to prevent buffer overflows by reserving space for null termination.
- Added explicit null termination for SSID and password fields in the wifi_config_t structure.
2025-12-27 18:00:19 -08:00
Robert McMahon 128596bd67 Clean up repository and improve console initialization
- Remove Emacs backup files (cmd_ip.c~, cmd_wifi.c~)
- Add new_rules.part to .gitignore (temp file used by gen_udev_rules.py)
- Update version to 2.1.0-CONSOLE-DEBUG for debugging
- Add debug logging around console REPL initialization
- Improve error handling for console initialization failures
- Remove unreachable code after esp_console_start_repl()
2025-12-27 17:56:46 -08:00
Robert McMahon d4cd861b80 Add error handling for UART and GPIO configurations in gps_sync_init
- Implemented error checks for uart_driver_install, uart_set_pin, gpio_config, gpio_install_isr_service, and gpio_isr_handler_add to ensure robust initialization of GPS synchronization components.
- Enhanced logging to provide detailed error messages for easier debugging.
2025-12-27 17:07:45 -08:00
Robert McMahon feb0d4d142 Remove deprecated Python scripts and clean up repository
- Removed 13 deprecated deployment/configuration scripts superseded by esp32_deploy.py:
  * flash_all.py, flash_all_parallel.py, flash_all_serial_config.py
  * flash_and_config.py, mass_deploy.py, async_mass_deploy.py
  * async_batch_config.py, batch_config.py, config_device.py
  * esp32_reconfig.py, reconfig_simple.py, reconfig_simple_nextip.py
  * map_usb_to_ip.py

- Updated .gitignore to exclude:
  * Emacs backup files (#filename#)
  * firmware/ directory and flash_args_* build artifacts

- Repository now contains only active scripts:
  * esp32_deploy.py (main unified deployment tool)
  * detect_esp32.py, control_iperf.py, parse_csi.py
  * add_license.py, gen_udev_rules.py, identiy_port.py
  * async_find_failed.py (diagnostic utility)
2025-12-27 16:42:09 -08:00
Robert McMahon 4ed4391068 final fixes for commands 2025-12-22 16:57:51 -08:00
Robert McMahon 2590a96b15 fix led so it works now 2025-12-22 15:52:02 -08:00
Robert McMahon 98b013569d some led work 2025-12-22 15:19:10 -08:00
Robert McMahon 64446be628 more cmd stuff 2025-12-22 15:00:54 -08:00
Robert McMahon b769dbc356 more on gps time 2025-12-22 13:06:05 -08:00
Robert McMahon 9974174d5b use sub commands for iperf major command 2025-12-22 12:27:12 -08:00
Robert McMahon b4b40de64d wifi connect without requiring reboot 2025-12-22 11:55:20 -08:00
Robert McMahon 099c28f9c7 wifi scan 2025-12-21 18:54:18 -08:00
Robert McMahon 42905200ea more on commands 2025-12-21 15:54:30 -08:00
Robert McMahon 6c214e8e92 udp with trip-times 2025-12-20 18:35:06 -08:00
Robert McMahon 969abb5ae4 update license 2025-12-19 18:13:49 -08:00
Robert McMahon 2a41edf491 add bssid to scan output 2025-12-19 18:10:02 -08:00
Robert McMahon 46f0cdb07b add ping support 2025-12-19 17:46:31 -08:00
Robert McMahon e8f7e2f75c more on gps 2025-12-19 14:50:06 -08:00
Robert McMahon 1b78440309 gps --status command 2025-12-19 14:46:24 -08:00
Robert McMahon 88a585408a add gps to nvs 2025-12-19 14:30:29 -08:00
Robert McMahon e5baa7cec5 fix gps sync 2025-12-19 12:10:35 -08:00
Robert McMahon 3969c5780d nvs saves 2025-12-19 10:36:10 -08:00
Robert McMahon ca8b382a40 more on console 2025-12-19 10:21:08 -08:00
Robert McMahon 0f1c5b3079 more on console 2025-12-19 10:12:32 -08:00
Robert McMahon 796ef43497 more command & console work, including save and reload 2025-12-18 21:43:18 -08:00
Robert McMahon 87744e2883 Merge branch 'feature/generic-shell' of https://git.umbernetworks.com/Umber/ESP32 into feature/generic-shell 2025-12-18 17:55:08 -08:00
Robert McMahon c640bc4df7 fixes to deploy to update udev rules 2025-12-18 17:53:11 -08:00
53 changed files with 4130 additions and 4378 deletions

7
.gitignore vendored
View File

@ -9,6 +9,11 @@ sdkconfig.old
*.bin
*.elf
*.map
firmware/
flash_args_*
# Temporary files
new_rules.part
# IDE
.vscode/
@ -16,6 +21,8 @@ sdkconfig.old
*.swp
*.swo
*~
# Emacs backup files
\#*\#
# Dependencies
dependencies/

88
add_license.py Normal file
View File

@ -0,0 +1,88 @@
import os
# Configuration
SEARCH_DIRS = ["main", "components"] # Directories to scan relative to script location
EXTENSIONS = (".c", ".h")
COPYRIGHT_MARKER = "Copyright (c) 2025 Umber Networks & Robert McMahon"
# The License Template ( {filename} will be replaced )
LICENSE_TEMPLATE = """/*
* {filename}
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
"""
def add_license_to_file(filepath):
filename = os.path.basename(filepath)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Check if license is already present
if COPYRIGHT_MARKER in content:
print(f"Skipping (License exists): {filepath}")
return
# Prepare the license text with the specific filename
license_text = LICENSE_TEMPLATE.format(filename=filename)
# Prepend license to content
new_content = license_text + content
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated: {filepath}")
except Exception as e:
print(f"Error processing {filepath}: {e}")
def main():
root_dir = os.getcwd()
print(f"Scanning directories from: {root_dir}")
for search_dir in SEARCH_DIRS:
target_path = os.path.join(root_dir, search_dir)
if not os.path.exists(target_path):
print(f"Warning: Directory '{search_dir}' not found. Skipping.")
continue
for dirpath, _, filenames in os.walk(target_path):
for filename in filenames:
if filename.endswith(EXTENSIONS):
full_path = os.path.join(dirpath, filename)
add_license_to_file(full_path)
if __name__ == "__main__":
main()

View File

@ -1,267 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Async Batch Configuration Tool
The definitive parallel configuration tool.
Configures 30+ ESP32 devices concurrently using non-blocking I/O.
Features:
- Concurrent execution (configure 30 devices in <20 seconds)
- Robust Regex-based state detection
- Supports verifying both Station Mode (IP check) and Monitor Mode
- Context-aware logging
- CSI enable/disable control
"""
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 ---
class DeviceLoggerAdapter(logging.LoggerAdapter):
"""Prefixes log messages with the device port name"""
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 via Async Serial.
"""
def __init__(self, port, target_ip, args):
self.port = port
self.target_ip = target_ip
self.args = args
self.log = DeviceLoggerAdapter(logger, {'connid': port})
# --- Regex Patterns ---
# Success indicators
self.regex_got_ip = re.compile(r'got ip:(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
self.regex_monitor_success = re.compile(r'Monitor mode active', re.IGNORECASE)
self.regex_csi_saved = re.compile(r'CSI enable state saved', re.IGNORECASE)
# Prompts indicating device is booting/ready
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)
async def run(self):
"""Main execution workflow 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 (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. Wait for Boot
# We assume the device is ready when we see logs or a prompt
if not await self._wait_for_boot(reader):
self.log.warning("Boot prompt missed, attempting config anyway...")
# 3. Send Configuration
await self._send_config(writer)
# 4. Verify Success
return await self._verify_configuration(reader)
except Exception as e:
self.log.error(f"Exception: {e}")
return False
finally:
writer.close()
await writer.wait_closed()
async def _wait_for_boot(self, reader):
"""Reads stream until a known 'ready' prompt appears"""
self.log.info("Waiting for boot...")
timeout = time.time() + 5 # 5 second boot timeout
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()
if not line: continue
if self.regex_ready.search(line):
return True
except asyncio.TimeoutError:
continue
return False
async def _send_config(self, writer):
"""Builds and transmits the configuration command"""
csi_val = '1' if self.args.csi_enable else '0'
self.log.info(f"Sending config for IP {self.target_ip} (CSI:{csi_val})...")
# Construct command block
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"CSI:{csi_val}\n"
f"END\n"
)
writer.write(config_str.encode('utf-8'))
await writer.drain()
async def _verify_configuration(self, reader):
"""Monitors output for confirmation of Success"""
self.log.info("Verifying configuration...")
timeout = time.time() + 15 # 15s verification timeout
csi_saved = False
while time.time() < timeout:
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
# Check for CSI save confirmation
if self.regex_csi_saved.search(line):
csi_saved = True
# Check for Station Mode Success (IP Address)
m_ip = self.regex_got_ip.search(line)
if m_ip:
got_ip = m_ip.group(1)
if got_ip == self.target_ip:
csi_status = "CSI saved" if csi_saved else ""
self.log.info(f"SUCCESS: Assigned {got_ip} {csi_status}")
return True
else:
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
if self.regex_error.search(line):
self.log.warning(f"Device Reported Error: {line}")
except asyncio.TimeoutError:
continue
self.log.error("Timeout: Device did not confirm configuration.")
return False
async def main_async():
parser = argparse.ArgumentParser(
description='Async ESP32 Batch Config with CSI Control',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Configure 20 iperf baseline devices (NO CSI)
%(prog)s --start-ip 192.168.1.81
# Configure devices WITH CSI enabled
%(prog)s --start-ip 192.168.1.111 --csi
# Configure for monitor mode on channel 36
%(prog)s --start-ip 192.168.1.90 -M MONITOR -mc 36
# 5GHz with 40MHz bandwidth
%(prog)s --start-ip 192.168.1.81 -b 5G -B HT40
"""
)
# Arguments
parser.add_argument('--start-ip', required=True, help='Starting Static IP')
parser.add_argument('-s', '--ssid', default='ClubHouse2G', help='WiFi SSID')
parser.add_argument('-P', '--password', default='ez2remember', help='WiFi password')
parser.add_argument('-g', '--gateway', default='192.168.1.1', help='Gateway IP')
parser.add_argument('-m', '--netmask', default='255.255.255.0', help='Netmask')
parser.add_argument('-b', '--band', default='2.4G', choices=['2.4G', '5G'])
parser.add_argument('-B', '--bandwidth', default='HT20', choices=['HT20', 'HT40', 'VHT80'])
parser.add_argument('-ps', '--powersave', default='NONE')
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',
help='Enable CSI capture (default: disabled)')
args = parser.parse_args()
# 1. Detect
print("Step 1: Detecting Devices...")
devices = detect_esp32.detect_esp32_devices()
if not devices:
print("No devices found.")
return
# Sort naturally (ttyUSB2 before ttyUSB10)
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)
try:
start_ip_obj = ipaddress.IPv4Address(args.start_ip)
except:
print(f"Invalid IP: {args.start_ip}")
return
# 2. Configure Concurrently
csi_status = "ENABLED" if args.csi_enable else "DISABLED"
print(f"Step 2: Configuring {len(devices)} devices concurrently (CSI: {csi_status})...")
tasks = []
for i, dev in enumerate(devices):
current_ip = str(start_ip_obj + i)
configurator = Esp32Configurator(dev.device, current_ip, args)
tasks.append(configurator.run())
# Run everything at once
results = await asyncio.gather(*tasks)
# 3. Report
success_count = results.count(True)
print("\n" + "="*40)
print(f"Total Devices: {len(devices)}")
print(f"Success: {success_count}")
print(f"Failed: {len(devices) - success_count}")
print(f"CSI Setting: {csi_status}")
print("="*40)
if __name__ == '__main__':
try:
if os.name == 'nt':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
asyncio.run(main_async())
except KeyboardInterrupt:
print("\nAborted by user.")

View File

@ -1,280 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Async Mass Deployment Tool
Combines parallel flashing (via esptool) with async configuration.
"""
import asyncio
import serial_asyncio
import sys
import os
import argparse
import ipaddress
import re
import time
import logging
from pathlib import Path
# 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)
# --- Configuration ---
MAX_CONCURRENT_FLASH = 8
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
RESET = '\033[0m'
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("Deploy")
class DeployWorker:
def __init__(self, port, target_ip, args, build_dir, flash_sem):
self.port = port
self.target_ip = target_ip
self.args = args
self.build_dir = build_dir
self.flash_sem = flash_sem
self.log = DeviceLoggerAdapter(logger, {'connid': port})
# Regex Patterns
self.regex_ready = re.compile(r'Initialization complete|GPS synced|No WiFi config found', re.IGNORECASE)
# Match output from 'mode_status' command
self.regex_status_connected = re.compile(r'WiFi connected: Yes', re.IGNORECASE)
self.regex_status_ip = re.compile(r'Got IP: (\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
async def run(self):
try:
# 1. Flash Phase
async with self.flash_sem:
if self.args.erase:
if not await self._erase_flash(): return False
if not await self._flash_firmware(): return False
# 2. Config Phase
await asyncio.sleep(1.0) # Wait for port to stabilize after flash reset
if self.args.ssid and self.args.password:
if not await self._configure_device(): return False
else:
self.log.info(f"{Colors.GREEN}Flash Complete (NVS Preserved){Colors.RESET}")
return True
except Exception as e:
self.log.error(f"Worker Exception: {e}")
return False
async def _erase_flash(self):
self.log.info("Erasing flash...")
cmd = ['esptool.py', '-p', self.port, '-b', '115200', 'erase_flash']
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
self.log.info("Erase successful.")
return True
self.log.error(f"Erase failed: {stderr.decode()}")
return False
async def _flash_firmware(self):
self.log.info("Flashing firmware...")
cmd = ['esptool.py', '-p', self.port, '-b', str(self.args.baud),
'--before', 'default_reset', '--after', 'hard_reset',
'write_flash', '@flash_args']
proc = await asyncio.create_subprocess_exec(
*cmd, cwd=self.build_dir,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
except asyncio.TimeoutError:
proc.kill()
self.log.error("Flash timed out.")
return False
if proc.returncode == 0:
self.log.info("Flash successful.")
return True
self.log.error(f"Flash failed: {stderr.decode()}")
return False
async def _configure_device(self):
self.log.info("Connecting to console...")
try:
reader, writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=115200)
except Exception as e:
self.log.error(f"Serial open failed: {e}")
return False
try:
# A. Boot Wait
self.log.info("Waiting for boot...")
booted = False
end_time = time.time() + 10
while time.time() < end_time:
try:
line_b = await asyncio.wait_for(reader.readline(), timeout=0.5)
line = line_b.decode('utf-8', errors='ignore').strip()
if self.regex_ready.search(line):
booted = True
break
except asyncio.TimeoutError:
continue
if not booted:
self.log.warning("Boot prompt missed, attempting config anyway...")
# B. Send Config
self.log.info(f"Sending config for {self.target_ip}...")
config_str = (f"CFG\nSSID:{self.args.ssid}\nPASS:{self.args.password}\n"
f"IP:{self.target_ip}\nMASK:{self.args.netmask}\nGW:{self.args.gateway}\n"
f"DHCP:0\nEND\n")
writer.write(config_str.encode('utf-8'))
await writer.drain()
# C. Active Polling Verification
self.log.info("Verifying configuration (Polling)...")
start_verify = time.time()
while time.time() < start_verify + 30:
# 1. Clear buffer
try:
while True:
await asyncio.wait_for(reader.read(1024), timeout=0.01)
except asyncio.TimeoutError:
pass
# 2. Send status request
writer.write(b"\nmode_status\n")
await writer.drain()
# 3. Read response for ~2 seconds
poll_end = time.time() + 2.0
while time.time() < poll_end:
try:
line_b = await asyncio.wait_for(reader.readline(), timeout=0.5)
line = line_b.decode('utf-8', errors='ignore').strip()
# Check for success indicators in status output
if self.regex_status_connected.search(line):
self.log.info(f"{Colors.GREEN}SUCCESS: Connected{Colors.RESET}")
return True
# Also catch passive "Got IP" logs if they appear
m = self.regex_status_ip.search(line)
if m:
if m.group(1) == self.target_ip:
self.log.info(f"{Colors.GREEN}SUCCESS: IP Confirmed ({m.group(1)}){Colors.RESET}")
return True
except asyncio.TimeoutError:
break
# Wait a bit before next poll
await asyncio.sleep(1.0)
self.log.error("Timeout: Device did not confirm connection.")
return False
except Exception as e:
self.log.error(f"Config error: {e}")
return False
finally:
writer.close()
await writer.wait_closed()
def parse_args():
parser = argparse.ArgumentParser(description='Async ESP32 Mass Deployment')
parser.add_argument('-d', '--dir', default=os.getcwd(), help='Project dir')
parser.add_argument('-s', '--ssid', help='WiFi SSID')
parser.add_argument('-P', '--password', help='WiFi Password')
parser.add_argument('--start-ip', default='192.168.1.51', help='Start IP')
parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud')
parser.add_argument('--erase', action='store_true', help='Full erase first')
parser.add_argument('-g', '--gateway', default='192.168.1.1', help='Gateway')
parser.add_argument('-m', '--netmask', default='255.255.255.0', help='Netmask')
return parser.parse_args()
async def run_deployment(args):
project_dir = Path(args.dir).resolve()
build_dir = project_dir / 'build'
# 1. Build Firmware
print(f"{Colors.YELLOW}[1/3] Building Firmware...{Colors.RESET}")
proc = await asyncio.create_subprocess_exec(
'idf.py', 'build',
cwd=project_dir,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
print(f"{Colors.RED}Build Failed:\n{stderr.decode()}{Colors.RESET}")
return
if not (build_dir / 'flash_args').exists():
print(f"{Colors.RED}Error: build/flash_args missing.{Colors.RESET}")
return
print(f"{Colors.GREEN}Build Complete.{Colors.RESET}")
# 2. Detect Devices
print(f"{Colors.YELLOW}[2/3] Scanning Devices...{Colors.RESET}")
devices = detect_esp32.detect_esp32_devices()
if not devices:
print(f"{Colors.RED}No devices found.{Colors.RESET}")
return
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)
# 3. Deploy
print(f"{Colors.YELLOW}[3/3] Deploying to {len(devices)} devices...{Colors.RESET}")
try:
start_ip_obj = ipaddress.IPv4Address(args.start_ip)
except:
print("Invalid Start IP")
return
flash_sem = asyncio.Semaphore(MAX_CONCURRENT_FLASH)
tasks = []
for i, dev in enumerate(devices):
target_ip = str(start_ip_obj + i)
worker = DeployWorker(dev.device, target_ip, args, build_dir, flash_sem)
tasks.append(worker.run())
results = await asyncio.gather(*tasks)
# 4. Summary
success = results.count(True)
print(f"\n{Colors.BLUE}{'='*40}{Colors.RESET}")
print(f"Total: {len(devices)}")
print(f"Success: {Colors.GREEN}{success}{Colors.RESET}")
print(f"Failed: {Colors.RED}{len(devices) - success}{Colors.RESET}")
print(f"{Colors.BLUE}{'='*40}{Colors.RESET}")
def main():
args = parse_args()
if os.name == 'nt':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
try:
asyncio.run(run_deployment(args))
except KeyboardInterrupt:
print("\nCancelled.")
if __name__ == '__main__':
main()

View File

@ -1,158 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Batch Configuration Tool
Detects all connected ESP32s and configures them with sequential Static IPs.
Requires: detect_esp32.py and config_device.py in the same directory.
"""
import sys
import os
import argparse
import time
import ipaddress
import re
# Ensure we can import the other scripts
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
import detect_esp32
import config_device
except ImportError as e:
print(f"Error: Could not import required modules ({e}).")
print("Make sure 'detect_esp32.py' and 'config_device.py' are in the same folder.")
sys.exit(1)
def natural_sort_key(device_obj):
"""
Sorts ports naturally (ttyUSB2 comes before ttyUSB10)
"""
s = device_obj.device
# Split string into a list of integers and non-integers
return [int(text) if text.isdigit() else text.lower()
for text in re.split('([0-9]+)', s)]
def main():
parser = argparse.ArgumentParser(
description='Batch Config: Detects all ESP32s and configures sequential IPs',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
# Arguments matching config_device.py options
parser.add_argument('--start-ip', required=True,
help='Starting Static IP (e.g., 192.168.1.101). Will increment for each device.')
parser.add_argument('-s', '--ssid', default='ClubHouse2G',
help='WiFi SSID')
parser.add_argument('-P', '--password', default='ez2remember',
help='WiFi password')
parser.add_argument('-g', '--gateway', default='192.168.1.1',
help='Gateway IP')
parser.add_argument('-m', '--netmask', default='255.255.255.0',
help='Netmask')
parser.add_argument('-b', '--band', default='2.4G', choices=['2.4G', '5G'],
help='WiFi band')
parser.add_argument('-B', '--bandwidth', default='HT20', choices=['HT20', 'HT40', 'VHT80'],
help='Channel bandwidth')
parser.add_argument('-ps', '--powersave', default='NONE',
choices=['NONE', 'MIN', 'MIN_MODEM', 'MAX', 'MAX_MODEM'],
help='Power save mode')
parser.add_argument('-M', '--mode', default='STA', choices=['STA', 'MONITOR'],
help='Operating mode')
parser.add_argument('-mc', '--monitor-channel', type=int, default=36,
help='Monitor mode channel')
parser.add_argument('-r', '--no-reboot', action='store_true',
help='Do NOT reboot devices after configuration')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
args = parser.parse_args()
# 1. Detect Devices
print(f"{'='*60}")
print("Step 1: Detecting ESP32 Devices...")
print(f"{'='*60}")
devices = detect_esp32.detect_esp32_devices()
if not devices:
print("No ESP32 devices found! Check USB connections.")
sys.exit(1)
# Sort devices naturally so IPs are assigned in order (USB0, USB1, USB2...)
devices.sort(key=natural_sort_key)
print(f"Found {len(devices)} devices:")
for d in devices:
print(f" - {d.device} ({d.description})")
print()
# 2. Parse Starting IP
try:
start_ip_obj = ipaddress.IPv4Address(args.start_ip)
except ipaddress.AddressValueError:
print(f"Error: Invalid IP address format: {args.start_ip}")
sys.exit(1)
# 3. Configure Each Device
print(f"{'='*60}")
print("Step 2: Configuring Devices Sequentially")
print(f"{'='*60}")
success_count = 0
fail_count = 0
failed_devices = []
for index, device in enumerate(devices):
# Calculate current IP
current_ip = str(start_ip_obj + index)
port = device.device
print(f"\n[{index+1}/{len(devices)}] Configuring {port} with IP {current_ip}...")
# Call the config function from your existing script
result = config_device.config_device(
port=port,
ip=current_ip,
ssid=args.ssid,
password=args.password,
gateway=args.gateway,
netmask=args.netmask,
band=args.band,
bandwidth=args.bandwidth,
powersave=args.powersave,
mode=args.mode,
monitor_channel=args.monitor_channel,
reboot=not args.no_reboot,
verbose=args.verbose
)
if result:
print(f"✓ Success: {port} -> {current_ip}")
success_count += 1
else:
print(f"✗ Failed: {port}")
fail_count += 1
failed_devices.append(port)
# Small delay to prevent USB power spikes if multiple devices reboot simultaneously
if not args.no_reboot and index < len(devices) - 1:
time.sleep(1.0)
# 4. Summary
print(f"\n{'='*60}")
print("Batch Configuration Complete")
print(f"{'='*60}")
print(f"Total Devices: {len(devices)}")
print(f"Successful: {success_count}")
print(f"Failed: {fail_count}")
if failed_devices:
print("\nFailed Ports:")
for p in failed_devices:
print(f" - {p}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nBatch process interrupted by user.")

View File

@ -1,3 +1,15 @@
idf_component_register(SRCS "app_console.c"
INCLUDE_DIRS "."
PRIV_REQUIRES console wifi_cfg iperf)
idf_component_register(
SRCS "app_console.c"
"cmd_wifi.c"
"cmd_iperf.c"
"cmd_system.c"
"cmd_monitor.c"
"cmd_nvs.c"
"cmd_gps.c"
"cmd_ping.c"
"cmd_ip.c"
INCLUDE_DIRS "."
REQUIRES console wifi_cfg
wifi_controller iperf status_led gps_sync
esp_wifi esp_netif nvs_flash spi_flash
)

View File

@ -1,162 +1,48 @@
/*
* app_console.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "app_console.h"
#include "esp_console.h"
#include "esp_log.h"
#include "argtable3/argtable3.h"
#include "wifi_cfg.h"
#include "iperf.h"
#include <string.h>
// ============================================================================
// COMMAND: iperf
// ============================================================================
static struct {
struct arg_lit *start;
struct arg_lit *stop;
struct arg_lit *status;
struct arg_int *pps;
struct arg_lit *help;
struct arg_end *end;
} iperf_args;
static int cmd_iperf(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&iperf_args);
if (nerrors > 0) {
arg_print_errors(stderr, iperf_args.end, argv[0]);
return 1;
}
if (iperf_args.help->count > 0) {
printf("Usage: iperf [start|stop|status] [--pps <n>]\n");
return 0;
}
if (iperf_args.stop->count > 0) {
iperf_stop();
return 0;
}
if (iperf_args.pps->count > 0) {
int val = iperf_args.pps->ival[0];
if (val > 0) {
iperf_set_pps((uint32_t)val);
} else {
printf("Error: PPS must be > 0\n");
}
return 0;
}
if (iperf_args.status->count > 0) {
iperf_print_status();
return 0;
}
if (iperf_args.start->count > 0) {
// Start using saved NVS config
iperf_cfg_t cfg = { .time = 0 };
iperf_start(&cfg);
return 0;
}
return 0;
}
static void register_iperf_cmd(void) {
iperf_args.start = arg_lit0(NULL, "start", "Start iperf traffic");
iperf_args.stop = arg_lit0(NULL, "stop", "Stop iperf traffic");
iperf_args.status = arg_lit0(NULL, "status", "Show current statistics");
iperf_args.pps = arg_int0(NULL, "pps", "<n>", "Set packets per second");
iperf_args.help = arg_lit0(NULL, "help", "Show help");
iperf_args.end = arg_end(20);
const esp_console_cmd_t cmd = {
.command = "iperf",
.help = "Control iperf traffic generator",
.hint = NULL,
.func = &cmd_iperf,
.argtable = &iperf_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
// ============================================================================
// COMMAND: wifi_config
// ============================================================================
static struct {
struct arg_str *ssid;
struct arg_str *pass;
struct arg_str *ip;
struct arg_lit *dhcp;
struct arg_lit *help;
struct arg_end *end;
} wifi_args;
static int cmd_wifi_config(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&wifi_args);
if (nerrors > 0) {
arg_print_errors(stderr, wifi_args.end, argv[0]);
return 1;
}
if (wifi_args.help->count > 0) {
printf("Usage: wifi_config -s <ssid> -p <pass> [-i <ip>] [-d]\n");
return 0;
}
if (wifi_args.ssid->count == 0) {
printf("Error: SSID is required (-s)\n");
return 1;
}
const char* ssid = wifi_args.ssid->sval[0];
const char* pass = (wifi_args.pass->count > 0) ? wifi_args.pass->sval[0] : "";
const char* ip = (wifi_args.ip->count > 0) ? wifi_args.ip->sval[0] : NULL;
bool dhcp = (wifi_args.dhcp->count > 0);
printf("Saving WiFi Config: SSID='%s' DHCP=%d\n", ssid, dhcp);
wifi_cfg_set_credentials(ssid, pass);
if (ip) {
char mask[] = "255.255.255.0";
char gw[32];
// FIXED: Use strlcpy instead of strncpy to prevent truncation warnings
strlcpy(gw, ip, sizeof(gw));
char *last_dot = strrchr(gw, '.');
if (last_dot) strcpy(last_dot, ".1");
wifi_cfg_set_static_ip(ip, mask, gw);
wifi_cfg_set_dhcp(false);
} else {
wifi_cfg_set_dhcp(dhcp);
}
printf("Config saved. Rebooting to apply...\n");
esp_restart();
return 0;
}
static void register_wifi_cmd(void) {
wifi_args.ssid = arg_str0("s", "ssid", "<ssid>", "WiFi SSID");
wifi_args.pass = arg_str0("p", "password", "<pass>", "WiFi Password");
wifi_args.ip = arg_str0("i", "ip", "<ip>", "Static IP");
wifi_args.dhcp = arg_lit0("d", "dhcp", "Enable DHCP");
wifi_args.help = arg_lit0("h", "help", "Show help");
wifi_args.end = arg_end(20);
const esp_console_cmd_t cmd = {
.command = "wifi_config",
.help = "Configure WiFi credentials",
.hint = NULL,
.func = &cmd_wifi_config,
.argtable = &wifi_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
// --- Registration ---
void app_console_register_commands(void) {
register_iperf_cmd();
register_system_cmd();
register_nvs_cmd();
register_wifi_cmd();
register_iperf_cmd();
register_gps_cmd();
register_ping_cmd();
register_monitor_cmd();
register_ip_cmd();
}

View File

@ -1,14 +1,57 @@
/*
* app_console.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Register application-specific console commands
*/
// This matches the call in main.c
void app_console_register_commands(void);
// Helper for prompt updates
void app_console_update_prompt(void);
// Sub-module registers
void register_system_cmd(void);
void register_wifi_cmd(void);
void register_iperf_cmd(void);
void register_nvs_cmd(void);
void register_gps_cmd(void);
void register_ping_cmd(void);
void register_monitor_cmd(void);
void register_ip_cmd(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,145 @@
/*
* cmd_gps.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <time.h>
#include <sys/time.h>
#include "esp_log.h"
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "gps_sync.h"
#include "app_console.h"
// --- Forward Declarations ---
static int gps_do_status(int argc, char **argv);
// ============================================================================
// COMMAND: gps (Dispatcher)
// ============================================================================
static void print_gps_usage(void) {
printf("Usage: gps <subcommand> [args]\n");
printf("Subcommands:\n");
printf(" status Show GPS lock status, time, and last NMEA message\n");
printf("\nType 'gps <subcommand> --help' for details.\n");
}
static int cmd_gps(int argc, char **argv) {
if (argc < 2 || strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
print_gps_usage();
return 0;
}
if (strcmp(argv[1], "status") == 0) return gps_do_status(argc - 1, &argv[1]);
if (strcmp(argv[1], "info") == 0) return gps_do_status(argc - 1, &argv[1]); // Alias
printf("Unknown subcommand '%s'.\n", argv[1]);
print_gps_usage();
return 1;
}
// ----------------------------------------------------------------------------
// Sub-command: status
// ----------------------------------------------------------------------------
static struct {
struct arg_lit *help;
struct arg_end *end;
} status_args;
static int gps_do_status(int argc, char **argv) {
status_args.help = arg_lit0("h", "help", "Help");
status_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&status_args);
if (nerrors > 0) {
arg_print_errors(stderr, status_args.end, argv[0]);
return 1;
}
if (status_args.help->count > 0) {
printf("Usage: gps status\n");
return 0;
}
// 1. Get GPS Data
gps_timestamp_t ts = gps_get_timestamp();
int64_t pps_age = gps_get_pps_age_ms();
char nmea_buf[128] = "<Empty>";
gps_get_last_nmea(nmea_buf, sizeof(nmea_buf));
// Strip trailing newline
size_t len = strlen(nmea_buf);
if (len > 0 && (nmea_buf[len-1] == '\n' || nmea_buf[len-1] == '\r')) nmea_buf[len-1] = 0;
// 2. Format Time
char time_str[64] = "Unknown";
struct timespec ts_now = {0, 0};
if (ts.gps_us > 0) {
// Get raw timespec for display
clock_gettime(CLOCK_REALTIME, &ts_now);
time_t now_sec = ts_now.tv_sec;
struct tm tm_info;
gmtime_r(&now_sec, &tm_info);
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S UTC", &tm_info);
}
// 3. Print
printf("GPS Status:\n");
printf(" Time: %s\n", time_str);
// UPDATED: Show raw timespec structure
printf(" Timespec: tv_sec=%" PRId64 " tv_nsec=%ld\n", (int64_t)ts_now.tv_sec, ts_now.tv_nsec);
printf(" PPS Locked: %s (Age: %" PRId64 " ms)\n", ts.synced ? "YES" : "NO", pps_age);
printf(" NMEA Valid: %s\n", ts.valid ? "YES" : "NO");
printf(" Last Message: %s\n", nmea_buf);
return 0;
}
// ----------------------------------------------------------------------------
// Registration
// ----------------------------------------------------------------------------
void register_gps_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "gps",
.help = "GPS Tool: status",
.hint = "<subcommand>",
.func = &cmd_gps,
.argtable = NULL
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,184 @@
/*
* cmd_ip.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "esp_console.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "argtable3/argtable3.h"
#include "wifi_cfg.h"
#include "app_console.h"
// --- Arguments ---
static struct {
struct arg_str *ip;
struct arg_str *mask;
struct arg_str *gw;
struct arg_end *end;
} set_args;
static struct {
struct arg_str *mode; // "on" or "off"
struct arg_end *end;
} dhcp_args;
static void print_if_info(esp_netif_t *netif, const char *name) {
if (netif == NULL) return;
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
printf("%s:\n", name);
printf(" IP: " IPSTR "\n", IP2STR(&ip_info.ip));
printf(" Mask: " IPSTR "\n", IP2STR(&ip_info.netmask));
printf(" GW: " IPSTR "\n", IP2STR(&ip_info.gw));
esp_netif_dhcp_status_t status;
esp_netif_dhcpc_get_status(netif, &status);
printf(" DHCP: %s\n", (status == ESP_NETIF_DHCP_STARTED) ? "ON" : "OFF");
uint8_t mac[6] = {0};
if (esp_netif_get_mac(netif, mac) == ESP_OK) {
printf(" MAC: %02x:%02x:%02x:%02x:%02x:%02x\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
printf("\n");
}
}
static int do_ip_addr(void) {
print_if_info(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"), "Wi-Fi Station");
return 0;
}
static int do_ip_set(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&set_args);
if (nerrors > 0) {
arg_print_errors(stderr, set_args.end, argv[0]);
return 1;
}
const char *ip = set_args.ip->sval[0];
const char *mask = set_args.mask->sval[0];
const char *gw = set_args.gw->sval[0];
// Validate and Convert (API Fix: esp_ip4addr_aton returns uint32_t)
esp_netif_ip_info_t info = {0};
info.ip.addr = esp_ip4addr_aton(ip);
info.netmask.addr = esp_ip4addr_aton(mask);
info.gw.addr = esp_ip4addr_aton(gw);
// Basic validation: 0 means conversion failed (or actual 0.0.0.0)
if (info.ip.addr == 0 || info.netmask.addr == 0) {
printf("Invalid IP format.\n");
return 1;
}
// Save
wifi_cfg_set_ipv4(ip, mask, gw);
wifi_cfg_set_dhcp(false); // Implicitly disable DHCP
// Apply
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_dhcpc_stop(netif);
esp_netif_set_ip_info(netif, &info);
printf("Static IP set. DHCP disabled.\n");
} else {
printf("Saved. Will apply on next init.\n");
}
return 0;
}
static int do_ip_dhcp(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&dhcp_args);
if (nerrors > 0) {
arg_print_errors(stderr, dhcp_args.end, argv[0]);
return 1;
}
bool enable = (strcmp(dhcp_args.mode->sval[0], "on") == 0);
wifi_cfg_set_dhcp(enable);
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
if (enable) {
esp_netif_dhcpc_start(netif);
printf("DHCP enabled.\n");
} else {
esp_netif_dhcpc_stop(netif);
printf("DHCP disabled.\n");
}
}
return 0;
}
static void print_ip_usage(void) {
printf("Usage: ip <subcommand>\n");
printf("Subcommands:\n");
printf(" addr Show config\n");
printf(" set <ip> <mask> <gw> Set static IP (Disables DHCP)\n");
printf(" dhcp <on|off> Enable/Disable DHCP\n");
}
static int cmd_ip(int argc, char **argv) {
if (argc < 2) {
print_ip_usage();
return 0;
}
if (strcmp(argv[1], "addr") == 0) return do_ip_addr();
if (strcmp(argv[1], "set") == 0) return do_ip_set(argc - 1, &argv[1]);
if (strcmp(argv[1], "dhcp") == 0) return do_ip_dhcp(argc - 1, &argv[1]);
print_ip_usage();
return 1;
}
void register_ip_cmd(void) {
// Args
set_args.ip = arg_str1(NULL, NULL, "<ip>", "IP Address");
set_args.mask = arg_str1(NULL, NULL, "<mask>", "Netmask");
set_args.gw = arg_str1(NULL, NULL, "<gw>", "Gateway");
set_args.end = arg_end(3);
dhcp_args.mode = arg_str1(NULL, NULL, "<on|off>", "Mode");
dhcp_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
.command = "ip",
.help = "IP Config: addr, set, dhcp",
.hint = "<subcommand>",
.func = &cmd_ip,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,239 @@
/*
* cmd_iperf.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "arpa/inet.h"
#include "iperf.h"
#include "app_console.h"
// --- Forward Declarations ---
static int iperf_do_start(int argc, char **argv);
static int iperf_do_stop(int argc, char **argv);
static int iperf_do_status(int argc, char **argv);
static int iperf_do_set(int argc, char **argv);
static int iperf_do_save(int argc, char **argv);
static int iperf_do_reload(int argc, char **argv);
static int iperf_do_clear(int argc, char **argv);
// ============================================================================
// COMMAND: iperf (Dispatcher)
// ============================================================================
static void print_iperf_usage(void) {
printf("Usage: iperf <subcommand> [args]\n");
printf("Subcommands:\n");
printf(" start Start traffic generation\n");
printf(" stop Stop traffic generation\n");
printf(" status Show current statistics\n");
printf(" set Configure parameters (IP, Port, PPS, etc.)\n");
printf(" save Save configuration to NVS\n");
printf(" reload Reload configuration from NVS\n");
printf(" clear Reset configuration to defaults\n");
printf("\nType 'iperf <subcommand> --help' for details.\n");
}
static int cmd_iperf(int argc, char **argv) {
if (argc < 2 || strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
print_iperf_usage();
return 0;
}
if (strcmp(argv[1], "start") == 0) return iperf_do_start(argc - 1, &argv[1]);
if (strcmp(argv[1], "stop") == 0) return iperf_do_stop(argc - 1, &argv[1]);
if (strcmp(argv[1], "status") == 0) return iperf_do_status(argc - 1, &argv[1]);
if (strcmp(argv[1], "set") == 0) return iperf_do_set(argc - 1, &argv[1]);
if (strcmp(argv[1], "save") == 0) return iperf_do_save(argc - 1, &argv[1]);
if (strcmp(argv[1], "reload") == 0) return iperf_do_reload(argc - 1, &argv[1]);
if (strcmp(argv[1], "clear") == 0) return iperf_do_clear(argc - 1, &argv[1]);
printf("Unknown subcommand '%s'.\n", argv[1]);
print_iperf_usage();
return 1;
}
// ----------------------------------------------------------------------------
// Sub-command: start
// ----------------------------------------------------------------------------
static int iperf_do_start(int argc, char **argv) {
iperf_start();
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: stop
// ----------------------------------------------------------------------------
static int iperf_do_stop(int argc, char **argv) {
iperf_stop();
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: status
// ----------------------------------------------------------------------------
static int iperf_do_status(int argc, char **argv) {
iperf_print_status();
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: set (Configuration)
// ----------------------------------------------------------------------------
static struct {
struct arg_str *ip;
struct arg_int *port;
struct arg_int *pps;
struct arg_int *len;
struct arg_int *burst;
struct arg_lit *help;
struct arg_end *end;
} set_args;
static int iperf_do_set(int argc, char **argv) {
// REVERTED: Now uses standard iPerf syntax ("client" / "-c")
set_args.ip = arg_str0("c", "client", "<ip>", "Destination IP");
set_args.port = arg_int0("p", "port", "<port>", "Destination Port");
set_args.pps = arg_int0(NULL, "pps", "<n>", "Packets Per Second");
set_args.len = arg_int0("l", "len", "<bytes>", "Packet Length");
set_args.burst = arg_int0("b", "burst", "<count>", "Burst Count");
set_args.help = arg_lit0("h", "help", "Help");
set_args.end = arg_end(20);
int nerrors = arg_parse(argc, argv, (void **)&set_args);
if (nerrors > 0) {
// --- CUSTOM ERROR HINTING ---
// Check if user tried to use "--ip"
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], "--ip") == 0) {
printf("Error: Invalid option '--ip'. Did you mean '--client' (or -c) to set the destination IP?\n");
return 1;
}
}
// Fallback to standard error
arg_print_errors(stderr, set_args.end, argv[0]);
return 1;
}
if (set_args.help->count > 0) {
printf("Usage: iperf set [options]\n");
arg_print_glossary(stdout, (void **)&set_args, " %-25s %s\n");
return 0;
}
iperf_cfg_t cfg;
iperf_param_get(&cfg);
bool changed = false;
if (set_args.ip->count > 0) {
cfg.dip = inet_addr(set_args.ip->sval[0]);
changed = true;
}
if (set_args.port->count > 0) {
cfg.dport = (uint16_t)set_args.port->ival[0];
changed = true;
}
if (set_args.len->count > 0) {
cfg.send_len = (uint32_t)set_args.len->ival[0];
changed = true;
}
if (set_args.burst->count > 0) {
cfg.burst_count = (uint32_t)set_args.burst->ival[0];
changed = true;
}
if (set_args.pps->count > 0) {
if (set_args.pps->ival[0] > 0) {
cfg.target_pps = (uint32_t)set_args.pps->ival[0];
changed = true;
} else {
printf("Error: PPS must be > 0.\n");
return 1;
}
}
if (changed) {
iperf_param_set(&cfg);
printf("Configuration updated (RAM only). Run 'iperf save' to persist.\n");
} else {
printf("No changes specified.\n");
}
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: save
// ----------------------------------------------------------------------------
static int iperf_do_save(int argc, char **argv) {
bool changed = false;
if (iperf_param_save(&changed) == ESP_OK) {
printf(changed ? "Configuration saved to NVS.\n" : "No changes to save (NVS matches RAM).\n");
} else {
printf("Error saving to NVS.\n");
}
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: reload
// ----------------------------------------------------------------------------
static int iperf_do_reload(int argc, char **argv) {
iperf_param_init(); // Force re-read from NVS
printf("Configuration reloaded from NVS.\n");
return 0;
}
// ----------------------------------------------------------------------------
// Sub-command: clear
// ----------------------------------------------------------------------------
static int iperf_do_clear(int argc, char **argv) {
iperf_param_clear();
printf("iPerf Configuration cleared (Reset to defaults).\n");
return 0;
}
// ----------------------------------------------------------------------------
// Registration
// ----------------------------------------------------------------------------
void register_iperf_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "iperf",
.help = "Traffic Gen: start, stop, set, status",
.hint = "<subcommand> [args]",
.func = &cmd_iperf,
.argtable = NULL
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,141 @@
/*
* cmd_monitor.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "wifi_controller.h"
#include "app_console.h"
// --- Subcommand Arguments ---
static struct {
struct arg_int *channel;
struct arg_end *end;
} start_args;
static struct {
struct arg_int *channel;
struct arg_end *end;
} channel_args;
static void print_monitor_usage(void) {
printf("Usage: monitor <subcommand> [args]\n");
printf("Subcommands:\n");
printf(" start [-c <n>] Start Monitor Mode (optional: set channel)\n");
printf(" stop Stop Monitor Mode\n");
printf(" status Show current status\n");
printf(" channel <n> Switch channel (while running)\n");
printf(" save Save current config to NVS\n");
printf(" reload Reload config from NVS\n");
printf(" clear Clear NVS config\n");
}
// --- Subcommand Handlers ---
static int do_monitor_start(int argc, char **argv) {
start_args.channel = arg_int0("c", "channel", "<n>", "Channel (1-13)");
start_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&start_args);
if (nerrors > 0) {
arg_print_errors(stderr, start_args.end, argv[0]);
return 1;
}
int ch = 0;
if (start_args.channel->count > 0) {
ch = start_args.channel->ival[0];
}
printf("Starting Monitor Mode%s...\n", ch ? " on specific channel" : "");
wifi_ctl_monitor_start(ch);
return 0;
}
static int do_monitor_channel(int argc, char **argv) {
channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel (1-13)");
channel_args.end = arg_end(1);
int nerrors = arg_parse(argc, argv, (void **)&channel_args);
if (nerrors > 0) {
arg_print_errors(stderr, channel_args.end, argv[0]);
return 1;
}
int ch = channel_args.channel->ival[0];
printf("Switching to Channel %d...\n", ch);
wifi_ctl_set_channel(ch);
return 0;
}
static int cmd_monitor(int argc, char **argv) {
if (argc < 2) {
print_monitor_usage();
return 0;
}
if (strcmp(argv[1], "start") == 0) return do_monitor_start(argc - 1, &argv[1]);
if (strcmp(argv[1], "stop") == 0) { wifi_ctl_stop(); return 0; }
if (strcmp(argv[1], "status") == 0) { wifi_ctl_status(); return 0; }
if (strcmp(argv[1], "save") == 0) { wifi_ctl_param_save(NULL); printf("Saved.\n"); return 0; }
if (strcmp(argv[1], "reload") == 0) { wifi_ctl_param_init(); printf("Reloaded.\n"); return 0; }
if (strcmp(argv[1], "clear") == 0) { wifi_ctl_param_clear(); printf("Cleared.\n"); return 0; }
if (strcmp(argv[1], "channel") == 0) return do_monitor_channel(argc - 1, &argv[1]);
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) {
print_monitor_usage();
return 0;
}
printf("Unknown subcommand '%s'.\n", argv[1]);
print_monitor_usage();
return 1;
}
void register_monitor_cmd(void) {
start_args.channel = arg_int0("c", "channel", "<n>", "Channel");
start_args.end = arg_end(1);
channel_args.channel = arg_int1(NULL, NULL, "<n>", "Channel");
channel_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
.command = "monitor",
.help = "Monitor Mode: start, stop, channel, status",
.hint = "<subcommand>",
.func = &cmd_monitor,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,210 @@
/*
* cmd_nvs.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_console.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "argtable3/argtable3.h"
#include "app_console.h"
static void print_nvs_usage(void) {
printf("Usage: nvs <subcommand>\n");
printf("Subcommands:\n");
printf(" dump Dump 'storage' namespace keys and values\n");
printf(" clear Erase 'storage' namespace (Factory Reset)\n");
}
static void print_value(nvs_handle_t h, const char *key, nvs_type_t type) {
esp_err_t err;
switch (type) {
case NVS_TYPE_I8: {
int8_t v = 0;
err = nvs_get_i8(h, key, &v);
if (err == ESP_OK) printf("Value: %d (I8)", v);
break;
}
case NVS_TYPE_U8: {
uint8_t v = 0;
err = nvs_get_u8(h, key, &v);
if (err == ESP_OK) printf("Value: %u (U8)", v);
break;
}
case NVS_TYPE_I16: {
int16_t v = 0;
err = nvs_get_i16(h, key, &v);
if (err == ESP_OK) printf("Value: %d (I16)", v);
break;
}
case NVS_TYPE_U16: {
uint16_t v = 0;
err = nvs_get_u16(h, key, &v);
if (err == ESP_OK) printf("Value: %u (U16)", v);
break;
}
case NVS_TYPE_I32: {
int32_t v = 0;
err = nvs_get_i32(h, key, &v);
if (err == ESP_OK) printf("Value: %" PRIi32 " (I32)", v);
break;
}
case NVS_TYPE_U32: {
uint32_t v = 0;
err = nvs_get_u32(h, key, &v);
if (err == ESP_OK) printf("Value: %" PRIu32 " (U32)", v);
break;
}
case NVS_TYPE_I64: {
int64_t v = 0;
err = nvs_get_i64(h, key, &v);
if (err == ESP_OK) printf("Value: %" PRIi64 " (I64)", v);
break;
}
case NVS_TYPE_U64: {
uint64_t v = 0;
err = nvs_get_u64(h, key, &v);
if (err == ESP_OK) printf("Value: %" PRIu64 " (U64)", v);
break;
}
case NVS_TYPE_STR: {
size_t len = 0;
if (nvs_get_str(h, key, NULL, &len) == ESP_OK) {
char *str = malloc(len);
if (nvs_get_str(h, key, str, &len) == ESP_OK) {
printf("Value: \"%s\" (STR)", str);
}
free(str);
}
break;
}
case NVS_TYPE_BLOB: {
size_t len = 0;
if (nvs_get_blob(h, key, NULL, &len) == ESP_OK) {
printf("Value: [BLOB %u bytes]", (unsigned int)len);
}
break;
}
default:
printf("Value: [Unknown Type 0x%02x]", type);
break;
}
}
static int do_nvs_dump(int argc, char **argv) {
nvs_iterator_t it = NULL;
nvs_handle_t h;
// 1. Open Handle (Needed to read values)
esp_err_t err = nvs_open("storage", NVS_READONLY, &h);
if (err != ESP_OK) {
printf("Error opening NVS handle: %s\n", esp_err_to_name(err));
return 1;
}
// 2. Start Iterator
esp_err_t res = nvs_entry_find("nvs", "storage", NVS_TYPE_ANY, &it);
if (res != ESP_OK) {
nvs_close(h);
if (res == ESP_ERR_NVS_NOT_FOUND) {
printf("No NVS entries found in 'storage'.\n");
} else {
printf("NVS Search Error: %s\n", esp_err_to_name(res));
}
return 0;
}
printf("NVS Dump (Namespace: storage):\n");
printf("%-20s | %-12s | %s\n", "Key", "Type", "Value");
printf("------------------------------------------------------------\n");
while (res == ESP_OK) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
printf("%-20s | 0x%02x | ", info.key, info.type);
print_value(h, info.key, info.type);
printf("\n");
res = nvs_entry_next(&it);
}
nvs_release_iterator(it);
nvs_close(h);
return 0;
}
static int do_nvs_clear(int argc, char **argv) {
nvs_handle_t h;
esp_err_t err = nvs_open("storage", NVS_READWRITE, &h);
if (err != ESP_OK) {
printf("Error opening NVS: %s\n", esp_err_to_name(err));
return 1;
}
err = nvs_erase_all(h);
if (err == ESP_OK) {
nvs_commit(h);
printf("NVS 'storage' namespace erased.\n");
} else {
printf("Error erasing NVS: %s\n", esp_err_to_name(err));
}
nvs_close(h);
return (err == ESP_OK) ? 0 : 1;
}
static int cmd_nvs(int argc, char **argv) {
if (argc < 2) {
print_nvs_usage();
return 0;
}
if (strcmp(argv[1], "dump") == 0) return do_nvs_dump(argc - 1, &argv[1]);
if (strcmp(argv[1], "clear") == 0) return do_nvs_clear(argc - 1, &argv[1]);
print_nvs_usage();
return 1;
}
void register_nvs_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "nvs",
.help = "Storage Tools: dump, clear",
.hint = "<subcommand>",
.func = &cmd_nvs,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,173 @@
/*
* cmd_ping.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "ping/ping_sock.h"
#include "lwip/inet.h"
#include "lwip/netdb.h"
#include "app_console.h"
// ============================================================================
// COMMAND: ping (ICMP Echo)
// ============================================================================
static struct {
struct arg_str *host;
struct arg_int *count;
struct arg_int *interval;
struct arg_end *end;
} ping_args;
static void cmd_ping_on_ping_success(esp_ping_handle_t hdl, void *args) {
uint8_t ttl;
uint16_t seqno;
uint32_t elapsed_time, recv_len;
ip_addr_t target_addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len));
esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time));
printf("%" PRIu32 " bytes from %s: icmp_seq=%u ttl=%u time=%" PRIu32 " ms\n",
recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time);
}
static void cmd_ping_on_ping_timeout(esp_ping_handle_t hdl, void *args) {
uint16_t seqno;
ip_addr_t target_addr;
esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
printf("From %s: icmp_seq=%u timeout\n", inet_ntoa(target_addr.u_addr.ip4), seqno);
}
static void cmd_ping_on_ping_end(esp_ping_handle_t hdl, void *args) {
uint32_t transmitted;
uint32_t received;
uint32_t total_time_ms;
esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms));
printf("\n--- ping statistics ---\n");
printf("%" PRIu32 " packets transmitted, %" PRIu32 " received, %" PRIu32 "%% packet loss, time %" PRIu32 "ms\n",
transmitted, received, (transmitted - received) * 100 / transmitted, total_time_ms);
esp_ping_delete_session(hdl);
}
static int cmd_ping(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&ping_args);
if (nerrors != 0) {
arg_print_errors(stderr, ping_args.end, argv[0]);
return 1;
}
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
// Parse Args
if (ping_args.count->count > 0) {
config.count = ping_args.count->ival[0];
}
if (ping_args.interval->count > 0) {
config.interval_ms = ping_args.interval->ival[0] * 1000;
}
// Parse Target IP or Hostname
ip_addr_t target_addr;
struct addrinfo hint;
struct addrinfo *res = NULL;
memset(&hint, 0, sizeof(hint));
memset(&target_addr, 0, sizeof(target_addr));
// Check if simple IP string
if (inet_aton(ping_args.host->sval[0], &target_addr.u_addr.ip4)) {
target_addr.type = IPADDR_TYPE_V4;
} else {
// Resolve Hostname
printf("Resolving %s...\n", ping_args.host->sval[0]);
// Set hint to prefer IPv4 if desired, or leave 0 for ANY
hint.ai_family = AF_INET;
if (getaddrinfo(ping_args.host->sval[0], NULL, &hint, &res) != 0) {
printf("ping: unknown host %s\n", ping_args.host->sval[0]);
return 1;
}
// Convert struct sockaddr_in to ip_addr_t
struct sockaddr_in *sa = (struct sockaddr_in *)res->ai_addr;
inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &sa->sin_addr);
target_addr.type = IPADDR_TYPE_V4;
freeaddrinfo(res);
}
config.target_addr = target_addr;
config.task_stack_size = 4096; // Ensure enough stack for callbacks
esp_ping_callbacks_t cbs = {
.on_ping_success = cmd_ping_on_ping_success,
.on_ping_timeout = cmd_ping_on_ping_timeout,
.on_ping_end = cmd_ping_on_ping_end,
.cb_args = NULL
};
esp_ping_handle_t ping;
esp_ping_new_session(&config, &cbs, &ping);
esp_ping_start(ping);
return 0;
}
void register_ping_cmd(void) {
ping_args.host = arg_str1(NULL, NULL, "<host>", "Host address or name");
ping_args.count = arg_int0("c", "count", "<n>", "Stop after <n> replies");
ping_args.interval = arg_int0("i", "interval", "<seconds>", "Wait interval");
ping_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
.command = "ping",
.help = "Send ICMP ECHO_REQUEST to network hosts",
.hint = NULL,
.func = &cmd_ping,
.argtable = &ping_args
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,167 @@
/*
* cmd_system.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_console.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "argtable3/argtable3.h"
#include "app_console.h"
// Define default version if not passed by CMake
#ifndef APP_VERSION
#define APP_VERSION "2.0.0-SHELL"
#endif
// --- Helper: Convert Model ID to String ---
static const char* get_chip_model_string(esp_chip_model_t model) {
switch (model) {
case CHIP_ESP32: return "ESP32";
case CHIP_ESP32S2: return "ESP32-S2";
case CHIP_ESP32S3: return "ESP32-S3";
case CHIP_ESP32C3: return "ESP32-C3";
case CHIP_ESP32C2: return "ESP32-C2";
case CHIP_ESP32C6: return "ESP32-C6";
case CHIP_ESP32H2: return "ESP32-H2";
// Explicitly handle ID 23 for C5 if macro is missing
case 23: return "ESP32-C5";
#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)) && defined(CHIP_ESP32P4)
case CHIP_ESP32P4: return "ESP32-P4";
#endif
#ifdef CHIP_ESP32C5
case CHIP_ESP32C5: return "ESP32-C5";
#endif
default: return "Unknown";
}
}
// --- Command Handlers ---
static int do_system_reboot(int argc, char **argv) {
printf("Rebooting...\n");
esp_restart();
return 0;
}
static int do_version(int argc, char **argv) {
printf("APP_VERSION: %s\n", APP_VERSION);
printf("IDF_VERSION: %s\n", esp_get_idf_version());
return 0;
}
static int do_system_info(int argc, char **argv) {
esp_chip_info_t info;
esp_chip_info(&info);
const char *model_str = get_chip_model_string(info.model);
printf("IDF Version: %s\n", esp_get_idf_version());
if (strcmp(model_str, "Unknown") == 0) {
printf("Chip Info: Model=%s (ID=%d), Cores=%d, Revision=%d\n",
model_str, info.model, info.cores, info.revision);
} else {
printf("Chip Info: Model=%s, Cores=%d, Revision=%d\n",
model_str, info.cores, info.revision);
}
uint32_t flash_size = 0;
if (esp_flash_get_size(NULL, &flash_size) == ESP_OK) {
printf("Flash Size: %" PRIu32 " MB\n", flash_size / (1024 * 1024));
} else {
printf("Flash Size: Unknown\n");
}
printf("Features: %s%s%s%s%s\n",
(info.features & CHIP_FEATURE_WIFI_BGN) ? "802.11bgn " : "",
(info.features & CHIP_FEATURE_BLE) ? "BLE " : "",
(info.features & CHIP_FEATURE_BT) ? "BT " : "",
(info.features & CHIP_FEATURE_IEEE802154) ? "802.15.4 " : "",
(info.features & CHIP_FEATURE_EMB_FLASH) ? "Embedded-Flash " : "");
return 0;
}
static int do_system_heap(int argc, char **argv) {
printf("Heap Info:\n");
printf(" Free: %" PRIu32 " bytes\n", esp_get_free_heap_size());
printf(" Min Free: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
return 0;
}
static int cmd_system(int argc, char **argv) {
if (argc < 2) {
printf("Usage: system <reboot|info|heap>\n");
return 0;
}
if (strcmp(argv[1], "reboot") == 0) return do_system_reboot(argc - 1, &argv[1]);
if (strcmp(argv[1], "info") == 0) return do_system_info(argc - 1, &argv[1]);
if (strcmp(argv[1], "heap") == 0) return do_system_heap(argc - 1, &argv[1]);
printf("Unknown subcommand '%s'.\n", argv[1]);
return 1;
}
// --- Registration ---
void register_system_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "system",
.help = "System Tools: reboot, info, heap",
.hint = "<subcommand>",
.func = &cmd_system,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
void register_reset_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "reset",
.help = "Software reset of the device",
.func = &do_system_reboot,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
void register_version_cmd(void) {
const esp_console_cmd_t cmd = {
.command = "version",
.help = "Get firmware version",
.func = &do_version,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -0,0 +1,221 @@
/*
* cmd_wifi.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_console.h"
#include "argtable3/argtable3.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "wifi_cfg.h"
#include "wifi_controller.h"
#include "app_console.h"
// --- Arguments ---
static struct {
struct arg_str *ssid;
struct arg_str *password;
struct arg_end *end;
} connect_args;
static struct {
struct arg_str *mode; // "sta", "monitor", "ap"
struct arg_int *channel;
struct arg_end *end;
} mode_args;
static struct {
struct arg_int *tx_power;
struct arg_end *end;
} power_args;
static void print_wifi_usage(void) {
printf("Usage: wifi <subcommand> [args]\n");
printf("Subcommands:\n");
printf(" scan Scan for networks\n");
printf(" connect <ssid> [<pass>] Connect to AP\n");
printf(" status Show connection info\n");
printf(" mode <sta|monitor> [-c <ch>] Switch Mode\n");
printf(" power <dBm> Set TX Power (8-84, 0.25dB units)\n");
}
// --- Handlers ---
static int wifi_do_scan(int argc, char **argv) {
printf("Scanning...\n");
wifi_scan_config_t scan_config = {0};
esp_wifi_scan_start(&scan_config, true); // Block until done
uint16_t ap_num = 0;
esp_wifi_scan_get_ap_num(&ap_num);
wifi_ap_record_t *ap_list = (wifi_ap_record_t *)malloc(ap_num * sizeof(wifi_ap_record_t));
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, ap_list));
printf("Found %d APs:\n", ap_num);
printf("%-32s | %-4s | %-4s | %-18s\n", "SSID", "RSSI", "CH", "BSSID");
for (int i = 0; i < ap_num; i++) {
printf("%-32s | %-4d | %-4d | %02x:%02x:%02x:%02x:%02x:%02x\n",
ap_list[i].ssid, ap_list[i].rssi, ap_list[i].primary,
ap_list[i].bssid[0], ap_list[i].bssid[1], ap_list[i].bssid[2],
ap_list[i].bssid[3], ap_list[i].bssid[4], ap_list[i].bssid[5]);
}
free(ap_list);
return 0;
}
static int wifi_do_connect(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&connect_args);
if (nerrors > 0) {
arg_print_errors(stderr, connect_args.end, argv[0]);
return 1;
}
const char *ssid = connect_args.ssid->sval[0];
const char *pass = (connect_args.password->count > 0) ? connect_args.password->sval[0] : "";
// Ensure we are in STA mode first
wifi_ctl_mode_t current_mode = wifi_ctl_get_mode();
if (current_mode != WIFI_CTL_MODE_STA) {
printf("Switching to Station Mode first...\n");
wifi_ctl_switch_to_sta(); // Fixed: Removed argument
}
printf("Connecting to '%s'...\n", ssid);
// Save to NVS
wifi_cfg_set_ssid(ssid);
wifi_cfg_set_password(pass);
// Apply
wifi_config_t wifi_config = {0};
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
wifi_config.sta.ssid[sizeof(wifi_config.sta.ssid) - 1] = '\0';
strncpy((char *)wifi_config.sta.password, pass, sizeof(wifi_config.sta.password) - 1);
wifi_config.sta.password[sizeof(wifi_config.sta.password) - 1] = '\0';
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_connect());
return 0;
}
static int wifi_do_status(int argc, char **argv) {
wifi_ctl_status();
return 0;
}
static int wifi_do_mode(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&mode_args);
if (nerrors > 0) {
arg_print_errors(stderr, mode_args.end, argv[0]);
return 1;
}
const char *mode_str = mode_args.mode->sval[0];
int channel = (mode_args.channel->count > 0) ? mode_args.channel->ival[0] : 0;
if (strcmp(mode_str, "sta") == 0) {
wifi_ctl_switch_to_sta(); // Fixed: Removed argument
} else if (strcmp(mode_str, "monitor") == 0) {
wifi_ctl_switch_to_monitor(channel, WIFI_BW_HT20);
} else {
printf("Unknown mode '%s'. Use 'sta' or 'monitor'.\n", mode_str);
return 1;
}
return 0;
}
static int wifi_do_power(int argc, char **argv) {
int nerrors = arg_parse(argc, argv, (void **)&power_args);
if (nerrors > 0) {
arg_print_errors(stderr, power_args.end, argv[0]);
return 1;
}
int pwr = power_args.tx_power->ival[0];
esp_err_t err = esp_wifi_set_max_tx_power(pwr);
if (err == ESP_OK) {
printf("TX Power set to %d (approx %.2f dBm)\n", pwr, pwr * 0.25);
} else {
printf("Failed to set TX power: %s\n", esp_err_to_name(err));
}
return 0;
}
static int cmd_wifi(int argc, char **argv) {
if (argc < 2) {
print_wifi_usage();
return 0;
}
if (strcmp(argv[1], "scan") == 0) return wifi_do_scan(argc - 1, &argv[1]);
if (strcmp(argv[1], "connect") == 0) return wifi_do_connect(argc - 1, &argv[1]);
if (strcmp(argv[1], "status") == 0) return wifi_do_status(argc - 1, &argv[1]);
if (strcmp(argv[1], "mode") == 0) return wifi_do_mode(argc - 1, &argv[1]);
if (strcmp(argv[1], "power") == 0) return wifi_do_power(argc - 1, &argv[1]);
if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) {
print_wifi_usage();
return 0;
}
printf("Unknown subcommand '%s'.\n", argv[1]);
print_wifi_usage();
return 1;
}
void register_wifi_cmd(void) {
// Connect Args
connect_args.ssid = arg_str1(NULL, NULL, "<ssid>", "SSID");
connect_args.password = arg_str0(NULL, NULL, "<pass>", "Password");
connect_args.end = arg_end(2);
// Mode Args
mode_args.mode = arg_str1(NULL, NULL, "<mode>", "sta | monitor");
mode_args.channel = arg_int0("c", "channel", "<n>", "Channel (Monitor only)");
mode_args.end = arg_end(2);
// Power Args
power_args.tx_power = arg_int1(NULL, NULL, "<dBm>", "Power (8-84)");
power_args.end = arg_end(1);
const esp_console_cmd_t cmd = {
.command = "wifi",
.help = "Wi-Fi Utils: scan, connect, mode, status, power",
.hint = "<subcommand>",
.func = &cmd_wifi,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

View File

@ -1,3 +1,36 @@
/*
* csi_log.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "csi_log.h"
#include <string.h>

View File

@ -1,3 +1,36 @@
/*
* csi_log.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <stdint.h>

View File

@ -1,3 +1,36 @@
/*
* csi_manager.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "csi_manager.h"
#include "csi_log.h"
#include "esp_wifi.h"

View File

@ -1,3 +1,36 @@
/*
* csi_manager.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <stdbool.h>

View File

@ -1,297 +1,282 @@
#include "gps_sync.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include <string.h>
#include <time.h>
#include <stdarg.h>
/*
* gps_sync.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "gps_sync.h"
static const char *TAG = "GPS_SYNC";
#define GPS_BAUD_RATE 9600
#define UART_BUF_SIZE 1024
#define GPS_BUF_SIZE 1024
// --- GLOBAL STATE ---
static uart_port_t gps_uart_num = UART_NUM_1;
static int64_t monotonic_offset_us = 0;
static volatile int64_t last_pps_monotonic = 0;
static volatile time_t next_pps_gps_second = 0;
static bool gps_has_fix = false;
static bool use_gps_for_logs = false;
static SemaphoreHandle_t sync_mutex;
static volatile bool force_sync_update = true;
// --- Internal State ---
static gps_sync_config_t s_cfg;
static volatile int64_t s_last_pps_us = 0;
static volatile int64_t s_nmea_epoch_us = 0;
static volatile bool s_nmea_valid = false;
static char s_last_nmea_msg[128] = {0};
static bool s_time_set = false;
// PPS interrupt
static void IRAM_ATTR pps_isr_handler(void* arg) {
static bool onetime = true;
last_pps_monotonic = esp_timer_get_time();
if (onetime) {
esp_rom_printf("PPS connected!\n");
onetime = false;
// --- PPS Handler ---
static void IRAM_ATTR pps_gpio_isr_handler(void* arg) {
s_last_pps_us = esp_timer_get_time();
}
// --- Time Helper ---
static void set_system_time(char *time_str, char *date_str) {
// time_str: HHMMSS.ss (e.g., 123519.00)
// date_str: DDMMYY (e.g., 230394)
struct tm tm_info = {0};
// Parse Time
int h, m, s;
if (sscanf(time_str, "%2d%2d%2d", &h, &m, &s) != 3) return;
tm_info.tm_hour = h;
tm_info.tm_min = m;
tm_info.tm_sec = s;
// Parse Date
int day, mon, year;
if (sscanf(date_str, "%2d%2d%2d", &day, &mon, &year) != 3) return;
tm_info.tm_mday = day;
tm_info.tm_mon = mon - 1; // 0-11
tm_info.tm_year = year + 100; // Years since 1900 (2025 -> 125)
time_t t = mktime(&tm_info);
if (t == -1) return;
struct timeval tv = { .tv_sec = t, .tv_usec = 0 };
// Simple sync: Only set if not set, or if drift is massive (>2s)
// In a real PTP/GPS app you'd use a PLL here, but this is a shell tool.
struct timeval now;
gettimeofday(&now, NULL);
if (!s_time_set || llabs(now.tv_sec - t) > 2) {
settimeofday(&tv, NULL);
s_time_set = true;
ESP_LOGI(TAG, "System Time Updated to GPS: %s", asctime(&tm_info));
}
}
// Parse GPS time from NMEA
static bool parse_gprmc(const char* nmea, struct tm* tm_out, bool* valid) {
if (strncmp(nmea, "$GPRMC", 6) != 0 && strncmp(nmea, "$GNRMC", 6) != 0) return false;
char *p = strchr(nmea, ',');
if (!p) return false;
p++;
int hour, min, sec;
if (sscanf(p, "%2d%2d%2d", &hour, &min, &sec) != 3) return false;
p = strchr(p, ',');
if (!p) return false;
p++;
*valid = (*p == 'A');
for (int i = 0; i < 7; i++) {
p = strchr(p, ',');
if (!p) return false;
p++;
}
int day, month, year;
if (sscanf(p, "%2d%2d%2d", &day, &month, &year) != 3) return false;
year += (year < 80) ? 2000 : 1900;
tm_out->tm_sec = sec;
tm_out->tm_min = min;
tm_out->tm_hour = hour;
tm_out->tm_mday = day;
tm_out->tm_mon = month - 1;
tm_out->tm_year = year - 1900;
tm_out->tm_isdst = 0;
return true;
}
// --- NMEA Parser ---
static void parse_nmea_line(char *line) {
strlcpy(s_last_nmea_msg, line, sizeof(s_last_nmea_msg));
void gps_force_next_update(void) {
force_sync_update = true;
ESP_LOGW(TAG, "Requesting forced GPS sync update");
}
// Support GPRMC and GNRMC
if (strncmp(line, "$GPRMC", 6) == 0 || strncmp(line, "$GNRMC", 6) == 0) {
char *p = line;
int field = 0;
char *time_ptr = NULL;
char *date_ptr = NULL;
char status = 'V';
static void gps_task(void* arg) {
uint8_t d_buf[64];
char line[128];
int pos = 0;
static int log_counter = 0;
// Walk fields
// $GPRMC,Time,Status,Lat,NS,Lon,EW,Spd,Trk,Date,...
// Field 1: Time
// Field 2: Status
// Field 9: Date
while (1) {
int len = uart_read_bytes(gps_uart_num, d_buf, sizeof(d_buf), pdMS_TO_TICKS(100));
while ((p = strchr(p, ',')) != NULL) {
p++;
field++;
if (len > 0) {
for (int i = 0; i < len; i++) {
uint8_t data = d_buf[i];
if (data == '\n') {
line[pos] = '\0';
struct tm gps_tm;
bool valid;
if (parse_gprmc(line, &gps_tm, &valid)) {
if (valid) {
time_t gps_time = mktime(&gps_tm);
xSemaphoreTake(sync_mutex, portMAX_DELAY);
next_pps_gps_second = gps_time + 1;
xSemaphoreGive(sync_mutex);
vTaskDelay(pdMS_TO_TICKS(300));
xSemaphoreTake(sync_mutex, portMAX_DELAY);
if (last_pps_monotonic > 0) {
int64_t gps_us = (int64_t)next_pps_gps_second * 1000000LL;
int64_t new_offset = gps_us - last_pps_monotonic;
if (monotonic_offset_us == 0 || force_sync_update) {
monotonic_offset_us = new_offset;
if (force_sync_update) {
ESP_LOGW(TAG, "GPS sync SNAP: Offset forced to %" PRIi64 " us", monotonic_offset_us);
force_sync_update = false;
log_counter = 0;
}
} else {
monotonic_offset_us = (monotonic_offset_us * 9 + new_offset) / 10;
}
gps_has_fix = true;
if (log_counter == 0) {
ESP_LOGI(TAG, "GPS sync: %04d-%02d-%02d %02d:%02d:%02d, offset=%" PRIi64 " us",
gps_tm.tm_year + 1900, gps_tm.tm_mon + 1, gps_tm.tm_mday,
gps_tm.tm_hour, gps_tm.tm_min, gps_tm.tm_sec,
monotonic_offset_us);
log_counter = 60;
}
log_counter--;
}
xSemaphoreGive(sync_mutex);
} else {
gps_has_fix = false;
}
if (field == 1) time_ptr = p;
else if (field == 2) status = *p;
else if (field == 9) {
date_ptr = p;
break; // We have what we need
}
}
s_nmea_valid = (status == 'A');
if (s_nmea_valid) {
s_nmea_epoch_us = esp_timer_get_time();
// Extract substrings for Time/Date (comma terminated)
if (time_ptr && date_ptr) {
char t_buf[16] = {0};
char d_buf[16] = {0};
char *end = strchr(time_ptr, ',');
if (end) {
int len = end - time_ptr;
if (len < sizeof(t_buf)) {
memcpy(t_buf, time_ptr, len);
t_buf[len] = 0;
}
pos = 0;
} else if (pos < sizeof(line) - 1) {
line[pos++] = data;
}
end = strchr(date_ptr, ',');
if (end) {
int len = end - date_ptr;
if (len < sizeof(d_buf)) {
memcpy(d_buf, date_ptr, len);
d_buf[len] = 0;
}
}
// Update System Clock
if (t_buf[0] && d_buf[0]) {
set_system_time(t_buf, d_buf);
}
}
}
}
}
void gps_sync_init(const gps_sync_config_t *config, bool use_gps_log_timestamps) {
ESP_LOGI(TAG, "Checking for GPS PPS signal on GPIO %d...", config->pps_pin);
// --- UART Task ---
static void gps_task(void *pvParameters) {
uint8_t *data = (uint8_t *)malloc(GPS_BUF_SIZE);
if (!data) {
ESP_LOGE(TAG, "Failed to allocate GPS buffer");
vTaskDelete(NULL);
return;
}
// 1. Configure PPS pin as Input to sense signal
gpio_config_t pps_conf = {
.pin_bit_mask = (1ULL << config->pps_pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE, // High-Z to detect active driving
.intr_type = GPIO_INTR_DISABLE
};
ESP_ERROR_CHECK(gpio_config(&pps_conf));
char line_buf[128];
int line_pos = 0;
// 2. Poll for ~3 seconds to detect ANY edge transition
bool pps_detected = false;
int start_level = gpio_get_level(config->pps_pin);
// Poll loop: 3000 iterations * 1ms = 3 seconds
for (int i = 0; i < 3000; i++) {
int current_level = gpio_get_level(config->pps_pin);
if (current_level != start_level) {
pps_detected = true;
break; // Signal found!
while (1) {
int len = uart_read_bytes(s_cfg.uart_port, data, GPS_BUF_SIZE, 20 / portTICK_PERIOD_MS);
if (len > 0) {
for (int i = 0; i < len; i++) {
char c = (char)data[i];
if (c == '\n' || c == '\r') {
if (line_pos > 0) {
line_buf[line_pos] = 0;
parse_nmea_line(line_buf);
line_pos = 0;
}
} else if (line_pos < sizeof(line_buf) - 1) {
line_buf[line_pos++] = c;
}
}
}
vTaskDelay(pdMS_TO_TICKS(1));
}
free(data);
vTaskDelete(NULL);
}
if (!pps_detected) {
printf("GPS PPS not found over GPIO with pin number %d\n", config->pps_pin);
ESP_LOGW(TAG, "GPS initialization aborted due to lack of PPS signal.");
return; // ABORT INITIALIZATION
}
// --- API ---
ESP_LOGI(TAG, "PPS signal detected! Initializing GPS subsystem...");
// 3. Proceed with Full Initialization
gps_uart_num = config->uart_port;
use_gps_for_logs = use_gps_log_timestamps;
gps_force_next_update();
if (use_gps_log_timestamps) {
ESP_LOGI(TAG, "ESP_LOG timestamps: GPS time in seconds.milliseconds format");
esp_log_set_vprintf(gps_log_vprintf);
}
sync_mutex = xSemaphoreCreateMutex();
void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) {
if (!cfg) return;
s_cfg = *cfg;
uart_config_t uart_config = {
.baud_rate = GPS_BAUD_RATE,
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_driver_install(s_cfg.uart_port, GPS_BUF_SIZE * 2, 0, 0, NULL, 0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to install UART driver: %s", esp_err_to_name(err));
return;
}
uart_param_config(s_cfg.uart_port, &uart_config);
err = uart_set_pin(s_cfg.uart_port, s_cfg.tx_pin, s_cfg.rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set UART pins: %s", esp_err_to_name(err));
return;
}
ESP_ERROR_CHECK(uart_driver_install(config->uart_port, UART_BUF_SIZE, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(config->uart_port, &uart_config));
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_POSEDGE;
io_conf.pin_bit_mask = (1ULL << s_cfg.pps_pin);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = 1;
err = gpio_config(&io_conf);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure PPS GPIO %d: %s", s_cfg.pps_pin, esp_err_to_name(err));
return;
}
ESP_ERROR_CHECK(uart_set_pin(config->uart_port,
config->tx_pin,
config->rx_pin,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE));
// Install ISR service (ignore error if already installed)
err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to install GPIO ISR service: %s", esp_err_to_name(err));
return;
}
// Re-configure PPS for Interrupts (Posedge)
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_POSEDGE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << config->pps_pin),
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_install_isr_service(0);
ESP_ERROR_CHECK(gpio_isr_handler_add(config->pps_pin, pps_isr_handler, NULL));
err = gpio_isr_handler_add(s_cfg.pps_pin, pps_gpio_isr_handler, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add PPS GPIO ISR handler: %s", esp_err_to_name(err));
return;
}
xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL);
ESP_LOGI(TAG, "GPS sync initialized (UART=%d, RX=%d, TX=%d, PPS=%d)",
config->uart_port, config->rx_pin, config->tx_pin, config->pps_pin);
ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d)", s_cfg.uart_port, s_cfg.pps_pin);
}
gps_timestamp_t gps_get_timestamp(void) {
gps_timestamp_t ts;
clock_gettime(CLOCK_MONOTONIC, &ts.mono_ts);
xSemaphoreTake(sync_mutex, portMAX_DELAY);
ts.monotonic_us = (int64_t)ts.mono_ts.tv_sec * 1000000LL + ts.mono_ts.tv_nsec / 1000;
ts.monotonic_ms = ts.monotonic_us / 1000;
ts.gps_us = ts.monotonic_us + monotonic_offset_us;
ts.gps_ms = ts.gps_us / 1000;
ts.synced = gps_has_fix;
xSemaphoreGive(sync_mutex);
gps_timestamp_t ts = {0};
int64_t now_boot = esp_timer_get_time(); // Boot time
// Check Flags
ts.synced = (now_boot - s_last_pps_us < 1100000);
ts.valid = s_nmea_valid && (now_boot - s_nmea_epoch_us < 2000000);
// Return WALL CLOCK time (Epoch), not boot time
struct timeval tv;
gettimeofday(&tv, NULL);
ts.gps_us = (int64_t)tv.tv_sec * 1000000LL + (int64_t)tv.tv_usec;
return ts;
}
int64_t gps_get_monotonic_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000;
int64_t gps_get_pps_age_ms(void) {
if (s_last_pps_us == 0) return -1;
return (esp_timer_get_time() - s_last_pps_us) / 1000;
}
bool gps_is_synced(void) {
return gps_has_fix;
}
// ---------------- LOGGING SYSTEM INTERCEPTION ----------------
uint32_t gps_log_timestamp(void) {
return (uint32_t)(esp_timer_get_time() / 1000ULL);
}
int gps_log_vprintf(const char *fmt, va_list args) {
static char buffer[512];
int ret = vsnprintf(buffer, sizeof(buffer), fmt, args);
assert(ret >= 0);
if (use_gps_for_logs) {
char *timestamp_start = NULL;
for (int i = 0; buffer[i] != '\0' && i < sizeof(buffer) - 20; i++) {
if ((buffer[i] == 'I' || buffer[i] == 'W' || buffer[i] == 'E' ||
buffer[i] == 'D' || buffer[i] == 'V') &&
buffer[i+1] == ' ' && buffer[i+2] == '(') {
timestamp_start = &buffer[i+3];
break;
}
}
if (timestamp_start) {
char *timestamp_end = strchr(timestamp_start, ')');
if (timestamp_end) {
uint32_t monotonic_log_ms = 0;
if (sscanf(timestamp_start, "%lu", &monotonic_log_ms) == 1) {
char reformatted[512];
size_t prefix_len = timestamp_start - buffer;
memcpy(reformatted, buffer, prefix_len);
int decimal_len = 0;
if (gps_has_fix) {
int64_t log_mono_us = (int64_t)monotonic_log_ms * 1000;
int64_t log_gps_us = log_mono_us + monotonic_offset_us;
uint64_t gps_sec = log_gps_us / 1000000;
uint32_t gps_ms = (log_gps_us % 1000000) / 1000;
decimal_len = snprintf(reformatted + prefix_len,
sizeof(reformatted) - prefix_len,
"+%" PRIu64 ".%03lu", gps_sec, gps_ms);
} else {
uint32_t sec = monotonic_log_ms / 1000;
uint32_t ms = monotonic_log_ms % 1000;
decimal_len = snprintf(reformatted + prefix_len,
sizeof(reformatted) - prefix_len,
"*%lu.%03lu", sec, ms);
}
strcpy(reformatted + prefix_len + decimal_len, timestamp_end);
return printf("%s", reformatted);
}
}
}
void gps_get_last_nmea(char *buf, size_t buf_len) {
if (buf && buf_len > 0) {
strlcpy(buf, s_last_nmea_msg, buf_len);
}
return printf("%s", buffer);
}

View File

@ -1,10 +1,49 @@
#pragma once
#include "driver/gpio.h"
#include "driver/uart.h"
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
/*
* gps_sync.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#ifdef __cplusplus
extern "C" {
#endif
// --- Configuration Struct ---
typedef struct {
uart_port_t uart_port;
gpio_num_t tx_pin;
@ -12,19 +51,26 @@ typedef struct {
gpio_num_t pps_pin;
} gps_sync_config_t;
// --- Timestamp Struct ---
typedef struct {
int64_t monotonic_us;
int64_t monotonic_ms;
int64_t gps_us;
int64_t gps_ms;
struct timespec mono_ts;
bool synced;
int64_t gps_us; // Current GPS time in microseconds
bool synced; // PPS signal is active and stable (Precision Lock)
bool valid; // NMEA data indicates valid fix ('A' status) (Data Lock)
} gps_timestamp_t;
void gps_sync_init(const gps_sync_config_t *config, bool use_gps_log_timestamps);
void gps_force_next_update(void);
// --- Initialization ---
// Initializes the GPS task with specific hardware pins
void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable);
// --- Getters ---
gps_timestamp_t gps_get_timestamp(void);
int64_t gps_get_monotonic_ms(void);
bool gps_is_synced(void);
uint32_t gps_log_timestamp(void);
int gps_log_vprintf(const char *fmt, va_list args);
// Returns milliseconds since the last PPS edge (Diagnostic)
int64_t gps_get_pps_age_ms(void);
// Copies the last received NMEA line into buffer (Diagnostic)
void gps_get_last_nmea(char *buf, size_t buf_len);
#ifdef __cplusplus
}
#endif

View File

@ -4,5 +4,5 @@ idf_component_register(
# Only if iperf.h needs types from these (unlikely based on your code):
REQUIRES lwip led_strip
# Internal implementation details only:
PRIV_REQUIRES esp_event esp_timer nvs_flash esp_netif esp_wifi status_led
PRIV_REQUIRES esp_event esp_timer nvs_flash esp_netif esp_wifi status_led gps_sync
)

View File

@ -1,3 +1,48 @@
/*
* iperf.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @file iperf.c
* @brief ESP32 iPerf Traffic Generator (UDP Client Only) with Trip-Time Support
*
* This module implements a lightweight UDP traffic generator compatible with iPerf2.
* It features:
* - Precise packet pacing (PPS) using monotonic timers (drift-free).
* - Finite State Machine (FSM) to detect stalls and slow links.
* - Non-Volatile Storage (NVS) for persistent configuration.
* - Detailed error tracking (ENOMEM vs Route errors).
* - GPS Timestamp integration for status reporting.
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
@ -7,6 +52,7 @@
#include <arpa/inet.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
@ -20,9 +66,24 @@
#include "esp_wifi.h"
#include "iperf.h"
#include "status_led.h"
#include "gps_sync.h"
static const char *TAG = "iperf";
// --- NVS Keys ---
#define NVS_KEY_IPERF_ENABLE "iperf_enabled"
#define NVS_KEY_IPERF_PPS "iperf_pps"
#define NVS_KEY_IPERF_ROLE "iperf_role"
#define NVS_KEY_IPERF_DST_IP "iperf_dst_ip"
#define NVS_KEY_IPERF_PORT "iperf_port"
#define NVS_KEY_IPERF_PROTO "iperf_proto"
#define NVS_KEY_IPERF_BURST "iperf_burst"
#define NVS_KEY_IPERF_LEN "iperf_len"
// --- Global Config State ---
static iperf_cfg_t s_staging_cfg = {0};
static bool s_staging_initialized = false;
static EventGroupHandle_t s_iperf_event_group = NULL;
#define IPERF_IP_READY_BIT (1 << 0)
#define IPERF_STOP_REQ_BIT (1 << 1)
@ -30,6 +91,7 @@ static EventGroupHandle_t s_iperf_event_group = NULL;
#define RATE_CHECK_INTERVAL_US 500000
#define MIN_PACING_INTERVAL_US 100
// --- Runtime Control ---
typedef struct {
iperf_cfg_t cfg;
bool finish;
@ -39,18 +101,14 @@ typedef struct {
static iperf_ctrl_t s_iperf_ctrl = {0};
static TaskHandle_t s_iperf_task_handle = NULL;
static iperf_cfg_t s_next_cfg; // Holding area for the new config
static bool s_reload_req = false; // Flag to trigger internal restart
static bool s_reload_req = false;
// Global Stats Tracker
static iperf_stats_t s_stats = {0};
// --- Session Persistence Variables ---
static int64_t s_session_start_time = 0;
static int64_t s_session_end_time = 0;
static uint64_t s_session_packets = 0;
// --- State Duration & Edge Counters ---
// --- FSM State & Stats ---
typedef enum {
IPERF_STATE_IDLE = 0,
IPERF_STATE_TX,
@ -58,6 +116,8 @@ typedef enum {
IPERF_STATE_TX_STALLED
} iperf_fsm_state_t;
static iperf_fsm_state_t s_current_fsm_state = IPERF_STATE_IDLE;
static int64_t s_time_tx_us = 0;
static int64_t s_time_slow_us = 0;
static int64_t s_time_stalled_us = 0;
@ -66,35 +126,148 @@ static uint32_t s_edge_tx = 0;
static uint32_t s_edge_slow = 0;
static uint32_t s_edge_stalled = 0;
static iperf_fsm_state_t s_current_fsm_state = IPERF_STATE_IDLE;
static esp_event_handler_instance_t instance_any_id;
static esp_event_handler_instance_t instance_got_ip;
// --- Helper: Pattern Initialization ---
// Fills buffer with 0-9 cyclic ASCII pattern (matches iperf2 "pattern" function)
static void iperf_pattern(uint8_t *buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
buf[i] = (i % 10) + '0';
// --- Packet Structures & Constants (Compatible with payloads.h) ---
#define HEADER_EXTEND 0x80000000
#define HEADER_SEQNO64B 0x08000000
#define HEADER_TRIPTIME 0x00020000 // Use the small trip time flag
// UDP Datagram Header
struct udp_datagram {
int32_t id;
uint32_t tv_sec;
uint32_t tv_usec;
int32_t id2;
uint16_t flags;
uint32_t start_tv_sec;
uint32_t start_tv_usec;
};
// --- Helper: Defaults ---
static void set_defaults(iperf_cfg_t *cfg) {
memset(cfg, 0, sizeof(iperf_cfg_t));
cfg->flag = IPERF_FLAG_CLIENT | IPERF_FLAG_UDP;
cfg->dip = 0;
cfg->dport = IPERF_DEFAULT_PORT;
cfg->target_pps = 100;
cfg->burst_count = 1;
cfg->send_len = IPERF_UDP_TX_LEN;
}
// --- Parameter Management ---
static void trim_whitespace(char *str) {
char *end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
*(end+1) = 0;
}
void iperf_param_clear(void) {
nvs_handle_t h;
if (nvs_open("storage", NVS_READWRITE, &h) == ESP_OK) {
nvs_erase_key(h, NVS_KEY_IPERF_PPS);
nvs_erase_key(h, NVS_KEY_IPERF_BURST);
nvs_erase_key(h, NVS_KEY_IPERF_LEN);
nvs_erase_key(h, NVS_KEY_IPERF_PORT);
nvs_erase_key(h, NVS_KEY_IPERF_DST_IP);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "iPerf NVS configuration cleared.");
}
set_defaults(&s_staging_cfg);
}
void iperf_param_init(void) {
if (s_staging_initialized) return;
set_defaults(&s_staging_cfg);
nvs_handle_t h;
if (nvs_open("storage", NVS_READONLY, &h) == ESP_OK) {
ESP_LOGI(TAG, "Loading saved config from NVS...");
uint32_t val;
if (nvs_get_u32(h, NVS_KEY_IPERF_PPS, &val) == ESP_OK && val > 0) s_staging_cfg.target_pps = val;
if (nvs_get_u32(h, NVS_KEY_IPERF_BURST, &val) == ESP_OK) s_staging_cfg.burst_count = val;
if (nvs_get_u32(h, NVS_KEY_IPERF_LEN, &val) == ESP_OK) s_staging_cfg.send_len = val;
if (nvs_get_u32(h, NVS_KEY_IPERF_PORT, &val) == ESP_OK) s_staging_cfg.dport = (uint16_t)val;
size_t req;
if (nvs_get_str(h, NVS_KEY_IPERF_DST_IP, NULL, &req) == ESP_OK) {
char *ip_str = malloc(req);
if (ip_str) {
nvs_get_str(h, NVS_KEY_IPERF_DST_IP, ip_str, &req);
trim_whitespace(ip_str);
s_staging_cfg.dip = inet_addr(ip_str);
free(ip_str);
}
}
nvs_close(h);
}
s_staging_initialized = true;
}
void iperf_param_get(iperf_cfg_t *out_cfg) {
if (!s_staging_initialized) iperf_param_init();
*out_cfg = s_staging_cfg;
}
void iperf_param_set(const iperf_cfg_t *new_cfg) {
if (!s_staging_initialized) iperf_param_init();
s_staging_cfg = *new_cfg;
if (s_iperf_task_handle) {
ESP_LOGI(TAG, "Hot reloading parameters...");
s_iperf_ctrl.cfg = s_staging_cfg;
s_reload_req = true;
if (s_iperf_event_group) xEventGroupSetBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
}
}
// --- Helper: Generate Client Header ---
// Modified to set all zeros except HEADER_SEQNO64B
static void iperf_generate_client_hdr(iperf_cfg_t *cfg, client_hdr_v1 *hdr) {
// Zero out the entire structure
memset(hdr, 0, sizeof(client_hdr_v1));
bool iperf_param_is_unsaved(void) {
if (!s_staging_initialized) return false;
nvs_handle_t h;
if (nvs_open("storage", NVS_READONLY, &h) != ESP_OK) return false;
// Set only the SEQNO64B flag (Server will detect 64-bit seqno in UDP header)
hdr->flags = htonl(HEADER_SEQNO64B);
uint32_t val;
bool match = true;
if (nvs_get_u32(h, NVS_KEY_IPERF_PPS, &val) == ESP_OK) { if (s_staging_cfg.target_pps != val) match = false; }
else if (s_staging_cfg.target_pps != 100) match = false;
// ...
nvs_close(h);
return !match;
}
// ... [Existing Status Reporting & Event Handler Code] ...
esp_err_t iperf_param_save(bool *out_changed) {
if (out_changed) *out_changed = false;
nvs_handle_t h;
if (nvs_open("storage", NVS_READWRITE, &h) != ESP_OK) return ESP_FAIL;
nvs_set_u32(h, NVS_KEY_IPERF_PPS, s_staging_cfg.target_pps);
nvs_set_u32(h, NVS_KEY_IPERF_BURST, s_staging_cfg.burst_count);
nvs_set_u32(h, NVS_KEY_IPERF_LEN, s_staging_cfg.send_len);
nvs_set_u32(h, NVS_KEY_IPERF_PORT, s_staging_cfg.dport);
char ip_str[32];
struct in_addr daddr; daddr.s_addr = s_staging_cfg.dip;
inet_ntop(AF_INET, &daddr, ip_str, sizeof(ip_str));
nvs_set_str(h, NVS_KEY_IPERF_DST_IP, ip_str);
esp_err_t err = nvs_commit(h);
if (err == ESP_OK && out_changed) *out_changed = true;
nvs_close(h);
return err;
}
// --- Status ---
void iperf_get_stats(iperf_stats_t *stats) {
if (stats) {
s_stats.config_pps = (s_iperf_ctrl.cfg.pacing_period_us > 0) ?
(1000000 / s_iperf_ctrl.cfg.pacing_period_us) : 0;
s_stats.config_pps = s_iperf_ctrl.cfg.target_pps;
*stats = s_stats;
}
}
@ -102,42 +275,25 @@ void iperf_get_stats(iperf_stats_t *stats) {
void iperf_print_status(void) {
iperf_get_stats(&s_stats);
// 1. Get Source IP
char src_ip[32] = "0.0.0.0";
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
inet_ntop(AF_INET, &ip_info.ip, src_ip, sizeof(src_ip));
}
gps_timestamp_t ts = gps_get_timestamp();
// Check both Synced (PPS) and Valid (NMEA)
if (ts.synced && ts.valid && ts.gps_us > 0) {
time_t now_sec = ts.gps_us / 1000000;
struct tm tm_info;
gmtime_r(&now_sec, &tm_info);
char time_buf[64];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S UTC", &tm_info);
printf("TIME: %s (GPS Locked)\n", time_buf);
} else {
printf("TIME: <Not Synced - PPS:%d NMEA:%d>\n", ts.synced, ts.valid);
}
// 2. Get Destination IP
char dst_ip[32] = "0.0.0.0";
struct in_addr daddr;
daddr.s_addr = s_iperf_ctrl.cfg.dip;
if (s_stats.running) daddr.s_addr = s_iperf_ctrl.cfg.dip;
else daddr.s_addr = s_staging_cfg.dip;
inet_ntop(AF_INET, &daddr, dst_ip, sizeof(dst_ip));
float err = 0.0f;
if (s_stats.running && s_stats.config_pps > 0) {
int32_t diff = (int32_t)s_stats.config_pps - (int32_t)s_stats.actual_pps;
err = (float)diff * 100.0f / (float)s_stats.config_pps;
}
// 3. Compute Session Bandwidth
float avg_bw_mbps = 0.0f;
if (s_session_start_time > 0) {
int64_t end_t = (s_stats.running) ? esp_timer_get_time() : s_session_end_time;
if (end_t > s_session_start_time) {
double duration_sec = (double)(end_t - s_session_start_time) / 1000000.0;
if (duration_sec > 0.001) {
double total_bits = (double)s_session_packets * (double)s_iperf_ctrl.cfg.send_len * 8.0;
avg_bw_mbps = (float)(total_bits / duration_sec / 1000000.0);
}
}
}
// 4. Calculate State Percentages
double total_us = (double)(s_time_tx_us + s_time_slow_us + s_time_stalled_us);
if (total_us < 1.0) total_us = 1.0;
@ -145,18 +301,59 @@ void iperf_print_status(void) {
double pct_slow = ((double)s_time_slow_us / total_us) * 100.0;
double pct_stalled = ((double)s_time_stalled_us / total_us) * 100.0;
// Standard Stats
printf("IPERF_STATUS: Src=%s, Dst=%s, Running=%d, Config=%" PRIu32 ", Actual=%" PRIu32 ", Err=%.1f%%, Pkts=%" PRIu64 ", AvgBW=%.2f Mbps\n",
src_ip, dst_ip, s_stats.running, s_stats.config_pps, s_stats.actual_pps, err, s_session_packets, avg_bw_mbps);
float avg_bw_mbps = 0.0f;
if (s_session_start_time > 0) {
int64_t end_t = (s_stats.running) ? esp_timer_get_time() : s_session_end_time;
if (end_t > s_session_start_time) {
double duration_sec = (double)(end_t - s_session_start_time) / 1000000.0;
if (duration_sec > 0.001) {
double total_bits = (double)s_stats.total_packets * (double)s_iperf_ctrl.cfg.send_len * 8.0;
avg_bw_mbps = (float)(total_bits / duration_sec / 1000000.0);
}
}
}
// New Format: Time + Percentage + Edges
printf("IPERF_STATES: TX=%.2fs/%.1f%% (%lu), SLOW=%.2fs/%.1f%% (%lu), STALLED=%.2fs/%.1f%% (%lu)\n",
printf("IPERF: Dest=%s:%u, Pkts=%llu, BW=%.2f Mbps, Running=%d\n",
dst_ip,
s_stats.running ? s_iperf_ctrl.cfg.dport : s_staging_cfg.dport,
s_stats.total_packets,
avg_bw_mbps,
s_stats.running);
printf("STATES: TX=%.2fs/%.1f%% (%lu), SLOW=%.2fs/%.1f%% (%lu), STALLED=%.2fs/%.1f%% (%lu)\n",
(double)s_time_tx_us/1000000.0, pct_tx, (unsigned long)s_edge_tx,
(double)s_time_slow_us/1000000.0, pct_slow, (unsigned long)s_edge_slow,
(double)s_time_stalled_us/1000000.0, pct_stalled, (unsigned long)s_edge_stalled);
printf("ERRORS: ENOMEM=%lu, EHOST=%lu, OTHER=%lu\n",
(unsigned long)s_stats.err_mem,
(unsigned long)s_stats.err_route,
(unsigned long)s_stats.err_other);
}
// --- Core Logic ---
static void iperf_pattern(uint8_t *buf, uint32_t len) {
for (uint32_t i = 0; i < len; i++) buf[i] = (i % 10) + '0';
}
static void iperf_generate_headers(iperf_cfg_t *cfg, uint8_t *buffer, bool gps_synced, struct timespec *start_time) {
struct udp_datagram *udp_hdr = (struct udp_datagram *)buffer;
// Clear Header Area
memset(udp_hdr, 0, sizeof(struct udp_datagram));
if (gps_synced) {
udp_hdr->flags = htonl(HEADER_EXTEND | HEADER_SEQNO64B | HEADER_TRIPTIME);
udp_hdr->start_tv_sec = htonl(start_time->tv_sec);
udp_hdr->start_tv_usec = htonl(start_time->tv_nsec / 1000);
#if 0
ESP_LOGI(TAG, "TX Start Timestamp: %" PRIu32 ".%06" PRIu32,
(uint32_t)start_time.tv_sec, (uint32_t)(start_time.tv_nsec / 1000));
#endif
} else {
udp_hdr->flags = htonl(HEADER_EXTEND | HEADER_SEQNO64B);
}
}
// --- Network Events ---
static void iperf_network_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (s_iperf_event_group == NULL) return;
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
@ -174,148 +371,91 @@ static bool iperf_wait_for_ip(void) {
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif) {
esp_netif_ip_info_t ip_info;
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
xEventGroupSetBits(s_iperf_event_group, IPERF_IP_READY_BIT);
}
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) return true;
}
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &iperf_network_event_handler, NULL, &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &iperf_network_event_handler, NULL, &instance_got_ip));
status_led_set_state(LED_STATE_WAITING);
ESP_LOGI(TAG, "Waiting for IP...");
EventBits_t bits = xEventGroupWaitBits(s_iperf_event_group, IPERF_IP_READY_BIT | IPERF_STOP_REQ_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id);
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip);
if (bits & IPERF_STOP_REQ_BIT) {
ESP_LOGW(TAG, "Stop requested while waiting for IP");
return false;
}
ESP_LOGI(TAG, "IP Ready. Starting traffic.");
if (bits & IPERF_STOP_REQ_BIT) return false;
return true;
}
static void trim_whitespace(char *str) {
char *end = str + strlen(str) - 1;
while(end > str && isspace((unsigned char)*end)) end--;
*(end+1) = 0;
}
static void iperf_read_nvs_config(iperf_cfg_t *cfg) {
nvs_handle_t my_handle;
if (nvs_open("storage", NVS_READONLY, &my_handle) != ESP_OK) return;
uint32_t val;
if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PERIOD, &val) == ESP_OK) cfg->pacing_period_us = val;
if (nvs_get_u32(my_handle, NVS_KEY_IPERF_BURST, &val) == ESP_OK) cfg->burst_count = val;
if (nvs_get_u32(my_handle, NVS_KEY_IPERF_LEN, &val) == ESP_OK) cfg->send_len = val;
if (nvs_get_u32(my_handle, NVS_KEY_IPERF_PORT, &val) == ESP_OK) cfg->dport = (uint16_t)val;
size_t req;
char buf[16];
req = sizeof(buf);
if (nvs_get_str(my_handle, NVS_KEY_IPERF_ROLE, buf, &req) == ESP_OK) {
if (strcmp(buf, "SERVER") == 0) cfg->flag |= IPERF_FLAG_SERVER;
else cfg->flag |= IPERF_FLAG_CLIENT;
}
req = sizeof(buf);
if (nvs_get_str(my_handle, NVS_KEY_IPERF_PROTO, buf, &req) == ESP_OK) {
if (strcmp(buf, "TCP") == 0) cfg->flag |= IPERF_FLAG_TCP;
else cfg->flag |= IPERF_FLAG_UDP;
}
if (nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, NULL, &req) == ESP_OK) {
char *ip_str = malloc(req);
if (ip_str) {
nvs_get_str(my_handle, NVS_KEY_IPERF_DST_IP, ip_str, &req);
trim_whitespace(ip_str);
cfg->dip = inet_addr(ip_str);
free(ip_str);
}
}
nvs_close(my_handle);
}
void iperf_set_pps(uint32_t pps) {
if (pps == 0) pps = 1;
uint32_t period_us = 1000000 / pps;
if (period_us < MIN_PACING_INTERVAL_US) period_us = MIN_PACING_INTERVAL_US;
if (s_iperf_task_handle != NULL) {
s_iperf_ctrl.cfg.pacing_period_us = period_us;
printf("IPERF_PPS_UPDATED: %" PRIu32 "\n", pps);
} else {
s_iperf_ctrl.cfg.pacing_period_us = period_us;
}
}
uint32_t iperf_get_pps(void) {
if (s_iperf_ctrl.cfg.pacing_period_us == 0) return 0;
return 1000000 / s_iperf_ctrl.cfg.pacing_period_us;
}
static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
if (!iperf_wait_for_ip()) {
printf("IPERF_STOPPED\n");
return ESP_OK;
}
if (!iperf_wait_for_ip()) return ESP_OK;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(ctrl->cfg.dport > 0 ? ctrl->cfg.dport : 5001);
addr.sin_port = htons(ctrl->cfg.dport);
addr.sin_addr.s_addr = ctrl->cfg.dip;
char ip_str[32];
inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));
ESP_LOGI(TAG, "Client sending to %s:%d", ip_str, ntohs(addr.sin_port));
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0) {
ESP_LOGE(TAG, "Socket failed: %d", errno);
status_led_set_state(LED_STATE_FAILED);
ESP_LOGE(TAG, "Socket creation failed: %d", errno);
printf("IPERF_STOPPED\n");
return ESP_FAIL;
}
status_led_set_state(LED_STATE_TRANSMITTING_SLOW);
udp_datagram *udp_hdr = (udp_datagram *)ctrl->buffer;
client_hdr_v1 *client_hdr = (client_hdr_v1 *)(ctrl->buffer + sizeof(udp_datagram));
iperf_generate_client_hdr(&ctrl->cfg, client_hdr);
// --- CHECK GPS SYNC ---
gps_timestamp_t gps = gps_get_timestamp();
// FIX: Must have valid NMEA (absolute time) AND PPS (precision)
bool gps_synced = gps.synced && gps.valid;
struct timespec start_ts = {0};
if (gps_synced) {
ESP_LOGI(TAG, "GPS Locked (PPS + NMEA). Enabling Trip-Times.");
clock_gettime(CLOCK_REALTIME, &start_ts);
} else {
ESP_LOGW(TAG, "GPS NOT Fully Locked (PPS:%d, NMEA:%d). Trip-Times disabled.", gps.synced, gps.valid);
}
// --- GENERATE HEADERS ---
iperf_generate_headers(&ctrl->cfg, ctrl->buffer, gps_synced, &start_ts);
s_stats.running = true;
s_session_start_time = esp_timer_get_time();
s_session_end_time = 0;
s_session_packets = 0;
s_stats.err_mem = 0; s_stats.err_route = 0; s_stats.err_other = 0;
s_stats.total_packets = 0;
s_session_start_time = esp_timer_get_time();
// Reset FSM
s_time_tx_us = 0; s_time_slow_us = 0; s_time_stalled_us = 0;
s_edge_tx = 0; s_edge_slow = 0; s_edge_stalled = 0;
s_current_fsm_state = IPERF_STATE_IDLE;
printf("IPERF_STARTED\n");
ESP_LOGI(TAG, "UDP Started. Target: %s", inet_ntoa(addr.sin_addr));
int64_t next_send_time = esp_timer_get_time();
int64_t end_time = (ctrl->cfg.time == 0) ? INT64_MAX : esp_timer_get_time() + (int64_t)ctrl->cfg.time * 1000000LL;
int64_t last_rate_check = esp_timer_get_time();
uint32_t packets_since_check = 0;
int64_t packet_id = 0;
struct timespec ts;
while (!ctrl->finish && esp_timer_get_time() < end_time) {
uint32_t period_us = (ctrl->cfg.target_pps > 0) ? (1000000 / ctrl->cfg.target_pps) : 10000;
if (period_us < MIN_PACING_INTERVAL_US) period_us = MIN_PACING_INTERVAL_US;
while (!ctrl->finish && !s_reload_req) {
int64_t now = esp_timer_get_time();
int64_t wait = next_send_time - now;
if (wait > 2000) vTaskDelay(pdMS_TO_TICKS(wait / 1000));
else while (esp_timer_get_time() < next_send_time) taskYIELD();
while (esp_timer_get_time() < next_send_time) taskYIELD();
if (xEventGroupGetBits(s_iperf_event_group) & IPERF_STOP_REQ_BIT) break;
for (int k = 0; k < ctrl->cfg.burst_count; k++) {
int64_t current_id = packet_id++;
struct udp_datagram *udp_hdr = (struct udp_datagram *)ctrl->buffer;
udp_hdr->id = htonl((uint32_t)(current_id & 0xFFFFFFFF));
udp_hdr->id2 = htonl((uint32_t)((current_id >> 32) & 0xFFFFFFFF));
@ -323,22 +463,18 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
udp_hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
int sent = sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
int ret = sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
if (sent > 0) {
if (ret > 0) {
s_stats.total_packets++;
packets_since_check++;
s_session_packets++;
} else {
// --- ROBUST FIX: Never Abort ---
// If send fails (buffer full, routing issue, etc.), we just yield and retry next loop.
// We do NOT goto exit.
if (errno != 12) {
// Log rarely to avoid spamming serial
if ((packet_id % 100) == 0) {
ESP_LOGW(TAG, "Send error: %d (Ignored)", errno);
}
if (errno == ENOMEM) s_stats.err_mem++;
else {
if (errno == EHOSTUNREACH) s_stats.err_route++;
else s_stats.err_other++;
vTaskDelay(pdMS_TO_TICKS(10));
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
@ -347,118 +483,109 @@ static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
uint32_t interval_us = (uint32_t)(now - last_rate_check);
if (interval_us > 0) {
s_stats.actual_pps = (uint32_t)((uint64_t)packets_since_check * 1000000 / interval_us);
uint32_t config_pps = iperf_get_pps();
uint32_t threshold = (config_pps * 3) / 4;
uint32_t threshold = (ctrl->cfg.target_pps * 3) / 4;
iperf_fsm_state_t next_state;
if (s_stats.actual_pps == 0) next_state = IPERF_STATE_TX_STALLED;
else if (s_stats.actual_pps >= threshold) next_state = IPERF_STATE_TX;
else next_state = IPERF_STATE_TX_SLOW;
switch (next_state) {
case IPERF_STATE_TX: s_time_tx_us += interval_us; break;
case IPERF_STATE_TX_SLOW: s_time_slow_us += interval_us; break;
case IPERF_STATE_TX_STALLED: s_time_stalled_us += interval_us; break;
default: break;
case IPERF_STATE_TX: s_time_tx_us += interval_us; break;
case IPERF_STATE_TX_SLOW: s_time_slow_us += interval_us; break;
case IPERF_STATE_TX_STALLED: s_time_stalled_us += interval_us; break;
default: break;
}
if (next_state != s_current_fsm_state) {
switch (next_state) {
case IPERF_STATE_TX: s_edge_tx++; break;
case IPERF_STATE_TX_SLOW: s_edge_slow++; break;
case IPERF_STATE_TX_STALLED: s_edge_stalled++; break;
default: break;
case IPERF_STATE_TX: s_edge_tx++; break;
case IPERF_STATE_TX_SLOW: s_edge_slow++; break;
case IPERF_STATE_TX_STALLED: s_edge_stalled++; break;
default: break;
}
s_current_fsm_state = next_state;
}
led_state_t led_target = (s_current_fsm_state == IPERF_STATE_TX) ? LED_STATE_TRANSMITTING : LED_STATE_TRANSMITTING_SLOW;
if (status_led_get_state() != led_target) status_led_set_state(led_target);
led_state_t led_target = LED_STATE_TRANSMITTING;
if (next_state == IPERF_STATE_TX_SLOW) led_target = LED_STATE_TRANSMITTING_SLOW;
if (next_state == IPERF_STATE_TX_STALLED) led_target = LED_STATE_STALLED;
status_led_set_state(led_target);
}
last_rate_check = now;
packets_since_check = 0;
}
next_send_time += ctrl->cfg.pacing_period_us;
next_send_time += period_us;
}
udp_datagram *hdr = (udp_datagram *)ctrl->buffer;
int64_t final_id = -packet_id;
hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
struct udp_datagram *udp_hdr = (struct udp_datagram *)ctrl->buffer;
udp_hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
udp_hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
clock_gettime(CLOCK_REALTIME, &ts);
hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
hdr->tv_usec = htonl(ts.tv_nsec / 1000);
for(int i=0; i<10; i++) {
udp_hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
for (int i=0; i<10; i++) {
sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
vTaskDelay(pdMS_TO_TICKS(2));
}
ESP_LOGI(TAG, "Sent termination packets (ID: %" PRId64 ")", final_id);
ESP_LOGI(TAG, "Sent termination (ID: %" PRId64 ")", final_id);
close(sockfd);
s_stats.running = false;
s_session_end_time = esp_timer_get_time();
s_stats.actual_pps = 0;
status_led_set_state(LED_STATE_CONNECTED); // <--- This is your "Solid Green"
printf("IPERF_STOPPED\n");
status_led_set_state(LED_STATE_CONNECTED);
return ESP_OK;
}
static void iperf_task(void *arg) {
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
do {
while (1) {
s_reload_req = false;
ctrl->finish = false;
xEventGroupClearBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
if (ctrl->cfg.flag & IPERF_FLAG_UDP && ctrl->cfg.flag & IPERF_FLAG_CLIENT) {
iperf_start_udp_client(ctrl);
}
iperf_start_udp_client(ctrl);
if (s_reload_req) {
ESP_LOGI(TAG, "Hot reloading iperf task with new config...");
ctrl->cfg = s_next_cfg;
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Task reloading config...");
if (ctrl->buffer_len < ctrl->cfg.send_len + 128) {
free(ctrl->buffer);
ctrl->buffer_len = ctrl->cfg.send_len + 128;
ctrl->buffer = calloc(1, ctrl->buffer_len);
iperf_pattern(ctrl->buffer, ctrl->buffer_len);
}
} else {
break;
}
} while (s_reload_req);
}
free(ctrl->buffer);
s_iperf_task_handle = NULL;
vTaskDelete(NULL);
}
void iperf_start(iperf_cfg_t *cfg) {
iperf_cfg_t new_cfg = *cfg;
iperf_read_nvs_config(&new_cfg);
if (new_cfg.send_len == 0) new_cfg.send_len = 1470;
if (new_cfg.pacing_period_us == 0) new_cfg.pacing_period_us = 10000;
if (new_cfg.burst_count == 0) new_cfg.burst_count = 1;
void iperf_start(void) {
if (!s_staging_initialized) iperf_param_init();
if (s_iperf_task_handle) {
ESP_LOGI(TAG, "Task running. Staging hot reload.");
s_next_cfg = new_cfg;
s_reload_req = true;
iperf_stop();
printf("IPERF_RELOADING\n");
ESP_LOGW(TAG, "Already running. Use 'set' to update parameters.");
return;
}
s_iperf_ctrl.cfg = new_cfg;
s_iperf_ctrl.cfg = s_staging_cfg;
s_iperf_ctrl.finish = false;
if (s_iperf_ctrl.buffer == NULL) {
s_iperf_ctrl.buffer_len = s_iperf_ctrl.cfg.send_len + 128;
s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len);
}
// Initialize Buffer Pattern
s_iperf_ctrl.buffer_len = s_iperf_ctrl.cfg.send_len + 128;
s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len);
if (s_iperf_ctrl.buffer) {
iperf_pattern(s_iperf_ctrl.buffer, s_iperf_ctrl.buffer_len);
}
if (s_iperf_event_group == NULL) {
s_iperf_event_group = xEventGroupCreate();
}
if (s_iperf_event_group == NULL) s_iperf_event_group = xEventGroupCreate();
xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, 5, &s_iperf_task_handle);
}
@ -467,7 +594,5 @@ void iperf_stop(void) {
if (s_iperf_task_handle) {
s_iperf_ctrl.finish = true;
if (s_iperf_event_group) xEventGroupSetBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
} else {
printf("IPERF_STOPPED\n");
}
}

View File

@ -1,8 +1,41 @@
/*
* iperf.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef IPERF_H
#define IPERF_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "led_strip.h"
// --- Configuration Flags ---
@ -11,84 +44,52 @@
#define IPERF_FLAG_TCP (1 << 2)
#define IPERF_FLAG_UDP (1 << 3)
// --- Standard Iperf2 Header Flags (from payloads.h) ---
#define HEADER_VERSION1 0x80000000
#define HEADER_EXTEND 0x40000000
#define HEADER_UDPTESTS 0x20000000
#define HEADER_SEQNO64B 0x08000000
// --- Defaults ---
#define IPERF_DEFAULT_PORT 5001
#define IPERF_DEFAULT_INTERVAL 3
#define IPERF_DEFAULT_TIME 30
#define IPERF_TRAFFIC_TASK_PRIORITY 4
#define IPERF_REPORT_TASK_PRIORITY 5
#define IPERF_UDP_TX_LEN (1470)
// --- NVS Keys ---
#define NVS_KEY_IPERF_ENABLE "iperf_enabled"
#define NVS_KEY_IPERF_PERIOD "iperf_period"
#define NVS_KEY_IPERF_ROLE "iperf_role"
#define NVS_KEY_IPERF_DST_IP "iperf_dst_ip"
#define NVS_KEY_IPERF_PORT "iperf_port"
#define NVS_KEY_IPERF_PROTO "iperf_proto"
#define NVS_KEY_IPERF_BURST "iperf_burst"
#define NVS_KEY_IPERF_LEN "iperf_len"
#define IPERF_UDP_TX_LEN 1470
typedef struct {
uint32_t flag;
uint32_t dip;
uint16_t dport;
uint32_t time;
uint32_t pacing_period_us;
uint32_t burst_count;
uint32_t send_len;
uint32_t dip; // Destination IP
uint16_t dport; // Destination Port
uint32_t target_pps; // Packets Per Second (Replaces period)
uint32_t burst_count; // Packets per RTOS tick
uint32_t send_len; // Packet payload length
} iperf_cfg_t;
// --- Stats Structure ---
typedef struct {
bool running;
uint32_t config_pps;
uint32_t actual_pps;
float error_rate;
uint64_t total_packets;
uint32_t err_mem; // ENOMEM (12)
uint32_t err_route; // EHOSTUNREACH (118)
uint32_t err_other; // All other errors
} iperf_stats_t;
// --- Wire Formats (Strict Layout) ---
// 1. Basic UDP Datagram Header (16 bytes)
// Corresponds to 'struct UDP_datagram' in payloads.h
typedef struct {
int32_t id; // Lower 32 bits of seqno
uint32_t tv_sec; // Seconds
uint32_t tv_usec; // Microseconds
int32_t id2; // Upper 32 bits of seqno (when HEADER_SEQNO64B is set)
} udp_datagram;
// 2. Client Header V1 (Used for First Packet Exchange)
// Corresponds to 'struct client_hdr_v1' in payloads.h
typedef struct {
int32_t flags;
int32_t numThreads;
int32_t mPort;
int32_t mBufLen;
int32_t mWinBand;
int32_t mAmount;
} client_hdr_v1;
// --- API ---
void iperf_init_led(led_strip_handle_t handle);
void iperf_set_pps(uint32_t pps);
uint32_t iperf_get_pps(void);
// Initialization (Call this in app_main to load NVS)
void iperf_param_init(void);
// Get snapshot of current stats
void iperf_get_stats(iperf_stats_t *stats);
// Parameter Management (Running Config)
void iperf_param_get(iperf_cfg_t *out_cfg);
void iperf_param_set(const iperf_cfg_t *new_cfg);
// Print formatted status to stdout (for CLI/Python)
// Save returns true if NVS was actually updated
esp_err_t iperf_param_save(bool *out_changed);
// Check if dirty
bool iperf_param_is_unsaved(void);
// Control
void iperf_start(void);
void iperf_stop(void);
void iperf_print_status(void);
void iperf_start(iperf_cfg_t *cfg);
void iperf_stop(void);
// Utils
void iperf_init_led(led_strip_handle_t handle);
// Erase NVS and reset RAM defaults
void iperf_param_clear(void);
#endif

View File

@ -1,3 +1,35 @@
/*
* status_led.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "status_led.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@ -5,6 +37,8 @@
#include "led_strip.h"
#include "esp_log.h"
static const char *TAG = "STATUS_LED"; // Added TAG for logging
static led_strip_handle_t s_led_strip = NULL;
static bool s_is_rgb = false;
static int s_gpio_pin = -1;
@ -27,28 +61,29 @@ static void led_task(void *arg) {
if (s_is_rgb) { set_color(25, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000)); }
else { set_color(1,1,1); vTaskDelay(100); set_color(0,0,0); vTaskDelay(100); }
break;
case LED_STATE_WAITING: // Blue Blink
// ... rest of cases identical to your code ...
case LED_STATE_WAITING:
set_color(0, 0, toggle ? 50 : 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(500));
break;
case LED_STATE_CONNECTED: // Green Solid
case LED_STATE_CONNECTED:
set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
break;
case LED_STATE_MONITORING: // Blue Solid
case LED_STATE_MONITORING:
set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
break;
case LED_STATE_TRANSMITTING: // Fast Purple (Busy)
case LED_STATE_TRANSMITTING:
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(50));
break;
case LED_STATE_TRANSMITTING_SLOW: // Slow Purple (Relaxed)
case LED_STATE_TRANSMITTING_SLOW:
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(250));
break;
case LED_STATE_STALLED: // Purple Solid
case LED_STATE_STALLED:
set_color(50, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
break;
case LED_STATE_FAILED: // Red Blink
case LED_STATE_FAILED:
set_color(toggle ? 50 : 0, 0, 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(200));
break;
@ -59,11 +94,21 @@ static void led_task(void *arg) {
void status_led_init(int gpio_pin, bool is_rgb_strip) {
s_gpio_pin = gpio_pin;
s_is_rgb = is_rgb_strip;
// --- DIAGNOSTIC LOG ---
ESP_LOGW(TAG, "Initializing LED on GPIO %d (RGB: %d)", gpio_pin, is_rgb_strip);
if (s_is_rgb) {
led_strip_config_t s_cfg = { .strip_gpio_num = gpio_pin, .max_leds = 1 };
led_strip_rmt_config_t r_cfg = { .resolution_hz = 10 * 1000 * 1000 };
led_strip_new_rmt_device(&s_cfg, &r_cfg, &s_led_strip);
led_strip_clear(s_led_strip);
esp_err_t ret = led_strip_new_rmt_device(&s_cfg, &r_cfg, &s_led_strip);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "RMT Device Init Failed: %s", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "RMT Device Init Success");
led_strip_clear(s_led_strip);
}
} else {
gpio_reset_pin(gpio_pin);
gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT);
@ -71,10 +116,6 @@ void status_led_init(int gpio_pin, bool is_rgb_strip) {
xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL);
}
void status_led_set_state(led_state_t state) {
s_current_state = state;
}
led_state_t status_led_get_state(void) {
return s_current_state;
}
// ... Setters/Getters ...
void status_led_set_state(led_state_t state) { s_current_state = state; }
led_state_t status_led_get_state(void) { return s_current_state; }

View File

@ -1,3 +1,36 @@
/*
* status_led.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <stdbool.h>

View File

@ -1,156 +1,211 @@
#include <stdio.h>
/*
* wifi_cfg.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "wifi_cfg.h"
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "wifi_cfg.h"
// Removed unused TAG
static const char *TAG = "WIFI_CFG";
static const char *NVS_NS = "storage"; // Shared namespace for all settings
static esp_netif_t *sta_netif = NULL;
// --- Defaults ---
#define DEFAULT_MONITOR_CHANNEL 6
// --- Helper: NVS Write ---
static void nvs_write_str(const char *key, const char *val) {
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
if (val) nvs_set_str(h, key, val);
else nvs_erase_key(h, key);
nvs_commit(h);
nvs_close(h);
}
}
static void nvs_write_u8(const char *key, uint8_t val) {
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, key, val);
nvs_commit(h);
nvs_close(h);
}
}
// --- Public Setters ---
void wifi_cfg_set_credentials(const char* ssid, const char* pass) {
nvs_write_str("ssid", ssid);
nvs_write_str("pass", pass);
}
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw) {
nvs_write_str("ip", ip);
nvs_write_str("mask", mask);
nvs_write_str("gw", gw);
}
void wifi_cfg_set_dhcp(bool enable) {
nvs_write_u8("dhcp", enable ? 1 : 0);
}
// --- Init & Load ---
// --- Initialization ---
void wifi_cfg_init(void) {
nvs_flash_init();
// NVS is usually initialized in app_main, but we can double check here
// or just leave it empty if no specific module init is needed.
}
static bool load_cfg(char* ssid, size_t ssz, char* pass, size_t psz,
char* ip, size_t isz, char* mask, size_t msz, char* gw, size_t gsz,
char* band, size_t bsz, char* bw, size_t bwsz, char* powersave, size_t pssz,
char* mode, size_t modesz, uint8_t* mon_ch, bool* dhcp){
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
size_t len;
// Load SSID (Mandatory)
len = ssz;
if (nvs_get_str(h, "ssid", ssid, &len) != ESP_OK) { nvs_close(h); return false; }
// Load Optionals
len = psz; if (nvs_get_str(h, "pass", pass, &len) != ESP_OK) pass[0]=0;
len = isz; if (nvs_get_str(h, "ip", ip, &len) != ESP_OK) ip[0]=0;
len = msz; if (nvs_get_str(h, "mask", mask, &len) != ESP_OK) mask[0]=0;
len = gsz; if (nvs_get_str(h, "gw", gw, &len) != ESP_OK) gw[0]=0;
// Defaults
len = bsz; if (nvs_get_str(h, "band", band, &len) != ESP_OK) strcpy(band, "2.4G");
len = bwsz; if (nvs_get_str(h, "bw", bw, &len) != ESP_OK) strcpy(bw, "HT20");
len = pssz; if (nvs_get_str(h, "powersave", powersave, &len) != ESP_OK) strcpy(powersave, "NONE");
len = modesz; if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
uint8_t ch=36; nvs_get_u8(h, "mon_ch", &ch); *mon_ch = ch;
uint8_t d=1; nvs_get_u8(h, "dhcp", &d); *dhcp = (d!=0);
nvs_close(h);
return true;
}
static void apply_ip_static(const char* ip, const char* mask, const char* gw){
if (!sta_netif) return;
if (!ip || !ip[0]) return;
esp_netif_ip_info_t info = {0};
esp_netif_dhcpc_stop(sta_netif);
info.ip.addr = esp_ip4addr_aton(ip);
info.netmask.addr = (mask && mask[0]) ? esp_ip4addr_aton(mask) : esp_ip4addr_aton("255.255.255.0");
info.gw.addr = (gw && gw[0]) ? esp_ip4addr_aton(gw) : 0;
esp_netif_set_ip_info(sta_netif, &info);
}
// --- Apply Configuration ---
bool wifi_cfg_apply_from_nvs(void) {
char ssid[64]={0}, pass[64]={0}, ip[32]={0}, mask[32]={0}, gw[32]={0};
char band[16]={0}, bw[16]={0}, powersave[16]={0}, mode[16]={0};
uint8_t mon_ch = 36; bool dhcp = true;
if (!load_cfg(ssid,sizeof(ssid), pass,sizeof(pass), ip,sizeof(ip), mask,sizeof(mask), gw,sizeof(gw),
band,sizeof(band), bw,sizeof(bw), powersave,sizeof(powersave), mode,sizeof(mode), &mon_ch, &dhcp)){
return false;
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
return false; // No config found
}
if (sta_netif == NULL) sta_netif = esp_netif_create_default_wifi_sta();
// 1. Load SSID/Pass
size_t len = 0;
if (nvs_get_str(h, "wifi_ssid", NULL, &len) == ESP_OK && len > 0) {
char *ssid = malloc(len);
char *pass = NULL;
nvs_get_str(h, "wifi_ssid", ssid, &len);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
if (nvs_get_str(h, "wifi_pass", NULL, &len) == ESP_OK && len > 0) {
pass = malloc(len);
nvs_get_str(h, "wifi_pass", pass, &len);
}
wifi_config_t wcfg = {0};
strlcpy((char*)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
strlcpy((char*)wcfg.sta.password, pass, sizeof(wcfg.sta.password));
wcfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
wcfg.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
wcfg.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
wifi_config_t wifi_config = {0};
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
wifi_config.sta.ssid[sizeof(wifi_config.sta.ssid) - 1] = '\0';
if (pass) {
strncpy((char *)wifi_config.sta.password, pass, sizeof(wifi_config.sta.password) - 1);
wifi_config.sta.password[sizeof(wifi_config.sta.password) - 1] = '\0';
}
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &wcfg);
ESP_LOGI(TAG, "Applying WiFi Config: SSID=%s", ssid);
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
if (!dhcp && ip[0]) apply_ip_static(ip, mask, gw);
else esp_netif_dhcpc_start(sta_netif);
free(ssid);
if (pass) free(pass);
}
esp_wifi_start();
esp_wifi_connect();
return true;
}
wifi_ps_type_t wifi_cfg_get_power_save_mode(void) {
return WIFI_PS_NONE;
}
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size) {
if (buf) strncpy(buf, "HT20", buf_size);
return true;
}
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch) {
nvs_handle_t h;
if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false;
size_t len = 16;
if (nvs_get_str(h, "mode", mode, &len) != ESP_OK) strcpy(mode, "STA");
nvs_get_u8(h, "mon_ch", mon_ch);
nvs_close(h);
return true;
}
// --- Getters ---
bool wifi_cfg_get_mode(char *mode_out, uint8_t *channel_out) {
// This function seems to be used to retrieve saved "mode" strings
// For now, we default to whatever is implicit, or implement saving "wifi_mode" later.
// Returning false implies default behavior.
return false;
}
// --- Setters (Credentials) ---
bool wifi_cfg_set_ssid(const char *ssid) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
esp_err_t err = nvs_set_str(h, "wifi_ssid", ssid);
if (err == ESP_OK) nvs_commit(h);
nvs_close(h);
return (err == ESP_OK);
}
bool wifi_cfg_set_password(const char *password) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
esp_err_t err;
if (password && strlen(password) > 0) {
err = nvs_set_str(h, "wifi_pass", password);
} else {
err = nvs_erase_key(h, "wifi_pass"); // Clear if empty
}
if (err == ESP_OK) nvs_commit(h);
nvs_close(h);
return (err == ESP_OK);
}
// --- Monitor Channel Settings ---
bool wifi_cfg_set_monitor_channel(uint8_t channel) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
esp_err_t err = nvs_set_u8(h, "mon_chan", channel);
if (err == ESP_OK) nvs_commit(h);
nvs_close(h);
return (err == ESP_OK);
}
void wifi_cfg_clear_monitor_channel(void) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) == ESP_OK) {
nvs_erase_key(h, "mon_chan");
nvs_commit(h);
nvs_close(h);
}
}
bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val) {
nvs_handle_t h;
uint8_t saved_val = DEFAULT_MONITOR_CHANNEL;
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
nvs_get_u8(h, "mon_chan", &saved_val);
nvs_close(h);
}
return (saved_val != current_val);
}
// ... existing code ...
// --- IP Configuration ---
bool wifi_cfg_set_ipv4(const char *ip, const char *mask, const char *gw) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
nvs_set_str(h, "static_ip", ip);
nvs_set_str(h, "static_mask", mask);
nvs_set_str(h, "static_gw", gw);
nvs_commit(h);
nvs_close(h);
return true;
}
bool wifi_cfg_get_ipv4(char *ip, char *mask, char *gw) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) return false;
size_t len = 16;
bool exists = (nvs_get_str(h, "static_ip", ip, &len) == ESP_OK);
len = 16; nvs_get_str(h, "static_mask", mask, &len);
len = 16; nvs_get_str(h, "static_gw", gw, &len);
nvs_close(h);
return exists;
}
bool wifi_cfg_set_dhcp(bool enable) {
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) return false;
nvs_set_u8(h, "dhcp_en", enable ? 1 : 0);
nvs_commit(h);
nvs_close(h);
return true;
}
bool wifi_cfg_get_dhcp(void) {
nvs_handle_t h;
uint8_t val = 1; // Default to Enabled
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
nvs_get_u8(h, "dhcp_en", &val);
nvs_close(h);
}
return (val != 0);
}

View File

@ -1,29 +1,68 @@
#ifndef WIFI_CFG_H
#define WIFI_CFG_H
/*
* wifi_cfg.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
// IP Configuration
#pragma once
#include "esp_wifi.h"
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// --- Initialization ---
// Init
void wifi_cfg_init(void);
// --- Getters (Used by Controller) ---
// Apply
bool wifi_cfg_apply_from_nvs(void);
wifi_ps_type_t wifi_cfg_get_power_save_mode(void);
bool wifi_cfg_get_bandwidth(char *buf, size_t buf_size);
bool wifi_cfg_get_mode(char *mode, uint8_t *mon_ch);
// --- Setters (Used by Console) ---
void wifi_cfg_set_credentials(const char* ssid, const char* pass);
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw);
void wifi_cfg_set_dhcp(bool enable);
// Getters
bool wifi_cfg_get_mode(char *mode_out, uint8_t *channel_out);
// Setters (These were missing)
bool wifi_cfg_set_ssid(const char *ssid);
bool wifi_cfg_set_password(const char *password);
// Monitor Specific
bool wifi_cfg_set_monitor_channel(uint8_t channel);
void wifi_cfg_clear_monitor_channel(void);
bool wifi_cfg_monitor_channel_is_unsaved(uint8_t current_val);
bool wifi_cfg_set_ipv4(const char *ip, const char *mask, const char *gw);
bool wifi_cfg_get_ipv4(char *ip_out, char *mask_out, char *gw_out); // buffers must be 16 bytes
bool wifi_cfg_set_dhcp(bool enable);
bool wifi_cfg_get_dhcp(void);
#ifdef __cplusplus
}
#endif
#endif // WIFI_CFG_H

View File

@ -1,4 +1,4 @@
idf_component_register(SRCS "wifi_controller.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi freertos
PRIV_REQUIRES csi_manager iperf status_led wifi_monitor gps_sync log esp_netif)
PRIV_REQUIRES csi_manager iperf status_led wifi_monitor wifi_cfg gps_sync log esp_netif)

View File

@ -1,9 +1,44 @@
/*
* wifi_controller.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "wifi_controller.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "inttypes.h"
#include "wifi_cfg.h"
// Dependencies
#include "iperf.h"
@ -11,7 +46,6 @@
#include "wifi_monitor.h"
#include "gps_sync.h"
// 1. GUARDED INCLUDE
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_manager.h"
#endif
@ -19,37 +53,38 @@
static const char *TAG = "WIFI_CTL";
static wifi_ctl_mode_t s_current_mode = WIFI_CTL_MODE_STA;
static uint8_t s_monitor_channel = 6;
static uint8_t s_monitor_channel_active = 6;
static uint8_t s_monitor_channel_staging = 6;
static bool s_monitor_enabled = false;
static uint32_t s_monitor_frame_count = 0;
static TaskHandle_t s_monitor_stats_task_handle = NULL;
// --- Helper: Log Collapse Events ---
static void log_collapse_event(float nav_duration_us, int rssi, int retry) {
gps_timestamp_t ts = gps_get_timestamp();
// CSV Format: COLLAPSE,MonoMS,GpsMS,Synced,Duration,RSSI,Retry
printf("COLLAPSE,%" PRIi64 ",%" PRIi64 ",%d,%.2f,%d,%d\n",
ts.monotonic_ms,
ts.gps_ms,
ts.synced ? 1 : 0,
nav_duration_us,
rssi,
retry);
// --- Event Handler ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ESP_LOGI(TAG, "Got IP -> LED Connected");
status_led_set_state(LED_STATE_CONNECTED);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_current_mode == WIFI_CTL_MODE_STA) {
status_led_set_state(LED_STATE_NO_CONFIG);
}
}
}
// --- Monitor Callbacks & Tasks ---
// ... [Log Collapse / Monitor Callback Logic] ...
static void log_collapse_event(uint32_t nav_duration_us, int rssi, int retry) {
gps_timestamp_t ts = gps_get_timestamp();
int64_t now_ms = ts.gps_us / 1000;
ESP_LOGI(TAG, "COLLAPSE: Time=%" PRId64 "ms, Sync=%d, Dur=%lu us, RSSI=%d, Retry=%d",
now_ms, ts.synced ? 1 : 0, nav_duration_us, rssi, retry);
}
static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) {
s_monitor_frame_count++;
// Check for Collapse conditions (High NAV + Retry)
if (frame->retry && frame->duration_id > 5000) {
log_collapse_event((float)frame->duration_id, frame->rssi, frame->retry);
}
if (frame->duration_id > 30000) {
ESP_LOGW("MONITOR", "⚠️ VERY HIGH NAV: %u us", frame->duration_id);
}
}
static void monitor_stats_task(void *arg) {
@ -59,87 +94,117 @@ static void monitor_stats_task(void *arg) {
if (wifi_monitor_get_stats(&stats) == ESP_OK) {
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
if (wifi_monitor_is_collapsed()) {
ESP_LOGW("MONITOR", "⚠️ ⚠️ COLLAPSE DETECTED! ⚠️ ⚠️");
}
if (wifi_monitor_is_collapsed()) ESP_LOGW("MONITOR", "⚠️ COLLAPSE DETECTED! ⚠️");
}
}
}
static void auto_monitor_task_func(void *arg) {
uint8_t channel = (uint8_t)(uintptr_t)arg;
// --- Helper to apply IP settings ---
static void apply_ip_settings(void) {
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) return;
ESP_LOGI(TAG, "Waiting for WiFi connection before switching to monitor mode...");
// Wait until LED indicates connected
while (status_led_get_state() != LED_STATE_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(500));
if (wifi_cfg_get_dhcp()) {
esp_netif_dhcpc_start(netif);
} else {
esp_netif_dhcpc_stop(netif);
char ip[16], mask[16], gw[16];
if (wifi_cfg_get_ipv4(ip, mask, gw)) {
esp_netif_ip_info_t info = {0};
// API Fix: esp_ip4addr_aton returns uint32_t
info.ip.addr = esp_ip4addr_aton(ip);
info.netmask.addr = esp_ip4addr_aton(mask);
info.gw.addr = esp_ip4addr_aton(gw);
esp_netif_set_ip_info(netif, &info);
ESP_LOGI(TAG, "Static IP applied: %s", ip);
}
}
ESP_LOGI(TAG, "WiFi connected, waiting for GPS sync (2s)...");
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "Auto-switching to MONITOR mode on channel %d...", channel);
wifi_ctl_switch_to_monitor(channel, WIFI_BW_HT20);
vTaskDelete(NULL);
}
// --- API Implementation ---
// ============================================================================
// PUBLIC API IMPLEMENTATION
// ============================================================================
void wifi_ctl_init(void) {
s_current_mode = WIFI_CTL_MODE_STA;
s_monitor_enabled = false;
s_monitor_frame_count = 0;
}
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bandwidth) {
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
ESP_LOGW(TAG, "Already in monitor mode");
return ESP_OK;
// 1. Initialize Network Interface
esp_netif_create_default_wifi_sta();
// 2. Apply IP Settings (Static vs DHCP)
apply_ip_settings();
// 3. Initialize Wi-Fi Driver
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 4. Register Events
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifi_event_handler, NULL, NULL));
// 5. Configure Storage & Mode
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// 6. Apply Saved Config
if (!wifi_cfg_apply_from_nvs()) {
ESP_LOGW(TAG, "No saved WiFi config found, driver initialized in defaults.");
status_led_set_state(LED_STATE_NO_CONFIG);
} else {
ESP_LOGI(TAG, "WiFi driver initialized from NVS.");
status_led_set_state(LED_STATE_WAITING);
esp_wifi_connect();
}
// Monitor mode typically requires 20MHz
if (bandwidth != WIFI_BW_HT20) {
ESP_LOGW(TAG, "Forcing bandwidth to 20MHz for monitor mode");
bandwidth = WIFI_BW_HT20;
// Load Staging Params
char mode_ignored[16];
wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging);
if (s_monitor_channel_staging == 0) s_monitor_channel_staging = 6;
}
// --- Mode Control (Core) ---
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw) {
if (channel == 0) channel = s_monitor_channel_staging;
if (s_current_mode == WIFI_CTL_MODE_MONITOR && s_monitor_channel_active == channel) {
ESP_LOGW(TAG, "Already in monitor mode (Ch %d)", channel);
return ESP_OK;
}
ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel);
// 1. Stop high-level apps
iperf_stop();
vTaskDelay(pdMS_TO_TICKS(500));
// 2. Disable CSI (hardware conflict)
// 2. GUARDED CALL
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
csi_mgr_disable();
#endif
// 3. Teardown Station
esp_wifi_disconnect();
esp_wifi_stop();
vTaskDelay(pdMS_TO_TICKS(500));
// 4. Re-init in NULL/Promiscuous Mode
esp_wifi_set_mode(WIFI_MODE_NULL);
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
ESP_LOGE(TAG, "Failed to init monitor mode");
return ESP_FAIL;
}
esp_wifi_set_bandwidth(WIFI_IF_STA, bandwidth);
esp_wifi_set_bandwidth(WIFI_IF_STA, bw);
if (wifi_monitor_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start monitor mode");
return ESP_FAIL;
}
// 5. Update State
s_monitor_enabled = true;
s_current_mode = WIFI_CTL_MODE_MONITOR;
s_monitor_channel = channel;
s_monitor_channel_active = channel;
status_led_set_state(LED_STATE_MONITORING);
if (s_monitor_stats_task_handle == NULL) {
@ -149,60 +214,132 @@ esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bandwidth
return ESP_OK;
}
esp_err_t wifi_ctl_switch_to_sta(wifi_band_mode_t band_mode) {
esp_err_t wifi_ctl_switch_to_sta(void) {
if (s_current_mode == WIFI_CTL_MODE_STA) {
ESP_LOGW(TAG, "Already in STA mode");
ESP_LOGI(TAG, "Already in STA mode");
return ESP_OK;
}
ESP_LOGI(TAG, "Switching to STA MODE");
// 1. Stop Monitor Tasks
if (s_monitor_stats_task_handle != NULL) {
vTaskDelete(s_monitor_stats_task_handle);
s_monitor_stats_task_handle = NULL;
}
// 2. Stop Monitor Driver
if (s_monitor_enabled) {
wifi_monitor_stop();
s_monitor_enabled = false;
vTaskDelay(pdMS_TO_TICKS(500));
}
// 3. Re-enable Station Mode
esp_wifi_set_mode(WIFI_MODE_STA);
vTaskDelay(pdMS_TO_TICKS(500));
// 4. Configure & Connect
wifi_config_t wifi_config;
esp_wifi_get_config(WIFI_IF_STA, &wifi_config);
wifi_config.sta.channel = 0; // Auto channel scan
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
esp_wifi_start();
vTaskDelay(pdMS_TO_TICKS(500));
esp_wifi_connect();
// 5. Update State
s_current_mode = WIFI_CTL_MODE_STA;
status_led_set_state(LED_STATE_WAITING);
return ESP_OK;
}
// --- Wrappers for cmd_monitor.c ---
void wifi_ctl_monitor_start(int channel) {
wifi_ctl_switch_to_monitor((uint8_t)channel, WIFI_BW_HT20);
}
void wifi_ctl_stop(void) {
wifi_ctl_switch_to_sta();
}
void wifi_ctl_start_station(void) {
wifi_ctl_switch_to_sta();
}
void wifi_ctl_start_ap(void) {
ESP_LOGW(TAG, "AP Mode not fully implemented, using STA");
wifi_ctl_switch_to_sta();
}
// --- Settings ---
void wifi_ctl_set_channel(int channel) {
if (channel < 1 || channel > 14) {
ESP_LOGE(TAG, "Invalid channel %d", channel);
return;
}
s_monitor_channel_staging = (uint8_t)channel;
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
ESP_LOGI(TAG, "Switching live channel to %d", channel);
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
s_monitor_channel_active = (uint8_t)channel;
}
}
void wifi_ctl_status(void) {
const char *mode_str = (s_current_mode == WIFI_CTL_MODE_MONITOR) ? "MONITOR" :
(s_current_mode == WIFI_CTL_MODE_AP) ? "AP" : "STATION";
printf("WiFi Status:\n");
printf(" Mode: %s\n", mode_str);
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
printf(" Channel: %d\n", s_monitor_channel_active);
printf(" Frames: %lu\n", (unsigned long)s_monitor_frame_count);
}
printf(" Staging Ch: %d\n", s_monitor_channel_staging);
}
// --- Params (NVS) ---
bool wifi_ctl_param_is_unsaved(void) {
return wifi_cfg_monitor_channel_is_unsaved(s_monitor_channel_staging);
}
void wifi_ctl_param_save(const char *dummy) {
(void)dummy;
if (wifi_cfg_set_monitor_channel(s_monitor_channel_staging)) {
ESP_LOGI(TAG, "Monitor channel (%d) saved to NVS", s_monitor_channel_staging);
} else {
ESP_LOGI(TAG, "No changes to save.");
}
}
void wifi_ctl_param_init(void) {
char mode_ignored[16];
uint8_t ch = 0;
wifi_cfg_get_mode(mode_ignored, &ch);
if (ch > 0) s_monitor_channel_staging = ch;
ESP_LOGI(TAG, "Reloaded monitor channel: %d", s_monitor_channel_staging);
}
void wifi_ctl_param_clear(void) {
wifi_cfg_clear_monitor_channel();
s_monitor_channel_staging = 6;
ESP_LOGI(TAG, "Monitor config cleared (Defaulting to Ch 6).");
}
// --- Getters ---
wifi_ctl_mode_t wifi_ctl_get_mode(void) { return s_current_mode; }
int wifi_ctl_get_channel(void) { return s_monitor_channel_active; }
// --- Deprecated ---
static void auto_monitor_task_func(void *arg) {
uint8_t channel = (uint8_t)(uintptr_t)arg;
ESP_LOGI(TAG, "Waiting for WiFi connection before switching to monitor mode...");
while (status_led_get_state() != LED_STATE_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(500));
}
ESP_LOGI(TAG, "WiFi connected, waiting for GPS sync (2s)...");
vTaskDelay(pdMS_TO_TICKS(2000));
wifi_ctl_switch_to_monitor(channel, WIFI_BW_HT20);
vTaskDelete(NULL);
}
void wifi_ctl_auto_monitor_start(uint8_t channel) {
xTaskCreate(auto_monitor_task_func, "auto_monitor", 4096, (void*)(uintptr_t)channel, 5, NULL);
}
wifi_ctl_mode_t wifi_ctl_get_mode(void) {
return s_current_mode;
}
uint8_t wifi_ctl_get_monitor_channel(void) {
return s_monitor_channel;
}
uint32_t wifi_ctl_get_monitor_frame_count(void) {
return s_monitor_frame_count;
}

View File

@ -1,56 +1,81 @@
/*
* wifi_controller.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#include "esp_wifi.h"
#include "esp_wifi_types.h" // Needed for wifi_bandwidth_t
#ifdef __cplusplus
extern "C" {
#endif
// Types
typedef enum {
WIFI_CTL_MODE_STA,
WIFI_CTL_MODE_STA, // Changed from _STATION to _STA to match cmd_wifi.c
WIFI_CTL_MODE_AP,
WIFI_CTL_MODE_MONITOR
} wifi_ctl_mode_t;
/**
* @brief Initialize the WiFi Controller
*/
// Init
void wifi_ctl_init(void);
/**
* @brief Switch operation mode to Monitor (Sniffer)
* @param channel WiFi channel (1-165)
* @param bandwidth Bandwidth (usually WIFI_BW_HT20 for monitor)
*/
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bandwidth);
// Mode Control (Advanced)
esp_err_t wifi_ctl_switch_to_sta(void);
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw);
/**
* @brief Switch operation mode to Station (Client)
* @param band_mode Band preference (Auto, 2G only, 5G only)
*/
esp_err_t wifi_ctl_switch_to_sta(wifi_band_mode_t band_mode);
// Simple Wrappers (for cmd_monitor.c)
void wifi_ctl_start_station(void);
void wifi_ctl_start_ap(void);
void wifi_ctl_monitor_start(int channel);
void wifi_ctl_stop(void);
/**
* @brief Start the auto-monitor task
* Waits for connection, waits for GPS, then switches to monitor mode.
* @param channel Channel to monitor
*/
void wifi_ctl_auto_monitor_start(uint8_t channel);
// Settings
void wifi_ctl_set_channel(int channel);
void wifi_ctl_status(void);
/**
* @brief Get current operation mode
*/
// Params (NVS)
bool wifi_ctl_param_is_unsaved(void);
void wifi_ctl_param_save(const char *dummy);
void wifi_ctl_param_init(void);
void wifi_ctl_param_clear(void);
// Getters
wifi_ctl_mode_t wifi_ctl_get_mode(void);
int wifi_ctl_get_channel(void);
/**
* @brief Get the current monitor channel
*/
uint8_t wifi_ctl_get_monitor_channel(void);
/**
* @brief Get total frames captured in monitor mode
*/
uint32_t wifi_ctl_get_monitor_frame_count(void);
// Deprecated / Compatibility
void wifi_ctl_auto_monitor_start(uint8_t channel);
#ifdef __cplusplus
}

View File

@ -1,3 +1,36 @@
/*
* wifi_monitor.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "wifi_monitor.h"
#include "esp_log.h"
#include "esp_wifi.h"

View File

@ -1,3 +1,36 @@
/*
* wifi_monitor.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef WIFI_MONITOR_H
#define WIFI_MONITOR_H

View File

@ -1,389 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 WiFi Configuration Tool - Static IP with auto-disable DHCP and CSI control
"""
import serial
import time
import sys
import argparse
def log_verbose(message, verbose=False):
"""Print message only if verbose is enabled"""
if verbose:
print(f"[VERBOSE] {message}")
def config_device(port, ip, ssid="ClubHouse2G", password="ez2remember",
gateway="192.168.1.1", netmask="255.255.255.0",
band="2.4G", bandwidth="HT20", powersave="NONE",
mode="STA", monitor_channel=36, csi_enable=False,
reboot=True, verbose=False):
"""Configure ESP32 device via serial with static IP and CSI control"""
print(f"\n{'='*70}")
print(f"ESP32 WiFi Configuration (Static IP + Mode + CSI)")
print(f"{'='*70}")
print(f"Port: {port}")
print(f"SSID: {ssid}")
print(f"Password: {'*' * len(password)}")
print(f"IP: {ip} (DHCP disabled)")
print(f"Gateway: {gateway}")
print(f"Netmask: {netmask}")
print(f"Mode: {mode}")
if mode == "MONITOR":
print(f"Mon Ch: {monitor_channel}")
print(f"Band: {band}")
print(f"Bandwidth: {bandwidth}")
print(f"PowerSave: {powersave}")
print(f"CSI: {'ENABLED' if csi_enable else 'DISABLED'}")
print(f"Reboot: {'Yes' if reboot else 'No'}")
print(f"Verbose: {verbose}")
print(f"{'='*70}\n")
try:
# Open serial connection
log_verbose(f"Opening serial port {port} at 115200 baud...", verbose)
ser = serial.Serial(port, 115200, timeout=0.5, write_timeout=0.5)
log_verbose(f"Serial port opened successfully", verbose)
log_verbose(f"Port settings: {ser}", verbose)
time.sleep(0.2)
# Check if there's any data waiting
if ser.in_waiting:
log_verbose(f"{ser.in_waiting} bytes waiting in buffer", verbose)
existing = ser.read(ser.in_waiting).decode('utf-8', errors='ignore')
log_verbose(f"Existing data: {existing[:100]}", verbose)
# Build config message
# DHCP is always disabled (0) when IP address is provided
config_lines = [
"CFG",
f"SSID:{ssid}",
f"PASS:{password}",
f"IP:{ip}",
f"MASK:{netmask}",
f"GW:{gateway}",
"DHCP:0", # Always disabled for static IP
f"BAND:{band}",
f"BW:{bandwidth}",
f"POWERSAVE:{powersave}",
f"MODE:{mode}",
f"MON_CH:{monitor_channel}",
f"CSI:{'1' if csi_enable else '0'}",
"END"
]
config = '\n'.join(config_lines) + '\n'
log_verbose(f"Config message size: {len(config)} bytes", verbose)
if verbose:
print("[VERBOSE] Config message:")
for line in config_lines:
display_line = line if not line.startswith("PASS:") else "PASS:********"
print(f"[VERBOSE] {display_line}")
# Send config
print("Sending configuration...")
print("\nConfiguration being sent:")
for line in config_lines:
display_line = line if not line.startswith("PASS:") else "PASS:********"
print(f" {display_line}")
print()
start_time = time.time()
bytes_written = ser.write(config.encode('utf-8'))
ser.flush()
send_time = time.time() - start_time
log_verbose(f"Wrote {bytes_written} bytes in {send_time:.3f}s", verbose)
print(f"Sent {bytes_written} bytes")
print("Waiting for response...")
time.sleep(3)
# Read response
if ser.in_waiting:
response_size = ser.in_waiting
print(f"\n✓ Response received: {response_size} bytes")
response = ser.read(response_size).decode('utf-8', errors='ignore')
print("\nDevice response:")
print("-" * 70)
for line in response.split('\n')[:30]:
if line.strip():
print(f" {line}")
print("-" * 70)
# Check for key indicators
success_indicators = []
warning_indicators = []
if "OK" in response:
success_indicators.append("✓ Configuration acknowledged (OK)")
if "Config saved" in response or "saved to NVS" in response:
success_indicators.append("✓ Config saved to NVS")
if "CSI enable state saved" in response:
csi_state = "ENABLED" if csi_enable else "DISABLED"
success_indicators.append(f"✓ CSI {csi_state} saved to NVS")
if "got ip:" in response.lower():
success_indicators.append("✓ Device connected to WiFi!")
import re
ip_match = re.search(r'got ip:(\d+\.\d+\.\d+\.\d+)', response, re.IGNORECASE)
if ip_match:
received_ip = ip_match.group(1)
success_indicators.append(f" Assigned IP: {received_ip}")
if received_ip != ip:
warning_indicators.append(f"⚠ Warning: Device got {received_ip} instead of configured {ip}")
warning_indicators.append(" This might indicate DHCP is still enabled")
if "connected" in response.lower():
success_indicators.append("✓ WiFi connection established")
if "failed" in response.lower() or "disconnect" in response.lower():
warning_indicators.append("⚠ WiFi connection may have failed")
if "error" in response.lower():
warning_indicators.append("⚠ Error detected in response")
if success_indicators:
print("\nStatus indicators:")
for indicator in success_indicators:
print(f" {indicator}")
if warning_indicators:
print("\nWarnings:")
for warning in warning_indicators:
print(f" {warning}")
else:
print("\n⚠ No response from device")
print(" This could mean:")
print(" - Device is not running config handler")
print(" - Wrong serial port")
print(" - Baud rate mismatch")
# Reboot device if requested
if reboot:
print("\n" + "="*70)
print("Rebooting device...")
print("="*70)
log_verbose("Performing hardware reset via DTR/RTS", verbose)
ser.dtr = False
ser.rts = True
time.sleep(0.1)
ser.rts = False
time.sleep(0.1)
ser.dtr = True
print("✓ Reset signal sent - waiting for boot...")
time.sleep(3)
if ser.in_waiting:
boot_msg = ser.read(ser.in_waiting).decode('utf-8', errors='ignore')
print("\nBoot messages:")
print("-" * 70)
for line in boot_msg.split('\n')[:40]:
if line.strip():
print(f" {line}")
print("-" * 70)
# Check boot status
boot_success = []
boot_warnings = []
if "WiFi config loaded from NVS" in boot_msg:
boot_success.append("✓ Config successfully loaded from NVS")
elif "No WiFi config" in boot_msg or "YELLOW LED" in boot_msg:
boot_warnings.append("✗ NO CONFIG found in NVS")
boot_warnings.append(" Device does not see saved config")
# Check CSI status
if "CSI Capture: ENABLED" in boot_msg:
boot_success.append("✓ CSI capture is ENABLED")
elif "CSI Capture: DISABLED" in boot_msg:
if csi_enable:
boot_warnings.append("⚠ CSI is DISABLED but was configured as ENABLED")
else:
boot_success.append("✓ CSI capture is DISABLED (as configured)")
# Check if device got the correct static IP
import re
ip_match = re.search(r'got ip:(\d+\.\d+\.\d+\.\d+)', boot_msg, re.IGNORECASE)
if ip_match:
received_ip = ip_match.group(1)
if received_ip == ip:
boot_success.append(f"✓ Device got correct static IP: {ip}")
else:
boot_warnings.append(f"⚠ Device got {received_ip} instead of {ip}")
boot_warnings.append(" DHCP may still be enabled or IP conflict exists")
if "WiFi CONNECTED" in boot_msg:
boot_success.append("✓ WiFi connection confirmed")
if boot_success:
print("\nBoot Status - SUCCESS:")
for msg in boot_success:
print(f" {msg}")
if boot_warnings:
print("\nBoot Status - ISSUES:")
for msg in boot_warnings:
print(f" {msg}")
else:
print("\n⚠ No boot messages received")
print(" Device may still be booting...")
if verbose:
log_verbose(f"Input buffer: {ser.in_waiting} bytes", verbose)
log_verbose(f"Output buffer empty: {ser.out_waiting == 0}", verbose)
ser.close()
log_verbose("Serial port closed", verbose)
print(f"\n{'='*70}")
print("Configuration Summary")
print(f"{'='*70}")
print(f"Port: {port}")
print(f"Static IP: {ip}")
print(f"SSID: {ssid}")
print(f"Mode: {mode}")
print(f"Band: {band}")
print(f"Bandwidth: {bandwidth}")
print(f"PowerSave: {powersave}")
print(f"CSI: {'ENABLED' if csi_enable else 'DISABLED'}")
print(f"DHCP: Disabled (static IP mode)")
print(f"{'='*70}")
print("\nNext steps:")
print(f" 1. Test connection:")
print(f" ping {ip}")
print(f" iperf -c {ip}")
print(f"\n 2. Verify device has correct IP:")
print(f" idf.py -p {port} monitor")
print(f" Look for: 'got ip:{ip}'")
if csi_enable:
print(f"\n 3. Verify CSI is capturing:")
print(f" Look for: 'CSI Capture: ENABLED'")
print(f" 'Captured X CSI packets'")
return True
except serial.SerialException as e:
print(f"\n✗ Serial error: {e}")
log_verbose(f"Serial exception details: {type(e).__name__}", verbose)
print(" Is another program using this port?")
return False
except KeyboardInterrupt:
print("\n\nConfiguration cancelled by user")
if 'ser' in locals() and ser.is_open:
ser.close()
log_verbose("Serial port closed after interrupt", verbose)
return False
except Exception as e:
print(f"\n✗ Error: {e}")
if verbose:
import traceback
print("\n[VERBOSE] Full traceback:")
traceback.print_exc()
return False
def main():
parser = argparse.ArgumentParser(
description='Configure ESP32 WiFi with static IP (DHCP automatically disabled) and CSI control',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Configure device #1 for STA mode with CSI DISABLED (baseline testing)
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA
# Configure device #1 for STA mode with CSI ENABLED
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA --csi
# Configure device #25 for MONITOR mode (collapse detection, CSI not needed)
%(prog)s -p /dev/ttyUSB1 -i 192.168.1.90 -M MONITOR -mc 36
# STA mode with CSI for iperf + CSI correlation testing
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA --csi -ps NONE
# Monitor mode on 2.4GHz channel 6
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.91 -M MONITOR -mc 6 -b 2.4G
# STA mode on 5GHz with 40MHz bandwidth and CSI
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.81 -M STA -b 5G -B HT40 --csi
# With verbose output
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.51 -v
Note:
- Mode and CSI enable state are saved to NVS
- Device will auto-start in configured mode on boot
- CSI defaults to DISABLED unless --csi flag is used
- DHCP is always disabled when using this script (static IP mode)
"""
)
parser.add_argument('-p', '--port', required=True,
help='Serial port (e.g., /dev/ttyUSB0)')
parser.add_argument('-i', '--ip', required=True,
help='Static IP address (DHCP will be disabled)')
parser.add_argument('-s', '--ssid', default='ClubHouse2G',
help='WiFi SSID (default: ClubHouse2G)')
parser.add_argument('-P', '--password', default='ez2remember',
help='WiFi password (default: ez2remember)')
parser.add_argument('-g', '--gateway', default='192.168.1.1',
help='Gateway IP (default: 192.168.1.1)')
parser.add_argument('-m', '--netmask', default='255.255.255.0',
help='Netmask (default: 255.255.255.0)')
parser.add_argument('-b', '--band', default='2.4G', choices=['2.4G', '5G'],
help='WiFi band: 2.4G or 5G (default: 2.4G)')
parser.add_argument('-B', '--bandwidth', default='HT20',
choices=['HT20', 'HT40', 'VHT80'],
help='Channel bandwidth: HT20 (20MHz), HT40 (40MHz), VHT80 (80MHz, 5GHz only) (default: HT20)')
parser.add_argument('-ps', '--powersave', default='NONE',
choices=['NONE', 'MIN', 'MIN_MODEM', 'MAX', 'MAX_MODEM'],
help='Power save mode: NONE (no PS, best for CSI), MIN/MIN_MODEM, MAX/MAX_MODEM (default: NONE)')
parser.add_argument('-M', '--mode', default='STA',
choices=['STA', 'MONITOR'],
help='Operating mode: STA (connect to AP, CSI+iperf) or MONITOR (promiscuous, collapse detection) (default: STA)')
parser.add_argument('-mc', '--monitor-channel', type=int, default=36,
help='Monitor mode channel (1-11 for 2.4GHz, 36-165 for 5GHz) (default: 36)')
parser.add_argument('--csi', action='store_true',
help='Enable CSI capture (default: disabled). Use for devices that need CSI data collection.')
parser.add_argument('-r', '--no-reboot', action='store_true',
help='Do NOT reboot device after configuration')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
args = parser.parse_args()
# Validate bandwidth selection
if args.bandwidth == 'VHT80' and args.band == '2.4G':
print("\n✗ Error: VHT80 (80MHz) is only supported on 5GHz band")
print(" Either use -b 5G or choose HT20/HT40 bandwidth")
sys.exit(1)
success = config_device(
port=args.port,
ip=args.ip,
ssid=args.ssid,
password=args.password,
gateway=args.gateway,
netmask=args.netmask,
band=args.band,
bandwidth=args.bandwidth,
powersave=args.powersave,
mode=args.mode,
monitor_channel=args.monitor_channel,
csi_enable=args.csi,
reboot=not args.no_reboot,
verbose=args.verbose
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

21
dependencies.lock Normal file
View File

@ -0,0 +1,21 @@
dependencies:
espressif/led_strip:
component_hash: 28c6509a727ef74925b372ed404772aeedf11cce10b78c3f69b3c66799095e2d
dependencies:
- name: idf
require: private
version: '>=4.4'
source:
registry_url: https://components.espressif.com/
type: service
version: 2.5.5
idf:
source:
type: idf
version: 6.0.0
direct_dependencies:
- espressif/led_strip
- idf
manifest_hash: cfead66889b7175cc6aa9a766fd00dc94649d6800986f3fcc4645dc58723ed39
target: esp32s3
version: 2.0.0

View File

@ -12,6 +12,8 @@ import logging
import glob
import random
from pathlib import Path
from serial.tools import list_ports
import subprocess
# Ensure detection script is available
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
@ -56,13 +58,35 @@ def generate_config_suffix(target, csi, ampdu):
return f"{target}_{csi_str}_{ampdu_str}"
def auto_detect_devices():
"""Prioritizes static udev paths (/dev/esp_port_XX) if they exist."""
"""Prioritizes static udev paths (/dev/esp_port_XX) and removes duplicates."""
try:
ports = glob.glob('/dev/esp_port_*')
if ports:
# --- New Deduplication Logic ---
unique_map = {}
for p in ports:
try:
# Resolve symlink (e.g., /dev/esp_port_01 -> /dev/ttyUSB0)
real_path = os.path.realpath(p)
if real_path not in unique_map:
unique_map[real_path] = p
else:
# Conflict! We have both esp_port_1 and esp_port_01.
# Keep the "shorter" one (esp_port_1) to match your new scheme.
current_alias = unique_map[real_path]
if len(p) < len(current_alias):
unique_map[real_path] = p
except OSError:
continue
# Use the filtered list
ports = list(unique_map.values())
# -------------------------------
# Sort by suffix number
ports.sort(key=lambda x: int(re.search(r'(\d+)$', x).group(1)) if re.search(r'(\d+)$', x) else 0)
print(f"{Colors.CYAN}Auto-detected {len(ports)} devices using static udev rules.{Colors.RESET}")
print(f"{Colors.CYAN}Auto-detected {len(ports)} devices (filtered from {len(unique_map) + (len(glob.glob('/dev/esp_port_*')) - len(unique_map))} aliases).{Colors.RESET}")
return [type('obj', (object,), {'device': p}) for p in ports]
except Exception:
pass
@ -111,7 +135,8 @@ class UnifiedDeployWorker:
# --- Semaphore Released Here ---
await asyncio.sleep(2.0)
# Brief pause after flash to allow device to start booting (hardware timing)
await asyncio.sleep(1.0)
if not self.args.flash_only:
if self.args.ssid and self.args.password:
@ -147,10 +172,10 @@ class UnifiedDeployWorker:
except Exception as e:
return False
try:
# Reset DTR/RTS logic
# Reset DTR/RTS logic (hardware reset timing - must use sleep)
writer.transport.serial.dtr = False
writer.transport.serial.rts = True
await asyncio.sleep(0.1)
await asyncio.sleep(0.1) # Hardware timing requirement, not event-based
writer.transport.serial.rts = False
writer.transport.serial.dtr = False
@ -160,36 +185,100 @@ class UnifiedDeployWorker:
return False
# 2. Send Configuration via CLI
# Command: wifi_config -s "SSID" -p "PASS" -i "IP"
# Note: The Shell will auto-reboot after this command.
cmd = f'wifi_config -s "{self.args.ssid}" -p "{self.args.password}" -i "{self.target_ip}"'
if not self.args.iperf_client and not self.args.iperf_server:
# If just connecting, maybe we want DHCP?
# But if target_ip is set, we force static.
pass
self.log.info(f"Sending: {cmd}")
writer.write(f"{cmd}\n".encode())
# Use separate commands: wifi connect and ip set
# First, connect to WiFi (saves credentials to NVS)
# Password is optional, only include if provided
if self.args.password:
wifi_cmd = f'wifi connect "{self.args.ssid}" "{self.args.password}"'
else:
wifi_cmd = f'wifi connect "{self.args.ssid}"'
self.log.info(f"Sending: {wifi_cmd}")
writer.write(f"{wifi_cmd}\n".encode())
await writer.drain()
# 3. Wait for the reboot and new prompt
# The device prints "Rebooting..." then restarts.
self.log.info("Waiting for reboot...")
await asyncio.sleep(3.0) # Give it time to actually reset
# Wait for "Connecting to..." message (command acknowledged)
match_idx, output = await self._wait_for_pattern(reader, ["Connecting to"], timeout=2.0)
if match_idx is None:
self.log.warning("WiFi connect command response not detected, continuing...")
if not await self._wait_for_prompt(reader, writer, timeout=20):
self.log.error("Device did not return to prompt after reboot.")
return False
# Second, set static IP address (if IP is provided)
if self.target_ip:
ip_cmd = f'ip set {self.target_ip} {self.args.netmask} {self.args.gateway}'
self.log.info(f"Sending: {ip_cmd}")
writer.write(f"{ip_cmd}\n".encode())
await writer.drain()
self.log.info(f"{Colors.GREEN}Reboot complete. Shell Ready.{Colors.RESET}")
# Wait for IP set confirmation
match_idx, output = await self._wait_for_pattern(
reader,
["Static IP set", "Saved. Will apply on next init", "Invalid IP format"],
timeout=3.0
)
if match_idx is None:
self.log.warning("IP set command response not detected, continuing...")
elif match_idx == 2: # "Invalid IP format"
self.log.error("IP configuration failed: Invalid IP format")
return False
# 4. (Optional) Start iperf if requested
# The new firmware does not auto-start iperf on boot unless commanded.
# 3. Wait for prompt to ensure command completed (may take a moment for WiFi to start connecting)
# WiFi connection is async, so we just verify the prompt returns
if not await self._wait_for_prompt(reader, writer, timeout=5.0):
self.log.warning("Prompt check failed after configuration, but continuing...")
self.log.info(f"{Colors.GREEN}Configuration complete.{Colors.RESET}")
# 4. (Optional) Configure and start iperf if requested
if not self.args.no_iperf:
self.log.info("Starting iperf listener...")
# Configure iperf parameters if specified
iperf_params = []
if self.args.iperf_dest_ip:
iperf_params.append(f'--client {self.args.iperf_dest_ip}')
if self.args.iperf_port:
iperf_params.append(f'--port {self.args.iperf_port}')
if self.args.iperf_len:
iperf_params.append(f'--len {self.args.iperf_len}')
if self.args.iperf_burst:
iperf_params.append(f'--burst {self.args.iperf_burst}')
# Note: iperf-period not directly supported, would need PPS calculation
if iperf_params:
iperf_set_cmd = f"iperf set {' '.join(iperf_params)}\n"
self.log.info(f"Configuring iperf: {iperf_set_cmd.strip()}")
writer.write(iperf_set_cmd.encode())
await writer.drain()
# Wait for iperf set confirmation
match_idx, output = await self._wait_for_pattern(
reader,
["Configuration updated", "No changes specified"],
timeout=2.0
)
if match_idx is None:
self.log.warning("iperf set response not detected, continuing...")
# Save configuration to NVS
self.log.info("Saving iperf config to NVS...")
writer.write(b"iperf save\n")
await writer.drain()
# Wait for iperf save confirmation
match_idx, output = await self._wait_for_pattern(
reader,
["Configuration saved to NVS", "No changes to save", "Error saving"],
timeout=2.0
)
if match_idx is None:
self.log.warning("iperf save response not detected, continuing...")
elif match_idx == 2: # "Error saving"
self.log.error("iperf save failed, but continuing...")
# Start iperf (no specific output expected, just wait for prompt)
self.log.info("Starting iperf...")
writer.write(b"iperf start\n")
await writer.drain()
await asyncio.sleep(0.5)
# Wait a brief moment for command to be processed, then check for prompt
await self._wait_for_prompt(reader, writer, timeout=2.0)
return True
@ -222,6 +311,48 @@ class UnifiedDeployWorker:
break
return False
async def _wait_for_pattern(self, reader, patterns, timeout=5.0, consume=True):
"""
Wait for one of the given patterns to appear in serial output.
patterns: list of regex patterns or simple strings to search for
timeout: maximum time to wait
consume: if True, read and discard data; if False, only peek
Returns: tuple (matched_pattern_index, buffer) or (None, buffer) if timeout
"""
end_time = time.time() + timeout
buffer = ""
# Compile patterns
compiled_patterns = []
for p in patterns:
if isinstance(p, str):
compiled_patterns.append(re.compile(re.escape(p), re.IGNORECASE))
else:
compiled_patterns.append(p)
while time.time() < end_time:
try:
chunk = await asyncio.wait_for(reader.read(1024), timeout=0.1)
if chunk:
buffer += chunk.decode('utf-8', errors='ignore')
# Keep buffer size manageable (last 4KB)
if len(buffer) > 4096:
buffer = buffer[-4096:]
# Check each pattern
for i, pattern in enumerate(compiled_patterns):
if pattern.search(buffer):
if not consume:
# Put data back (not easily possible with async, so we'll consume)
pass
return (i, buffer)
except asyncio.TimeoutError:
continue
except Exception:
break
return (None, buffer)
# [Keep _query_version, _identify_chip, _erase_flash, _flash_firmware AS IS]
async def _query_version(self):
try:
@ -230,7 +361,8 @@ class UnifiedDeployWorker:
writer.transport.serial.rts = False
writer.write(b'\n')
await writer.drain()
await asyncio.sleep(0.1)
# Brief pause for prompt, then send command
await asyncio.sleep(0.1) # Hardware timing for prompt to appear
writer.write(b'version\n')
await writer.drain()
@ -344,6 +476,81 @@ class UnifiedDeployWorker:
self.log.error(f"Flash Prep Error: {e}")
return False
def update_udev_map(dry_run=False):
"""
Scans all USB serial devices, sorts them by physical topology (Bus/Port),
and generates a udev rule file to map them to /dev/esp_port_XX.
"""
print(f"{Colors.BLUE}Scanning USB topology to generate stable port maps...{Colors.RESET}")
# Get all USB serial devices
devices = list(list_ports.grep("USB|ACM|CP210|FT232"))
if not devices:
print(f"{Colors.RED}No devices found.{Colors.RESET}")
return
# Sort by "location" (Physical USB path: e.g., 1-1.2.3)
# This guarantees esp_port_01 is always the first physical port.
devices.sort(key=lambda x: x.location if x.location else x.device)
generated_rules = []
print(f"{'Physical Path':<20} | {'Current Dev':<15} | {'Assigned Symlink'}")
print("-" * 65)
for i, dev in enumerate(devices):
port_num = i + 1
symlink = f"esp_port_{port_num}" # e.g., esp_port_1
# Get detailed udev info to find the stable physical path ID
try:
cmd = ['udevadm', 'info', '--name', dev.device, '--query=property']
proc = subprocess.run(cmd, capture_output=True, text=True)
props = dict(line.split('=', 1) for line in proc.stdout.splitlines() if '=' in line)
# ID_PATH is the robust physical identifier (e.g., pci-0000:00:14.0-usb-0:1.4.3:1.0)
dev_path = props.get('ID_PATH', '')
if not dev_path:
print(f"{Colors.YELLOW}Skipping {dev.device} (No ID_PATH found){Colors.RESET}")
continue
# Generate the rule
rule = f'SUBSYSTEM=="tty", ENV{{ID_PATH}}=="{dev_path}", SYMLINK+="{symlink}"'
generated_rules.append(rule)
print(f"{dev.location:<20} | {dev.device:<15} | {symlink}")
except Exception as e:
print(f"Error inspecting {dev.device}: {e}")
print("-" * 65)
rules_content = "# Auto-generated by esp32_deploy.py\n" + "\n".join(generated_rules) + "\n"
rule_file = "/etc/udev/rules.d/99-esp32-stable.rules"
if dry_run:
print(f"\n{Colors.YELLOW}--- DRY RUN: Rules that would be written to {rule_file} ---{Colors.RESET}")
print(rules_content)
else:
if os.geteuid() != 0:
print(f"\n{Colors.RED}ERROR: Root privileges required to write udev rules.{Colors.RESET}")
print(f"Run: sudo ./esp32_deploy.py --map-ports")
return
print(f"\nWriting rules to {rule_file}...")
try:
with open(rule_file, 'w') as f:
f.write(rules_content)
print("Reloading udev rules...")
subprocess.run(['udevadm', 'control', '--reload-rules'], check=True)
subprocess.run(['udevadm', 'trigger'], check=True)
print(f"{Colors.GREEN}Success! Devices re-mapped.{Colors.RESET}")
except Exception as e:
print(f"{Colors.RED}Failed to write rules: {e}{Colors.RESET}")
def parse_args():
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
parser.add_argument('-i', '--interactive', action='store_true', help='Prompt for build options')
@ -380,8 +587,9 @@ def parse_args():
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')
parser.add_argument('--map-ports', action='store_true', help="Rescan USB topology and generate udev rules for esp_port_xx")
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 and not args.check_version and not args.map_ports:
parser.error("the following arguments are required: --start-ip")
if args.config_only and args.flash_only: parser.error("Conflicting modes")
return args
@ -553,9 +761,22 @@ async def run_deployment(args):
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
def main():
if os.name == 'nt': asyncio.set_event_loop(asyncio.ProactorEventLoop())
try: asyncio.run(run_deployment(parse_args()))
except KeyboardInterrupt: sys.exit(1)
args = parse_args()
# --- INTERCEPT --map-ports HERE ---
if args.map_ports:
# Run synchronously, no async loop needed
update_udev_map(dry_run=False)
sys.exit(0)
# Standard async deployment flow
if os.name == 'nt':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
try:
asyncio.run(run_deployment(args))
except KeyboardInterrupt:
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,235 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Reconfiguration Tool
Iterates through connected devices and updates their settings (SSID, IP, etc.)
without reflashing firmware. Runs sequentially for reliability.
"""
import sys
import os
import argparse
import ipaddress
import re
import time
import serial
from serial.tools import list_ports
# --- Configuration ---
BAUD_RATE = 115200
TIMEOUT = 0.5 # Serial read timeout
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
CYAN = '\033[96m'
RESET = '\033[0m'
def detect_devices():
"""Returns a sorted list of ESP32 USB serial ports."""
candidates = list(list_ports.grep("CP210|FT232|USB Serial|10C4:EA60"))
ports = [p.device for p in candidates]
ports.sort(key=lambda x: [int(c) if c.isdigit() else c for c in re.split(r'(\d+)', x)])
return ports
def extract_device_number(device_path):
match = re.search(r'(\d+)$', device_path)
return int(match.group(1)) if match else 0
def configure_device(port, target_ip, args):
"""
Connects to a single device, resets it, and injects the config.
Returns True if verification succeeds.
"""
try:
ser = serial.Serial(port, BAUD_RATE, timeout=0.1)
except Exception as e:
print(f"[{port}] {Colors.RED}Connection Failed: {e}{Colors.RESET}")
return False
try:
# 1. Reset Device
ser.dtr = False
ser.rts = True
time.sleep(0.1)
ser.rts = False
ser.dtr = True
# 2. Wait for App to Settle (Handle GPS delay)
print(f"[{port}] Waiting for App (GPS timeout ~3s)...", end='', flush=True)
start_time = time.time()
# We wait until we see the "esp32>" prompt OR specific log lines
# The prompt is the safest indicator that the console is ready.
prompt_detected = False
buffer = ""
while time.time() - start_time < 10.0:
try:
# Read char by char to catch prompts that don't end in newline
chunk = ser.read(ser.in_waiting or 1).decode('utf-8', errors='ignore')
if chunk:
buffer += chunk
# Check for prompt or end of init
if "esp32>" in buffer or "Entering console loop" in buffer:
prompt_detected = True
break
# Keep buffer size manageable
if len(buffer) > 1000: buffer = buffer[-1000:]
except Exception:
pass
time.sleep(0.05)
if not prompt_detected:
print(f" {Colors.YELLOW}Timeout waiting for prompt (continuing anyway){Colors.RESET}")
else:
print(" OK")
# 3. Clear Buffers & Wakeup
ser.reset_input_buffer()
ser.write(b'\n') # Send an Enter to clear any partial commands
time.sleep(0.2)
# 4. Construct Config String (Using CRLF \r\n for safety)
csi_val = '1' if args.csi_enable else '0'
role_str = "SERVER" if args.iperf_server else "CLIENT"
iperf_enable_val = '0' if args.no_iperf else '1'
period_us = int(args.iperf_period * 1000000)
# Note: We send \r\n explicitly
config_lines = [
"CFG",
f"SSID:{args.ssid}",
f"PASS:{args.password}",
f"IP:{target_ip}",
f"MASK:{args.netmask}",
f"GW:{args.gateway}",
f"DHCP:0",
f"BAND:{args.band}",
f"BW:{args.bandwidth}",
f"POWERSAVE:{args.powersave}",
f"MODE:{args.mode}",
f"MON_CH:{args.monitor_channel}",
f"CSI:{csi_val}",
f"IPERF_PERIOD_US:{period_us}",
f"IPERF_ROLE:{role_str}",
f"IPERF_PROTO:{args.iperf_proto}",
f"IPERF_DEST_IP:{args.iperf_dest_ip}",
f"IPERF_PORT:{args.iperf_port}",
f"IPERF_BURST:{args.iperf_burst}",
f"IPERF_LEN:{args.iperf_len}",
f"IPERF_ENABLED:{iperf_enable_val}",
"END"
]
config_payload = "\r\n".join(config_lines) + "\r\n"
# 5. Send Config
print(f"[{port}] Sending Config ({target_ip})...", end='', flush=True)
ser.write(config_payload.encode('utf-8'))
ser.flush()
# 6. Verify
verify_start = time.time()
verified = False
ser.timeout = 0.5 # Increase timeout for line reading
while time.time() - verify_start < 8.0:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if not line: continue
# Check for success indicators
if "Config saved" in line or "CSI enable state saved" in line:
verified = True
break
# Check for IP confirmation
if f"got ip:{target_ip}" in line:
verified = True
break
if verified:
print(f" {Colors.GREEN}SUCCESS{Colors.RESET}")
# Final Reset to apply settings cleanly
ser.dtr = False
ser.rts = True
time.sleep(0.1)
ser.rts = False
return True
else:
print(f" {Colors.RED}FAILED (Verify Timeout){Colors.RESET}")
return False
except Exception as e:
print(f"[{port}] Error: {e}")
return False
finally:
if ser.is_open:
ser.close()
def main():
parser = argparse.ArgumentParser(description='ESP32 Sequential Reconfiguration Tool')
parser.add_argument('--start-ip', required=True, help='Start IP (e.g., 192.168.1.51)')
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('--band', default='2.4G', choices=['2.4G', '5G'])
parser.add_argument('-B', '--bandwidth', default='HT20', choices=['HT20', 'HT40', 'VHT80'])
parser.add_argument('-ps', '--powersave', default='NONE')
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)
parser.add_argument('--iperf-proto', default='UDP', choices=['UDP', 'TCP'])
parser.add_argument('--iperf-dest-ip', default='192.168.1.50')
parser.add_argument('--iperf-port', type=int, default=5001)
parser.add_argument('--no-iperf', action='store_true')
parser.add_argument('--iperf-client', action='store_true')
parser.add_argument('--iperf-server', action='store_true')
parser.add_argument('-M', '--mode', default='STA', choices=['STA', 'MONITOR'])
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
parser.add_argument('--csi', dest='csi_enable', action='store_true')
parser.add_argument('--retries', type=int, default=3, help="Retry attempts per device")
args = parser.parse_args()
print(f"{Colors.BLUE}{'='*60}{Colors.RESET}")
print(f" ESP32 Sequential Reconfig Tool")
print(f"{Colors.BLUE}{'='*60}{Colors.RESET}")
devices = detect_devices()
if not devices:
print(f"{Colors.RED}No devices found.{Colors.RESET}")
sys.exit(1)
print(f"Found {len(devices)} devices. Starting reconfiguration...\n")
start_ip = ipaddress.IPv4Address(args.start_ip)
for i, port in enumerate(devices):
offset = extract_device_number(port)
target_ip = str(start_ip + offset)
print(f"Device {i+1}/{len(devices)}: {Colors.CYAN}{port}{Colors.RESET} -> {Colors.YELLOW}{target_ip}{Colors.RESET}")
success = False
for attempt in range(1, args.retries + 1):
if attempt > 1:
print(f" Retry {attempt}/{args.retries}...")
if configure_device(port, target_ip, args):
success = True
break
time.sleep(1.0)
if not success:
print(f"{Colors.RED} [ERROR] Failed to configure {port} after {args.retries} attempts.{Colors.RESET}\n")
else:
print("")
print(f"{Colors.BLUE}Done.{Colors.RESET}")
if __name__ == '__main__':
main()

View File

@ -1,187 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Mass Flash Script (filtered & robust)
Changes in this version:
- Filters out non-USB system serials (e.g., /dev/ttyS*)
- Only enumerates typical USB serial ports: /dev/ttyUSB* and /dev/ttyACM*
- Further filters to known USB-serial vendor IDs by default:
* FTDI 0x0403
* SiliconLabs/CP210x 0x10C4
* QinHeng/CH34x 0x1A86
* Prolific PL2303 0x067B
* Espressif native 0x303A
- Adds --ports to override selection (glob or comma-separated patterns)
- Uses detect_esp32.detect_chip_type(port) for exact chip string
- Maps to correct idf.py target before flashing
Example:
python3 flash_all.py --project /path/to/project --start-ip 192.168.1.50
python3 flash_all.py --project . --ports '/dev/ttyUSB*,/dev/ttyACM*'
"""
import argparse
import fnmatch
import glob
import os
import subprocess
import sys
from pathlib import Path
from typing import List, Dict
try:
import serial.tools.list_ports as list_ports
except Exception:
print("pyserial is required: pip install pyserial")
raise
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)
try:
import detect_esp32
except Exception as e:
print("Error: detect_esp32.py must be in the same directory and importable.")
print(f"Import error: {e}")
sys.exit(1)
# Known USB VIDs commonly used for ESP32 dev boards/adapters
KNOWN_VIDS = {0x0403, 0x10C4, 0x1A86, 0x067B, 0x303A}
def map_chip_to_idf_target(chip_str: str) -> str:
if not chip_str or chip_str == 'Unknown':
return 'unknown'
s = chip_str.upper()
if s.startswith('ESP32-S3'):
return 'esp32s3'
if s.startswith('ESP32-S2'):
return 'esp32s2'
if s.startswith('ESP32-C3'):
return 'esp32c3'
if s.startswith('ESP32-C6'):
return 'esp32c6'
if s.startswith('ESP32-H2'):
return 'esp32h2'
if s.startswith('ESP32'):
return 'esp32'
return 'unknown'
def run(cmd: List[str], cwd: str = None, check: bool = True) -> int:
print(">>", " ".join(cmd))
proc = subprocess.run(cmd, cwd=cwd)
if check and proc.returncode != 0:
raise RuntimeError(f"Command failed with code {proc.returncode}: {' '.join(cmd)}")
return proc.returncode
def ensure_target(project_dir: str, target: str):
if not target or target == 'unknown':
raise ValueError("Unknown IDF target; cannot set-target.")
run(['idf.py', 'set-target', target], cwd=project_dir, check=True)
def flash_device(project_dir: str, port: str, idf_target: str, baud: int = 460800) -> bool:
try:
ensure_target(project_dir, idf_target)
run(['idf.py', '-p', port, '-b', str(baud), 'flash'], cwd=project_dir, check=True)
return True
except Exception as e:
print(f" Flash failed on {port}: {e}")
return False
def match_any(path: str, patterns: List[str]) -> bool:
return any(fnmatch.fnmatch(path, pat) for pat in patterns)
def list_ports_filtered(patterns: List[str] = None) -> List[object]:
"""Return a filtered list of pyserial list_ports items."""
ports = list(list_ports.comports())
filtered = []
for p in ports:
dev = p.device
# Default pattern filter: only /dev/ttyUSB* and /dev/ttyACM*
if patterns:
if not match_any(dev, patterns):
continue
else:
if not (dev.startswith('/dev/ttyUSB') or dev.startswith('/dev/ttyACM')):
continue
# VID filter (allow if vid is known or missing (some systems omit it), but exclude obvious non-USB)
vid = getattr(p, 'vid', None)
if vid is not None and vid not in KNOWN_VIDS:
# Skip unknown vendor to reduce noise; user can override with --ports
continue
filtered.append(p)
return filtered
def main():
ap = argparse.ArgumentParser(description="Mass flash multiple ESP32 devices with proper chip detection.")
ap.add_argument('--project', required=True, help='Path to the ESP-IDF project to flash')
ap.add_argument('--ssid', help='WiFi SSID (optional)')
ap.add_argument('--password', help='WiFi password (optional)')
ap.add_argument('--start-ip', default='192.168.1.50', help='Base IP address for plan display')
ap.add_argument('--baud', type=int, default=460800, help='Flashing baud rate')
ap.add_argument('--dry-run', action='store_true', help='Plan only; do not flash')
ap.add_argument('--ports', help='Comma-separated glob(s), e.g. "/dev/ttyUSB*,/dev/ttyACM*" to override selection')
args = ap.parse_args()
project_dir = os.path.abspath(args.project)
if not os.path.isdir(project_dir):
print(f"Project directory not found: {project_dir}")
sys.exit(1)
patterns = None
if args.ports:
patterns = [pat.strip() for pat in args.ports.split(',') if pat.strip()]
devices = list_ports_filtered(patterns)
print(f"Found {len(devices)} USB serial device(s) after filtering")
if not devices:
print("No candidate USB serial ports found. Try --ports '/dev/ttyUSB*,/dev/ttyACM*' or check permissions.")
device_list: List[Dict] = []
for idx, dev in enumerate(devices, 1):
port = dev.device
print(f"Probing {port} for exact chip...")
raw_chip = detect_esp32.detect_chip_type(port)
idf_target = map_chip_to_idf_target(raw_chip)
if idf_target == 'unknown':
print(f" WARNING: Could not determine idf.py target for {port} (got '{raw_chip}')")
device_list.append({
'number': idx,
'port': port,
'raw_chip': raw_chip,
'idf_target': idf_target,
'info': dev,
})
# Plan output
base = args.start_ip.split('.')
try:
base0, base1, base2, base3 = int(base[0]), int(base[1]), int(base[2]), int(base[3])
except Exception:
base0, base1, base2, base3 = 192, 168, 1, 50
print("\nFlash plan:")
for d in device_list:
ip_last = base3 + d['number'] - 1
ip = f"{base0}.{base1}.{base2}.{ip_last}"
print(f" Device {d['number']:2d}: {d['port']} -> {d['raw_chip']} [{d['idf_target']}] -> {ip}")
if args.dry_run:
print("\nDry run: not flashing any devices.")
return
failed = []
for d in device_list:
if d['idf_target'] == 'unknown':
print(f"\n ERROR: Unknown IDF target for {d['port']} (raw chip '{d['raw_chip']}'). Skipping.")
failed.append(d['number'])
continue
print(f"\nFlashing {d['port']} as target {d['idf_target']}...")
ok = flash_device(project_dir, d['port'], d['idf_target'], baud=args.baud)
if not ok:
failed.append(d['number'])
if failed:
print(f"\nCompleted with failures on devices: {failed}")
sys.exit(2)
print("\nAll devices flashed successfully.")
if __name__ == '__main__':
main()

View File

@ -1,432 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Parallel Mass Flash Script
Build and flash multiple ESP32 devices concurrently for much faster deployment
"""
import subprocess
import sys
import os
import time
import argparse
import shutil
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from multiprocessing import cpu_count
# Import the detection script
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
import detect_esp32
except ImportError:
print("Error: detect_esp32.py must be in the same directory")
sys.exit(1)
def detect_device_type(port_info):
"""Detect ESP32 variant based on USB chip"""
if port_info.vid == 0x303A:
return 'esp32s3'
return 'esp32'
def probe_chip_type(port):
"""Probe the actual chip type using esptool.py"""
try:
result = subprocess.run(
['esptool.py', '--port', port, 'chip_id'],
capture_output=True,
text=True,
timeout=10
)
output = result.stdout + result.stderr
if 'ESP32-S3' in output:
return 'esp32s3'
elif 'ESP32-S2' in output:
return 'esp32s2'
elif 'ESP32-C3' in output:
return 'esp32c3'
elif 'ESP32' in output:
return 'esp32'
except Exception as e:
print(f" Warning: Could not probe {port}: {e}")
return 'esp32'
def create_sdkconfig(build_dir, ssid, password, ip_addr, gateway='192.168.1.1', netmask='255.255.255.0'):
"""Create sdkconfig.defaults file with WiFi and IP configuration"""
sdkconfig_path = os.path.join(build_dir, 'sdkconfig.defaults')
config_content = f"""# WiFi Configuration
CONFIG_WIFI_SSID="{ssid}"
CONFIG_WIFI_PASSWORD="{password}"
CONFIG_WIFI_MAXIMUM_RETRY=5
# Static IP Configuration
CONFIG_USE_STATIC_IP=y
CONFIG_STATIC_IP_ADDR="{ip_addr}"
CONFIG_STATIC_GATEWAY_ADDR="{gateway}"
CONFIG_STATIC_NETMASK_ADDR="{netmask}"
"""
with open(sdkconfig_path, 'w') as f:
f.write(config_content)
def build_firmware(device_info, project_dir, build_dir, ssid, password):
"""Build firmware for a single device with unique configuration"""
dev_num = device_info['number']
chip_type = device_info['chip']
ip_addr = device_info['ip']
port = device_info['port']
print(f"[Device {dev_num}] [{port}] Chip: {chip_type.upper()} | Building with IP {ip_addr}")
try:
# Create build directory
os.makedirs(build_dir, exist_ok=True)
# Copy project files to build directory
for item in ['main', 'CMakeLists.txt']:
src = os.path.join(project_dir, item)
dst = os.path.join(build_dir, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
# Create sdkconfig.defaults
create_sdkconfig(build_dir, ssid, password, ip_addr)
# Set target
result = subprocess.run(
['idf.py', 'set-target', chip_type],
cwd=build_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return {
'success': False,
'device': dev_num,
'error': f"Set target failed: {result.stderr[:200]}"
}
# Build
result = subprocess.run(
['idf.py', 'build'],
cwd=build_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return {
'success': False,
'device': dev_num,
'error': f"Build failed: {result.stderr[-500:]}"
}
print(f"[Device {dev_num}] ✓ Build complete ({chip_type.upper()})")
return {
'success': True,
'device': dev_num,
'build_dir': build_dir
}
except Exception as e:
return {
'success': False,
'device': dev_num,
'error': str(e)
}
def flash_device(device_info, build_dir):
"""Flash a single device"""
dev_num = device_info['number']
port = device_info['port']
ip_addr = device_info['ip']
chip_type = device_info['chip']
print(f"[Device {dev_num}] [{port}] {chip_type.upper()} | Flashing -> {ip_addr}")
try:
result = subprocess.run(
['idf.py', '-p', port, 'flash'],
cwd=build_dir,
capture_output=True,
text=True,
timeout=120
)
if result.returncode != 0:
return {
'success': False,
'device': dev_num,
'port': port,
'error': f"Flash failed: {result.stderr[-500:]}"
}
print(f"[Device {dev_num}] ✓ Flash complete ({chip_type.upper()}) at {ip_addr}")
return {
'success': True,
'device': dev_num,
'port': port,
'ip': ip_addr
}
except subprocess.TimeoutExpired:
return {
'success': False,
'device': dev_num,
'port': port,
'error': "Flash timeout"
}
except Exception as e:
return {
'success': False,
'device': dev_num,
'port': port,
'error': str(e)
}
def build_and_flash(device_info, project_dir, work_dir, ssid, password):
"""Combined build and flash for a single device"""
dev_num = device_info['number']
build_dir = os.path.join(work_dir, f'build_device_{dev_num}')
# Build
build_result = build_firmware(device_info, project_dir, build_dir, ssid, password)
if not build_result['success']:
return build_result
# Flash
flash_result = flash_device(device_info, build_dir)
# Clean up build directory to save space
try:
shutil.rmtree(build_dir)
except:
pass
return flash_result
def main():
parser = argparse.ArgumentParser(description='Parallel mass flash ESP32 devices')
parser.add_argument('--ssid', required=True, help='WiFi SSID')
parser.add_argument('--password', required=True, help='WiFi password')
parser.add_argument('--start-ip', default='192.168.1.50',
help='Starting IP address (default: 192.168.1.50)')
parser.add_argument('--gateway', default='192.168.1.1',
help='Gateway IP (default: 192.168.1.1)')
parser.add_argument('--project-dir', default=None,
help='ESP32 iperf project directory')
parser.add_argument('--probe', action='store_true',
help='Probe each device to detect exact chip type (slower)')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be done without building/flashing')
parser.add_argument('--build-parallel', type=int, default=None,
help='Number of parallel builds (default: CPU cores)')
parser.add_argument('--flash-parallel', type=int, default=8,
help='Number of parallel flash operations (default: 8)')
parser.add_argument('--strategy', choices=['build-then-flash', 'build-and-flash'],
default='build-and-flash',
help='Deployment strategy (default: build-and-flash)')
args = parser.parse_args()
# Determine parallelism
if args.build_parallel is None:
args.build_parallel = max(1, cpu_count() - 1)
# Find project directory
if args.project_dir:
project_dir = args.project_dir
else:
script_dir = os.path.dirname(os.path.abspath(__file__))
project_dir = script_dir
if not os.path.exists(os.path.join(project_dir, 'main')):
project_dir = os.path.join(os.path.expanduser('~/Code/esp32'), 'esp32-iperf')
if not os.path.exists(project_dir):
print(f"ERROR: Project directory not found: {project_dir}")
sys.exit(1)
# Create work directory for builds
work_dir = os.path.join(project_dir, '.builds')
os.makedirs(work_dir, exist_ok=True)
print(f"Using project directory: {project_dir}")
print(f"Work directory: {work_dir}")
# Detect devices
print("\nDetecting ESP32 devices...")
devices = detect_esp32.detect_esp32_devices()
if not devices:
print("No ESP32 devices detected!")
sys.exit(1)
print(f"Found {len(devices)} device(s)")
# Prepare device list with IPs
base_parts = args.start_ip.split('.')
device_list = []
for idx, device in enumerate(devices, 1):
if args.probe:
print(f"Probing {device.device}...")
chip_type = probe_chip_type(device.device)
else:
chip_type = detect_device_type(device)
ip_last = int(base_parts[3]) + idx - 1
ip = f"{base_parts[0]}.{base_parts[1]}.{base_parts[2]}.{ip_last}"
device_list.append({
'number': idx,
'port': device.device,
'chip': chip_type,
'ip': ip,
'info': device
})
# Display plan
print(f"\n{'='*70}")
print("PARALLEL FLASH PLAN")
print(f"{'='*70}")
print(f"SSID: {args.ssid}")
print(f"Strategy: {args.strategy}")
print(f"Build parallelism: {args.build_parallel}")
print(f"Flash parallelism: {args.flash_parallel}")
print()
for dev in device_list:
print(f"Device {dev['number']:2d}: {dev['port']} -> {dev['chip']:8s} -> {dev['ip']}")
if args.dry_run:
print("\nDry run - no devices will be built or flashed")
return
# Confirm
print(f"\n{'='*70}")
response = input("Proceed with parallel flashing? (yes/no): ").strip().lower()
if response != 'yes':
print("Aborted.")
return
print(f"\n{'='*70}")
print("STARTING PARALLEL DEPLOYMENT")
print(f"{'='*70}\n")
start_time = time.time()
if args.strategy == 'build-then-flash':
# Strategy 1: Build all, then flash all
print(f"Phase 1: Building {len(device_list)} configurations with {args.build_parallel} parallel builds...")
build_results = []
with ProcessPoolExecutor(max_workers=args.build_parallel) as executor:
futures = {}
for dev in device_list:
build_dir = os.path.join(work_dir, f'build_device_{dev["number"]}')
future = executor.submit(
build_firmware, dev, project_dir, build_dir, args.ssid, args.password
)
futures[future] = dev
for future in as_completed(futures):
result = future.result()
build_results.append(result)
if not result['success']:
print(f"[Device {result['device']}] ✗ Build failed: {result['error']}")
# Flash phase
successful_builds = [r for r in build_results if r['success']]
print(f"\nPhase 2: Flashing {len(successful_builds)} devices with {args.flash_parallel} parallel operations...")
flash_results = []
with ThreadPoolExecutor(max_workers=args.flash_parallel) as executor:
futures = {}
for result in successful_builds:
dev = device_list[result['device'] - 1]
build_dir = os.path.join(work_dir, f'build_device_{dev["number"]}')
future = executor.submit(flash_device, dev, build_dir)
futures[future] = dev
for future in as_completed(futures):
result = future.result()
flash_results.append(result)
if not result['success']:
print(f"[Device {result['device']}] ✗ Flash failed: {result['error']}")
# Cleanup
print("\nCleaning up build directories...")
try:
shutil.rmtree(work_dir)
except:
pass
final_results = flash_results
else:
# Strategy 2: Build and flash together (limited parallelism)
print(f"Building and flashing with {args.build_parallel} parallel operations...")
final_results = []
with ProcessPoolExecutor(max_workers=args.build_parallel) as executor:
futures = {}
for dev in device_list:
future = executor.submit(
build_and_flash, dev, project_dir, work_dir, args.ssid, args.password
)
futures[future] = dev
for future in as_completed(futures):
result = future.result()
final_results.append(result)
if not result['success']:
print(f"[Device {result['device']}] ✗ Failed: {result['error']}")
# Summary
elapsed_time = time.time() - start_time
success_count = sum(1 for r in final_results if r['success'])
failed_devices = [r['device'] for r in final_results if not r['success']]
print(f"\n{'='*70}")
print("DEPLOYMENT SUMMARY")
print(f"{'='*70}")
print(f"Successfully deployed: {success_count}/{len(device_list)} devices")
print(f"Total time: {elapsed_time:.1f} seconds ({elapsed_time/60:.1f} minutes)")
print(f"Average time per device: {elapsed_time/len(device_list):.1f} seconds")
if failed_devices:
print(f"\nFailed devices: {', '.join(map(str, failed_devices))}")
print(f"{'='*70}")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(1)
except Exception as e:
print(f"\nFATAL ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,72 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import serial
import time
from pathlib import Path
def send_cfg(port, ssid, password, dhcp, start_ip, mask, gw):
print(f"Connecting to {port}...")
with serial.Serial(port, baudrate=115200, timeout=2) as ser:
time.sleep(0.5)
ser.reset_input_buffer()
ser.write(b"CFG\n")
ser.write(f"SSID:{ssid}\n".encode())
ser.write(f"PASS:{password}\n".encode())
if dhcp == 0:
ser.write(f"IP:{start_ip}\n".encode())
ser.write(f"MASK:{mask}\n".encode())
ser.write(f"GW:{gw}\n".encode())
ser.write(f"DHCP:{dhcp}\n".encode())
ser.write(b"END\n")
time.sleep(0.3)
print("\nDevice response:")
while ser.in_waiting:
sys.stdout.write(ser.read(ser.in_waiting).decode(errors='ignore'))
sys.stdout.flush()
time.sleep(0.1)
print("\n✅ Configuration sent successfully.")
def main():
parser = argparse.ArgumentParser(description="Configure ESP32 Wi-Fi settings over serial")
parser.add_argument("--project", help="ESP-IDF project path (defaults to current working directory)")
parser.add_argument("--ssid", required=True)
parser.add_argument("--password", required=True)
parser.add_argument("--start-ip", help="Static IP address")
parser.add_argument("--mask", default="255.255.255.0")
parser.add_argument("--gw", help="Gateway address")
parser.add_argument("--dhcp", type=int, choices=[0,1], default=1)
parser.add_argument("--baud", type=int, default=460800)
parser.add_argument("--cfg-baud", type=int, default=115200)
parser.add_argument("--ports", nargs='+', help="Serial port(s), e.g., /dev/ttyUSB0 /dev/ttyUSB1")
parser.add_argument("--port", help="Single serial port (shorthand for --ports PORT)")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
# Default to current working directory
project_path = args.project or os.getcwd()
print(f"Using project directory: {project_path}")
# Resolve ports
ports = []
if args.port:
ports.append(args.port)
elif args.ports:
ports = args.ports
else:
print("❌ No serial port specified. Use --port or --ports.")
sys.exit(1)
# Apply configuration
for port in ports:
if args.dry_run:
print(f"[DRY RUN] Would send Wi-Fi config to {port}")
else:
send_cfg(port, args.ssid, args.password, args.dhcp, args.start_ip, args.mask, args.gw)
if __name__ == "__main__":
main()

View File

@ -1,198 +0,0 @@
#!/usr/bin/env python3
"""
Flash uniform firmware to all ESP32 devices, then reconfigure via serial
This is much faster than building unique firmwares for each device
"""
import os
import sys
import subprocess
import argparse
import glob
from pathlib import Path
import time
def detect_devices():
"""Detect all ESP32 devices"""
devices = sorted(glob.glob('/dev/ttyUSB*'))
return devices
def build_firmware(project_dir):
"""Build the firmware once"""
print("=" * 60)
print("Building firmware (one time)...")
print("=" * 60)
result = subprocess.run(
['idf.py', 'build'],
cwd=project_dir,
capture_output=False
)
if result.returncode != 0:
print("✗ Build failed!")
return False
print("✓ Build complete")
return True
def flash_device(port, project_dir):
"""Flash a single device"""
try:
result = subprocess.run(
['idf.py', '-p', port, 'flash'],
cwd=project_dir,
capture_output=True,
text=True,
timeout=120
)
return result.returncode == 0
except Exception as e:
return False
def flash_all_devices(devices, project_dir):
"""Flash all devices with the same firmware"""
print(f"\nFlashing {len(devices)} devices...")
success_count = 0
for idx, dev in enumerate(devices, 1):
print(f"[{idx:2d}/{len(devices)}] Flashing {dev}...", end='', flush=True)
if flash_device(dev, project_dir):
print("")
success_count += 1
else:
print("")
print(f"\nFlashed: {success_count}/{len(devices)}")
return success_count
def reconfigure_devices(ssid, password, start_ip, gateway="192.168.1.1"):
"""Reconfigure devices using the reconfig script"""
script_path = os.path.join(os.path.dirname(__file__), 'reconfig_simple.py')
if not os.path.exists(script_path):
print(f"Error: {script_path} not found!")
return False
print("\n" + "=" * 60)
print("Reconfiguring WiFi settings via serial...")
print("=" * 60)
cmd = [
'python3', script_path,
'-s', ssid,
'-p', password,
'--start-ip', start_ip,
'-g', gateway
]
result = subprocess.run(cmd)
return result.returncode == 0
def main():
parser = argparse.ArgumentParser(
description='Flash and configure all ESP32 devices',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
This script:
1. Builds firmware ONCE
2. Flashes the SAME firmware to all devices (fast!)
3. Reconfigures each device via serial with unique IP
Much faster than building 32 different firmwares!
Examples:
# Basic usage
%(prog)s --ssid MyWiFi --password mypass
# Custom IP range
%(prog)s --ssid MyWiFi --password mypass --start-ip 192.168.1.100
# Build only (no flash)
%(prog)s --build-only
# Reconfigure only (no flash)
%(prog)s --reconfig-only --ssid MyWiFi --password mypass
"""
)
parser.add_argument('--ssid', help='WiFi SSID')
parser.add_argument('--password', help='WiFi password')
parser.add_argument('--start-ip', default='192.168.1.50',
help='Starting IP address (default: 192.168.1.50)')
parser.add_argument('--gateway', default='192.168.1.1',
help='Gateway IP (default: 192.168.1.1)')
parser.add_argument('--project-dir', default=None,
help='ESP32 project directory (default: current dir)')
parser.add_argument('--build-only', action='store_true',
help='Only build, do not flash')
parser.add_argument('--reconfig-only', action='store_true',
help='Only reconfigure, do not build/flash')
parser.add_argument('--skip-build', action='store_true',
help='Skip build, use existing firmware')
args = parser.parse_args()
# Determine project directory
if args.project_dir:
project_dir = args.project_dir
else:
project_dir = os.path.dirname(os.path.abspath(__file__))
if not os.path.exists(os.path.join(project_dir, 'CMakeLists.txt')):
print(f"Error: Not an ESP-IDF project directory: {project_dir}")
return 1
# Detect devices
devices = detect_devices()
if not devices and not args.build_only:
print("Error: No devices found!")
return 1
print(f"Found {len(devices)} device(s)")
# Reconfigure only mode
if args.reconfig_only:
if not args.ssid or not args.password:
print("Error: --ssid and --password required for reconfigure mode")
return 1
return 0 if reconfigure_devices(args.ssid, args.password, args.start_ip, args.gateway) else 1
# Build firmware
if not args.skip_build:
if not build_firmware(project_dir):
return 1
if args.build_only:
print("\n Build complete. Use --skip-build to flash later.")
return 0
# Flash all devices
flash_count = flash_all_devices(devices, project_dir)
if flash_count == 0:
print("✗ No devices flashed successfully")
return 1
# Reconfigure if credentials provided
if args.ssid and args.password:
print("\nWaiting for devices to boot...")
time.sleep(5)
if not reconfigure_devices(args.ssid, args.password, args.start_ip, args.gateway):
print("✗ Reconfiguration failed")
return 1
else:
print("\n" + "=" * 60)
print("Flashing complete!")
print("=" * 60)
print("\nTo configure WiFi settings, run:")
print(f" python3 reconfig_simple.py -s YourSSID -p YourPassword --start-ip {args.start_ip}")
print("\n✓ Done!")
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,68 +1,249 @@
#!/usr/bin/env python3
"""
Unified udev rules generator for ESP32 devices.
Combines functionality from gen_udev_rules.py and append_new_rules.py.
Features:
- Generate rules from scratch (full scan)
- Append new rules to existing file (incremental update)
- Uses ID_PATH for stable device identification
- Sorts devices by physical topology for consistent ordering
"""
import os
import pyudev
import re
import sys
import subprocess
from pathlib import Path
from serial.tools import list_ports
def generate_rules():
context = pyudev.Context()
class Colors:
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
CYAN = '\033[96m'
RESET = '\033[0m'
# Find all TTY devices driven by usb-serial drivers (CP210x, FTDI, etc.)
# Default paths
DEFAULT_RULES_FILE = "/etc/udev/rules.d/99-esp32-stable.rules"
TEMP_RULES_FILE = "new_rules.part"
def get_existing_rules(rules_path):
"""
Parse existing rules file to extract:
- Set of ID_PATH values already mapped
- Maximum port number used
Returns: (known_paths, max_port)
"""
known_paths = set()
max_port = 0
if not os.path.exists(rules_path):
return known_paths, 0
regex_path = re.compile(r'ENV\{ID_PATH\}=="([^"]+)"')
regex_port = re.compile(r'esp_port_(\d+)')
try:
with open(rules_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
path_match = regex_path.search(line)
port_match = regex_port.search(line)
if path_match:
known_paths.add(path_match.group(1))
if port_match:
port_num = int(port_match.group(1))
if port_num > max_port:
max_port = port_num
except Exception as e:
print(f"{Colors.RED}Error reading rules file: {e}{Colors.RESET}")
return known_paths, 0
return known_paths, max_port
def scan_usb_devices():
"""
Scan all USB serial devices and return list of (device, id_path, location) tuples.
Uses udevadm to get stable ID_PATH identifiers.
"""
devices = []
for device in context.list_devices(subsystem='tty'):
if 'ID_VENDOR_ID' in device.properties and 'ID_MODEL_ID' in device.properties:
# We filter for USB serial devices only
parent = device.find_parent('usb', 'usb_interface')
if parent:
devices.append(device)
# Sort by their physical path so they are ordered by hub port
# The 'DEVPATH' usually looks like .../usb1/1-2/1-2.3/1-2.3.4...
devices.sort(key=lambda x: x.properties.get('DEVPATH', ''))
# Get all USB serial devices
usb_devices = list(list_ports.grep("USB|ACM|CP210|FT232"))
print(f"# Detected {len(devices)} devices. Generating rules...\n")
if not usb_devices:
return devices
# Sort by physical location for consistent ordering
usb_devices.sort(key=lambda x: x.location if x.location else x.device)
for dev in usb_devices:
try:
# Get ID_PATH via udevadm (most stable identifier)
cmd = ['udevadm', 'info', '--name', dev.device, '--query=property']
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
if proc.returncode != 0:
continue
props = dict(line.split('=', 1) for line in proc.stdout.splitlines() if '=' in line)
id_path = props.get('ID_PATH', '')
if not id_path:
continue
devices.append({
'device': dev.device,
'id_path': id_path,
'location': dev.location or '',
'serial': props.get('ID_SERIAL_SHORT', '')
})
except Exception as e:
print(f"{Colors.YELLOW}Warning: Could not inspect {dev.device}: {e}{Colors.RESET}")
continue
return devices
def generate_rules(devices, start_port=1):
"""
Generate udev rules for the given devices.
Returns list of rule strings.
"""
rules = []
hub_counter = 1
port_counter = 1
last_parent_path = None
port_num = start_port
for dev in devices:
# Get the unique physical path identifier (KERNELS)
# We need the parent USB interface kernel name (e.g., '1-1.2:1.0')
parent = dev.find_parent('usb', 'usb_interface')
if not parent: continue
kernels_path = parent.device_path.split('/')[-1]
# Simple heuristic to detect a new hub (big jump in path length or numbering)
# You might need to manually tweak the hub numbers in the file later.
# Generate the rule
# KERNELS matches the physical port.
# SYMLINK creates the static alias.
rule = f'SUBSYSTEM=="tty", KERNELS=="{kernels_path}", SYMLINK+="esp_port_{len(rules)+1:02d}"'
symlink = f"esp_port_{port_num}"
rule = f'SUBSYSTEM=="tty", ENV{{ID_PATH}}=="{dev["id_path"]}", SYMLINK+="{symlink}"'
rules.append(rule)
print(f"Mapped {dev.device_node} ({kernels_path}) -> /dev/esp_port_{len(rules):02d}")
port_num += 1
# Write to file
with open("99-esp32-static.rules", "w") as f:
f.write("# Persistent USB Serial mapping for ESP32 Hubs\n")
f.write("# Generated automatically. Do not edit unless topology changes.\n\n")
for r in rules:
f.write(r + "\n")
return rules
print(f"\n{'-'*60}")
print(f"SUCCESS: Generated {len(rules)} rules in '99-esp32-static.rules'.")
print(f"To install:\n 1. Review the file to ensure order is correct.")
print(f" 2. sudo cp 99-esp32-static.rules /etc/udev/rules.d/")
print(f" 3. sudo udevadm control --reload-rules")
print(f" 4. sudo udevadm trigger")
print(f"{'-'*60}")
def main():
import argparse
parser = argparse.ArgumentParser(
description='Generate or update udev rules for ESP32 devices',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate rules from scratch (overwrites existing)
%(prog)s --full
# Append only new devices to existing rules
%(prog)s --append
# Dry run (show what would be generated)
%(prog)s --append --dry-run
"""
)
parser.add_argument('--full', action='store_true',
help='Generate complete rules file from scratch')
parser.add_argument('--append', action='store_true',
help='Append only new devices to existing rules')
parser.add_argument('--rules-file', default=DEFAULT_RULES_FILE,
help=f'Path to udev rules file (default: {DEFAULT_RULES_FILE})')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be done without writing files')
parser.add_argument('--output', help='Write to custom file instead of rules file')
args = parser.parse_args()
# Default to append mode if neither specified
if not args.full and not args.append:
args.append = True
print(f"{Colors.BLUE}{'='*60}{Colors.RESET}")
print(f" ESP32 Udev Rules Generator")
print(f"{Colors.BLUE}{'='*60}{Colors.RESET}\n")
# Scan connected devices
print(f"{Colors.CYAN}Scanning USB topology...{Colors.RESET}")
all_devices = scan_usb_devices()
if not all_devices:
print(f"{Colors.RED}No USB serial devices found.{Colors.RESET}")
return 1
print(f"Found {len(all_devices)} USB serial devices\n")
if args.full:
# Generate complete rules file
print(f"{Colors.CYAN}Generating complete rules file...{Colors.RESET}")
rules = generate_rules(all_devices, start_port=1)
output_file = args.output or args.rules_file
print(f"\n{'Physical Path':<20} | {'Current Dev':<15} | {'Assigned Symlink'}")
print("-" * 65)
for i, dev in enumerate(all_devices):
port_num = i + 1
symlink = f"esp_port_{port_num}"
print(f"{dev['location']:<20} | {dev['device']:<15} | {symlink}")
if not args.dry_run:
rules_content = "# Auto-generated by gen_udev_rules.py\n"
rules_content += "# Stable port mapping based on USB physical topology (ID_PATH)\n\n"
rules_content += "\n".join(rules) + "\n"
if output_file == args.rules_file and os.geteuid() != 0:
# Write to temp file for user to copy
temp_file = "99-esp32-stable.rules"
with open(temp_file, 'w') as f:
f.write(rules_content)
print(f"\n{Colors.YELLOW}Rules written to: {temp_file}{Colors.RESET}")
print(f"To install, run:")
print(f" {Colors.GREEN}sudo cp {temp_file} {args.rules_file}{Colors.RESET}")
else:
with open(output_file, 'w') as f:
f.write(rules_content)
print(f"\n{Colors.GREEN}Rules written to: {output_file}{Colors.RESET}")
else:
print(f"\n{Colors.YELLOW}DRY RUN: Would generate {len(rules)} rules{Colors.RESET}")
else: # append mode
# Read existing rules
known_paths, max_port = get_existing_rules(args.rules_file)
print(f"Existing rules: {len(known_paths)} devices mapped (Max Port: {max_port})")
# Find new devices
new_devices = [d for d in all_devices if d['id_path'] not in known_paths]
if not new_devices:
print(f"\n{Colors.GREEN}All connected devices are already mapped. No changes needed.{Colors.RESET}")
return 0
print(f"\n{Colors.CYAN}Found {len(new_devices)} UNMAPPED devices:{Colors.RESET}")
print(f"{'Physical Path':<20} | {'Current Dev':<15} | {'Assigned Symlink'}")
print("-" * 65)
new_rules = generate_rules(new_devices, start_port=max_port + 1)
for i, dev in enumerate(new_devices):
port_num = max_port + 1 + i
symlink = f"esp_port_{port_num:02d}"
print(f"{dev['location']:<20} | {dev['device']:<15} | {symlink}")
if not args.dry_run:
# Write new rules to temp file
with open(TEMP_RULES_FILE, 'w') as f:
f.write("\n# --- Added by gen_udev_rules.py ---\n")
f.write("\n".join(new_rules) + "\n")
print(f"\n{Colors.CYAN}New rules saved to: {TEMP_RULES_FILE}{Colors.RESET}")
print(f"To apply, run:")
print(f" {Colors.GREEN}cat {TEMP_RULES_FILE} | sudo tee -a {args.rules_file}{Colors.RESET}")
print(f" {Colors.GREEN}sudo udevadm control --reload-rules && sudo udevadm trigger{Colors.RESET}")
else:
print(f"\n{Colors.YELLOW}DRY RUN: Would append {len(new_rules)} rules{Colors.RESET}")
print(f"\n{Colors.BLUE}{'='*60}{Colors.RESET}")
return 0
if __name__ == '__main__':
# Requires 'pyudev'. Install with: sudo dnf install python3-pyudev (or pip install pyudev)
try:
import pyudev
generate_rules()
except ImportError:
print("Error: This script requires 'pyudev'.")
print("Install it via: pip install pyudev")
sys.exit(main())

188
leddiff.txt Normal file
View File

@ -0,0 +1,188 @@
diff --git a/components/status_led/status_led.c b/components/status_led/status_led.c
index 5a34d7e..6dd6d09 100644
--- a/components/status_led/status_led.c
+++ b/components/status_led/status_led.c
@@ -30,7 +30,6 @@
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
-
#include "status_led.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -38,87 +37,56 @@
#include "led_strip.h"
#include "esp_log.h"
-static const char *TAG = "status_led";
+static const char *TAG = "STATUS_LED"; // Added TAG for logging
static led_strip_handle_t s_led_strip = NULL;
static bool s_is_rgb = false;
static int s_gpio_pin = -1;
static volatile led_state_t s_current_state = LED_STATE_NO_CONFIG;
-// Helper to set color safely
static void set_color(uint8_t r, uint8_t g, uint8_t b) {
if (s_is_rgb && s_led_strip) {
led_strip_set_pixel(s_led_strip, 0, r, g, b);
led_strip_refresh(s_led_strip);
} else if (!s_is_rgb && s_gpio_pin >= 0) {
- // Simple LED logic: If any color is requested, turn ON.
- // NOTE: If your LED is active-low (VCC->LED->Pin), invert this to !((r+g+b)>0)
- gpio_set_level(s_gpio_pin, (r + g + b) > 0 ? 1 : 0);
+ gpio_set_level(s_gpio_pin, (r+g+b) > 0);
}
}
static void led_task(void *arg) {
int toggle = 0;
-
- // --- Startup Diagnostic Sequence ---
- // Cycle R -> G -> B to prove hardware is working
- ESP_LOGW(TAG, "Running LED Diagnostic Sequence on GPIO %d...", s_gpio_pin);
- set_color(50, 0, 0); // Red
- vTaskDelay(pdMS_TO_TICKS(300));
- set_color(0, 50, 0); // Green
- vTaskDelay(pdMS_TO_TICKS(300));
- set_color(0, 0, 50); // Blue
- vTaskDelay(pdMS_TO_TICKS(300));
- set_color(0, 0, 0); // Off
- vTaskDelay(pdMS_TO_TICKS(100));
-
while (1) {
- // Brightness set to 30-50 (out of 255) for visibility
switch (s_current_state) {
- case LED_STATE_NO_CONFIG: // Yellow (Solid RGB / Blink Simple)
- if (s_is_rgb) {
- set_color(40, 30, 0);
- vTaskDelay(pdMS_TO_TICKS(1000));
- } else {
- set_color(1,1,1); vTaskDelay(pdMS_TO_TICKS(100));
- set_color(0,0,0); vTaskDelay(pdMS_TO_TICKS(100));
- }
+ case LED_STATE_NO_CONFIG: // Yellow
+ if (s_is_rgb) { set_color(25, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000)); }
+ else { set_color(1,1,1); vTaskDelay(100); set_color(0,0,0); vTaskDelay(100); }
break;
- case LED_STATE_WAITING: // Blue Blink
- set_color(0, 0, toggle ? 50 : 0);
- toggle = !toggle;
+ // ... rest of cases identical to your code ...
+ case LED_STATE_WAITING:
+ set_color(0, 0, toggle ? 50 : 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(500));
break;
- case LED_STATE_CONNECTED: // Green Solid
- set_color(0, 30, 0);
- vTaskDelay(pdMS_TO_TICKS(1000));
+ case LED_STATE_CONNECTED:
+ set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
break;
- case LED_STATE_MONITORING: // Cyan Solid
- set_color(0, 30, 30);
- vTaskDelay(pdMS_TO_TICKS(1000));
+ case LED_STATE_MONITORING:
+ set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
break;
- case LED_STATE_TRANSMITTING: // Purple Fast Flash
- set_color(toggle ? 40 : 0, 0, toggle ? 40 : 0);
- toggle = !toggle;
- vTaskDelay(pdMS_TO_TICKS(100));
+ case LED_STATE_TRANSMITTING:
+ set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
+ vTaskDelay(pdMS_TO_TICKS(50));
break;
- case LED_STATE_TRANSMITTING_SLOW: // Purple Slow Pulse
- set_color(toggle ? 40 : 0, 0, toggle ? 40 : 0);
- toggle = !toggle;
- vTaskDelay(pdMS_TO_TICKS(500));
+ case LED_STATE_TRANSMITTING_SLOW:
+ set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
+ vTaskDelay(pdMS_TO_TICKS(250));
break;
- case LED_STATE_STALLED: // Red/Purple Solid
- set_color(50, 0, 20);
- vTaskDelay(pdMS_TO_TICKS(1000));
+ case LED_STATE_STALLED:
+ set_color(50, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
break;
- case LED_STATE_FAILED: // Red Blink
- set_color(toggle ? 50 : 0, 0, 0);
- toggle = !toggle;
+ case LED_STATE_FAILED:
+ set_color(toggle ? 50 : 0, 0, 0); toggle = !toggle;
vTaskDelay(pdMS_TO_TICKS(200));
break;
- default:
- vTaskDelay(pdMS_TO_TICKS(100));
- break;
}
}
}
@@ -127,41 +95,27 @@ void status_led_init(int gpio_pin, bool is_rgb_strip) {
s_gpio_pin = gpio_pin;
s_is_rgb = is_rgb_strip;
- ESP_LOGI(TAG, "Initializing Status LED: GPIO=%d, Type=%s",
- gpio_pin, is_rgb_strip ? "RGB Strip (WS2812)" : "Simple GPIO");
+ // --- DIAGNOSTIC LOG ---
+ ESP_LOGW(TAG, "Initializing LED on GPIO %d (RGB: %d)", gpio_pin, is_rgb_strip);
if (s_is_rgb) {
- led_strip_config_t s_cfg = {
- .strip_gpio_num = gpio_pin,
- .max_leds = 1,
- .led_pixel_format = LED_PIXEL_FORMAT_GRB,
- .led_model = LED_MODEL_WS2812,
- .flags.invert_out = false,
- };
- led_strip_rmt_config_t r_cfg = {
- .resolution_hz = 10 * 1000 * 1000,
- .flags.with_dma = false,
- };
+ led_strip_config_t s_cfg = { .strip_gpio_num = gpio_pin, .max_leds = 1 };
+ led_strip_rmt_config_t r_cfg = { .resolution_hz = 10 * 1000 * 1000 };
esp_err_t ret = led_strip_new_rmt_device(&s_cfg, &r_cfg, &s_led_strip);
if (ret != ESP_OK) {
- ESP_LOGE(TAG, "Failed to create RMT LED strip: %s", esp_err_to_name(ret));
- return;
+ ESP_LOGE(TAG, "RMT Device Init Failed: %s", esp_err_to_name(ret));
+ } else {
+ ESP_LOGI(TAG, "RMT Device Init Success");
+ led_strip_clear(s_led_strip);
}
- led_strip_clear(s_led_strip);
} else {
gpio_reset_pin(gpio_pin);
gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT);
- gpio_set_level(gpio_pin, 0);
}
-
xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL);
}
-void status_led_set_state(led_state_t state) {
- s_current_state = state;
-}
-
-led_state_t status_led_get_state(void) {
- return s_current_state;
-}
+// ... Setters/Getters ...
+void status_led_set_state(led_state_t state) { s_current_state = state; }
+led_state_t status_led_get_state(void) { return s_current_state; }
diff --git a/main/board_config.h b/main/board_config.h
index 6e4aa28..05d7726 100644
--- a/main/board_config.h
+++ b/main/board_config.h
@@ -42,7 +42,7 @@
// ============================================================================
// ESP32-C5 (DevKitC-1) 3.3V VCC Pin 1 GND PIN 15
// ============================================================================
- #define RGB_LED_GPIO 8 // Common addressable LED pin for C5
+ #define RGB_LED_GPIO 27 // Common addressable LED pin for C5
#define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_24
#define GPS_RX_PIN GPIO_NUM_23

View File

@ -1,45 +1,81 @@
/*
* board_config.h
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H
#include "sdkconfig.h"
#include "driver/gpio.h"
#if defined (CONFIG_IDF_TARGET_ESP32C5)
// ============================================================================
// ESP32-C5 (DevKitC-1)
// ESP32-C5 (DevKitC-1) 3.3V VCC Pin 1 GND PIN 15
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32C5
#define RGB_LED_GPIO 8 // Common addressable LED pin for C5
#define RGB_LED_GPIO 27 // Common addressable LED pin for C5
#define HAS_RGB_LED 1
#endif
#define GPS_TX_PIN GPIO_NUM_24
#define GPS_RX_PIN GPIO_NUM_23
#define GPS_PPS_PIN GPIO_NUM_25
#elif defined (CONFIG_IDF_TARGET_ESP32S3)
// ============================================================================
// ESP32-S3 (DevKitC-1)
// Most S3 DevKits use GPIO 48 for the addressable RGB LED.
// If yours uses GPIO 38, change this value.
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32S3
// Most S3 DevKits use GPIO 48 for the addressable RGB LED.
// If yours uses GPIO 38, change this value.
#define RGB_LED_GPIO 48
#define HAS_RGB_LED 1
#endif
#define RGB_LED_GPIO 48
#define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_5
#define GPS_RX_PIN GPIO_NUM_4
#define GPS_PPS_PIN GPIO_NUM_6
#elif defined (CONFIG_IDF_TARGET_ESP32)
// ============================================================================
// ESP32 (Original / Standard)
// Standard ESP32 DevKits usually have a single blue LED on GPIO 2.
// They rarely have an addressable RGB LED built-in.
// ============================================================================
#ifdef CONFIG_IDF_TARGET_ESP32
// Standard ESP32 DevKits usually have a single blue LED on GPIO 2.
// They rarely have an addressable RGB LED built-in.
#define RGB_LED_GPIO 2
#define HAS_RGB_LED 0
#endif
// ============================================================================
// Fallbacks (Prevent Compilation Errors)
// ============================================================================
#ifndef RGB_LED_GPIO
#define RGB_LED_GPIO 2
#endif
#ifndef HAS_RGB_LED
#define HAS_RGB_LED 0
#define RGB_LED_GPIO 2 // Standard Blue LED
#define HAS_RGB_LED 0 // Not RGB
#define GPS_TX_PIN GPIO_NUM_17
#define GPS_RX_PIN GPIO_NUM_16
#define GPS_PPS_PIN GPIO_NUM_4
#else
// Fallback
#define RGB_LED_GPIO 8
#define HAS_RGB_LED 1
#define GPS_TX_PIN GPIO_NUM_1
#define GPS_RX_PIN GPIO_NUM_3
#define GPS_PPS_PIN GPIO_NUM_5
#endif
#endif // BOARD_CONFIG_H

17
main/idf_component.yml Normal file
View File

@ -0,0 +1,17 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/led_strip: ^2.5.3

View File

@ -1,33 +1,115 @@
/*
* main.c
*
* Copyright (c) 2025 Umber Networks & Robert McMahon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include <sys/time.h> // Added for gettimeofday
#include <time.h> // Added for time structs
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_vfs_dev.h"
#include "driver/uart.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_netif.h"
#include "esp_event.h"
// Components
#include "status_led.h"
#include "board_config.h"
#include "gps_sync.h"
#include "wifi_controller.h"
#include "wifi_cfg.h"
#include "app_console.h"
#include "iperf.h"
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
#include "csi_log.h"
#include "csi_manager.h"
#endif
#define APP_VERSION "2.0.0-SHELL"
#define APP_VERSION "2.1.0-CONSOLE-DEBUG"
static const char *TAG = "MAIN";
// --- Global Prompt Buffer (Mutable) ---
static char s_cli_prompt[32] = "esp32> ";
// --- Custom Log Formatter (Epoch Timestamp) ---
// This ensures logs match tcpdump/wireshark format: [Seconds.Microseconds]
static int custom_log_vprintf(const char *fmt, va_list args) {
struct timeval tv;
gettimeofday(&tv, NULL);
// Print [1766437791.123456] prefix
printf("[%ld.%06ld] ", (long)tv.tv_sec, (long)tv.tv_usec);
return vprintf(fmt, args);
}
// --- Prompt Updater ---
void app_console_update_prompt(void) {
bool dirty = false;
if (wifi_ctl_param_is_unsaved()) dirty = true;
if (iperf_param_is_unsaved()) dirty = true;
if (dirty) {
snprintf(s_cli_prompt, sizeof(s_cli_prompt), "esp32*> ");
} else {
snprintf(s_cli_prompt, sizeof(s_cli_prompt), "esp32> ");
}
}
// --- Helper: Check NVS for GPS Enable ---
static bool is_gps_enabled(void) {
nvs_handle_t h;
uint8_t val = 1; // Default to Enabled (1)
// Check 'storage' namespace first (where iperf/system settings live)
if (nvs_open("storage", NVS_READONLY, &h) == ESP_OK) {
if (nvs_get_u8(h, "gps_enabled", &val) != ESP_OK) {
val = 1; // Key missing = Enabled
}
nvs_close(h);
}
return (val != 0);
}
// --- System Commands ---
static int cmd_restart(int argc, char **argv) {
@ -69,42 +151,78 @@ void app_main(void) {
}
ESP_ERROR_CHECK(ret);
// 2. Initialize Netif & Event Loop
// 2. Register Custom Log Formatter (Epoch Time)
// Must be done before any logs are printed to ensure consistency
esp_log_set_vprintf(custom_log_vprintf);
// 3. Initialize Netif & Event Loop
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// 3. Hardware Init
// -------------------------------------------------------------
// GPS Initialization (Conditional)
// -------------------------------------------------------------
if (is_gps_enabled()) {
const gps_sync_config_t gps_cfg = {
.uart_port = UART_NUM_1,
.tx_pin = GPS_TX_PIN,
.rx_pin = GPS_RX_PIN,
.pps_pin = GPS_PPS_PIN,
};
gps_sync_init(&gps_cfg, true);
} else {
ESP_LOGW(TAG, "GPS initialization skipped (Disabled in NVS)");
}
// Hardware Init
status_led_init(RGB_LED_GPIO, HAS_RGB_LED);
status_led_set_state(LED_STATE_FAILED); // Force Red Blink
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
ESP_ERROR_CHECK(csi_log_init());
csi_mgr_init();
#endif
// 4. Initialize WiFi Controller (Loads config from NVS automatically)
// 5. Initialize WiFi Controller & iPerf
wifi_ctl_init();
iperf_param_init();
// 5. Initialize Console
// 6. Initialize Console (REPL)
ESP_LOGI(TAG, "Initializing console REPL...");
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
// This prompt is the anchor for your Python script
repl_config.prompt = "esp32> ";
repl_config.prompt = s_cli_prompt;
repl_config.max_cmdline_length = 1024;
// Install UART driver for Console (Standard IO)
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
esp_err_t repl_init_err = esp_console_new_repl_uart(&hw_config, &repl_config, &repl);
if (repl_init_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to create console REPL: %s", esp_err_to_name(repl_init_err));
esp_restart();
}
ESP_LOGI(TAG, "Console REPL object created successfully");
// 6. Register Commands
// 7. Register Commands
ESP_LOGI(TAG, "Registering console commands...");
register_system_common();
app_console_register_commands();
ESP_LOGI(TAG, "Console commands registered");
// 7. Start Shell
// 8. Initial Prompt State Check
app_console_update_prompt();
// 9. Start Shell
ESP_LOGI(TAG, "Starting console REPL...");
printf("\n ==================================================\n");
printf(" | ESP32 iPerf Shell - Ready |\n");
printf(" | Type 'help' for commands |\n");
printf(" ==================================================\n");
fflush(stdout);
// This function runs the REPL loop and does not return
ESP_ERROR_CHECK(esp_console_start_repl(repl));
esp_err_t repl_err = esp_console_start_repl(repl);
if (repl_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start console REPL: %s", esp_err_to_name(repl_err));
esp_restart();
}
// Note: esp_console_start_repl() blocks and never returns on success
// so code below would never execute
}

View File

@ -1,244 +0,0 @@
#!/usr/bin/env python3
"""
Map ESP32 USB ports to IP addresses
Creates and manages mapping between /dev/ttyUSB* and assigned IPs
"""
import serial.tools.list_ports
import argparse
import json
import glob
import re
from pathlib import Path
class USBIPMapper:
def __init__(self, start_ip="192.168.1.51", config_file="usb_ip_map.json"):
self.start_ip = start_ip
self.config_file = config_file
self.mapping = {}
def get_ip_for_index(self, index):
"""Calculate IP address for a given index"""
ip_parts = self.start_ip.split('.')
base_ip = int(ip_parts[3])
ip_parts[3] = str(base_ip + index)
return '.'.join(ip_parts)
def extract_usb_number(self, port):
"""Extract number from /dev/ttyUSBX"""
match = re.search(r'ttyUSB(\d+)', port)
if match:
return int(match.group(1))
return None
def detect_devices(self):
"""Detect all ESP32 USB devices and create mapping"""
devices = sorted(glob.glob('/dev/ttyUSB*'))
print(f"\n{'='*70}")
print(f"ESP32 USB to IP Address Mapping")
print(f"{'='*70}")
print(f"Start IP: {self.start_ip}")
print(f"Detected {len(devices)} USB device(s)\n")
self.mapping = {}
for idx, port in enumerate(devices):
usb_num = self.extract_usb_number(port)
ip = self.get_ip_for_index(idx)
# Get device info
try:
ports = serial.tools.list_ports.comports()
device_info = next((p for p in ports if p.device == port), None)
if device_info:
serial_num = device_info.serial_number or "Unknown"
description = device_info.description or "Unknown"
else:
serial_num = "Unknown"
description = "Unknown"
except:
serial_num = "Unknown"
description = "Unknown"
self.mapping[port] = {
'index': idx,
'usb_number': usb_num,
'ip': ip,
'serial': serial_num,
'description': description
}
print(f"[{idx:2d}] {port:14s}{ip:15s} (USB{usb_num}, SN: {serial_num})")
print(f"\n{'='*70}")
print(f"Total: {len(devices)} devices mapped")
print(f"IP Range: {self.mapping[devices[0]]['ip']} - {self.mapping[devices[-1]]['ip']}" if devices else "")
print(f"{'='*70}\n")
return self.mapping
def save_mapping(self):
"""Save mapping to JSON file"""
with open(self.config_file, 'w') as f:
json.dump(self.mapping, f, indent=2)
print(f"✓ Mapping saved to {self.config_file}")
def load_mapping(self):
"""Load mapping from JSON file"""
try:
with open(self.config_file, 'r') as f:
self.mapping = json.load(f)
print(f"✓ Mapping loaded from {self.config_file}")
return self.mapping
except FileNotFoundError:
print(f"✗ No saved mapping found at {self.config_file}")
return {}
def get_ip(self, port):
"""Get IP address for a specific USB port"""
if port in self.mapping:
return self.mapping[port]['ip']
return None
def get_port(self, ip):
"""Get USB port for a specific IP address"""
for port, info in self.mapping.items():
if info['ip'] == ip:
return port
return None
def print_mapping(self):
"""Print current mapping"""
if not self.mapping:
print("No mapping loaded. Run with --detect first.")
return
print(f"\n{'='*70}")
print(f"Current USB to IP Mapping")
print(f"{'='*70}")
for port, info in sorted(self.mapping.items(), key=lambda x: x[1]['index']):
print(f"[{info['index']:2d}] {port:14s}{info['ip']:15s} (USB{info['usb_number']})")
print(f"{'='*70}\n")
def export_bash_script(self, filename="usb_ip_vars.sh"):
"""Export mapping as bash variables"""
with open(filename, 'w') as f:
f.write("#!/bin/bash\n")
f.write("# USB to IP mapping - Auto-generated\n\n")
# Create associative array
f.write("declare -A USB_TO_IP\n")
for port, info in self.mapping.items():
f.write(f"USB_TO_IP[{port}]=\"{info['ip']}\"\n")
f.write("\n# Create reverse mapping\n")
f.write("declare -A IP_TO_USB\n")
for port, info in self.mapping.items():
f.write(f"IP_TO_USB[{info['ip']}]=\"{port}\"\n")
f.write("\n# Helper functions\n")
f.write("get_ip_for_usb() { echo \"${USB_TO_IP[$1]}\"; }\n")
f.write("get_usb_for_ip() { echo \"${IP_TO_USB[$1]}\"; }\n")
print(f"✓ Bash script exported to {filename}")
print(f" Usage: source {filename} && get_ip_for_usb /dev/ttyUSB0")
def main():
parser = argparse.ArgumentParser(
description='Map ESP32 USB ports to IP addresses',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Detect devices and create mapping
%(prog)s --detect
# Detect and save to file
%(prog)s --detect --save
# Load saved mapping and display
%(prog)s --load --print
# Get IP for specific USB port
%(prog)s --load --port /dev/ttyUSB5
# Get USB port for specific IP
%(prog)s --load --ip 192.168.1.55
# Export as bash script
%(prog)s --load --export
# Use custom IP range
%(prog)s --detect --start-ip 10.0.0.100
"""
)
parser.add_argument('--detect', action='store_true',
help='Detect USB devices and create mapping')
parser.add_argument('--save', action='store_true',
help='Save mapping to file')
parser.add_argument('--load', action='store_true',
help='Load mapping from file')
parser.add_argument('--print', action='store_true',
help='Print current mapping')
parser.add_argument('--start-ip', default='192.168.1.51',
help='Starting IP address (default: 192.168.1.51)')
parser.add_argument('--config', default='usb_ip_map.json',
help='Config file path (default: usb_ip_map.json)')
parser.add_argument('--port', metavar='PORT',
help='Get IP for specific USB port (e.g., /dev/ttyUSB5)')
parser.add_argument('--ip', metavar='IP',
help='Get USB port for specific IP address')
parser.add_argument('--export', action='store_true',
help='Export mapping as bash script')
args = parser.parse_args()
mapper = USBIPMapper(start_ip=args.start_ip, config_file=args.config)
# Detect devices
if args.detect:
mapper.detect_devices()
if args.save:
mapper.save_mapping()
# Load mapping
if args.load:
mapper.load_mapping()
# Print mapping
if args.print:
mapper.print_mapping()
# Query specific port
if args.port:
if not mapper.mapping:
mapper.load_mapping()
ip = mapper.get_ip(args.port)
if ip:
print(f"{args.port}{ip}")
else:
print(f"Port {args.port} not found in mapping")
# Query specific IP
if args.ip:
if not mapper.mapping:
mapper.load_mapping()
port = mapper.get_port(args.ip)
if port:
print(f"{args.ip}{port}")
else:
print(f"IP {args.ip} not found in mapping")
# Export bash script
if args.export:
if not mapper.mapping:
mapper.load_mapping()
mapper.export_bash_script()
# Default: detect and print
if not any([args.detect, args.load, args.print, args.port, args.ip, args.export]):
mapper.detect_devices()
if __name__ == '__main__':
main()

View File

@ -1,319 +0,0 @@
#!/usr/bin/env python3
"""
ESP32 Mass Deployment Tool (Fixed for Parallel Flashing & Path Issues)
Uses esptool.py from the build directory to resolve relative paths correctly.
"""
import os
import sys
import subprocess
import glob
import time
import argparse
import serial
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
class DeviceDeployer:
def __init__(self, project_dir, ssid, password, start_ip="192.168.1.51",
netmask="255.255.255.0", gateway="192.168.1.1",
baud_rate=460800, max_retries=2, verify_ping=True,
num_devices=None, verbose=False, parallel=True):
self.project_dir = Path(project_dir).resolve() # Absolute path is safer
self.build_dir = self.project_dir / 'build'
self.ssid = ssid
self.password = password
self.start_ip = start_ip
self.netmask = netmask
self.gateway = gateway
self.baud_rate = baud_rate
self.max_retries = max_retries
self.verify_ping = verify_ping
self.num_devices = num_devices
self.verbose = verbose
self.parallel = parallel
self.config_mode = (self.ssid is not None and self.password is not None)
if self.start_ip:
ip_parts = start_ip.split('.')
self.ip_base = '.'.join(ip_parts[:3])
self.ip_start = int(ip_parts[3])
else:
self.ip_base = "192.168.1"
self.ip_start = 51
self.devices = []
self.results = {}
self.log_dir = Path('/tmp')
def print_banner(self):
print()
print(f"{Colors.BLUE}{'='*70}")
print("ESP32 Mass Deployment Tool")
print(f"{'='*70}{Colors.NC}")
print(f"Project: {self.project_dir}")
print(f"Build Dir: {self.build_dir}")
if self.config_mode:
print(f"Mode: {Colors.YELLOW}FLASH + CONFIGURE{Colors.NC}")
print(f"SSID: {self.ssid}")
print(f"Password: {'*' * len(self.password)}")
print(f"Start IP: {self.start_ip}")
else:
print(f"Mode: {Colors.GREEN}FLASH ONLY (Preserve NVS){Colors.NC}")
print(f"Flash Baud: {self.baud_rate}")
print(f"Parallel: {self.parallel}")
if self.num_devices:
print(f"Max Devices: {self.num_devices}")
print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
def build_firmware(self):
print()
print(f"{Colors.YELLOW}[1/4] Building firmware...{Colors.NC}")
try:
subprocess.run(
['idf.py', 'build'],
cwd=self.project_dir,
check=True,
capture_output=not self.verbose
)
flash_args_path = self.build_dir / 'flash_args'
if not flash_args_path.exists():
print(f"{Colors.RED}Error: build/flash_args not found.{Colors.NC}")
return False
print(f"{Colors.GREEN}✓ Build complete{Colors.NC}")
return True
except subprocess.CalledProcessError as e:
print(f"{Colors.RED}✗ Build failed!{Colors.NC}")
if self.verbose: print(e.stderr.decode() if e.stderr else "")
return False
def detect_devices(self):
print()
print(f"{Colors.YELLOW}[2/4] Detecting ESP32 devices...{Colors.NC}")
self.devices = sorted(glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*'))
if not self.devices:
print(f"{Colors.RED}ERROR: No devices found!{Colors.NC}")
return False
if self.num_devices and len(self.devices) > self.num_devices:
print(f"Limiting to first {self.num_devices} devices")
self.devices = self.devices[:self.num_devices]
print(f"{Colors.GREEN}Found {len(self.devices)} device(s):{Colors.NC}")
for i, device in enumerate(self.devices):
if self.config_mode:
print(f" [{i:2d}] {device:14s}{self.get_ip_for_index(i)}")
else:
print(f" [{i:2d}] {device:14s} → (Existing IP)")
return True
def get_ip_for_index(self, index):
return f"{self.ip_base}.{self.ip_start + index}"
def flash_and_configure(self, index, device):
target_ip = self.get_ip_for_index(index) if self.config_mode else "Existing IP"
log_file = self.log_dir / f"esp32_deploy_{index}.log"
log_lines = []
flash_args_file = 'flash_args' # Relative to build_dir
def log(msg):
log_lines.append(msg)
if self.verbose or not self.parallel:
print(f"[{index}] {msg}")
for attempt in range(1, self.max_retries + 1):
log(f"=== Device {index}: {device} (Attempt {attempt}/{self.max_retries}) ===")
# --- FLASHING ---
log("Flashing via esptool...")
try:
cmd = [
'esptool.py',
'-p', device,
'-b', str(self.baud_rate),
'--before', 'default_reset',
'--after', 'hard_reset',
'write_flash',
f"@{flash_args_file}"
]
# CRITICAL FIX: Run from build_dir so relative paths in flash_args are valid
result = subprocess.run(
cmd,
cwd=self.build_dir,
check=True,
capture_output=True,
timeout=300
)
log("✓ Flash successful")
except subprocess.CalledProcessError as e:
log(f"✗ Flash failed: {e.stderr.decode() if e.stderr else 'Unknown error'}")
if attempt == self.max_retries:
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines}
time.sleep(2)
continue
except subprocess.TimeoutExpired:
log("✗ Flash timeout")
if attempt == self.max_retries:
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'TIMEOUT', 'log': log_lines}
continue
# --- CONFIGURATION ---
log("Waiting for boot (3s)...")
time.sleep(3)
if self.config_mode:
log(f"Configuring WiFi ({target_ip})...")
try:
config = (
f"CFG\n"
f"SSID:{self.ssid}\n"
f"PASS:{self.password}\n"
f"IP:{target_ip}\n"
f"MASK:{self.netmask}\n"
f"GW:{self.gateway}\n"
f"DHCP:0\n"
f"END\n"
)
with serial.Serial(device, 115200, timeout=2, write_timeout=2) as ser:
ser.reset_input_buffer()
ser.write(config.encode('utf-8'))
ser.flush()
log("✓ Config sent")
if self.verify_ping:
log("Waiting for network (6s)...")
time.sleep(6)
log(f"Pinging {target_ip}...")
try:
res = subprocess.run(['ping', '-c', '2', '-W', '3', target_ip], capture_output=True, timeout=10)
if res.returncode == 0:
log("✓ Ping successful")
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines}
else:
log("✗ Ping failed")
except:
log("✗ Ping error")
else:
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines}
except Exception as e:
log(f"✗ Config error: {e}")
else:
log("Configuration skipped (Preserving NVS)")
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'SUCCESS', 'log': log_lines}
time.sleep(2)
with open(log_file, 'w') as f: f.write('\n'.join(log_lines))
return {'index': index, 'device': device, 'ip': target_ip, 'status': 'FAILED', 'log': log_lines}
def deploy_all_parallel(self):
print()
print(f"{Colors.YELLOW}[3/4] Flashing (parallel)...{Colors.NC}")
max_workers = min(10, len(self.devices))
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(self.flash_and_configure, i, device): (i, device)
for i, device in enumerate(self.devices)
}
for future in as_completed(futures):
result = future.result()
self.results[result['index']] = result
self.print_device_status(result)
def deploy_all_sequential(self):
print()
print(f"{Colors.YELLOW}[3/4] Flashing (sequential)...{Colors.NC}")
for i, device in enumerate(self.devices):
print(f"\n{Colors.BLUE}--- Device {i+1}/{len(self.devices)} ---{Colors.NC}")
result = self.flash_and_configure(i, device)
self.results[result['index']] = result
self.print_device_status(result)
def print_device_status(self, result):
status_color = {
'SUCCESS': Colors.GREEN, 'NO_PING': Colors.YELLOW,
'FAILED': Colors.RED, 'TIMEOUT': Colors.RED
}.get(result['status'], Colors.RED)
print(f"{status_color}[Device {result['index']:2d}] {result['device']:14s}{result['ip']:15s} [{result['status']}]{Colors.NC}")
def deploy_all(self):
if self.parallel: self.deploy_all_parallel()
else: self.deploy_all_sequential()
def print_summary(self):
print()
print(f"{Colors.YELLOW}[4/4] Deployment Summary{Colors.NC}")
print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
success = sum(1 for r in self.results.values() if r['status'] == 'SUCCESS')
failed = sum(1 for r in self.results.values() if r['status'] in ['FAILED', 'TIMEOUT'])
for i in range(len(self.devices)):
if i in self.results:
r = self.results[i]
icon = f"{Colors.GREEN}{Colors.NC}" if r['status'] == 'SUCCESS' else f"{Colors.RED}{Colors.NC}"
print(f"{icon} {r['device']:14s}{r['ip']}")
print(f"{Colors.BLUE}{'='*70}{Colors.NC}")
print(f"Total: {len(self.devices)}")
print(f"Success: {success}")
print(f"Failed: {failed}")
return failed
def main():
parser = argparse.ArgumentParser(description='ESP32 Mass Deployment Tool')
parser.add_argument('-d', '--dir', default=os.getcwd(), help='ESP-IDF project dir')
parser.add_argument('-s', '--ssid', help='WiFi SSID')
parser.add_argument('-p', '--password', help='WiFi Password')
parser.add_argument('--start-ip', default='192.168.1.51', help='Starting IP')
parser.add_argument('-n', '--num-devices', type=int, default=30, help='Max devices')
parser.add_argument('-g', '--gateway', default='192.168.1.1', help='Gateway IP')
parser.add_argument('-m', '--netmask', default='255.255.255.0', help='Netmask')
parser.add_argument('-b', '--baud', type=int, default=460800, help='Flash baud rate')
parser.add_argument('-r', '--retries', type=int, default=2, help='Retries')
parser.add_argument('--no-verify', action='store_true', help='Skip ping check')
parser.add_argument('--sequential', action='store_true', help='Run sequentially')
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose')
args = parser.parse_args()
if (args.ssid and not args.password) or (args.password and not args.ssid):
print(f"{Colors.RED}ERROR: Provide both SSID and Password for config, or neither for flash-only.{Colors.NC}")
sys.exit(1)
deployer = DeviceDeployer(
project_dir=args.dir, ssid=args.ssid, password=args.password,
start_ip=args.start_ip, netmask=args.netmask, gateway=args.gateway,
baud_rate=args.baud, max_retries=args.retries, verify_ping=not args.no_verify,
num_devices=args.num_devices if args.num_devices > 0 else None,
verbose=args.verbose, parallel=not args.sequential
)
deployer.print_banner()
if not deployer.build_firmware(): sys.exit(1)
if not deployer.detect_devices(): sys.exit(1)
deployer.deploy_all()
failed_count = deployer.print_summary()
sys.exit(failed_count)
if __name__ == '__main__':
main()

21
new_rules.part Normal file
View File

@ -0,0 +1,21 @@
# --- Added by update script ---
SUBSYSTEM=="tty", ATTRS{serial}=="04d3540e9223f011aefcbef12a319464", SYMLINK+="esp_port_01"
SUBSYSTEM=="tty", ATTRS{serial}=="0aa1c0a3a323f0118893c2f12a319464", SYMLINK+="esp_port_02"
SUBSYSTEM=="tty", ATTRS{serial}=="1ca7d2748a23f0118d9db8f12a319464", SYMLINK+="esp_port_03"
SUBSYSTEM=="tty", ATTRS{serial}=="1e8972fca871f011ae8af99e1045c30f", SYMLINK+="esp_port_04"
SUBSYSTEM=="tty", ATTRS{serial}=="263cd138a871f011bcecff9e1045c30f", SYMLINK+="esp_port_05"
SUBSYSTEM=="tty", ATTRS{serial}=="28e8c61a8523f011a56ac2f12a319464", SYMLINK+="esp_port_06"
SUBSYSTEM=="tty", ATTRS{serial}=="38717231a723f011a006c3f12a319464", SYMLINK+="esp_port_07"
SUBSYSTEM=="tty", ATTRS{serial}=="3e1afd689523f011b363baf12a319464", SYMLINK+="esp_port_08"
SUBSYSTEM=="tty", ATTRS{serial}=="4a6d2844a071f011bceaff9e1045c30f", SYMLINK+="esp_port_09"
SUBSYSTEM=="tty", ATTRS{serial}=="4e52608ba171f011af3ffb9e1045c30f", SYMLINK+="esp_port_10"
SUBSYSTEM=="tty", ATTRS{serial}=="4eaecac7a371f011be18fb9e1045c30f", SYMLINK+="esp_port_11"
SUBSYSTEM=="tty", ATTRS{serial}=="600faabf9a71f01195bbfb9e1045c30f", SYMLINK+="esp_port_12"
SUBSYSTEM=="tty", ATTRS{serial}=="640c56829723f01183c1bef12a319464", SYMLINK+="esp_port_13"
SUBSYSTEM=="tty", ATTRS{serial}=="904f28aede6ef011beac4d9b1045c30f", SYMLINK+="esp_port_14"
SUBSYSTEM=="tty", ATTRS{serial}=="A5069RR4", SYMLINK+="esp_port_15"
SUBSYSTEM=="tty", ATTRS{serial}=="a86592298f23f01199b0c2f12a319464", SYMLINK+="esp_port_16"
SUBSYSTEM=="tty", ATTRS{serial}=="ba2efc7fa071f0119aa1fb9e1045c30f", SYMLINK+="esp_port_17"
SUBSYSTEM=="tty", ATTRS{serial}=="fc641417fbf4ef11bd28a1a29ed47d52", SYMLINK+="esp_port_18"
SUBSYSTEM=="tty", ATTRS{serial}=="fedb86249723f01198ebc2f12a319464", SYMLINK+="esp_port_19"

View File

@ -1,167 +0,0 @@
#!/usr/bin/env python3
"""
Simple ESP32 WiFi Reconfiguration Tool
Sends WiFi config to all connected ESP32 devices via serial
"""
import serial
import time
import glob
import argparse
import sys
def reconfig_devices(ssid, password, start_ip, gateway="192.168.1.1",
netmask="255.255.255.0", verbose=False):
"""Reconfigure all connected devices"""
devices = sorted(glob.glob('/dev/ttyUSB*'))
num_devices = len(devices)
if num_devices == 0:
print("ERROR: No devices found!")
return 0
# Parse start IP
ip_parts = start_ip.split('.')
ip_base = '.'.join(ip_parts[:3])
ip_start = int(ip_parts[3])
ok_devices = 0
print(f"Found {num_devices} devices")
print(f"SSID: {ssid}")
print(f"Password: {'*' * len(password)}")
print(f"IP Range: {ip_base}.{ip_start} - {ip_base}.{ip_start + num_devices - 1}")
print()
for idx, dev in enumerate(devices):
ip = f"{ip_base}.{ip_start + idx}"
print(f"[{idx:2d}] Configuring {dev:14s}{ip}", end='')
try:
ser = serial.Serial(dev, 115200, timeout=1)
time.sleep(0.5) # Let serial port stabilize
# Send configuration
ser.write(b"CFG\n")
time.sleep(0.1)
ser.write(f"SSID:{ssid}\n".encode())
time.sleep(0.1)
ser.write(f"PASS:{password}\n".encode())
time.sleep(0.1)
ser.write(f"IP:{ip}\n".encode())
time.sleep(0.1)
ser.write(f"MASK:{netmask}\n".encode())
time.sleep(0.1)
ser.write(f"GW:{gateway}\n".encode())
time.sleep(0.1)
ser.write(b"DHCP:0\n")
time.sleep(0.1)
ser.write(b"END\n")
# Wait for OK response
time.sleep(0.5)
response = ser.read(100).decode('utf-8', errors='ignore')
if verbose and response.strip():
print(f"\n Response: {response[:80]}")
if 'OK' in response:
print("")
ok_devices += 1
else:
print(" ⚠ (no OK)")
ser.close()
except Exception as e:
print(f" ✗ Error: {e}")
time.sleep(0.5)
print()
print(f"{'='*60}")
print(f"Success: {ok_devices}/{num_devices}")
print(f"Failed: {num_devices - ok_devices}/{num_devices}")
print(f"{'='*60}")
return ok_devices
def main():
parser = argparse.ArgumentParser(
description='Reconfigure WiFi settings on all connected ESP32 devices',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic usage with defaults
%(prog)s
# Custom IP range
%(prog)s --start-ip 192.168.1.100
# Custom WiFi credentials
%(prog)s -s MyNetwork -p mypassword
# Different subnet
%(prog)s --start-ip 10.0.0.50 -g 10.0.0.1
# Verbose mode
%(prog)s -v
"""
)
parser.add_argument('-s', '--ssid', default='ClubHouse2G',
help='WiFi SSID (default: ClubHouse2G)')
parser.add_argument('-p', '--password', default='ez2remember',
help='WiFi password (default: ez2remember)')
parser.add_argument('--start-ip', default='192.168.1.51',
help='Starting IP address (default: 192.168.1.51)')
parser.add_argument('-g', '--gateway', default='192.168.1.1',
help='Gateway IP (default: 192.168.1.1)')
parser.add_argument('-m', '--netmask', default='255.255.255.0',
help='Network mask (default: 255.255.255.0)')
parser.add_argument('-v', '--verbose', action='store_true',
help='Show device responses')
parser.add_argument('-w', '--wait', type=int, default=30,
help='Seconds to wait for connections (default: 30)')
args = parser.parse_args()
# Reconfigure all devices
ok_count = reconfig_devices(
ssid=args.ssid,
password=args.password,
start_ip=args.start_ip,
gateway=args.gateway,
netmask=args.netmask,
verbose=args.verbose
)
# Wait for connections
if ok_count > 0:
print(f"\nWaiting {args.wait}s for WiFi connections...")
time.sleep(args.wait)
print("Done!")
print()
print("Test commands:")
# Extract IP info
ip_parts = args.start_ip.split('.')
ip_base = '.'.join(ip_parts[:3])
ip_start = int(ip_parts[3])
num_devices = len(sorted(glob.glob('/dev/ttyUSB*')))
print(f" # Ping all devices")
print(f" for i in {{{ip_start}..{ip_start + num_devices - 1}}}; do ping -c 1 {ip_base}.$i & done; wait")
print()
print(f" # Check device status")
print(f" ./check_device_status.py --reset")
print()
print(f" # Test first device")
print(f" iperf -c {ip_base}.{ip_start}")
print()
sys.exit(0 if ok_count > 0 else 1)
if __name__ == '__main__':
main()

View File

@ -1,315 +0,0 @@
#!/usr/bin/env python3
import argparse
import glob
import re
import serial
import time
import sys
from serial.tools import list_ports
import json
import os
DEFAULT_PATTERN = "/dev/ttyUSB*"
MAP_FILE = os.path.expanduser("~/.reconfig_ipmap.json")
YELLOW_TOKENS = [
"NO WIFI CONFIG", "NO_WIFI_CONFIG", "NO CONFIG", "NO_CONFIG",
"YELLOW", "LED_STATE_NO_CONFIG"
]
IP_REGEX = re.compile(r'(?:(?:IP[ :]*|STA[ _-]*IP[ :]*|ADDR[ :]*|ADDRESS[ :]*))?(\d{1,3}(?:\.\d{1,3}){3})', re.IGNORECASE)
def eprint(*a, **kw):
print(*a, file=sys.stderr, **kw)
def detect_no_config(ser, verbose=False, settle=0.1, timeout=0.3, probes=(b"STATUS\n", b"IP\n"), deadline=None):
ser.timeout = timeout
ser.write_timeout = timeout
def now(): return time.time()
def read_and_collect(sleep_s=0.05):
buf = b""
# sleep but respect deadline
t_end = now() + sleep_s
while now() < t_end:
time.sleep(0.01)
try:
while True:
if deadline and now() >= deadline: break
chunk = ser.read(256)
if not chunk:
break
buf += chunk
except Exception:
pass
return buf.decode('utf-8', errors='ignore')
text = ""
# initial settle
t_end = now() + settle
while now() < t_end:
time.sleep(0.01)
text += read_and_collect(0.0)
# probes
for cmd in probes:
if deadline and now() >= deadline: break
try:
ser.write(cmd)
except Exception:
pass
text += read_and_collect(0.1)
if verbose and text.strip():
eprint("--- STATUS TEXT BEGIN ---")
eprint(text)
eprint("--- STATUS TEXT END ---")
utext = text.upper()
return any(tok in utext for tok in YELLOW_TOKENS), text
def parse_ip_from_text(text):
for m in IP_REGEX.finditer(text or ""):
ip = m.group(1)
try:
octs = [int(x) for x in ip.split(".")]
if all(0 <= x <= 255 for x in octs):
return ip
except Exception:
pass
return None
def next_free_ip(used_last_octets, start_ip_octet, max_octet=254):
x = start_ip_octet
while x <= max_octet:
if x not in used_last_octets:
used_last_octets.add(x)
return x
x += 1
raise RuntimeError("No free IPs left in the range")
def load_map(path):
if os.path.exists(path):
try:
with open(path, "r") as f:
return json.load(f)
except Exception:
return {}
return {}
def usb_serial_for_port(dev):
for p in list_ports.comports():
if p.device == dev:
return p.serial_number or p.hwid or dev
return dev
def configure_device(ser, ssid, password, ip, dhcp, verbose=False):
def writeln(s):
if isinstance(s, str):
s = s.encode()
ser.write(s + b"\n")
time.sleep(0.05)
time.sleep(0.15)
writeln("CFG")
writeln(f"SSID:{ssid}")
writeln(f"PASS:{password}")
if dhcp:
writeln("DHCP:1")
else:
writeln(f"IP:{ip}")
writeln("MASK:255.255.255.0")
writeln("GW:192.168.1.1")
writeln("DHCP:0")
writeln("END")
time.sleep(0.2)
resp = b""
try:
while True:
chunk = ser.read(256)
if not chunk:
break
resp += chunk
except Exception:
pass
text = resp.decode('utf-8', errors='ignore')
if verbose and text.strip():
eprint("--- CONFIG RESPONSE BEGIN ---")
eprint(text)
eprint("--- CONFIG RESPONSE END ---")
ok = ("OK" in text) or ("Saved" in text) or ("DONE" in text.upper())
return ok, text
def main():
parser = argparse.ArgumentParser(
description="Configure ESP32-S3 devices over serial. Fast, with strict per-device deadlines and exclude regex."
)
parser.add_argument("--ssid", default="ClubHouse2G", help="WiFi SSID")
parser.add_argument("--password", default="ez2remember", help="WiFi password")
parser.add_argument("--pattern", default=DEFAULT_PATTERN, help=f"Glob for serial ports (default: {DEFAULT_PATTERN})")
parser.add_argument("--exclude", default="", help="Regex of device paths to skip, e.g. 'ttyUSB10|ttyUSB11'")
parser.add_argument("--baud", type=int, default=115200, help="Serial baud rate")
parser.add_argument("--timeout", type=float, default=0.3, help="Serial read/write timeout (s)")
parser.add_argument("--settle", type=float, default=0.1, help="Settle delay before first read (s)")
parser.add_argument("--per-device-cap", type=float, default=1.2, help="Hard deadline seconds per device during probe")
parser.add_argument("--only-yellow", action="store_true",
help="Only program devices that appear to be in 'no WiFi config' (solid yellow) state")
parser.add_argument("--dhcp", action="store_true", help="Configure device for DHCP instead of static IP")
parser.add_argument("--start-ip", type=int, default=51, help="Starting host octet for static IPs (x in 192.168.1.x)")
parser.add_argument("--persist-map", action="store_true",
help=f"Persist USB-serial → IP assignments to {MAP_FILE} to keep continuity across runs")
parser.add_argument("--full-probes", action="store_true", help="Use extended probes (STATUS, STAT, GET STATUS, IP)")
parser.add_argument("--list", action="store_true", help="List ports with serial numbers and exit")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose status prints to stderr")
parser.add_argument("--dry-run", action="store_true", help="Do not send CFG/END; just print what would happen")
args = parser.parse_args()
if args.list:
print("Ports:")
for p in list_ports.comports():
print(f" {p.device:>12} sn={p.serial_number} desc={p.description}")
return
devices = sorted(glob.glob(args.pattern))
if args.exclude:
devices = [d for d in devices if not re.search(args.exclude, d)]
print(f"Found {len(devices)} devices matching {args.pattern}", flush=True)
if args.exclude:
print(f"Excluding devices matching /{args.exclude}/", flush=True)
ip_map = load_map(MAP_FILE) if args.persist_map else {}
used_last_octets = set()
prepass_info = {}
for i, dev in enumerate(devices):
print(f"[pre] {i+1}/{len(devices)} probing {dev}", flush=True)
start_t = time.time()
already_ip = None
no_cfg = False
try:
ser = serial.Serial(
dev,
args.baud,
timeout=args.timeout,
write_timeout=args.timeout,
rtscts=False,
dsrdtr=False,
xonxoff=False,
)
# gentle DTR/RTS toggle
try:
ser.dtr = False; ser.rts = False; time.sleep(0.02)
ser.dtr = True; ser.rts = True; time.sleep(0.02)
except Exception:
pass
probes = (b"STATUS\n", b"IP\n") if not args.full_probes else (b"STATUS\n", b"STAT\n", b"GET STATUS\n", b"IP\n")
deadline = start_t + max(0.4, args.per_device_cap)
no_cfg, text = detect_no_config(
ser, verbose=args.verbose, settle=args.settle,
timeout=args.timeout, probes=probes, deadline=deadline
)
already_ip = parse_ip_from_text(text)
ser.close()
except Exception as e:
eprint(f" [warn] {dev} probe error: {e}")
dur = time.time() - start_t
print(f" → no_cfg={no_cfg} ip={already_ip} ({dur:.2f}s)", flush=True)
prepass_info[dev] = {"no_cfg": no_cfg, "ip": already_ip}
if already_ip and not args.dhcp:
try:
last = int(already_ip.split(".")[-1])
used_last_octets.add(last)
except Exception:
pass
ok_devices = 0
skipped = 0
errors = 0
for idx, dev in enumerate(devices):
info = prepass_info.get(dev, {})
already_ip = info.get("ip")
no_cfg = info.get("no_cfg", False)
usb_key = usb_serial_for_port(dev)
if already_ip and not args.dhcp:
print(f"[cfg] {idx+1}/{len(devices)} {dev}: already has {already_ip} → skip", flush=True)
skipped += 1
if args.persist_map:
ip_map[usb_key] = already_ip
continue
if args.only_yellow and not no_cfg:
print(f"[cfg] {idx+1}/{len(devices)} {dev}: not yellow/no-config → skip", flush=True)
skipped += 1
continue
# pick target IP
if args.dhcp:
target_ip = None
mode = "DHCP"
else:
target_last_octet = None
if args.persist_map and usb_key in ip_map:
try:
prev_ip = ip_map[usb_key]
target_last_octet = int(prev_ip.split(".")[-1])
if target_last_octet in used_last_octets:
target_last_octet = None
except Exception:
target_last_octet = None
if target_last_octet is None:
target_last_octet = next_free_ip(used_last_octets, args.start_ip, 254)
target_ip = f"192.168.1.{target_last_octet}"
mode = f"Static {target_ip}"
print(f"[cfg] {idx+1}/{len(devices)} {dev}: configuring ({mode})", flush=True)
if args.dry_run:
print(" (dry-run) Would send CFG/END", flush=True)
ok = True
else:
try:
ser = serial.Serial(dev, args.baud, timeout=args.timeout, write_timeout=args.timeout)
ok, resp = configure_device(ser, args.ssid, args.password, target_ip, args.dhcp, verbose=args.verbose)
ser.close()
except Exception as e:
print(f" ✗ Error opening/configuring: {e}", flush=True)
ok = False
if ok:
print(" ✓ OK", flush=True)
ok_devices += 1
if not args.dhcp and args.persist_map and target_ip:
ip_map[usb_key] = target_ip
else:
print(" ✗ Failed", flush=True)
errors += 1
time.sleep(0.05)
if args.persist_map:
try:
with open(MAP_FILE, "w") as f:
json.dump(ip_map, f, indent=2, sort_keys=True)
print(f"Persisted mapping to {MAP_FILE}", flush=True)
except Exception as e:
print(f"Warning: could not save mapping to {MAP_FILE}: {e}", flush=True)
print(f"Summary: OK={ok_devices} Skipped={skipped} Errors={errors} Total={len(devices)}", flush=True)
if __name__ == "__main__":
main()

View File

@ -7,3 +7,14 @@ CONFIG_FREERTOS_HZ=1000
CONFIG_CONSOLE_UART_RX_BUF_SIZE=1024
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_ESP_WIFI_CSI_ENABLED=n
# Use System Time (Wall Clock) for Logs instead of Boot Time
# Shared Base Defaults
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=6144
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_FREERTOS_ISR_STACKSIZE=2048
CONFIG_FREERTOS_HZ=1000
CONFIG_CONSOLE_UART_RX_BUF_SIZE=1024
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_ESP_WIFI_CSI_ENABLED=n
CONFIG_LOG_TIMESTAMP_SOURCE_NONE=y