Compare commits

...

23 Commits

Author SHA1 Message Date
J. Nick Koston
7336985753 reduce some more 2026-01-22 17:53:50 -10:00
J. Nick Koston
73d076c278 reduce some more 2026-01-22 17:35:00 -10:00
J. Nick Koston
3a2c66171b use placement new to avoid duplicate code 2026-01-22 17:29:21 -10:00
J. Nick Koston
fca867e18d [wifi] Add CompactString to reduce WiFi scan heap fragmentation 2026-01-22 17:18:13 -10:00
J. Nick Koston
0ae90512cf [wifi] Add CompactString to reduce WiFi scan heap fragmentation 2026-01-22 17:16:35 -10:00
J. Nick Koston
165f81dc97 Merge branch 'dev' into filter_wifi_scan_results 2026-01-22 15:05:38 -10:00
J. Nick Koston
dc971b4ed0 tidy 2026-01-20 22:54:52 -10:00
J. Nick Koston
a4fe9852aa tidy 2026-01-20 22:54:36 -10:00
J. Nick Koston
f6ec5e9c28 tweak 2026-01-20 22:41:45 -10:00
J. Nick Koston
0051196e86 fix 2026-01-20 21:41:43 -10:00
J. Nick Koston
9f83b24913 tweak 2026-01-20 21:19:30 -10:00
J. Nick Koston
5c0747cfe0 Update esphome/components/wifi/wifi_component.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 21:10:51 -10:00
J. Nick Koston
f0c7306ad5 log scan complete 2026-01-20 21:04:52 -10:00
J. Nick Koston
09b42b778b log scan complete 2026-01-20 20:57:39 -10:00
J. Nick Koston
d610c3ae91 fix bssid only 2026-01-20 20:54:30 -10:00
J. Nick Koston
687f9a762d fixes for libretiny 2026-01-20 20:44:28 -10:00
J. Nick Koston
acb22ed286 tweaks 2026-01-20 20:39:30 -10:00
J. Nick Koston
692167341e tweaks 2026-01-20 20:37:16 -10:00
J. Nick Koston
d5d6936845 tweaks 2026-01-20 20:35:32 -10:00
J. Nick Koston
bffe4a2e05 tweaks 2026-01-20 20:34:53 -10:00
J. Nick Koston
d7c3947ccc tweak loggig 2026-01-20 20:31:38 -10:00
J. Nick Koston
6f3a49e509 tweak loggig 2026-01-20 20:30:55 -10:00
J. Nick Koston
7aef173e65 [wifi] Filter scan results to only store matching networks 2026-01-20 20:19:35 -10:00
10 changed files with 367 additions and 85 deletions

View File

@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &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<uint8_t> 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(ssid);
networks.push_back(std::move(ssid));
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -39,6 +39,10 @@
#include "esphome/components/esp32_improv/esp32_improv_component.h"
#endif
#ifdef USE_IMPROV_SERIAL
#include "esphome/components/improv_serial/improv_serial_component.h"
#endif
namespace esphome::wifi {
static const char *const TAG = "wifi";
@@ -347,7 +351,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 std::string &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_) {
@@ -365,6 +369,75 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
return false;
}
bool WiFiComponent::needs_full_scan_results_() const {
// Components that require full scan results (for example, scan result listeners)
// are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
if (this->keep_scan_results_) {
return true;
}
#ifdef USE_CAPTIVE_PORTAL
// Captive portal needs full results when active (showing network list to user)
if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) {
return true;
}
#endif
#ifdef USE_IMPROV_SERIAL
// Improv serial needs results during provisioning (before connected)
if (improv_serial::global_improv_serial_component != nullptr && !this->is_connected()) {
return true;
}
#endif
#ifdef USE_IMPROV
// BLE improv also needs results during provisioning
if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) {
return true;
}
#endif
return false;
}
bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
// Hidden networks in scan results have empty SSIDs - skip them
if (ssid[0] == '\0') {
return false;
}
for (const auto &sta : this->sta_) {
// Skip hidden network configs (they don't appear in normal scans)
if (sta.get_hidden()) {
continue;
}
// For BSSID-only configs (empty SSID), match by BSSID
if (sta.get_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) {
return true;
}
}
return false;
}
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// Skip logging during roaming scans to avoid log buffer overflow
// (roaming scans typically find many networks but only care about same-SSID APs)
if (this->roaming_state_ == RoamingState::SCANNING) {
return;
}
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
format_mac_addr_upper(bssid, bssid_s);
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
#endif
}
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
// Find next SSID to try in RETRY_HIDDEN phase.
//
@@ -656,8 +729,12 @@ void WiFiComponent::loop() {
ESP_LOGI(TAG, "Starting fallback AP");
this->setup_ap_config_();
#ifdef USE_CAPTIVE_PORTAL
if (captive_portal::global_captive_portal != nullptr)
if (captive_portal::global_captive_portal != nullptr) {
// Reset so we force one full scan after captive portal starts
// (previous scans were filtered because captive portal wasn't active yet)
this->has_completed_scan_after_captive_portal_start_ = false;
captive_portal::global_captive_portal->start();
}
#endif
}
}
@@ -862,9 +939,12 @@ WiFiAP WiFiComponent::get_sta() const {
return config ? *config : WiFiAP{};
}
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save);
// ensure it's written immediately
global_preferences->sync();
@@ -1179,7 +1259,7 @@ template<typename VectorType> static void insertion_sort_scan_results(VectorType
// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
char bssid_s[18];
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
auto bssid = res.get_bssid();
format_mac_addr_upper(bssid.data(), bssid_s);
@@ -1195,18 +1275,6 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
#endif
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// Helper function to log non-matching scan results at verbose level
__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) {
char bssid_s[18];
auto bssid = res.get_bssid();
format_mac_addr_upper(bssid.data(), bssid_s);
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
}
#endif
void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) {
if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
@@ -1216,6 +1284,8 @@ void WiFiComponent::check_scanning_finished() {
return;
}
this->scan_done_ = false;
this->has_completed_scan_after_captive_portal_start_ =
true; // Track that we've done a scan since captive portal started
this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED;
if (this->scan_result_.empty()) {
@@ -1243,21 +1313,12 @@ void WiFiComponent::check_scanning_finished() {
// Sort scan results using insertion sort for better memory efficiency
insertion_sort_scan_results(this->scan_result_);
size_t non_matching_count = 0;
// Log matching networks (non-matching already logged at VERBOSE in scan callback)
for (auto &res : this->scan_result_) {
if (res.get_matches()) {
log_scan_result(res);
} else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
log_scan_result_non_matching(res);
#else
non_matching_count++;
#endif
}
}
if (non_matching_count > 0) {
ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count);
}
// SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
// After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
@@ -1516,7 +1577,10 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
if (this->went_through_explicit_hidden_phase_()) {
return WiFiRetryPhase::EXPLICIT_HIDDEN;
}
// Skip scanning when captive portal/improv is active to avoid disrupting AP.
// Skip scanning when captive portal/improv is active to avoid disrupting AP,
// BUT only if we've already completed at least one scan AFTER the portal started.
// When captive portal first starts, scan results may be filtered/stale, so we need
// to do one full scan to populate available networks for the captive portal UI.
//
// WHY SCANNING DISRUPTS AP MODE:
// WiFi scanning requires the radio to leave the AP's channel and hop through
@@ -1533,7 +1597,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
//
// This allows users to configure WiFi via captive portal while the device keeps
// attempting to connect to all configured networks in sequence.
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
// Captive portal needs scan results to show available networks.
// If captive portal is active, only skip scanning if we've done a scan after it started.
// If only improv is active (no captive portal), skip scanning since improv doesn't need results.
if (this->is_captive_portal_active_()) {
if (this->has_completed_scan_after_captive_portal_start_) {
return WiFiRetryPhase::RETRY_HIDDEN;
}
// Need to scan for captive portal
} else if (this->is_esp32_improv_active_()) {
// Improv doesn't need scan results
return WiFiRetryPhase::RETRY_HIDDEN;
}
return WiFiRetryPhase::SCAN_CONNECTING;
@@ -1716,11 +1789,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
// Get SSID for logging (use pointer to avoid copy)
const std::string *ssid = nullptr;
const char *ssid = nullptr;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
ssid = &this->scan_result_[0].get_ssid();
ssid = this->scan_result_[0].get_ssid().c_str();
} else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = &config->get_ssid();
ssid = config->get_ssid().c_str();
}
// Only decrease priority on the last attempt for this phase
@@ -1740,8 +1813,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
bssid_s, old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -1989,10 +2062,14 @@ void WiFiComponent::save_fast_connect_settings_() {
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
@@ -2002,10 +2079,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
@@ -2016,12 +2091,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden)
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
: bssid_(bssid),
channel_(channel),
rssi_(rssi),
ssid_(std::move(ssid)),
ssid_(ssid, ssid_len),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const {
@@ -2064,7 +2139,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
bool WiFiScanResult::get_matches() const { return this->matches_; }
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
@@ -2080,7 +2154,7 @@ void WiFiComponent::clear_roaming_state_() {
void WiFiComponent::release_scan_results_() {
if (!this->keep_scan_results_) {
#ifdef USE_RP2040
#if defined(USE_RP2040) || defined(USE_ESP32)
// std::vector - use swap trick since shrink_to_fit is non-binding
decltype(this->scan_result_)().swap(this->scan_result_);
#else
@@ -2137,7 +2211,7 @@ void WiFiComponent::process_roaming_scan_() {
for (const auto &result : this->scan_result_) {
// Must be same SSID, different BSSID
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid)
continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE

View File

@@ -161,9 +161,12 @@ struct EAPAuth {
using bssid_t = std::array<uint8_t, 6>;
// Use std::vector for RP2040 since scan count is unknown (callback-based)
// Use FixedVector for other platforms where count is queried first
#ifdef USE_RP2040
/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID)
static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8;
// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API)
// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible
#if defined(USE_RP2040) || defined(USE_ESP32)
template<typename T> using wifi_scan_vector_t = std::vector<T>;
#else
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
@@ -172,9 +175,13 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
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_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; }
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
@@ -185,10 +192,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden);
const std::string &get_ssid() const;
const CompactString &get_ssid() const { return this->ssid_; }
const CompactString &get_password() const { return this->password_; }
const bssid_t &get_bssid() const;
bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
@@ -201,8 +208,8 @@ class WiFiAP {
bool get_hidden() const;
protected:
std::string ssid_;
std::string password_;
CompactString ssid_;
CompactString password_;
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
@@ -218,14 +225,15 @@ class WiFiAP {
class WiFiScanResult {
public:
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden);
bool matches(const WiFiAP &config) const;
bool get_matches() const;
void set_matches(bool matches);
const bssid_t &get_bssid() const;
const std::string &get_ssid() const;
const CompactString &get_ssid() const { return this->ssid_; }
uint8_t get_channel() const;
int8_t get_rssi() const;
bool get_with_auth() const;
@@ -239,7 +247,7 @@ class WiFiScanResult {
bssid_t bssid_;
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
CompactString ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
@@ -378,6 +386,10 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive);
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());
}
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -538,7 +550,14 @@ 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 std::string &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)
/// Matches by SSID when configured, or by BSSID for BSSID-only configs
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const;
/// Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel);
/// Find next SSID that wasn't in scan results (might be hidden)
/// Returns index of next potentially hidden SSID, or -1 if none found
/// @param start_index Start searching from index after this (-1 to start from beginning)
@@ -710,6 +729,8 @@ class WiFiComponent : public Component {
bool enable_on_boot_{true};
bool got_ipv4_address_{false};
bool keep_scan_results_{false};
bool has_completed_scan_after_captive_portal_start_{
false}; // Tracks if we've completed a scan after captive portal started
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
bool skip_cooldown_next_cycle_{false};
bool post_connect_roaming_{true}; // Enabled by default

View File

@@ -756,20 +756,35 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
return;
}
// Count the number of results first
auto *head = reinterpret_cast<bss_info *>(arg);
bool needs_full = this->needs_full_scan_results_();
// First pass: count matching networks (linked list is non-destructive)
size_t total = 0;
size_t count = 0;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
count++;
total++;
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
count++;
}
}
this->scan_result_.init(count);
this->scan_result_.init(count); // Exact allocation
// Second pass: store matching networks
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN,
it->is_hidden != 0);
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
} else {
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
}
}
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
this->scan_done_ = true;
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : global_wifi_component->scan_results_listeners_) {

View File

@@ -827,7 +827,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
uint16_t number = it.number;
scan_result_.init(number);
bool needs_full = this->needs_full_scan_results_();
// Smart reserve: full capacity if needed, small reserve otherwise
if (needs_full) {
this->scan_result_.reserve(number);
} else {
this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE);
}
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
@@ -838,12 +845,22 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
ssid.empty());
// Check C string first - avoid std::string construction for non-matching networks
const char *ssid_cstr = reinterpret_cast<const char *>(record.ssid);
// Only construct std::string and store if needed
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
}
}
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : this->scan_results_listeners_) {
listener->on_wifi_scan_results(this->scan_result_);

View File

@@ -666,18 +666,39 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0)
return;
this->scan_result_.init(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
bool needs_full = this->needs_full_scan_results_();
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
ssid.length() == 0);
// Access scan results directly via WiFi.scan struct to avoid Arduino String allocations
// WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers
auto *scan = WiFi.scan;
// First pass: count matching networks
size_t count = 0;
for (int i = 0; i < num; i++) {
const char *ssid_cstr = scan->ap[i].ssid;
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
count++;
}
}
this->scan_result_.init(count); // Exact allocation
// Second pass: store matching networks
for (int i = 0; i < num; i++) {
const char *ssid_cstr = scan->ap[i].ssid;
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
auto &ap = scan->ap[i];
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
ap.bssid.addr[4], ap.bssid.addr[5]},
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0');
} else {
auto &ap = scan->ap[i];
this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel);
}
}
ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
WiFi.scanDelete();
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : this->scan_results_listeners_) {

View File

@@ -21,6 +21,7 @@ static const char *const TAG = "wifi_pico_w";
// Track previous state for detecting changes
static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static size_t s_scan_result_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
if (sta.has_value()) {
@@ -137,10 +138,19 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r
}
void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
s_scan_result_count++;
const char *ssid_cstr = reinterpret_cast<const char *>(result->ssid);
// Skip networks that don't match any configured network (unless full results needed)
if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) {
this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel);
return;
}
bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(result->ssid));
WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty());
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res);
}
@@ -149,6 +159,7 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
bool WiFiComponent::wifi_scan_start_(bool passive) {
this->scan_result_.clear();
this->scan_done_ = false;
s_scan_result_count = 0;
cyw43_wifi_scan_options_t scan_options = {0};
scan_options.scan_type = passive ? 1 : 0;
int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result);
@@ -244,7 +255,9 @@ void WiFiComponent::wifi_loop_() {
// Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true;
ESP_LOGV(TAG, "Scan done");
bool needs_full = this->needs_full_scan_results_();
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
for (auto *listener : this->scan_results_listeners_) {
listener->on_wifi_scan_results(this->scan_result_);

View File

@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
for (const auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &ssid = scan.get_ssid();
const auto &ssid = scan.get_ssid();
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end)
break;

View File

@@ -12,6 +12,7 @@
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <new>
#ifdef USE_ESP32
#include "rom/crc.h"
@@ -857,4 +858,60 @@ 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

View File

@@ -1749,4 +1749,58 @@ template<typename T, enable_if_t<std::is_pointer<T *>::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");
} // namespace esphome