fix(provisioning): fast-fail wifi on bad PSK / missing SSID

Match the failure path's latency to the happy path. Before: a wrong
password meant the user stared at the yellow Step 1/2 screen for the
full 30 s WIFI_TIMEOUT_MS before the red retry repaint started — total
~50 s to "Connection Failed" visible. After: WL_CONNECT_FAILED and
WL_NO_SSID_AVAIL bail attempt_wifi() immediately, so the red repaint
starts within a few seconds of the radio giving up — total ~25 s,
matching the happy-path-to-Step-2/2 timing.

Also collapse the duplicate boot-time poll loop in main.cpp onto the
shared attempt_wifi() so the same fast-fail covers boot-with-stored-
creds, not just captive-portal submission.

Tests: FW-15a (auth fail) and FW-15b (no SSID) assert millis() never
reaches WIFI_TIMEOUT_MS on those statuses. Existing FW-15 tightened
to use WL_DISCONNECTED so it actually exercises the timeout path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:31:41 -04:00
parent 6a924963e5
commit 05e869d190
4 changed files with 41 additions and 15 deletions
+5 -11
View File
@@ -212,22 +212,16 @@ void setup() {
return; return;
} }
// Attempt to join saved network // Attempt to join saved network — uses the shared attempt_wifi() so the
// fast-fail-on-WL_CONNECT_FAILED behavior covers boot-with-stored-creds
// too, not just the captive-portal submission path.
Serial.println("[wifi] connecting to ssid=" + ssid); Serial.println("[wifi] connecting to ssid=" + ssid);
WiFi.mode(WIFI_STA); if (attempt_wifi(ssid, pass)) {
WiFi.begin(ssid.c_str(), pass.c_str());
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - start > WIFI_TIMEOUT_MS) break;
delay(200);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("[wifi] connected ip=" + WiFi.localIP().toString()); Serial.println("[wifi] connected ip=" + WiFi.localIP().toString());
normal_operation(mac); normal_operation(mac);
// normal_operation calls deep_sleep — never returns // normal_operation calls deep_sleep — never returns
} else { } else {
Serial.println("[wifi] failed after " + String(WIFI_TIMEOUT_MS) + " ms — entering provisioning"); Serial.println("[wifi] failed — entering provisioning");
enter_provisioning(mac); enter_provisioning(mac);
} }
} }
+8 -2
View File
@@ -80,11 +80,17 @@ inline bool attempt_wifi(const char* ssid, const char* pass) {
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass); WiFi.begin(ssid, pass);
uint32_t start = millis(); uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED) { while (true) {
int s = WiFi.status();
if (s == WL_CONNECTED) return true;
// Bail the moment the radio reports a terminal failure — bad PSK
// surfaces as WL_CONNECT_FAILED and missing SSID as WL_NO_SSID_AVAIL
// within a few seconds. Without this the user stares at the yellow
// Step 1/2 for the full WIFI_TIMEOUT_MS before the red retry repaints.
if (s == WL_CONNECT_FAILED || s == WL_NO_SSID_AVAIL) return false;
if (millis() - start > WIFI_TIMEOUT_MS) return false; if (millis() - start > WIFI_TIMEOUT_MS) return false;
delay(200); delay(200);
} }
return true;
} }
// ── Reset button hold detection ─────────────────────────────────────────────── // ── Reset button hold detection ───────────────────────────────────────────────
+2
View File
@@ -2,7 +2,9 @@
#include "Arduino.h" #include "Arduino.h"
#define WIFI_STA 0 #define WIFI_STA 0
#define WIFI_AP 1 #define WIFI_AP 1
#define WL_NO_SSID_AVAIL 1
#define WL_CONNECTED 3 #define WL_CONNECTED 3
#define WL_CONNECT_FAILED 4
extern int g_wifi_status; extern int g_wifi_status;
+25 -1
View File
@@ -40,10 +40,32 @@ void test_fw14_attempt_wifi_returns_true_on_connect() {
// millis() auto-increments by 10 on each call; after enough iterations the // millis() auto-increments by 10 on each call; after enough iterations the
// elapsed time exceeds WIFI_TIMEOUT_MS (30000 ms). // elapsed time exceeds WIFI_TIMEOUT_MS (30000 ms).
void test_fw15_attempt_wifi_returns_false_on_timeout() { void test_fw15_attempt_wifi_returns_false_on_timeout() {
g_wifi_status = 0; // never WL_CONNECTED g_wifi_status = 6; // WL_DISCONNECTED — never connects, never terminal-fails
g_millis_value = 0; g_millis_value = 0;
bool result = attempt_wifi("myssid", "mypass"); bool result = attempt_wifi("myssid", "mypass");
TEST_ASSERT_FALSE(result); TEST_ASSERT_FALSE(result);
// Sanity: we actually waited the full timeout, not bailed early
TEST_ASSERT_GREATER_THAN(WIFI_TIMEOUT_MS, g_millis_value);
}
// ── FW-15a: attempt_wifi bails fast on WL_CONNECT_FAILED (bad PSK) ────────────
// Without fast-fail the user would wait the full 30 s before the red retry
// screen repaints; with it, failure surfaces in seconds.
void test_fw15a_attempt_wifi_returns_false_on_auth_fail() {
g_wifi_status = WL_CONNECT_FAILED;
g_millis_value = 0;
bool result = attempt_wifi("myssid", "wrongpass");
TEST_ASSERT_FALSE(result);
TEST_ASSERT_LESS_THAN(WIFI_TIMEOUT_MS, g_millis_value);
}
// ── FW-15b: attempt_wifi bails fast on WL_NO_SSID_AVAIL (network not visible) ─
void test_fw15b_attempt_wifi_returns_false_on_no_ssid() {
g_wifi_status = WL_NO_SSID_AVAIL;
g_millis_value = 0;
bool result = attempt_wifi("notthere", "anypass");
TEST_ASSERT_FALSE(result);
TEST_ASSERT_LESS_THAN(WIFI_TIMEOUT_MS, g_millis_value);
} }
// ── FW-16: loop() state-machine (WiFi-credential submission path) ───────────── // ── FW-16: loop() state-machine (WiFi-credential submission path) ─────────────
@@ -77,6 +99,8 @@ int main(int argc, char** argv) {
UNITY_BEGIN(); UNITY_BEGIN();
RUN_TEST(test_fw14_attempt_wifi_returns_true_on_connect); RUN_TEST(test_fw14_attempt_wifi_returns_true_on_connect);
RUN_TEST(test_fw15_attempt_wifi_returns_false_on_timeout); RUN_TEST(test_fw15_attempt_wifi_returns_false_on_timeout);
RUN_TEST(test_fw15a_attempt_wifi_returns_false_on_auth_fail);
RUN_TEST(test_fw15b_attempt_wifi_returns_false_on_no_ssid);
RUN_TEST(test_fw16_loop_state_machine_deferred); RUN_TEST(test_fw16_loop_state_machine_deferred);
RUN_TEST(test_fw17_reset_button_held_returns_true); RUN_TEST(test_fw17_reset_button_held_returns_true);
RUN_TEST(test_fw18_reset_button_not_pressed_returns_false); RUN_TEST(test_fw18_reset_button_not_pressed_returns_false);