Compare commits

..
2 Commits
Author SHA1 Message Date
alex 5304aad4c8 Release v3.2.4 2026-09-08 22:40:17 +10:00
alex 21607d41bd Fix portal station hand-off ownership 2026-09-08 22:26:19 +10:00
9 changed files with 46 additions and 7 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## 3.2.4
- Correct profile-backed Wi-Fi hand-off from the embedded web portal: the
station controller now owns attempts started through either portal surface,
preventing the legacy empty-SSID path from reporting success before a
station address is available.
## 3.2.3 ## 3.2.3
- Clarify the public integration boundary, built-in browser protocol, and - Clarify the public integration boundary, built-in browser protocol, and
+1 -1
View File
@@ -91,7 +91,7 @@ The [Branded Portal](examples/BrandedPortal/) example includes a static SVG, acc
```ini ```ini
[common] [common]
lib_deps = lib_deps =
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.3 WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.4
``` ```
The suffix after `#` is a Git ref. PlatformIO clones the repository and checks out that release tag; GitHub Release assets are unrelated. Arduino IDE users can install this repository as a library checkout. The suffix after `#` is a Git ref. PlatformIO clones the repository and checks out that release tag; GitHub Release assets are unrelated. Arduino IDE users can install this repository as a library checkout.
+1 -1
View File
@@ -26,7 +26,7 @@ void loop() {
```ini ```ini
lib_deps = lib_deps =
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.3 WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.4
``` ```
The package includes the asynchronous web and TCP dependencies required by the selected ESP8266 or ESP32 target. Add WiFiManager as the application’s direct dependency; do not copy its internal dependency list into your project. The package includes the asynchronous web and TCP dependencies required by the selected ESP8266 or ESP32 target. Add WiFiManager as the application’s direct dependency; do not copy its internal dependency list into your project.
+6
View File
@@ -1082,6 +1082,12 @@ protected:
_configPortalStart = millis(); _configPortalStart = millis();
} }
} }
void wmTestSetWebPortalActive(bool active) {
webPortalActive = active;
if (active) {
_configPortalStart = millis();
}
}
void wmTestSetPortalConnectSuccess(const String& message, const String& stationIp, uint8_t status = WL_CONNECTED) { void wmTestSetPortalConnectSuccess(const String& message, const String& stationIp, uint8_t status = WL_CONNECTED) {
_cpConnectState = wm_cp_connect_state_t::success; _cpConnectState = wm_cp_connect_state_t::success;
_cpConnectMessage = message; _cpConnectMessage = message;
+5 -2
View File
@@ -385,7 +385,10 @@ bool WiFiManager::startStationCandidate(const WiFiManagerStationProfiles& candid
_begin(); _begin();
_stationCandidate = candidate; _stationCandidate = candidate;
_stationCandidateActive = true; _stationCandidateActive = true;
_stationCandidateFromPortal = configPortalActive; // A profile submitted from either portal surface is owned by the station
// controller. Do not let the legacy empty-SSID portal state machine mark
// it successful before the controller has a usable station address.
_stationCandidateFromPortal = configPortalActive || webPortalActive;
_stationAttemptMask = 0; _stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE; _stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0; _stationNextAttemptAt = 0;
@@ -506,7 +509,7 @@ void WiFiManager::queueStationProfile(uint8_t slot) {
} }
void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, const String& message) { void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, const String& message) {
if (!_stationCandidateFromPortal && !configPortalActive) { if (!_stationCandidateFromPortal && !configPortalActive && !webPortalActive) {
return; return;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "WiFiManager", "name": "WiFiManager",
"version": "3.2.3", "version": "3.2.4",
"keywords": [ "keywords": [
"wifi", "wifi",
"wi-fi", "wi-fi",
+1 -1
View File
@@ -14,6 +14,7 @@ TestCase tests[] = {
TEST_ENTRY(test_api_wifi_meta_password_field_type), TEST_ENTRY(test_api_wifi_meta_password_field_type),
TEST_ENTRY(test_api_wifi_connect_status_success_redirect), TEST_ENTRY(test_api_wifi_connect_status_success_redirect),
TEST_ENTRY(test_profile_portal_success_keeps_handoff_alive), TEST_ENTRY(test_profile_portal_success_keeps_handoff_alive),
TEST_ENTRY(test_profile_portal_candidate_does_not_take_legacy_empty_ssid_path),
TEST_ENTRY(test_api_info_json_shape), TEST_ENTRY(test_api_info_json_shape),
TEST_ENTRY(test_api_params_json_shape), TEST_ENTRY(test_api_params_json_shape),
TEST_ENTRY(test_api_status_json_shape), TEST_ENTRY(test_api_status_json_shape),
@@ -183,4 +184,3 @@ void loop() {
begun = false; // avoid repeating begun = false; // avoid repeating
} }
} }
+1 -1
View File
@@ -119,6 +119,7 @@ void test_api_wifi_meta_json_shape();
void test_api_wifi_meta_password_field_type(); void test_api_wifi_meta_password_field_type();
void test_api_wifi_connect_status_success_redirect(); void test_api_wifi_connect_status_success_redirect();
void test_profile_portal_success_keeps_handoff_alive(); void test_profile_portal_success_keeps_handoff_alive();
void test_profile_portal_candidate_does_not_take_legacy_empty_ssid_path();
void test_api_info_json_shape(); void test_api_info_json_shape();
void test_api_params_json_shape(); void test_api_params_json_shape();
void test_api_status_json_shape(); void test_api_status_json_shape();
@@ -146,4 +147,3 @@ void test_portal_resource_recovery();
void test_scan_result_storage_released_when_portal_closes(); void test_scan_result_storage_released_when_portal_closes();
#endif // TEST_MAIN_H #endif // TEST_MAIN_H
@@ -96,6 +96,29 @@ void test_profile_portal_success_keeps_handoff_alive() {
Serial.println("[TEST] Profile portal success hand-off delay test completed successfully"); Serial.println("[TEST] Profile portal success hand-off delay test completed successfully");
} }
void test_profile_portal_candidate_does_not_take_legacy_empty_ssid_path() {
Serial.println("[TEST] Testing profile portal candidate state ownership...");
WiFiManager wm;
#ifdef UNIT_TEST
WiFiManagerStationProfiles candidate;
candidate.slots[0].enabled = true;
snprintf(candidate.slots[0].ssid, sizeof(candidate.slots[0].ssid), "%s", "candidate-network");
wm.wmTestSetWebPortalActive(true);
TEST_ASSERT_TRUE(wm.startStationCandidate(candidate));
wm.process();
TEST_ASSERT_EQUAL_MESSAGE(
WiFiManager::WM_CP_CONNECT_WAITING,
wm.getConfigPortalConnectState(),
"A web-portal profile candidate must not be treated as the legacy empty-SSID save path"
);
#else
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Profile portal candidate state ownership test completed successfully");
}
void test_api_info_json_shape() { void test_api_info_json_shape() {
Serial.println("[TEST] Testing /api/info JSON shape..."); Serial.println("[TEST] Testing /api/info JSON shape...");