Compare commits

...

2 Commits

9 changed files with 241 additions and 71 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,18 @@ 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,
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, 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) {}
ssid_(ssid),
flags_((with_auth ? FLAG_WITH_AUTH : 0) | (is_hidden ? FLAG_IS_HIDDEN : 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 +1844,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 +1862,17 @@ 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; }
void WiFiScanResult::set_matches(bool matches) {
if (matches) {
this->flags_ |= FLAG_MATCHES;
} else {
this->flags_ &= ~FLAG_MATCHES;
}
}
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
const char *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_; }
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,25 @@ 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;
/// Maximum unique SSIDs tracked for deduplication during scan
/// Beyond this limit, duplicate SSID storage may occur
static constexpr size_t MAX_UNIQUE_SSIDS = 32;
struct SavedWifiSettings {
char ssid[33];
char password[65];
char ssid[SSID_BUFFER_SIZE];
char password[PASSWORD_BUFFER_SIZE];
} PACKED; // NOLINT
struct SavedWifiFastConnectSettings {
@@ -202,32 +216,159 @@ 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 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;
const char *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; }
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 (now 4-byte aligned for pointer)
const char *ssid_; // 4 bytes - points into WiFiScanResults::ssid_pool_
int8_t priority_{0}; // 1 byte
uint8_t flags_{0}; // 1 byte (+ 2 bytes padding for struct alignment)
};
/// SSID entry with length for efficient comparison
struct SSIDEntry {
char ssid[SSID_BUFFER_SIZE]; // SSID data, always null-terminated
uint8_t len{0}; // Length of SSID (0-ESPHOME_MAX_SSID_LEN)
/// Get null-terminated SSID
const char *c_str() const { return this->ssid; }
/// Check if this entry matches the given SSID
bool matches(const char *other, uint8_t other_len) const {
return this->len == other_len && memcmp(this->ssid, other, other_len) == 0;
}
/// Store an SSID with its length (always null-terminates)
void set(const char *data, uint8_t length) {
this->len = length > ESPHOME_MAX_SSID_LEN ? ESPHOME_MAX_SSID_LEN : length;
memcpy(this->ssid, data, this->len);
this->ssid[this->len] = '\0';
}
};
/// Helper to count unique SSIDs using length + memcmp comparison.
/// Designed for single-pass counting during scan result iteration.
/// Tracks up to MAX_UNIQUE_SSIDS - sufficient for typical environments.
/// After capacity is reached, continues checking against stored SSIDs
/// and counts additional unique ones separately.
class UniqueSSIDCounter {
public:
/// Add an SSID and return true if not previously seen (vs stored SSIDs)
/// @param ssid Pointer to SSID data (not required to be null-terminated)
/// @param len Length of SSID (0-32)
__attribute__((noinline)) bool add(const char *ssid, uint8_t len) {
// Always check against stored SSIDs
for (size_t i = 0; i < this->count_; i++) {
if (this->ssids_[i].matches(ssid, len))
return false; // Already seen
}
if (this->count_ < MAX_UNIQUE_SSIDS) {
this->ssids_[this->count_].set(ssid, len);
this->count_++;
} else {
// Can't store, but wasn't a duplicate of stored SSIDs
// Count as potential unique (worst case for pool sizing)
this->overflow_count_++;
}
return true; // Not seen before (in stored set)
}
/// Total pool size needed: stored + overflow (worst case)
size_t pool_size() const { return this->count_ + this->overflow_count_; }
protected:
std::array<SSIDEntry, MAX_UNIQUE_SSIDS> ssids_{};
uint8_t count_{0};
uint8_t overflow_count_{0};
};
/// Container for WiFi scan results with SSID deduplication.
/// SSIDs are interned into a pool - duplicates point to the same storage.
class WiFiScanResults {
public:
#ifndef USE_RP2040
/// Initialize storage for the expected number of results and SSIDs.
/// @param result_count Number of scan results to store
/// @param ssid_pool_size Size of SSID pool (use result_count if UniqueSSIDCounter overflowed)
void init(size_t result_count, size_t ssid_pool_size) {
this->results_.init(result_count);
this->ssid_pool_.init(ssid_pool_size);
}
#endif
/// Add a scan result, interning the SSID
/// @param ssid Pointer to SSID data (not required to be null-terminated)
/// @param ssid_len Length of SSID (0-32)
void emplace_back(const bssid_t &bssid, const char *ssid, uint8_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden) {
const char *interned_ssid = this->intern_ssid_(ssid, ssid_len);
this->results_.emplace_back(bssid, interned_ssid, channel, rssi, with_auth, is_hidden);
}
/// Clear both results and SSID pool
void clear() {
this->results_.clear();
this->ssid_pool_.clear();
}
/// Release memory (for shrink_to_fit semantics)
void shrink_to_fit() {
this->results_.shrink_to_fit();
this->ssid_pool_.shrink_to_fit();
}
// Vector-like accessors
bool empty() const { return this->results_.empty(); }
size_t size() const { return this->results_.size(); }
WiFiScanResult &operator[](size_t idx) { return this->results_[idx]; }
const WiFiScanResult &operator[](size_t idx) const { return this->results_[idx]; }
auto begin() { return this->results_.begin(); }
auto end() { return this->results_.end(); }
auto begin() const { return this->results_.begin(); }
auto end() const { return this->results_.end(); }
/// Get underlying vector (for listener interface compatibility)
const wifi_scan_vector_t<WiFiScanResult> &results() const { return this->results_; }
protected:
/// Intern an SSID - returns pointer to existing or newly added pool entry
/// @param ssid Pointer to SSID data (not required to be null-terminated)
/// @param len Length of SSID (0-32)
__attribute__((noinline)) const char *intern_ssid_(const char *ssid, uint8_t len) {
// Check if already in pool
for (auto &entry : this->ssid_pool_) {
if (entry.matches(ssid, len)) {
return entry.c_str();
}
}
// Add new entry (pool is sized via UniqueSSIDCounter::pool_size())
this->ssid_pool_.emplace_back();
this->ssid_pool_.back().set(ssid, len);
return this->ssid_pool_.back().c_str();
}
wifi_scan_vector_t<WiFiScanResult> results_;
wifi_scan_vector_t<SSIDEntry> ssid_pool_;
};
struct WiFiSTAPriority {
@@ -378,7 +519,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_.results(); }
network::IPAddress wifi_soft_ap_ip();
@@ -598,7 +739,7 @@ class WiFiComponent : public Component {
FixedVector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;
wifi_scan_vector_t<WiFiScanResult> scan_result_;
WiFiScanResults scan_result_;
#ifdef USE_WIFI_AP
WiFiAP ap_;
#endif

View File

@@ -751,24 +751,28 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
return;
}
// Count the number of results first
// Count results and unique SSIDs in single pass
auto *head = reinterpret_cast<bss_info *>(arg);
size_t count = 0;
UniqueSSIDCounter ssid_counter;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
count++;
uint8_t len = std::min(it->ssid_len, static_cast<uint8>(ESPHOME_MAX_SSID_LEN));
ssid_counter.add(reinterpret_cast<const char *>(it->ssid), len);
}
this->scan_result_.init(count);
this->scan_result_.init(count, ssid_counter.pool_size());
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;
#ifdef USE_WIFI_LISTENERS
for (auto *listener : global_wifi_component->scan_results_listeners_) {
listener->on_wifi_scan_results(global_wifi_component->scan_result_);
listener->on_wifi_scan_results(global_wifi_component->scan_result_.results());
}
#endif
}

View File

@@ -837,18 +837,27 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return;
}
scan_result_.init(number);
// Count unique SSIDs
UniqueSSIDCounter ssid_counter;
for (int i = 0; i < number; i++) {
const char *ssid = reinterpret_cast<const char *>(records[i].ssid);
ssid_counter.add(ssid, static_cast<uint8_t>(strlen(ssid)));
}
scan_result_.init(number, ssid_counter.pool_size());
for (int i = 0; i < number; i++) {
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_) {
listener->on_wifi_scan_results(this->scan_result_);
listener->on_wifi_scan_results(this->scan_result_.results());
}
#endif
@@ -1099,8 +1108,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

@@ -478,22 +478,29 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0)
return;
this->scan_result_.init(static_cast<unsigned int>(num));
// Count unique SSIDs
UniqueSSIDCounter ssid_counter;
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
ssid_counter.add(ssid.c_str(), static_cast<uint8_t>(ssid.length()));
}
this->scan_result_.init(static_cast<unsigned int>(num), ssid_counter.pool_size());
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);
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
for (auto *listener : this->scan_results_listeners_) {
listener->on_wifi_scan_results(this->scan_result_);
listener->on_wifi_scan_results(this->scan_result_.results());
}
#endif
}

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) {
@@ -247,7 +254,7 @@ void WiFiComponent::wifi_loop_() {
ESP_LOGV(TAG, "Scan done");
#ifdef USE_WIFI_LISTENERS
for (auto *listener : this->scan_results_listeners_) {
listener->on_wifi_scan_results(this->scan_result_);
listener->on_wifi_scan_results(this->scan_result_.results());
}
#endif
}

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';