diff --git a/LoggerFirmware/include/Configuration.h b/LoggerFirmware/include/Configuration.h index 3d11cdab..7d348188 100644 --- a/LoggerFirmware/include/Configuration.h +++ b/LoggerFirmware/include/Configuration.h @@ -89,6 +89,7 @@ class Config { CONFIG_STATION_DELAY_S, /* String: delay (seconds) before web-server attempts to re-joint a client network */ CONFIG_STATION_RETRIES_S,/* String: retries (int) before web-server reverts to "safe-mode". */ CONFIG_STATION_TIMEOUT_S,/* String: delay (seconds) before declaring a WiFi connection attempt failed */ + CONFIG_STATION_SCAN_INTERVAL_S,/* String: delay (seconds) between background hotspot scans in AP fallback mode */ CONFIG_WS_STATUS_S, /* String: status of the configuration web server */ CONFIG_WS_BOOTSTATUS_S, /* String: status of the configuration web server at boot time */ CONFIG_DEFAULTS_S, /* String: JSON-format for default "lab reset" parameters */ @@ -99,7 +100,8 @@ class Config { CONFIG_UPLOAD_INTERVAL_S,/* String: interval (seconds) between upload attempts */ CONFIG_UPLOAD_DURATION_S,/* String: duration (seconds) for each upload event */ CONFIG_UPLOAD_CERT_S, /* String: certificate to pass to upload server for authentication */ - CONFIG_MDNS_NAME_S /* String: recognition name for mDNS responder (hostname: name.local) */ + CONFIG_MDNS_NAME_S, /* String: recognition name for mDNS responder (hostname: name.local) */ + CONFIG_REQUIRE_PMF_S /* String: Require PMF for WPA3 connections (true/false) */ }; /// \brief Extract a configuration string for the specified parameter diff --git a/LoggerFirmware/src/Configuration.cpp b/LoggerFirmware/src/Configuration.cpp index 2b3a9d08..0c6f58aa 100644 --- a/LoggerFirmware/src/Configuration.cpp +++ b/LoggerFirmware/src/Configuration.cpp @@ -76,6 +76,7 @@ const String lookup[] = { "StationDelay", ///< Set the timeout between attempts of the webserver joining a client network "StationRetries", ///< Set number of join attempts before the webserver reverts to safe mode "StationTimeout", ///< Set the timeout for any connect attempt + "StationScanInterval", ///< Set the background scan interval (seconds) in AP fallback mode "WSStatus", ///< The current status of the configuration webserver "WSBootStatus", ///< The status of the webserver on boot "LabDefaults", ///< A JSON string for lab-default configuration @@ -86,7 +87,8 @@ const String lookup[] = { "UploadInterval", ///< Interval (seconds) between upload attempts "UploadDuration", ///< Time (seconds) for upload activity before diverting back to other efforts "UploadCert", ///< Certificate to pass to the upload server for TLS - "mDNSName" + "mDNSName", + "RequirePMF" ///< Require PMF for WPA3 (string) }; /// Default constructor. This sets up for a dummy parameter store, which is configured @@ -227,13 +229,14 @@ DynamicJsonDocument ConfigJSON::ExtractConfig(bool secure) params["enable"]["upload"] = upload_online; // String configurations for the various parameters in configuration - String wifi_station_delay, wifi_station_retries, wifi_station_timeout, wifi_ip_address, wifi_mode; + String wifi_station_delay, wifi_station_retries, wifi_station_timeout, wifi_station_scan_interval, wifi_ip_address, wifi_mode; String wifi_ap_ssid, wifi_ap_password, wifi_station_ssid, wifi_station_password, wifi_station_mdns_name; String moduleid, shipname, baudrate_port1, baudrate_port2, udp_bridge_port; LoggerConfig.GetConfigString(Config::CONFIG_STATION_DELAY_S, wifi_station_delay); LoggerConfig.GetConfigString(Config::CONFIG_STATION_RETRIES_S, wifi_station_retries); LoggerConfig.GetConfigString(Config::CONFIG_STATION_TIMEOUT_S, wifi_station_timeout); + LoggerConfig.GetConfigString(Config::CONFIG_STATION_SCAN_INTERVAL_S, wifi_station_scan_interval); LoggerConfig.GetConfigString(Config::CONFIG_MODULEID_S, moduleid); LoggerConfig.GetConfigString(Config::CONFIG_SHIPNAME_S, shipname); LoggerConfig.GetConfigString(Config::CONFIG_AP_SSID_S, wifi_ap_ssid); @@ -251,6 +254,7 @@ DynamicJsonDocument ConfigJSON::ExtractConfig(bool secure) params["wifi"]["station"]["delay"] = wifi_station_delay.toInt(); params["wifi"]["station"]["retries"] = wifi_station_retries.toInt(); params["wifi"]["station"]["timeout"] = wifi_station_timeout.toInt(); + params["wifi"]["station"]["scaninterval"] = wifi_station_scan_interval.toInt(); params["wifi"]["station"]["mdns"] = wifi_station_mdns_name; params["wifi"]["ssids"]["ap"] = wifi_ap_ssid; params["wifi"]["ssids"]["station"] = wifi_station_ssid; @@ -329,6 +333,8 @@ bool ConfigJSON::SetConfig(String const& json_string) LoggerConfig.SetConfigString(Config::CONFIG_STATION_RETRIES_S, params["wifi"]["station"]["retries"]); if (params["wifi"]["station"].containsKey("timeout")) LoggerConfig.SetConfigString(Config::CONFIG_STATION_TIMEOUT_S, params["wifi"]["station"]["timeout"]); + if (params["wifi"]["station"].containsKey("scaninterval")) + LoggerConfig.SetConfigString(Config::CONFIG_STATION_SCAN_INTERVAL_S, params["wifi"]["station"]["scaninterval"]); if (params["wifi"]["station"].containsKey("mdns")) LoggerConfig.SetConfigString(Config::CONFIG_MDNS_NAME_S, params["wifi"]["station"]["mdns"]); } diff --git a/LoggerFirmware/src/WiFiAdapter.cpp b/LoggerFirmware/src/WiFiAdapter.cpp index 9bbc332d..2fc99b36 100644 --- a/LoggerFirmware/src/WiFiAdapter.cpp +++ b/LoggerFirmware/src/WiFiAdapter.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +135,8 @@ class ConnectionStateMachine { Serial.printf("DBG: starting ConnectionStateMachine; boot status is %s\n", boot_status.c_str()); } m_lastConnectAttempt = m_lastStatusCheck = millis(); + m_lastScanTime = 0; + m_scanStarted = false; m_connectionRetries = maximumReties(); m_retryDelay = retryDelay(); @@ -177,7 +180,50 @@ class ConnectionStateMachine { case STOPPED: break; case AP_MODE: - // Once in AP mode and established, we stay in the state. + if (WiFiAdapter::GetWirelessMode() == WiFiAdapter::WirelessMode::ADAPTER_STATION) { + if (!m_scanStarted) { + String scan_interval_s; + long scan_interval_ms = 30000; + if (logger::LoggerConfig.GetConfigString(logger::Config::ConfigParam::CONFIG_STATION_SCAN_INTERVAL_S, scan_interval_s)) { + if (scan_interval_s.toInt() > 0) scan_interval_ms = scan_interval_s.toInt() * 1000; + } + if (m_lastScanTime == 0 || (now - m_lastScanTime) > scan_interval_ms) { + if (m_verbose) Serial.printf("DBG: triggering background scan (interval %ld ms)...\n", scan_interval_ms); + WiFi.scanNetworks(true); // true = async non-blocking + m_scanStarted = true; + m_lastScanTime = now; + } + } else { + // Scan is running asynchronously, poll for completion + int16_t n = WiFi.scanComplete(); + if (n >= 0) { + String targetSsid; + logger::LoggerConfig.GetConfigString(logger::Config::ConfigParam::CONFIG_STATION_SSID_S, targetSsid); + if (m_verbose) { + Serial.printf("DBG: background scan returned %d results. targetSsid='%s'\n", n, targetSsid.c_str()); + } + bool found = false; + for (int i = 0; i < n; ++i) { + if (m_verbose) { + Serial.printf("DBG: - Scan %d: '%s' (RSSI: %d)\n", i, WiFi.SSID(i).c_str(), WiFi.RSSI(i)); + } + if (WiFi.SSID(i) == targetSsid) { + found = true; + break; + } + } + WiFi.scanDelete(); // Memory cleanup + m_scanStarted = false; + if (found) { + if (m_verbose) Serial.printf("DBG: found target hotspot %s over the air, dropping AP to reconnect...\n", targetSsid.c_str()); + if (attemptStationJoin()) m_currentState = STATION_CONNECTED; + else m_currentState = STATION_CONNECTING; + } + } else if (n == WIFI_SCAN_FAILED) { + m_scanStarted = false; // reset and try again later + } + } + } break; case STATION_CONNECTING: // We're waiting for the connection to complete, so check status @@ -229,15 +275,17 @@ class ConnectionStateMachine { // We're out of retries for a station connection, so we have to assume // that the network isn't there, or there's a problem with the password // etc. -- so we revert to AP mode. - WiFiAdapter::SetWirelessMode(WiFiAdapter::WirelessMode::ADAPTER_SOFTAP); - logger::LoggerConfig.SetConfigString(logger::Config::ConfigParam::CONFIG_WS_STATUS_S, "AP-Enabled,Station-Join-Failed"); - if (m_verbose) - Serial.print("DBG: set status to Station-Join-Failed, rebooting to AP safe mode.\n"); - ESP.restart(); + logger::LoggerConfig.SetConfigString(logger::Config::ConfigParam::CONFIG_WS_STATUS_S, "AP-Fallback,Station-Join-Failed"); + WiFi.disconnect(); // Stop background AutoReconnect spam so scans can run + WiFi.mode(WIFI_AP_STA); // Ensure both AP and Station interfaces are up for scanning + apSetup(); + m_currentState = AP_MODE; + m_lastScanTime = now; // Delay first scan by interval after dropping to AP break; case STATION_CONNECTED: // The system (finally?) connected, so we update status, and then go into // connection checking mode. + m_connectionRetries = maximumReties(); // Reset retry count for future dropouts logger::LoggerConfig.SetConfigString(logger::Config::ConfigParam::CONFIG_WS_STATUS_S, "Station-Enabled,Connected"); m_currentState = CONNECTION_CHECK; if (m_verbose) { @@ -277,6 +325,8 @@ class ConnectionStateMachine { bool m_verbose; // Flag for debug information to happen int m_lastConnectAttempt; // Time (ms) for last connection attempt int m_lastStatusCheck; // Time (ms) for last connection status attempt + int m_lastScanTime; // Time (ms) for background scan triggers + bool m_scanStarted; // Whether an async scan is running int m_connectionRetries; // Count of remaining connection attempts int m_retryDelay; // Delay (ms) before attempt to retry to connect int m_statusDelay; // Delay (ms) before checking connection status again @@ -291,6 +341,13 @@ class ConnectionStateMachine { // logger first boots, the WIFi comes up with known SSID and password. if (ssid.length() == 0) ssid = "wibl-config"; if (ssid.length() == 0) password = "wibl-config-password"; + + String logger_name; + logger::LoggerConfig.GetConfigString(logger::Config::CONFIG_MDNS_NAME_S, logger_name); + if (logger_name.length() > 0) { + WiFi.softAPsetHostname(logger_name.c_str()); + } + WiFi.softAP(ssid.c_str(), password.c_str()); WiFi.setSleep(false); IPAddress server_address = WiFi.softAPIP(); @@ -322,8 +379,48 @@ class ConnectionStateMachine { Serial.print("ERR: attempting to join a WiFi network as a station without a specified SSID\n"); return false; } - wl_status_t status = WiFi.begin(ssid.c_str(), password.c_str()); + + // Configure WPA3/PMF fallback & parameters for modern hotspots + WiFi.mode(WIFI_STA); + + String logger_name; + logger::LoggerConfig.GetConfigString(logger::Config::CONFIG_MDNS_NAME_S, logger_name); + if (logger_name.length() > 0) { + WiFi.setHostname(logger_name.c_str()); + } + + wifi_config_t conf; + esp_wifi_get_config(WIFI_IF_STA, &conf); + + bool require_pmf = false; + String require_pmf_str; + if (logger::LoggerConfig.GetConfigString(logger::Config::ConfigParam::CONFIG_REQUIRE_PMF_S, require_pmf_str)) { + require_pmf = require_pmf_str.equalsIgnoreCase("true") || require_pmf_str == "1"; + } + + if (m_verbose) { + Serial.printf("DBG: WPA3 PMF configured as %s\n", require_pmf ? "REQUIRED" : "CAPABLE-ONLY"); + } + + WiFi.disconnect(true); + delay(100); WiFi.setSleep(false); + + // Blank and build the sta configuration struct manually so we can set WPA3 options + memset(&conf, 0, sizeof(conf)); + memcpy(conf.sta.ssid, ssid.c_str(), ssid.length()); + memcpy(conf.sta.password, password.c_str(), password.length()); + + conf.sta.pmf_cfg.capable = true; + conf.sta.pmf_cfg.required = require_pmf; +#ifdef WPA3_SAE_PWE_BOTH + conf.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH; +#endif + esp_wifi_set_config(WIFI_IF_STA, &conf); + esp_wifi_set_bandwidth(WIFI_IF_STA, WIFI_BW_HT20); + + wl_status_t status = WiFi.begin(); // DO NOT pass ssid/password here, it overwrites the PMF config we just set! + m_lastConnectAttempt = millis(); if (m_verbose) { Serial.printf("DBG: started network join on %s:%s at %d with immediate status %d\n", ssid.c_str(), password.c_str(), m_lastConnectAttempt, (int)status); @@ -469,7 +566,7 @@ class ESP32WiFiAdapter : public WiFiAdapter { m_server->serveStatic("/logs", m_storage->Controller(), "/logs/"); m_server->serveStatic("/", LittleFS, "/website/"); // Note trailing '/' since this is a directory being served. } - //m_state.Verbose(true); + m_state.Verbose(m_verbose); m_state.Start(); m_server->begin(); return true; diff --git a/LoggerFirmware/website/actions/configure.htm b/LoggerFirmware/website/actions/configure.htm index 22134b42..abbfd337 100644 --- a/LoggerFirmware/website/actions/configure.htm +++ b/LoggerFirmware/website/actions/configure.htm @@ -68,6 +68,9 @@

Wireless Inexpensive Bathymetry Logger


+ + +
Identification diff --git a/LoggerFirmware/website/js/configuration.js b/LoggerFirmware/website/js/configuration.js index 2089c4d1..49456547 100644 --- a/LoggerFirmware/website/js/configuration.js +++ b/LoggerFirmware/website/js/configuration.js @@ -42,6 +42,7 @@ function createJSONConfig() { const stationDelay = document.getElementById("retry-delay").value; const stationRetries = document.getElementById("retry-count").value; const stationTimeout = document.getElementById("join-timeout").value; + const stationScanInterval = document.getElementById("scan-interval").value; const mdnsName = document.getElementById("mdns-name").value; const apSSID = document.getElementById("ap-ssid").value; const stationSSID = document.getElementById("station-ssid").value; @@ -77,6 +78,7 @@ function createJSONConfig() { "delay": ${stationDelay}, "retries": ${stationRetries}, "timeout": ${stationTimeout}, + "scaninterval": ${stationScanInterval}, "mdns": "${mdnsName}" }, "ssids": { @@ -127,6 +129,7 @@ function parseConfigJSON(config) { document.getElementById("retry-delay").value = config.wifi.station.delay; document.getElementById("retry-count").value = config.wifi.station.retries; document.getElementById("join-timeout").value = config.wifi.station.timeout; + document.getElementById("scan-interval").value = config.wifi.station.scaninterval; document.getElementById("mdns-name").value = config.wifi.station.mdns; document.getElementById("ap-ssid").value = config.wifi.ssids.ap; document.getElementById("station-ssid").value = config.wifi.ssids.station;