Compare commits
30 Commits
a56832c01c
...
c58e70a658
| Author | SHA1 | Date |
|---|---|---|
|
|
c58e70a658 | |
|
|
a303b7171a | |
|
|
1eddb8e84f | |
|
|
56ea987f75 | |
|
|
128596bd67 | |
|
|
d4cd861b80 | |
|
|
feb0d4d142 | |
|
|
4ed4391068 | |
|
|
2590a96b15 | |
|
|
98b013569d | |
|
|
64446be628 | |
|
|
b769dbc356 | |
|
|
9974174d5b | |
|
|
b4b40de64d | |
|
|
099c28f9c7 | |
|
|
42905200ea | |
|
|
6c214e8e92 | |
|
|
969abb5ae4 | |
|
|
2a41edf491 | |
|
|
46f0cdb07b | |
|
|
e8f7e2f75c | |
|
|
1b78440309 | |
|
|
88a585408a | |
|
|
e5baa7cec5 | |
|
|
3969c5780d | |
|
|
ca8b382a40 | |
|
|
0f1c5b3079 | |
|
|
796ef43497 | |
|
|
87744e2883 | |
|
|
c640bc4df7 |
|
|
@ -9,6 +9,11 @@ sdkconfig.old
|
||||||
*.bin
|
*.bin
|
||||||
*.elf
|
*.elf
|
||||||
*.map
|
*.map
|
||||||
|
firmware/
|
||||||
|
flash_args_*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
new_rules.part
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
@ -16,6 +21,8 @@ sdkconfig.old
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
# Emacs backup files
|
||||||
|
\#*\#
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
dependencies/
|
dependencies/
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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.")
|
|
||||||
|
|
@ -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()
|
|
||||||
158
batch_config.py
158
batch_config.py
|
|
@ -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.")
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
idf_component_register(SRCS "app_console.c"
|
idf_component_register(
|
||||||
INCLUDE_DIRS "."
|
SRCS "app_console.c"
|
||||||
PRIV_REQUIRES console wifi_cfg iperf)
|
"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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 "app_console.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_log.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) {
|
void app_console_register_commands(void) {
|
||||||
register_iperf_cmd();
|
register_system_cmd();
|
||||||
|
register_nvs_cmd();
|
||||||
register_wifi_cmd();
|
register_wifi_cmd();
|
||||||
|
register_iperf_cmd();
|
||||||
|
register_gps_cmd();
|
||||||
|
register_ping_cmd();
|
||||||
|
register_monitor_cmd();
|
||||||
|
register_ip_cmd();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
// This matches the call in main.c
|
||||||
* @brief Register application-specific console commands
|
|
||||||
*/
|
|
||||||
void app_console_register_commands(void);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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 "csi_log.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
|
||||||
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
|
||||||
|
|
@ -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_manager.h"
|
||||||
#include "csi_log.h"
|
#include "csi_log.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
|
|
||||||
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
|
||||||
|
|
@ -1,297 +1,282 @@
|
||||||
#include "gps_sync.h"
|
/*
|
||||||
#include "driver/gpio.h"
|
* gps_sync.c
|
||||||
#include "driver/uart.h"
|
*
|
||||||
#include "esp_timer.h"
|
* Copyright (c) 2025 Umber Networks & Robert McMahon
|
||||||
#include "esp_log.h"
|
* All rights reserved.
|
||||||
#include "esp_rom_sys.h"
|
*
|
||||||
#include <string.h>
|
* Redistribution and use in source and binary forms, with or without
|
||||||
#include <time.h>
|
* modification, are permitted provided that the following conditions are met:
|
||||||
#include <stdarg.h>
|
*
|
||||||
|
* 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 <stdio.h>
|
||||||
#include <assert.h>
|
#include <string.h>
|
||||||
#include <inttypes.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";
|
static const char *TAG = "GPS_SYNC";
|
||||||
|
|
||||||
#define GPS_BAUD_RATE 9600
|
#define GPS_BUF_SIZE 1024
|
||||||
#define UART_BUF_SIZE 1024
|
|
||||||
|
|
||||||
// --- GLOBAL STATE ---
|
// --- Internal State ---
|
||||||
static uart_port_t gps_uart_num = UART_NUM_1;
|
static gps_sync_config_t s_cfg;
|
||||||
static int64_t monotonic_offset_us = 0;
|
static volatile int64_t s_last_pps_us = 0;
|
||||||
static volatile int64_t last_pps_monotonic = 0;
|
static volatile int64_t s_nmea_epoch_us = 0;
|
||||||
static volatile time_t next_pps_gps_second = 0;
|
static volatile bool s_nmea_valid = false;
|
||||||
static bool gps_has_fix = false;
|
static char s_last_nmea_msg[128] = {0};
|
||||||
static bool use_gps_for_logs = false;
|
static bool s_time_set = false;
|
||||||
static SemaphoreHandle_t sync_mutex;
|
|
||||||
static volatile bool force_sync_update = true;
|
|
||||||
|
|
||||||
// PPS interrupt
|
// --- PPS Handler ---
|
||||||
static void IRAM_ATTR pps_isr_handler(void* arg) {
|
static void IRAM_ATTR pps_gpio_isr_handler(void* arg) {
|
||||||
static bool onetime = true;
|
s_last_pps_us = esp_timer_get_time();
|
||||||
last_pps_monotonic = esp_timer_get_time();
|
}
|
||||||
if (onetime) {
|
|
||||||
esp_rom_printf("PPS connected!\n");
|
// --- Time Helper ---
|
||||||
onetime = false;
|
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
|
// --- NMEA Parser ---
|
||||||
static bool parse_gprmc(const char* nmea, struct tm* tm_out, bool* valid) {
|
static void parse_nmea_line(char *line) {
|
||||||
if (strncmp(nmea, "$GPRMC", 6) != 0 && strncmp(nmea, "$GNRMC", 6) != 0) return false;
|
strlcpy(s_last_nmea_msg, line, sizeof(s_last_nmea_msg));
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void gps_force_next_update(void) {
|
// Support GPRMC and GNRMC
|
||||||
force_sync_update = true;
|
if (strncmp(line, "$GPRMC", 6) == 0 || strncmp(line, "$GNRMC", 6) == 0) {
|
||||||
ESP_LOGW(TAG, "Requesting forced GPS sync update");
|
char *p = line;
|
||||||
}
|
int field = 0;
|
||||||
|
char *time_ptr = NULL;
|
||||||
|
char *date_ptr = NULL;
|
||||||
|
char status = 'V';
|
||||||
|
|
||||||
static void gps_task(void* arg) {
|
// Walk fields
|
||||||
uint8_t d_buf[64];
|
// $GPRMC,Time,Status,Lat,NS,Lon,EW,Spd,Trk,Date,...
|
||||||
char line[128];
|
// Field 1: Time
|
||||||
int pos = 0;
|
// Field 2: Status
|
||||||
static int log_counter = 0;
|
// Field 9: Date
|
||||||
|
|
||||||
while (1) {
|
while ((p = strchr(p, ',')) != NULL) {
|
||||||
int len = uart_read_bytes(gps_uart_num, d_buf, sizeof(d_buf), pdMS_TO_TICKS(100));
|
p++;
|
||||||
|
field++;
|
||||||
|
|
||||||
if (len > 0) {
|
if (field == 1) time_ptr = p;
|
||||||
for (int i = 0; i < len; i++) {
|
else if (field == 2) status = *p;
|
||||||
uint8_t data = d_buf[i];
|
else if (field == 9) {
|
||||||
if (data == '\n') {
|
date_ptr = p;
|
||||||
line[pos] = '\0';
|
break; // We have what we need
|
||||||
struct tm gps_tm;
|
}
|
||||||
bool valid;
|
}
|
||||||
if (parse_gprmc(line, &gps_tm, &valid)) {
|
|
||||||
if (valid) {
|
s_nmea_valid = (status == 'A');
|
||||||
time_t gps_time = mktime(&gps_tm);
|
|
||||||
xSemaphoreTake(sync_mutex, portMAX_DELAY);
|
if (s_nmea_valid) {
|
||||||
next_pps_gps_second = gps_time + 1;
|
s_nmea_epoch_us = esp_timer_get_time();
|
||||||
xSemaphoreGive(sync_mutex);
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(300));
|
// Extract substrings for Time/Date (comma terminated)
|
||||||
xSemaphoreTake(sync_mutex, portMAX_DELAY);
|
if (time_ptr && date_ptr) {
|
||||||
if (last_pps_monotonic > 0) {
|
char t_buf[16] = {0};
|
||||||
int64_t gps_us = (int64_t)next_pps_gps_second * 1000000LL;
|
char d_buf[16] = {0};
|
||||||
int64_t new_offset = gps_us - last_pps_monotonic;
|
|
||||||
if (monotonic_offset_us == 0 || force_sync_update) {
|
char *end = strchr(time_ptr, ',');
|
||||||
monotonic_offset_us = new_offset;
|
if (end) {
|
||||||
if (force_sync_update) {
|
int len = end - time_ptr;
|
||||||
ESP_LOGW(TAG, "GPS sync SNAP: Offset forced to %" PRIi64 " us", monotonic_offset_us);
|
if (len < sizeof(t_buf)) {
|
||||||
force_sync_update = false;
|
memcpy(t_buf, time_ptr, len);
|
||||||
log_counter = 0;
|
t_buf[len] = 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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) {
|
// --- UART Task ---
|
||||||
ESP_LOGI(TAG, "Checking for GPS PPS signal on GPIO %d...", config->pps_pin);
|
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
|
char line_buf[128];
|
||||||
gpio_config_t pps_conf = {
|
int line_pos = 0;
|
||||||
.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));
|
|
||||||
|
|
||||||
// 2. Poll for ~3 seconds to detect ANY edge transition
|
while (1) {
|
||||||
bool pps_detected = false;
|
int len = uart_read_bytes(s_cfg.uart_port, data, GPS_BUF_SIZE, 20 / portTICK_PERIOD_MS);
|
||||||
int start_level = gpio_get_level(config->pps_pin);
|
if (len > 0) {
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
// Poll loop: 3000 iterations * 1ms = 3 seconds
|
char c = (char)data[i];
|
||||||
for (int i = 0; i < 3000; i++) {
|
if (c == '\n' || c == '\r') {
|
||||||
int current_level = gpio_get_level(config->pps_pin);
|
if (line_pos > 0) {
|
||||||
if (current_level != start_level) {
|
line_buf[line_pos] = 0;
|
||||||
pps_detected = true;
|
parse_nmea_line(line_buf);
|
||||||
break; // Signal found!
|
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) {
|
// --- API ---
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "PPS signal detected! Initializing GPS subsystem...");
|
void gps_sync_init(const gps_sync_config_t *cfg, bool force_enable) {
|
||||||
|
if (!cfg) return;
|
||||||
// 3. Proceed with Full Initialization
|
s_cfg = *cfg;
|
||||||
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();
|
|
||||||
|
|
||||||
uart_config_t uart_config = {
|
uart_config_t uart_config = {
|
||||||
.baud_rate = GPS_BAUD_RATE,
|
.baud_rate = 9600,
|
||||||
.data_bits = UART_DATA_8_BITS,
|
.data_bits = UART_DATA_8_BITS,
|
||||||
.parity = UART_PARITY_DISABLE,
|
.parity = UART_PARITY_DISABLE,
|
||||||
.stop_bits = UART_STOP_BITS_1,
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
.source_clk = UART_SCLK_DEFAULT,
|
.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));
|
gpio_config_t io_conf = {};
|
||||||
ESP_ERROR_CHECK(uart_param_config(config->uart_port, &uart_config));
|
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,
|
// Install ISR service (ignore error if already installed)
|
||||||
config->tx_pin,
|
err = gpio_install_isr_service(0);
|
||||||
config->rx_pin,
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
UART_PIN_NO_CHANGE,
|
ESP_LOGE(TAG, "Failed to install GPIO ISR service: %s", esp_err_to_name(err));
|
||||||
UART_PIN_NO_CHANGE));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-configure PPS for Interrupts (Posedge)
|
err = gpio_isr_handler_add(s_cfg.pps_pin, pps_gpio_isr_handler, NULL);
|
||||||
gpio_config_t io_conf = {
|
if (err != ESP_OK) {
|
||||||
.intr_type = GPIO_INTR_POSEDGE,
|
ESP_LOGE(TAG, "Failed to add PPS GPIO ISR handler: %s", esp_err_to_name(err));
|
||||||
.mode = GPIO_MODE_INPUT,
|
return;
|
||||||
.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));
|
|
||||||
|
|
||||||
xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL);
|
xTaskCreate(gps_task, "gps_task", 4096, NULL, 5, NULL);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "GPS sync initialized (UART=%d, RX=%d, TX=%d, PPS=%d)",
|
ESP_LOGI(TAG, "Initialized (UART:%d, PPS:%d)", s_cfg.uart_port, s_cfg.pps_pin);
|
||||||
config->uart_port, config->rx_pin, config->tx_pin, config->pps_pin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gps_timestamp_t gps_get_timestamp(void) {
|
gps_timestamp_t gps_get_timestamp(void) {
|
||||||
gps_timestamp_t ts;
|
gps_timestamp_t ts = {0};
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts.mono_ts);
|
int64_t now_boot = esp_timer_get_time(); // Boot time
|
||||||
xSemaphoreTake(sync_mutex, portMAX_DELAY);
|
|
||||||
ts.monotonic_us = (int64_t)ts.mono_ts.tv_sec * 1000000LL + ts.mono_ts.tv_nsec / 1000;
|
// Check Flags
|
||||||
ts.monotonic_ms = ts.monotonic_us / 1000;
|
ts.synced = (now_boot - s_last_pps_us < 1100000);
|
||||||
ts.gps_us = ts.monotonic_us + monotonic_offset_us;
|
ts.valid = s_nmea_valid && (now_boot - s_nmea_epoch_us < 2000000);
|
||||||
ts.gps_ms = ts.gps_us / 1000;
|
|
||||||
ts.synced = gps_has_fix;
|
// Return WALL CLOCK time (Epoch), not boot time
|
||||||
xSemaphoreGive(sync_mutex);
|
struct timeval tv;
|
||||||
|
gettimeofday(&tv, NULL);
|
||||||
|
ts.gps_us = (int64_t)tv.tv_sec * 1000000LL + (int64_t)tv.tv_usec;
|
||||||
|
|
||||||
return ts;
|
return ts;
|
||||||
}
|
}
|
||||||
|
|
||||||
int64_t gps_get_monotonic_ms(void) {
|
int64_t gps_get_pps_age_ms(void) {
|
||||||
struct timespec ts;
|
if (s_last_pps_us == 0) return -1;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
return (esp_timer_get_time() - s_last_pps_us) / 1000;
|
||||||
return (int64_t)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool gps_is_synced(void) {
|
void gps_get_last_nmea(char *buf, size_t buf_len) {
|
||||||
return gps_has_fix;
|
if (buf && buf_len > 0) {
|
||||||
}
|
strlcpy(buf, s_last_nmea_msg, buf_len);
|
||||||
|
|
||||||
// ---------------- 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return printf("%s", buffer);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,49 @@
|
||||||
#pragma once
|
/*
|
||||||
#include "driver/gpio.h"
|
* gps_sync.h
|
||||||
#include "driver/uart.h"
|
*
|
||||||
#include <stdbool.h>
|
* Copyright (c) 2025 Umber Networks & Robert McMahon
|
||||||
#include <stdint.h>
|
* All rights reserved.
|
||||||
#include <time.h>
|
*
|
||||||
|
* 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 {
|
typedef struct {
|
||||||
uart_port_t uart_port;
|
uart_port_t uart_port;
|
||||||
gpio_num_t tx_pin;
|
gpio_num_t tx_pin;
|
||||||
|
|
@ -12,19 +51,26 @@ typedef struct {
|
||||||
gpio_num_t pps_pin;
|
gpio_num_t pps_pin;
|
||||||
} gps_sync_config_t;
|
} gps_sync_config_t;
|
||||||
|
|
||||||
|
// --- Timestamp Struct ---
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int64_t monotonic_us;
|
int64_t gps_us; // Current GPS time in microseconds
|
||||||
int64_t monotonic_ms;
|
bool synced; // PPS signal is active and stable (Precision Lock)
|
||||||
int64_t gps_us;
|
bool valid; // NMEA data indicates valid fix ('A' status) (Data Lock)
|
||||||
int64_t gps_ms;
|
|
||||||
struct timespec mono_ts;
|
|
||||||
bool synced;
|
|
||||||
} gps_timestamp_t;
|
} gps_timestamp_t;
|
||||||
|
|
||||||
void gps_sync_init(const gps_sync_config_t *config, bool use_gps_log_timestamps);
|
// --- Initialization ---
|
||||||
void gps_force_next_update(void);
|
// 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);
|
gps_timestamp_t gps_get_timestamp(void);
|
||||||
int64_t gps_get_monotonic_ms(void);
|
|
||||||
bool gps_is_synced(void);
|
// Returns milliseconds since the last PPS edge (Diagnostic)
|
||||||
uint32_t gps_log_timestamp(void);
|
int64_t gps_get_pps_age_ms(void);
|
||||||
int gps_log_vprintf(const char *fmt, va_list args);
|
|
||||||
|
// Copies the last received NMEA line into buffer (Diagnostic)
|
||||||
|
void gps_get_last_nmea(char *buf, size_t buf_len);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ idf_component_register(
|
||||||
# Only if iperf.h needs types from these (unlikely based on your code):
|
# Only if iperf.h needs types from these (unlikely based on your code):
|
||||||
REQUIRES lwip led_strip
|
REQUIRES lwip led_strip
|
||||||
# Internal implementation details only:
|
# 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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
|
@ -7,6 +52,7 @@
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <errno.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "freertos/event_groups.h"
|
#include "freertos/event_groups.h"
|
||||||
|
|
@ -20,9 +66,24 @@
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "iperf.h"
|
#include "iperf.h"
|
||||||
#include "status_led.h"
|
#include "status_led.h"
|
||||||
|
#include "gps_sync.h"
|
||||||
|
|
||||||
static const char *TAG = "iperf";
|
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;
|
static EventGroupHandle_t s_iperf_event_group = NULL;
|
||||||
#define IPERF_IP_READY_BIT (1 << 0)
|
#define IPERF_IP_READY_BIT (1 << 0)
|
||||||
#define IPERF_STOP_REQ_BIT (1 << 1)
|
#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 RATE_CHECK_INTERVAL_US 500000
|
||||||
#define MIN_PACING_INTERVAL_US 100
|
#define MIN_PACING_INTERVAL_US 100
|
||||||
|
|
||||||
|
// --- Runtime Control ---
|
||||||
typedef struct {
|
typedef struct {
|
||||||
iperf_cfg_t cfg;
|
iperf_cfg_t cfg;
|
||||||
bool finish;
|
bool finish;
|
||||||
|
|
@ -39,18 +101,14 @@ typedef struct {
|
||||||
|
|
||||||
static iperf_ctrl_t s_iperf_ctrl = {0};
|
static iperf_ctrl_t s_iperf_ctrl = {0};
|
||||||
static TaskHandle_t s_iperf_task_handle = NULL;
|
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;
|
||||||
static bool s_reload_req = false; // Flag to trigger internal restart
|
|
||||||
|
|
||||||
// Global Stats Tracker
|
// Global Stats Tracker
|
||||||
static iperf_stats_t s_stats = {0};
|
static iperf_stats_t s_stats = {0};
|
||||||
|
|
||||||
// --- Session Persistence Variables ---
|
|
||||||
static int64_t s_session_start_time = 0;
|
static int64_t s_session_start_time = 0;
|
||||||
static int64_t s_session_end_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 {
|
typedef enum {
|
||||||
IPERF_STATE_IDLE = 0,
|
IPERF_STATE_IDLE = 0,
|
||||||
IPERF_STATE_TX,
|
IPERF_STATE_TX,
|
||||||
|
|
@ -58,6 +116,8 @@ typedef enum {
|
||||||
IPERF_STATE_TX_STALLED
|
IPERF_STATE_TX_STALLED
|
||||||
} iperf_fsm_state_t;
|
} 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_tx_us = 0;
|
||||||
static int64_t s_time_slow_us = 0;
|
static int64_t s_time_slow_us = 0;
|
||||||
static int64_t s_time_stalled_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_slow = 0;
|
||||||
static uint32_t s_edge_stalled = 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_any_id;
|
||||||
static esp_event_handler_instance_t instance_got_ip;
|
static esp_event_handler_instance_t instance_got_ip;
|
||||||
|
|
||||||
// --- Helper: Pattern Initialization ---
|
// --- Packet Structures & Constants (Compatible with payloads.h) ---
|
||||||
// Fills buffer with 0-9 cyclic ASCII pattern (matches iperf2 "pattern" function)
|
|
||||||
static void iperf_pattern(uint8_t *buf, uint32_t len) {
|
#define HEADER_EXTEND 0x80000000
|
||||||
for (uint32_t i = 0; i < len; i++) {
|
#define HEADER_SEQNO64B 0x08000000
|
||||||
buf[i] = (i % 10) + '0';
|
#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 ---
|
bool iperf_param_is_unsaved(void) {
|
||||||
// Modified to set all zeros except HEADER_SEQNO64B
|
if (!s_staging_initialized) return false;
|
||||||
static void iperf_generate_client_hdr(iperf_cfg_t *cfg, client_hdr_v1 *hdr) {
|
nvs_handle_t h;
|
||||||
// Zero out the entire structure
|
if (nvs_open("storage", NVS_READONLY, &h) != ESP_OK) return false;
|
||||||
memset(hdr, 0, sizeof(client_hdr_v1));
|
|
||||||
|
|
||||||
// Set only the SEQNO64B flag (Server will detect 64-bit seqno in UDP header)
|
uint32_t val;
|
||||||
hdr->flags = htonl(HEADER_SEQNO64B);
|
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) {
|
void iperf_get_stats(iperf_stats_t *stats) {
|
||||||
if (stats) {
|
if (stats) {
|
||||||
s_stats.config_pps = (s_iperf_ctrl.cfg.pacing_period_us > 0) ?
|
s_stats.config_pps = s_iperf_ctrl.cfg.target_pps;
|
||||||
(1000000 / s_iperf_ctrl.cfg.pacing_period_us) : 0;
|
|
||||||
*stats = s_stats;
|
*stats = s_stats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,42 +275,25 @@ void iperf_get_stats(iperf_stats_t *stats) {
|
||||||
void iperf_print_status(void) {
|
void iperf_print_status(void) {
|
||||||
iperf_get_stats(&s_stats);
|
iperf_get_stats(&s_stats);
|
||||||
|
|
||||||
// 1. Get Source IP
|
gps_timestamp_t ts = gps_get_timestamp();
|
||||||
char src_ip[32] = "0.0.0.0";
|
// Check both Synced (PPS) and Valid (NMEA)
|
||||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
if (ts.synced && ts.valid && ts.gps_us > 0) {
|
||||||
if (netif) {
|
time_t now_sec = ts.gps_us / 1000000;
|
||||||
esp_netif_ip_info_t ip_info;
|
struct tm tm_info;
|
||||||
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
|
gmtime_r(&now_sec, &tm_info);
|
||||||
inet_ntop(AF_INET, &ip_info.ip, src_ip, sizeof(src_ip));
|
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";
|
char dst_ip[32] = "0.0.0.0";
|
||||||
struct in_addr daddr;
|
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));
|
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);
|
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;
|
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_slow = ((double)s_time_slow_us / total_us) * 100.0;
|
||||||
double pct_stalled = ((double)s_time_stalled_us / total_us) * 100.0;
|
double pct_stalled = ((double)s_time_stalled_us / total_us) * 100.0;
|
||||||
|
|
||||||
// Standard Stats
|
float avg_bw_mbps = 0.0f;
|
||||||
printf("IPERF_STATUS: Src=%s, Dst=%s, Running=%d, Config=%" PRIu32 ", Actual=%" PRIu32 ", Err=%.1f%%, Pkts=%" PRIu64 ", AvgBW=%.2f Mbps\n",
|
if (s_session_start_time > 0) {
|
||||||
src_ip, dst_ip, s_stats.running, s_stats.config_pps, s_stats.actual_pps, err, s_session_packets, avg_bw_mbps);
|
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: Dest=%s:%u, Pkts=%llu, BW=%.2f Mbps, Running=%d\n",
|
||||||
printf("IPERF_STATES: TX=%.2fs/%.1f%% (%lu), SLOW=%.2fs/%.1f%% (%lu), STALLED=%.2fs/%.1f%% (%lu)\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_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_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);
|
(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) {
|
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 (s_iperf_event_group == NULL) return;
|
||||||
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
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");
|
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||||
if (netif) {
|
if (netif) {
|
||||||
esp_netif_ip_info_t ip_info;
|
esp_netif_ip_info_t ip_info;
|
||||||
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
|
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) return true;
|
||||||
xEventGroupSetBits(s_iperf_event_group, IPERF_IP_READY_BIT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(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));
|
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...");
|
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);
|
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(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id);
|
||||||
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip);
|
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip);
|
||||||
|
|
||||||
if (bits & IPERF_STOP_REQ_BIT) {
|
if (bits & IPERF_STOP_REQ_BIT) return false;
|
||||||
ESP_LOGW(TAG, "Stop requested while waiting for IP");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ESP_LOGI(TAG, "IP Ready. Starting traffic.");
|
|
||||||
return true;
|
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) {
|
static esp_err_t iperf_start_udp_client(iperf_ctrl_t *ctrl) {
|
||||||
if (!iperf_wait_for_ip()) {
|
if (!iperf_wait_for_ip()) return ESP_OK;
|
||||||
printf("IPERF_STOPPED\n");
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
addr.sin_family = AF_INET;
|
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;
|
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);
|
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
if (sockfd < 0) {
|
if (sockfd < 0) {
|
||||||
|
ESP_LOGE(TAG, "Socket failed: %d", errno);
|
||||||
status_led_set_state(LED_STATE_FAILED);
|
status_led_set_state(LED_STATE_FAILED);
|
||||||
ESP_LOGE(TAG, "Socket creation failed: %d", errno);
|
|
||||||
printf("IPERF_STOPPED\n");
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
status_led_set_state(LED_STATE_TRANSMITTING_SLOW);
|
status_led_set_state(LED_STATE_TRANSMITTING_SLOW);
|
||||||
|
|
||||||
udp_datagram *udp_hdr = (udp_datagram *)ctrl->buffer;
|
// --- CHECK GPS SYNC ---
|
||||||
client_hdr_v1 *client_hdr = (client_hdr_v1 *)(ctrl->buffer + sizeof(udp_datagram));
|
gps_timestamp_t gps = gps_get_timestamp();
|
||||||
iperf_generate_client_hdr(&ctrl->cfg, client_hdr);
|
// 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_stats.running = true;
|
||||||
s_session_start_time = esp_timer_get_time();
|
s_stats.err_mem = 0; s_stats.err_route = 0; s_stats.err_other = 0;
|
||||||
s_session_end_time = 0;
|
s_stats.total_packets = 0;
|
||||||
s_session_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_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_edge_tx = 0; s_edge_slow = 0; s_edge_stalled = 0;
|
||||||
s_current_fsm_state = IPERF_STATE_IDLE;
|
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 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();
|
int64_t last_rate_check = esp_timer_get_time();
|
||||||
uint32_t packets_since_check = 0;
|
uint32_t packets_since_check = 0;
|
||||||
int64_t packet_id = 0;
|
int64_t packet_id = 0;
|
||||||
struct timespec ts;
|
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 now = esp_timer_get_time();
|
||||||
int64_t wait = next_send_time - now;
|
int64_t wait = next_send_time - now;
|
||||||
|
|
||||||
if (wait > 2000) vTaskDelay(pdMS_TO_TICKS(wait / 1000));
|
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++) {
|
for (int k = 0; k < ctrl->cfg.burst_count; k++) {
|
||||||
int64_t current_id = packet_id++;
|
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->id = htonl((uint32_t)(current_id & 0xFFFFFFFF));
|
||||||
udp_hdr->id2 = htonl((uint32_t)((current_id >> 32) & 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_sec = htonl((uint32_t)ts.tv_sec);
|
||||||
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
|
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++;
|
packets_since_check++;
|
||||||
s_session_packets++;
|
|
||||||
} else {
|
} else {
|
||||||
// --- ROBUST FIX: Never Abort ---
|
if (errno == ENOMEM) s_stats.err_mem++;
|
||||||
// If send fails (buffer full, routing issue, etc.), we just yield and retry next loop.
|
else {
|
||||||
// We do NOT goto exit.
|
if (errno == EHOSTUNREACH) s_stats.err_route++;
|
||||||
if (errno != 12) {
|
else s_stats.err_other++;
|
||||||
// Log rarely to avoid spamming serial
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
if ((packet_id % 100) == 0) {
|
|
||||||
ESP_LOGW(TAG, "Send error: %d (Ignored)", errno);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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);
|
uint32_t interval_us = (uint32_t)(now - last_rate_check);
|
||||||
if (interval_us > 0) {
|
if (interval_us > 0) {
|
||||||
s_stats.actual_pps = (uint32_t)((uint64_t)packets_since_check * 1000000 / interval_us);
|
s_stats.actual_pps = (uint32_t)((uint64_t)packets_since_check * 1000000 / interval_us);
|
||||||
uint32_t config_pps = iperf_get_pps();
|
uint32_t threshold = (ctrl->cfg.target_pps * 3) / 4;
|
||||||
uint32_t threshold = (config_pps * 3) / 4;
|
|
||||||
iperf_fsm_state_t next_state;
|
iperf_fsm_state_t next_state;
|
||||||
if (s_stats.actual_pps == 0) next_state = IPERF_STATE_TX_STALLED;
|
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 if (s_stats.actual_pps >= threshold) next_state = IPERF_STATE_TX;
|
||||||
else next_state = IPERF_STATE_TX_SLOW;
|
else next_state = IPERF_STATE_TX_SLOW;
|
||||||
|
|
||||||
switch (next_state) {
|
switch (next_state) {
|
||||||
case IPERF_STATE_TX: s_time_tx_us += interval_us; 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_SLOW: s_time_slow_us += interval_us; break;
|
||||||
case IPERF_STATE_TX_STALLED: s_time_stalled_us += interval_us; break;
|
case IPERF_STATE_TX_STALLED: s_time_stalled_us += interval_us; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next_state != s_current_fsm_state) {
|
if (next_state != s_current_fsm_state) {
|
||||||
switch (next_state) {
|
switch (next_state) {
|
||||||
case IPERF_STATE_TX: s_edge_tx++; break;
|
case IPERF_STATE_TX: s_edge_tx++; break;
|
||||||
case IPERF_STATE_TX_SLOW: s_edge_slow++; break;
|
case IPERF_STATE_TX_SLOW: s_edge_slow++; break;
|
||||||
case IPERF_STATE_TX_STALLED: s_edge_stalled++; break;
|
case IPERF_STATE_TX_STALLED: s_edge_stalled++; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
s_current_fsm_state = next_state;
|
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;
|
last_rate_check = now;
|
||||||
packets_since_check = 0;
|
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;
|
int64_t final_id = -packet_id;
|
||||||
hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
|
struct udp_datagram *udp_hdr = (struct udp_datagram *)ctrl->buffer;
|
||||||
hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
|
udp_hdr->id = htonl((uint32_t)(final_id & 0xFFFFFFFF));
|
||||||
|
udp_hdr->id2 = htonl((uint32_t)((final_id >> 32) & 0xFFFFFFFF));
|
||||||
clock_gettime(CLOCK_REALTIME, &ts);
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
|
udp_hdr->tv_sec = htonl((uint32_t)ts.tv_sec);
|
||||||
hdr->tv_usec = htonl(ts.tv_nsec / 1000);
|
udp_hdr->tv_usec = htonl(ts.tv_nsec / 1000);
|
||||||
for(int i=0; i<10; i++) {
|
|
||||||
|
for (int i=0; i<10; i++) {
|
||||||
sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
|
sendto(sockfd, ctrl->buffer, ctrl->cfg.send_len, 0, (struct sockaddr *)&addr, sizeof(addr));
|
||||||
vTaskDelay(pdMS_TO_TICKS(2));
|
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);
|
close(sockfd);
|
||||||
s_stats.running = false;
|
s_stats.running = false;
|
||||||
s_session_end_time = esp_timer_get_time();
|
s_session_end_time = esp_timer_get_time();
|
||||||
s_stats.actual_pps = 0;
|
status_led_set_state(LED_STATE_CONNECTED);
|
||||||
status_led_set_state(LED_STATE_CONNECTED); // <--- This is your "Solid Green"
|
|
||||||
printf("IPERF_STOPPED\n");
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void iperf_task(void *arg) {
|
static void iperf_task(void *arg) {
|
||||||
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
|
iperf_ctrl_t *ctrl = (iperf_ctrl_t *)arg;
|
||||||
|
|
||||||
do {
|
while (1) {
|
||||||
s_reload_req = false;
|
s_reload_req = false;
|
||||||
ctrl->finish = false;
|
ctrl->finish = false;
|
||||||
xEventGroupClearBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
|
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) {
|
if (s_reload_req) {
|
||||||
ESP_LOGI(TAG, "Hot reloading iperf task with new config...");
|
ESP_LOGI(TAG, "Task reloading config...");
|
||||||
ctrl->cfg = s_next_cfg;
|
if (ctrl->buffer_len < ctrl->cfg.send_len + 128) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
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);
|
free(ctrl->buffer);
|
||||||
s_iperf_task_handle = NULL;
|
s_iperf_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void iperf_start(iperf_cfg_t *cfg) {
|
void iperf_start(void) {
|
||||||
iperf_cfg_t new_cfg = *cfg;
|
if (!s_staging_initialized) iperf_param_init();
|
||||||
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;
|
|
||||||
|
|
||||||
if (s_iperf_task_handle) {
|
if (s_iperf_task_handle) {
|
||||||
ESP_LOGI(TAG, "Task running. Staging hot reload.");
|
ESP_LOGW(TAG, "Already running. Use 'set' to update parameters.");
|
||||||
s_next_cfg = new_cfg;
|
|
||||||
s_reload_req = true;
|
|
||||||
iperf_stop();
|
|
||||||
printf("IPERF_RELOADING\n");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_iperf_ctrl.cfg = new_cfg;
|
s_iperf_ctrl.cfg = s_staging_cfg;
|
||||||
s_iperf_ctrl.finish = false;
|
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_len = s_iperf_ctrl.cfg.send_len + 128;
|
s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len);
|
||||||
s_iperf_ctrl.buffer = calloc(1, s_iperf_ctrl.buffer_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Buffer Pattern
|
|
||||||
if (s_iperf_ctrl.buffer) {
|
if (s_iperf_ctrl.buffer) {
|
||||||
iperf_pattern(s_iperf_ctrl.buffer, s_iperf_ctrl.buffer_len);
|
iperf_pattern(s_iperf_ctrl.buffer, s_iperf_ctrl.buffer_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_iperf_event_group == NULL) {
|
if (s_iperf_event_group == NULL) s_iperf_event_group = xEventGroupCreate();
|
||||||
s_iperf_event_group = xEventGroupCreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
xTaskCreate(iperf_task, "iperf", 4096, &s_iperf_ctrl, 5, &s_iperf_task_handle);
|
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) {
|
if (s_iperf_task_handle) {
|
||||||
s_iperf_ctrl.finish = true;
|
s_iperf_ctrl.finish = true;
|
||||||
if (s_iperf_event_group) xEventGroupSetBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
|
if (s_iperf_event_group) xEventGroupSetBits(s_iperf_event_group, IPERF_STOP_REQ_BIT);
|
||||||
} else {
|
|
||||||
printf("IPERF_STOPPED\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
#ifndef IPERF_H
|
||||||
#define IPERF_H
|
#define IPERF_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include "esp_err.h"
|
||||||
#include "led_strip.h"
|
#include "led_strip.h"
|
||||||
|
|
||||||
// --- Configuration Flags ---
|
// --- Configuration Flags ---
|
||||||
|
|
@ -11,84 +44,52 @@
|
||||||
#define IPERF_FLAG_TCP (1 << 2)
|
#define IPERF_FLAG_TCP (1 << 2)
|
||||||
#define IPERF_FLAG_UDP (1 << 3)
|
#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 ---
|
// --- Defaults ---
|
||||||
#define IPERF_DEFAULT_PORT 5001
|
#define IPERF_DEFAULT_PORT 5001
|
||||||
#define IPERF_DEFAULT_INTERVAL 3
|
#define IPERF_UDP_TX_LEN 1470
|
||||||
#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"
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t flag;
|
uint32_t flag;
|
||||||
uint32_t dip;
|
uint32_t dip; // Destination IP
|
||||||
uint16_t dport;
|
uint16_t dport; // Destination Port
|
||||||
uint32_t time;
|
uint32_t target_pps; // Packets Per Second (Replaces period)
|
||||||
uint32_t pacing_period_us;
|
uint32_t burst_count; // Packets per RTOS tick
|
||||||
uint32_t burst_count;
|
uint32_t send_len; // Packet payload length
|
||||||
uint32_t send_len;
|
|
||||||
} iperf_cfg_t;
|
} iperf_cfg_t;
|
||||||
|
|
||||||
// --- Stats Structure ---
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool running;
|
bool running;
|
||||||
uint32_t config_pps;
|
uint32_t config_pps;
|
||||||
uint32_t actual_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;
|
} 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 ---
|
// --- API ---
|
||||||
|
|
||||||
void iperf_init_led(led_strip_handle_t handle);
|
// Initialization (Call this in app_main to load NVS)
|
||||||
void iperf_set_pps(uint32_t pps);
|
void iperf_param_init(void);
|
||||||
uint32_t iperf_get_pps(void);
|
|
||||||
|
|
||||||
// Get snapshot of current stats
|
// Parameter Management (Running Config)
|
||||||
void iperf_get_stats(iperf_stats_t *stats);
|
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_print_status(void);
|
||||||
|
|
||||||
void iperf_start(iperf_cfg_t *cfg);
|
// Utils
|
||||||
void iperf_stop(void);
|
void iperf_init_led(led_strip_handle_t handle);
|
||||||
|
|
||||||
|
// Erase NVS and reset RAM defaults
|
||||||
|
void iperf_param_clear(void);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -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 "status_led.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
@ -5,6 +37,8 @@
|
||||||
#include "led_strip.h"
|
#include "led_strip.h"
|
||||||
#include "esp_log.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 led_strip_handle_t s_led_strip = NULL;
|
||||||
static bool s_is_rgb = false;
|
static bool s_is_rgb = false;
|
||||||
static int s_gpio_pin = -1;
|
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)); }
|
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); }
|
else { set_color(1,1,1); vTaskDelay(100); set_color(0,0,0); vTaskDelay(100); }
|
||||||
break;
|
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;
|
set_color(0, 0, toggle ? 50 : 0); toggle = !toggle;
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
break;
|
break;
|
||||||
case LED_STATE_CONNECTED: // Green Solid
|
case LED_STATE_CONNECTED:
|
||||||
set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
|
set_color(0, 25, 0); vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
break;
|
break;
|
||||||
case LED_STATE_MONITORING: // Blue Solid
|
case LED_STATE_MONITORING:
|
||||||
set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
|
set_color(0, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
break;
|
break;
|
||||||
case LED_STATE_TRANSMITTING: // Fast Purple (Busy)
|
case LED_STATE_TRANSMITTING:
|
||||||
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
|
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
|
||||||
vTaskDelay(pdMS_TO_TICKS(50));
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
break;
|
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;
|
set_color(toggle ? 50 : 0, 0, toggle ? 50 : 0); toggle = !toggle;
|
||||||
vTaskDelay(pdMS_TO_TICKS(250));
|
vTaskDelay(pdMS_TO_TICKS(250));
|
||||||
break;
|
break;
|
||||||
case LED_STATE_STALLED: // Purple Solid
|
case LED_STATE_STALLED:
|
||||||
set_color(50, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
|
set_color(50, 0, 50); vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
break;
|
break;
|
||||||
case LED_STATE_FAILED: // Red Blink
|
case LED_STATE_FAILED:
|
||||||
set_color(toggle ? 50 : 0, 0, 0); toggle = !toggle;
|
set_color(toggle ? 50 : 0, 0, 0); toggle = !toggle;
|
||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
break;
|
break;
|
||||||
|
|
@ -59,11 +94,21 @@ static void led_task(void *arg) {
|
||||||
void status_led_init(int gpio_pin, bool is_rgb_strip) {
|
void status_led_init(int gpio_pin, bool is_rgb_strip) {
|
||||||
s_gpio_pin = gpio_pin;
|
s_gpio_pin = gpio_pin;
|
||||||
s_is_rgb = is_rgb_strip;
|
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) {
|
if (s_is_rgb) {
|
||||||
led_strip_config_t s_cfg = { .strip_gpio_num = gpio_pin, .max_leds = 1 };
|
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_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 {
|
} else {
|
||||||
gpio_reset_pin(gpio_pin);
|
gpio_reset_pin(gpio_pin);
|
||||||
gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT);
|
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);
|
xTaskCreate(led_task, "led_task", 2048, NULL, 5, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void status_led_set_state(led_state_t state) {
|
// ... Setters/Getters ...
|
||||||
s_current_state = state;
|
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; }
|
||||||
|
|
||||||
led_state_t status_led_get_state(void) {
|
|
||||||
return s_current_state;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
|
||||||
|
|
@ -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 <string.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/task.h"
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "nvs.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 ---
|
// --- Initialization ---
|
||||||
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 ---
|
|
||||||
|
|
||||||
void wifi_cfg_init(void) {
|
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,
|
// --- Apply Configuration ---
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool wifi_cfg_apply_from_nvs(void) {
|
bool wifi_cfg_apply_from_nvs(void) {
|
||||||
char ssid[64]={0}, pass[64]={0}, ip[32]={0}, mask[32]={0}, gw[32]={0};
|
nvs_handle_t h;
|
||||||
char band[16]={0}, bw[16]={0}, powersave[16]={0}, mode[16]={0};
|
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
|
||||||
uint8_t mon_ch = 36; bool dhcp = true;
|
return false; // No config found
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
if (nvs_get_str(h, "wifi_pass", NULL, &len) == ESP_OK && len > 0) {
|
||||||
esp_wifi_init(&cfg);
|
pass = malloc(len);
|
||||||
|
nvs_get_str(h, "wifi_pass", pass, &len);
|
||||||
|
}
|
||||||
|
|
||||||
wifi_config_t wcfg = {0};
|
wifi_config_t wifi_config = {0};
|
||||||
strlcpy((char*)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
|
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
|
||||||
strlcpy((char*)wcfg.sta.password, pass, sizeof(wcfg.sta.password));
|
wifi_config.sta.ssid[sizeof(wifi_config.sta.ssid) - 1] = '\0';
|
||||||
wcfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
if (pass) {
|
||||||
wcfg.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
|
strncpy((char *)wifi_config.sta.password, pass, sizeof(wifi_config.sta.password) - 1);
|
||||||
wcfg.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
wifi_config.sta.password[sizeof(wifi_config.sta.password) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
esp_wifi_set_mode(WIFI_MODE_STA);
|
ESP_LOGI(TAG, "Applying WiFi Config: SSID=%s", ssid);
|
||||||
esp_wifi_set_config(WIFI_IF_STA, &wcfg);
|
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
|
||||||
|
|
||||||
if (!dhcp && ip[0]) apply_ip_static(ip, mask, gw);
|
free(ssid);
|
||||||
else esp_netif_dhcpc_start(sta_netif);
|
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);
|
nvs_close(h);
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// --- Initialization ---
|
// Init
|
||||||
void wifi_cfg_init(void);
|
void wifi_cfg_init(void);
|
||||||
|
|
||||||
// --- Getters (Used by Controller) ---
|
// Apply
|
||||||
bool wifi_cfg_apply_from_nvs(void);
|
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) ---
|
// Getters
|
||||||
void wifi_cfg_set_credentials(const char* ssid, const char* pass);
|
bool wifi_cfg_get_mode(char *mode_out, uint8_t *channel_out);
|
||||||
void wifi_cfg_set_static_ip(const char* ip, const char* mask, const char* gw);
|
|
||||||
void wifi_cfg_set_dhcp(bool enable);
|
// 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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif // WIFI_CFG_H
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
idf_component_register(SRCS "wifi_controller.c"
|
idf_component_register(SRCS "wifi_controller.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES esp_wifi freertos
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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 "wifi_controller.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
#include "inttypes.h"
|
#include "inttypes.h"
|
||||||
|
#include "wifi_cfg.h"
|
||||||
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
#include "iperf.h"
|
#include "iperf.h"
|
||||||
|
|
@ -11,7 +46,6 @@
|
||||||
#include "wifi_monitor.h"
|
#include "wifi_monitor.h"
|
||||||
#include "gps_sync.h"
|
#include "gps_sync.h"
|
||||||
|
|
||||||
// 1. GUARDED INCLUDE
|
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
#include "csi_manager.h"
|
#include "csi_manager.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -19,37 +53,38 @@
|
||||||
static const char *TAG = "WIFI_CTL";
|
static const char *TAG = "WIFI_CTL";
|
||||||
|
|
||||||
static wifi_ctl_mode_t s_current_mode = WIFI_CTL_MODE_STA;
|
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 bool s_monitor_enabled = false;
|
||||||
static uint32_t s_monitor_frame_count = 0;
|
static uint32_t s_monitor_frame_count = 0;
|
||||||
static TaskHandle_t s_monitor_stats_task_handle = NULL;
|
static TaskHandle_t s_monitor_stats_task_handle = NULL;
|
||||||
|
|
||||||
// --- Helper: Log Collapse Events ---
|
// --- Event Handler ---
|
||||||
static void log_collapse_event(float nav_duration_us, int rssi, int retry) {
|
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
|
||||||
gps_timestamp_t ts = gps_get_timestamp();
|
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||||
// CSV Format: COLLAPSE,MonoMS,GpsMS,Synced,Duration,RSSI,Retry
|
ESP_LOGI(TAG, "Got IP -> LED Connected");
|
||||||
printf("COLLAPSE,%" PRIi64 ",%" PRIi64 ",%d,%.2f,%d,%d\n",
|
status_led_set_state(LED_STATE_CONNECTED);
|
||||||
ts.monotonic_ms,
|
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
ts.gps_ms,
|
if (s_current_mode == WIFI_CTL_MODE_STA) {
|
||||||
ts.synced ? 1 : 0,
|
status_led_set_state(LED_STATE_NO_CONFIG);
|
||||||
nav_duration_us,
|
}
|
||||||
rssi,
|
}
|
||||||
retry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 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) {
|
static void monitor_frame_callback(const wifi_frame_info_t *frame, const uint8_t *payload, uint16_t len) {
|
||||||
s_monitor_frame_count++;
|
s_monitor_frame_count++;
|
||||||
|
|
||||||
// Check for Collapse conditions (High NAV + Retry)
|
|
||||||
if (frame->retry && frame->duration_id > 5000) {
|
if (frame->retry && frame->duration_id > 5000) {
|
||||||
log_collapse_event((float)frame->duration_id, frame->rssi, frame->retry);
|
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) {
|
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) {
|
if (wifi_monitor_get_stats(&stats) == ESP_OK) {
|
||||||
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
|
ESP_LOGI("MONITOR", "--- Stats: %lu frames, Retry: %.2f%%, Avg NAV: %u us ---",
|
||||||
(unsigned long)stats.total_frames, stats.retry_rate, stats.avg_nav);
|
(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) {
|
// --- Helper to apply IP settings ---
|
||||||
uint8_t channel = (uint8_t)(uintptr_t)arg;
|
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...");
|
if (wifi_cfg_get_dhcp()) {
|
||||||
// Wait until LED indicates connected
|
esp_netif_dhcpc_start(netif);
|
||||||
while (status_led_get_state() != LED_STATE_CONNECTED) {
|
} else {
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
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) {
|
void wifi_ctl_init(void) {
|
||||||
s_current_mode = WIFI_CTL_MODE_STA;
|
s_current_mode = WIFI_CTL_MODE_STA;
|
||||||
s_monitor_enabled = false;
|
s_monitor_enabled = false;
|
||||||
s_monitor_frame_count = 0;
|
s_monitor_frame_count = 0;
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bandwidth) {
|
// 1. Initialize Network Interface
|
||||||
if (s_current_mode == WIFI_CTL_MODE_MONITOR) {
|
esp_netif_create_default_wifi_sta();
|
||||||
ESP_LOGW(TAG, "Already in monitor mode");
|
|
||||||
return ESP_OK;
|
// 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
|
// Load Staging Params
|
||||||
if (bandwidth != WIFI_BW_HT20) {
|
char mode_ignored[16];
|
||||||
ESP_LOGW(TAG, "Forcing bandwidth to 20MHz for monitor mode");
|
wifi_cfg_get_mode(mode_ignored, &s_monitor_channel_staging);
|
||||||
bandwidth = WIFI_BW_HT20;
|
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);
|
ESP_LOGI(TAG, "Switching to MONITOR MODE (Ch %d)", channel);
|
||||||
|
|
||||||
// 1. Stop high-level apps
|
|
||||||
iperf_stop();
|
iperf_stop();
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
|
||||||
// 2. Disable CSI (hardware conflict)
|
|
||||||
// 2. GUARDED CALL
|
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
csi_mgr_disable();
|
csi_mgr_disable();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 3. Teardown Station
|
|
||||||
esp_wifi_disconnect();
|
esp_wifi_disconnect();
|
||||||
esp_wifi_stop();
|
esp_wifi_stop();
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
|
||||||
// 4. Re-init in NULL/Promiscuous Mode
|
|
||||||
esp_wifi_set_mode(WIFI_MODE_NULL);
|
esp_wifi_set_mode(WIFI_MODE_NULL);
|
||||||
|
|
||||||
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
|
if (wifi_monitor_init(channel, monitor_frame_callback) != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to init monitor mode");
|
ESP_LOGE(TAG, "Failed to init monitor mode");
|
||||||
return ESP_FAIL;
|
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) {
|
if (wifi_monitor_start() != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to start monitor mode");
|
ESP_LOGE(TAG, "Failed to start monitor mode");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update State
|
|
||||||
s_monitor_enabled = true;
|
s_monitor_enabled = true;
|
||||||
s_current_mode = WIFI_CTL_MODE_MONITOR;
|
s_current_mode = WIFI_CTL_MODE_MONITOR;
|
||||||
s_monitor_channel = channel;
|
s_monitor_channel_active = channel;
|
||||||
status_led_set_state(LED_STATE_MONITORING);
|
status_led_set_state(LED_STATE_MONITORING);
|
||||||
|
|
||||||
if (s_monitor_stats_task_handle == NULL) {
|
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;
|
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) {
|
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;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Switching to STA MODE");
|
ESP_LOGI(TAG, "Switching to STA MODE");
|
||||||
|
|
||||||
// 1. Stop Monitor Tasks
|
|
||||||
if (s_monitor_stats_task_handle != NULL) {
|
if (s_monitor_stats_task_handle != NULL) {
|
||||||
vTaskDelete(s_monitor_stats_task_handle);
|
vTaskDelete(s_monitor_stats_task_handle);
|
||||||
s_monitor_stats_task_handle = NULL;
|
s_monitor_stats_task_handle = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Stop Monitor Driver
|
|
||||||
if (s_monitor_enabled) {
|
if (s_monitor_enabled) {
|
||||||
wifi_monitor_stop();
|
wifi_monitor_stop();
|
||||||
s_monitor_enabled = false;
|
s_monitor_enabled = false;
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Re-enable Station Mode
|
|
||||||
esp_wifi_set_mode(WIFI_MODE_STA);
|
esp_wifi_set_mode(WIFI_MODE_STA);
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
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();
|
esp_wifi_start();
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
|
||||||
esp_wifi_connect();
|
esp_wifi_connect();
|
||||||
|
|
||||||
// 5. Update State
|
|
||||||
s_current_mode = WIFI_CTL_MODE_STA;
|
s_current_mode = WIFI_CTL_MODE_STA;
|
||||||
status_led_set_state(LED_STATE_WAITING);
|
status_led_set_state(LED_STATE_WAITING);
|
||||||
|
|
||||||
return ESP_OK;
|
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) {
|
void wifi_ctl_auto_monitor_start(uint8_t channel) {
|
||||||
xTaskCreate(auto_monitor_task_func, "auto_monitor", 4096, (void*)(uintptr_t)channel, 5, NULL);
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi_types.h" // Needed for wifi_bandwidth_t
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Types
|
||||||
typedef enum {
|
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_MONITOR
|
||||||
} wifi_ctl_mode_t;
|
} wifi_ctl_mode_t;
|
||||||
|
|
||||||
/**
|
// Init
|
||||||
* @brief Initialize the WiFi Controller
|
|
||||||
*/
|
|
||||||
void wifi_ctl_init(void);
|
void wifi_ctl_init(void);
|
||||||
|
|
||||||
/**
|
// Mode Control (Advanced)
|
||||||
* @brief Switch operation mode to Monitor (Sniffer)
|
esp_err_t wifi_ctl_switch_to_sta(void);
|
||||||
* @param channel WiFi channel (1-165)
|
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bw);
|
||||||
* @param bandwidth Bandwidth (usually WIFI_BW_HT20 for monitor)
|
|
||||||
*/
|
|
||||||
esp_err_t wifi_ctl_switch_to_monitor(uint8_t channel, wifi_bandwidth_t bandwidth);
|
|
||||||
|
|
||||||
/**
|
// Simple Wrappers (for cmd_monitor.c)
|
||||||
* @brief Switch operation mode to Station (Client)
|
void wifi_ctl_start_station(void);
|
||||||
* @param band_mode Band preference (Auto, 2G only, 5G only)
|
void wifi_ctl_start_ap(void);
|
||||||
*/
|
void wifi_ctl_monitor_start(int channel);
|
||||||
esp_err_t wifi_ctl_switch_to_sta(wifi_band_mode_t band_mode);
|
void wifi_ctl_stop(void);
|
||||||
|
|
||||||
/**
|
// Settings
|
||||||
* @brief Start the auto-monitor task
|
void wifi_ctl_set_channel(int channel);
|
||||||
* Waits for connection, waits for GPS, then switches to monitor mode.
|
void wifi_ctl_status(void);
|
||||||
* @param channel Channel to monitor
|
|
||||||
*/
|
|
||||||
void wifi_ctl_auto_monitor_start(uint8_t channel);
|
|
||||||
|
|
||||||
/**
|
// Params (NVS)
|
||||||
* @brief Get current operation mode
|
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);
|
wifi_ctl_mode_t wifi_ctl_get_mode(void);
|
||||||
|
int wifi_ctl_get_channel(void);
|
||||||
|
|
||||||
/**
|
// Deprecated / Compatibility
|
||||||
* @brief Get the current monitor channel
|
void wifi_ctl_auto_monitor_start(uint8_t 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);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 "wifi_monitor.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
|
|
|
||||||
|
|
@ -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
|
#ifndef WIFI_MONITOR_H
|
||||||
#define WIFI_MONITOR_H
|
#define WIFI_MONITOR_H
|
||||||
|
|
||||||
|
|
|
||||||
389
config_device.py
389
config_device.py
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
||||||
285
esp32_deploy.py
285
esp32_deploy.py
|
|
@ -12,6 +12,8 @@ import logging
|
||||||
import glob
|
import glob
|
||||||
import random
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from serial.tools import list_ports
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# Ensure detection script is available
|
# Ensure detection script is available
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
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}"
|
return f"{target}_{csi_str}_{ampdu_str}"
|
||||||
|
|
||||||
def auto_detect_devices():
|
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:
|
try:
|
||||||
ports = glob.glob('/dev/esp_port_*')
|
ports = glob.glob('/dev/esp_port_*')
|
||||||
if ports:
|
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
|
# 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)
|
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]
|
return [type('obj', (object,), {'device': p}) for p in ports]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -111,7 +135,8 @@ class UnifiedDeployWorker:
|
||||||
|
|
||||||
# --- Semaphore Released Here ---
|
# --- 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 not self.args.flash_only:
|
||||||
if self.args.ssid and self.args.password:
|
if self.args.ssid and self.args.password:
|
||||||
|
|
@ -147,10 +172,10 @@ class UnifiedDeployWorker:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
# Reset DTR/RTS logic
|
# Reset DTR/RTS logic (hardware reset timing - must use sleep)
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
writer.transport.serial.rts = True
|
writer.transport.serial.rts = True
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1) # Hardware timing requirement, not event-based
|
||||||
writer.transport.serial.rts = False
|
writer.transport.serial.rts = False
|
||||||
writer.transport.serial.dtr = False
|
writer.transport.serial.dtr = False
|
||||||
|
|
||||||
|
|
@ -160,36 +185,100 @@ class UnifiedDeployWorker:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 2. Send Configuration via CLI
|
# 2. Send Configuration via CLI
|
||||||
# Command: wifi_config -s "SSID" -p "PASS" -i "IP"
|
# Use separate commands: wifi connect and ip set
|
||||||
# Note: The Shell will auto-reboot after this command.
|
# First, connect to WiFi (saves credentials to NVS)
|
||||||
cmd = f'wifi_config -s "{self.args.ssid}" -p "{self.args.password}" -i "{self.target_ip}"'
|
# Password is optional, only include if provided
|
||||||
if not self.args.iperf_client and not self.args.iperf_server:
|
if self.args.password:
|
||||||
# If just connecting, maybe we want DHCP?
|
wifi_cmd = f'wifi connect "{self.args.ssid}" "{self.args.password}"'
|
||||||
# But if target_ip is set, we force static.
|
else:
|
||||||
pass
|
wifi_cmd = f'wifi connect "{self.args.ssid}"'
|
||||||
|
self.log.info(f"Sending: {wifi_cmd}")
|
||||||
self.log.info(f"Sending: {cmd}")
|
writer.write(f"{wifi_cmd}\n".encode())
|
||||||
writer.write(f"{cmd}\n".encode())
|
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
# 3. Wait for the reboot and new prompt
|
# Wait for "Connecting to..." message (command acknowledged)
|
||||||
# The device prints "Rebooting..." then restarts.
|
match_idx, output = await self._wait_for_pattern(reader, ["Connecting to"], timeout=2.0)
|
||||||
self.log.info("Waiting for reboot...")
|
if match_idx is None:
|
||||||
await asyncio.sleep(3.0) # Give it time to actually reset
|
self.log.warning("WiFi connect command response not detected, continuing...")
|
||||||
|
|
||||||
if not await self._wait_for_prompt(reader, writer, timeout=20):
|
# Second, set static IP address (if IP is provided)
|
||||||
self.log.error("Device did not return to prompt after reboot.")
|
if self.target_ip:
|
||||||
return False
|
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
|
# 3. Wait for prompt to ensure command completed (may take a moment for WiFi to start connecting)
|
||||||
# The new firmware does not auto-start iperf on boot unless commanded.
|
# 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:
|
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")
|
writer.write(b"iperf start\n")
|
||||||
await writer.drain()
|
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
|
return True
|
||||||
|
|
||||||
|
|
@ -222,6 +311,48 @@ class UnifiedDeployWorker:
|
||||||
break
|
break
|
||||||
return False
|
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]
|
# [Keep _query_version, _identify_chip, _erase_flash, _flash_firmware AS IS]
|
||||||
async def _query_version(self):
|
async def _query_version(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -230,7 +361,8 @@ class UnifiedDeployWorker:
|
||||||
writer.transport.serial.rts = False
|
writer.transport.serial.rts = False
|
||||||
writer.write(b'\n')
|
writer.write(b'\n')
|
||||||
await writer.drain()
|
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')
|
writer.write(b'version\n')
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
|
|
@ -344,6 +476,81 @@ class UnifiedDeployWorker:
|
||||||
self.log.error(f"Flash Prep Error: {e}")
|
self.log.error(f"Flash Prep Error: {e}")
|
||||||
return False
|
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():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
|
parser = argparse.ArgumentParser(description='ESP32 Unified Deployment Tool')
|
||||||
parser.add_argument('-i', '--interactive', action='store_true', help='Prompt for build options')
|
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('-M', '--mode', default='STA')
|
||||||
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
parser.add_argument('-mc', '--monitor-channel', type=int, default=36)
|
||||||
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
parser.add_argument('--csi', dest='csi_enable', action='store_true')
|
||||||
|
parser.add_argument('--map-ports', action='store_true', help="Rescan USB topology and generate udev rules for esp_port_xx")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.target != 'all' and not args.start_ip and not args.check_version:
|
if args.target != 'all' and not args.start_ip and not args.check_version and not args.map_ports:
|
||||||
parser.error("the following arguments are required: --start-ip")
|
parser.error("the following arguments are required: --start-ip")
|
||||||
if args.config_only and args.flash_only: parser.error("Conflicting modes")
|
if args.config_only and args.flash_only: parser.error("Conflicting modes")
|
||||||
return args
|
return args
|
||||||
|
|
@ -553,9 +761,22 @@ async def run_deployment(args):
|
||||||
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
print(f"\n{Colors.BLUE}Summary: {success}/{len(devs)} Success{Colors.RESET}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if os.name == 'nt': asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
args = parse_args()
|
||||||
try: asyncio.run(run_deployment(parse_args()))
|
|
||||||
except KeyboardInterrupt: sys.exit(1)
|
# --- 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__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
187
flash_all.py
187
flash_all.py
|
|
@ -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()
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -1,68 +1,249 @@
|
||||||
#!/usr/bin/env python3
|
#!/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 os
|
||||||
import pyudev
|
import re
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from serial.tools import list_ports
|
||||||
|
|
||||||
def generate_rules():
|
class Colors:
|
||||||
context = pyudev.Context()
|
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 = []
|
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
|
# Get all USB serial devices
|
||||||
# The 'DEVPATH' usually looks like .../usb1/1-2/1-2.3/1-2.3.4...
|
usb_devices = list(list_ports.grep("USB|ACM|CP210|FT232"))
|
||||||
devices.sort(key=lambda x: x.properties.get('DEVPATH', ''))
|
|
||||||
|
|
||||||
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 = []
|
rules = []
|
||||||
hub_counter = 1
|
port_num = start_port
|
||||||
port_counter = 1
|
|
||||||
last_parent_path = None
|
|
||||||
|
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
# Get the unique physical path identifier (KERNELS)
|
symlink = f"esp_port_{port_num}"
|
||||||
# We need the parent USB interface kernel name (e.g., '1-1.2:1.0')
|
rule = f'SUBSYSTEM=="tty", ENV{{ID_PATH}}=="{dev["id_path"]}", SYMLINK+="{symlink}"'
|
||||||
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}"'
|
|
||||||
rules.append(rule)
|
rules.append(rule)
|
||||||
print(f"Mapped {dev.device_node} ({kernels_path}) -> /dev/esp_port_{len(rules):02d}")
|
port_num += 1
|
||||||
|
|
||||||
# Write to file
|
return rules
|
||||||
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")
|
|
||||||
|
|
||||||
print(f"\n{'-'*60}")
|
def main():
|
||||||
print(f"SUCCESS: Generated {len(rules)} rules in '99-esp32-static.rules'.")
|
import argparse
|
||||||
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/")
|
parser = argparse.ArgumentParser(
|
||||||
print(f" 3. sudo udevadm control --reload-rules")
|
description='Generate or update udev rules for ESP32 devices',
|
||||||
print(f" 4. sudo udevadm trigger")
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
print(f"{'-'*60}")
|
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__':
|
if __name__ == '__main__':
|
||||||
# Requires 'pyudev'. Install with: sudo dnf install python3-pyudev (or pip install pyudev)
|
sys.exit(main())
|
||||||
try:
|
|
||||||
import pyudev
|
|
||||||
generate_rules()
|
|
||||||
except ImportError:
|
|
||||||
print("Error: This script requires 'pyudev'.")
|
|
||||||
print("Install it via: pip install pyudev")
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
#ifndef BOARD_CONFIG_H
|
||||||
#define BOARD_CONFIG_H
|
#define BOARD_CONFIG_H
|
||||||
|
|
||||||
#include "sdkconfig.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 27 // Common addressable LED pin for C5
|
||||||
#define RGB_LED_GPIO 8 // Common addressable LED pin for C5
|
|
||||||
#define HAS_RGB_LED 1
|
#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)
|
// 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
|
#define RGB_LED_GPIO 48
|
||||||
// Most S3 DevKits use GPIO 48 for the addressable RGB LED.
|
#define HAS_RGB_LED 1
|
||||||
// If yours uses GPIO 38, change this value.
|
#define GPS_TX_PIN GPIO_NUM_5
|
||||||
#define RGB_LED_GPIO 48
|
#define GPS_RX_PIN GPIO_NUM_4
|
||||||
#define HAS_RGB_LED 1
|
#define GPS_PPS_PIN GPIO_NUM_6
|
||||||
#endif
|
#elif defined (CONFIG_IDF_TARGET_ESP32)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ESP32 (Original / Standard)
|
// 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
|
#define RGB_LED_GPIO 2 // Standard Blue LED
|
||||||
// Standard ESP32 DevKits usually have a single blue LED on GPIO 2.
|
#define HAS_RGB_LED 0 // Not RGB
|
||||||
// They rarely have an addressable RGB LED built-in.
|
#define GPS_TX_PIN GPIO_NUM_17
|
||||||
#define RGB_LED_GPIO 2
|
#define GPS_RX_PIN GPIO_NUM_16
|
||||||
#define HAS_RGB_LED 0
|
#define GPS_PPS_PIN GPIO_NUM_4
|
||||||
#endif
|
#else
|
||||||
|
// Fallback
|
||||||
// ============================================================================
|
#define RGB_LED_GPIO 8
|
||||||
// Fallbacks (Prevent Compilation Errors)
|
#define HAS_RGB_LED 1
|
||||||
// ============================================================================
|
#define GPS_TX_PIN GPIO_NUM_1
|
||||||
#ifndef RGB_LED_GPIO
|
#define GPS_RX_PIN GPIO_NUM_3
|
||||||
#define RGB_LED_GPIO 2
|
#define GPS_PPS_PIN GPIO_NUM_5
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAS_RGB_LED
|
|
||||||
#define HAS_RGB_LED 0
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif // BOARD_CONFIG_H
|
#endif // BOARD_CONFIG_H
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
146
main/main.c
146
main/main.c
|
|
@ -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 <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/time.h> // Added for gettimeofday
|
||||||
|
#include <time.h> // Added for time structs
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
|
#include "esp_err.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_console.h"
|
#include "esp_console.h"
|
||||||
#include "esp_vfs_dev.h"
|
#include "esp_vfs_dev.h"
|
||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
|
#include "nvs.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
#include "status_led.h"
|
#include "status_led.h"
|
||||||
#include "board_config.h"
|
#include "board_config.h"
|
||||||
|
#include "gps_sync.h"
|
||||||
#include "wifi_controller.h"
|
#include "wifi_controller.h"
|
||||||
#include "wifi_cfg.h"
|
#include "wifi_cfg.h"
|
||||||
#include "app_console.h"
|
#include "app_console.h"
|
||||||
|
#include "iperf.h"
|
||||||
|
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
#include "csi_log.h"
|
#include "csi_log.h"
|
||||||
#include "csi_manager.h"
|
#include "csi_manager.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define APP_VERSION "2.0.0-SHELL"
|
#define APP_VERSION "2.1.0-CONSOLE-DEBUG"
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
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 ---
|
// --- System Commands ---
|
||||||
|
|
||||||
static int cmd_restart(int argc, char **argv) {
|
static int cmd_restart(int argc, char **argv) {
|
||||||
|
|
@ -69,42 +151,78 @@ void app_main(void) {
|
||||||
}
|
}
|
||||||
ESP_ERROR_CHECK(ret);
|
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_netif_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
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_init(RGB_LED_GPIO, HAS_RGB_LED);
|
||||||
|
status_led_set_state(LED_STATE_FAILED); // Force Red Blink
|
||||||
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
#ifdef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
ESP_ERROR_CHECK(csi_log_init());
|
ESP_ERROR_CHECK(csi_log_init());
|
||||||
csi_mgr_init();
|
csi_mgr_init();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 4. Initialize WiFi Controller (Loads config from NVS automatically)
|
// 5. Initialize WiFi Controller & iPerf
|
||||||
wifi_ctl_init();
|
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_t *repl = NULL;
|
||||||
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
|
||||||
|
repl_config.prompt = s_cli_prompt;
|
||||||
// This prompt is the anchor for your Python script
|
|
||||||
repl_config.prompt = "esp32> ";
|
|
||||||
repl_config.max_cmdline_length = 1024;
|
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_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();
|
register_system_common();
|
||||||
app_console_register_commands();
|
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("\n ==================================================\n");
|
||||||
printf(" | ESP32 iPerf Shell - Ready |\n");
|
printf(" | ESP32 iPerf Shell - Ready |\n");
|
||||||
printf(" | Type 'help' for commands |\n");
|
printf(" | Type 'help' for commands |\n");
|
||||||
printf(" ==================================================\n");
|
printf(" ==================================================\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
// This function runs the REPL loop and does not return
|
esp_err_t repl_err = esp_console_start_repl(repl);
|
||||||
ESP_ERROR_CHECK(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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
244
map_usb_to_ip.py
244
map_usb_to_ip.py
|
|
@ -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()
|
|
||||||
319
mass_deploy.py
319
mass_deploy.py
|
|
@ -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()
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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="Wi‑Fi SSID")
|
|
||||||
parser.add_argument("--password", default="ez2remember", help="Wi‑Fi 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 Wi‑Fi 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()
|
|
||||||
|
|
@ -7,3 +7,14 @@ CONFIG_FREERTOS_HZ=1000
|
||||||
CONFIG_CONSOLE_UART_RX_BUF_SIZE=1024
|
CONFIG_CONSOLE_UART_RX_BUF_SIZE=1024
|
||||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
CONFIG_ESP_WIFI_CSI_ENABLED=n
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue