more scripts
This commit is contained in:
parent
289efa97a6
commit
42d0ce9a7b
|
|
@ -0,0 +1,198 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
ESP32 WiFi Configuration Tool - With verbose mode
|
||||||
|
"""
|
||||||
|
|
||||||
|
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", verbose=False):
|
||||||
|
"""Configure ESP32 device via serial"""
|
||||||
|
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"ESP32 WiFi Configuration")
|
||||||
|
print(f"{'='*70}")
|
||||||
|
print(f"Port: {port}")
|
||||||
|
print(f"SSID: {ssid}")
|
||||||
|
print(f"Password: {'*' * len(password)}")
|
||||||
|
print(f"IP: {ip}")
|
||||||
|
print(f"Gateway: {gateway}")
|
||||||
|
print(f"Netmask: {netmask}")
|
||||||
|
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
|
||||||
|
config_lines = [
|
||||||
|
"CFG",
|
||||||
|
f"SSID:{ssid}",
|
||||||
|
f"PASS:{password}",
|
||||||
|
f"IP:{ip}",
|
||||||
|
f"MASK:{netmask}",
|
||||||
|
f"GW:{gateway}",
|
||||||
|
"DHCP: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...")
|
||||||
|
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("Sent! Waiting for response...")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
if ser.in_waiting:
|
||||||
|
response_size = ser.in_waiting
|
||||||
|
log_verbose(f"Response available: {response_size} bytes", verbose)
|
||||||
|
|
||||||
|
response = ser.read(response_size).decode('utf-8', errors='ignore')
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("[VERBOSE] Raw response:")
|
||||||
|
for line in response.split('\n')[:20]: # Show first 20 lines
|
||||||
|
if line.strip():
|
||||||
|
print(f"[VERBOSE] {line}")
|
||||||
|
|
||||||
|
# Check for key indicators
|
||||||
|
if "OK" in response:
|
||||||
|
print("✓ Device acknowledged configuration (OK)")
|
||||||
|
if "got ip:" in response.lower():
|
||||||
|
print("✓ Device connected to WiFi!")
|
||||||
|
# Extract IP from response
|
||||||
|
import re
|
||||||
|
ip_match = re.search(r'got ip:(\d+\.\d+\.\d+\.\d+)', response, re.IGNORECASE)
|
||||||
|
if ip_match:
|
||||||
|
print(f" Assigned IP: {ip_match.group(1)}")
|
||||||
|
if "connected" in response.lower():
|
||||||
|
print("✓ WiFi connection established")
|
||||||
|
if "failed" in response.lower() or "disconnect" in response.lower():
|
||||||
|
print("✗ WiFi connection may have failed")
|
||||||
|
if verbose:
|
||||||
|
print("[VERBOSE] Check response above for error details")
|
||||||
|
else:
|
||||||
|
log_verbose("No immediate response from device", verbose)
|
||||||
|
print("⚠ No response (device may still be processing)")
|
||||||
|
|
||||||
|
# Get final port stats
|
||||||
|
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"\nConfiguration sent to {port}")
|
||||||
|
print(f"Expected IP: {ip}")
|
||||||
|
print(f"Test with: ping {ip}")
|
||||||
|
print(f" iperf -c {ip}")
|
||||||
|
|
||||||
|
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? (idf.py monitor, screen, etc.)")
|
||||||
|
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 via serial',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Basic configuration
|
||||||
|
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.51
|
||||||
|
|
||||||
|
# With verbose output
|
||||||
|
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.51 -v
|
||||||
|
|
||||||
|
# Custom WiFi credentials
|
||||||
|
%(prog)s -p /dev/ttyUSB0 -i 192.168.1.52 -s MyWiFi -P mypass -v
|
||||||
|
|
||||||
|
# Custom gateway
|
||||||
|
%(prog)s -p /dev/ttyUSB0 -i 10.0.0.100 -g 10.0.0.1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
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')
|
||||||
|
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('-v', '--verbose', action='store_true',
|
||||||
|
help='Enable verbose output (show detailed debug info)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
success = config_device(
|
||||||
|
port=args.port,
|
||||||
|
ip=args.ip,
|
||||||
|
ssid=args.ssid,
|
||||||
|
password=args.password,
|
||||||
|
gateway=args.gateway,
|
||||||
|
netmask=args.netmask,
|
||||||
|
verbose=args.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -1,15 +1,5 @@
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c"
|
SRCS "main.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES
|
PRIV_REQUIRES esp_driver_gpio
|
||||||
esp_wifi
|
|
||||||
nvs_flash
|
|
||||||
esp_netif
|
|
||||||
console
|
|
||||||
lwip
|
|
||||||
driver
|
|
||||||
esp_driver_uart
|
|
||||||
vfs
|
|
||||||
iperf
|
|
||||||
wifi_cfg
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
145
main/main.c
145
main/main.c
|
|
@ -9,28 +9,139 @@
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "lwip/inet.h"
|
#include "lwip/inet.h"
|
||||||
|
|
||||||
|
#include "led_strip.h"
|
||||||
|
|
||||||
#include "iperf.h"
|
#include "iperf.h"
|
||||||
#include "wifi_cfg.h"
|
#include "wifi_cfg.h"
|
||||||
|
|
||||||
static const char *TAG = "main";
|
static const char *TAG = "main";
|
||||||
|
|
||||||
|
// WS2812 RGB LED - try both GPIO 38 and 48
|
||||||
|
#define RGB_LED_GPIO 48 // If this doesn't work, try 38
|
||||||
|
|
||||||
|
static led_strip_handle_t led_strip;
|
||||||
|
|
||||||
|
// WiFi connection status
|
||||||
|
static bool wifi_connected = false;
|
||||||
|
static bool has_config = false;
|
||||||
|
|
||||||
|
// LED states
|
||||||
|
typedef enum {
|
||||||
|
LED_STATE_NO_CONFIG, // Yellow - no WiFi config in NVS
|
||||||
|
LED_STATE_WAITING, // Blue slow blink - trying to connect
|
||||||
|
LED_STATE_CONNECTED, // Blue solid - connected successfully
|
||||||
|
LED_STATE_FAILED // Red fast blink - connection failed
|
||||||
|
} led_state_t;
|
||||||
|
|
||||||
|
static led_state_t current_led_state = LED_STATE_NO_CONFIG;
|
||||||
|
|
||||||
|
static void rgb_led_init(void)
|
||||||
|
{
|
||||||
|
led_strip_config_t strip_config = {
|
||||||
|
.strip_gpio_num = RGB_LED_GPIO,
|
||||||
|
.max_leds = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
led_strip_rmt_config_t rmt_config = {
|
||||||
|
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||||
|
led_strip_clear(led_strip);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "WS2812 RGB LED initialized on GPIO %d", RGB_LED_GPIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_led_color(uint8_t r, uint8_t g, uint8_t b)
|
||||||
|
{
|
||||||
|
led_strip_set_pixel(led_strip, 0, r, g, b);
|
||||||
|
led_strip_refresh(led_strip);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void led_task(void *arg)
|
||||||
|
{
|
||||||
|
int blink_state = 0;
|
||||||
|
|
||||||
|
while(1) {
|
||||||
|
switch(current_led_state) {
|
||||||
|
case LED_STATE_NO_CONFIG:
|
||||||
|
// Yellow solid - no WiFi config
|
||||||
|
set_led_color(255, 255, 0); // Red + Green = Yellow
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LED_STATE_WAITING:
|
||||||
|
// Blue slow blink - waiting for WiFi
|
||||||
|
if (blink_state) {
|
||||||
|
set_led_color(0, 0, 255); // Blue
|
||||||
|
} else {
|
||||||
|
set_led_color(0, 0, 0); // Off
|
||||||
|
}
|
||||||
|
blink_state = !blink_state;
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LED_STATE_CONNECTED:
|
||||||
|
// Blue solid - connected successfully
|
||||||
|
set_led_color(0, 0, 255); // Blue
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LED_STATE_FAILED:
|
||||||
|
// Red fast blink - connection failed
|
||||||
|
if (blink_state) {
|
||||||
|
set_led_color(255, 0, 0); // Red
|
||||||
|
} else {
|
||||||
|
set_led_color(0, 0, 0); // Off
|
||||||
|
}
|
||||||
|
blink_state = !blink_state;
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void event_handler(void* arg, esp_event_base_t event_base,
|
static void event_handler(void* arg, esp_event_base_t event_base,
|
||||||
int32_t event_id, void* event_data)
|
int32_t event_id, void* event_data)
|
||||||
{
|
{
|
||||||
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
if (event_base == WIFI_EVENT) {
|
||||||
|
switch (event_id) {
|
||||||
|
case WIFI_EVENT_STA_START:
|
||||||
|
ESP_LOGI(TAG, "WiFi started, attempting connection...");
|
||||||
|
if (has_config) {
|
||||||
|
current_led_state = LED_STATE_WAITING;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WIFI_EVENT_STA_DISCONNECTED:
|
||||||
|
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*) event_data;
|
||||||
|
ESP_LOGW(TAG, "WiFi disconnected, reason: %d", event->reason);
|
||||||
|
|
||||||
|
if (!wifi_connected && has_config) {
|
||||||
|
current_led_state = LED_STATE_FAILED;
|
||||||
|
ESP_LOGE(TAG, "WiFi connection FAILED - RED LED blinking");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||||
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
|
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
|
||||||
ESP_LOGI(TAG, "got ip:" IPSTR " gw:" IPSTR " netmask:" IPSTR,
|
ESP_LOGI(TAG, "got ip:" IPSTR " gw:" IPSTR " netmask:" IPSTR,
|
||||||
IP2STR(&event->ip_info.ip),
|
IP2STR(&event->ip_info.ip),
|
||||||
IP2STR(&event->ip_info.gw),
|
IP2STR(&event->ip_info.gw),
|
||||||
IP2STR(&event->ip_info.netmask));
|
IP2STR(&event->ip_info.netmask));
|
||||||
|
|
||||||
// Auto-start iperf server after getting IP
|
wifi_connected = true;
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Wait 1 second for stability
|
current_led_state = LED_STATE_CONNECTED;
|
||||||
|
ESP_LOGI(TAG, "WiFi CONNECTED - BLUE LED solid");
|
||||||
|
|
||||||
|
// Auto-start iperf server
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
|
||||||
iperf_cfg_t cfg;
|
iperf_cfg_t cfg;
|
||||||
memset(&cfg, 0, sizeof(cfg));
|
memset(&cfg, 0, sizeof(cfg));
|
||||||
|
|
@ -48,13 +159,37 @@ void app_main(void)
|
||||||
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());
|
||||||
|
|
||||||
|
// Initialize RGB LED
|
||||||
|
rgb_led_init();
|
||||||
|
|
||||||
|
// Start LED task
|
||||||
|
xTaskCreate(led_task, "led_task", 4096, NULL, 5, NULL);
|
||||||
|
|
||||||
|
// Register WiFi events
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||||
|
WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
|
||||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
|
IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
|
||||||
|
|
||||||
|
// Initialize WiFi config
|
||||||
wifi_cfg_init();
|
wifi_cfg_init();
|
||||||
wifi_cfg_apply_from_nvs();
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "System init complete. iperf server will start after WiFi connects.");
|
// Try to load config from NVS
|
||||||
|
if (wifi_cfg_apply_from_nvs()) {
|
||||||
|
has_config = true;
|
||||||
|
current_led_state = LED_STATE_WAITING;
|
||||||
|
ESP_LOGI(TAG, "WiFi config loaded from NVS");
|
||||||
|
} else {
|
||||||
|
has_config = false;
|
||||||
|
current_led_state = LED_STATE_NO_CONFIG;
|
||||||
|
ESP_LOGI(TAG, "No WiFi config - YELLOW LED");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "LED Status:");
|
||||||
|
ESP_LOGI(TAG, " YELLOW solid = NO CONFIG (send CFG/END)");
|
||||||
|
ESP_LOGI(TAG, " BLUE slow blink = Connecting");
|
||||||
|
ESP_LOGI(TAG, " BLUE solid = Connected ✓");
|
||||||
|
ESP_LOGI(TAG, " RED fast blink = Failed ✗");
|
||||||
|
|
||||||
while(1) {
|
while(1) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
#!/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()
|
||||||
|
|
@ -8,16 +8,18 @@ PASSWORD = "ez2remember"
|
||||||
START_IP = 51
|
START_IP = 51
|
||||||
|
|
||||||
devices = sorted(glob.glob('/dev/ttyUSB*'))
|
devices = sorted(glob.glob('/dev/ttyUSB*'))
|
||||||
print(f"Found {len(devices)} devices\n")
|
num_devices = len(devices)
|
||||||
|
ok_devices = 0
|
||||||
|
print(f"Found {num_devices} devices\n")
|
||||||
|
|
||||||
for idx, dev in enumerate(devices):
|
for idx, dev in enumerate(devices):
|
||||||
ip = f"192.168.1.{START_IP + idx}"
|
ip = f"192.168.1.{START_IP + idx}"
|
||||||
print(f"[{idx}] Configuring {dev} → {ip}")
|
print(f"[{idx}] Configuring {dev} → {ip}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ser = serial.Serial(dev, 115200, timeout=1)
|
ser = serial.Serial(dev, 115200, timeout=1)
|
||||||
time.sleep(0.5) # Let serial port stabilize
|
time.sleep(0.5) # Let serial port stabilize
|
||||||
|
|
||||||
# Send configuration
|
# Send configuration
|
||||||
ser.write(b"CFG\n")
|
ser.write(b"CFG\n")
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
@ -34,19 +36,20 @@ for idx, dev in enumerate(devices):
|
||||||
ser.write(b"DHCP:0\n")
|
ser.write(b"DHCP:0\n")
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
ser.write(b"END\n")
|
ser.write(b"END\n")
|
||||||
|
|
||||||
# Wait for OK response
|
# Wait for OK response
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
response = ser.read(100).decode('utf-8', errors='ignore')
|
response = ser.read(100).decode('utf-8', errors='ignore')
|
||||||
if 'OK' in response:
|
if 'OK' in response:
|
||||||
print(f" ✓ Got OK response")
|
print(f" ✓ Got OK response")
|
||||||
|
ok_devices += 1
|
||||||
ser.close()
|
ser.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ✗ Error: {e}")
|
print(f" ✗ Error: {e}")
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print(f"\nOk={ok_devices} Not ok={num_devices - ok_devices}")
|
||||||
print("\nWaiting 30s for connections...")
|
print("\nWaiting 30s for connections...")
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
print("Done! Test with: NUM_DEVICES=31 ./test_devices.sh")
|
print(f"Done! Test with: NUM_DEVICES={num_devices} ./test_devices.sh")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
#!/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()
|
||||||
Loading…
Reference in New Issue