ESPHome 2026.4.2
Loading...
Searching...
No Matches
wifi_component.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
2#ifdef USE_WIFI
3#include <cassert>
4#include <cinttypes>
5#include <cmath>
6#include <type_traits>
7
8#ifdef USE_ESP32
9#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
10#include <esp_eap_client.h>
11#else
12#include <esp_wpa2.h>
13#endif
14#endif
15
16#if defined(USE_ESP32)
17#include <esp_wifi.h>
18#endif
19#ifdef USE_ESP8266
20#include <user_interface.h>
21#endif
22
23#include <algorithm>
24#include <new>
25#include <utility>
26#include "lwip/dns.h"
27#include "lwip/err.h"
28
30#include "esphome/core/hal.h"
32#include "esphome/core/log.h"
34#include "esphome/core/util.h"
35
36#ifdef USE_CAPTIVE_PORTAL
38#endif
39
40#ifdef USE_IMPROV
42#endif
43
44#ifdef USE_IMPROV_SERIAL
46#endif
47
48namespace esphome::wifi {
49
50static const char *const TAG = "wifi";
51
52// CompactString implementation
53CompactString::CompactString(const char *str, size_t len) {
54 if (len > MAX_LENGTH) {
55 len = MAX_LENGTH; // Clamp to max valid length
56 }
57
58 this->length_ = len;
59 if (len <= INLINE_CAPACITY) {
60 // Store inline with null terminator
61 this->is_heap_ = 0;
62 if (len > 0) {
63 std::memcpy(this->storage_, str, len);
64 }
65 this->storage_[len] = '\0';
66 } else {
67 // Heap allocate with null terminator
68 this->is_heap_ = 1;
69 char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
70 std::memcpy(heap_data, str, len);
71 heap_data[len] = '\0';
72 this->set_heap_ptr_(heap_data);
73 }
74}
75
76CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
77
79 if (this != &other) {
80 this->~CompactString();
81 new (this) CompactString(other);
82 }
83 return *this;
84}
85
86CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
87 // Copy full storage (includes null terminator for inline, or pointer for heap)
88 std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
89 other.length_ = 0;
90 other.is_heap_ = 0;
91 other.storage_[0] = '\0';
92}
93
95 if (this != &other) {
96 this->~CompactString();
97 new (this) CompactString(std::move(other));
98 }
99 return *this;
100}
101
103 if (this->is_heap_) {
104 delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
105 }
106}
107
108bool CompactString::operator==(const CompactString &other) const {
109 return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
110}
111bool CompactString::operator==(const StringRef &other) const {
112 return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0;
113}
114
310
311// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
312static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
314 return LOG_STR("INITIAL_CONNECT");
315#ifdef USE_WIFI_FAST_CONNECT
317 return LOG_STR("FAST_CONNECT_CYCLING");
318#endif
320 return LOG_STR("EXPLICIT_HIDDEN");
322 return LOG_STR("SCAN_CONNECTING");
323 if (phase == WiFiRetryPhase::RETRY_HIDDEN)
324 return LOG_STR("RETRY_HIDDEN");
326 return LOG_STR("RESTARTING");
327 return LOG_STR("UNKNOWN");
328}
329
331 // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
332 // This means those networks were already tried and should be skipped in RETRY_HIDDEN
333 return !this->sta_.empty() && this->sta_[0].get_hidden();
334}
335
337 // Find the first network that is NOT marked hidden:true
338 // This is where EXPLICIT_HIDDEN phase would have stopped
339 for (size_t i = 0; i < this->sta_.size(); i++) {
340 if (!this->sta_[i].get_hidden()) {
341 return static_cast<int8_t>(i);
342 }
343 }
344 return -1; // All networks are hidden
345}
346
347// 2 attempts per BSSID in SCAN_CONNECTING phase
348// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
349// Auth failures are common immediately after scan due to WiFi stack state transitions.
350// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
351// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
352static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
353
354// 1 attempt per SSID in RETRY_HIDDEN phase
355// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
356static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
357
358// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
359// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
360static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
361
364static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
365
369static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
370
374static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
375
384static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
385
386static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
387 switch (phase) {
389#ifdef USE_WIFI_FAST_CONNECT
391#endif
392 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
393 return WIFI_RETRY_COUNT_PER_AP;
395 // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
396 return WIFI_RETRY_COUNT_PER_SSID;
398 // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
399 return WIFI_RETRY_COUNT_PER_BSSID;
401 // Hidden network mode: 1 attempt per SSID
402 return WIFI_RETRY_COUNT_PER_SSID;
403 default:
404 return WIFI_RETRY_COUNT_PER_BSSID;
405 }
406}
407
408static void apply_scan_result_to_params(WiFiAP &params, const WiFiScanResult &scan) {
409 params.set_hidden(false);
410 params.set_ssid(scan.get_ssid());
411 params.set_bssid(scan.get_bssid());
412 params.set_channel(scan.get_channel());
413}
414
416 // Only SCAN_CONNECTING phase needs scan results
418 return false;
419 }
420 // Need scan if we have no results or no matching networks
421 return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
422}
423
425 // Check if this SSID is configured as hidden
426 // If explicitly marked hidden, we should always try hidden mode regardless of scan results
427 for (const auto &conf : this->sta_) {
428 if (conf.ssid_ == ssid && conf.get_hidden()) {
429 return false; // Treat as not seen - force hidden mode attempt
430 }
431 }
432
433 // Otherwise, check if we saw it in scan results
434 for (const auto &scan : this->scan_result_) {
435 if (scan.ssid_ == ssid) {
436 return true;
437 }
438 }
439 return false;
440}
441
443 // Components that require full scan results (for example, scan result listeners)
444 // are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
445 if (this->keep_scan_results_) {
446 return true;
447 }
448
449#ifdef USE_CAPTIVE_PORTAL
450 // Captive portal needs full results when active (showing network list to user)
452 return true;
453 }
454#endif
455
456#ifdef USE_IMPROV_SERIAL
457 // Improv serial needs results during provisioning (before connected)
459 return true;
460 }
461#endif
462
463#ifdef USE_IMPROV
464 // BLE improv also needs results during provisioning
466 return true;
467 }
468#endif
469
470 return false;
471}
472
473bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
474 // Hidden networks in scan results have empty SSIDs - skip them
475 if (ssid[0] == '\0') {
476 return false;
477 }
478 for (const auto &sta : this->sta_) {
479 // Skip hidden network configs (they don't appear in normal scans)
480 if (sta.get_hidden()) {
481 continue;
482 }
483 // For BSSID-only configs (empty SSID), match by BSSID
484 if (sta.ssid_.empty()) {
485 if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
486 return true;
487 }
488 continue;
489 }
490 // Match by SSID
491 if (sta.ssid_ == ssid) {
492 return true;
493 }
494 }
495 return false;
496}
497
499 for (auto &it : this->sta_priorities_) {
500 if (it.bssid == bssid) {
501 it.priority = priority;
502 return;
503 }
504 }
505 this->sta_priorities_.push_back(WiFiSTAPriority{
506 .bssid = bssid,
507 .priority = priority,
508 });
509}
510
511void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
512#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
513 // Skip logging during roaming scans to avoid log buffer overflow
514 // (roaming scans typically find many networks but only care about same-SSID APs)
516 return;
517 }
518 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
519 format_mac_addr_upper(bssid, bssid_s);
520 ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
521#endif
522}
523
524int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
525 // Find next SSID to try in RETRY_HIDDEN phase.
526 //
527 // This function operates in two modes based on retry_hidden_mode_:
528 //
529 // 1. SCAN_BASED mode:
530 // After SCAN_CONNECTING phase, only returns networks that were NOT visible
531 // in the scan (truly hidden networks that need probe requests).
532 //
533 // 2. BLIND_RETRY mode:
534 // When captive portal/improv is active, scanning is skipped to avoid
535 // disrupting the AP. In this mode, ALL configured networks are returned
536 // as candidates, cycling through them sequentially. This allows the device
537 // to keep trying all networks while users configure WiFi via captive portal.
538 //
539 // Additionally, if EXPLICIT_HIDDEN phase was executed (first network marked hidden:true),
540 // those networks are skipped here since they were already tried.
541 //
542 bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
543 // Start searching from start_index + 1
544 for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
545 const auto &sta = this->sta_[i];
546
547 // Skip networks that were already tried in EXPLICIT_HIDDEN phase
548 // Those are: networks marked hidden:true that appear before the first non-hidden network
549 // If all networks are hidden (first_non_hidden_idx == -1), skip all of them
550 if (!include_explicit_hidden && sta.get_hidden()) {
551 int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
552 if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
553 ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str());
554 continue;
555 }
556 }
557
558 // In BLIND_RETRY mode, treat all networks as candidates
559 // In SCAN_BASED mode, only retry networks that weren't seen in the scan
561 ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
562 return static_cast<int8_t>(i);
563 }
564 ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
565 }
566 // No hidden SSIDs found
567 return -1;
568}
569
571 // If first network (highest priority) is explicitly marked hidden, try it first before scanning
572 // This respects user's priority order when they explicitly configure hidden networks
573 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
574 ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
575 this->selected_sta_index_ = 0;
578 this->start_connecting(params);
579 } else {
580 this->start_scanning();
581 }
582}
583
584#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
585static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
586 switch (type) {
587 case ESP_EAP_TTLS_PHASE2_PAP:
588 return "pap";
589 case ESP_EAP_TTLS_PHASE2_CHAP:
590 return "chap";
591 case ESP_EAP_TTLS_PHASE2_MSCHAP:
592 return "mschap";
593 case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
594 return "mschapv2";
595 case ESP_EAP_TTLS_PHASE2_EAP:
596 return "eap";
597 default:
598 return "unknown";
599 }
600}
601#endif
602
604
606 this->wifi_pre_setup_();
607
608#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
609 // Create semaphore for high-performance mode requests
610 // Start at 0, increment on request, decrement on release
611 this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
612 if (this->high_performance_semaphore_ == nullptr) {
613 ESP_LOGE(TAG, "Failed semaphore");
614 }
615
616 // Store the configured power save mode as baseline
618#endif
619
620 if (this->enable_on_boot_) {
621 this->start();
622 } else {
623#ifdef USE_ESP32
624 esp_netif_init();
625#endif
627 }
628}
629
631 ESP_LOGCONFIG(TAG, "Starting");
632 this->last_connected_ = millis();
633
634 uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL;
635
637#ifdef USE_WIFI_FAST_CONNECT
639#endif
640
641 SavedWifiSettings save{};
642 if (this->pref_.load(&save)) {
643 ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
644
645 WiFiAP sta{};
646 sta.set_ssid(save.ssid);
647 sta.set_password(save.password);
648 this->set_sta(sta);
649 }
650
651 if (this->has_sta()) {
652 this->wifi_sta_pre_setup_();
653 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
654 ESP_LOGV(TAG, "Setting Output Power Option failed");
655 }
656
657#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
658 // Synchronize power_save_ with semaphore state before applying
659 if (this->high_performance_semaphore_ != nullptr) {
660 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
661 if (semaphore_count > 0) {
663 this->is_high_performance_mode_ = true;
664 } else {
666 this->is_high_performance_mode_ = false;
667 }
668 }
669#endif
670 if (!this->wifi_apply_power_save_()) {
671 ESP_LOGV(TAG, "Setting Power Save Option failed");
672 }
673
675#ifdef USE_WIFI_FAST_CONNECT
676 WiFiAP params;
677 bool loaded_fast_connect = this->load_fast_connect_settings_(params);
678 // Fast connect optimization: only use when we have saved BSSID+channel data
679 // Without saved data, try first configured network or use normal flow
680 if (loaded_fast_connect) {
681 ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str());
682 this->start_connecting(params);
683 } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
684 // No saved data, but have configured networks - try first non-hidden network
685 ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
686 this->selected_sta_index_ = 0;
687 params = this->build_params_for_current_phase_();
688 this->start_connecting(params);
689 } else {
690 // No saved data and (no networks OR first is hidden) - use normal flow
692 }
693#else
694 // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
696#endif
697#ifdef USE_WIFI_AP
698 } else if (this->has_ap()) {
699 this->setup_ap_config_();
700 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
701 ESP_LOGV(TAG, "Setting Output Power Option failed");
702 }
703#ifdef USE_CAPTIVE_PORTAL
705 this->wifi_sta_pre_setup_();
706 this->start_scanning();
708 }
709#endif
710#endif // USE_WIFI_AP
711 }
712#ifdef USE_IMPROV
713 if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
714 if (this->wifi_mode_(true, {}))
716 }
717#endif
718 this->wifi_apply_hostname_();
719}
720
722 ESP_LOGW(TAG, "Restarting adapter");
723 this->wifi_mode_(false, {});
724 // Clear error flag here because restart_adapter() enters COOLDOWN state,
725 // and check_connecting_finished() is called after cooldown without going
726 // through start_connecting() first. Without this clear, stale errors would
727 // trigger spurious "failed (callback)" logs. The canonical clear location
728 // is in start_connecting(); this is the only exception to that pattern.
729 this->error_from_callback_ = false;
730}
731
733 this->wifi_loop_();
736
737 if (this->has_sta()) {
738#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
739 if (this->is_connected() != this->handled_connected_state_) {
740#ifdef USE_WIFI_DISCONNECT_TRIGGER
741 if (this->handled_connected_state_) {
743 }
744#endif
745#ifdef USE_WIFI_CONNECT_TRIGGER
746 if (!this->handled_connected_state_) {
748 }
749#endif
751 }
752#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER
753
754 switch (this->state_) {
756 this->status_set_warning(LOG_STR("waiting to reconnect"));
757 // Skip cooldown if new credentials were provided while connecting
758 if (this->skip_cooldown_next_cycle_) {
759 this->skip_cooldown_next_cycle_ = false;
760 this->check_connecting_finished(now);
761 break;
762 }
763 // Use longer cooldown when captive portal/improv is active to avoid disrupting user config
764 bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
765 uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
766 if (now - this->action_started_ > cooldown_duration) {
767 // After cooldown we either restarted the adapter because of
768 // a failure, or something tried to connect over and over
769 // so we entered cooldown. In both cases we call
770 // check_connecting_finished to continue the state machine.
771 this->check_connecting_finished(now);
772 }
773 break;
774 }
776 this->status_set_warning(LOG_STR("scanning for networks"));
778 break;
779 }
781 this->status_set_warning(LOG_STR("associating to network"));
782 this->check_connecting_finished(now);
783 break;
784 }
785
787 // Use cached connected_ set unconditionally at the top of loop()
788 if (!this->connected_) {
789 ESP_LOGW(TAG, "Connection lost; reconnecting");
791 this->retry_connect();
792 } else {
793 this->status_clear_warning();
794 this->last_connected_ = now;
795
796 // Post-connect roaming: check for better AP
797 if (this->post_connect_roaming_) {
799 if (this->scan_done_) {
800 this->process_roaming_scan_();
801 }
802 // else: scan in progress, wait
805 this->check_roaming_(now);
806 }
807 }
808 }
809 break;
810 }
813 break;
815 return;
816 }
817
818#ifdef USE_WIFI_AP
819 if (this->has_ap() && !this->ap_setup_) {
820 if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
821 ESP_LOGI(TAG, "Starting fallback AP");
822 this->setup_ap_config_();
823#ifdef USE_CAPTIVE_PORTAL
825 // Reset so we force one full scan after captive portal starts
826 // (previous scans were filtered because captive portal wasn't active yet)
829 }
830#endif
831 }
832 }
833#endif // USE_WIFI_AP
834
835#ifdef USE_IMPROV
837 !esp32_improv::global_improv_component->should_start()) {
838 if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
839 if (this->wifi_mode_(true, {}))
841 }
842 }
843
844#endif
845
846 if (!this->has_ap() && this->reboot_timeout_ != 0) {
847 if (now - this->last_connected_ > this->reboot_timeout_) {
848 ESP_LOGE(TAG, "Can't connect; rebooting");
849 App.reboot();
850 }
851 }
852 }
853
854#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
855 // Check if power save mode needs to be updated based on high-performance requests
856 if (this->high_performance_semaphore_ != nullptr) {
857 // Semaphore count directly represents active requests (starts at 0, increments on request)
858 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
859
860 if (semaphore_count > 0 && !this->is_high_performance_mode_) {
861 // Transition to high-performance mode (no power save)
862 ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
863 semaphore_count == 1 ? "request" : "requests");
865 if (this->wifi_apply_power_save_()) {
866 this->is_high_performance_mode_ = true;
867 }
868 } else if (semaphore_count == 0 && this->is_high_performance_mode_) {
869 // Restore to configured power save mode
870 ESP_LOGV(TAG, "Restoring power save mode to configured setting");
872 if (this->wifi_apply_power_save_()) {
873 this->is_high_performance_mode_ = false;
874 }
875 }
876 }
877#endif
878}
879
881
882#ifdef USE_WIFI_11KV_SUPPORT
883void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
884void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
885#endif
887 if (this->has_sta())
888 return this->wifi_sta_ip_addresses();
889
890#ifdef USE_WIFI_AP
891 if (this->has_ap())
892 return {this->wifi_soft_ap_ip()};
893#endif // USE_WIFI_AP
894
895 return {};
896}
898 if (this->has_sta())
899 return this->wifi_dns_ip_(num);
900 return {};
901}
902
903#ifdef USE_WIFI_AP
905 this->wifi_mode_({}, true);
906
907 if (this->ap_setup_)
908 return;
909
910 if (this->ap_.ssid_.empty()) {
911 // Build AP SSID from app name without heap allocation
912 // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
913 static constexpr size_t AP_SSID_MAX_LEN = 32;
914 static constexpr size_t AP_SSID_PREFIX_LEN = 25;
915 static constexpr size_t AP_SSID_SUFFIX_LEN = 7;
916
917 const auto &app_name = App.get_name();
918 const char *name_ptr = app_name.c_str();
919 size_t name_len = app_name.length();
920
921 if (name_len <= AP_SSID_MAX_LEN) {
922 // Name fits, use directly
923 this->ap_.set_ssid(name_ptr);
924 } else {
925 // Name too long, need to truncate into stack buffer
926 char ssid_buf[AP_SSID_MAX_LEN + 1];
928 // Keep first 25 chars and last 7 chars (MAC suffix), remove middle
929 memcpy(ssid_buf, name_ptr, AP_SSID_PREFIX_LEN);
930 memcpy(ssid_buf + AP_SSID_PREFIX_LEN, name_ptr + name_len - AP_SSID_SUFFIX_LEN, AP_SSID_SUFFIX_LEN);
931 } else {
932 memcpy(ssid_buf, name_ptr, AP_SSID_MAX_LEN);
933 }
934 ssid_buf[AP_SSID_MAX_LEN] = '\0';
935 this->ap_.set_ssid(ssid_buf);
936 }
937 }
938 this->ap_setup_ = this->wifi_start_ap_(this->ap_);
939
940 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
941 ESP_LOGCONFIG(TAG,
942 "Setting up AP:\n"
943 " AP SSID: '%s'\n"
944 " AP Password: '%s'\n"
945 " IP Address: %s",
946 this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
947
948#ifdef USE_WIFI_MANUAL_IP
949 auto manual_ip = this->ap_.get_manual_ip();
950 if (manual_ip.has_value()) {
951 char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
952 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
953 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
954 ESP_LOGCONFIG(TAG,
955 " AP Static IP: '%s'\n"
956 " AP Gateway: '%s'\n"
957 " AP Subnet: '%s'",
958 manual_ip->static_ip.str_to(static_ip_buf), manual_ip->gateway.str_to(gateway_buf),
959 manual_ip->subnet.str_to(subnet_buf));
960 }
961#endif
962
963 if (!this->has_sta()) {
965 }
966}
967
969 this->ap_ = ap;
970 this->has_ap_ = true;
971}
972#endif // USE_WIFI_AP
973
974void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
975void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
977 // Clear roaming state - no more configured networks
978 this->clear_roaming_state_();
979 this->sta_.clear();
980 this->selected_sta_index_ = -1;
981}
983 this->clear_sta(); // Also clears roaming state
984 this->init_sta(1);
985 this->add_sta(ap);
986 this->selected_sta_index_ = 0;
987 // When new credentials are set (e.g., from improv), skip cooldown to retry immediately
988 this->skip_cooldown_next_cycle_ = true;
989}
990
992 const WiFiAP *config = this->get_selected_sta_();
993 if (config == nullptr) {
994 ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
995 static_cast<int>(this->selected_sta_index_), this->sta_.size());
996 // Return empty params - caller should handle this gracefully
997 return WiFiAP();
998 }
999
1000 WiFiAP params = *config;
1001
1002 switch (this->retry_phase_) {
1004#ifdef USE_WIFI_FAST_CONNECT
1006#endif
1007 // Fast connect phases: use config-only (no scan results)
1008 // BSSID/channel from config if user specified them, otherwise empty
1009 break;
1010
1013 // Hidden network mode: clear BSSID/channel to trigger probe request
1014 // (both explicit hidden and retry hidden use same behavior)
1015 params.clear_bssid();
1016 params.clear_channel();
1017 break;
1018
1020 // Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
1021 if (!this->scan_result_.empty()) {
1022 apply_scan_result_to_params(params, this->scan_result_[0]);
1023 }
1024 break;
1025
1027 // Should not be building params during restart
1028 break;
1029 }
1030
1031 return params;
1032}
1033
1035 const WiFiAP *config = this->get_selected_sta_();
1036 return config ? *config : WiFiAP{};
1037}
1038void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
1039 this->save_wifi_sta(ssid.c_str(), password.c_str());
1040}
1041void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
1042 SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
1043 strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
1044 strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
1045 this->pref_.save(&save);
1046 // ensure it's written immediately
1048
1049 WiFiAP sta{};
1050 sta.set_ssid(ssid);
1051 sta.set_password(password);
1052 this->set_sta(sta);
1053
1054 // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
1055 this->connect_soon_();
1056}
1057
1059 // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
1061 ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
1062 this->retry_connect();
1063 }
1064}
1065
1067 // Log connection attempt at INFO level with priority
1068 char bssid_s[18];
1069 int8_t priority = 0;
1070
1071 if (ap.has_bssid()) {
1072 format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
1073 priority = this->get_sta_priority(ap.get_bssid());
1074 }
1075
1076 ESP_LOGI(TAG,
1077 "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
1078 ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
1079 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1080
1081#ifdef ESPHOME_LOG_HAS_VERBOSE
1082 ESP_LOGV(TAG,
1083 "Connection Params:\n"
1084 " SSID: '%s'",
1085 ap.ssid_.c_str());
1086 if (ap.has_bssid()) {
1087 ESP_LOGV(TAG, " BSSID: %s", bssid_s);
1088 } else {
1089 ESP_LOGV(TAG, " BSSID: Not Set");
1090 }
1091
1092#ifdef USE_WIFI_WPA2_EAP
1093 auto eap_opt = ap.get_eap();
1094 if (eap_opt.has_value()) {
1095 EAPAuth eap_config = *eap_opt;
1096 // clang-format off
1097 ESP_LOGV(
1098 TAG,
1099 " WPA2 Enterprise authentication configured:\n"
1100 " Identity: " LOG_SECRET("'%s'") "\n"
1101 " Username: " LOG_SECRET("'%s'") "\n"
1102 " Password: " LOG_SECRET("'%s'"),
1103 eap_config.identity.c_str(), eap_config.username.c_str(), eap_config.password.c_str());
1104 // clang-format on
1105#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1106 ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
1107#endif
1108 bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
1109 bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
1110 bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
1111 ESP_LOGV(TAG,
1112 " CA Cert: %s\n"
1113 " Client Cert: %s\n"
1114 " Client Key: %s",
1115 ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present",
1116 client_key_present ? "present" : "not present");
1117 } else {
1118#endif
1119 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
1120#ifdef USE_WIFI_WPA2_EAP
1121 }
1122#endif
1123 if (ap.has_channel()) {
1124 ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
1125 } else {
1126 ESP_LOGV(TAG, " Channel not set");
1127 }
1128#ifdef USE_WIFI_MANUAL_IP
1129 auto manual_ip = ap.get_manual_ip();
1130 if (manual_ip.has_value()) {
1131 ManualIP m = *manual_ip;
1132 char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1133 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
1134 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
1135 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
1136 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
1137 ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf),
1138 m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf),
1139 m.dns2.str_to(dns2_buf));
1140 } else
1141#endif
1142 {
1143 ESP_LOGV(TAG, " Using DHCP IP");
1144 }
1145 ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
1146#endif
1147
1148 // Clear any stale error from previous connection attempt.
1149 // This is the canonical location for clearing the flag since all connection
1150 // attempts go through start_connecting(). The only other clear is in
1151 // restart_adapter() which enters COOLDOWN without calling start_connecting().
1152 this->error_from_callback_ = false;
1153
1154 if (!this->wifi_sta_connect_(ap)) {
1155 ESP_LOGE(TAG, "wifi_sta_connect_ failed");
1156 // Enter cooldown to allow WiFi hardware to stabilize
1157 // (immediate failure suggests hardware not ready, different from connection timeout)
1159 } else {
1161 }
1162 this->action_started_ = millis();
1163}
1164
1165const LogString *get_signal_bars(int8_t rssi) {
1166 // LOWER ONE QUARTER BLOCK
1167 // Unicode: U+2582, UTF-8: E2 96 82
1168 // LOWER HALF BLOCK
1169 // Unicode: U+2584, UTF-8: E2 96 84
1170 // LOWER THREE QUARTERS BLOCK
1171 // Unicode: U+2586, UTF-8: E2 96 86
1172 // FULL BLOCK
1173 // Unicode: U+2588, UTF-8: E2 96 88
1174 if (rssi >= -50) {
1175 return LOG_STR("\033[0;32m" // green
1176 "\xe2\x96\x82"
1177 "\xe2\x96\x84"
1178 "\xe2\x96\x86"
1179 "\xe2\x96\x88"
1180 "\033[0m");
1181 } else if (rssi >= -65) {
1182 return LOG_STR("\033[0;33m" // yellow
1183 "\xe2\x96\x82"
1184 "\xe2\x96\x84"
1185 "\xe2\x96\x86"
1186 "\033[0;37m"
1187 "\xe2\x96\x88"
1188 "\033[0m");
1189 } else if (rssi >= -85) {
1190 return LOG_STR("\033[0;33m" // yellow
1191 "\xe2\x96\x82"
1192 "\xe2\x96\x84"
1193 "\033[0;37m"
1194 "\xe2\x96\x86"
1195 "\xe2\x96\x88"
1196 "\033[0m");
1197 } else {
1198 return LOG_STR("\033[0;31m" // red
1199 "\xe2\x96\x82"
1200 "\033[0;37m"
1201 "\xe2\x96\x84"
1202 "\xe2\x96\x86"
1203 "\xe2\x96\x88"
1204 "\033[0m");
1205 }
1206}
1207
1209 bssid_t bssid = wifi_bssid();
1210 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1211 format_mac_addr_upper(bssid.data(), bssid_s);
1212 // Use stack buffers for IP address formatting to avoid heap allocations
1213 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1214 for (auto &ip : wifi_sta_ip_addresses()) {
1215 if (ip.is_set()) {
1216 ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf));
1217 }
1218 }
1219 int8_t rssi = wifi_rssi();
1220 // Use stack buffers for SSID and all IP addresses to avoid heap allocations
1221 char ssid_buf[SSID_BUFFER_SIZE];
1222 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
1223 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
1224 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
1225 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
1226 // clang-format off
1227 ESP_LOGCONFIG(TAG,
1228 " SSID: " LOG_SECRET("'%s'") "\n"
1229 " BSSID: " LOG_SECRET("%s") "\n"
1230 " Hostname: '%s'\n"
1231 " Signal strength: %d dB %s\n"
1232 " Channel: %" PRId32 "\n"
1233 " Subnet: %s\n"
1234 " Gateway: %s\n"
1235 " DNS1: %s\n"
1236 " DNS2: %s",
1237 wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
1238 get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf),
1239 wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf));
1240 // clang-format on
1241#ifdef ESPHOME_LOG_HAS_VERBOSE
1242 if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1243 ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid()));
1244 }
1245#endif
1246#ifdef USE_WIFI_11KV_SUPPORT
1247 ESP_LOGCONFIG(TAG,
1248 " BTM: %s\n"
1249 " RRM: %s",
1250 this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled");
1251#endif
1252}
1253
1256 return;
1257
1258 ESP_LOGD(TAG, "Enabling");
1260 this->start();
1261}
1262
1265 return;
1266
1267 ESP_LOGD(TAG, "Disabling");
1269 this->wifi_disconnect_();
1270 this->wifi_mode_(false, false);
1271}
1272
1274
1276 this->action_started_ = millis();
1277 ESP_LOGD(TAG, "Starting scan");
1278 this->wifi_scan_start_(this->passive_scan_);
1280}
1281
1315[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
1316 // Matching networks always come before non-matching
1317 if (a.get_matches() && !b.get_matches())
1318 return true;
1319 if (!a.get_matches() && b.get_matches())
1320 return false;
1321
1322 // Both matching: check priority first (tracks connection failures via priority degradation)
1323 // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
1324 if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
1325 return a.get_priority() > b.get_priority();
1326 }
1327
1328 // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
1329 return a.get_rssi() > b.get_rssi();
1330}
1331
1332// Helper function for insertion sort of WiFi scan results
1333// Using insertion sort instead of std::stable_sort saves flash memory
1334// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
1335// IMPORTANT: This sort is stable (preserves relative order of equal elements)
1336//
1337// Uses raw memcpy instead of copy assignment to avoid CompactString's
1338// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
1339// Copy assignment calls ~CompactString() then placement-new for every shift,
1340// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
1341// networks (e.g., captive portal showing full scan results), this caused
1342// event loop blocking from hundreds of heap operations in a tight loop.
1343//
1344// This is safe because we're permuting elements within the same array —
1345// each slot is overwritten exactly once, so no ownership duplication occurs.
1346// All members of WiFiScanResult are either trivially copyable (bssid, channel,
1347// rssi, priority, flags) or CompactString, which stores either inline data or
1348// a heap pointer — never a self-referential pointer (unlike std::string's SSO
1349// on some implementations). This was not possible before PR#13472 replaced
1350// std::string with CompactString, since std::string's internal layout is
1351// implementation-defined and may use self-referential pointers.
1352//
1353// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
1354// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
1355template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
1356 // memcpy-based sort requires no self-referential pointers or virtual dispatch.
1357 // These static_asserts guard the assumptions. If any fire, the memcpy sort
1358 // must be reviewed for safety before updating the expected values.
1359 //
1360 // No vtable pointers (memcpy would corrupt vptr)
1361 static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
1362 static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
1363 // Standard layout ensures predictable memory layout with no virtual bases
1364 // and no mixed-access-specifier reordering
1365 static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
1366 static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
1367 // Size checks catch added/removed fields that may need safety review
1368 static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
1369 static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
1370 // Alignment must match for reinterpret_cast of key_buf to be valid
1371 static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
1372 const size_t size = results.size();
1373 constexpr size_t elem_size = sizeof(WiFiScanResult);
1374 // Suppress warnings for intentional memcpy on non-trivially-copyable type.
1375 // Safety is guaranteed by the static_asserts above and the permutation invariant.
1376 // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
1377 auto *memcpy_fn = &memcpy;
1378 for (size_t i = 1; i < size; i++) {
1379 alignas(WiFiScanResult) uint8_t key_buf[elem_size];
1380 memcpy_fn(key_buf, &results[i], elem_size);
1381 const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
1382 int32_t j = i - 1;
1383
1384 // Move elements that are worse than key to the right
1385 // For stability, we only move if key is strictly better than results[j]
1386 while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
1387 memcpy_fn(&results[j + 1], &results[j], elem_size);
1388 j--;
1389 }
1390 memcpy_fn(&results[j + 1], key_buf, elem_size);
1391 }
1392}
1393
1394// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop
1395//
1396// IMPORTANT: This function deliberately uses a SINGLE log call to minimize blocking.
1397// In environments with many matching networks (e.g., 18+ mesh APs), multiple log calls
1398// per network would block the main loop for an unacceptable duration. Each log call
1399// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
1400// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
1401__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
1402 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1403 auto bssid = res.get_bssid();
1404 format_mac_addr_upper(bssid.data(), bssid_s);
1405
1406#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
1407 // Single combined log line with all details when DEBUG enabled
1408 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s Ch:%2u %3ddB P:%d", res.get_ssid().c_str(),
1409 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1410 LOG_STR_ARG(get_signal_bars(res.get_rssi())), res.get_channel(), res.get_rssi(), res.get_priority());
1411#else
1412 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
1413 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1414 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
1415#endif
1416}
1417
1419 if (!this->scan_done_) {
1420 if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
1421 ESP_LOGE(TAG, "Scan timeout");
1422 this->retry_connect();
1423 }
1424 return;
1425 }
1426 this->scan_done_ = false;
1428 true; // Track that we've done a scan since captive portal started
1430
1431 if (this->scan_result_.empty()) {
1432 ESP_LOGW(TAG, "No networks found");
1433 this->retry_connect();
1434 return;
1435 }
1436
1437 ESP_LOGD(TAG, "Found networks:");
1438 for (auto &res : this->scan_result_) {
1439 for (auto &ap : this->sta_) {
1440 if (res.matches(ap)) {
1441 res.set_matches(true);
1442 // Cache priority lookup - do single search instead of 2 separate searches
1443 const bssid_t &bssid = res.get_bssid();
1444 if (!this->has_sta_priority(bssid)) {
1445 this->set_sta_priority(bssid, ap.get_priority());
1446 }
1447 res.set_priority(this->get_sta_priority(bssid));
1448 break;
1449 }
1450 }
1451 }
1452
1453 // Sort scan results using insertion sort for better memory efficiency
1454 insertion_sort_scan_results(this->scan_result_);
1455
1456 // Log matching networks (non-matching already logged at VERBOSE in scan callback)
1457 for (auto &res : this->scan_result_) {
1458 if (res.get_matches()) {
1459 log_scan_result(res);
1460 }
1461 }
1462
1463 // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
1464 // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
1465 // matches that network and record it in selected_sta_index_. This keeps the two indices
1466 // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
1467 const WiFiScanResult &scan_res = this->scan_result_[0];
1468 bool found_match = false;
1469 if (scan_res.get_matches()) {
1470 for (size_t i = 0; i < this->sta_.size(); i++) {
1471 if (scan_res.matches(this->sta_[i])) {
1472 // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
1473 // No overflow check needed - YAML validation prevents >127 networks
1474 this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
1475 found_match = true;
1476 break;
1477 }
1478 }
1479 }
1480
1481 if (!found_match) {
1482 ESP_LOGW(TAG, "No matching network found");
1483 // No scan results matched our configured networks - transition directly to hidden mode
1484 // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
1486 // If no hidden networks to try, skip connection attempt (will be handled on next loop)
1487 if (this->selected_sta_index_ == -1) {
1488 return;
1489 }
1490 // Now start connection attempt in hidden mode
1492 return; // scan started, wait for next loop iteration
1493 }
1494
1495 yield();
1496
1497 WiFiAP params = this->build_params_for_current_phase_();
1498 // Ensure we're in SCAN_CONNECTING phase when connecting with scan results
1499 // (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
1500 this->start_connecting(params);
1501}
1502
1504 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1505 ESP_LOGCONFIG(TAG,
1506 "WiFi:\n"
1507 " Local MAC: %s\n"
1508 " Connected: %s",
1509 get_mac_address_pretty_into_buffer(mac_s), YESNO(this->is_connected()));
1510 if (this->is_disabled()) {
1511 ESP_LOGCONFIG(TAG, " Disabled");
1512 return;
1513 }
1514#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
1515 const char *band_mode_s;
1516 switch (this->band_mode_) {
1517 case WIFI_BAND_MODE_2G_ONLY:
1518 band_mode_s = "2.4GHz";
1519 break;
1520 case WIFI_BAND_MODE_5G_ONLY:
1521 band_mode_s = "5GHz";
1522 break;
1523 case WIFI_BAND_MODE_AUTO:
1524 default:
1525 band_mode_s = "Auto";
1526 break;
1527 }
1528 ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s);
1529#endif
1530 if (this->is_connected()) {
1531 this->print_connect_params_();
1532 }
1533}
1534
1536 auto status = this->wifi_sta_connect_status_();
1537
1539 char ssid_buf[SSID_BUFFER_SIZE];
1540 if (wifi_ssid_to(ssid_buf)[0] == '\0') {
1541 ESP_LOGW(TAG, "Connection incomplete");
1542 this->retry_connect();
1543 return;
1544 }
1545
1546 ESP_LOGI(TAG, "Connected");
1547 // Warn if we had to retry with hidden network mode for a network that's not marked hidden
1548 // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
1549 if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
1550 config && !config->get_hidden() &&
1551 this->scan_result_.empty()) {
1552 ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
1553 }
1554 // Reset to initial phase on successful connection (don't log transition, just reset state)
1556 this->num_retried_ = 0;
1557 if (this->has_ap()) {
1558#ifdef USE_CAPTIVE_PORTAL
1559 if (this->is_captive_portal_active_()) {
1561 }
1562#endif
1563 ESP_LOGD(TAG, "Disabling AP");
1564 this->wifi_mode_({}, false);
1565 }
1566#ifdef USE_IMPROV
1567 if (this->is_esp32_improv_active_()) {
1569 }
1570#endif
1571
1573 this->num_retried_ = 0;
1574 this->print_connect_params_();
1575
1576 // Reset roaming state on successful connection
1577 this->roaming_last_check_ = now;
1578 // Only preserve attempts if reconnecting after a failed roam attempt
1579 // This prevents ping-pong between APs when a roam target is unreachable
1581 // Successful roam to better AP on first try - reset attempts so we can roam again later
1582 ESP_LOGD(TAG, "Roam successful");
1583 this->roaming_attempts_ = 0;
1584 } else if (this->roaming_state_ == RoamingState::RECONNECTING) {
1585 // Check if we ended up on the roam target despite needing a retry
1586 // (e.g., first connect failed but scan-based retry found and connected to the same better AP)
1587 bssid_t current_bssid = this->wifi_bssid();
1588 if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) {
1589 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1590 format_mac_addr_upper(current_bssid.data(), bssid_buf);
1591 ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS,
1592 bssid_buf);
1593 this->roaming_attempts_ = 0;
1594 } else if (this->roaming_target_bssid_ != bssid_t{}) {
1595 // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong
1596 ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1597 } else {
1598 // Reconnected after scan-induced disconnect (no roam target) - keep attempts
1599 ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1600 }
1601 } else {
1602 // Normal connection (boot, credentials changed, etc.)
1603 this->roaming_attempts_ = 0;
1604 }
1606 this->roaming_target_bssid_ = {};
1607 this->roaming_scan_end_ = 0;
1608
1609 // Clear all priority penalties - the next reconnect will happen when an AP disconnects,
1610 // which means the landscape has likely changed and previous tracked failures are stale
1612
1613#ifdef USE_WIFI_FAST_CONNECT
1615#endif
1616
1617 this->release_scan_results_();
1618
1619#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1620 // Notify listeners now that state machine has reached STA_CONNECTED
1621 // This ensures wifi.connected condition returns true in listener automations
1623#endif
1624
1625#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
1626 // On ESP8266, GOT_IP event may not fire for static IP configurations,
1627 // so notify IP state listeners here as a fallback.
1628 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
1630 }
1631#endif
1632
1633 return;
1634 }
1635
1636 if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
1637 ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
1638 this->wifi_disconnect_();
1639 this->retry_connect();
1640 return;
1641 }
1642
1643 if (this->error_from_callback_) {
1644 // ESP8266: logging done in callback, listeners deferred via pending_.disconnect
1645 // Other platforms: just log generic failure message
1646#ifndef USE_ESP8266
1647 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1648#endif
1649 this->retry_connect();
1650 return;
1651 }
1652
1654 return;
1655 }
1656
1658 ESP_LOGW(TAG, "Network no longer found");
1659 this->retry_connect();
1660 return;
1661 }
1662
1664 ESP_LOGW(TAG, "Connecting to network failed");
1665 this->retry_connect();
1666 return;
1667 }
1668
1669 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1670 this->retry_connect();
1671}
1672
1680 switch (this->retry_phase_) {
1682#ifdef USE_WIFI_FAST_CONNECT
1684 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1685 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1686 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1687 }
1688#endif
1689 // Check if we should try explicit hidden networks before scanning
1690 // This handles reconnection after connection loss where first network is hidden
1691 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1693 }
1694 // No more APs to try, fall back to scan
1696
1698 // Try all explicitly hidden networks before scanning
1699 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1700 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1701 }
1702
1703 // Exhausted retries on current SSID - check for more explicitly hidden networks
1704 // Stop when we reach a visible network (proceed to scanning)
1705 size_t next_index = this->selected_sta_index_ + 1;
1706 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1707 // Found another explicitly hidden network
1709 }
1710
1711 // No more consecutive explicitly hidden networks
1712 // If ALL networks are hidden, skip scanning and go directly to restart
1713 if (this->find_first_non_hidden_index_() < 0) {
1715 }
1716 // Otherwise proceed to scanning for non-hidden networks
1718 }
1719
1721 // If scan found no networks or no matching networks, skip to hidden network mode
1722 if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) {
1724 }
1725
1726 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1727 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1728 }
1729
1730 // Exhausted retries on current BSSID (scan_result_[0])
1731 // Its priority has been decreased, so on next scan it will be sorted lower
1732 // and we'll try the next best BSSID.
1733 // Check if there are any potentially hidden networks to try
1734 if (this->find_next_hidden_sta_(-1) >= 0) {
1735 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1736 }
1737 // No hidden networks - always go through RESTARTING_ADAPTER phase
1738 // This ensures num_retried_ gets reset and a fresh scan is triggered
1739 // The actual adapter restart will be skipped if captive portal/improv is active
1741
1743 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1744 if (this->selected_sta_index_ >= 0) {
1745 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1746 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1747 }
1748
1749 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1750 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1751 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1752 // as it might have been seen in the scan results and we want to skip those
1753 // otherwise we will get stuck in RETRY_HIDDEN phase
1754 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1755 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1757 }
1758 }
1759 }
1760 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1761 // This ensures num_retried_ gets reset and a fresh scan is triggered
1762 // The actual adapter restart will be skipped if captive portal/improv is active
1764
1766 // After restart, go back to explicit hidden if we went through it initially
1769 }
1770 // Skip scanning when captive portal/improv is active to avoid disrupting AP,
1771 // BUT only if we've already completed at least one scan AFTER the portal started.
1772 // When captive portal first starts, scan results may be filtered/stale, so we need
1773 // to do one full scan to populate available networks for the captive portal UI.
1774 //
1775 // WHY SCANNING DISRUPTS AP MODE:
1776 // WiFi scanning requires the radio to leave the AP's channel and hop through
1777 // other channels to listen for beacons. During this time (even for passive scans),
1778 // the AP cannot service connected clients - they experience disconnections or
1779 // timeouts. On ESP32, even passive scans cause brief but noticeable disruptions
1780 // that break captive portal HTTP requests and DNS lookups.
1781 //
1782 // BLIND RETRY MODE:
1783 // When captive portal/improv is active, we use RETRY_HIDDEN as a "try all networks
1784 // blindly" mode. Since retry_hidden_mode_ is set to BLIND_RETRY (in RESTARTING_ADAPTER
1785 // transition), find_next_hidden_sta_() will treat ALL configured networks as
1786 // candidates, cycling through them without requiring scan results.
1787 //
1788 // This allows users to configure WiFi via captive portal while the device keeps
1789 // attempting to connect to all configured networks in sequence.
1790 // Captive portal needs scan results to show available networks.
1791 // If captive portal is active, only skip scanning if we've done a scan after it started.
1792 // If only improv is active (no captive portal), skip scanning since improv doesn't need results.
1793 if (this->is_captive_portal_active_()) {
1796 }
1797 // Need to scan for captive portal
1798 } else if (this->is_esp32_improv_active_()) {
1799 // Improv doesn't need scan results
1801 }
1803 }
1804
1805 // Should never reach here
1807}
1808
1819 WiFiRetryPhase old_phase = this->retry_phase_;
1820
1821 // No-op if staying in same phase
1822 if (old_phase == new_phase) {
1823 return false;
1824 }
1825
1826 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1827 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1828
1829 this->retry_phase_ = new_phase;
1830 this->num_retried_ = 0; // Reset retry counter on phase change
1831
1832 // Phase-specific setup
1833 switch (new_phase) {
1834#ifdef USE_WIFI_FAST_CONNECT
1836 // Move to next configured AP - clear old scan data so new AP is tried with config only
1837 this->selected_sta_index_++;
1838 this->scan_result_.clear();
1839 break;
1840#endif
1841
1843 // Starting explicit hidden phase - reset to first network
1844 this->selected_sta_index_ = 0;
1845 break;
1846
1848 // Transitioning to scan-based connection
1849#ifdef USE_WIFI_FAST_CONNECT
1851 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1852 }
1853#endif
1854 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1855 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1857 this->selected_sta_index_ = -1; // Will be set after scan completes
1858 this->start_scanning();
1859 return true; // Started scan, wait for completion
1860 }
1861 // Already have scan results - selected_sta_index_ should already be synchronized
1862 // (set in check_scanning_finished() when scan completed)
1863 // No need to reset it here
1864 break;
1865
1867 // Always reset to first candidate when entering this phase.
1868 // This phase can be entered from:
1869 // - SCAN_CONNECTING: normal flow, find_next_hidden_sta_() skips networks visible in scan
1870 // - RESTARTING_ADAPTER: captive portal active, find_next_hidden_sta_() tries ALL networks
1871 //
1872 // The retry_hidden_mode_ controls the behavior:
1873 // - SCAN_BASED: scan_result_ is checked, visible networks are skipped
1874 // - BLIND_RETRY: scan_result_ is ignored, all networks become candidates
1875 // We don't clear scan_result_ here - the mode controls whether it's consulted.
1877
1878 if (this->selected_sta_index_ == -1) {
1879 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1880 }
1881 break;
1882
1884 // Skip actual adapter restart if captive portal/improv is active
1885 // This allows state machine to reset num_retried_ and trigger fresh scan
1886 // without disrupting the captive portal/improv connection
1887 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1888 this->restart_adapter();
1889 } else {
1890 // Even when skipping full restart, disconnect to clear driver state
1891 // Without this, platforms like LibreTiny may think we're still connecting
1892 this->wifi_disconnect_();
1893 }
1894 // Clear scan flag - we're starting a new retry cycle
1895 // This is critical for captive portal/improv flow: when determine_next_phase_()
1896 // returns RETRY_HIDDEN (because scanning is skipped), find_next_hidden_sta_()
1897 // will see BLIND_RETRY mode and treat ALL networks as candidates,
1898 // effectively cycling through all configured networks without scan results.
1900 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1901 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1903 this->action_started_ = millis();
1904 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1905 return true;
1906
1907 default:
1908 break;
1909 }
1910
1911 return false; // Did not start scan, can proceed with connection
1912}
1913
1915 if (!this->sta_priorities_.empty()) {
1916 decltype(this->sta_priorities_)().swap(this->sta_priorities_);
1917 }
1918}
1919
1924 if (this->sta_priorities_.empty()) {
1925 return;
1926 }
1927
1928 int8_t first_priority = this->sta_priorities_[0].priority;
1929
1930 // Only clear if all priorities have been decremented to the minimum value
1931 // At this point, all BSSIDs have been equally penalized and priority info is useless
1932 if (first_priority != std::numeric_limits<int8_t>::min()) {
1933 return;
1934 }
1935
1936 for (const auto &pri : this->sta_priorities_) {
1937 if (pri.priority != first_priority) {
1938 return; // Not all same, nothing to do
1939 }
1940 }
1941
1942 // All priorities are at minimum - clear the vector to save memory and reset
1943 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1945}
1946
1966 // Determine which BSSID we tried to connect to
1967 optional<bssid_t> failed_bssid;
1968
1969 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1970 // Scan-based phase: always use best result (index 0)
1971 failed_bssid = this->scan_result_[0].get_bssid();
1972 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1973 // Config has specific BSSID (fast_connect or user-specified)
1974 failed_bssid = config->get_bssid();
1975 }
1976
1977 if (!failed_bssid.has_value()) {
1978 return; // No BSSID to penalize
1979 }
1980
1981 // Get SSID for logging (use pointer to avoid copy)
1982 const char *ssid = nullptr;
1983 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1984 ssid = this->scan_result_[0].ssid_.c_str();
1985 } else if (const WiFiAP *config = this->get_selected_sta_()) {
1986 ssid = config->ssid_.c_str();
1987 }
1988
1989 // Only decrease priority on the last attempt for this phase
1990 // This prevents false positives from transient WiFi stack issues
1991 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
1992 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
1993
1994 // Decrease priority only on last attempt to avoid false positives from transient failures
1995 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
1996 int8_t new_priority = old_priority;
1997
1998 if (is_last_attempt) {
1999 // Decrease priority, but clamp to int8_t::min to prevent overflow
2000 new_priority =
2001 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
2002 this->set_sta_priority(failed_bssid.value(), new_priority);
2003 }
2004 char bssid_s[18];
2005 format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
2006 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
2007 bssid_s, old_priority, new_priority);
2008
2009 // After adjusting priority, check if all priorities are now at minimum
2010 // If so, clear the vector to save memory and reset for fresh start
2012}
2013
2025 WiFiRetryPhase current_phase = this->retry_phase_;
2026
2027 // Check if we need to advance to next AP/SSID within the same phase
2028#ifdef USE_WIFI_FAST_CONNECT
2029 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
2030 // Fast connect: always advance to next AP (no retries per AP)
2031 this->selected_sta_index_++;
2032 this->num_retried_ = 0;
2033 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2034 return;
2035 }
2036#endif
2037
2038 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2039 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
2040 // Stop when we reach a visible network (proceed to scanning)
2041 size_t next_index = this->selected_sta_index_ + 1;
2042 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
2043 this->selected_sta_index_ = static_cast<int8_t>(next_index);
2044 this->num_retried_ = 0;
2045 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
2046 return;
2047 }
2048 // No more consecutive explicit hidden networks found - fall through to trigger phase change
2049 }
2050
2051 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2052 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
2053 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
2054 // In that case, skip networks marked hidden:true (already tried)
2055 // Otherwise, include them (they haven't been tried yet)
2056 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
2057 if (next_index != -1) {
2058 // Found another potentially hidden SSID
2059 this->selected_sta_index_ = next_index;
2060 this->num_retried_ = 0;
2061 return;
2062 }
2063 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
2064 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
2065 this->selected_sta_index_ = -1;
2066 // Return early - phase change will happen on next wifi_loop() iteration
2067 return;
2068 }
2069
2070 // Don't increment retry counter if we're in a scan phase with no valid targets
2071 if (this->needs_scan_results_()) {
2072 return;
2073 }
2074
2075 // Increment retry counter to try the same target again
2076 this->num_retried_++;
2077 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
2078 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2079}
2080
2082 // Handle roaming state transitions - preserve attempts counter to prevent ping-pong
2083 // to unreachable APs after ROAMING_MAX_ATTEMPTS failures
2085 // Roam connection failed - transition to reconnecting
2086 ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2088 } else if (this->roaming_state_ == RoamingState::SCANNING) {
2089 // Disconnected during roam scan - transition to RECONNECTING so the attempts
2090 // counter is preserved when reconnection succeeds (IDLE would reset it)
2091 ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2093 } else if (this->roaming_state_ == RoamingState::IDLE) {
2094 // Check if a roaming scan recently completed - on ESP8266, going off-channel
2095 // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes.
2096 // Transition to RECONNECTING so the attempts counter is preserved on reconnect.
2098 ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2100 } else {
2101 // Not a roaming-triggered reconnect, reset state
2102 this->clear_roaming_state_();
2103 }
2104 }
2105 // RECONNECTING: keep state and counter, still trying to reconnect
2106
2108
2109 // Determine next retry phase based on current state
2110 WiFiRetryPhase current_phase = this->retry_phase_;
2111 WiFiRetryPhase next_phase = this->determine_next_phase_();
2112
2113 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
2114 if (this->transition_to_phase_(next_phase)) {
2115 return; // Scan started or adapter restarted (which sets its own state)
2116 }
2117
2118 if (next_phase == current_phase) {
2120 }
2121
2122 yield();
2123 // Check if we have a valid target before building params
2124 // After exhausting all networks in a phase, selected_sta_index_ may be -1
2125 // In that case, skip connection and let next wifi_loop() handle phase transition
2126 if (this->selected_sta_index_ >= 0) {
2127 WiFiAP params = this->build_params_for_current_phase_();
2128 this->start_connecting(params);
2129 }
2130}
2131
2132void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
2134 this->power_save_ = power_save;
2135#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2136 this->configured_power_save_ = power_save;
2137#endif
2138}
2139
2140void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
2141
2143#ifdef USE_CAPTIVE_PORTAL
2145#else
2146 return false;
2147#endif
2148}
2150#ifdef USE_IMPROV
2152#else
2153 return false;
2154#endif
2155}
2156
2157#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2159 // Already configured for high performance - request satisfied
2161 return true;
2162 }
2163
2164 // Semaphore initialization failed
2165 if (this->high_performance_semaphore_ == nullptr) {
2166 return false;
2167 }
2168
2169 // Give the semaphore (non-blocking). This increments the count.
2170 return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
2171}
2172
2174 // Already configured for high performance - nothing to release
2176 return true;
2177 }
2178
2179 // Semaphore initialization failed
2180 if (this->high_performance_semaphore_ == nullptr) {
2181 return false;
2182 }
2183
2184 // Take the semaphore (non-blocking). This decrements the count.
2185 return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
2186}
2187#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
2188
2189#ifdef USE_WIFI_FAST_CONNECT
2191 SavedWifiFastConnectSettings fast_connect_save{};
2192
2193 if (this->fast_connect_pref_.load(&fast_connect_save)) {
2194 // Validate saved AP index
2195 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
2196 ESP_LOGW(TAG, "AP index out of bounds");
2197 return false;
2198 }
2199
2200 // Set selected index for future operations (save, retry, etc)
2201 this->selected_sta_index_ = fast_connect_save.ap_index;
2202
2203 // Copy entire config, then override with fast connect data
2204 params = this->sta_[fast_connect_save.ap_index];
2205
2206 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
2207 bssid_t bssid{};
2208 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
2209 params.set_bssid(bssid);
2210 params.set_channel(fast_connect_save.channel);
2211 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
2212 params.set_hidden(false);
2213
2214 ESP_LOGD(TAG, "Loaded fast_connect settings");
2215#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
2216 if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) ||
2217 (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) {
2218 ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel);
2219 this->selected_sta_index_ = -1;
2220 return false;
2221 }
2222#endif
2223 return true;
2224 }
2225
2226 return false;
2227}
2228
2230 bssid_t bssid = wifi_bssid();
2231 uint8_t channel = get_wifi_channel();
2232 // selected_sta_index_ is always valid here (called only after successful connection)
2233 // Fallback to 0 is defensive programming for robustness
2234 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
2235
2236 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
2237 SavedWifiFastConnectSettings previous_save{};
2238 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
2239 previous_save.channel == channel && previous_save.ap_index == ap_index) {
2240 return; // No change, nothing to save
2241 }
2242
2243 SavedWifiFastConnectSettings fast_connect_save{};
2244 memcpy(fast_connect_save.bssid, bssid.data(), 6);
2245 fast_connect_save.channel = channel;
2246 fast_connect_save.ap_index = ap_index;
2247
2248 this->fast_connect_pref_.save(&fast_connect_save);
2249
2250 ESP_LOGD(TAG, "Saved fast_connect settings");
2251}
2252#endif
2253
2254void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
2255void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
2256void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
2257void WiFiAP::clear_bssid() { this->bssid_ = {}; }
2258void WiFiAP::set_password(const std::string &password) {
2259 this->password_ = CompactString(password.c_str(), password.size());
2260}
2261void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
2262#ifdef USE_WIFI_WPA2_EAP
2263void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
2264#endif
2265void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
2266void WiFiAP::clear_channel() { this->channel_ = 0; }
2267#ifdef USE_WIFI_MANUAL_IP
2268void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
2269#endif
2270void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
2271const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
2272bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
2273#ifdef USE_WIFI_WPA2_EAP
2274const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
2275#endif
2276#ifdef USE_WIFI_MANUAL_IP
2277const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
2278#endif
2279bool WiFiAP::get_hidden() const { return this->hidden_; }
2280
2281WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
2282 bool with_auth, bool is_hidden)
2283 : bssid_(bssid),
2284 channel_(channel),
2285 rssi_(rssi),
2286 ssid_(ssid, ssid_len),
2287 with_auth_(with_auth),
2288 is_hidden_(is_hidden) {}
2289bool WiFiScanResult::matches(const WiFiAP &config) const {
2290 if (config.get_hidden()) {
2291 // User configured a hidden network, only match actually hidden networks
2292 // don't match SSID
2293 if (!this->is_hidden_)
2294 return false;
2295 } else if (!config.ssid_.empty()) {
2296 // check if SSID matches
2297 if (this->ssid_ != config.ssid_)
2298 return false;
2299 } else {
2300 // network is configured without SSID - match other settings
2301 }
2302 // If BSSID configured, only match for correct BSSIDs
2303 if (config.has_bssid() && config.get_bssid() != this->bssid_)
2304 return false;
2305
2306#ifdef USE_WIFI_WPA2_EAP
2307 // BSSID requires auth but no PSK or EAP credentials given
2308 if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
2309 return false;
2310
2311 // BSSID does not require auth, but PSK or EAP credentials given
2312 if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
2313 return false;
2314#else
2315 // If PSK given, only match for networks with auth (and vice versa)
2316 if (config.password_.empty() == this->with_auth_)
2317 return false;
2318#endif
2319
2320 // If channel configured, only match networks on that channel.
2321 if (config.has_channel() && config.get_channel() != this->channel_) {
2322 return false;
2323 }
2324 return true;
2325}
2326bool WiFiScanResult::get_matches() const { return this->matches_; }
2327void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
2328const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
2329uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
2330int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
2331bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
2332bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
2333
2334bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
2335
2337 this->roaming_attempts_ = 0;
2338 this->roaming_last_check_ = 0;
2339 this->roaming_scan_end_ = 0;
2340 this->roaming_target_bssid_ = {};
2342}
2343
2345 if (!this->keep_scan_results_) {
2346#if defined(USE_RP2040) || defined(USE_ESP32)
2347 // std::vector - use swap trick since shrink_to_fit is non-binding
2348 decltype(this->scan_result_)().swap(this->scan_result_);
2349#else
2350 // FixedVector::release() frees all memory
2351 this->scan_result_.release();
2352#endif
2353 }
2354}
2355
2356#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
2358 if (!this->pending_.connect_state)
2359 return;
2360 this->pending_.connect_state = false;
2361 // Get current SSID and BSSID from the WiFi driver
2362 char ssid_buf[SSID_BUFFER_SIZE];
2363 const char *ssid = this->wifi_ssid_to(ssid_buf);
2364 bssid_t bssid = this->wifi_bssid();
2365 for (auto *listener : this->connect_state_listeners_) {
2366 listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
2367 }
2368}
2369
2371 constexpr uint8_t empty_bssid[6] = {};
2372 for (auto *listener : this->connect_state_listeners_) {
2373 listener->on_wifi_connect_state(StringRef(), empty_bssid);
2374 }
2375}
2376#endif // USE_WIFI_CONNECT_STATE_LISTENERS
2377
2378#ifdef USE_WIFI_IP_STATE_LISTENERS
2380 for (auto *listener : this->ip_state_listeners_) {
2381 listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
2382 }
2383}
2384#endif // USE_WIFI_IP_STATE_LISTENERS
2385
2386#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
2388 for (auto *listener : this->scan_results_listeners_) {
2389 listener->on_wifi_scan_results(this->scan_result_);
2390 }
2391}
2392#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
2393
2395 // Guard: not for hidden networks (may not appear in scan)
2396 const WiFiAP *selected = this->get_selected_sta_();
2397 if (selected == nullptr || selected->get_hidden()) {
2398 this->roaming_attempts_ = ROAMING_MAX_ATTEMPTS; // Stop checking forever
2399 return;
2400 }
2401
2402 this->roaming_last_check_ = now;
2403 this->roaming_attempts_++;
2404
2405 // Guard: skip scan if signal is already good (no meaningful improvement possible)
2406 int8_t rssi = this->wifi_rssi();
2407 if (rssi > ROAMING_GOOD_RSSI) {
2408 ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_,
2410 return;
2411 }
2412
2413 ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2415 this->wifi_scan_start_(this->passive_scan_);
2416}
2417
2419 this->scan_done_ = false;
2420 // Default to IDLE - will be set to CONNECTING if we find a better AP
2422 // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout)
2423 // can be attributed to the scan and avoid resetting the attempts counter
2424 this->roaming_scan_end_ = millis();
2425
2426 // Get current connection info
2427 int8_t current_rssi = this->wifi_rssi();
2428 // Guard: must still be connected (RSSI may have become invalid during scan)
2429 if (current_rssi == WIFI_RSSI_DISCONNECTED) {
2430 this->release_scan_results_();
2431 return;
2432 }
2433
2434 char ssid_buf[SSID_BUFFER_SIZE];
2435 StringRef current_ssid(this->wifi_ssid_to(ssid_buf));
2436 bssid_t current_bssid = this->wifi_bssid();
2437
2438 // Find best candidate: same SSID, different BSSID
2439 const WiFiScanResult *best = nullptr;
2440 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
2441
2442 for (const auto &result : this->scan_result_) {
2443 // Must be same SSID, different BSSID
2444 if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
2445 continue;
2446
2447#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
2448 format_mac_addr_upper(result.get_bssid().data(), bssid_buf);
2449 ESP_LOGV(TAG, "Roam candidate %s %d dBm", bssid_buf, result.get_rssi());
2450#endif
2451
2452 // Track the best candidate
2453 if (best == nullptr || result.get_rssi() > best->get_rssi()) {
2454 best = &result;
2455 }
2456 }
2457
2458 // Check if best candidate meets minimum improvement threshold
2459 const WiFiAP *selected = this->get_selected_sta_();
2460 int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi;
2461 if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) {
2462 ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT,
2464 this->release_scan_results_();
2465 return;
2466 }
2467
2468 format_mac_addr_upper(best->get_bssid().data(), bssid_buf);
2469 ESP_LOGI(TAG, "Roaming to %s (%+d dB)", bssid_buf, improvement);
2470
2471 WiFiAP roam_params = *selected;
2472 apply_scan_result_to_params(roam_params, *best);
2473
2474 // Mark as roaming attempt - affects retry behavior if connection fails
2476 this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results
2477
2478 this->release_scan_results_();
2479
2480 // Connect directly - wifi_sta_connect_ handles disconnect internally
2481 this->start_connecting(roam_params);
2482}
2483
2484WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
2485
2486} // namespace esphome::wifi
2487#endif
uint8_t m
Definition bl0906.h:1
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
bool is_name_add_mac_suffix_enabled() const
uint32_t get_config_version_hash()
Get the config hash extended with ESPHome version.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_clear_warning()
Definition component.h:306
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr size_type size() const
Definition string_ref.h:74
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:482
20-byte string: 18 chars inline + null, heap for longer.
const char * data() const
CompactString & operator=(const CompactString &other)
bool operator==(const CompactString &other) const
static constexpr uint8_t INLINE_CAPACITY
const char * c_str() const
char storage_[INLINE_CAPACITY+1]
static constexpr uint8_t MAX_LENGTH
uint8_t get_channel() const
void set_ssid(const std::string &ssid)
const optional< EAPAuth > & get_eap() const
void set_bssid(const bssid_t &bssid)
void set_channel(uint8_t channel)
optional< EAPAuth > eap_
optional< ManualIP > manual_ip_
void set_eap(optional< EAPAuth > eap_auth)
void set_password(const std::string &password)
void set_manual_ip(optional< ManualIP > manual_ip)
const optional< ManualIP > & get_manual_ip() const
void set_hidden(bool hidden)
const bssid_t & get_bssid() const
This component is responsible for managing the ESP WiFi interface.
void notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
void add_sta(const WiFiAP &ap)
bool load_fast_connect_settings_(WiFiAP &params)
void set_ap(const WiFiAP &ap)
Setup an Access Point that should be created if no connection to a station can be made.
bool request_high_performance()
Request high-performance mode (no power saving) for improved WiFi latency.
void set_sta(const WiFiAP &ap)
bool has_sta_priority(const bssid_t &bssid)
const WiFiAP * get_selected_sta_() const
WiFiSTAConnectStatus wifi_sta_connect_status_() const
int8_t get_sta_priority(const bssid_t bssid)
void log_and_adjust_priority_for_failed_connect_()
Log failed connection and decrease BSSID priority to avoid repeated attempts.
void save_wifi_sta(const std::string &ssid, const std::string &password)
void notify_connect_state_listeners_()
Notify connect state listeners (called after state machine reaches STA_CONNECTED)
wifi_scan_vector_t< WiFiScanResult > scan_result_
WiFiPowerSaveMode configured_power_save_
struct esphome::wifi::WiFiComponent::@190 pending_
void set_sta_priority(bssid_t bssid, int8_t priority)
StaticVector< WiFiScanResultsListener *, ESPHOME_WIFI_SCAN_RESULTS_LISTENERS > scan_results_listeners_
void loop() override
Reconnect WiFi if required.
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
void start_connecting(const WiFiAP &ap)
void advance_to_next_target_or_increment_retry_()
Advance to next target (AP/SSID) within current phase, or increment retry counter Called when staying...
static constexpr uint32_t ROAMING_CHECK_INTERVAL
SemaphoreHandle_t high_performance_semaphore_
network::IPAddress get_dns_address(int num)
WiFiComponent()
Construct a WiFiComponent.
std::vector< WiFiSTAPriority > sta_priorities_
static constexpr int8_t ROAMING_GOOD_RSSI
void notify_disconnect_state_listeners_()
Notify connect state listeners of disconnection.
StaticVector< WiFiConnectStateListener *, ESPHOME_WIFI_CONNECT_STATE_LISTENERS > connect_state_listeners_
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel)
Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
static constexpr uint8_t ROAMING_MAX_ATTEMPTS
void set_passive_scan(bool passive)
void set_power_save_mode(WiFiPowerSaveMode power_save)
int8_t find_next_hidden_sta_(int8_t start_index)
Find next SSID that wasn't in scan results (might be hidden) Returns index of next potentially hidden...
ESPPreferenceObject fast_connect_pref_
void clear_priorities_if_all_min_()
Clear BSSID priority tracking if all priorities are at minimum (saves memory)
static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD
WiFiRetryPhase determine_next_phase_()
Determine next retry phase based on current state and failure conditions.
network::IPAddress wifi_dns_ip_(int num)
network::IPAddresses get_ip_addresses()
static constexpr int8_t ROAMING_MIN_IMPROVEMENT
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const
Check if network matches any configured network (for scan result filtering) Matches by SSID when conf...
float get_setup_priority() const override
WIFI setup_priority.
StaticVector< WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS > ip_state_listeners_
FixedVector< WiFiAP > sta_
int8_t find_first_non_hidden_index_() const
Find the index of the first non-hidden network Returns where EXPLICIT_HIDDEN phase would have stopped...
void release_scan_results_()
Free scan results memory unless a component needs them.
bool needs_scan_results_() const
Check if we need valid scan results for the current phase but don't have any Returns true if the phas...
bool transition_to_phase_(WiFiRetryPhase new_phase)
Transition to a new retry phase with logging Returns true if a scan was started (caller should wait),...
bool needs_full_scan_results_() const
Check if full scan results are needed (captive portal active, improv, listeners)
static constexpr uint8_t FIRST_5GHZ_CHANNEL
bool release_high_performance()
Release a high-performance mode request.
bool wifi_apply_output_power_(float output_power)
bool went_through_explicit_hidden_phase_() const
Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) Used in RETRY_HIDDEN ...
bool wifi_mode_(optional< bool > sta, optional< bool > ap)
void set_reboot_timeout(uint32_t reboot_timeout)
network::IPAddresses wifi_sta_ip_addresses()
void check_connecting_finished(uint32_t now)
void start_initial_connection_()
Start initial connection - either scan or connect directly to hidden networks.
bool ssid_was_seen_in_scan_(const CompactString &ssid) const
Check if an SSID was seen in the most recent scan results Used to skip hidden mode for SSIDs we know ...
void setup() override
Setup WiFi interface.
void clear_all_bssid_priorities_()
Clear all BSSID priority penalties after successful connection (stale after disconnect)
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden)
const bssid_t & get_bssid() const
bool matches(const WiFiAP &config) const
bool operator==(const WiFiScanResult &rhs) const
struct @65::@66 __attribute__
uint16_t type
uint8_t priority
CaptivePortal * global_captive_portal
ESP32ImprovComponent * global_improv_component
ImprovSerialComponent * global_improv_serial_component
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:187
constexpr float WIFI
Definition component.h:47
const char *const TAG
Definition spi.cpp:7
std::array< uint8_t, 6 > bssid_t
const LogString * get_signal_bars(int8_t rssi)
@ BLIND_RETRY
Blind retry mode: scanning disabled (captive portal/improv active), try ALL configured networks seque...
@ SCAN_BASED
Normal mode: scan completed, only try networks NOT visible in scan results (truly hidden networks tha...
WiFiRetryPhase
Tracks the current retry strategy/phase for WiFi connection attempts.
@ RETRY_HIDDEN
Retry networks not found in scan (might be hidden)
@ RESTARTING_ADAPTER
Restarting WiFi adapter to clear stuck state.
@ INITIAL_CONNECT
Initial connection attempt (varies based on fast_connect setting)
@ EXPLICIT_HIDDEN
Explicitly hidden networks (user marked as hidden, try before scanning)
@ FAST_CONNECT_CYCLING_APS
Fast connect mode: cycling through configured APs (config-only, no scan)
@ SCAN_CONNECTING
Scan-based: connecting to best AP from scan results.
WiFiComponent * global_wifi_component
@ SCANNING
Scanning for better AP.
@ CONNECTING
Attempting to connect to better AP found in scan.
@ IDLE
Not roaming, waiting for next check interval.
@ RECONNECTING
Roam connection failed, reconnecting to any available AP.
@ WIFI_COMPONENT_STATE_DISABLED
WiFi is disabled.
@ WIFI_COMPONENT_STATE_AP
WiFi is in AP-only mode and internal AP is already enabled.
@ WIFI_COMPONENT_STATE_STA_CONNECTING
WiFi is in STA(+AP) mode and currently connecting to an AP.
@ WIFI_COMPONENT_STATE_OFF
Nothing has been initialized yet.
@ WIFI_COMPONENT_STATE_STA_SCANNING
WiFi is in STA-only mode and currently scanning for APs.
@ WIFI_COMPONENT_STATE_COOLDOWN
WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of ti...
@ WIFI_COMPONENT_STATE_STA_CONNECTED
WiFi is in STA(+AP) mode and successfully connected.
uint16_t uint16_t size_t elem_size
Definition helpers.cpp:26
std::string size_t len
Definition helpers.h:1045
uint16_t size
Definition helpers.cpp:25
ESPPreferences * global_preferences
void HOT yield()
Definition core.cpp:25
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:856
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1420
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:32
esp_eap_ttls_phase2_types ttls_phase_2
Struct for setting static IPs in WiFiComponent.