mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[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:
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user