wifi.cpp
1 #include <networking/wifi.h> 2 #include "wifi_internal.h" 3 #include <identity.h> 4 #include "../services/preferences.h" 5 6 #include <Arduino.h> 7 #include <Preferences.h> 8 #include <WiFi.h> 9 bool networking::wifi::internal::mdns_started = false; 10 bool networking::wifi::internal::ap_active = false; 11 12 const char networking::wifi::internal::wifi_ssid_slot[33] __attribute__((used, aligned(4))) = 13 "@@WIFI_SSID@@"; 14 const char networking::wifi::internal::wifi_pass_slot[65] __attribute__((used, aligned(4))) = 15 "@@WIFI_PASS@@"; 16 17 bool networking::wifi::internal::openPreferences(bool readonly, Preferences *prefs) { 18 return services::preferences::open(config::wifi::NVS_NAMESPACE, readonly, prefs); 19 } 20 21 bool networking::wifi::accessSnapshot(NetworkStatusSnapshot *snapshot) { 22 if (!snapshot) return false; 23 memset(snapshot, 0, sizeof(*snapshot)); 24 25 snapshot->connected = WiFi.isConnected(); 26 snapshot->rssi = snapshot->connected ? WiFi.RSSI() : 0; 27 snapshot->channel = snapshot->connected ? WiFi.channel() : 0; 28 29 strncpy(snapshot->ssid, WiFi.SSID().c_str(), sizeof(snapshot->ssid) - 1); 30 strncpy(snapshot->bssid, WiFi.BSSIDstr().c_str(), sizeof(snapshot->bssid) - 1); 31 strncpy(snapshot->ip, WiFi.localIP().toString().c_str(), sizeof(snapshot->ip) - 1); 32 strncpy(snapshot->gateway, WiFi.gatewayIP().toString().c_str(), sizeof(snapshot->gateway) - 1); 33 strncpy(snapshot->subnet, WiFi.subnetMask().toString().c_str(), sizeof(snapshot->subnet) - 1); 34 strncpy(snapshot->dns, WiFi.dnsIP().toString().c_str(), sizeof(snapshot->dns) - 1); 35 strncpy(snapshot->mac, WiFi.macAddress().c_str(), sizeof(snapshot->mac) - 1); 36 strncpy(snapshot->hostname, services::identity::access_hostname(), sizeof(snapshot->hostname) - 1); 37 38 snapshot->ap.active = networking::wifi::ap::isActive(); 39 APConfig ap_config = {}; 40 networking::wifi::ap::accessConfig(&ap_config); 41 strncpy(snapshot->ap.ssid, ap_config.ssid, sizeof(snapshot->ap.ssid) - 1); 42 strncpy(snapshot->ap.password, ap_config.password, sizeof(snapshot->ap.password) - 1); 43 strncpy(snapshot->ap.ip, WiFi.softAPIP().toString().c_str(), sizeof(snapshot->ap.ip) - 1); 44 snapshot->ap.clients = WiFi.softAPgetStationNum(); 45 strncpy(snapshot->ap.hostname, services::identity::access_hostname(), sizeof(snapshot->ap.hostname) - 1); 46 strncpy(snapshot->ap.mac, WiFi.softAPmacAddress().c_str(), sizeof(snapshot->ap.mac) - 1); 47 return true; 48 } 49 50 bool networking::wifi::accessConfig(WifiSavedConfig *config) { 51 if (!config) return false; 52 memset(config, 0, sizeof(*config)); 53 54 Preferences prefs; 55 if (!networking::wifi::internal::openPreferences(true, &prefs)) return false; 56 57 bool has_ssid = prefs.getString("sta_ssid", config->ssid, sizeof(config->ssid)) > 0; 58 bool has_password = prefs.getString("sta_pass", config->password, sizeof(config->password)) >= 0; 59 prefs.end(); 60 config->valid = has_ssid; 61 return has_ssid && has_password; 62 } 63 64 bool networking::wifi::storeConfig(WifiSavedConfig *config) { 65 if (!config) return false; 66 Preferences prefs; 67 if (!networking::wifi::internal::openPreferences(false, &prefs)) return false; 68 69 prefs.putString("sta_ssid", config->ssid); 70 prefs.putString("sta_pass", config->password); 71 prefs.end(); 72 config->valid = config->ssid[0] != '\0'; 73 return config->valid; 74 } 75 76 bool networking::wifi::connect(WifiConnectCommand *command) { 77 if (!command) return false; 78 command->result = {}; 79 command->result.connected = false; 80 command->result.status_code = WL_DISCONNECTED; 81 command->result.ap_enabled_for_fallback = false; 82 83 WiFi.setAutoReconnect(false); 84 WiFi.disconnect(false); 85 WiFi.mode(networking::wifi::ap::isActive() ? WIFI_AP_STA : WIFI_MODE_STA); 86 networking::wifi::configure_hostname(services::identity::access_hostname()); 87 88 if (command->request.ssid && command->request.ssid[0] != '\0') { 89 WiFi.begin(command->request.ssid, 90 command->request.password ? command->request.password : ""); 91 } else { 92 WifiSavedConfig saved_config = {}; 93 if (networking::wifi::accessConfig(&saved_config) && saved_config.valid) { 94 Serial.printf("[wifi] credentials from NVS: %s\n", saved_config.ssid); 95 WiFi.begin(saved_config.ssid, saved_config.password); 96 } else 97 #if defined(CONFIG_WIFI_SSID) && defined(CONFIG_WIFI_PASS) 98 if (strlen(CONFIG_WIFI_SSID) > 0) { 99 Serial.printf("[wifi] credentials from build flags: %s\n", CONFIG_WIFI_SSID); 100 WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PASS); 101 } else 102 #endif 103 if (networking::wifi::internal::wifi_ssid_slot[0] != '@' && networking::wifi::internal::wifi_ssid_slot[0] != '\0') { 104 Serial.printf("[wifi] credentials from embedded: %s\n", networking::wifi::internal::wifi_ssid_slot); 105 WiFi.begin(networking::wifi::internal::wifi_ssid_slot, networking::wifi::internal::wifi_pass_slot); 106 } else { 107 WiFi.begin(); 108 } 109 } 110 111 command->result.status_code = WiFi.waitForConnectResult(config::wifi::CONNECT_TIMEOUT_MS); 112 command->result.connected = (command->result.status_code == WL_CONNECTED); 113 114 if (!command->result.connected && command->request.enable_ap_fallback 115 && !networking::wifi::ap::isActive()) { 116 networking::wifi::ap::enable(); 117 command->result.ap_enabled_for_fallback = true; 118 } 119 120 return command->result.connected; 121 } 122 123 bool networking::wifi::scan(WifiScanCommand *command) { 124 if (!command || !command->results || command->max_results == 0) return false; 125 command->result_count = -1; 126 127 WiFi.scanDelete(); 128 int16_t count = WiFi.scanNetworks(); 129 if (count < 0) return false; 130 131 int16_t limit = (count < (int16_t)command->max_results) ? count : (int16_t)command->max_results; 132 for (int16_t index = 0; index < limit; index++) { 133 memset(&command->results[index], 0, sizeof(command->results[index])); 134 strlcpy(command->results[index].ssid, WiFi.SSID(index).c_str(), sizeof(command->results[index].ssid)); 135 strlcpy(command->results[index].bssid, WiFi.BSSIDstr(index).c_str(), sizeof(command->results[index].bssid)); 136 command->results[index].rssi = WiFi.RSSI(index); 137 command->results[index].channel = WiFi.channel(index); 138 139 const char *encryption = "unknown"; 140 switch (WiFi.encryptionType(index)) { 141 case WIFI_AUTH_OPEN: encryption = "open"; break; 142 case WIFI_AUTH_WEP: encryption = "wep"; break; 143 case WIFI_AUTH_WPA_PSK: encryption = "wpa"; break; 144 case WIFI_AUTH_WPA2_PSK: encryption = "wpa2"; break; 145 case WIFI_AUTH_WPA_WPA2_PSK: encryption = "wpa_wpa2"; break; 146 case WIFI_AUTH_WPA2_ENTERPRISE: encryption = "wpa2_enterprise"; break; 147 case WIFI_AUTH_WPA3_PSK: encryption = "wpa3"; break; 148 case WIFI_AUTH_WPA2_WPA3_PSK: encryption = "wpa2_wpa3"; break; 149 default: break; 150 } 151 strlcpy(command->results[index].encryption, encryption, sizeof(command->results[index].encryption)); 152 command->results[index].open = (WiFi.encryptionType(index) == WIFI_AUTH_OPEN); 153 } 154 155 WiFi.scanDelete(); 156 command->result_count = count; 157 return true; 158 } 159 160 161 // ───────────────────────────────────────────────────────────────────────────── 162 // Tests 163 // ───────────────────────────────────────────────────────────────────────────── 164 #ifdef PIO_UNIT_TESTING 165 166 167 #include <networking/wifi.h> 168 #include <testing/utils.h> 169 170 171 namespace networking::wifi { void test(void); } 172 173 #include <Arduino.h> 174 #include <WiFi.h> 175 #include <esp_wifi.h> 176 177 static WifiNvsSnapshot saved; 178 static void save_nvs(void) { wifi_nvs_save(&saved); } 179 static void restore_nvs(void) { wifi_nvs_restore(&saved); } 180 181 static void test_wifi_persistent_credentials(void) { 182 WHEN("WiFi.begin(ssid, pass) is called"); 183 184 WiFi.begin("test_ssid_persist", "test_pass_persist"); 185 delay(100); 186 WiFi.disconnect(true); 187 188 wifi_config_t conf; 189 esp_err_t err = esp_wifi_get_config(WIFI_IF_STA, &conf); 190 TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, err, 191 "device: esp_wifi_get_config failed"); 192 TEST_ASSERT_EQUAL_STRING_MESSAGE("test_ssid_persist", (const char *)conf.sta.ssid, 193 "device: SSID not persisted by WiFi.begin()"); 194 195 } 196 197 static void test_wifi_connect_fails_without_ssid(void) { 198 GIVEN("no WiFi credentials are stored"); 199 200 save_nvs(); 201 202 Preferences preferences; 203 preferences.begin(config::wifi::NVS_NAMESPACE, false); 204 preferences.remove("sta_ssid"); 205 preferences.remove("sta_pass"); 206 preferences.end(); 207 208 WiFi.disconnect(true, true); 209 WiFi.eraseAP(); 210 delay(100); 211 212 #if defined(CONFIG_WIFI_SSID) && defined(CONFIG_WIFI_PASS) 213 if (strlen(CONFIG_WIFI_SSID) > 0) { 214 restore_nvs(); 215 TEST_IGNORE_MESSAGE("build-flag WiFi credentials configured — skipping no-SSID failure assertion"); 216 } 217 #endif 218 219 if (networking::wifi::internal::wifi_ssid_slot[0] != '@' 220 && networking::wifi::internal::wifi_ssid_slot[0] != '\0') { 221 restore_nvs(); 222 TEST_IGNORE_MESSAGE("embedded WiFi credentials configured — skipping no-SSID failure assertion"); 223 } 224 225 WHEN("wifi_connect is called"); 226 networking::wifi::sta::initialize(); 227 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::sta::connect(), 228 "device: wifi_connect should return false when no SSID stored"); 229 230 restore_nvs(); 231 } 232 233 static void test_wifi_saved_sta_config_roundtrip(void) { 234 WHEN("STA config is stored and read back"); 235 save_nvs(); 236 237 WifiSavedConfig written = {}; 238 strlcpy(written.ssid, "test-sta-ssid", sizeof(written.ssid)); 239 strlcpy(written.password, "test-sta-pass", sizeof(written.password)); 240 241 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::storeConfig(&written), 242 "device: storing WiFi STA config should succeed"); 243 TEST_ASSERT_TRUE_MESSAGE(written.valid, 244 "device: stored WiFi STA config should be marked valid"); 245 246 WifiSavedConfig read_back = {}; 247 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::accessConfig(&read_back), 248 "device: reading WiFi STA config should succeed after store"); 249 TEST_ASSERT_TRUE_MESSAGE(read_back.valid, 250 "device: read-back WiFi STA config should be marked valid"); 251 TEST_ASSERT_EQUAL_STRING_MESSAGE("test-sta-ssid", read_back.ssid, 252 "device: STA SSID mismatch after config roundtrip"); 253 TEST_ASSERT_EQUAL_STRING_MESSAGE("test-sta-pass", read_back.password, 254 "device: STA password mismatch after config roundtrip"); 255 256 restore_nvs(); 257 } 258 259 static void test_wifi_connect_prefers_explicit_credentials(void) { 260 GIVEN("a saved STA config exists"); 261 save_nvs(); 262 263 WifiSavedConfig saved_config = {}; 264 strlcpy(saved_config.ssid, "saved-ssid", sizeof(saved_config.ssid)); 265 strlcpy(saved_config.password, "saved-password", sizeof(saved_config.password)); 266 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::storeConfig(&saved_config), 267 "device: storing saved STA config should succeed"); 268 269 WHEN("connect is called with explicit credentials"); 270 WifiConnectCommand command = { 271 .request = { 272 .ssid = "request-ssid", 273 .password = "request-password", 274 .enable_ap_fallback = false, 275 }, 276 .result = {}, 277 }; 278 279 networking::wifi::connect(&command); 280 delay(100); 281 282 THEN("the explicit credentials override saved config"); 283 wifi_config_t station_config = {}; 284 esp_err_t err = esp_wifi_get_config(WIFI_IF_STA, &station_config); 285 TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, err, 286 "device: esp_wifi_get_config should succeed after explicit connect request"); 287 TEST_ASSERT_EQUAL_STRING_MESSAGE("request-ssid", (const char *)station_config.sta.ssid, 288 "device: explicit connect request should override saved SSID"); 289 TEST_ASSERT_EQUAL_STRING_MESSAGE("request-password", (const char *)station_config.sta.password, 290 "device: explicit connect request should override saved password"); 291 292 WiFi.disconnect(true); 293 restore_nvs(); 294 } 295 296 static void test_wifi_connect_uses_saved_config(void) { 297 GIVEN("a saved STA config exists and no request SSID is provided"); 298 save_nvs(); 299 300 WifiSavedConfig saved_config = {}; 301 strlcpy(saved_config.ssid, "saved-only-ssid", sizeof(saved_config.ssid)); 302 strlcpy(saved_config.password, "saved-only-password", sizeof(saved_config.password)); 303 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::storeConfig(&saved_config), 304 "device: storing fallback STA config should succeed"); 305 306 WHEN("connect is called with empty request SSID"); 307 WifiConnectCommand command = { 308 .request = { 309 .ssid = "", 310 .password = "", 311 .enable_ap_fallback = false, 312 }, 313 .result = {}, 314 }; 315 316 networking::wifi::connect(&command); 317 delay(100); 318 319 THEN("the saved config is used"); 320 wifi_config_t station_config = {}; 321 esp_err_t err = esp_wifi_get_config(WIFI_IF_STA, &station_config); 322 TEST_ASSERT_EQUAL_MESSAGE(ESP_OK, err, 323 "device: esp_wifi_get_config should succeed after saved-config connect request"); 324 TEST_ASSERT_EQUAL_STRING_MESSAGE("saved-only-ssid", (const char *)station_config.sta.ssid, 325 "device: saved STA config should be used when request SSID is empty"); 326 TEST_ASSERT_EQUAL_STRING_MESSAGE("saved-only-password", (const char *)station_config.sta.password, 327 "device: saved STA password should be used when request SSID is empty"); 328 329 WiFi.disconnect(true); 330 restore_nvs(); 331 } 332 333 static void test_wifi_ap_fallback_on_failed_connect(void) { 334 GIVEN("AP is disabled and WiFi is disconnected"); 335 save_nvs(); 336 337 networking::wifi::ap::disable(); 338 WiFi.disconnect(true, true); 339 delay(100); 340 341 WifiConnectCommand command = { 342 .request = { 343 .ssid = "ssid-that-should-not-exist", 344 .password = "definitely-not-the-right-password", 345 .enable_ap_fallback = true, 346 }, 347 .result = {}, 348 }; 349 350 WHEN("connect fails with fallback enabled"); 351 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::connect(&command), 352 "device: WiFi connect should fail with intentionally invalid credentials"); 353 TEST_ASSERT_FALSE_MESSAGE(command.result.connected, 354 "device: connect result should report disconnected after failed connect"); 355 356 THEN("AP fallback is activated"); 357 TEST_ASSERT_TRUE_MESSAGE(command.result.ap_enabled_for_fallback, 358 "device: AP fallback should be enabled after failed connect when requested"); 359 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::ap::isActive(), 360 "device: AP should be active after fallback-enabled failed connect"); 361 362 networking::wifi::ap::disable(); 363 WiFi.disconnect(true, true); 364 restore_nvs(); 365 } 366 367 static void test_wifi_no_ap_fallback_without_request(void) { 368 GIVEN("AP is disabled and WiFi is disconnected"); 369 save_nvs(); 370 371 networking::wifi::ap::disable(); 372 WiFi.disconnect(true, true); 373 delay(100); 374 375 WifiConnectCommand command = { 376 .request = { 377 .ssid = "ssid-that-should-not-exist", 378 .password = "definitely-not-the-right-password", 379 .enable_ap_fallback = false, 380 }, 381 .result = {}, 382 }; 383 384 WHEN("connect fails without fallback requested"); 385 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::connect(&command), 386 "device: WiFi connect should fail with intentionally invalid credentials"); 387 TEST_ASSERT_FALSE_MESSAGE(command.result.connected, 388 "device: connect result should report disconnected after failed connect"); 389 390 THEN("AP remains inactive"); 391 TEST_ASSERT_FALSE_MESSAGE(command.result.ap_enabled_for_fallback, 392 "device: AP fallback should stay disabled when not requested"); 393 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::ap::isActive(), 394 "device: AP should remain inactive after failed connect without fallback request"); 395 396 WiFi.disconnect(true, true); 397 restore_nvs(); 398 } 399 400 static void test_wifi_snapshot_ap_fallback_state(void) { 401 GIVEN("a failed connect with AP fallback enabled"); 402 save_nvs(); 403 404 networking::wifi::ap::disable(); 405 WiFi.disconnect(true, true); 406 delay(100); 407 408 WifiConnectCommand command = { 409 .request = { 410 .ssid = "ssid-that-should-not-exist", 411 .password = "definitely-not-the-right-password", 412 .enable_ap_fallback = true, 413 }, 414 .result = {}, 415 }; 416 417 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::connect(&command), 418 "device: WiFi connect should fail before snapshot fallback verification"); 419 TEST_ASSERT_TRUE_MESSAGE(command.result.ap_enabled_for_fallback, 420 "device: AP fallback should be active before snapshot verification"); 421 422 THEN("the snapshot reflects AP fallback state"); 423 NetworkStatusSnapshot snapshot = {}; 424 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::accessSnapshot(&snapshot), 425 "device: accessSnapshot should succeed after AP fallback activation"); 426 427 TEST_ASSERT_FALSE_MESSAGE(snapshot.connected, 428 "device: station snapshot should report disconnected after failed connect"); 429 TEST_ASSERT_TRUE_MESSAGE(snapshot.ap.active, 430 "device: AP snapshot should report active after fallback activation"); 431 TEST_ASSERT_EQUAL_STRING_MESSAGE(services::identity::access_hostname(), snapshot.hostname, 432 "device: station snapshot hostname should match identity hostname"); 433 TEST_ASSERT_EQUAL_STRING_MESSAGE(services::identity::access_hostname(), snapshot.ap.hostname, 434 "device: AP snapshot hostname should match identity hostname"); 435 436 APConfig ap_config = {}; 437 networking::wifi::ap::accessConfig(&ap_config); 438 TEST_ASSERT_EQUAL_STRING_MESSAGE(ap_config.ssid, snapshot.ap.ssid, 439 "device: AP snapshot SSID should match AP config"); 440 TEST_ASSERT_EQUAL_STRING_MESSAGE(ap_config.password, snapshot.ap.password, 441 "device: AP snapshot password should match AP config"); 442 TEST_ASSERT_TRUE_MESSAGE(snapshot.ap.ip[0] != '\0', 443 "device: AP snapshot IP should be populated when AP is active"); 444 TEST_ASSERT_TRUE_MESSAGE(snapshot.ap.mac[0] != '\0', 445 "device: AP snapshot MAC should be populated when AP is active"); 446 447 networking::wifi::ap::disable(); 448 WiFi.disconnect(true, true); 449 restore_nvs(); 450 } 451 452 static void test_wifi_snapshot_rejects_null(void) { 453 THEN("null snapshot buffer is rejected"); 454 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::accessSnapshot(nullptr), 455 "device: accessSnapshot should reject null output buffer"); 456 } 457 458 static void test_wifi_config_rejects_null(void) { 459 THEN("null config buffer is rejected"); 460 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::accessConfig(nullptr), 461 "device: accessConfig should reject null output buffer"); 462 } 463 464 static void test_wifi_store_config_rejects_null(void) { 465 THEN("null config input is rejected"); 466 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::storeConfig(nullptr), 467 "device: storeConfig should reject null config buffer"); 468 } 469 470 static void test_wifi_connect_rejects_null(void) { 471 THEN("null connect command is rejected"); 472 TEST_ASSERT_FALSE_MESSAGE(networking::wifi::connect(nullptr), 473 "device: connect should reject null command buffer"); 474 } 475 476 static void test_wifi_snapshot_ap_inactive(void) { 477 GIVEN("the AP is disabled"); 478 save_nvs(); 479 480 networking::wifi::ap::disable(); 481 WiFi.disconnect(true, true); 482 delay(100); 483 484 THEN("the snapshot reports AP inactive"); 485 NetworkStatusSnapshot snapshot = {}; 486 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::accessSnapshot(&snapshot), 487 "device: accessSnapshot should succeed when AP is disabled"); 488 TEST_ASSERT_FALSE_MESSAGE(snapshot.ap.active, 489 "device: AP snapshot should report inactive when AP is disabled"); 490 TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, snapshot.ap.clients, 491 "device: AP snapshot should report zero clients when AP is disabled"); 492 493 restore_nvs(); 494 } 495 496 static void test_wifi_ap_config_roundtrip(void) { 497 WHEN("AP config is stored and read back"); 498 save_nvs(); 499 500 APConfigureCommand command = { 501 .config = {}, 502 .snapshot = {}, 503 }; 504 strlcpy(command.config.ssid, "my-custom-ap", sizeof(command.config.ssid)); 505 strlcpy(command.config.password, "secret123", sizeof(command.config.password)); 506 networking::wifi::ap::applyConfig(&command); 507 508 APConfig cfg = {}; 509 networking::wifi::ap::accessConfig(&cfg); 510 511 TEST_ASSERT_EQUAL_STRING_MESSAGE("my-custom-ap", cfg.ssid, 512 "device: AP SSID mismatch after roundtrip"); 513 TEST_ASSERT_EQUAL_STRING_MESSAGE("secret123", cfg.password, 514 "device: AP password mismatch after roundtrip"); 515 516 restore_nvs(); 517 } 518 519 static void test_wifi_ap_default_ssid(void) { 520 GIVEN("the AP SSID key is removed from NVS"); 521 save_nvs(); 522 523 Preferences preferences; 524 preferences.begin(config::wifi::NVS_NAMESPACE, false); 525 preferences.remove("ap_ssid"); 526 preferences.end(); 527 528 THEN("the AP SSID defaults to the configured value"); 529 APConfig cfg = {}; 530 networking::wifi::ap::accessConfig(&cfg); 531 532 TEST_ASSERT_EQUAL_STRING_MESSAGE(config::wifi::ap::SSID, cfg.ssid, 533 "device: AP SSID should default to config::wifi::ap::SSID"); 534 535 restore_nvs(); 536 } 537 538 static void test_wifi_ap_enabled_default_true(void) { 539 GIVEN("the ap_on key is removed from NVS"); 540 save_nvs(); 541 542 Preferences preferences; 543 preferences.begin(config::wifi::NVS_NAMESPACE, false); 544 preferences.remove("ap_on"); 545 preferences.end(); 546 547 THEN("AP is enabled by default"); 548 Preferences prefs; 549 bool opened = networking::wifi::internal::openPreferences(true, &prefs); 550 bool enabled = opened ? prefs.getBool("ap_on", true) : true; 551 if (opened) prefs.end(); 552 TEST_ASSERT_TRUE_MESSAGE(enabled, 553 "device: AP should be enabled by default"); 554 555 restore_nvs(); 556 } 557 558 static void test_wifi_ap_enabled_toggle(void) { 559 WHEN("the AP enabled flag is toggled"); 560 save_nvs(); 561 562 Preferences preferences; 563 preferences.begin(config::wifi::NVS_NAMESPACE, false); 564 preferences.putBool("ap_on", false); 565 preferences.end(); 566 567 { 568 Preferences prefs; 569 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::internal::openPreferences(true, &prefs), 570 "device: wifi NVS namespace must be readable"); 571 TEST_ASSERT_FALSE_MESSAGE(prefs.getBool("ap_on", true), 572 "device: AP should be disabled after setting false"); 573 prefs.end(); 574 } 575 576 preferences.begin(config::wifi::NVS_NAMESPACE, false); 577 preferences.putBool("ap_on", true); 578 preferences.end(); 579 580 { 581 Preferences prefs; 582 TEST_ASSERT_TRUE_MESSAGE(networking::wifi::internal::openPreferences(true, &prefs), 583 "device: wifi NVS namespace must be readable"); 584 TEST_ASSERT_TRUE_MESSAGE(prefs.getBool("ap_on", true), 585 "device: AP should be enabled after setting true"); 586 prefs.end(); 587 } 588 589 restore_nvs(); 590 } 591 592 void networking::wifi::test(void) { 593 MODULE("WiFi"); 594 RUN_TEST(test_wifi_snapshot_rejects_null); 595 RUN_TEST(test_wifi_config_rejects_null); 596 RUN_TEST(test_wifi_store_config_rejects_null); 597 RUN_TEST(test_wifi_connect_rejects_null); 598 RUN_TEST(test_wifi_saved_sta_config_roundtrip); 599 RUN_TEST(test_wifi_connect_prefers_explicit_credentials); 600 RUN_TEST(test_wifi_connect_uses_saved_config); 601 RUN_TEST(test_wifi_ap_fallback_on_failed_connect); 602 RUN_TEST(test_wifi_no_ap_fallback_without_request); 603 RUN_TEST(test_wifi_snapshot_ap_fallback_state); 604 RUN_TEST(test_wifi_snapshot_ap_inactive); 605 RUN_TEST(test_wifi_connect_fails_without_ssid); 606 RUN_TEST(test_wifi_ap_config_roundtrip); 607 RUN_TEST(test_wifi_ap_default_ssid); 608 RUN_TEST(test_wifi_ap_enabled_default_true); 609 RUN_TEST(test_wifi_ap_enabled_toggle); 610 } 611 612 #endif