From 90e212fed6647f34317d2e9775fae6b39080a3a9 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 15 Nov 2025 12:12:33 -0800 Subject: [PATCH] Add flash_and_config.py and WiFi reconfiguration system --- flash_and_config.py | 198 ++++++++++++++++++++++++++++++++++++++++++++ main/CMakeLists.txt | 8 +- main/wifi_cfg.c | 161 +++++++++++++++++++++++++++++++++++ main/wifi_cfg.h | 15 ++++ 4 files changed, 377 insertions(+), 5 deletions(-) create mode 100755 flash_and_config.py create mode 100644 main/wifi_cfg.c create mode 100644 main/wifi_cfg.h diff --git a/flash_and_config.py b/flash_and_config.py new file mode 100755 index 0000000..2f67ea0 --- /dev/null +++ b/flash_and_config.py @@ -0,0 +1,198 @@ +#!/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()) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ac46b82..6a68988 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,3 @@ -idf_component_register( - SRCS "main.c" - INCLUDE_DIRS "." - PRIV_REQUIRES esp_driver_gpio -) +idf_component_register(SRCS "main.c" "iperf.c" "wifi_cfg.c" + INCLUDE_DIRS "." + REQUIRES led_strip driver) diff --git a/main/wifi_cfg.c b/main/wifi_cfg.c new file mode 100644 index 0000000..ba44ecc --- /dev/null +++ b/main/wifi_cfg.c @@ -0,0 +1,161 @@ + +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "nvs.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "driver/uart.h" +#include "esp_vfs_dev.h" +#include "driver/usb_serial_jtag.h" +#include "esp_vfs_usb_serial_jtag.h" + +static const char *TAG = "wifi_cfg"; + +static void trim(char *s){ + int n = strlen(s); + while(n>0 && (s[n-1]=='\r' || s[n-1]=='\n' || isspace((unsigned char)s[n-1]))) s[--n]=0; + while(*s && isspace((unsigned char)*s)) memmove(s, s+1, strlen(s)); +} + +static esp_err_t nvs_set_str2(nvs_handle_t h, const char *key, const char *val){ + return val ? nvs_set_str(h, key, val) : nvs_erase_key(h, key); +} + +static bool cfg_dhcp = true; + +static void save_cfg(const char* ssid, const char* pass, const char* ip, const char* mask, const char* gw, bool dhcp){ + nvs_handle_t h; + if (nvs_open("netcfg", NVS_READWRITE, &h) != ESP_OK) return; + if (ssid) nvs_set_str2(h, "ssid", ssid); + if (pass) nvs_set_str2(h, "pass", pass); + if (ip) nvs_set_str2(h, "ip", ip); + if (mask) nvs_set_str2(h, "mask", mask); + if (gw) nvs_set_str2(h, "gw", gw); + nvs_set_u8(h, "dhcp", dhcp ? 1 : 0); + nvs_commit(h); + nvs_close(h); + cfg_dhcp = dhcp; +} + +static bool load_cfg(char* ssid, size_t ssz, char* pass, size_t psz, char* ip, size_t isz, char* mask, size_t msz, char* gw, size_t gsz, bool* dhcp){ + nvs_handle_t h; + if (nvs_open("netcfg", NVS_READONLY, &h) != ESP_OK) return false; + size_t len; + esp_err_t e; + if ((e = nvs_get_str(h, "ssid", NULL, &len)) != ESP_OK){ nvs_close(h); return false; } + if (len >= ssz){ nvs_close(h); return false; } + nvs_get_str(h, "ssid", ssid, &len); + len = psz; e = nvs_get_str(h, "pass", pass, &len); if (e!=ESP_OK) pass[0]=0; + len = isz; e = nvs_get_str(h, "ip", ip, &len); if (e!=ESP_OK) ip[0]=0; + len = msz; e = nvs_get_str(h, "mask", mask, &len); if (e!=ESP_OK) mask[0]=0; + len = gsz; e = nvs_get_str(h, "gw", gw, &len); if (e!=ESP_OK) gw[0]=0; + uint8_t d=1; nvs_get_u8(h, "dhcp", &d); *dhcp = (d!=0); + nvs_close(h); + return true; +} + +void wifi_cfg_force_dhcp(bool enable){ cfg_dhcp = enable; } + +static esp_netif_t *sta_netif = NULL; + +static void apply_ip_static(const char* ip, const char* mask, const char* gw){ + if (!sta_netif) return; + esp_netif_ip_info_t info = {0}; + esp_netif_dhcpc_stop(sta_netif); + esp_ip4addr_aton(ip, &info.ip.addr); + esp_ip4addr_aton(mask, &info.netmask.addr); + esp_ip4addr_aton(gw, &info.gw.addr); + ESP_ERROR_CHECK( esp_netif_set_ip_info(sta_netif, &info) ); +} + +bool wifi_cfg_apply_from_nvs(void){ + char ssid[64]={0}, pass[64]={0}, ip[32]={0}, mask[32]={0}, gw[32]={0}; + bool dhcp = true; + if (!load_cfg(ssid,sizeof(ssid), pass,sizeof(pass), ip,sizeof(ip), mask,sizeof(mask), gw,sizeof(gw), &dhcp)){ + ESP_LOGW(TAG, "No Wi‑Fi config in NVS"); + return false; + } + ESP_LOGI(TAG, "Applying Wi‑Fi config: SSID=%s DHCP=%d IP=%s", ssid, dhcp, ip); + + static bool inited = false; + if (!inited){ + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + sta_netif = esp_netif_create_default_wifi_sta(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK( esp_wifi_init(&cfg) ); + inited = true; + } + + wifi_config_t wcfg = {0}; + strncpy((char*)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid)-1); + strncpy((char*)wcfg.sta.password, pass, sizeof(wcfg.sta.password)-1); + wcfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + wcfg.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH; + ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wcfg) ); + + if (!dhcp && ip[0] && mask[0] && gw[0]){ + apply_ip_static(ip, mask, gw); + }else{ + if (sta_netif) esp_netif_dhcpc_start(sta_netif); + } + + ESP_ERROR_CHECK( esp_wifi_start() ); + ESP_ERROR_CHECK( esp_wifi_connect() ); + return true; +} + +static void cfg_listener_task(void *arg){ + esp_vfs_usb_serial_jtag_use_driver(); + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + + char line[160]; + char ssid[64]={0}, pass[64]={0}, ip[32]={0}, mask[32]={0}, gw[32]={0}; + bool dhcp = true; + bool in_cfg = false; + + while (1){ + if (!fgets(line, sizeof(line), stdin)){ + vTaskDelay(pdMS_TO_TICKS(50)); + continue; + } + trim(line); + if (!in_cfg){ + if (strcmp(line, "CFG")==0){ + in_cfg = true; + ssid[0]=pass[0]=ip[0]=mask[0]=gw[0]=0; + dhcp = true; + } + continue; + } + if (strcmp(line, "END")==0){ + save_cfg(ssid, pass, ip, mask, gw, dhcp); + printf("OK\n"); + wifi_cfg_apply_from_nvs(); + in_cfg = false; + continue; + } + if (strncmp(line, "SSID:",5)==0){ strncpy(ssid, line+5, sizeof(ssid)-1); continue; } + if (strncmp(line, "PASS:",5)==0){ strncpy(pass, line+5, sizeof(pass)-1); continue; } + if (strncmp(line, "IP:",3)==0){ strncpy(ip, line+3, sizeof(ip)-1); continue; } + if (strncmp(line, "MASK:",5)==0){ strncpy(mask, line+5, sizeof(mask)-1); continue; } + if (strncmp(line, "GW:",3)==0){ strncpy(gw, line+3, sizeof(gw)-1); continue; } + if (strncmp(line, "DHCP:",5)==0){ dhcp = atoi(line+5) ? true:false; continue; } + } +} + +void wifi_cfg_init(void){ + nvs_flash_init(); + xTaskCreatePinnedToCore(cfg_listener_task, "cfg_listener", 4096, NULL, 5, NULL, tskNO_AFFINITY); +} diff --git a/main/wifi_cfg.h b/main/wifi_cfg.h new file mode 100644 index 0000000..3782d72 --- /dev/null +++ b/main/wifi_cfg.h @@ -0,0 +1,15 @@ + +#pragma once +#ifdef __cplusplus +extern "C" { +#endif + +#include + +void wifi_cfg_init(void); // starts serial listener task +bool wifi_cfg_apply_from_nvs(void); // reads saved config and connects Wi‑Fi +void wifi_cfg_force_dhcp(bool enable); // for testing + +#ifdef __cplusplus +} +#endif