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