[wifi] Drop a scan instead of aborting when its results cannot be allocated, filter ESP32 scans by SSID in the driver (#19254)

This commit is contained in:
J. Nick Koston
2026-09-14 09:49:13 +12:00
committed by GitHub
parent ce8fad1435
commit b1bfc512ac
7 changed files with 74 additions and 22 deletions
+3
View File
@@ -626,6 +626,9 @@ async def to_code(config):
networks = config.get(CONF_NETWORKS, [])
if networks:
cg.add(var.init_sta(len(networks)))
if len(networks) > 1:
# The ESP32 scan can filter one SSID in the driver; with several the whole list is kept
cg.add_define("USE_WIFI_MULTI_SSID")
def add_sta(ap: cg.MockObj, network: dict) -> None:
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
+3 -3
View File
@@ -1499,8 +1499,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
// A driver filtered scan saw one SSID; a portal that started during it still needs a full scan
this->has_completed_scan_after_captive_portal_start_ = !this->is_scan_driver_filtered_();
this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED;
if (this->scan_result_.empty()) {
@@ -2416,7 +2416,7 @@ void WiFiComponent::handle_driver_roam_(const bssid_t &bssid, uint8_t channel) {
void WiFiComponent::release_scan_results_() {
if (!this->keep_scan_results_) {
ScanResultsLock lock(this);
#if defined(USE_RP2) || defined(USE_ESP32)
#if defined(USE_RP2)
// std::vector - use swap trick since shrink_to_fit is non-binding
decltype(this->scan_result_)().swap(this->scan_result_);
#else
+11 -5
View File
@@ -178,12 +178,12 @@ struct EAPAuth {
using bssid_t = std::array<uint8_t, 6>;
/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID)
static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8;
// ESP32 with one configured network: the driver filters the scan by its SSID and only this many of
// its BSSIDs are kept, the strongest ones
static constexpr size_t WIFI_SCAN_RESULT_BOUND = 12;
// 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_RP2) || defined(USE_ESP32)
// RP2040's callback delivers results one at a time with no count, so it needs a growable vector
#if defined(USE_RP2)
template<typename T> using wifi_scan_vector_t = std::vector<T>;
#else
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
@@ -954,6 +954,12 @@ class WiFiComponent final : public Component {
uint8_t num_ipv6_addresses_{0};
#endif /* USE_NETWORK_IPV6 */
bool error_from_callback_{false};
#if defined(USE_ESP32) && !defined(USE_WIFI_MULTI_SSID)
bool scan_driver_filtered_{false};
bool is_scan_driver_filtered_() const { return this->scan_driver_filtered_; }
#else
constexpr bool is_scan_driver_filtered_() const { return false; }
#endif
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
// Platform-specific STA state enum, defined in platform cpp file.
// On ESP8266, written from SDK system context (wifi_event_callback) —
@@ -773,7 +773,11 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
}
}
this->scan_result_.init(count); // Exact allocation
if (!this->scan_result_.try_init(count)) {
ESP_LOGW(TAG, "No memory for %zu scan results", count);
this->scan_done_ = true;
return;
}
// Second pass: store matching networks
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
@@ -909,7 +909,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
uint16_t number = it.number;
bool needs_full = this->needs_full_scan_results_();
const bool filtered = this->is_scan_driver_filtered_();
const bool needs_full = this->needs_full_scan_results_();
{
// Mutate in place under the lock; blocking a portal request is fine and
// avoids scratch buffers
@@ -926,8 +927,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return;
}
// Smart reserve: full capacity if needed, small reserve otherwise
this->scan_result_.reserve(needs_full ? number : WIFI_SCAN_RESULT_FILTERED_RESERVE);
const size_t wanted = filtered ? std::min<size_t>(number, WIFI_SCAN_RESULT_BOUND) : number;
// Storage is reused across the scans of one retry cycle and freed on connect; an exhausted
// heap drops this scan and the retry logic scans again
if (this->scan_result_.capacity() < wanted && !this->scan_result_.try_init(wanted)) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "No memory for %zu scan results", wanted);
return;
}
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
@@ -955,22 +962,38 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
#endif // USE_ESP32_HOSTED
// 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());
if (!needs_full && !this->matches_configured_network_(ssid_cstr, record.bssid)) {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
continue;
}
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
if (this->scan_result_.size() < wanted) {
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);
continue;
}
// Records arrive in scan order, not by signal, so a bounded store keeps the strongest by
// replacing its weakest entry. Only SSID and signal decide here; a channel or auth constrained
// network hidden behind 12 stronger APs of its own SSID is not a real deployment
WiFiScanResult *weakest = &this->scan_result_[0];
for (auto &res : this->scan_result_) {
if (res.get_rssi() < weakest->get_rssi())
weakest = &res;
}
if (record.rssi <= weakest->get_rssi()) {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
continue;
}
// Rebuilt in place rather than assigned; assignment pulls in CompactString's operators, 104 B of flash
weakest->~WiFiScanResult();
new (weakest) WiFiScanResult(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
}
}
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
filtered ? LOG_STR_LITERAL(" (driver filtered)") : LOG_STR_LITERAL(""));
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
this->notify_scan_results_listeners_();
#endif
@@ -1047,6 +1070,16 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
wifi_scan_config_t config{};
config.ssid = nullptr;
config.bssid = nullptr;
#ifndef USE_WIFI_MULTI_SSID
// One configured network with an SSID: let the driver keep only its APs, so the WiFi library
// holds fewer records during the scan. Full results (portal, provisioning, listeners) and a
// network configured by BSSID alone still scan everything
this->scan_driver_filtered_ =
!this->needs_full_scan_results_() && this->sta_.size() == 1 && !this->sta_[0].get_ssid().empty();
if (this->scan_driver_filtered_) {
config.ssid = const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(this->sta_[0].get_ssid().c_str()));
}
#endif
config.channel = 0;
config.show_hidden = true;
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
@@ -709,7 +709,11 @@ void WiFiComponent::wifi_scan_done_callback_() {
}
}
this->scan_result_.init(count); // Exact allocation
if (!this->scan_result_.try_init(count)) {
ESP_LOGW(TAG, "No memory for %zu scan results", count);
WiFi.scanDelete();
return;
}
// Second pass: store matching networks
for (int i = 0; i < num; i++) {
+2
View File
@@ -298,6 +298,8 @@
#ifdef USE_ARDUINO
#define USE_PROMETHEUS
#define USE_WIFI_WPA2_EAP
// Kept in the Arduino block so clang-tidy sees both scan storage paths
#define USE_WIFI_MULTI_SSID
#endif
// Platforms with native 64-bit time sources (no rollover tracking needed)