From fc7e9c8a0c1ebabe7b3bd9a63b604a4ce7182c73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 12:20:36 -0600 Subject: [PATCH 1/9] add reverse operators --- esphome/core/helpers.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9bf5b0eeb4c..dfca1e51f0b 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1841,4 +1841,10 @@ class CompactString { static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); +// Reverse comparison overloads so CompactString works on either side of == and != +inline bool operator==(const std::string &lhs, const CompactString &rhs) { return rhs == lhs; } +inline bool operator==(const char *lhs, const CompactString &rhs) { return rhs == lhs; } +inline bool operator!=(const std::string &lhs, const CompactString &rhs) { return !(rhs == lhs); } +inline bool operator!=(const char *lhs, const CompactString &rhs) { return !(rhs == lhs); } + } // namespace esphome From 2506831445c47b4199ff0843e2e7826d008496f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 13:34:18 -0600 Subject: [PATCH 2/9] contain complexity into wifi --- esphome/components/wifi/wifi_component.cpp | 61 ++++++++++++++++++++- esphome/components/wifi/wifi_component.h | 63 ++++++++++++++++++---- esphome/core/helpers.cpp | 57 -------------------- esphome/core/helpers.h | 60 --------------------- 4 files changed, 113 insertions(+), 128 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8f0ea89e82c..6fe97c69a1d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -20,6 +20,7 @@ #endif #include +#include #include #include "lwip/dns.h" #include "lwip/err.h" @@ -47,6 +48,62 @@ namespace esphome::wifi { static const char *const TAG = "wifi"; +// CompactString implementation +CompactString::CompactString(const char *str, size_t len) { + if (len > MAX_LENGTH) { + len = MAX_LENGTH; // Clamp to max valid length + } + + this->length_ = len; + if (len <= INLINE_CAPACITY) { + // Store inline with null terminator + this->is_heap_ = 0; + if (len > 0) { + std::memcpy(this->storage_, str, len); + } + this->storage_[len] = '\0'; + } else { + // Heap allocate with null terminator + this->is_heap_ = 1; + char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(heap_data, str, len); + heap_data[len] = '\0'; + this->set_heap_ptr_(heap_data); + } +} + +CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {} + +CompactString &CompactString::operator=(const CompactString &other) { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(other); + } + return *this; +} + +CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) { + // Copy full storage (includes null terminator for inline, or pointer for heap) + std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1); + other.length_ = 0; + other.is_heap_ = 0; + other.storage_[0] = '\0'; +} + +CompactString &CompactString::operator=(CompactString &&other) noexcept { + if (this != &other) { + this->~CompactString(); + new (this) CompactString(std::move(other)); + } + return *this; +} + +CompactString::~CompactString() { + if (this->is_heap_) { + delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory) + } +} + /// WiFi Retry Logic - Priority-Based BSSID Selection /// /// The WiFi component uses a state machine with priority degradation to handle connection failures @@ -349,7 +406,7 @@ bool WiFiComponent::needs_scan_results_() const { return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); } -bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const { +bool WiFiComponent::ssid_was_seen_in_scan_(StringRef ssid) const { // Check if this SSID is configured as hidden // If explicitly marked hidden, we should always try hidden mode regardless of scan results for (const auto &conf : this->sta_) { @@ -2146,7 +2203,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { return false; } else if (!config.get_ssid().empty()) { // check if SSID matches - if (config.get_ssid() != this->ssid_) + if (config.get_ssid() != this->ssid_.ref()) return false; } else { // network is configured without SSID - match other settings diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4f7982178fd..89621515900 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -172,16 +172,63 @@ template using wifi_scan_vector_t = std::vector; template using wifi_scan_vector_t = FixedVector; #endif +/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. +/// Used internally for WiFi SSID/password storage to reduce heap fragmentation. +class CompactString { + public: + static constexpr uint8_t MAX_LENGTH = 127; + static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes + static constexpr uint8_t BUFFER_SIZE = MAX_LENGTH + 1; // For external buffer (128 bytes) + + CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; } + CompactString(const char *str, size_t len); + CompactString(const CompactString &other); + CompactString(CompactString &&other) noexcept; + CompactString &operator=(const CompactString &other); + CompactString &operator=(CompactString &&other) noexcept; + ~CompactString(); + + const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; } + const char *c_str() const { return this->data(); } // Always null-terminated + size_t size() const { return this->length_; } + bool empty() const { return this->length_ == 0; } + + /// Return a StringRef view of this string (zero-copy) + StringRef ref() const { return StringRef(this->data(), this->size()); } + + bool operator==(const CompactString &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; + } + bool operator!=(const CompactString &other) const { return !(*this == other); } + + protected: + char *get_heap_ptr_() const { + char *ptr; + std::memcpy(&ptr, this->storage_, sizeof(ptr)); + return ptr; + } + void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); } + + // Storage for string data. When is_heap_=0, contains the string directly (null-terminated). + // When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation. + char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator + uint8_t length_ : 7; // String length (0-127) + uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage + // Total size: 20 bytes (19 bytes storage + 1 byte bitfields) +}; + +static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); + class WiFiAP { public: void set_ssid(const std::string &ssid); void set_ssid(const char *ssid); - void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; } + void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } void set_bssid(const bssid_t &bssid); void clear_bssid(); void set_password(const std::string &password); void set_password(const char *password); - void set_password(const CompactString &password) { this->password_ = password; } + void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); } #ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP @@ -192,8 +239,8 @@ class WiFiAP { void set_manual_ip(optional manual_ip); #endif void set_hidden(bool hidden); - const CompactString &get_ssid() const { return this->ssid_; } - const CompactString &get_password() const { return this->password_; } + StringRef get_ssid() const { return this->ssid_.ref(); } + StringRef get_password() const { return this->password_.ref(); } const bssid_t &get_bssid() const; bool has_bssid() const; #ifdef USE_WIFI_WPA2_EAP @@ -233,7 +280,7 @@ class WiFiScanResult { bool get_matches() const; void set_matches(bool matches); const bssid_t &get_bssid() const; - const CompactString &get_ssid() const { return this->ssid_; } + StringRef get_ssid() const { return this->ssid_.ref(); } uint8_t get_channel() const; int8_t get_rssi() const; bool get_with_auth() const; @@ -387,9 +434,7 @@ class WiFiComponent : public Component { void save_wifi_sta(const std::string &ssid, const std::string &password); void save_wifi_sta(const char *ssid, const char *password); - void save_wifi_sta(const CompactString &ssid, const CompactString &password) { - this->save_wifi_sta(ssid.c_str(), password.c_str()); - } + void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -554,7 +599,7 @@ class WiFiComponent : public Component { int8_t find_first_non_hidden_index_() const; /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible - bool ssid_was_seen_in_scan_(const CompactString &ssid) const; + bool ssid_was_seen_in_scan_(StringRef ssid) const; /// Check if full scan results are needed (captive portal active, improv, listeners) bool needs_full_scan_results_() const; /// Check if network matches any configured network (for scan result filtering) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9d478f8bae5..c2f7f67d9a5 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #ifdef USE_ESP32 #include "rom/crc.h" @@ -859,60 +858,4 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { ; } -// CompactString implementation -CompactString::CompactString(const char *str, size_t len) { - if (len > MAX_LENGTH) { - len = MAX_LENGTH; // Clamp to max valid length - } - - this->length_ = len; - if (len <= INLINE_CAPACITY) { - // Store inline with null terminator - this->is_heap_ = 0; - if (len > 0) { - std::memcpy(this->storage_, str, len); - } - this->storage_[len] = '\0'; - } else { - // Heap allocate with null terminator - this->is_heap_ = 1; - char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory) - std::memcpy(heap_data, str, len); - heap_data[len] = '\0'; - this->set_heap_ptr_(heap_data); - } -} - -CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {} - -CompactString &CompactString::operator=(const CompactString &other) { - if (this != &other) { - this->~CompactString(); - new (this) CompactString(other); - } - return *this; -} - -CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) { - // Copy full storage (includes null terminator for inline, or pointer for heap) - std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1); - other.length_ = 0; - other.is_heap_ = 0; - other.storage_[0] = '\0'; -} - -CompactString &CompactString::operator=(CompactString &&other) noexcept { - if (this != &other) { - this->~CompactString(); - new (this) CompactString(std::move(other)); - } - return *this; -} - -CompactString::~CompactString() { - if (this->is_heap_) { - delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory) - } -} - } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index dfca1e51f0b..f7de34b6d5a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1787,64 +1787,4 @@ template::value, int> = 0> T &id(T ///@} -/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. -class CompactString { - public: - static constexpr uint8_t MAX_LENGTH = 127; - static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes - static constexpr uint8_t BUFFER_SIZE = MAX_LENGTH + 1; // For external buffer (128 bytes) - - CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; } - CompactString(const char *str, size_t len); - CompactString(const CompactString &other); - CompactString(CompactString &&other) noexcept; - CompactString &operator=(const CompactString &other); - CompactString &operator=(CompactString &&other) noexcept; - ~CompactString(); - - const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; } - const char *c_str() const { return this->data(); } // Always null-terminated - size_t size() const { return this->length_; } - bool empty() const { return this->length_ == 0; } - - // Implicit conversion to std::string for backwards compatibility - operator std::string() const { return std::string(this->data(), this->size()); } - - bool operator==(const CompactString &other) const { - return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; - } - bool operator==(const std::string &other) const { - return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; - } - bool operator==(const char *other) const { - return this->size() == std::strlen(other) && std::memcmp(this->data(), other, this->size()) == 0; - } - bool operator!=(const CompactString &other) const { return !(*this == other); } - bool operator!=(const std::string &other) const { return !(*this == other); } - bool operator!=(const char *other) const { return !(*this == other); } - - protected: - char *get_heap_ptr_() const { - char *ptr; - std::memcpy(&ptr, this->storage_, sizeof(ptr)); - return ptr; - } - void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); } - - // Storage for string data. When is_heap_=0, contains the string directly (null-terminated). - // When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation. - char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator - uint8_t length_ : 7; // String length (0-127) - uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage - // Total size: 20 bytes (19 bytes storage + 1 byte bitfields) -}; - -static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); - -// Reverse comparison overloads so CompactString works on either side of == and != -inline bool operator==(const std::string &lhs, const CompactString &rhs) { return rhs == lhs; } -inline bool operator==(const char *lhs, const CompactString &rhs) { return rhs == lhs; } -inline bool operator!=(const std::string &lhs, const CompactString &rhs) { return !(rhs == lhs); } -inline bool operator!=(const char *lhs, const CompactString &rhs) { return !(rhs == lhs); } - } // namespace esphome From 18a6e43db86b62b7df5f760f9f0f27f7bb787671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 13:51:49 -0600 Subject: [PATCH 3/9] avoid some conversion overhead --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6fe97c69a1d..d1014de43c4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2203,7 +2203,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { return false; } else if (!config.get_ssid().empty()) { // check if SSID matches - if (config.get_ssid() != this->ssid_.ref()) + if (this->ssid_ != config.get_ssid()) return false; } else { // network is configured without SSID - match other settings @@ -2345,7 +2345,7 @@ void WiFiComponent::process_roaming_scan_() { for (const auto &result : this->scan_result_) { // Must be same SSID, different BSSID - if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid) + if (result.get_ssid() != current_ssid || result.get_bssid() == current_bssid) continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 89621515900..cc97381e394 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -196,10 +196,10 @@ class CompactString { /// Return a StringRef view of this string (zero-copy) StringRef ref() const { return StringRef(this->data(), this->size()); } - bool operator==(const CompactString &other) const { - return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; + bool operator==(const StringRef &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0; } - bool operator!=(const CompactString &other) const { return !(*this == other); } + bool operator!=(const StringRef &other) const { return !(*this == other); } protected: char *get_heap_ptr_() const { From c1d53d9bad37a93b73745318021a92b5d765203d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:02:10 -0600 Subject: [PATCH 4/9] reduce blast --- .../improv_serial/improv_serial_component.cpp | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 33514c9e06a..ab91ca6765a 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command for (auto &scan : results) { if (scan.get_is_hidden()) continue; - const char *ssid_cstr = scan.get_ssid().c_str(); - // Check if we've already sent this SSID - bool duplicate = false; - for (const auto &seen : networks) { - if (strcmp(seen.c_str(), ssid_cstr) == 0) { - duplicate = true; - break; - } - } - if (duplicate) + StringRef ssid = scan.get_ssid(); + if (std::find(networks.begin(), networks.end(), ssid) != networks.end()) continue; - // Only allocate std::string after confirming it's not a duplicate - std::string ssid(ssid_cstr); // Send each ssid separately to avoid overflowing the buffer char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null *int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; std::vector data = improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false); this->send_response_(data); - networks.push_back(std::move(ssid)); + networks.emplace_back(ssid.c_str(), ssid.size()); } // Send empty response to signify the end of the list. std::vector data = From 0113575e4c65d6d212103a461def48f3c90380ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:33:29 -0600 Subject: [PATCH 5/9] reduce some of the StringRef bloat --- esphome/components/wifi/wifi_component.cpp | 57 +++++++++++-------- esphome/components/wifi/wifi_component.h | 13 +++-- .../wifi/wifi_component_esp8266.cpp | 22 +++---- .../wifi/wifi_component_esp_idf.cpp | 22 +++---- .../wifi/wifi_component_libretiny.cpp | 6 +- .../components/wifi/wifi_component_pico_w.cpp | 4 +- 6 files changed, 68 insertions(+), 56 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d1014de43c4..a1a5699e1e4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -104,6 +104,13 @@ CompactString::~CompactString() { } } +bool CompactString::operator==(const CompactString &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0; +} +bool CompactString::operator==(const StringRef &other) const { + return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0; +} + /// WiFi Retry Logic - Priority-Based BSSID Selection /// /// The WiFi component uses a state machine with priority degradation to handle connection failures @@ -406,18 +413,18 @@ bool WiFiComponent::needs_scan_results_() const { return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); } -bool WiFiComponent::ssid_was_seen_in_scan_(StringRef ssid) const { +bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const { // Check if this SSID is configured as hidden // If explicitly marked hidden, we should always try hidden mode regardless of scan results for (const auto &conf : this->sta_) { - if (conf.get_ssid() == ssid && conf.get_hidden()) { + if (conf.ssid_ == ssid && conf.get_hidden()) { return false; // Treat as not seen - force hidden mode attempt } } // Otherwise, check if we saw it in scan results for (const auto &scan : this->scan_result_) { - if (scan.get_ssid() == ssid) { + if (scan.ssid_ == ssid) { return true; } } @@ -466,14 +473,14 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t continue; } // For BSSID-only configs (empty SSID), match by BSSID - if (sta.get_ssid().empty()) { + if (sta.ssid_.empty()) { if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) { return true; } continue; } // Match by SSID - if (sta.get_ssid() == ssid) { + if (sta.ssid_ == StringRef(ssid)) { return true; } } @@ -522,18 +529,18 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { if (!include_explicit_hidden && sta.get_hidden()) { int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { - ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str()); continue; } } // In BLIND_RETRY mode, treat all networks as candidates // In SCAN_BASED mode, only retry networks that weren't seen in the scan - if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { - ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); + if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast(i)); return static_cast(i); } - ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str()); } // No hidden SSIDs found return -1; @@ -650,11 +657,11 @@ void WiFiComponent::start() { // Fast connect optimization: only use when we have saved BSSID+channel data // Without saved data, try first configured network or use normal flow if (loaded_fast_connect) { - ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str()); this->start_connecting(params); } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { // No saved data, but have configured networks - try first non-hidden network - ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str()); this->selected_sta_index_ = 0; params = this->build_params_for_current_phase_(); this->start_connecting(params); @@ -884,7 +891,7 @@ void WiFiComponent::setup_ap_config_() { if (this->ap_setup_) return; - if (this->ap_.get_ssid().empty()) { + if (this->ap_.ssid_.empty()) { // Build AP SSID from app name without heap allocation // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7 static constexpr size_t AP_SSID_MAX_LEN = 32; @@ -920,7 +927,7 @@ void WiFiComponent::setup_ap_config_() { " AP SSID: '%s'\n" " AP Password: '%s'\n" " IP Address: %s", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); + this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); @@ -1056,14 +1063,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, + ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:\n" " SSID: '%s'", - ap.get_ssid().c_str()); + ap.ssid_.c_str()); if (ap.has_bssid()) { ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { @@ -1096,7 +1103,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { client_key_present ? "present" : "not present"); } else { #endif - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str()); #ifdef USE_WIFI_WPA2_EAP } #endif @@ -1471,7 +1478,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && config && !config->get_hidden() && this->scan_result_.empty()) { - ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str()); } // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; @@ -1887,9 +1894,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { // Get SSID for logging (use pointer to avoid copy) const char *ssid = nullptr; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { - ssid = this->scan_result_[0].get_ssid().c_str(); + ssid = this->scan_result_[0].ssid_.c_str(); } else if (const WiFiAP *config = this->get_selected_sta_()) { - ssid = config->get_ssid().c_str(); + ssid = config->ssid_.c_str(); } // Only decrease priority on the last attempt for this phase @@ -2201,9 +2208,9 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { // don't match SSID if (!this->is_hidden_) return false; - } else if (!config.get_ssid().empty()) { + } else if (!config.ssid_.empty()) { // check if SSID matches - if (this->ssid_ != config.get_ssid()) + if (this->ssid_ != config.ssid_) return false; } else { // network is configured without SSID - match other settings @@ -2214,15 +2221,15 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { #ifdef USE_WIFI_WPA2_EAP // BSSID requires auth but no PSK or EAP credentials given - if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) + if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value())) return false; // BSSID does not require auth, but PSK or EAP credentials given - if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value())) + if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value())) return false; #else // If PSK given, only match for networks with auth (and vice versa) - if (config.get_password().empty() == this->with_auth_) + if (config.password_.empty() == this->with_auth_) return false; #endif @@ -2345,7 +2352,7 @@ void WiFiComponent::process_roaming_scan_() { for (const auto &result : this->scan_result_) { // Must be same SSID, different BSSID - if (result.get_ssid() != current_ssid || result.get_bssid() == current_bssid) + if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid) continue; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index cc97381e394..e146f9a45bc 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -196,9 +196,9 @@ class CompactString { /// Return a StringRef view of this string (zero-copy) StringRef ref() const { return StringRef(this->data(), this->size()); } - bool operator==(const StringRef &other) const { - return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0; - } + bool operator==(const CompactString &other) const; + bool operator!=(const CompactString &other) const { return !(*this == other); } + bool operator==(const StringRef &other) const; bool operator!=(const StringRef &other) const { return !(*this == other); } protected: @@ -220,6 +220,9 @@ class CompactString { static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); class WiFiAP { + friend class WiFiComponent; + friend class WiFiScanResult; + public: void set_ssid(const std::string &ssid); void set_ssid(const char *ssid); @@ -271,6 +274,8 @@ class WiFiAP { }; class WiFiScanResult { + friend class WiFiComponent; + public: WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); @@ -599,7 +604,7 @@ class WiFiComponent : public Component { int8_t find_first_non_hidden_index_() const; /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible - bool ssid_was_seen_in_scan_(StringRef ssid) const; + bool ssid_was_seen_in_scan_(const CompactString &ssid) const; /// Check if full scan results are needed (captive portal active, improv, listeners) bool needs_full_scan_results_() const; /// Check if network matches any configured network (for scan result filtering) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index db2fae660b9..c87345f0bf0 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -247,16 +247,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast(conf.password), ap.password_.c_str(), ap.password_.size()); if (ap.has_bssid()) { conf.bssid_set = 1; @@ -266,7 +266,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.threshold.authmode = AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -832,27 +832,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; struct softap_config conf {}; - if (ap.get_ssid().size() > sizeof(conf.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - conf.ssid_len = static_cast(ap.get_ssid().size()); + memcpy(reinterpret_cast(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + conf.ssid_len = static_cast(ap.ssid_.size()); conf.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ssid_hidden = ap.get_hidden(); conf.max_connection = 5; conf.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.authmode = AUTH_OPEN; *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.password)) { + if (ap.password_.size() > sizeof(conf.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.password), ap.password_.c_str(), ap.password_.size()); } ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 5bc435f7123..fe92df049a9 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -300,19 +300,19 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) { + if (ap.ssid_.size() > sizeof(conf.sta.ssid)) { ESP_LOGE(TAG, "SSID too long"); return false; } - if (ap.get_password().size() > sizeof(conf.sta.password)) { + if (ap.password_.size() > sizeof(conf.sta.password)) { ESP_LOGE(TAG, "Password too long"); return false; } - memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - memcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size()); + memcpy(reinterpret_cast(conf.sta.password), ap.password_.c_str(), ap.password_.size()); // The weakest authmode to accept in the fast scan mode - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { // Set threshold based on configured minimum auth mode @@ -1054,26 +1054,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) { + if (ap.ssid_.size() > sizeof(conf.ap.ssid)) { ESP_LOGE(TAG, "AP SSID too long"); return false; } - memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); + memcpy(reinterpret_cast(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size()); conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; - conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.ssid_hidden = ap.ssid_.size(); conf.ap.max_connection = 5; conf.ap.beacon_interval = 100; - if (ap.get_password().empty()) { + if (ap.password_.empty()) { conf.ap.authmode = WIFI_AUTH_OPEN; *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - if (ap.get_password().size() > sizeof(conf.ap.password)) { + if (ap.password_.size() > sizeof(conf.ap.password)) { ESP_LOGE(TAG, "AP password too long"); return false; } - memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size()); + memcpy(reinterpret_cast(conf.ap.password), ap.password_.c_str(), ap.password_.size()); } // pairwise cipher of SoftAP, group cipher will be derived using this. diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 7abee5f0820..2cc05928afd 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -193,7 +193,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; String ssid = WiFi.SSID(); - if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) { + if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) { WiFi.disconnect(); } @@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { s_sta_state = LTWiFiSTAState::CONNECTING; s_ignored_disconnect_count = 0; - WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.get_channel(), // 0 = auto ap.has_bssid() ? ap.get_bssid().data() : NULL); if (status != WL_CONNECTED) { @@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { yield(); - return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), + return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 4a7dc9ba22c..1baf21e2b2c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -78,7 +78,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; #endif - auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); + auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); if (ret != WL_CONNECTED) return false; @@ -203,7 +203,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1); + WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); return true; } From f8115e9b1816092b1e4a8eea27eaf63bc0ecd4e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:39:54 -0600 Subject: [PATCH 6/9] Revert "reduce blast" This reverts commit c1d53d9bad37a93b73745318021a92b5d765203d. --- .../improv_serial/improv_serial_component.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ab91ca6765a..33514c9e06a 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command for (auto &scan : results) { if (scan.get_is_hidden()) continue; - StringRef ssid = scan.get_ssid(); - if (std::find(networks.begin(), networks.end(), ssid) != networks.end()) + const char *ssid_cstr = scan.get_ssid().c_str(); + // Check if we've already sent this SSID + bool duplicate = false; + for (const auto &seen : networks) { + if (strcmp(seen.c_str(), ssid_cstr) == 0) { + duplicate = true; + break; + } + } + if (duplicate) continue; + // Only allocate std::string after confirming it's not a duplicate + std::string ssid(ssid_cstr); // Send each ssid separately to avoid overflowing the buffer char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null *int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; std::vector data = improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false); this->send_response_(data); - networks.emplace_back(ssid.c_str(), ssid.size()); + networks.push_back(std::move(ssid)); } // Send empty response to signify the end of the list. std::vector data = From 322fe205abe391745710cea5d69839bbdd3e8a3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:40:05 -0600 Subject: [PATCH 7/9] debloat --- esphome/components/esp32_improv/esp32_improv_component.cpp | 4 ++-- esphome/components/improv_serial/improv_serial_component.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 1a19472c874..83bc842a3d3 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() { return; } wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 33514c9e06a..edceb9a3b13 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command switch (command.command) { case improv::WIFI_SETTINGS: { wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); From 6a5015b2df3836b4f9b61123819b19d77350f61c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:43:24 -0600 Subject: [PATCH 8/9] debloat --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a1a5699e1e4..61d05d76357 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -480,7 +480,7 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t continue; } // Match by SSID - if (sta.ssid_ == StringRef(ssid)) { + if (sta.ssid_ == ssid) { return true; } } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e146f9a45bc..4cf19a8059b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -200,6 +200,8 @@ class CompactString { bool operator!=(const CompactString &other) const { return !(*this == other); } bool operator==(const StringRef &other) const; bool operator!=(const StringRef &other) const { return !(*this == other); } + bool operator==(const char *other) const { return *this == StringRef(other); } + bool operator!=(const char *other) const { return !(*this == other); } protected: char *get_heap_ptr_() const { From ee0f3a8548f783e29f40dc95b28df1a9f3ee593a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 14:52:06 -0600 Subject: [PATCH 9/9] fix pre-existing bug --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index fe92df049a9..52ee4821215 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1060,7 +1060,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } memcpy(reinterpret_cast(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size()); conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; - conf.ap.ssid_hidden = ap.ssid_.size(); + conf.ap.ssid_hidden = ap.get_hidden(); conf.ap.max_connection = 5; conf.ap.beacon_interval = 100;