[wifi] Add runtime suppression of post-connect roaming scans (#17012)

Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
Kevin Ahrendt
2026-06-18 09:31:49 -04:00
committed by GitHub
parent 1753ccd811
commit bf12af4645
5 changed files with 84 additions and 2 deletions

View File

@@ -764,6 +764,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression"
# Keys for listener counts # Keys for listener counts
IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners" IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners"
SCAN_RESULTS_LISTENERS_KEY = "wifi_scan_results_listeners" SCAN_RESULTS_LISTENERS_KEY = "wifi_scan_results_listeners"
@@ -794,6 +795,19 @@ def enable_runtime_power_save_control():
CORE.data[RUNTIME_POWER_SAVE_KEY] = True CORE.data[RUNTIME_POWER_SAVE_KEY] = True
def enable_runtime_roaming_suppression() -> None:
"""Enable runtime suppression of post-connect roaming scans.
Components that are disrupted by the radio briefly going off-channel during a
roaming scan (e.g., audio playback) should call this function during their code
generation. This enables the request_roaming_suppression() and
release_roaming_suppression() APIs, which pause periodic roaming scans while active.
Only supported on ESP32.
"""
CORE.data[RUNTIME_ROAMING_SUPPRESSION_KEY] = True
def request_wifi_ip_state_listener() -> None: def request_wifi_ip_state_listener() -> None:
"""Request an IP state listener slot.""" """Request an IP state listener slot."""
CORE.data[IP_STATE_LISTENERS_KEY] = CORE.data.get(IP_STATE_LISTENERS_KEY, 0) + 1 CORE.data[IP_STATE_LISTENERS_KEY] = CORE.data.get(IP_STATE_LISTENERS_KEY, 0) + 1
@@ -827,6 +841,8 @@ async def final_step():
) )
if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False):
cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE")
if CORE.data.get(RUNTIME_ROAMING_SUPPRESSION_KEY, False):
cg.add_define("USE_WIFI_RUNTIME_ROAMING_SUPPRESSION")
# Generate listener defines - each listener type has its own #ifdef # Generate listener defines - each listener type has its own #ifdef
ip_state_count = CORE.data.get(IP_STATE_LISTENERS_KEY, 0) ip_state_count = CORE.data.get(IP_STATE_LISTENERS_KEY, 0)

View File

@@ -822,7 +822,7 @@ void WiFiComponent::loop() {
} }
// else: scan in progress, wait // else: scan in progress, wait
} else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && } else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL) { now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) {
this->check_roaming_(now); this->check_roaming_(now);
} }
} }

View File

@@ -16,6 +16,8 @@
#endif #endif
#include "esphome/core/string_ref.h" #include "esphome/core/string_ref.h"
#include <atomic>
#include <limits>
#include <span> #include <span>
#include <string> #include <string>
#include <type_traits> #include <type_traits>
@@ -604,6 +606,49 @@ class WiFiComponent final : public Component {
bool release_high_performance(); bool release_high_performance();
#endif // USE_WIFI_RUNTIME_POWER_SAVE #endif // USE_WIFI_RUNTIME_POWER_SAVE
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
/** Request that post-connect roaming scans be suppressed.
*
* Components that are disrupted by the radio briefly going off-channel during a
* scan (e.g., audio playback) can call this to pause periodic roaming scans while
* active. Multiple components can request suppression simultaneously; roaming
* resumes once every requester has called release_roaming_suppression().
*
* A roaming scan already in progress is allowed to finish; this only prevents new
* roaming scans from starting. The roaming interval timer is not reset, so roaming
* resumes on the next loop once suppression is released (and the interval elapsed).
*
* Note: Only supported on ESP32.
*
* Thread-safe: may be called from any task.
*/
void request_roaming_suppression() {
uint8_t current = this->roaming_suppression_count_.load(std::memory_order_relaxed);
// CAS loop: saturate at max instead of wrapping, so an excess of requests can't roll the
// counter back to zero and unintentionally re-enable roaming.
while (current < std::numeric_limits<uint8_t>::max() &&
!this->roaming_suppression_count_.compare_exchange_weak(current, current + 1, std::memory_order_relaxed)) {
}
}
/** Release a roaming suppression request.
*
* Must be paired with a prior request_roaming_suppression() call. When all requests
* are released (count reaches zero), post-connect roaming resumes. A release with no
* outstanding request is ignored rather than underflowing the counter.
*
* Thread-safe: may be called from any task.
*/
void release_roaming_suppression() {
uint8_t current = this->roaming_suppression_count_.load(std::memory_order_relaxed);
// CAS loop: decrement only if non-zero, so an unmatched release can't wrap the counter
// and permanently suppress roaming.
while (current > 0 &&
!this->roaming_suppression_count_.compare_exchange_weak(current, current - 1, std::memory_order_relaxed)) {
}
}
#endif // USE_ESP32 && USE_WIFI_RUNTIME_ROAMING_SUPPRESSION
protected: protected:
#ifdef USE_WIFI_AP #ifdef USE_WIFI_AP
void setup_ap_config_(); void setup_ap_config_();
@@ -732,6 +777,15 @@ class WiFiComponent final : public Component {
void process_roaming_scan_(); void process_roaming_scan_();
void clear_roaming_state_(); void clear_roaming_state_();
/// Returns true if a component has requested that roaming scans be suppressed (e.g. during audio playback).
bool roaming_suppressed_() const {
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
return this->roaming_suppression_count_.load(std::memory_order_relaxed) != 0;
#else
return false;
#endif
}
/// Free scan results memory unless a component needs them /// Free scan results memory unless a component needs them
void release_scan_results_(); void release_scan_results_();
@@ -845,6 +899,13 @@ class WiFiComponent final : public Component {
// int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS)
int8_t selected_sta_index_{-1}; int8_t selected_sta_index_{-1};
uint8_t roaming_attempts_{0}; uint8_t roaming_attempts_{0};
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
// Count of active roaming-suppression requests. Incremented/decremented from any task
// (e.g. audio playback), read in loop(). Roaming scans are paused while non-zero.
// Relaxed ordering is sufficient: the count value is the only data shared across threads,
// so no happens-before relationship with other memory needs to be established.
std::atomic<uint8_t> roaming_suppression_count_{0};
#endif
#if USE_NETWORK_IPV6 #if USE_NETWORK_IPV6
uint8_t num_ipv6_addresses_{0}; uint8_t num_ipv6_addresses_{0};
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */

View File

@@ -312,6 +312,7 @@
#define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2
#define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2 #define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2
#define USE_WIFI_RUNTIME_POWER_SAVE #define USE_WIFI_RUNTIME_POWER_SAVE
#define USE_WIFI_RUNTIME_ROAMING_SUPPRESSION
#define USB_HOST_MAX_REQUESTS 16 #define USB_HOST_MAX_REQUESTS 16
#define USB_HOST_MAX_PACKET_SIZE 64 #define USB_HOST_MAX_PACKET_SIZE 64
#define USB_UART_OUTPUT_CHUNK_COUNT 5 #define USB_UART_OUTPUT_CHUNK_COUNT 5

View File

@@ -1,15 +1,19 @@
psram: psram:
# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define # Tests the high performance and roaming suppression request/release APIs;
# requires the USE_WIFI_RUNTIME_POWER_SAVE and USE_WIFI_RUNTIME_ROAMING_SUPPRESSION defines
esphome: esphome:
platformio_options: platformio_options:
build_flags: build_flags:
- "-DUSE_WIFI_RUNTIME_POWER_SAVE" - "-DUSE_WIFI_RUNTIME_POWER_SAVE"
- "-DUSE_WIFI_RUNTIME_ROAMING_SUPPRESSION"
on_boot: on_boot:
- then: - then:
- lambda: |- - lambda: |-
esphome::wifi::global_wifi_component->request_high_performance(); esphome::wifi::global_wifi_component->request_high_performance();
esphome::wifi::global_wifi_component->release_high_performance(); esphome::wifi::global_wifi_component->release_high_performance();
esphome::wifi::global_wifi_component->request_roaming_suppression();
esphome::wifi::global_wifi_component->release_roaming_suppression();
wifi: wifi:
use_psram: true use_psram: true