Compare commits

...

6 Commits

Author SHA1 Message Date
J. Nick Koston
217ce9d082 [wifi] Replace std::string with inline char array in WiFiScanResult 2026-01-01 12:16:41 -10:00
J. Nick Koston
b20a16c0de tweak 2026-01-01 12:11:55 -10:00
J. Nick Koston
1968d6c101 tweak 2026-01-01 12:10:35 -10:00
J. Nick Koston
25b6913e8d tweak 2026-01-01 12:10:14 -10:00
J. Nick Koston
abcf363a3c [wifi] Reduce heap fragmentation from WiFi scan results with SSID deduplication 2026-01-01 12:02:20 -10:00
J. Nick Koston
127c910207 [wifi] Reduce heap fragmentation from WiFi scan results with SSID deduplication 2026-01-01 11:57:13 -10:00
9 changed files with 95 additions and 70 deletions

View File

@@ -29,18 +29,17 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
if (scan.get_is_hidden())
continue;
// Assumes no " in ssid, possible unicode isses?
// Assumes no " in ssid, possible unicode issues?
#ifdef USE_ESP8266
stream->print(ESPHOME_F(",{\"ssid\":\""));
stream->print(scan.get_ssid().c_str());
stream->print(scan.get_ssid());
stream->print(ESPHOME_F("\",\"rssi\":"));
stream->print(scan.get_rssi());
stream->print(ESPHOME_F(",\"lock\":"));
stream->print(scan.get_with_auth());
stream->print(ESPHOME_F("}"));
#else
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
scan.get_with_auth());
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid(), scan.get_rssi(), scan.get_with_auth());
#endif
}
stream->print(ESPHOME_F("]}"));

View File

@@ -259,14 +259,14 @@ 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();
const char *ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
continue;
// Send each ssid separately to avoid overflowing the buffer
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_WIFI_NETWORKS, {ssid, str_sprintf("%d", scan.get_rssi()), YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(ssid);
networks.emplace_back(ssid);
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -1045,7 +1045,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
auto bssid = res.get_bssid();
format_mac_addr_upper(bssid.data(), bssid_s);
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid(),
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
@@ -1058,7 +1058,7 @@ __attribute__((noinline)) static void log_scan_result_non_matching(const WiFiSca
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,
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid(), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
}
#endif
@@ -1532,11 +1532,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();
} 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
@@ -1556,8 +1556,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
@@ -1818,19 +1818,22 @@ 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, uint8_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)),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
flags_((with_auth ? FLAG_WITH_AUTH : 0) | (is_hidden ? FLAG_IS_HIDDEN : 0)) {
// Copy SSID with length limit and null-terminate
uint8_t len = ssid_len > ESPHOME_MAX_SSID_LEN ? ESPHOME_MAX_SSID_LEN : ssid_len;
memcpy(this->ssid_, ssid, len);
this->ssid_[len] = '\0';
}
bool WiFiScanResult::matches(const WiFiAP &config) const {
if (config.get_hidden()) {
// User configured a hidden network, only match actually hidden networks
// don't match SSID
if (!this->is_hidden_)
if (!this->get_is_hidden())
return false;
} else if (!config.get_ssid().empty()) {
// check if SSID matches
@@ -1845,15 +1848,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->get_with_auth() && (config.get_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->get_with_auth() && (!config.get_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.get_password().empty() == this->get_with_auth())
return false;
#endif
@@ -1863,14 +1866,13 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
}
return true;
}
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_; }
bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
void WiFiScanResult::set_matches(bool matches) {
if (matches) {
this->flags_ |= FLAG_MATCHES;
} else {
this->flags_ &= ~FLAG_MATCHES;
}
}
bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }

View File

@@ -8,6 +8,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include <cstring>
#include <span>
#include <string>
#include <vector>
@@ -61,12 +62,21 @@ namespace esphome::wifi {
/// Sentinel value for RSSI when WiFi is not connected
static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator)
static constexpr size_t SSID_BUFFER_SIZE = 33;
/// Maximum SSID length per IEEE 802.11
static constexpr uint8_t ESPHOME_MAX_SSID_LEN = 32;
/// Buffer size for SSID (max length + null terminator)
static constexpr size_t SSID_BUFFER_SIZE = ESPHOME_MAX_SSID_LEN + 1;
/// Maximum password length per WPA2
static constexpr uint8_t MAX_PASSWORD_LEN = 64;
/// Buffer size for password (max length + null terminator)
static constexpr size_t PASSWORD_BUFFER_SIZE = MAX_PASSWORD_LEN + 1;
struct SavedWifiSettings {
char ssid[33];
char password[65];
char ssid[SSID_BUFFER_SIZE];
char password[PASSWORD_BUFFER_SIZE];
} PACKED; // NOLINT
struct SavedWifiFastConnectSettings {
@@ -202,32 +212,35 @@ 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, uint8_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;
bool get_matches() const { return this->flags_ & FLAG_MATCHES; }
void set_matches(bool matches);
const bssid_t &get_bssid() const;
const std::string &get_ssid() const;
uint8_t get_channel() const;
int8_t get_rssi() const;
bool get_with_auth() const;
bool get_is_hidden() const;
int8_t get_priority() const { return priority_; }
void set_priority(int8_t priority) { priority_ = priority; }
const bssid_t &get_bssid() const { return this->bssid_; }
const char *get_ssid() const { return this->ssid_; }
uint8_t get_channel() const { return this->channel_; }
int8_t get_rssi() const { return this->rssi_; }
bool get_with_auth() const { return this->flags_ & FLAG_WITH_AUTH; }
bool get_is_hidden() const { return this->flags_ & FLAG_IS_HIDDEN; }
int8_t get_priority() const { return this->priority_; }
void set_priority(int8_t priority) { this->priority_ = priority; }
bool operator==(const WiFiScanResult &rhs) const;
protected:
bssid_t bssid_;
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
bool is_hidden_;
static constexpr uint8_t FLAG_MATCHES = 1 << 0;
static constexpr uint8_t FLAG_WITH_AUTH = 1 << 1;
static constexpr uint8_t FLAG_IS_HIDDEN = 1 << 2;
bssid_t bssid_; // 6 bytes
uint8_t channel_; // 1 byte
int8_t rssi_; // 1 byte
char ssid_[SSID_BUFFER_SIZE]; // 33 bytes - inline storage, no heap
int8_t priority_{0}; // 1 byte
uint8_t flags_{0}; // 1 byte (+ 1 byte padding)
};
struct WiFiSTAPriority {
@@ -378,7 +391,7 @@ class WiFiComponent : public Component {
const char *get_use_address() const;
void set_use_address(const char *use_address);
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return this->scan_result_; }
network::IPAddress wifi_soft_ap_ip();

View File

@@ -751,7 +751,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
return;
}
// Count the number of results first
// Count results first
auto *head = reinterpret_cast<bss_info *>(arg);
size_t count = 0;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
@@ -760,9 +760,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->scan_result_.init(count);
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
uint8_t len = std::min(it->ssid_len, static_cast<uint8>(ESPHOME_MAX_SSID_LEN));
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,
reinterpret_cast<const char *>(it->ssid), len, it->channel, it->rssi, it->authmode != AUTH_OPEN,
it->is_hidden != 0);
}
this->scan_done_ = true;

View File

@@ -842,9 +842,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
auto &record = records[i];
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());
// ESP-IDF ssid is null-terminated uint8_t[33]
const char *ssid = reinterpret_cast<const char *>(record.ssid);
uint8_t ssid_len = static_cast<uint8_t>(strlen(ssid));
scan_result_.emplace_back(bssid, ssid, ssid_len, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
ssid_len == 0);
}
#ifdef USE_WIFI_LISTENERS
for (auto *listener : this->scan_results_listeners_) {
@@ -1099,8 +1101,8 @@ const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer
buffer[0] = '\0';
return buffer.data();
}
// info.ssid is uint8[33], but only 32 bytes are SSID data
size_t len = strnlen(reinterpret_cast<const char *>(info.ssid), 32);
// info.ssid is uint8[33], but only ESPHOME_MAX_SSID_LEN bytes are SSID data
size_t len = strnlen(reinterpret_cast<const char *>(info.ssid), ESPHOME_MAX_SSID_LEN);
memcpy(buffer.data(), info.ssid, len);
buffer[len] = '\0';
return buffer.data();

View File

@@ -485,10 +485,10 @@ void WiFiComponent::wifi_scan_done_callback_() {
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
uint8_t ssid_len = static_cast<uint8_t>(ssid.length());
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);
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, ssid.c_str(),
ssid_len, channel, rssi, authmode != WIFI_AUTH_OPEN, ssid_len == 0);
}
WiFi.scanDelete();
#ifdef USE_WIFI_LISTENERS

View File

@@ -139,11 +139,18 @@ 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) {
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());
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res);
const char *ssid = reinterpret_cast<const char *>(result->ssid);
uint8_t ssid_len = result->ssid_len;
// Check for duplicates by BSSID (same AP) - RP2040 can report same AP multiple times
for (const auto &existing : this->scan_result_) {
if (existing.get_bssid() == bssid) {
return; // Already have this BSSID
}
}
this->scan_result_.emplace_back(bssid, ssid, ssid_len, result->channel, result->rssi,
result->auth_mode != CYW43_AUTH_OPEN, ssid_len == 0);
}
bool WiFiComponent::wifi_scan_start_(bool passive) {

View File

@@ -84,11 +84,12 @@ 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 char *ssid = scan.get_ssid();
size_t ssid_len = strlen(ssid);
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end)
if (ptr + ssid_len + 9 > end)
break;
ptr = format_scan_entry(ptr, ssid.c_str(), ssid.size(), scan.get_rssi());
ptr = format_scan_entry(ptr, ssid, ssid_len, scan.get_rssi());
}
*ptr = '\0';