ESPHome 2026.3.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_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
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
303
304// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
305static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
307 return LOG_STR("INITIAL_CONNECT");
308#ifdef USE_WIFI_FAST_CONNECT
310 return LOG_STR("FAST_CONNECT_CYCLING");
311#endif
313 return LOG_STR("EXPLICIT_HIDDEN");
315 return LOG_STR("SCAN_CONNECTING");
316 if (phase == WiFiRetryPhase::RETRY_HIDDEN)
317 return LOG_STR("RETRY_HIDDEN");
319 return LOG_STR("RESTARTING");
320 return LOG_STR("UNKNOWN");
321}
322
324 // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
325 // This means those networks were already tried and should be skipped in RETRY_HIDDEN
326 return !this->sta_.empty() && this->sta_[0].get_hidden();
327}
328
330 // Find the first network that is NOT marked hidden:true
331 // This is where EXPLICIT_HIDDEN phase would have stopped
332 for (size_t i = 0; i < this->sta_.size(); i++) {
333 if (!this->sta_[i].get_hidden()) {
334 return static_cast<int8_t>(i);
335 }
336 }
337 return -1; // All networks are hidden
338}
339
340// 2 attempts per BSSID in SCAN_CONNECTING phase
341// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
342// Auth failures are common immediately after scan due to WiFi stack state transitions.
343// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
344// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
345static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
346
347// 1 attempt per SSID in RETRY_HIDDEN phase
348// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
349static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
350
351// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
352// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
353static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
354
357static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
358
362static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
363
367static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
368
377static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
378
379static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
380 switch (phase) {
382#ifdef USE_WIFI_FAST_CONNECT
384#endif
385 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
386 return WIFI_RETRY_COUNT_PER_AP;
388 // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
389 return WIFI_RETRY_COUNT_PER_SSID;
391 // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
392 return WIFI_RETRY_COUNT_PER_BSSID;
394 // Hidden network mode: 1 attempt per SSID
395 return WIFI_RETRY_COUNT_PER_SSID;
396 default:
397 return WIFI_RETRY_COUNT_PER_BSSID;
398 }
399}
400
401static void apply_scan_result_to_params(WiFiAP &params, const WiFiScanResult &scan) {
402 params.set_hidden(false);
403 params.set_ssid(scan.get_ssid());
404 params.set_bssid(scan.get_bssid());
405 params.set_channel(scan.get_channel());
406}
407
409 // Only SCAN_CONNECTING phase needs scan results
411 return false;
412 }
413 // Need scan if we have no results or no matching networks
414 return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
415}
416
418 // Check if this SSID is configured as hidden
419 // If explicitly marked hidden, we should always try hidden mode regardless of scan results
420 for (const auto &conf : this->sta_) {
421 if (conf.ssid_ == ssid && conf.get_hidden()) {
422 return false; // Treat as not seen - force hidden mode attempt
423 }
424 }
425
426 // Otherwise, check if we saw it in scan results
427 for (const auto &scan : this->scan_result_) {
428 if (scan.ssid_ == ssid) {
429 return true;
430 }
431 }
432 return false;
433}
434
436 // Components that require full scan results (for example, scan result listeners)
437 // are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
438 if (this->keep_scan_results_) {
439 return true;
440 }
441
442#ifdef USE_CAPTIVE_PORTAL
443 // Captive portal needs full results when active (showing network list to user)
445 return true;
446 }
447#endif
448
449#ifdef USE_IMPROV_SERIAL
450 // Improv serial needs results during provisioning (before connected)
452 return true;
453 }
454#endif
455
456#ifdef USE_IMPROV
457 // BLE improv also needs results during provisioning
459 return true;
460 }
461#endif
462
463 return false;
464}
465
466bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
467 // Hidden networks in scan results have empty SSIDs - skip them
468 if (ssid[0] == '\0') {
469 return false;
470 }
471 for (const auto &sta : this->sta_) {
472 // Skip hidden network configs (they don't appear in normal scans)
473 if (sta.get_hidden()) {
474 continue;
475 }
476 // For BSSID-only configs (empty SSID), match by BSSID
477 if (sta.ssid_.empty()) {
478 if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
479 return true;
480 }
481 continue;
482 }
483 // Match by SSID
484 if (sta.ssid_ == ssid) {
485 return true;
486 }
487 }
488 return false;
489}
490
492 for (auto &it : this->sta_priorities_) {
493 if (it.bssid == bssid) {
494 it.priority = priority;
495 return;
496 }
497 }
498 this->sta_priorities_.push_back(WiFiSTAPriority{
499 .bssid = bssid,
500 .priority = priority,
501 });
502}
503
504void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
505#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
506 // Skip logging during roaming scans to avoid log buffer overflow
507 // (roaming scans typically find many networks but only care about same-SSID APs)
509 return;
510 }
511 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
512 format_mac_addr_upper(bssid, bssid_s);
513 ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
514#endif
515}
516
517int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
518 // Find next SSID to try in RETRY_HIDDEN phase.
519 //
520 // This function operates in two modes based on retry_hidden_mode_:
521 //
522 // 1. SCAN_BASED mode:
523 // After SCAN_CONNECTING phase, only returns networks that were NOT visible
524 // in the scan (truly hidden networks that need probe requests).
525 //
526 // 2. BLIND_RETRY mode:
527 // When captive portal/improv is active, scanning is skipped to avoid
528 // disrupting the AP. In this mode, ALL configured networks are returned
529 // as candidates, cycling through them sequentially. This allows the device
530 // to keep trying all networks while users configure WiFi via captive portal.
531 //
532 // Additionally, if EXPLICIT_HIDDEN phase was executed (first network marked hidden:true),
533 // those networks are skipped here since they were already tried.
534 //
535 bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
536 // Start searching from start_index + 1
537 for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
538 const auto &sta = this->sta_[i];
539
540 // Skip networks that were already tried in EXPLICIT_HIDDEN phase
541 // Those are: networks marked hidden:true that appear before the first non-hidden network
542 // If all networks are hidden (first_non_hidden_idx == -1), skip all of them
543 if (!include_explicit_hidden && sta.get_hidden()) {
544 int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
545 if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
546 ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str());
547 continue;
548 }
549 }
550
551 // In BLIND_RETRY mode, treat all networks as candidates
552 // In SCAN_BASED mode, only retry networks that weren't seen in the scan
554 ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
555 return static_cast<int8_t>(i);
556 }
557 ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
558 }
559 // No hidden SSIDs found
560 return -1;
561}
562
564 // If first network (highest priority) is explicitly marked hidden, try it first before scanning
565 // This respects user's priority order when they explicitly configure hidden networks
566 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
567 ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
568 this->selected_sta_index_ = 0;
571 this->start_connecting(params);
572 } else {
573 this->start_scanning();
574 }
575}
576
577#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
578static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
579 switch (type) {
580 case ESP_EAP_TTLS_PHASE2_PAP:
581 return "pap";
582 case ESP_EAP_TTLS_PHASE2_CHAP:
583 return "chap";
584 case ESP_EAP_TTLS_PHASE2_MSCHAP:
585 return "mschap";
586 case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
587 return "mschapv2";
588 case ESP_EAP_TTLS_PHASE2_EAP:
589 return "eap";
590 default:
591 return "unknown";
592 }
593}
594#endif
595
597
599 this->wifi_pre_setup_();
600
601#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
602 // Create semaphore for high-performance mode requests
603 // Start at 0, increment on request, decrement on release
604 this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
605 if (this->high_performance_semaphore_ == nullptr) {
606 ESP_LOGE(TAG, "Failed semaphore");
607 }
608
609 // Store the configured power save mode as baseline
611#endif
612
613 if (this->enable_on_boot_) {
614 this->start();
615 } else {
616#ifdef USE_ESP32
617 esp_netif_init();
618#endif
620 }
621}
622
624 ESP_LOGCONFIG(TAG, "Starting");
625 this->last_connected_ = millis();
626
627 uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL;
628
630#ifdef USE_WIFI_FAST_CONNECT
632#endif
633
634 SavedWifiSettings save{};
635 if (this->pref_.load(&save)) {
636 ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
637
638 WiFiAP sta{};
639 sta.set_ssid(save.ssid);
640 sta.set_password(save.password);
641 this->set_sta(sta);
642 }
643
644 if (this->has_sta()) {
645 this->wifi_sta_pre_setup_();
646 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
647 ESP_LOGV(TAG, "Setting Output Power Option failed");
648 }
649
650#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
651 // Synchronize power_save_ with semaphore state before applying
652 if (this->high_performance_semaphore_ != nullptr) {
653 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
654 if (semaphore_count > 0) {
656 this->is_high_performance_mode_ = true;
657 } else {
659 this->is_high_performance_mode_ = false;
660 }
661 }
662#endif
663 if (!this->wifi_apply_power_save_()) {
664 ESP_LOGV(TAG, "Setting Power Save Option failed");
665 }
666
668#ifdef USE_WIFI_FAST_CONNECT
669 WiFiAP params;
670 bool loaded_fast_connect = this->load_fast_connect_settings_(params);
671 // Fast connect optimization: only use when we have saved BSSID+channel data
672 // Without saved data, try first configured network or use normal flow
673 if (loaded_fast_connect) {
674 ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str());
675 this->start_connecting(params);
676 } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
677 // No saved data, but have configured networks - try first non-hidden network
678 ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
679 this->selected_sta_index_ = 0;
680 params = this->build_params_for_current_phase_();
681 this->start_connecting(params);
682 } else {
683 // No saved data and (no networks OR first is hidden) - use normal flow
685 }
686#else
687 // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
689#endif
690#ifdef USE_WIFI_AP
691 } else if (this->has_ap()) {
692 this->setup_ap_config_();
693 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
694 ESP_LOGV(TAG, "Setting Output Power Option failed");
695 }
696#ifdef USE_CAPTIVE_PORTAL
698 this->wifi_sta_pre_setup_();
699 this->start_scanning();
701 }
702#endif
703#endif // USE_WIFI_AP
704 }
705#ifdef USE_IMPROV
706 if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
707 if (this->wifi_mode_(true, {}))
709 }
710#endif
711 this->wifi_apply_hostname_();
712}
713
715 ESP_LOGW(TAG, "Restarting adapter");
716 this->wifi_mode_(false, {});
717 // Clear error flag here because restart_adapter() enters COOLDOWN state,
718 // and check_connecting_finished() is called after cooldown without going
719 // through start_connecting() first. Without this clear, stale errors would
720 // trigger spurious "failed (callback)" logs. The canonical clear location
721 // is in start_connecting(); this is the only exception to that pattern.
722 this->error_from_callback_ = false;
723}
724
726 this->wifi_loop_();
727 const uint32_t now = App.get_loop_component_start_time();
728
729 if (this->has_sta()) {
730#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
731 if (this->is_connected() != this->handled_connected_state_) {
732#ifdef USE_WIFI_DISCONNECT_TRIGGER
733 if (this->handled_connected_state_) {
735 }
736#endif
737#ifdef USE_WIFI_CONNECT_TRIGGER
738 if (!this->handled_connected_state_) {
740 }
741#endif
743 }
744#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER
745
746 switch (this->state_) {
748 this->status_set_warning(LOG_STR("waiting to reconnect"));
749 // Skip cooldown if new credentials were provided while connecting
750 if (this->skip_cooldown_next_cycle_) {
751 this->skip_cooldown_next_cycle_ = false;
752 this->check_connecting_finished(now);
753 break;
754 }
755 // Use longer cooldown when captive portal/improv is active to avoid disrupting user config
756 bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
757 uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
758 if (now - this->action_started_ > cooldown_duration) {
759 // After cooldown we either restarted the adapter because of
760 // a failure, or something tried to connect over and over
761 // so we entered cooldown. In both cases we call
762 // check_connecting_finished to continue the state machine.
763 this->check_connecting_finished(now);
764 }
765 break;
766 }
768 this->status_set_warning(LOG_STR("scanning for networks"));
770 break;
771 }
773 this->status_set_warning(LOG_STR("associating to network"));
774 this->check_connecting_finished(now);
775 break;
776 }
777
779 if (!this->is_connected()) {
780 ESP_LOGW(TAG, "Connection lost; reconnecting");
782 this->retry_connect();
783 } else {
784 this->status_clear_warning();
785 this->last_connected_ = now;
786
787 // Post-connect roaming: check for better AP
788 if (this->post_connect_roaming_) {
790 if (this->scan_done_) {
791 this->process_roaming_scan_();
792 }
793 // else: scan in progress, wait
796 this->check_roaming_(now);
797 }
798 }
799 }
800 break;
801 }
804 break;
806 return;
807 }
808
809#ifdef USE_WIFI_AP
810 if (this->has_ap() && !this->ap_setup_) {
811 if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
812 ESP_LOGI(TAG, "Starting fallback AP");
813 this->setup_ap_config_();
814#ifdef USE_CAPTIVE_PORTAL
816 // Reset so we force one full scan after captive portal starts
817 // (previous scans were filtered because captive portal wasn't active yet)
820 }
821#endif
822 }
823 }
824#endif // USE_WIFI_AP
825
826#ifdef USE_IMPROV
828 !esp32_improv::global_improv_component->should_start()) {
829 if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
830 if (this->wifi_mode_(true, {}))
832 }
833 }
834
835#endif
836
837 if (!this->has_ap() && this->reboot_timeout_ != 0) {
838 if (now - this->last_connected_ > this->reboot_timeout_) {
839 ESP_LOGE(TAG, "Can't connect; rebooting");
840 App.reboot();
841 }
842 }
843 }
844
845#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
846 // Check if power save mode needs to be updated based on high-performance requests
847 if (this->high_performance_semaphore_ != nullptr) {
848 // Semaphore count directly represents active requests (starts at 0, increments on request)
849 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
850
851 if (semaphore_count > 0 && !this->is_high_performance_mode_) {
852 // Transition to high-performance mode (no power save)
853 ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
854 semaphore_count == 1 ? "request" : "requests");
856 if (this->wifi_apply_power_save_()) {
857 this->is_high_performance_mode_ = true;
858 }
859 } else if (semaphore_count == 0 && this->is_high_performance_mode_) {
860 // Restore to configured power save mode
861 ESP_LOGV(TAG, "Restoring power save mode to configured setting");
863 if (this->wifi_apply_power_save_()) {
864 this->is_high_performance_mode_ = false;
865 }
866 }
867 }
868#endif
869}
870
872
873bool WiFiComponent::has_ap() const { return this->has_ap_; }
874bool WiFiComponent::is_ap_active() const { return this->ap_started_; }
875bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
876#ifdef USE_WIFI_11KV_SUPPORT
877void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
878void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
879#endif
881 if (this->has_sta())
882 return this->wifi_sta_ip_addresses();
883
884#ifdef USE_WIFI_AP
885 if (this->has_ap())
886 return {this->wifi_soft_ap_ip()};
887#endif // USE_WIFI_AP
888
889 return {};
890}
892 if (this->has_sta())
893 return this->wifi_dns_ip_(num);
894 return {};
895}
896// set_use_address() is guaranteed to be called during component setup by Python code generation,
897// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
898const char *WiFiComponent::get_use_address() const { return this->use_address_; }
899void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
900
901#ifdef USE_WIFI_AP
903 this->wifi_mode_({}, true);
904
905 if (this->ap_setup_)
906 return;
907
908 if (this->ap_.ssid_.empty()) {
909 // Build AP SSID from app name without heap allocation
910 // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
911 static constexpr size_t AP_SSID_MAX_LEN = 32;
912 static constexpr size_t AP_SSID_PREFIX_LEN = 25;
913 static constexpr size_t AP_SSID_SUFFIX_LEN = 7;
914
915 const std::string &app_name = App.get_name();
916 const char *name_ptr = app_name.c_str();
917 size_t name_len = app_name.length();
918
919 if (name_len <= AP_SSID_MAX_LEN) {
920 // Name fits, use directly
921 this->ap_.set_ssid(name_ptr);
922 } else {
923 // Name too long, need to truncate into stack buffer
924 char ssid_buf[AP_SSID_MAX_LEN + 1];
926 // Keep first 25 chars and last 7 chars (MAC suffix), remove middle
927 memcpy(ssid_buf, name_ptr, AP_SSID_PREFIX_LEN);
928 memcpy(ssid_buf + AP_SSID_PREFIX_LEN, name_ptr + name_len - AP_SSID_SUFFIX_LEN, AP_SSID_SUFFIX_LEN);
929 } else {
930 memcpy(ssid_buf, name_ptr, AP_SSID_MAX_LEN);
931 }
932 ssid_buf[AP_SSID_MAX_LEN] = '\0';
933 this->ap_.set_ssid(ssid_buf);
934 }
935 }
936 this->ap_setup_ = this->wifi_start_ap_(this->ap_);
937
938 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
939 ESP_LOGCONFIG(TAG,
940 "Setting up AP:\n"
941 " AP SSID: '%s'\n"
942 " AP Password: '%s'\n"
943 " IP Address: %s",
944 this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
945
946#ifdef USE_WIFI_MANUAL_IP
947 auto manual_ip = this->ap_.get_manual_ip();
948 if (manual_ip.has_value()) {
949 char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
950 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
951 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
952 ESP_LOGCONFIG(TAG,
953 " AP Static IP: '%s'\n"
954 " AP Gateway: '%s'\n"
955 " AP Subnet: '%s'",
956 manual_ip->static_ip.str_to(static_ip_buf), manual_ip->gateway.str_to(gateway_buf),
957 manual_ip->subnet.str_to(subnet_buf));
958 }
959#endif
960
961 if (!this->has_sta()) {
963 }
964}
965
967 this->ap_ = ap;
968 this->has_ap_ = true;
969}
970#endif // USE_WIFI_AP
971
972#ifdef USE_LOOP_PRIORITY
974 return 10.0f; // before other loop components
975}
976#endif
977
978void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
979void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
981 // Clear roaming state - no more configured networks
982 this->clear_roaming_state_();
983 this->sta_.clear();
984 this->selected_sta_index_ = -1;
985}
987 this->clear_sta(); // Also clears roaming state
988 this->init_sta(1);
989 this->add_sta(ap);
990 this->selected_sta_index_ = 0;
991 // When new credentials are set (e.g., from improv), skip cooldown to retry immediately
992 this->skip_cooldown_next_cycle_ = true;
993}
994
996 const WiFiAP *config = this->get_selected_sta_();
997 if (config == nullptr) {
998 ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
999 static_cast<int>(this->selected_sta_index_), this->sta_.size());
1000 // Return empty params - caller should handle this gracefully
1001 return WiFiAP();
1002 }
1003
1004 WiFiAP params = *config;
1005
1006 switch (this->retry_phase_) {
1008#ifdef USE_WIFI_FAST_CONNECT
1010#endif
1011 // Fast connect phases: use config-only (no scan results)
1012 // BSSID/channel from config if user specified them, otherwise empty
1013 break;
1014
1017 // Hidden network mode: clear BSSID/channel to trigger probe request
1018 // (both explicit hidden and retry hidden use same behavior)
1019 params.clear_bssid();
1020 params.clear_channel();
1021 break;
1022
1024 // Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
1025 if (!this->scan_result_.empty()) {
1026 apply_scan_result_to_params(params, this->scan_result_[0]);
1027 }
1028 break;
1029
1031 // Should not be building params during restart
1032 break;
1033 }
1034
1035 return params;
1036}
1037
1039 const WiFiAP *config = this->get_selected_sta_();
1040 return config ? *config : WiFiAP{};
1041}
1042void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
1043 this->save_wifi_sta(ssid.c_str(), password.c_str());
1044}
1045void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
1046 SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
1047 strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
1048 strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
1049 this->pref_.save(&save);
1050 // ensure it's written immediately
1052
1053 WiFiAP sta{};
1054 sta.set_ssid(ssid);
1055 sta.set_password(password);
1056 this->set_sta(sta);
1057
1058 // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
1059 this->connect_soon_();
1060}
1061
1063 // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
1065 ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
1066 this->retry_connect();
1067 }
1068}
1069
1071 // Log connection attempt at INFO level with priority
1072 char bssid_s[18];
1073 int8_t priority = 0;
1074
1075 if (ap.has_bssid()) {
1076 format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
1077 priority = this->get_sta_priority(ap.get_bssid());
1078 }
1079
1080 ESP_LOGI(TAG,
1081 "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
1082 ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
1083 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1084
1085#ifdef ESPHOME_LOG_HAS_VERBOSE
1086 ESP_LOGV(TAG,
1087 "Connection Params:\n"
1088 " SSID: '%s'",
1089 ap.ssid_.c_str());
1090 if (ap.has_bssid()) {
1091 ESP_LOGV(TAG, " BSSID: %s", bssid_s);
1092 } else {
1093 ESP_LOGV(TAG, " BSSID: Not Set");
1094 }
1095
1096#ifdef USE_WIFI_WPA2_EAP
1097 if (ap.get_eap().has_value()) {
1098 EAPAuth eap_config = ap.get_eap().value();
1099 // clang-format off
1100 ESP_LOGV(
1101 TAG,
1102 " WPA2 Enterprise authentication configured:\n"
1103 " Identity: " LOG_SECRET("'%s'") "\n"
1104 " Username: " LOG_SECRET("'%s'") "\n"
1105 " Password: " LOG_SECRET("'%s'"),
1106 eap_config.identity.c_str(), eap_config.username.c_str(), eap_config.password.c_str());
1107 // clang-format on
1108#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1109 ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
1110#endif
1111 bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
1112 bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
1113 bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
1114 ESP_LOGV(TAG,
1115 " CA Cert: %s\n"
1116 " Client Cert: %s\n"
1117 " Client Key: %s",
1118 ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present",
1119 client_key_present ? "present" : "not present");
1120 } else {
1121#endif
1122 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
1123#ifdef USE_WIFI_WPA2_EAP
1124 }
1125#endif
1126 if (ap.has_channel()) {
1127 ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
1128 } else {
1129 ESP_LOGV(TAG, " Channel not set");
1130 }
1131#ifdef USE_WIFI_MANUAL_IP
1132 if (ap.get_manual_ip().has_value()) {
1133 ManualIP m = *ap.get_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 - 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 // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong
1588 ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1589 } else {
1590 // Normal connection (boot, credentials changed, etc.)
1591 this->roaming_attempts_ = 0;
1592 }
1594
1595 // Clear all priority penalties - the next reconnect will happen when an AP disconnects,
1596 // which means the landscape has likely changed and previous tracked failures are stale
1598
1599#ifdef USE_WIFI_FAST_CONNECT
1601#endif
1602
1603 this->release_scan_results_();
1604
1605#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1606 // Notify listeners now that state machine has reached STA_CONNECTED
1607 // This ensures wifi.connected condition returns true in listener automations
1609#endif
1610
1611#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
1612 // On ESP8266, GOT_IP event may not fire for static IP configurations,
1613 // so notify IP state listeners here as a fallback.
1614 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
1616 }
1617#endif
1618
1619 return;
1620 }
1621
1622 if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
1623 ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
1624 this->wifi_disconnect_();
1625 this->retry_connect();
1626 return;
1627 }
1628
1629 if (this->error_from_callback_) {
1630 // ESP8266: logging done in callback, listeners deferred via pending_.disconnect
1631 // Other platforms: just log generic failure message
1632#ifndef USE_ESP8266
1633 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1634#endif
1635 this->retry_connect();
1636 return;
1637 }
1638
1640 return;
1641 }
1642
1644 ESP_LOGW(TAG, "Network no longer found");
1645 this->retry_connect();
1646 return;
1647 }
1648
1650 ESP_LOGW(TAG, "Connecting to network failed");
1651 this->retry_connect();
1652 return;
1653 }
1654
1655 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1656 this->retry_connect();
1657}
1658
1666 switch (this->retry_phase_) {
1668#ifdef USE_WIFI_FAST_CONNECT
1670 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1671 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1672 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1673 }
1674#endif
1675 // Check if we should try explicit hidden networks before scanning
1676 // This handles reconnection after connection loss where first network is hidden
1677 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1679 }
1680 // No more APs to try, fall back to scan
1682
1684 // Try all explicitly hidden networks before scanning
1685 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1686 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1687 }
1688
1689 // Exhausted retries on current SSID - check for more explicitly hidden networks
1690 // Stop when we reach a visible network (proceed to scanning)
1691 size_t next_index = this->selected_sta_index_ + 1;
1692 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1693 // Found another explicitly hidden network
1695 }
1696
1697 // No more consecutive explicitly hidden networks
1698 // If ALL networks are hidden, skip scanning and go directly to restart
1699 if (this->find_first_non_hidden_index_() < 0) {
1701 }
1702 // Otherwise proceed to scanning for non-hidden networks
1704 }
1705
1707 // If scan found no networks or no matching networks, skip to hidden network mode
1708 if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) {
1710 }
1711
1712 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1713 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1714 }
1715
1716 // Exhausted retries on current BSSID (scan_result_[0])
1717 // Its priority has been decreased, so on next scan it will be sorted lower
1718 // and we'll try the next best BSSID.
1719 // Check if there are any potentially hidden networks to try
1720 if (this->find_next_hidden_sta_(-1) >= 0) {
1721 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1722 }
1723 // No hidden networks - always go through RESTARTING_ADAPTER phase
1724 // This ensures num_retried_ gets reset and a fresh scan is triggered
1725 // The actual adapter restart will be skipped if captive portal/improv is active
1727
1729 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1730 if (this->selected_sta_index_ >= 0) {
1731 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1732 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1733 }
1734
1735 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1736 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1737 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1738 // as it might have been seen in the scan results and we want to skip those
1739 // otherwise we will get stuck in RETRY_HIDDEN phase
1740 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1741 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1743 }
1744 }
1745 }
1746 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1747 // This ensures num_retried_ gets reset and a fresh scan is triggered
1748 // The actual adapter restart will be skipped if captive portal/improv is active
1750
1752 // After restart, go back to explicit hidden if we went through it initially
1755 }
1756 // Skip scanning when captive portal/improv is active to avoid disrupting AP,
1757 // BUT only if we've already completed at least one scan AFTER the portal started.
1758 // When captive portal first starts, scan results may be filtered/stale, so we need
1759 // to do one full scan to populate available networks for the captive portal UI.
1760 //
1761 // WHY SCANNING DISRUPTS AP MODE:
1762 // WiFi scanning requires the radio to leave the AP's channel and hop through
1763 // other channels to listen for beacons. During this time (even for passive scans),
1764 // the AP cannot service connected clients - they experience disconnections or
1765 // timeouts. On ESP32, even passive scans cause brief but noticeable disruptions
1766 // that break captive portal HTTP requests and DNS lookups.
1767 //
1768 // BLIND RETRY MODE:
1769 // When captive portal/improv is active, we use RETRY_HIDDEN as a "try all networks
1770 // blindly" mode. Since retry_hidden_mode_ is set to BLIND_RETRY (in RESTARTING_ADAPTER
1771 // transition), find_next_hidden_sta_() will treat ALL configured networks as
1772 // candidates, cycling through them without requiring scan results.
1773 //
1774 // This allows users to configure WiFi via captive portal while the device keeps
1775 // attempting to connect to all configured networks in sequence.
1776 // Captive portal needs scan results to show available networks.
1777 // If captive portal is active, only skip scanning if we've done a scan after it started.
1778 // If only improv is active (no captive portal), skip scanning since improv doesn't need results.
1779 if (this->is_captive_portal_active_()) {
1782 }
1783 // Need to scan for captive portal
1784 } else if (this->is_esp32_improv_active_()) {
1785 // Improv doesn't need scan results
1787 }
1789 }
1790
1791 // Should never reach here
1793}
1794
1805 WiFiRetryPhase old_phase = this->retry_phase_;
1806
1807 // No-op if staying in same phase
1808 if (old_phase == new_phase) {
1809 return false;
1810 }
1811
1812 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1813 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1814
1815 this->retry_phase_ = new_phase;
1816 this->num_retried_ = 0; // Reset retry counter on phase change
1817
1818 // Phase-specific setup
1819 switch (new_phase) {
1820#ifdef USE_WIFI_FAST_CONNECT
1822 // Move to next configured AP - clear old scan data so new AP is tried with config only
1823 this->selected_sta_index_++;
1824 this->scan_result_.clear();
1825 break;
1826#endif
1827
1829 // Starting explicit hidden phase - reset to first network
1830 this->selected_sta_index_ = 0;
1831 break;
1832
1834 // Transitioning to scan-based connection
1835#ifdef USE_WIFI_FAST_CONNECT
1837 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1838 }
1839#endif
1840 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1841 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1843 this->selected_sta_index_ = -1; // Will be set after scan completes
1844 this->start_scanning();
1845 return true; // Started scan, wait for completion
1846 }
1847 // Already have scan results - selected_sta_index_ should already be synchronized
1848 // (set in check_scanning_finished() when scan completed)
1849 // No need to reset it here
1850 break;
1851
1853 // Always reset to first candidate when entering this phase.
1854 // This phase can be entered from:
1855 // - SCAN_CONNECTING: normal flow, find_next_hidden_sta_() skips networks visible in scan
1856 // - RESTARTING_ADAPTER: captive portal active, find_next_hidden_sta_() tries ALL networks
1857 //
1858 // The retry_hidden_mode_ controls the behavior:
1859 // - SCAN_BASED: scan_result_ is checked, visible networks are skipped
1860 // - BLIND_RETRY: scan_result_ is ignored, all networks become candidates
1861 // We don't clear scan_result_ here - the mode controls whether it's consulted.
1863
1864 if (this->selected_sta_index_ == -1) {
1865 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1866 }
1867 break;
1868
1870 // Skip actual adapter restart if captive portal/improv is active
1871 // This allows state machine to reset num_retried_ and trigger fresh scan
1872 // without disrupting the captive portal/improv connection
1873 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1874 this->restart_adapter();
1875 } else {
1876 // Even when skipping full restart, disconnect to clear driver state
1877 // Without this, platforms like LibreTiny may think we're still connecting
1878 this->wifi_disconnect_();
1879 }
1880 // Clear scan flag - we're starting a new retry cycle
1881 // This is critical for captive portal/improv flow: when determine_next_phase_()
1882 // returns RETRY_HIDDEN (because scanning is skipped), find_next_hidden_sta_()
1883 // will see BLIND_RETRY mode and treat ALL networks as candidates,
1884 // effectively cycling through all configured networks without scan results.
1886 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1887 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1889 this->action_started_ = millis();
1890 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1891 return true;
1892
1893 default:
1894 break;
1895 }
1896
1897 return false; // Did not start scan, can proceed with connection
1898}
1899
1901 if (!this->sta_priorities_.empty()) {
1902 decltype(this->sta_priorities_)().swap(this->sta_priorities_);
1903 }
1904}
1905
1910 if (this->sta_priorities_.empty()) {
1911 return;
1912 }
1913
1914 int8_t first_priority = this->sta_priorities_[0].priority;
1915
1916 // Only clear if all priorities have been decremented to the minimum value
1917 // At this point, all BSSIDs have been equally penalized and priority info is useless
1918 if (first_priority != std::numeric_limits<int8_t>::min()) {
1919 return;
1920 }
1921
1922 for (const auto &pri : this->sta_priorities_) {
1923 if (pri.priority != first_priority) {
1924 return; // Not all same, nothing to do
1925 }
1926 }
1927
1928 // All priorities are at minimum - clear the vector to save memory and reset
1929 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1931}
1932
1952 // Determine which BSSID we tried to connect to
1953 optional<bssid_t> failed_bssid;
1954
1955 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1956 // Scan-based phase: always use best result (index 0)
1957 failed_bssid = this->scan_result_[0].get_bssid();
1958 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1959 // Config has specific BSSID (fast_connect or user-specified)
1960 failed_bssid = config->get_bssid();
1961 }
1962
1963 if (!failed_bssid.has_value()) {
1964 return; // No BSSID to penalize
1965 }
1966
1967 // Get SSID for logging (use pointer to avoid copy)
1968 const char *ssid = nullptr;
1969 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1970 ssid = this->scan_result_[0].ssid_.c_str();
1971 } else if (const WiFiAP *config = this->get_selected_sta_()) {
1972 ssid = config->ssid_.c_str();
1973 }
1974
1975 // Only decrease priority on the last attempt for this phase
1976 // This prevents false positives from transient WiFi stack issues
1977 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
1978 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
1979
1980 // Decrease priority only on last attempt to avoid false positives from transient failures
1981 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
1982 int8_t new_priority = old_priority;
1983
1984 if (is_last_attempt) {
1985 // Decrease priority, but clamp to int8_t::min to prevent overflow
1986 new_priority =
1987 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
1988 this->set_sta_priority(failed_bssid.value(), new_priority);
1989 }
1990 char bssid_s[18];
1991 format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
1992 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
1993 bssid_s, old_priority, new_priority);
1994
1995 // After adjusting priority, check if all priorities are now at minimum
1996 // If so, clear the vector to save memory and reset for fresh start
1998}
1999
2011 WiFiRetryPhase current_phase = this->retry_phase_;
2012
2013 // Check if we need to advance to next AP/SSID within the same phase
2014#ifdef USE_WIFI_FAST_CONNECT
2015 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
2016 // Fast connect: always advance to next AP (no retries per AP)
2017 this->selected_sta_index_++;
2018 this->num_retried_ = 0;
2019 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2020 return;
2021 }
2022#endif
2023
2024 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2025 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
2026 // Stop when we reach a visible network (proceed to scanning)
2027 size_t next_index = this->selected_sta_index_ + 1;
2028 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
2029 this->selected_sta_index_ = static_cast<int8_t>(next_index);
2030 this->num_retried_ = 0;
2031 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
2032 return;
2033 }
2034 // No more consecutive explicit hidden networks found - fall through to trigger phase change
2035 }
2036
2037 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2038 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
2039 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
2040 // In that case, skip networks marked hidden:true (already tried)
2041 // Otherwise, include them (they haven't been tried yet)
2042 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
2043 if (next_index != -1) {
2044 // Found another potentially hidden SSID
2045 this->selected_sta_index_ = next_index;
2046 this->num_retried_ = 0;
2047 return;
2048 }
2049 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
2050 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
2051 this->selected_sta_index_ = -1;
2052 // Return early - phase change will happen on next wifi_loop() iteration
2053 return;
2054 }
2055
2056 // Don't increment retry counter if we're in a scan phase with no valid targets
2057 if (this->needs_scan_results_()) {
2058 return;
2059 }
2060
2061 // Increment retry counter to try the same target again
2062 this->num_retried_++;
2063 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
2064 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2065}
2066
2068 // Handle roaming state transitions - preserve attempts counter to prevent ping-pong
2069 // to unreachable APs after ROAMING_MAX_ATTEMPTS failures
2071 // Roam connection failed - transition to reconnecting
2072 ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2074 } else if (this->roaming_state_ == RoamingState::SCANNING) {
2075 // Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter
2076 ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2078 } else if (this->roaming_state_ == RoamingState::IDLE) {
2079 // Not a roaming-triggered reconnect, reset state
2080 this->clear_roaming_state_();
2081 }
2082 // RECONNECTING: keep state and counter, still trying to reconnect
2083
2085
2086 // Determine next retry phase based on current state
2087 WiFiRetryPhase current_phase = this->retry_phase_;
2088 WiFiRetryPhase next_phase = this->determine_next_phase_();
2089
2090 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
2091 if (this->transition_to_phase_(next_phase)) {
2092 return; // Scan started or adapter restarted (which sets its own state)
2093 }
2094
2095 if (next_phase == current_phase) {
2097 }
2098
2099 yield();
2100 // Check if we have a valid target before building params
2101 // After exhausting all networks in a phase, selected_sta_index_ may be -1
2102 // In that case, skip connection and let next wifi_loop() handle phase transition
2103 if (this->selected_sta_index_ >= 0) {
2104 WiFiAP params = this->build_params_for_current_phase_();
2105 this->start_connecting(params);
2106 }
2107}
2108
2109#ifdef USE_RP2040
2110// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart
2111// mDNS when the network interface reconnects. However, this callback is disabled
2112// in the arduino-pico framework. As a workaround, we block component setup until
2113// WiFi is connected, ensuring mDNS.begin() is called with an active connection.
2114
2116 if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) {
2117 return true;
2118 }
2119 return this->is_connected();
2120}
2121#endif
2122
2123void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
2129 this->power_save_ = power_save;
2130#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2131 this->configured_power_save_ = power_save;
2132#endif
2133}
2134
2135void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
2136
2138#ifdef USE_CAPTIVE_PORTAL
2140#else
2141 return false;
2142#endif
2143}
2145#ifdef USE_IMPROV
2147#else
2148 return false;
2149#endif
2150}
2151
2152#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2154 // Already configured for high performance - request satisfied
2156 return true;
2157 }
2158
2159 // Semaphore initialization failed
2160 if (this->high_performance_semaphore_ == nullptr) {
2161 return false;
2162 }
2163
2164 // Give the semaphore (non-blocking). This increments the count.
2165 return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
2166}
2167
2169 // Already configured for high performance - nothing to release
2171 return true;
2172 }
2173
2174 // Semaphore initialization failed
2175 if (this->high_performance_semaphore_ == nullptr) {
2176 return false;
2177 }
2178
2179 // Take the semaphore (non-blocking). This decrements the count.
2180 return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
2181}
2182#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
2183
2184#ifdef USE_WIFI_FAST_CONNECT
2186 SavedWifiFastConnectSettings fast_connect_save{};
2187
2188 if (this->fast_connect_pref_.load(&fast_connect_save)) {
2189 // Validate saved AP index
2190 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
2191 ESP_LOGW(TAG, "AP index out of bounds");
2192 return false;
2193 }
2194
2195 // Set selected index for future operations (save, retry, etc)
2196 this->selected_sta_index_ = fast_connect_save.ap_index;
2197
2198 // Copy entire config, then override with fast connect data
2199 params = this->sta_[fast_connect_save.ap_index];
2200
2201 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
2202 bssid_t bssid{};
2203 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
2204 params.set_bssid(bssid);
2205 params.set_channel(fast_connect_save.channel);
2206 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
2207 params.set_hidden(false);
2208
2209 ESP_LOGD(TAG, "Loaded fast_connect settings");
2210 return true;
2211 }
2212
2213 return false;
2214}
2215
2217 bssid_t bssid = wifi_bssid();
2218 uint8_t channel = get_wifi_channel();
2219 // selected_sta_index_ is always valid here (called only after successful connection)
2220 // Fallback to 0 is defensive programming for robustness
2221 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
2222
2223 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
2224 SavedWifiFastConnectSettings previous_save{};
2225 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
2226 previous_save.channel == channel && previous_save.ap_index == ap_index) {
2227 return; // No change, nothing to save
2228 }
2229
2230 SavedWifiFastConnectSettings fast_connect_save{};
2231 memcpy(fast_connect_save.bssid, bssid.data(), 6);
2232 fast_connect_save.channel = channel;
2233 fast_connect_save.ap_index = ap_index;
2234
2235 this->fast_connect_pref_.save(&fast_connect_save);
2236
2237 ESP_LOGD(TAG, "Saved fast_connect settings");
2238}
2239#endif
2240
2241void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
2242void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
2243void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
2244void WiFiAP::clear_bssid() { this->bssid_ = {}; }
2245void WiFiAP::set_password(const std::string &password) {
2246 this->password_ = CompactString(password.c_str(), password.size());
2247}
2248void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
2249#ifdef USE_WIFI_WPA2_EAP
2250void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
2251#endif
2252void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
2253void WiFiAP::clear_channel() { this->channel_ = 0; }
2254#ifdef USE_WIFI_MANUAL_IP
2255void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
2256#endif
2257void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
2258const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
2259bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
2260#ifdef USE_WIFI_WPA2_EAP
2261const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
2262#endif
2263uint8_t WiFiAP::get_channel() const { return this->channel_; }
2264bool WiFiAP::has_channel() const { return this->channel_ != 0; }
2265#ifdef USE_WIFI_MANUAL_IP
2267#endif
2268bool WiFiAP::get_hidden() const { return this->hidden_; }
2269
2270WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
2271 bool with_auth, bool is_hidden)
2272 : bssid_(bssid),
2273 channel_(channel),
2274 rssi_(rssi),
2275 ssid_(ssid, ssid_len),
2276 with_auth_(with_auth),
2277 is_hidden_(is_hidden) {}
2278bool WiFiScanResult::matches(const WiFiAP &config) const {
2279 if (config.get_hidden()) {
2280 // User configured a hidden network, only match actually hidden networks
2281 // don't match SSID
2282 if (!this->is_hidden_)
2283 return false;
2284 } else if (!config.ssid_.empty()) {
2285 // check if SSID matches
2286 if (this->ssid_ != config.ssid_)
2287 return false;
2288 } else {
2289 // network is configured without SSID - match other settings
2290 }
2291 // If BSSID configured, only match for correct BSSIDs
2292 if (config.has_bssid() && config.get_bssid() != this->bssid_)
2293 return false;
2294
2295#ifdef USE_WIFI_WPA2_EAP
2296 // BSSID requires auth but no PSK or EAP credentials given
2297 if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
2298 return false;
2299
2300 // BSSID does not require auth, but PSK or EAP credentials given
2301 if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
2302 return false;
2303#else
2304 // If PSK given, only match for networks with auth (and vice versa)
2305 if (config.password_.empty() == this->with_auth_)
2306 return false;
2307#endif
2308
2309 // If channel configured, only match networks on that channel.
2310 if (config.has_channel() && config.get_channel() != this->channel_) {
2311 return false;
2312 }
2313 return true;
2314}
2315bool WiFiScanResult::get_matches() const { return this->matches_; }
2316void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
2317const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
2318uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
2319int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
2320bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
2321bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
2322
2323bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
2324
2330
2332 if (!this->keep_scan_results_) {
2333#if defined(USE_RP2040) || defined(USE_ESP32)
2334 // std::vector - use swap trick since shrink_to_fit is non-binding
2335 decltype(this->scan_result_)().swap(this->scan_result_);
2336#else
2337 // FixedVector::release() frees all memory
2338 this->scan_result_.release();
2339#endif
2340 }
2341}
2342
2343#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
2345 if (!this->pending_.connect_state)
2346 return;
2347 this->pending_.connect_state = false;
2348 // Get current SSID and BSSID from the WiFi driver
2349 char ssid_buf[SSID_BUFFER_SIZE];
2350 const char *ssid = this->wifi_ssid_to(ssid_buf);
2351 bssid_t bssid = this->wifi_bssid();
2352 for (auto *listener : this->connect_state_listeners_) {
2353 listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
2354 }
2355}
2356
2358 constexpr uint8_t empty_bssid[6] = {};
2359 for (auto *listener : this->connect_state_listeners_) {
2360 listener->on_wifi_connect_state(StringRef(), empty_bssid);
2361 }
2362}
2363#endif // USE_WIFI_CONNECT_STATE_LISTENERS
2364
2365#ifdef USE_WIFI_IP_STATE_LISTENERS
2367 for (auto *listener : this->ip_state_listeners_) {
2368 listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
2369 }
2370}
2371#endif // USE_WIFI_IP_STATE_LISTENERS
2372
2373#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
2375 for (auto *listener : this->scan_results_listeners_) {
2376 listener->on_wifi_scan_results(this->scan_result_);
2377 }
2378}
2379#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
2380
2382 // Guard: not for hidden networks (may not appear in scan)
2383 const WiFiAP *selected = this->get_selected_sta_();
2384 if (selected == nullptr || selected->get_hidden()) {
2385 this->roaming_attempts_ = ROAMING_MAX_ATTEMPTS; // Stop checking forever
2386 return;
2387 }
2388
2389 this->roaming_last_check_ = now;
2390 this->roaming_attempts_++;
2391
2392 // Guard: skip scan if signal is already good (no meaningful improvement possible)
2393 int8_t rssi = this->wifi_rssi();
2394 if (rssi > ROAMING_GOOD_RSSI) {
2395 ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_,
2397 return;
2398 }
2399
2400 ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2402 this->wifi_scan_start_(this->passive_scan_);
2403}
2404
2406 this->scan_done_ = false;
2407 // Default to IDLE - will be set to CONNECTING if we find a better AP
2409
2410 // Get current connection info
2411 int8_t current_rssi = this->wifi_rssi();
2412 // Guard: must still be connected (RSSI may have become invalid during scan)
2413 if (current_rssi == WIFI_RSSI_DISCONNECTED) {
2414 this->release_scan_results_();
2415 return;
2416 }
2417
2418 char ssid_buf[SSID_BUFFER_SIZE];
2419 StringRef current_ssid(this->wifi_ssid_to(ssid_buf));
2420 bssid_t current_bssid = this->wifi_bssid();
2421
2422 // Find best candidate: same SSID, different BSSID
2423 const WiFiScanResult *best = nullptr;
2424 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
2425
2426 for (const auto &result : this->scan_result_) {
2427 // Must be same SSID, different BSSID
2428 if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
2429 continue;
2430
2431#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
2432 format_mac_addr_upper(result.get_bssid().data(), bssid_buf);
2433 ESP_LOGV(TAG, "Roam candidate %s %d dBm", bssid_buf, result.get_rssi());
2434#endif
2435
2436 // Track the best candidate
2437 if (best == nullptr || result.get_rssi() > best->get_rssi()) {
2438 best = &result;
2439 }
2440 }
2441
2442 // Check if best candidate meets minimum improvement threshold
2443 const WiFiAP *selected = this->get_selected_sta_();
2444 int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi;
2445 if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) {
2446 ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT,
2448 this->release_scan_results_();
2449 return;
2450 }
2451
2452 format_mac_addr_upper(best->get_bssid().data(), bssid_buf);
2453 ESP_LOGI(TAG, "Roaming to %s (%+d dB)", bssid_buf, improvement);
2454
2455 WiFiAP roam_params = *selected;
2456 apply_scan_result_to_params(roam_params, *best);
2457 this->release_scan_results_();
2458
2459 // Mark as roaming attempt - affects retry behavior if connection fails
2461
2462 // Connect directly - wifi_sta_connect_ handles disconnect internally
2463 this->start_connecting(roam_params);
2464}
2465
2466WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
2467
2468} // namespace esphome::wifi
2469#endif
uint8_t m
Definition bl0906.h:1
uint8_t status
Definition bl0942.h:8
bool is_name_add_mac_suffix_enabled() const
const std::string & get_name() const
Get the name of this Application set by pre_setup().
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_set_warning(const char *message=nullptr)
void status_clear_warning()
bool save(const T *src)
Definition preferences.h:21
virtual bool sync()=0
Commit pending writes to flash.
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
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)
Inform the parent automation that the event has triggered.
Definition automation.h:325
bool has_value() const
Definition optional.h:92
value_type const & value() const
Definition optional.h:94
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)
struct esphome::wifi::WiFiComponent::@175 pending_
wifi_scan_vector_t< WiFiScanResult > scan_result_
WiFiPowerSaveMode configured_power_save_
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)
WiFiRetryPhase determine_next_phase_()
Determine next retry phase based on current state and failure conditions.
network::IPAddress wifi_dns_ip_(int num)
float get_loop_priority() const override
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)
bool release_high_performance()
Release a high-performance mode request.
bool wifi_apply_output_power_(float output_power)
const char * get_use_address() const
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)
void set_use_address(const char *use_address)
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:36
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.
std::string size_t len
Definition helpers.h:817
size_t size
Definition helpers.h:854
ESPPreferences * global_preferences
void HOT yield()
Definition core.cpp:24
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:817
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
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:1170
esp_eap_ttls_phase2_types ttls_phase_2
Struct for setting static IPs in WiFiComponent.