diff --git a/Kconfig b/Kconfig
index dbfbcd98f06..7f97713dac2 100644
--- a/Kconfig
+++ b/Kconfig
@@ -852,6 +852,5 @@ mainmenu "Espressif IoT Development Framework Configuration"
- CONFIG_SPIRAM_SPEED_120M && CONFIG_SPIRAM_MODE_OCT
- CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH
- CONFIG_ESP_WIFI_EAP_TLS1_3
- - CONFIG_ESP_WIFI_ENABLE_ROAMING_APP
- CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS
- CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION
diff --git a/components/esp_wifi/Kconfig b/components/esp_wifi/Kconfig
index 66b8ac6b07e..cddc919aefa 100644
--- a/components/esp_wifi/Kconfig
+++ b/components/esp_wifi/Kconfig
@@ -710,19 +710,13 @@ menu "Wi-Fi"
Select this option to enable WiFi Multiband operation certification support.
config ESP_WIFI_ENABLE_ROAMING_APP
- bool "Advanced support for Wi-Fi Roaming (Experimental)"
- depends on IDF_EXPERIMENTAL_FEATURES
+ bool "Advanced support for Wi-Fi Roaming"
default n
help
Enable Espressif's roaming app to allow for efficient Wi-Fi roaming.
This includes configurable periodic environment scans, maintaining a cache of the
best APs, handling low rssi events etc.
- Risk Warning
- Please note that this feature is still experimental and enabling this potentially can
- lead to unpredictable scanning, connection and roaming attempts.
- We are still working on tuning and optimising this feature to ensure reliable and stable use.
-
menu "Configure roaming App"
depends on ESP_WIFI_ENABLE_ROAMING_APP
rsource "wifi_apps/roaming_app/src/Kconfig.roaming"
diff --git a/components/esp_wifi/remote/Kconfig.wifi.in b/components/esp_wifi/remote/Kconfig.wifi.in
index ddc35664e05..3d833da8f0c 100644
--- a/components/esp_wifi/remote/Kconfig.wifi.in
+++ b/components/esp_wifi/remote/Kconfig.wifi.in
@@ -689,19 +689,13 @@ config WIFI_RMT_MBO_SUPPORT
Select this option to enable WiFi Multiband operation certification support.
config WIFI_RMT_ENABLE_ROAMING_APP
- bool "Advanced support for Wi-Fi Roaming (Experimental)"
- depends on IDF_EXPERIMENTAL_FEATURES
+ bool "Advanced support for Wi-Fi Roaming"
default n
help
Enable Espressif's roaming app to allow for efficient Wi-Fi roaming.
This includes configurable periodic environment scans, maintaining a cache of the
best APs, handling low rssi events etc.
- Risk Warning
- Please note that this feature is still experimental and enabling this potentially can
- lead to unpredictable scanning, connection and roaming attempts.
- We are still working on tuning and optimising this feature to ensure reliable and stable use.
-
menu "Configure roaming App"
depends on WIFI_RMT_ENABLE_ROAMING_APP
rsource "wifi_apps/roaming_app/src/Kconfig.roaming"
diff --git a/components/esp_wifi/remote/Kconfig.wifi_is_remote.in b/components/esp_wifi/remote/Kconfig.wifi_is_remote.in
index e82fe9ecac5..426dd148ffa 100644
--- a/components/esp_wifi/remote/Kconfig.wifi_is_remote.in
+++ b/components/esp_wifi/remote/Kconfig.wifi_is_remote.in
@@ -360,7 +360,6 @@ endif
if WIFI_RMT_ENABLE_ROAMING_APP
config ESP_WIFI_ENABLE_ROAMING_APP # ignore: multiple-definition
bool
- depends on IDF_EXPERIMENTAL_FEATURES
default WIFI_RMT_ENABLE_ROAMING_APP
endif
diff --git a/components/esp_wifi/sdkconfig.rename b/components/esp_wifi/sdkconfig.rename
index c4b403e54bd..3700bdbc2dd 100644
--- a/components/esp_wifi/sdkconfig.rename
+++ b/components/esp_wifi/sdkconfig.rename
@@ -43,3 +43,4 @@ CONFIG_WPA_MBO_SUPPORT CONFIG_ESP_WIFI_MBO_SUPPORT
CONFIG_WPA_DPP_SUPPORT CONFIG_ESP_WIFI_DPP_SUPPORT
CONFIG_WPA_11R_SUPPORT CONFIG_ESP_WIFI_11R_SUPPORT
CONFIG_WPA_WPS_SOFTAP_REGISTRAR CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR
+CONFIG_ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP CONFIG_ESP_WIFI_ROAMING_IP_RENEW_SKIP
diff --git a/components/esp_wifi/src/wifi_default.c b/components/esp_wifi/src/wifi_default.c
index 3773618cdfb..330f2c5c34c 100644
--- a/components/esp_wifi/src/wifi_default.c
+++ b/components/esp_wifi/src/wifi_default.c
@@ -29,7 +29,7 @@ static esp_netif_t *s_wifi_netifs[MAX_WIFI_IFS] = { NULL };
static bool wifi_default_handlers_set = false;
static esp_err_t disconnect_and_destroy(esp_netif_t* esp_netif);
-#ifdef CONFIG_ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP
+#ifdef CONFIG_ESP_WIFI_ROAMING_IP_RENEW_SKIP
static bool roaming_ongoing = false;
#endif
@@ -85,7 +85,7 @@ static void wifi_default_action_sta_stop(void *arg, esp_event_base_t base, int32
{
#ifdef CONFIG_ESP_WIFI_ENABLE_ROAMING_APP
roam_disable_reconnect();
-#ifdef CONFIG_ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP
+#ifdef CONFIG_ESP_WIFI_ROAMING_IP_RENEW_SKIP
roaming_ongoing = false;
#endif
#endif /* CONFIG_ESP_WIFI_ENABLE_ROAMING_APP */
@@ -98,7 +98,7 @@ static void wifi_default_action_sta_connected(void *arg, esp_event_base_t base,
{
#if CONFIG_ESP_WIFI_ENABLE_ROAMING_APP
roam_sta_connected();
-#ifdef CONFIG_ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP
+#ifdef CONFIG_ESP_WIFI_ROAMING_IP_RENEW_SKIP
if (roaming_ongoing) {
/* IP stack is already in ready state */
roaming_ongoing = false;
@@ -127,7 +127,7 @@ static void wifi_default_action_sta_disconnected(void *arg, esp_event_base_t bas
{
#if CONFIG_ESP_WIFI_ENABLE_ROAMING_APP
roam_sta_disconnected(data);
-#ifdef CONFIG_ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP
+#ifdef CONFIG_ESP_WIFI_ROAMING_IP_RENEW_SKIP
wifi_event_sta_disconnected_t *disconn = data;
if (disconn->reason == WIFI_REASON_ROAMING) {
roaming_ongoing = true;
diff --git a/components/esp_wifi/wifi_apps/roaming_app/include/esp_roaming.h b/components/esp_wifi/wifi_apps/roaming_app/include/esp_roaming.h
index 28ccb7a79e1..d3d7acc4daa 100644
--- a/components/esp_wifi/wifi_apps/roaming_app/include/esp_roaming.h
+++ b/components/esp_wifi/wifi_apps/roaming_app/include/esp_roaming.h
@@ -6,10 +6,15 @@
#pragma once
+#include "esp_wifi.h"
+
#ifdef __cplusplus
extern "C" {
#endif
+#define ROAM_SCAN_FILTER_SSID_LEN (MAX_SSID_LEN + 1)
+#define ROAM_SCAN_FILTER_BSSID_LEN 6
+
struct roam_config {
uint8_t backoff_time;
bool low_rssi_roam_trigger;
@@ -28,6 +33,9 @@ struct roam_config {
uint8_t rrm_monitor_time;
int8_t rrm_monitor_rssi_threshold;
wifi_scan_config_t scan_config;
+ uint8_t scan_filter_ssid[ROAM_SCAN_FILTER_SSID_LEN];
+ uint8_t scan_filter_bssid[ROAM_SCAN_FILTER_BSSID_LEN];
+ bool scan_filter_bssid_set;
};
void roam_init_app(void);
@@ -38,6 +46,10 @@ void roam_sta_connected(void);
void roam_sta_disconnected(void *disconn);
esp_err_t roam_get_config_params(struct roam_config *config);
esp_err_t roam_set_config_params(struct roam_config *config);
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+esp_err_t esp_wifi_blacklist_add(const uint8_t *bssid);
+esp_err_t esp_wifi_blacklist_remove(const uint8_t *bssid);
+#endif
#if CONFIG_ESP_WIFI_ENABLE_ROAMING_APP
void esp_wifi_roaming_set_current_bssid(const uint8_t *bssid);
diff --git a/components/esp_wifi/wifi_apps/roaming_app/src/Kconfig.roaming b/components/esp_wifi/wifi_apps/roaming_app/src/Kconfig.roaming
index 57bc23649a5..d566a902360 100644
--- a/components/esp_wifi/wifi_apps/roaming_app/src/Kconfig.roaming
+++ b/components/esp_wifi/wifi_apps/roaming_app/src/Kconfig.roaming
@@ -90,13 +90,13 @@ menu "Roaming Methods"
Retry threshold after which the station should stop using Network Assisted
roaming methods and start using legacy roaming instead.
- config ESP_WIFI_NETWORK_ASSISTED_ROAMING_IP_RENEW_SKIP
- bool "Skip IP renew during BTM based roaming"
- depends on ESP_WIFI_ROAMING_NETWORK_ASSISTED_ROAM
+ config ESP_WIFI_ROAMING_IP_RENEW_SKIP
+ bool "Skip IP renew after roaming"
default n
help
- Station will not ask for IP renew after a BTM based roaming. Before enabling please
- make sure your network supports this.
+ Skip the default IP renew path when the Wi-Fi stack reports a roaming event.
+ Enable this only if your network preserves IP state across a roam, for example
+ during 802.11r or BTM-based roaming.
endmenu #"Roaming Methods"
@@ -129,7 +129,9 @@ menu "Scan Configuration"
default "None"
help
Channels your wireless network operates on to allow for faster scanning.
- Specify the channels(between 1-14) in a comma separated manner.
+ Specify channels in a comma-separated manner. The list is used as a
+ channel filter, not as an ordered scan sequence. When 5 GHz support
+ is enabled, valid 5 GHz channels are also accepted.
config ESP_WIFI_ROAMING_SCAN_EXPIRY_WINDOW
int "Scan results expiry window (in seconds)"
@@ -214,12 +216,4 @@ menu "Blacklist Configuration"
help
Maximum number of roaming candidates to consider. This also defines the size of the blacklist.
- config ESP_WIFI_ROAMING_PREVENT_DOWNGRADE
- bool "Prevent security downgrade when roaming"
- default n
- help
- If the currently connected AP sends a "transition disable" bit,
- this option will make the roaming logic ignore less secure APs.
- This helps prevent security downgrades when roaming in a mixed
- security environment (e.g., WPA2/WPA3).
endmenu # "Blacklist Configuration"
diff --git a/components/esp_wifi/wifi_apps/roaming_app/src/README.md b/components/esp_wifi/wifi_apps/roaming_app/src/README.md
index c8ad2f37e7b..4e8c69b4794 100644
--- a/components/esp_wifi/wifi_apps/roaming_app/src/README.md
+++ b/components/esp_wifi/wifi_apps/roaming_app/src/README.md
@@ -1,6 +1,8 @@
**Introduction**
-The advanced Wi-Fi roaming app has been written with the intention of simplifying the process of developing applications that will function in a network environment that allows for roaming between the service areas of multiple compatible APs. It gathers basic approaches about how different roaming mechanisms and APIs can be integrated for an efficient solution to roaming and bundles into an easy to user yet highly configurable module.
+The advanced Wi-Fi roaming app has been written with the intention of simplifying the process of developing applications that will function in a network environment that allows for roaming between the service areas of multiple compatible APs. It gathers basic approaches about how different roaming mechanisms and APIs can be integrated for an efficient solution to roaming and bundles into an easy to use yet highly configurable module.
+
+For runtime flow and configuration details, see [ROAMING_FLOW.md](ROAMING_FLOW.md).
**How to use:**
@@ -9,26 +11,26 @@ To enable the roaming app in the menuconfig, please navigate to Component Settin
**Configuring the advanced Wi-Fi roaming app :**
-After enabling the roaming app in the menuconfig, this roaming app can be configured to best suit your application requirements and the network environment. The configurations are classified into Roaming Triggers (better understood as “Under what conditions to roam?”), Roaming methods (better understood as “How to Roam’), and then some additional configurations such as scanning parameters, backoff times, periodic neighbor report requests etc.
+After enabling the roaming app in the menuconfig, this roaming app can be configured to best suit your application requirements and the network environment. The configurations are classified into Roaming Triggers (better understood as “Under what conditions to roam?”), Roaming methods (better understood as “How to roam?”), and then some additional configurations such as scanning parameters, backoff times, periodic neighbor report requests, blacklisting, etc.
**Roaming Triggers: (Roaming Module Settings --> Roaming Triggers)**
-There are broadly two different Roaming triggers you can choose from:
+There are broadly two different roaming triggers you can choose from:
1) Low RSSI triggered roaming :
If enabled, in this method the roaming app sets a Wi-Fi Threshold (configured by “Wi-Fi RSSI threshold to trigger roaming”), which when reached by the connection to the current AP, triggers a check for a better AP and if found will trigger roaming.
-Every time the threshold is reached, a new threshold needs to be set at an even lower rssi threshold since if we fail to find a better AP the first time the RSSI threshold is reached there will be no further attempts to find better APs leading to possible disconnection as the only avenue for finding a better AP. The offset by which the next RSSI threshold can be set is decided by “Wi-Fi RSSI threshold to trigger roaming” which defaults to 5.
+Every time the threshold is reached, a new threshold needs to be set at an even lower RSSI threshold since if we fail to find a better AP the first time the RSSI threshold is reached there will be no further attempts to find better APs, leading to possible disconnection as the only avenue for finding a better AP. The offset by which the next RSSI threshold can be set is decided by “Offset by which to reset the RSSI Threshold after attempt to roam” which defaults to 5.
-Also please note that if the AP we connect to, upon connecting itself has a worse RSSI than the threshold set in the configuration, the new RSSI threshold is set to the current RSSI - offset. Additionally, the RSSI threshold gets reset to the configured value if the RSSI ever goes below the configured threshold by the offset amount.
+Also please note that if the AP we connect to, upon connecting, itself has a worse RSSI than the threshold set in the configuration, the new RSSI threshold is set to the current RSSI - offset. Additionally, the RSSI threshold gets reset to the configured value if the RSSI later improves above the configured threshold.
2) Periodic Scan based roaming:
-Unlike Low RSSI triggered roaming, which is a bit reactive to the changing network environment, periodic scanning-based roaming (enabled by “Conduct periodic scans to check if a better AP is available”) allows for a more active approach to ensuring that you are connected to the best AP in the network. You can also decide the threshold after which you want to conduct the periodic scans with a default of –20dbm. (Configured by “Threshold at which to begin periodic scanning for a better AP”)
+Unlike Low RSSI triggered roaming, which is a bit reactive to the changing network environment, periodic scanning-based roaming (enabled by “Conduct periodic scans to check if a better AP is available”) allows for a more active approach to ensuring that you are connected to the best AP in the network. You can also decide the threshold after which you want to conduct the periodic scans with a default of -50 dBm. (Configured by “Threshold at which to begin periodic scanning for a better AP”)
-The intervals at which this periodic scan will take place can also be configured through “Time intervals at which station will initiate a scan”. This defaults to 30 seconds.The RSSI difference between a candidate AP and the current AP, which can be considered as acceptable to initiate roaming can also be configured as the “RSSI difference b/w current AP and a candidate AP to initiate roaming”
+The intervals at which this periodic scan will take place can also be configured through “Time intervals at which station will initiate a scan”. This defaults to 30 seconds. The RSSI difference between a candidate AP and the current AP, which can be considered as acceptable to initiate roaming, can also be configured as the “RSSI difference b/w current AP and a candidate AP to initiate roaming”
**Please note that at least one of the two Roaming Triggers needs to be enabled.**
@@ -39,9 +41,9 @@ Currently 2 methods of roaming are supported by the roaming app. (Roaming App Se
1) Network Assisted Roaming:
-Enabled by “Support Network Assisted Roaming using 802.11v”, this method primarily uses the BSS transition Management mechanisms outlined in IEEE 802.11v. It uses Candidates received from neighbor report requests (if enabled, explained later.) and scanning results to Send BSS transition Management Queries to the AP it is currently associated to. Depending on the current radio environment and vendor implementation on the side of the AP, this could then lead to BSS Transition Management Requests and corresponding BSS Transition Management responses which could lead to a seamless transition from one AP to another. For a better understanding of the mechanisms involved and the general implementation please look up the IEEE 802.11v specification and upstream wpa_supplicant’s implementation.
+Enabled by “Support Network Assisted Roaming using 802.11v”, this method primarily uses the BSS transition Management mechanisms outlined in IEEE 802.11v. It uses candidates received from neighbor report requests (if enabled, explained later) and scanning results to send BSS transition Management Queries to the AP it is currently associated to. Depending on the current radio environment and vendor implementation on the side of the AP, this could then lead to BSS Transition Management Requests and corresponding BSS Transition Management responses which could lead to a seamless transition from one AP to another. For a better understanding of the mechanisms involved and the general implementation please look up the IEEE 802.11v specification and upstream wpa_supplicant’s implementation.
-Please note that for this to work as expected, the APs should support 802.11k & 802.11v and be setup in a network where they are aware of each other.
+Please note that for this to work as expected, the APs should be setup in a network where they are aware of each other. If the network is expected to preserve IP configuration across a roaming event, `Skip IP renew after roaming` can also be enabled. This is relevant only when the roam keeps the same IP state, such as 802.11r or BTM-based roaming.
2) Legacy Roaming approach.
@@ -58,7 +60,7 @@ The scan configuration allows for configuring the parameters for the scans that
The minimum and maximum active scanning duration for each channel in milliseconds can be configured through “Minimum duration of active scan time for a station” & “Maximum duration of active scan time for a station”. If connected, the “Home channel dwell time between scanning consecutive channels” configuration decides for how long the station will return to the home channel for Tx of various frames in the buffer, and Rx of frames buffered by the AP.
-Additionally, if channels of operation of the APs that the application designed will work with is known, you can provide this information as an ordered list of comma-separated channels. (configured using “Preferred channel list for scanning”) (for e.g. 1,6,9,11) Only those channels will be scanned in the order mentioned. Keeping in mind that network discovery/scanning is the process that takes up most of the time in roaming as well connecting to an AP, providing such a list could significantly increase the efficiency of the roaming app and process.
+Additionally, if channels of operation of the APs that the application is designed to work with are known, you can provide this information as a comma-separated list of channels. (configured using “Preferred channel list for scanning”) (for e.g. 1,6,9,11) Only those channels will be scanned, but the list is treated as a filter and not as an ordered scan sequence. Keeping in mind that network discovery/scanning is the process that takes up most of the time in roaming as well as connecting to an AP, providing such a list could significantly increase the efficiency of the roaming app and process.
This module can trigger scans due to several reasons. The configuration “Scan results expiry window” decides the duration for which different modules will use the most recent scan results instead of triggering a new scan of their own.
@@ -73,17 +75,28 @@ Successive roaming attempts by multiple roaming triggers simultaneously could le
Neighbor Report requests are a part of the IEEE 802.11k specification, and hence the intended network would need to support and be setup in a way to support its mechanisms. Periodic neighbor report requests provide vital information about the network and other APs in the vicinity that are candidate APs of the same network. This can be enabled using “Send Periodic Neighbor Report requests for updating the internal list”. The frequency of these requests is controlled by “Time interval between Periodic Neighbor report Requests”. There can also be a RSSI threshold (“Threshold for sending periodic neighbor report requests”) after which you wish to consider sending these requests however this is set by default to -20.
+**Blacklist and security behavior:**
+
+`Enable BSSID blacklisting` allows the app to skip specific BSSIDs during candidate selection. Entries can be added manually through `esp_wifi_blacklist_add()` and `esp_wifi_blacklist_remove()`.
+
+If `Enable automatic BSSID blacklisting` is enabled, a BSSID is blacklisted after repeated connection or authentication failures. Blacklisted entries are removed again after `Blacklist timeout (in seconds)`.
+
+The roaming logic also honors AP-advertised transition disable policy while selecting candidates and prevents roaming to candidates that do not meet the authmode threshold set by `wifi_config_t.sta`.
+
+
**Notes :**
1) Advanced roaming support is disabled by default.
-2) When enabling the advanced roaming support , it is expected that the bssid to connect to is not specifically set by the application. This would defeat the purpose of roaming between different APs of the network. Hence if the BSSID is set (in wifi_config_t.sta) , it will be unset at the first disconnection/connection.
+2) When enabling the advanced roaming support, it is expected that the BSSID to connect to is not specifically set by the application. This would defeat the purpose of roaming between different APs of the network. The roaming app may temporarily set BSSID/channel hints for a directed reconnect, but these hints are cleared again so normal roaming can continue.
-3) For roaming to work as expected, the APs between which the station is expected to roam must have the same or compatible authmode. These include :
+3) For roaming to work as expected, the APs between which the station is expected to roam must have the same or compatible authmode. These include :
Open <--> OWE
- PSK based authmodes <--> PSK based authmodes (Does not include WEP)
+ PSK based authmodes <--> PSK based authmodes (Does not include WEP)
+
+ `WAPI_PSK` <--> legacy WPA/WPA2 PSK authmodes
Enterprise <--> Enterprise.
diff --git a/components/esp_wifi/wifi_apps/roaming_app/src/ROAMING_FLOW.md b/components/esp_wifi/wifi_apps/roaming_app/src/ROAMING_FLOW.md
new file mode 100644
index 00000000000..a703062c6f6
--- /dev/null
+++ b/components/esp_wifi/wifi_apps/roaming_app/src/ROAMING_FLOW.md
@@ -0,0 +1,201 @@
+# Roaming App Flow
+
+## Scope
+
+This document describes the runtime control flow of the advanced Wi-Fi roaming app and identifies the configuration options that influence each stage. Each diagram focuses on one stage of the app, and the table below it lists the related configuration options.
+
+## Terminology
+
+- `AP`: Access Point.
+- `BSSID`: MAC address of one AP radio.
+- `Candidate AP`: A scanned AP that is still under consideration for roaming.
+- `BTM`: Network-assisted roaming using IEEE 802.11v BSS Transition Management.
+- `RRM / Neighbor Report`: Information about neighboring APs obtained using IEEE 802.11k.
+- `Legacy roaming`: A directed reconnect initiated by the roaming app.
+- `Blacklist`: A temporary deny-list of BSSIDs that the app should avoid.
+
+## Initialization Preconditions
+
+The roaming app starts only if both of the following are true:
+
+- At least one roaming trigger is enabled.
+- At least one roaming method is enabled.
+
+If either condition is not satisfied, `roam_init_app()` returns without starting roaming support.
+
+## 1. High-Level Flow
+
+The following diagram summarizes the complete roaming flow before the later sections break it down by stage.
+
+```mermaid
+flowchart TD
+ A["roam_init_app()"] --> B{"Triggers and methods enabled?"}
+ B -- "No" --> X["Return without starting roaming support"]
+ B -- "Yes" --> C["Wait for station connection"]
+ C --> D["On connection, apply roaming configuration
and start enabled monitors"]
+
+ D --> E{"Runtime event"}
+ E -- "Low RSSI or periodic scan" --> F["Find the best candidate AP"]
+ E -- "Periodic RRM timer" --> G["Refresh cached neighbor list for BTM"]
+ G --> E
+ E -- "Disconnect event" --> H{"Reconnect allowed?"}
+ H -- "No" --> I["No action"]
+ H -- "Yes" --> J{"After disconnect handling,
is the disconnected BSSID blacklisted now?"}
+ J -- "No" --> K["Use default reconnect path"]
+ J -- "Yes" --> F
+
+ F --> L{"Reuse scan cache?"}
+ L -- "Yes" --> M["Evaluate scanned APs"]
+ L -- "No" --> N["Run a new scan and evaluate APs"]
+ M --> O{"Candidate selected?"}
+ N --> O
+
+ O -- "No, still connected" --> P["Stay on current AP"]
+ O -- "No, disconnected recovery" --> Q["Schedule retry with a fresh scan"]
+ O -- "Yes" --> R{"How to roam?"}
+
+ R -- "BTM query can be used" --> S["Send BTM query and wait for AP or network action"]
+ R -- "Directed reconnect is used" --> T["Start reconnect to the selected AP"]
+ R -- "No method available" --> U["No roam action"]
+
+ P --> E
+ Q --> E
+ S --> E
+ T --> E
+ K --> E
+```
+
+## 2. Initialization and Monitoring
+
+```mermaid
+flowchart TD
+ A["roam_init_app()"] --> B{"At least one trigger enabled?"}
+ B -- "No" --> X["Return without starting roaming support"]
+ B -- "Yes" --> C{"At least one roaming method enabled?"}
+ C -- "No" --> X
+ C -- "Yes" --> D["Load default configuration"]
+ D --> E["Register Wi-Fi event handlers"]
+ E --> F["Wait for station connection"]
+
+ F --> G["On connection: read AP state,
refresh scan filters, apply runtime config,
and arm enabled monitors"]
+
+ G --> H{"Runtime event"}
+ H -- "Low RSSI" --> I["Candidate discovery"]
+ H -- "Periodic scan timer" --> I
+ H -- "Periodic RRM timer" --> J["Send neighbor report request"]
+ J --> K["Update cached BTM neighbor list"]
+ K --> H
+ H -- "Disconnect event" --> L{"Reconnect allowed?"}
+ L -- "No" --> M["No action"]
+ L -- "Yes" --> N{"After disconnect handling,
is the disconnected BSSID blacklisted now?"}
+ N -- "Yes" --> I
+ N -- "No" --> O["Default reconnect"]
+```
+
+### Configuration used in this stage
+
+| Area | Configuration |
+| --- | --- |
+| Low RSSI trigger | `ESP_WIFI_ROAMING_LOW_RSSI_ROAMING`, `ESP_WIFI_ROAMING_LOW_RSSI_THRESHOLD`, `ESP_WIFI_ROAMING_LOW_RSSI_OFFSET` |
+| Periodic scan trigger | `ESP_WIFI_ROAMING_PERIODIC_SCAN_MONITOR`, `ESP_WIFI_ROAMING_PERIODIC_SCAN_THRESHOLD`, `ESP_WIFI_ROAMING_SCAN_MONITOR_INTERVAL`, `ESP_WIFI_ROAMING_SCAN_ROAM_RSSI_DIFF` |
+| Periodic RRM | `ESP_WIFI_ROAMING_PERIODIC_RRM_MONITORING`, `ESP_WIFI_ROAMING_RRM_MONITOR_TIME`, `ESP_WIFI_ROAMING_RRM_MONITOR_THRESHOLD` |
+| Blacklist handling | `ESP_WIFI_ROAMING_BSSID_BLACKLIST`, `ESP_WIFI_ROAMING_AUTO_BLACKLISTING`, `ESP_WIFI_ROAMING_MAX_CONN_FAILURES`, `ESP_WIFI_ROAMING_BLACKLIST_TIMEOUT` |
+| Runtime overrides | `roam_set_config_params()`, `scan_filter_ssid`, `scan_filter_bssid` |
+
+## 3. Candidate Discovery and Selection
+
+```mermaid
+flowchart TD
+ A["determine_best_ap(threshold)"] --> A1{"Can the app act on a roam candidate now?"}
+ A1 -- "No" --> O["Stay on current AP"]
+ A1 -- "Yes" --> B{"Recent scan cache still valid?"}
+ B -- "Yes" --> C["Reuse cached scan results"]
+ B -- "No" --> D["Run a new scan"]
+ C --> E["Evaluate scanned APs"]
+ D --> E
+
+ E --> F{"Station connected?"}
+ F -- "Yes" --> G["Set baseline RSSI from current connection state"]
+ F -- "No" --> H["Use disconnected recovery rules"]
+ G --> I["Skip blacklisted BSSIDs"]
+ H --> I
+
+ I --> J["Apply security policy"]
+ J --> K["Compare RSSI delta with threshold"]
+ K --> L{"Candidate selected?"}
+
+ L -- "Yes" --> M{"Still able to act on this candidate?"}
+ M -- "Yes" --> Q["Post roam event"]
+ M -- "No" --> O
+ L -- "No" --> N{"Station connected?"}
+ N -- "Yes" --> O["Stay on current AP"]
+ N -- "No" --> P["Schedule retry and invalidate scan cache"]
+```
+
+### Configuration used in this stage
+
+| Area | Configuration |
+| --- | --- |
+| Scan timing | `ESP_WIFI_ROAMING_SCAN_MIN_SCAN_TIME`, `ESP_WIFI_ROAMING_SCAN_MAX_SCAN_TIME`, `ESP_WIFI_ROAMING_HOME_CHANNEL_DWELL_TIME` |
+| Scan filter and scope | `ESP_WIFI_ROAMING_SCAN_CHAN_LIST`, `scan_filter_ssid`, `scan_filter_bssid` |
+| Scan cache reuse | `ESP_WIFI_ROAMING_SCAN_EXPIRY_WINDOW` |
+| Blacklist filter | `ESP_WIFI_ROAMING_BSSID_BLACKLIST`, `ESP_WIFI_ROAMING_BLACKLIST_TIMEOUT`, `ESP_WIFI_ROAMING_MAX_CANDIDATES` |
+| Security policy | `wifi_config_t.sta.threshold.authmode`, `wifi_config_t.sta.owe_enabled`, `wifi_config_t.sta.pmf_cfg.required`, `wifi_config_t.sta.sae_pwe_h2e`, `wifi_config_t.sta.sae_pk_mode` |
+| Periodic scan threshold input | `ESP_WIFI_ROAMING_SCAN_ROAM_RSSI_DIFF` |
+
+## 4. Roam Execution
+
+```mermaid
+flowchart TD
+ A["roaming_app_trigger_roam(candidate)"] --> B{"Station connected?"}
+ B -- "No" --> C["Start reconnect to selected AP"]
+ B -- "Yes" --> D{"Inside backoff window?"}
+ D -- "Yes" --> X["Ignore request"]
+ D -- "No" --> E{"BTM enabled and supported by current AP?"}
+
+ E -- "Yes" --> F{"BTM retries remaining?"}
+ F -- "Yes" --> G{"Submit BTM query?"}
+ G -- "Yes" --> H["Send BTM query with selected candidate
and cached neighbor list"]
+ H --> I["Wait for AP or network action"]
+ G -- "No" --> J{"Legacy roaming enabled?"}
+ F -- "No" --> J
+
+ E -- "No" --> J
+ J -- "Yes" --> K["Start directed reconnect to selected AP"]
+ J -- "No" --> L["No roam action"]
+```
+
+### Configuration used in this stage
+
+| Area | Configuration |
+| --- | --- |
+| Backoff | `ESP_WIFI_ROAMING_BACKOFF_TIME` |
+| BTM path | `ESP_WIFI_ROAMING_NETWORK_ASSISTED_ROAM`, `ESP_WIFI_NETWORK_ASSISTED_ROAMING_RETRY_COUNT` |
+| Legacy path | `ESP_WIFI_ROAMING_LEGACY_ROAMING` |
+| Post-roam IP behavior | `ESP_WIFI_ROAMING_IP_RENEW_SKIP` |
+
+## 5. Configuration Summary by Functional Area
+
+| Functional area | Configuration |
+| --- | --- |
+| Low RSSI roaming | `ESP_WIFI_ROAMING_LOW_RSSI_ROAMING`, `ESP_WIFI_ROAMING_LOW_RSSI_THRESHOLD`, `ESP_WIFI_ROAMING_LOW_RSSI_OFFSET` |
+| Periodic scan roaming | `ESP_WIFI_ROAMING_PERIODIC_SCAN_MONITOR`, `ESP_WIFI_ROAMING_PERIODIC_SCAN_THRESHOLD`, `ESP_WIFI_ROAMING_SCAN_MONITOR_INTERVAL`, `ESP_WIFI_ROAMING_SCAN_ROAM_RSSI_DIFF` |
+| Network-assisted roaming | `ESP_WIFI_ROAMING_NETWORK_ASSISTED_ROAM`, `ESP_WIFI_NETWORK_ASSISTED_ROAMING_RETRY_COUNT` |
+| Post-roam IP behavior | `ESP_WIFI_ROAMING_IP_RENEW_SKIP` |
+| Legacy roaming | `ESP_WIFI_ROAMING_LEGACY_ROAMING` |
+| Periodic RRM / neighbor reports | `ESP_WIFI_ROAMING_PERIODIC_RRM_MONITORING`, `ESP_WIFI_ROAMING_RRM_MONITOR_TIME`, `ESP_WIFI_ROAMING_RRM_MONITOR_THRESHOLD` |
+| Scan behavior | `ESP_WIFI_ROAMING_SCAN_MIN_SCAN_TIME`, `ESP_WIFI_ROAMING_SCAN_MAX_SCAN_TIME`, `ESP_WIFI_ROAMING_HOME_CHANNEL_DWELL_TIME`, `ESP_WIFI_ROAMING_SCAN_CHAN_LIST`, `ESP_WIFI_ROAMING_SCAN_EXPIRY_WINDOW` |
+| Blacklist behavior | `ESP_WIFI_ROAMING_BSSID_BLACKLIST`, `ESP_WIFI_ROAMING_AUTO_BLACKLISTING`, `ESP_WIFI_ROAMING_MAX_CONN_FAILURES`, `ESP_WIFI_ROAMING_BLACKLIST_TIMEOUT`, `ESP_WIFI_ROAMING_MAX_CANDIDATES` |
+| Runtime scan filters | `roam_set_config_params()`, `scan_filter_ssid`, `scan_filter_bssid` |
+| Candidate security policy | `wifi_config_t.sta.threshold.authmode`, `wifi_config_t.sta.owe_enabled`, `wifi_config_t.sta.pmf_cfg.required`, `wifi_config_t.sta.sae_pwe_h2e`, `wifi_config_t.sta.sae_pk_mode` |
+
+## 6. Behavioral Notes
+
+- A periodic RRM event does not directly trigger roaming. Its purpose is to refresh the cached neighbor list used by the BTM path.
+- While connected, candidate ranking uses the stronger of two values: the RSSI from the current connection and the RSSI for the same BSSID in the latest scan results.
+- After a disconnect, the app checks whether the disconnected BSSID is blacklisted. This covers both BSSIDs that were already blacklisted and BSSIDs that become blacklisted because the latest failure reached the configured threshold.
+- After a disconnect, the app does not compare other APs against the failed AP's scan RSSI. This lets it choose any valid AP that is not blacklisted.
+- Candidate security checks allow roaming only within supported compatibility groups: Open/OWE, Personal, and Enterprise. Transition-disable policy and station security requirements are still enforced.
+- A BTM query is asynchronous. After the query is sent, the app waits for AP or network action. It does not immediately force legacy roaming in the same step.
+- The reconnect paths start a connection attempt. Final success or failure is reported later through the normal Wi-Fi connection and disconnection events.
+- If the station is already disconnected, the app can still reconnect to a selected AP even when legacy roaming is disabled.
diff --git a/components/esp_wifi/wifi_apps/roaming_app/src/esp_roaming_i.h b/components/esp_wifi/wifi_apps/roaming_app/src/esp_roaming_i.h
index 29d3effc5b3..1a17f431401 100644
--- a/components/esp_wifi/wifi_apps/roaming_app/src/esp_roaming_i.h
+++ b/components/esp_wifi/wifi_apps/roaming_app/src/esp_roaming_i.h
@@ -89,17 +89,6 @@ extern "C" {
#define MAX_NEIGHBOR_LEN 512
-#define IS_PSK(authmode) \
- (((authmode == WIFI_AUTH_WPA_PSK) || (authmode == WIFI_AUTH_WPA2_PSK) || \
- (authmode == WIFI_AUTH_WPA_WPA2_PSK) || (authmode == WIFI_AUTH_WPA3_PSK) || \
- (authmode == WIFI_AUTH_WPA2_WPA3_PSK) || (authmode == WIFI_AUTH_WAPI_PSK) ? 1 : 0))
-
-#define OWE_COMPATIBLE(curr_auth, cand_auth) \
- ((((curr_auth == WIFI_AUTH_OPEN) || (curr_auth == WIFI_AUTH_OWE)) && ((cand_auth == WIFI_AUTH_OPEN) || (cand_auth == WIFI_AUTH_OWE)))? 1 : 0)
-
-#define PSK_COMPATIBLE(curr_auth, cand_auth) \
- ((IS_PSK(curr_auth) && IS_PSK(cand_auth)) ? 1 : 0)
-
struct scanned_ap_info {
uint16_t current_count;
struct timeval time;
@@ -118,10 +107,14 @@ struct roam_bss_info {
struct roaming_app {
struct roam_config config;
+ bool app_active;
bool scan_ongoing;
+ bool sta_connected;
+ bool connect_hint_active;
int8_t current_rssi_threshold;
char *btm_neighbor_list;
- struct timeval last_roamed_time;
+ struct timeval last_roam_attempt_time;
+ struct timeval last_roam_success_time;
struct scanned_ap_info scanned_aps;
struct roam_bss_info current_bss;
@@ -144,6 +137,7 @@ struct roaming_app {
#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
struct blacklist_entry {
uint8_t bssid[ETH_ALEN];
+ bool manual;
#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
uint8_t failures;
#endif
diff --git a/components/esp_wifi/wifi_apps/roaming_app/src/roaming_app.c b/components/esp_wifi/wifi_apps/roaming_app/src/roaming_app.c
index cce0db7aa4e..c35d29b6948 100644
--- a/components/esp_wifi/wifi_apps/roaming_app/src/roaming_app.c
+++ b/components/esp_wifi/wifi_apps/roaming_app/src/roaming_app.c
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
+ * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
@@ -27,11 +28,13 @@
#include "esp_wifi_driver.h"
#include "utils/eloop.h"
#include "rom/ets_sys.h"
+#include "common/ieee802_11_common.h"
#include "common/ieee802_11_defs.h"
#include "common/wpa_common.h"
#include "esp_wpas_glue.h"
-struct roaming_app g_roaming_app;
+static struct roaming_app g_roaming_app;
+extern bool current_task_is_wifi_task(void);
typedef void (* scan_done_cb_t)(void *arg, ETS_STATUS status);
extern int esp_wifi_promiscuous_scan_start(wifi_scan_config_t *config, scan_done_cb_t cb);
@@ -39,15 +42,85 @@ extern int esp_wifi_promiscuous_scan_start(wifi_scan_config_t *config, scan_done
static int wifi_post_roam_event(struct cand_bss *bss);
static void determine_best_ap(int8_t rssi_threshold);
static bool is_bssid_blacklisted(const uint8_t *bssid);
+static void roaming_app_refresh_scan_config_filters(void);
+static void roaming_app_apply_runtime_config(void);
+static void roaming_app_invalidate_scan_cache(void);
+static esp_err_t roaming_app_run_blocking(eloop_blocking_timeout_handler handler, void *user_ctx);
+static void roaming_app_connected_event_handler(void *ctx, void *data);
+static void roaming_app_disconnected_event_handler(void *ctx, void *data);
+static void roaming_app_trigger_roam_internal_handler(void *ctx, void *data);
#if PERIODIC_RRM_MONITORING
+static void init_periodic_rrm_event(void);
static void roaming_app_periodic_rrm_internal_handler(void *data, void *ctx);
+static void roaming_app_neighbor_report_recv_internal_handler(void *ctx, void *data);
#endif
#if PERIODIC_SCAN_MONITORING
+static void init_periodic_scan_roam_event(void);
static void roaming_app_periodic_scan_internal_handler(void *data, void *ctx);
#endif
+#if LOW_RSSI_ROAMING_ENABLED
+static void roaming_app_rssi_low_internal_handler(void *ctx, void *data);
+#endif
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+static void roaming_app_blacklist_add_handler(void *ctx, void *data);
+static void roaming_app_blacklist_remove_handler(void *ctx, void *data);
+#endif
static const char *ROAMING_TAG = "ROAM";
+#define RSSI_THRESHOLD_DISABLED -100
+#define RSSI_THRESHOLD_MAX 10
+#define BTM_QUERY_LIST_MAX_LEN (MAX_NEIGHBOR_LEN + 96)
+#define ROAMING_PENDING_TIMEOUT_USER_DATA_MAX 16
+
+static void *s_pending_timeout_user_data[ROAMING_PENDING_TIMEOUT_USER_DATA_MAX];
+
+static void roaming_app_track_timeout_user_data(void *user_data)
+{
+ if (!user_data) {
+ return;
+ }
+
+ for (size_t i = 0; i < ROAMING_PENDING_TIMEOUT_USER_DATA_MAX; i++) {
+ if (s_pending_timeout_user_data[i] == user_data) {
+ return;
+ }
+ }
+
+ for (size_t i = 0; i < ROAMING_PENDING_TIMEOUT_USER_DATA_MAX; i++) {
+ if (!s_pending_timeout_user_data[i]) {
+ s_pending_timeout_user_data[i] = user_data;
+ return;
+ }
+ }
+
+ ESP_LOGW(ROAMING_TAG, "Timeout user_data tracking overflow; deinit may leak pending event payload");
+}
+
+static void roaming_app_untrack_timeout_user_data(void *user_data)
+{
+ if (!user_data) {
+ return;
+ }
+
+ for (size_t i = 0; i < ROAMING_PENDING_TIMEOUT_USER_DATA_MAX; i++) {
+ if (s_pending_timeout_user_data[i] == user_data) {
+ s_pending_timeout_user_data[i] = NULL;
+ return;
+ }
+ }
+}
+
+static void roaming_app_free_tracked_timeout_user_data(void)
+{
+ for (size_t i = 0; i < ROAMING_PENDING_TIMEOUT_USER_DATA_MAX; i++) {
+ if (s_pending_timeout_user_data[i]) {
+ os_free(s_pending_timeout_user_data[i]);
+ s_pending_timeout_user_data[i] = NULL;
+ }
+ }
+}
+
void esp_wifi_roaming_set_current_bssid(const uint8_t *bssid)
{
if (bssid) {
@@ -55,11 +128,478 @@ void esp_wifi_roaming_set_current_bssid(const uint8_t *bssid)
}
}
-static inline long time_diff_sec(struct timeval *a, struct timeval *b)
+static inline long time_diff_sec(const struct timeval *a, const struct timeval *b)
{
return (a->tv_sec - b->tv_sec);
}
+static int32_t roaming_app_clamp_rssi_threshold(int threshold)
+{
+ if (threshold < RSSI_THRESHOLD_DISABLED) {
+ return RSSI_THRESHOLD_DISABLED;
+ }
+
+ if (threshold > RSSI_THRESHOLD_MAX) {
+ return RSSI_THRESHOLD_MAX;
+ }
+
+ return threshold;
+}
+
+static bool roaming_app_scan_cache_is_valid(const struct timeval *now)
+{
+ if (g_roaming_app.scanned_aps.time.tv_sec == 0 && g_roaming_app.scanned_aps.time.tv_usec == 0) {
+ return false;
+ }
+
+ return time_diff_sec(now, &g_roaming_app.scanned_aps.time) <= SCAN_RESULTS_USABILITY_WINDOW;
+}
+
+static const wifi_ap_record_t *roaming_app_find_scan_record(const uint8_t *bssid)
+{
+ for (uint16_t i = 0; i < g_roaming_app.scanned_aps.current_count; i++) {
+ if (memcmp(g_roaming_app.scanned_aps.ap_records[i].bssid, bssid, ETH_ALEN) == 0) {
+ return &g_roaming_app.scanned_aps.ap_records[i];
+ }
+ }
+
+ return NULL;
+}
+
+static uint8_t roaming_app_get_phy_type(const wifi_ap_record_t *record)
+{
+ int sec_chan = 0;
+
+ if (!record) {
+ return PHY_TYPE_UNSPECIFIED;
+ }
+
+ if (record->second == WIFI_SECOND_CHAN_ABOVE) {
+ sec_chan = 1;
+ } else if (record->second == WIFI_SECOND_CHAN_BELOW) {
+ sec_chan = -1;
+ }
+
+ if (record->phy_11ax) {
+ return PHY_TYPE_HE;
+ }
+
+ if (record->phy_11ac) {
+ return PHY_TYPE_VHT;
+ }
+
+ if (record->phy_11n || sec_chan != 0 || record->bandwidth > WIFI_BW20) {
+ return PHY_TYPE_HT;
+ }
+
+ return (record->primary > 14) ? PHY_TYPE_OFDM : PHY_TYPE_ERP;
+}
+
+static bool roaming_app_authmode_is_enterprise(wifi_auth_mode_t authmode)
+{
+ return authmode == WIFI_AUTH_WPA_ENTERPRISE ||
+ authmode == WIFI_AUTH_WPA2_ENTERPRISE ||
+ authmode == WIFI_AUTH_WPA3_ENTERPRISE ||
+ authmode == WIFI_AUTH_WPA2_WPA3_ENTERPRISE ||
+ authmode == WIFI_AUTH_WPA3_ENT_192;
+}
+
+static bool roaming_app_authmode_is_open_mode(wifi_auth_mode_t authmode)
+{
+ return authmode == WIFI_AUTH_OPEN || authmode == WIFI_AUTH_OWE;
+}
+
+static bool roaming_app_authmode_is_legacy_personal(wifi_auth_mode_t authmode)
+{
+ return authmode == WIFI_AUTH_WPA_PSK ||
+ authmode == WIFI_AUTH_WPA2_PSK ||
+ authmode == WIFI_AUTH_WPA_WPA2_PSK;
+}
+
+static bool roaming_app_authmode_is_personal(wifi_auth_mode_t authmode)
+{
+ return roaming_app_authmode_is_legacy_personal(authmode) ||
+ authmode == WIFI_AUTH_WPA3_PSK ||
+ authmode == WIFI_AUTH_WPA2_WPA3_PSK ||
+ authmode == WIFI_AUTH_WAPI_PSK;
+}
+
+static bool roaming_app_authmode_is_personal_compatible(wifi_auth_mode_t curr_auth, wifi_auth_mode_t cand_auth)
+{
+ if (!roaming_app_authmode_is_personal(curr_auth) || !roaming_app_authmode_is_personal(cand_auth)) {
+ return false;
+ }
+
+ if (curr_auth == WIFI_AUTH_WAPI_PSK || cand_auth == WIFI_AUTH_WAPI_PSK) {
+ return (curr_auth == cand_auth) ||
+ (curr_auth == WIFI_AUTH_WAPI_PSK && roaming_app_authmode_is_legacy_personal(cand_auth)) ||
+ (cand_auth == WIFI_AUTH_WAPI_PSK && roaming_app_authmode_is_legacy_personal(curr_auth));
+ }
+
+ return true;
+}
+
+static bool roaming_app_allows_wapi_legacy_threshold_transition(wifi_auth_mode_t threshold_auth,
+ wifi_auth_mode_t curr_auth,
+ wifi_auth_mode_t cand_auth)
+{
+ return threshold_auth == WIFI_AUTH_WAPI_PSK &&
+ curr_auth == WIFI_AUTH_WAPI_PSK &&
+ roaming_app_authmode_is_legacy_personal(cand_auth);
+}
+
+static int roaming_app_authmode_strength_rank(wifi_auth_mode_t authmode)
+{
+ switch (authmode) {
+ case WIFI_AUTH_OPEN:
+ return 0;
+ case WIFI_AUTH_WEP:
+ return 1;
+ case WIFI_AUTH_WPA_PSK:
+ return 2;
+ case WIFI_AUTH_OWE:
+ return 3;
+ case WIFI_AUTH_WPA2_PSK:
+ case WIFI_AUTH_WPA_WPA2_PSK:
+ return 4;
+ case WIFI_AUTH_WAPI_PSK:
+ return 5;
+ case WIFI_AUTH_WPA3_PSK:
+ case WIFI_AUTH_WPA2_WPA3_PSK:
+ case WIFI_AUTH_DPP:
+ return 6;
+ case WIFI_AUTH_WPA_ENTERPRISE:
+ return 7;
+ case WIFI_AUTH_ENTERPRISE:
+ return 8;
+ case WIFI_AUTH_WPA3_ENTERPRISE:
+ case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
+ return 9;
+ case WIFI_AUTH_WPA3_ENT_192:
+ return 10;
+ default:
+ return -1;
+ }
+}
+
+static int roaming_app_candidate_strength_rank(wifi_auth_mode_t cand_auth,
+ bool candidate_supports_sae,
+ bool candidate_supports_owe,
+ bool candidate_supports_wpa3_enterprise,
+ bool candidate_supports_suite_b_192)
+{
+ if (candidate_supports_suite_b_192) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WPA3_ENT_192);
+ }
+
+ if (candidate_supports_wpa3_enterprise) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WPA3_ENTERPRISE);
+ }
+
+ if (cand_auth == WIFI_AUTH_WPA_ENTERPRISE) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WPA_ENTERPRISE);
+ }
+
+ if (cand_auth == WIFI_AUTH_ENTERPRISE) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_ENTERPRISE);
+ }
+
+ if (candidate_supports_sae) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WPA3_PSK);
+ }
+
+ if (cand_auth == WIFI_AUTH_WAPI_PSK) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WAPI_PSK);
+ }
+
+ if (cand_auth == WIFI_AUTH_WPA2_PSK || cand_auth == WIFI_AUTH_WPA_WPA2_PSK) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_WPA2_PSK);
+ }
+
+ if (candidate_supports_owe) {
+ return roaming_app_authmode_strength_rank(WIFI_AUTH_OWE);
+ }
+
+ return roaming_app_authmode_strength_rank(cand_auth);
+}
+
+static bool roaming_app_candidate_meets_threshold(wifi_auth_mode_t threshold_auth,
+ wifi_auth_mode_t curr_auth,
+ wifi_auth_mode_t cand_auth,
+ bool candidate_supports_sae,
+ bool candidate_supports_owe,
+ bool candidate_supports_wpa3_enterprise,
+ bool candidate_supports_suite_b_192)
+{
+ int threshold_rank = roaming_app_authmode_strength_rank(threshold_auth);
+ int candidate_rank = roaming_app_candidate_strength_rank(cand_auth,
+ candidate_supports_sae,
+ candidate_supports_owe,
+ candidate_supports_wpa3_enterprise,
+ candidate_supports_suite_b_192);
+
+ if (threshold_rank < 0 || candidate_rank < 0) {
+ return false;
+ }
+
+ if (candidate_rank >= threshold_rank) {
+ return true;
+ }
+
+ return roaming_app_allows_wapi_legacy_threshold_transition(threshold_auth, curr_auth, cand_auth);
+}
+
+static const u8 *roaming_app_get_rsnxe_ie(const uint8_t *bssid)
+{
+ const u8 *rsnxe = NULL;
+
+ if (!bssid) {
+ return NULL;
+ }
+
+ if (esp_wifi_is_wpa3_compatible_mode_enabled(WIFI_IF_STA)) {
+ rsnxe = esp_wifi_sta_get_ie((u8 *) bssid, WFA_RSNXE_OVERRIDE_OUI_TYPE);
+ }
+
+ if (!rsnxe) {
+ rsnxe = esp_wifi_sta_get_ie((u8 *) bssid, WLAN_EID_RSNX);
+ }
+
+ return rsnxe;
+}
+
+static int roaming_app_build_btm_candidate(const struct cand_bss *bss, char *buf, size_t buf_len)
+{
+ const wifi_ap_record_t *record;
+ uint8_t channel;
+ int sec_chan = 0;
+ uint8_t phy_type;
+ uint8_t op_class;
+
+ if (!bss || !buf || buf_len == 0) {
+ return -1;
+ }
+
+ record = roaming_app_find_scan_record(bss->bssid);
+ channel = bss->channel;
+
+ if (record) {
+ channel = record->primary;
+ if (record->second == WIFI_SECOND_CHAN_ABOVE) {
+ sec_chan = 1;
+ } else if (record->second == WIFI_SECOND_CHAN_BELOW) {
+ sec_chan = -1;
+ }
+ phy_type = roaming_app_get_phy_type(record);
+ } else {
+ phy_type = (channel > 14) ? PHY_TYPE_OFDM : PHY_TYPE_ERP;
+ }
+
+ op_class = get_operating_class(channel, sec_chan);
+ if (op_class == 0) {
+ ESP_LOGW(ROAMING_TAG, "Could not determine operating class for " MACSTR, MAC2STR(bss->bssid));
+ return -1;
+ }
+
+ if (snprintf(buf, buf_len, " neighbor=" MACSTR ",0x0000,%u,%u,%u",
+ MAC2STR(bss->bssid), op_class, channel, phy_type) >= (int) buf_len) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static void roaming_app_set_scan_filter_bssid(const uint8_t *bssid, bool enabled)
+{
+ if (enabled && bssid) {
+ memcpy(g_roaming_app.config.scan_filter_bssid, bssid, sizeof(g_roaming_app.config.scan_filter_bssid));
+ g_roaming_app.config.scan_filter_bssid_set = true;
+ g_roaming_app.config.scan_config.bssid = g_roaming_app.config.scan_filter_bssid;
+ } else {
+ memset(g_roaming_app.config.scan_filter_bssid, 0, sizeof(g_roaming_app.config.scan_filter_bssid));
+ g_roaming_app.config.scan_filter_bssid_set = false;
+ g_roaming_app.config.scan_config.bssid = NULL;
+ }
+}
+
+static void roaming_app_refresh_scan_config_filters(void)
+{
+ if (g_roaming_app.config.scan_filter_ssid[0] != '\0') {
+ g_roaming_app.config.scan_config.ssid = g_roaming_app.config.scan_filter_ssid;
+ } else if (g_roaming_app.current_bss.ap.ssid[0] != '\0') {
+ g_roaming_app.config.scan_config.ssid = g_roaming_app.current_bss.ap.ssid;
+ } else {
+ g_roaming_app.config.scan_config.ssid = NULL;
+ }
+
+ roaming_app_set_scan_filter_bssid(g_roaming_app.config.scan_filter_bssid_set ?
+ g_roaming_app.config.scan_filter_bssid : NULL,
+ g_roaming_app.config.scan_filter_bssid_set);
+
+ if (g_roaming_app.config.scan_config.channel_bitmap.ghz_2_channels ||
+ g_roaming_app.config.scan_config.channel_bitmap.ghz_5_channels) {
+ g_roaming_app.config.scan_config.channel = 0;
+ }
+}
+
+static bool roaming_app_clear_connect_hint(void)
+{
+ wifi_config_t wifi_cfg = {0};
+ esp_err_t err = esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg);
+
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to read sta config while clearing roam hint: %s", esp_err_to_name(err));
+ return false;
+ }
+
+ if (wifi_cfg.sta.bssid_set || wifi_cfg.sta.channel) {
+ wifi_cfg.sta.channel = 0;
+ wifi_cfg.sta.bssid_set = 0;
+ err = esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to clear roam hint: %s", esp_err_to_name(err));
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void roaming_app_reset_connect_hint_state(void)
+{
+ if (!g_roaming_app.connect_hint_active) {
+ return;
+ }
+
+ if (roaming_app_clear_connect_hint()) {
+ g_roaming_app.connect_hint_active = false;
+ }
+}
+
+static void roaming_app_mark_roam_attempt(void)
+{
+ gettimeofday(&g_roaming_app.last_roam_attempt_time, NULL);
+}
+
+static void roaming_app_mark_roam_success(void)
+{
+ gettimeofday(&g_roaming_app.last_roam_success_time, NULL);
+}
+
+static bool roaming_app_connected_roam_method_available(void)
+{
+#if NETWORK_ASSISTED_ROAMING_ENABLED
+ if (g_roaming_app.config.btm_roaming_enabled && g_roaming_app.current_bss.btm_support) {
+#if LEGACY_ROAM_ENABLED && NETWORK_ASSISTED_ROAMING_ENABLED
+ if (g_roaming_app.btm_attempt <= g_roaming_app.config.btm_retry_cnt) {
+ return true;
+ }
+#else
+ return true;
+#endif
+ }
+#endif /*NETWORK_ASSISTED_ROAMING_ENABLED*/
+
+#if LEGACY_ROAM_ENABLED
+ if (g_roaming_app.config.legacy_roam_enabled) {
+ return true;
+ }
+#endif /*LEGACY_ROAM_ENABLED*/
+
+ return false;
+}
+
+static const struct timeval *roaming_app_get_backoff_anchor(void)
+{
+ const struct timeval *anchor = &g_roaming_app.last_roam_attempt_time;
+
+ if (g_roaming_app.last_roam_success_time.tv_sec > anchor->tv_sec ||
+ (g_roaming_app.last_roam_success_time.tv_sec == anchor->tv_sec &&
+ g_roaming_app.last_roam_success_time.tv_usec > anchor->tv_usec)) {
+ anchor = &g_roaming_app.last_roam_success_time;
+ }
+
+ return anchor;
+}
+
+static bool roaming_app_roam_backoff_active(const struct timeval *now, uint8_t margin_sec)
+{
+ const struct timeval *anchor;
+ long required_gap;
+
+ if (g_roaming_app.config.backoff_time <= margin_sec) {
+ return false;
+ }
+
+ anchor = roaming_app_get_backoff_anchor();
+ required_gap = g_roaming_app.config.backoff_time - margin_sec;
+ return time_diff_sec(now, anchor) < required_gap;
+}
+
+static bool roaming_app_can_act_on_candidate(const struct timeval *now, bool log_reason)
+{
+ if (!g_roaming_app.sta_connected) {
+ return true;
+ }
+
+ if (roaming_app_roam_backoff_active(now, 0)) {
+ if (log_reason) {
+ ESP_LOGD(ROAMING_TAG, "Skipping candidate processing during roam backoff window");
+ }
+ return false;
+ }
+
+ if (!roaming_app_connected_roam_method_available()) {
+ if (log_reason) {
+ ESP_LOGD(ROAMING_TAG, "Skipping candidate processing as no roaming method is currently available");
+ }
+ return false;
+ }
+
+ return true;
+}
+
+static void roaming_app_stop_periodic_monitors(void)
+{
+#if PERIODIC_RRM_MONITORING
+ g_roaming_app.periodic_rrm_active = false;
+ g_roaming_app.rrm_request_active = false;
+ eloop_cancel_timeout(roaming_app_periodic_rrm_internal_handler, NULL, NULL);
+#endif
+
+#if PERIODIC_SCAN_MONITORING
+ g_roaming_app.periodic_scan_active = false;
+ eloop_cancel_timeout(roaming_app_periodic_scan_internal_handler, NULL, NULL);
+#endif
+}
+
+static void roaming_app_set_disconnected_state(const wifi_event_sta_disconnected_t *disconn)
+{
+ g_roaming_app.sta_connected = false;
+ g_roaming_app.current_bss.btm_support = false;
+ g_roaming_app.current_bss.rrm_support = false;
+ g_roaming_app.current_bss.ap.rssi = -128;
+ g_roaming_app.current_bss.ap.authmode = WIFI_AUTH_OPEN;
+ memset(g_roaming_app.current_bss.ap.ssid, 0, sizeof(g_roaming_app.current_bss.ap.ssid));
+
+ if (disconn) {
+ memcpy(g_roaming_app.current_bss.ap.bssid, disconn->bssid, ETH_ALEN);
+ } else {
+ memset(g_roaming_app.current_bss.ap.bssid, 0, ETH_ALEN);
+ }
+}
+
+static bool roaming_app_try_default_reconnect(const char *reason)
+{
+ roaming_app_reset_connect_hint_state();
+ if (esp_wifi_connect() != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "%s failed", reason);
+ return false;
+ }
+
+ return true;
+}
+
static void disable_reconnect(void *ctx, void *data)
{
ESP_LOGD(ROAMING_TAG, "Disable roaming app reconnect");
@@ -82,42 +622,325 @@ void roam_enable_reconnect(void)
eloop_register_timeout(0, 0, enable_reconnect, NULL, NULL);
}
-static void roaming_app_get_ap_info(wifi_ap_record_t *ap_info)
+static bool roaming_app_get_ap_info(wifi_ap_record_t *ap_info)
{
- esp_wifi_sta_get_ap_info(ap_info);
+ esp_err_t err;
+
+ if (!ap_info) {
+ return false;
+ }
+
+ err = esp_wifi_sta_get_ap_info(ap_info);
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to get current AP info: %s", esp_err_to_name(err));
+ return false;
+ }
#if LOW_RSSI_ROAMING_ENABLED
/*
* If the current rssi is below the configured rssi threshold for
* low rssi based roaming and the current rssi threshold is below that,
* we should reset the rssi threshold back to the configured rssi threshold */
- if ((ap_info->rssi > g_roaming_app.config.low_rssi_threshold) && (g_roaming_app.current_low_rssi_threshold < g_roaming_app.config.low_rssi_threshold)) {
- g_roaming_app.current_low_rssi_threshold = g_roaming_app.config.low_rssi_threshold;
- esp_wifi_set_rssi_threshold(g_roaming_app.config.low_rssi_threshold);
- ESP_LOGD(ROAMING_TAG, "Reset the low rssi threshold back to %d", g_roaming_app.config.low_rssi_threshold);
+ if (g_roaming_app.config.low_rssi_roam_trigger &&
+ (ap_info->rssi > g_roaming_app.config.low_rssi_threshold) &&
+ (g_roaming_app.current_low_rssi_threshold < g_roaming_app.config.low_rssi_threshold)) {
+ if (esp_wifi_set_rssi_threshold(g_roaming_app.config.low_rssi_threshold) == ESP_OK) {
+ g_roaming_app.current_low_rssi_threshold = g_roaming_app.config.low_rssi_threshold;
+ ESP_LOGD(ROAMING_TAG, "Reset the low rssi threshold back to %d", g_roaming_app.config.low_rssi_threshold);
+ } else {
+ ESP_LOGW(ROAMING_TAG, "failed to reset low RSSI threshold to %d", g_roaming_app.config.low_rssi_threshold);
+ }
}
#endif /*LOW_RSSI_ROAMING_ENABLED*/
+ return true;
}
#if LEGACY_ROAM_ENABLED
static void legacy_roam_clear_bssid_flag(void)
{
- wifi_config_t *config = {0};
+ if (g_roaming_app.connect_hint_active && roaming_app_clear_connect_hint()) {
+ g_roaming_app.connect_hint_active = false;
+ ESP_LOGD(ROAMING_TAG, "cleared bssid/channel hint");
+ }
+}
+#endif /*LEGACY_ROAM_ENABLED*/
- config = os_zalloc(sizeof(wifi_config_t));
- if (!config) {
- ESP_LOGE(ROAMING_TAG, "failed to allocate memory");
+#if LOW_RSSI_ROAMING_ENABLED
+static void roaming_app_apply_low_rssi_config(void)
+{
+ int32_t next_threshold;
+
+ if (!g_roaming_app.sta_connected) {
return;
}
- esp_wifi_get_config(WIFI_IF_STA, config);
- if (config->sta.bssid_set) {
- config->sta.bssid_set = 0;
- esp_wifi_set_config(WIFI_IF_STA, config);
+ if (!g_roaming_app.config.low_rssi_roam_trigger) {
+ if (esp_wifi_set_rssi_threshold(RSSI_THRESHOLD_DISABLED) == ESP_OK) {
+ g_roaming_app.current_low_rssi_threshold = RSSI_THRESHOLD_DISABLED;
+ ESP_LOGD(ROAMING_TAG, "Disabled low RSSI roaming threshold");
+ } else {
+ ESP_LOGW(ROAMING_TAG, "failed to disable low RSSI roaming threshold");
+ }
+ return;
+ }
+
+ if (!roaming_app_get_ap_info(&g_roaming_app.current_bss.ap)) {
+ return;
+ }
+
+ if (g_roaming_app.current_bss.ap.rssi < g_roaming_app.config.low_rssi_threshold) {
+ /* To ensure that the threshold is set to one offset below the current AP RSSI
+ * in case, the AP is already below the RSSI threshold */
+ next_threshold = g_roaming_app.current_bss.ap.rssi -
+ g_roaming_app.config.rssi_threshold_reduction_offset;
+ } else {
+ next_threshold = g_roaming_app.config.low_rssi_threshold;
+ }
+
+ next_threshold = roaming_app_clamp_rssi_threshold(next_threshold);
+ if (esp_wifi_set_rssi_threshold(next_threshold) == ESP_OK) {
+ g_roaming_app.current_low_rssi_threshold = next_threshold;
+ ESP_LOGD(ROAMING_TAG, "setting rssi threshold as %d", g_roaming_app.current_low_rssi_threshold);
+ } else {
+ ESP_LOGW(ROAMING_TAG, "failed to set low RSSI threshold to %d", next_threshold);
}
- os_free(config);
- ESP_LOGD(ROAMING_TAG, "cleared bssid flag");
}
+#endif /*LOW_RSSI_ROAMING_ENABLED*/
+
+static void roaming_app_apply_runtime_config(void)
+{
+ roaming_app_stop_periodic_monitors();
+
+#if LOW_RSSI_ROAMING_ENABLED
+ roaming_app_apply_low_rssi_config();
+#endif /*LOW_RSSI_ROAMING_ENABLED*/
+
+#if PERIODIC_RRM_MONITORING
+ if (g_roaming_app.sta_connected && g_roaming_app.current_bss.rrm_support && g_roaming_app.config.rrm_monitor) {
+ init_periodic_rrm_event();
+ }
+#endif /*PERIODIC_RRM_MONITORING*/
+
+#if PERIODIC_SCAN_MONITORING
+ if (g_roaming_app.sta_connected && g_roaming_app.config.scan_monitor) {
+ init_periodic_scan_roam_event();
+ }
+#endif /*PERIODIC_SCAN_MONITORING*/
+}
+
+static void roaming_app_invalidate_scan_cache(void)
+{
+ g_roaming_app.scanned_aps.current_count = 0;
+ g_roaming_app.scanned_aps.time.tv_sec = 0;
+ g_roaming_app.scanned_aps.time.tv_usec = 0;
+}
+
+static esp_err_t roaming_app_run_blocking(eloop_blocking_timeout_handler handler, void *user_ctx)
+{
+ int ret;
+
+ if (!handler) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ if (current_task_is_wifi_task()) {
+ ret = handler(NULL, user_ctx);
+ } else {
+ ret = eloop_register_timeout_blocking(handler, NULL, user_ctx);
+ }
+
+ return ret < 0 ? ESP_FAIL : ret;
+}
+
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+static int roaming_app_find_blacklist_entry_index(const uint8_t *bssid)
+{
+ if (!bssid) {
+ return -1;
+ }
+
+ for (int i = 0; i < g_roaming_app.bssid_blacklist_count; i++) {
+ if (memcmp(g_roaming_app.bssid_blacklist[i].bssid, bssid, ETH_ALEN) == 0) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+static struct blacklist_entry *roaming_app_find_blacklist_entry(const uint8_t *bssid)
+{
+ int index = roaming_app_find_blacklist_entry_index(bssid);
+ return (index >= 0) ? &g_roaming_app.bssid_blacklist[index] : NULL;
+}
+
+static int roaming_app_find_oldest_blacklist_entry_index(void)
+{
+ int oldest_idx = 0;
+
+ for (int i = 1; i < g_roaming_app.bssid_blacklist_count; i++) {
+ if (g_roaming_app.bssid_blacklist[i].timestamp.tv_sec <
+ g_roaming_app.bssid_blacklist[oldest_idx].timestamp.tv_sec) {
+ oldest_idx = i;
+ }
+ }
+
+ return oldest_idx;
+}
+
+static bool roaming_app_blacklist_entry_blocks_roaming(const struct blacklist_entry *entry)
+{
+ if (!entry) {
+ return false;
+ }
+
+#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
+ return entry->manual || (entry->failures >= CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES);
+#else
+ return true;
+#endif
+}
+
+static void roaming_app_touch_blacklist_entry(struct blacklist_entry *entry)
+{
+ if (entry) {
+ gettimeofday(&entry->timestamp, NULL);
+ }
+}
+
+static void roaming_app_mark_blacklist_entry_manual(struct blacklist_entry *entry, const uint8_t *bssid)
+{
+ if (!entry || !bssid) {
+ return;
+ }
+
+ memcpy(entry->bssid, bssid, ETH_ALEN);
+ entry->manual = true;
+#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
+ entry->failures = CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES;
+#endif
+ roaming_app_touch_blacklist_entry(entry);
+}
+
+static bool roaming_app_add_manual_blacklist_entry(const uint8_t *bssid)
+{
+ struct blacklist_entry *entry = roaming_app_find_blacklist_entry(bssid);
+
+ if (entry) {
+ roaming_app_mark_blacklist_entry_manual(entry, bssid);
+ return true;
+ }
+
+ if (g_roaming_app.bssid_blacklist_count >= CONFIG_ESP_WIFI_ROAMING_MAX_CANDIDATES) {
+ return false;
+ }
+
+ entry = &g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count++];
+ roaming_app_mark_blacklist_entry_manual(entry, bssid);
+ return true;
+}
+
+static void roaming_app_record_connection_failure(const uint8_t *bssid)
+{
+#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
+ struct blacklist_entry *entry;
+
+ if (!bssid) {
+ return;
+ }
+
+ entry = roaming_app_find_blacklist_entry(bssid);
+ if (entry) {
+ if (!entry->manual) {
+ entry->failures++;
+ }
+ roaming_app_touch_blacklist_entry(entry);
+ ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " connection failures: %d",
+ MAC2STR(bssid), entry->failures);
+ if (!entry->manual && entry->failures >= CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES) {
+ ESP_LOGI(ROAMING_TAG, "BSSID " MACSTR " blacklisted", MAC2STR(bssid));
+ }
+ return;
+ }
+
+ if (g_roaming_app.bssid_blacklist_count < CONFIG_ESP_WIFI_ROAMING_MAX_CANDIDATES) {
+ entry = &g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count++];
+ } else {
+ int oldest_idx = roaming_app_find_oldest_blacklist_entry_index();
+ ESP_LOGD(ROAMING_TAG, "Blacklist is full. Replacing oldest entry for " MACSTR,
+ MAC2STR(g_roaming_app.bssid_blacklist[oldest_idx].bssid));
+ entry = &g_roaming_app.bssid_blacklist[oldest_idx];
+ }
+
+ memcpy(entry->bssid, bssid, ETH_ALEN);
+ entry->manual = false;
+ entry->failures = 1;
+ roaming_app_touch_blacklist_entry(entry);
+ ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " added to blacklist tracking", MAC2STR(bssid));
+#else
+ (void) bssid;
+#endif
+}
+
+static void roaming_app_reset_auto_blacklist_failures(const uint8_t *bssid)
+{
+#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
+ struct blacklist_entry *entry = roaming_app_find_blacklist_entry(bssid);
+
+ if (entry && !entry->manual && entry->failures > 0) {
+ entry->failures = 0;
+ roaming_app_touch_blacklist_entry(entry);
+ ESP_LOGD(ROAMING_TAG, "Reset automatic blacklist failure count for " MACSTR, MAC2STR(bssid));
+ }
+#else
+ (void) bssid;
+#endif
+}
+
+static void roaming_app_blacklist_recovery_internal_handler(void *data, void *ctx)
+{
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
+ if (g_roaming_app.sta_connected || !g_roaming_app.allow_reconnect) {
+ return;
+ }
+
+ if (!is_bssid_blacklisted(g_roaming_app.current_bss.ap.bssid)) {
+ ESP_LOGI(ROAMING_TAG, "Blacklist expired for " MACSTR ", retrying default reconnect",
+ MAC2STR(g_roaming_app.current_bss.ap.bssid));
+ roaming_app_try_default_reconnect("Default reconnect after blacklist expiry");
+ return;
+ }
+
+ roaming_app_invalidate_scan_cache();
+ determine_best_ap(0);
+}
+
+static void roaming_app_schedule_blacklist_recovery(void)
+{
+ uint8_t retry_delay = g_roaming_app.config.backoff_time ? g_roaming_app.config.backoff_time : 1;
+
+ eloop_cancel_timeout(roaming_app_blacklist_recovery_internal_handler, NULL, NULL);
+ if (eloop_register_timeout(retry_delay, 0, roaming_app_blacklist_recovery_internal_handler, NULL, NULL) != 0) {
+ ESP_LOGW(ROAMING_TAG, "Could not schedule blacklist recovery retry");
+ }
+}
+
+static void roaming_app_retry_blacklist_recovery_if_needed(void)
+{
+ if (!g_roaming_app.sta_connected && is_bssid_blacklisted(g_roaming_app.current_bss.ap.bssid)) {
+ ESP_LOGI(ROAMING_TAG, "No non-blacklisted AP found, scheduling recovery retry");
+#if LEGACY_ROAM_ENABLED
+ if (g_roaming_app.force_roam_ongoing) {
+ legacy_roam_clear_bssid_flag();
+ }
#endif /*LEGACY_ROAM_ENABLED*/
+ roaming_app_reset_connect_hint_state();
+ roaming_app_schedule_blacklist_recovery();
+ }
+}
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
static int8_t initialize_roaming_event(void)
{
@@ -142,10 +965,11 @@ static void init_periodic_rrm_event(void)
return;
}
ESP_LOGV(ROAMING_TAG, "Initialised Periodic RRM Monitoring event!");
- g_roaming_app.periodic_rrm_active = true;
if (eloop_register_timeout(g_roaming_app.config.rrm_monitor_time, 0, roaming_app_periodic_rrm_internal_handler, NULL, NULL)) {
ESP_LOGE(ROAMING_TAG, "Could not register periodic neighbor report event.");
+ return;
}
+ g_roaming_app.periodic_rrm_active = true;
}
#endif /*PERIODIC_RRM_MONITORING*/
@@ -157,68 +981,33 @@ static void init_periodic_scan_roam_event(void)
return;
}
ESP_LOGV(ROAMING_TAG, "Initialised Periodic Scan Roam event!");
- g_roaming_app.periodic_scan_active = true;
if (eloop_register_timeout(g_roaming_app.config.scan_interval, 0, roaming_app_periodic_scan_internal_handler, NULL, NULL)) {
ESP_LOGE(ROAMING_TAG, "Could not register periodic scan monitoring event");
+ return;
}
+ g_roaming_app.periodic_scan_active = true;
}
-#endif /*PERIODIC_SCAN_ROAM_MONITORING*/
+#endif /*PERIODIC_SCAN_MONITORING*/
static void roaming_app_disconnected_event_handler(void *ctx, void *data)
{
-#if PERIODIC_RRM_MONITORING
- g_roaming_app.periodic_rrm_active = false;
-#endif /*PERIODIC_RRM_MONITORING*/
+ roaming_app_untrack_timeout_user_data(data);
-#if PERIODIC_SCAN_MONITORING
- g_roaming_app.periodic_scan_active = false;
-#endif /*PERIODIC_SCAN_MONITORING*/
+ if (!g_roaming_app.app_active) {
+ if (data) {
+ os_free(data);
+ }
+ return;
+ }
wifi_event_sta_disconnected_t *disconn = data;
-#define RSSI_INVALID -128
- g_roaming_app.current_bss.ap.rssi = RSSI_INVALID;
-#undef RSSI_INVALID
+ roaming_app_stop_periodic_monitors();
+ roaming_app_set_disconnected_state(disconn);
ESP_LOGD(ROAMING_TAG, "station got disconnected reason=%d, rssi =%d", disconn->reason, disconn->rssi);
#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
if (disconn->reason == WIFI_REASON_CONNECTION_FAIL || disconn->reason == WIFI_REASON_AUTH_FAIL) {
- bool found = false;
- for (int i = 0; i < g_roaming_app.bssid_blacklist_count; i++) {
- if (memcmp(g_roaming_app.bssid_blacklist[i].bssid, g_roaming_app.current_bss.ap.bssid, ETH_ALEN) == 0) {
- g_roaming_app.bssid_blacklist[i].failures++;
- gettimeofday(&g_roaming_app.bssid_blacklist[i].timestamp, NULL);
- ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " connection failures: %d", MAC2STR(g_roaming_app.current_bss.ap.bssid), g_roaming_app.bssid_blacklist[i].failures);
- if (g_roaming_app.bssid_blacklist[i].failures >= CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES) {
- ESP_LOGI(ROAMING_TAG, "BSSID " MACSTR " blacklisted", MAC2STR(g_roaming_app.current_bss.ap.bssid));
- /* The BSSID is now blacklisted. The logic later in this function
- * will check the blacklist and trigger a scan for a new AP
- * instead of trying to reconnect to this one. */
- }
- found = true;
- break;
- }
- }
- if (!found) {
- if (g_roaming_app.bssid_blacklist_count < CONFIG_ESP_WIFI_ROAMING_MAX_CANDIDATES) {
- memcpy(g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].bssid, g_roaming_app.current_bss.ap.bssid, ETH_ALEN);
- g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].failures = 1;
- gettimeofday(&g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].timestamp, NULL);
- g_roaming_app.bssid_blacklist_count++;
- ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " added to blacklist tracking", MAC2STR(g_roaming_app.current_bss.ap.bssid));
- } else {
- int oldest_idx = 0;
- for (int i = 1; i < g_roaming_app.bssid_blacklist_count; i++) {
- if (g_roaming_app.bssid_blacklist[i].timestamp.tv_sec < g_roaming_app.bssid_blacklist[oldest_idx].timestamp.tv_sec) {
- oldest_idx = i;
- }
- }
- ESP_LOGD(ROAMING_TAG, "Blacklist is full. Replacing oldest entry for " MACSTR, MAC2STR(g_roaming_app.bssid_blacklist[oldest_idx].bssid));
- memcpy(g_roaming_app.bssid_blacklist[oldest_idx].bssid, g_roaming_app.current_bss.ap.bssid, ETH_ALEN);
- g_roaming_app.bssid_blacklist[oldest_idx].failures = 1;
- gettimeofday(&g_roaming_app.bssid_blacklist[oldest_idx].timestamp, NULL);
- ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " added to blacklist tracking", MAC2STR(g_roaming_app.current_bss.ap.bssid));
- }
- }
+ roaming_app_record_connection_failure(g_roaming_app.current_bss.ap.bssid);
}
#endif
if (disconn->reason == WIFI_REASON_ROAMING) {
@@ -242,7 +1031,7 @@ static void roaming_app_disconnected_event_handler(void *ctx, void *data)
legacy_roam_clear_bssid_flag();
}
#endif /*LEGACY_ROAM_ENABLED*/
- esp_wifi_connect();
+ roaming_app_try_default_reconnect("Default reconnect");
#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
}
#endif
@@ -252,38 +1041,45 @@ static void roaming_app_disconnected_event_handler(void *ctx, void *data)
static void roaming_app_connected_event_handler(void *ctx, void *data)
{
- roaming_app_get_ap_info(&g_roaming_app.current_bss.ap);
- g_roaming_app.config.scan_config.ssid = g_roaming_app.current_bss.ap.ssid;
-#if LOW_RSSI_ROAMING_ENABLED
- if (g_roaming_app.current_bss.ap.rssi < g_roaming_app.config.low_rssi_threshold) {
- /* To ensure that the threshold is set to one offset below the current AP RSSI
- * in case, the AP is already below the RSSI threshold */
- g_roaming_app.current_low_rssi_threshold = g_roaming_app.current_bss.ap.rssi - g_roaming_app.config.rssi_threshold_reduction_offset;
- } else {
- g_roaming_app.current_low_rssi_threshold = g_roaming_app.config.low_rssi_threshold;
+ bool ap_info_available;
+
+ if (!g_roaming_app.app_active) {
+ return;
}
- ESP_LOGD(ROAMING_TAG, "setting rssi threshold as %d", g_roaming_app.current_low_rssi_threshold);
- esp_wifi_set_rssi_threshold(g_roaming_app.current_low_rssi_threshold);
-#endif /*LOW_RSSI_ROAMING_ENABLED*/
+
+ g_roaming_app.sta_connected = true;
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ eloop_cancel_timeout(roaming_app_blacklist_recovery_internal_handler, NULL, NULL);
+#endif
+ roaming_app_reset_connect_hint_state();
+ memset(&g_roaming_app.current_bss.ap, 0, sizeof(g_roaming_app.current_bss.ap));
+ g_roaming_app.current_bss.ap.rssi = -128;
+ ap_info_available = roaming_app_get_ap_info(&g_roaming_app.current_bss.ap);
+ if (!ap_info_available) {
+ ESP_LOGW(ROAMING_TAG, "Skipping AP info-dependent connected handling");
+ }
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ if (ap_info_available) {
+ roaming_app_reset_auto_blacklist_failures(g_roaming_app.current_bss.ap.bssid);
+ }
+#endif
+ roaming_app_invalidate_scan_cache();
+ roaming_app_refresh_scan_config_filters();
g_roaming_app.current_bss.rrm_support = esp_rrm_is_rrm_supported_connection();
g_roaming_app.current_bss.btm_support = esp_wnm_is_btm_supported_connection();
ESP_LOGD(ROAMING_TAG, "Station connected, RRM %ssupported, BTM %ssupported",
g_roaming_app.current_bss.rrm_support ? " " : "not ",
g_roaming_app.current_bss.btm_support ? " " : "not ");
- gettimeofday(&g_roaming_app.last_roamed_time, NULL);
+ roaming_app_mark_roam_success();
if (!initialize_roaming_event()) {
-#if PERIODIC_RRM_MONITORING
- if (g_roaming_app.current_bss.rrm_support) {
- init_periodic_rrm_event();
- }
-#endif /*PERIODIC_RRM_MONITORING*/
-#if PERIODIC_SCAN_MONITORING
- init_periodic_scan_roam_event();
-#endif /*PERIODIC_SCAN_ROAM_MONITORING*/
+ roaming_app_apply_runtime_config();
ESP_LOGD(ROAMING_TAG, "Initialised initialise roaming events!");
} else {
ESP_LOGE(ROAMING_TAG, "Failed to Initialise roaming events");
}
+#if LEGACY_ROAM_ENABLED && NETWORK_ASSISTED_ROAMING_ENABLED
+ g_roaming_app.btm_attempt = 0;
+#endif /*LEGACY_ROAM_ENABLED && NETWORK_ASSISTED_ROAMING_ENABLED*/
#if LEGACY_ROAM_ENABLED
g_roaming_app.force_roam_ongoing = false;
#endif /*LEGACY_ROAM_ENABLED*/
@@ -292,11 +1088,21 @@ static void roaming_app_connected_event_handler(void *ctx, void *data)
void roam_sta_connected(void)
{
- eloop_register_timeout(0, 0, roaming_app_connected_event_handler, NULL, NULL);
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
+ if (eloop_register_timeout(0, 0, roaming_app_connected_event_handler, NULL, NULL) != 0) {
+ ESP_LOGW(ROAMING_TAG, "Could not schedule connected event handler");
+ }
}
void roam_sta_disconnected(void *data)
{
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
wifi_event_sta_disconnected_t *disconn = os_malloc(sizeof(*disconn));
if (!disconn) {
@@ -305,11 +1111,33 @@ void roam_sta_disconnected(void *data)
os_memcpy(disconn, data, sizeof(*disconn));
if (eloop_register_timeout(0, 0, roaming_app_disconnected_event_handler, NULL, disconn) != 0) {
os_free(disconn);
+ } else {
+ roaming_app_track_timeout_user_data(disconn);
}
}
-#define MAX_NEIGHBOR_LEN 512
#if PERIODIC_RRM_MONITORING
+static int roaming_app_append_string(char *buf, size_t buf_len, size_t *len, const char *fmt, ...)
+{
+ va_list ap;
+ int written;
+
+ if (*len >= buf_len) {
+ return -1;
+ }
+
+ va_start(ap, fmt);
+ written = vsnprintf(buf + *len, buf_len - *len, fmt, ap);
+ va_end(ap);
+
+ if (written < 0 || (size_t) written >= buf_len - *len) {
+ return -1;
+ }
+
+ *len += written;
+ return 0;
+}
+
static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
{
size_t len = 0;
@@ -363,37 +1191,37 @@ static char * get_btm_neighbor_list(uint8_t *report, size_t report_len)
nr = pos;
pos += NR_IE_MIN_LEN;
- while (end - pos > 2) {
+ while (end - pos >= 2) {
+ uint8_t s_id;
uint8_t s_len;
+ s_id = *pos++;
s_len = *pos++;
+ (void) s_id;
if (s_len > end - pos) {
ret = -1;
goto cleanup;
}
pos += s_len;
}
+ if (pos != end) {
+ ret = -1;
+ goto cleanup;
+ }
ESP_LOGD(ROAMING_TAG, "RMM neighbor report bssid=" MACSTR
" info=0x%" PRIx32 " op_class=%u chan=%u phy_type=%u",
MAC2STR(nr), WPA_GET_LE32(nr + ETH_ALEN),
nr[ETH_ALEN + 4], nr[ETH_ALEN + 5],
nr[ETH_ALEN + 6]);
/* neighbor start */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, " neighbor=");
- /* bssid */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, MACSTR, MAC2STR(nr));
- /* , */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, ",");
- /* bssid info */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, "0x%04" PRIx32 "", WPA_GET_LE32(nr + ETH_ALEN));
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, ",");
- /* operating class */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, "%u", nr[ETH_ALEN + 4]);
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, ",");
- /* channel number */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, "%u", nr[ETH_ALEN + 5]);
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, ",");
- /* phy type */
- len += snprintf(buf + len, MAX_NEIGHBOR_LEN - len, "%u", nr[ETH_ALEN + 6]);
+ if (roaming_app_append_string(buf, MAX_NEIGHBOR_LEN, &len,
+ " neighbor=" MACSTR ",0x%04" PRIx32 ",%u,%u,%u",
+ MAC2STR(nr), WPA_GET_LE32(nr + ETH_ALEN),
+ nr[ETH_ALEN + 4], nr[ETH_ALEN + 5],
+ nr[ETH_ALEN + 6]) < 0) {
+ ESP_LOGW(ROAMING_TAG, "Neighbor report too large for BTM candidate buffer");
+ ret = -1;
+ goto cleanup;
+ }
/* optional elements, skip */
data = end;
@@ -411,6 +1239,15 @@ cleanup:
static void roaming_app_neighbor_report_recv_internal_handler(void *ctx, void *data)
{
wifi_event_neighbor_report_t *neighbor_report_event = data;
+ roaming_app_untrack_timeout_user_data(neighbor_report_event);
+
+ if (!g_roaming_app.app_active) {
+ if (neighbor_report_event) {
+ os_free(neighbor_report_event);
+ }
+ return;
+ }
+
if (!g_roaming_app.rrm_request_active) {
ESP_LOGV(ROAMING_TAG, "Not the response for our Neighbor Report Request");
goto cleanup;
@@ -425,7 +1262,7 @@ static void roaming_app_neighbor_report_recv_internal_handler(void *ctx, void *d
ESP_LOGD(ROAMING_TAG, "Received cb for Neighbor Report Request");
uint8_t *pos = (uint8_t *)neighbor_report_event->n_report;
- uint8_t report_len = neighbor_report_event->report_len;
+ uint16_t report_len = neighbor_report_event->report_len;
if (!report_len) {
ESP_LOGE(ROAMING_TAG, "Neighbor report is empty");
goto cleanup;
@@ -451,6 +1288,9 @@ cleanup:
static void roaming_app_neighbor_report_recv_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
wifi_event_neighbor_report_t *event = (wifi_event_neighbor_report_t*)event_data;
+ if (!g_roaming_app.app_active) {
+ return;
+ }
if (!event) {
return;
}
@@ -463,6 +1303,8 @@ static void roaming_app_neighbor_report_recv_handler(void* arg, esp_event_base_t
if (eloop_register_timeout(0, 0, roaming_app_neighbor_report_recv_internal_handler, NULL, event_copy) != 0) {
os_free(event_copy);
+ } else {
+ roaming_app_track_timeout_user_data(event_copy);
}
}
#endif /*PERIODIC_RRM_MONITORING*/
@@ -471,13 +1313,35 @@ static void roaming_app_neighbor_report_recv_handler(void* arg, esp_event_base_t
static void roaming_app_rssi_low_internal_handler(void *ctx, void *data)
{
wifi_event_bss_rssi_low_t *event = data;
+ roaming_app_untrack_timeout_user_data(event);
+
+ if (!g_roaming_app.app_active) {
+ if (event) {
+ os_free(event);
+ }
+ return;
+ }
+
ESP_LOGI(ROAMING_TAG, "%s:bss rssi is=%ld", __func__, event->rssi);
- roaming_app_get_ap_info(&g_roaming_app.current_bss.ap);
+ if (!g_roaming_app.sta_connected || !g_roaming_app.config.low_rssi_roam_trigger) {
+ os_free(event);
+ return;
+ }
+
+ if (!roaming_app_get_ap_info(&g_roaming_app.current_bss.ap)) {
+ g_roaming_app.current_bss.ap.rssi = event->rssi;
+ }
determine_best_ap(0);
- g_roaming_app.current_low_rssi_threshold -= g_roaming_app.config.rssi_threshold_reduction_offset;
- ESP_LOGD(ROAMING_TAG, "Resetting RSSI Threshold to %d", g_roaming_app.current_low_rssi_threshold);
- esp_wifi_set_rssi_threshold(g_roaming_app.current_low_rssi_threshold);
+ int32_t next_threshold = g_roaming_app.current_low_rssi_threshold -
+ g_roaming_app.config.rssi_threshold_reduction_offset;
+ next_threshold = roaming_app_clamp_rssi_threshold(next_threshold);
+ if (esp_wifi_set_rssi_threshold(next_threshold) == ESP_OK) {
+ g_roaming_app.current_low_rssi_threshold = next_threshold;
+ ESP_LOGD(ROAMING_TAG, "Resetting RSSI Threshold to %d", g_roaming_app.current_low_rssi_threshold);
+ } else {
+ ESP_LOGW(ROAMING_TAG, "failed to reset RSSI threshold to %d", next_threshold);
+ }
os_free(event);
}
@@ -485,6 +1349,9 @@ static void roaming_app_rssi_low_internal_handler(void *ctx, void *data)
static void roaming_app_rssi_low_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
wifi_event_bss_rssi_low_t *event = event_data;
+ if (!g_roaming_app.app_active) {
+ return;
+ }
if (!event) {
return;
}
@@ -497,77 +1364,171 @@ static void roaming_app_rssi_low_handler(void* arg, esp_event_base_t event_base,
if (eloop_register_timeout(0, 0, roaming_app_rssi_low_internal_handler, NULL, event_copy) != 0) {
os_free(event_copy);
+ } else {
+ roaming_app_track_timeout_user_data(event_copy);
}
}
#endif
#if NETWORK_ASSISTED_ROAMING_ENABLED
-static void trigger_network_assisted_roam(void)
+static bool trigger_network_assisted_roam(struct cand_bss *bss)
{
- if (esp_wnm_send_bss_transition_mgmt_query(REASON_RSSI, g_roaming_app.btm_neighbor_list, 1) < 0) {
+ char selected_candidate[96] = {0};
+ char *query_list = NULL;
+ const char *btm_candidates = g_roaming_app.btm_neighbor_list;
+
+ if (!g_roaming_app.sta_connected) {
+ ESP_LOGD(ROAMING_TAG, "Skipping BTM query while disconnected");
+ return false;
+ }
+
+ if (bss && roaming_app_build_btm_candidate(bss, selected_candidate, sizeof(selected_candidate)) == 0) {
+ query_list = os_calloc(1, BTM_QUERY_LIST_MAX_LEN);
+ if (!query_list) {
+ ESP_LOGW(ROAMING_TAG, "Skipping BTM query due to memory allocation failure");
+ return false;
+ }
+ strlcpy(query_list, selected_candidate, BTM_QUERY_LIST_MAX_LEN);
+ if (g_roaming_app.btm_neighbor_list) {
+ size_t selected_len = strlen(query_list);
+ size_t neighbor_len = strlen(g_roaming_app.btm_neighbor_list);
+
+ if (selected_len + neighbor_len < BTM_QUERY_LIST_MAX_LEN) {
+ strlcat(query_list, g_roaming_app.btm_neighbor_list, BTM_QUERY_LIST_MAX_LEN);
+ } else {
+ ESP_LOGW(ROAMING_TAG, "Skipping cached neighbor list as explicit BTM candidate list is full");
+ }
+ }
+ btm_candidates = query_list;
+ }
+
+ if (esp_wnm_send_bss_transition_mgmt_query(REASON_RSSI, btm_candidates, 1) < 0) {
ESP_LOGD(ROAMING_TAG, "failed to send btm query");
+ os_free(query_list);
+ return false;
}
ESP_LOGD(ROAMING_TAG, "Sent BTM Query");
- gettimeofday(&g_roaming_app.last_roamed_time, NULL);
+ roaming_app_mark_roam_attempt();
#if LEGACY_ROAM_ENABLED
g_roaming_app.btm_attempt++;
#endif
+ os_free(query_list);
+ return true;
}
#endif /*NETWORK_ASSISTED_ROAMING*/
-#if LEGACY_ROAM_ENABLED
-static void trigger_legacy_roam(struct cand_bss *bss)
+static bool trigger_targeted_reconnect(struct cand_bss *bss, bool disconnect_first)
{
wifi_config_t wifi_cfg = {0};
- esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg);
+ esp_err_t err;
+
+ if (!bss) {
+ return false;
+ }
+
+ err = esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg);
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to read sta config for targeted reconnect: %s", esp_err_to_name(err));
+ return false;
+ }
+
+ if (disconnect_first) {
+ err = esp_wifi_internal_issue_disconnect(WIFI_REASON_BSS_TRANSITION_DISASSOC);
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to disconnect for targeted reconnect: %s", esp_err_to_name(err));
+ return false;
+ }
+ }
+
wifi_cfg.sta.channel = bss->channel;
wifi_cfg.sta.bssid_set = true;
os_memcpy(wifi_cfg.sta.bssid, bss->bssid, ETH_ALEN);
- esp_wifi_internal_issue_disconnect(WIFI_REASON_BSS_TRANSITION_DISASSOC);
+
+ err = esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to apply targeted reconnect config: %s", esp_err_to_name(err));
+ return false;
+ }
+ g_roaming_app.connect_hint_active = true;
+
+ err = esp_wifi_connect();
+ if (err != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to start targeted reconnect: %s", esp_err_to_name(err));
+ roaming_app_reset_connect_hint_state();
+ return false;
+ }
+
esp_wifi_roaming_set_current_bssid(bss->bssid);
- esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
- esp_wifi_connect();
- ESP_LOGI(ROAMING_TAG, "Disconnecting and connecting to "MACSTR" on account of better rssi",MAC2STR(bss->bssid));
- gettimeofday(&g_roaming_app.last_roamed_time, NULL);
+ if (disconnect_first) {
+ ESP_LOGI(ROAMING_TAG, "Disconnecting and connecting to " MACSTR " on account of better RSSI", MAC2STR(bss->bssid));
+ } else {
+ ESP_LOGI(ROAMING_TAG, "Connecting to " MACSTR " as the best recovery candidate", MAC2STR(bss->bssid));
+ }
+ roaming_app_mark_roam_attempt();
+#if LEGACY_ROAM_ENABLED
g_roaming_app.force_roam_ongoing = true;
-}
#endif /*LEGACY_ROAM_ENABLED*/
+ return true;
+}
void roaming_app_trigger_roam(struct cand_bss *bss)
{
struct timeval now;
+
+ if (!g_roaming_app.app_active) {
+ goto free_bss;
+ }
+
gettimeofday(&now, NULL);
- ESP_LOGD(ROAMING_TAG,"Processing trigger roaming request.");
- if (time_diff_sec(&now, &g_roaming_app.last_roamed_time) < g_roaming_app.config.backoff_time ) {
- ESP_LOGD(ROAMING_TAG,"Ignoring request as time difference to last request is %ld",time_diff_sec(&now, &g_roaming_app.last_roamed_time));
+ ESP_LOGD(ROAMING_TAG, "Processing trigger roaming request.");
+ if (g_roaming_app.sta_connected && roaming_app_roam_backoff_active(&now, 0)) {
+ ESP_LOGD(ROAMING_TAG, "Ignoring request as time difference to last roam attempt is %ld",
+ time_diff_sec(&now, &g_roaming_app.last_roam_attempt_time));
goto free_bss;
}
#if NETWORK_ASSISTED_ROAMING_ENABLED
- if (g_roaming_app.config.btm_roaming_enabled && g_roaming_app.current_bss.btm_support) {
+ if (g_roaming_app.sta_connected && g_roaming_app.config.btm_roaming_enabled && g_roaming_app.current_bss.btm_support) {
#if LEGACY_ROAM_ENABLED && NETWORK_ASSISTED_ROAMING_ENABLED
if (g_roaming_app.btm_attempt <= g_roaming_app.config.btm_retry_cnt) {
#endif
- trigger_network_assisted_roam();
- goto free_bss;
+ if (trigger_network_assisted_roam(bss)) {
+ goto free_bss;
+ }
#if LEGACY_ROAM_ENABLED && NETWORK_ASSISTED_ROAMING_ENABLED
} else {
ESP_LOGD(ROAMING_TAG, "Not Sending BTM query as this method has failed too many times.");
- g_roaming_app.btm_attempt = 0;
+ g_roaming_app.btm_attempt = 0;
}
#endif
}
#endif /*NETWORK_ASSISTED_ROAMING_ENABLED*/
+ if (!g_roaming_app.sta_connected) {
+ if (trigger_targeted_reconnect(bss, false)) {
+ goto free_bss;
+ }
+ }
#if LEGACY_ROAM_ENABLED
- if (g_roaming_app.config.legacy_roam_enabled) {
- trigger_legacy_roam(bss);
+ if (g_roaming_app.sta_connected && g_roaming_app.config.legacy_roam_enabled) {
+ if (trigger_targeted_reconnect(bss, true)) {
+ goto free_bss;
+ }
}
#endif /*LEGACY_ROAM_ENABLED*/
free_bss :
os_free(bss);
}
-void roaming_app_trigger_roam_internal_handler(void *ctx, void *data)
+static void roaming_app_trigger_roam_internal_handler(void *ctx, void *data)
{
+ roaming_app_untrack_timeout_user_data(data);
+
+ if (!g_roaming_app.app_active) {
+ if (data) {
+ os_free(data);
+ }
+ return;
+ }
+
if (!data) {
ESP_LOGE(ROAMING_TAG, "No data received for roaming event");
} else {
@@ -577,6 +1538,10 @@ void roaming_app_trigger_roam_internal_handler(void *ctx, void *data)
static int wifi_post_roam_event(struct cand_bss *bss)
{
+ if (!g_roaming_app.app_active) {
+ return -1;
+ }
+
if (bss) {
struct cand_bss *cand_bss = (struct cand_bss *)os_zalloc(sizeof(struct cand_bss));
if (!cand_bss) {
@@ -585,11 +1550,12 @@ static int wifi_post_roam_event(struct cand_bss *bss)
}
os_memcpy(cand_bss, bss, sizeof(struct cand_bss));
/* trigger the roaming event */
- if (eloop_register_timeout(0, 0, roaming_app_trigger_roam_internal_handler, NULL, (void*)cand_bss)) {
+ if (eloop_register_timeout(0, 0, roaming_app_trigger_roam_internal_handler, NULL, (void *)cand_bss)) {
ESP_LOGE(ROAMING_TAG, "Could not register roaming event.");
os_free(cand_bss);
return -1;
}
+ roaming_app_track_timeout_user_data(cand_bss);
} else {
ESP_LOGE(ROAMING_TAG, "Cannot trigger roaming event without any candidate APs");
return -1;
@@ -600,11 +1566,11 @@ static int wifi_post_roam_event(struct cand_bss *bss)
void print_ap_records(struct scanned_ap_info *ap_info)
{
- ESP_LOGD(ROAMING_TAG, "Scanned AP List");
+ ESP_LOGD(ROAMING_TAG, "Scanned AP List");
for (int i = 0; i < ap_info->current_count ; i++) {
ESP_LOGD(ROAMING_TAG, "%d. ssid : %s bssid :"MACSTR" channel : %d rssi : %d authmode : %d", i,
- ap_info->ap_records[i].ssid,MAC2STR(ap_info->ap_records[i].bssid),
- ap_info->ap_records[i].primary,ap_info->ap_records[i].rssi, ap_info->ap_records[i].authmode);
+ ap_info->ap_records[i].ssid, MAC2STR(ap_info->ap_records[i].bssid),
+ ap_info->ap_records[i].primary, ap_info->ap_records[i].rssi, ap_info->ap_records[i].authmode);
}
}
@@ -612,7 +1578,9 @@ void print_ap_records(struct scanned_ap_info *ap_info)
#if PERIODIC_RRM_MONITORING
static void periodic_rrm_request(struct timeval *now)
{
- roaming_app_get_ap_info(&g_roaming_app.current_bss.ap);
+ if (!roaming_app_get_ap_info(&g_roaming_app.current_bss.ap)) {
+ return;
+ }
if (esp_rrm_is_rrm_supported_connection() && (g_roaming_app.current_bss.ap.rssi < g_roaming_app.config.rrm_monitor_rssi_threshold)) {
if (esp_rrm_send_neighbor_report_request() < 0) {
ESP_LOGE(ROAMING_TAG, "failed to send neighbor report request");
@@ -625,60 +1593,149 @@ static void periodic_rrm_request(struct timeval *now)
static bool candidate_security_match(wifi_ap_record_t candidate)
{
-#if CONFIG_ESP_WIFI_ROAMING_PREVENT_DOWNGRADE
+ const u8 *rsn_ie = esp_wifi_sta_get_ie((u8 *) candidate.bssid, WLAN_EID_RSN);
+ const u8 *rsnxe_ie = roaming_app_get_rsnxe_ie(candidate.bssid);
+ struct wpa_ie_data rsn_data = {0};
+ struct wpa_ie_data rsnxe_data = {0};
+ bool candidate_has_rsn = false;
+ bool candidate_has_rsnxe = false;
u8 transition_disable = wpa_supplicant_get_transition_disable();
+ wifi_auth_mode_t curr_auth = g_roaming_app.current_bss.ap.authmode;
+ wifi_auth_mode_t cand_auth = candidate.authmode;
+ wifi_config_t wifi_cfg = {0};
+ bool candidate_supports_pmf;
+ bool candidate_supports_sae;
+ bool candidate_supports_owe;
+ bool candidate_supports_wpa3_enterprise;
+ bool candidate_supports_suite_b_192;
+ bool candidate_supports_sae_pk;
+ bool candidate_supports_sae_h2e;
+
+ if (esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg) != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "failed to read sta config while validating candidate security");
+ return false;
+ }
+
+ if (rsn_ie) {
+ if (wpa_parse_wpa_ie_rsn(rsn_ie, rsn_ie[1] + 2, &rsn_data) != 0) {
+ ESP_LOGW(ROAMING_TAG, "Skipping " MACSTR " due to invalid RSN IE", MAC2STR(candidate.bssid));
+ return false;
+ }
+ candidate_has_rsn = true;
+ }
+
+ if (rsnxe_ie) {
+ if (wpa_parse_wpa_ie_rsnxe(rsnxe_ie, rsnxe_ie[1] + 2, &rsnxe_data) != 0) {
+ ESP_LOGW(ROAMING_TAG, "Skipping " MACSTR " due to invalid/incompatible RSNXE", MAC2STR(candidate.bssid));
+ return false;
+ }
+ candidate_has_rsnxe = true;
+ }
+
+ candidate_supports_pmf = candidate_has_rsn && (rsn_data.capabilities & WPA_CAPABILITY_MFPC);
+ candidate_supports_sae = candidate_has_rsn ? wpa_key_mgmt_sae(rsn_data.key_mgmt) :
+ (cand_auth == WIFI_AUTH_WPA3_PSK || cand_auth == WIFI_AUTH_WPA2_WPA3_PSK);
+ candidate_supports_owe = candidate_has_rsn ? wpa_key_mgmt_owe(rsn_data.key_mgmt) :
+ (cand_auth == WIFI_AUTH_OWE);
+ candidate_supports_wpa3_enterprise = candidate_has_rsn ?
+ (!!(rsn_data.key_mgmt & (WPA_KEY_MGMT_IEEE8021X_SHA256 |
+ WPA_KEY_MGMT_IEEE8021X_SUITE_B |
+ WPA_KEY_MGMT_IEEE8021X_SUITE_B_192)) &&
+ candidate_supports_pmf) :
+ ((cand_auth == WIFI_AUTH_WPA3_ENTERPRISE ||
+ cand_auth == WIFI_AUTH_WPA2_WPA3_ENTERPRISE) &&
+ candidate_supports_pmf);
+ candidate_supports_suite_b_192 = candidate_has_rsn ? wpa_key_mgmt_sha384(rsn_data.key_mgmt) :
+ (cand_auth == WIFI_AUTH_WPA3_ENT_192);
+ candidate_supports_sae_pk = candidate_has_rsnxe &&
+ (rsnxe_data.rsnxe_capa & BIT(WLAN_RSNX_CAPAB_SAE_PK));
+ candidate_supports_sae_h2e = candidate_has_rsnxe &&
+ (rsnxe_data.rsnxe_capa & BIT(WLAN_RSNX_CAPAB_SAE_H2E));
+
+ if (wifi_cfg.sta.pmf_cfg.required && !candidate_supports_pmf) {
+ ESP_LOGV(ROAMING_TAG, "Rejecting " MACSTR " as PMF is required", MAC2STR(candidate.bssid));
+ return false;
+ }
+
+ if (wifi_cfg.sta.sae_pk_mode == WPA3_SAE_PK_MODE_ONLY &&
+ candidate_supports_sae && !candidate_supports_sae_pk) {
+ ESP_LOGV(ROAMING_TAG, "Rejecting " MACSTR " as SAE-PK is required", MAC2STR(candidate.bssid));
+ return false;
+ }
+
+ if (wifi_cfg.sta.sae_pwe_h2e == WPA3_SAE_PWE_HASH_TO_ELEMENT &&
+ candidate_supports_sae && !candidate_supports_sae_h2e) {
+ ESP_LOGV(ROAMING_TAG, "Rejecting " MACSTR " as SAE H2E is required", MAC2STR(candidate.bssid));
+ return false;
+ }
+
+ if (curr_auth == WIFI_AUTH_WPA3_ENT_192 &&
+ (!candidate_supports_suite_b_192 || !candidate_supports_pmf)) {
+ return false;
+ }
+
+ /* Transition Disable is a learned AP policy and should always be honored
+ * during candidate selection to avoid downgrade roams. */
if (transition_disable & TRANSITION_DISABLE_WPA3_PERSONAL) {
- if (candidate.authmode == WIFI_AUTH_WPA2_PSK) {
+ if (!candidate_supports_sae) {
return false;
}
}
+ if ((transition_disable & TRANSITION_DISABLE_SAE_PK) && !candidate_supports_sae_pk) {
+ return false;
+ }
if (transition_disable & TRANSITION_DISABLE_ENHANCED_OPEN) {
- if (candidate.authmode == WIFI_AUTH_OPEN) {
+ if (!candidate_supports_owe) {
return false;
}
}
if (transition_disable & TRANSITION_DISABLE_WPA3_ENTERPRISE) {
- if (candidate.authmode == WIFI_AUTH_WPA2_ENTERPRISE) {
+ if (!candidate_supports_wpa3_enterprise) {
return false;
}
}
-#if TODO // application doesn't have a way to know SAE-PK enabled AP atm
- if (transition_disable & TRANSITION_DISABLE_SAE_PK) {
- if (candidate.authmode == WIFI_AUTH_WPA3_PSK) {
+
+ if (!roaming_app_candidate_meets_threshold(wifi_cfg.sta.threshold.authmode,
+ curr_auth,
+ cand_auth,
+ candidate_supports_sae,
+ candidate_supports_owe,
+ candidate_supports_wpa3_enterprise,
+ candidate_supports_suite_b_192)) {
+ ESP_LOGV(ROAMING_TAG, "Authmode threshold failure %d -> %d",
+ wifi_cfg.sta.threshold.authmode, cand_auth);
+ return false;
+ }
+
+ if (roaming_app_authmode_is_open_mode(curr_auth) && roaming_app_authmode_is_open_mode(cand_auth)) {
+ if ((curr_auth == WIFI_AUTH_OWE || candidate_supports_owe) && !wifi_cfg.sta.owe_enabled) {
+ ESP_LOGV(ROAMING_TAG, "Open/OWE transition rejected as OWE is disabled");
return false;
}
}
-#endif
-#endif
- wifi_auth_mode_t curr_auth = g_roaming_app.current_bss.ap.authmode;
- wifi_auth_mode_t cand_auth = candidate.authmode;
+
ESP_LOGV(ROAMING_TAG, "Cand authmode : %d, Current Authmode : %d", cand_auth, curr_auth);
if (cand_auth == curr_auth) {
ESP_LOGV(ROAMING_TAG, "Authmode matched!");
return true;
}
- wifi_config_t wifi_cfg = {0};
- esp_wifi_get_config(WIFI_IF_STA, &wifi_cfg);
- if (wifi_cfg.sta.owe_enabled && OWE_COMPATIBLE(curr_auth, cand_auth)) {
- if (wifi_cfg.sta.threshold.authmode == WIFI_AUTH_OPEN) {
- ESP_LOGV(ROAMING_TAG, "transition between OWE and open permitted");
- return true;
- } else {
- ESP_LOGV(ROAMING_TAG, "transition between OWE and open not permitted");
- return false;
- }
- } else if (wifi_cfg.sta.threshold.authmode > cand_auth) {
- /* If the authmode of the candidate AP is less than our threshold, it
- * will fail during connection */
- ESP_LOGV(ROAMING_TAG, "Authmode threshold failure %d -> %d", wifi_cfg.sta.threshold.authmode, cand_auth);
- return false;
- } else if (PSK_COMPATIBLE(curr_auth, cand_auth)) {
- /*
- * PSK based authmodes are compatible with each other for roaming
- */
- ESP_LOGV(ROAMING_TAG, "Roaming between a PSK APs");
+
+ if (roaming_app_authmode_is_open_mode(curr_auth) && roaming_app_authmode_is_open_mode(cand_auth)) {
+ ESP_LOGV(ROAMING_TAG, "Roaming between open modes");
return true;
}
+
+ if (roaming_app_authmode_is_personal_compatible(curr_auth, cand_auth)) {
+ ESP_LOGV(ROAMING_TAG, "Roaming between personal modes");
+ return true;
+ }
+
+ if (roaming_app_authmode_is_enterprise(curr_auth) &&
+ roaming_app_authmode_is_enterprise(cand_auth)) {
+ ESP_LOGV(ROAMING_TAG, "Roaming between enterprise modes");
+ return true;
+ }
+
return false;
}
@@ -705,39 +1762,45 @@ static void remove_expired_blacklist_entries(void)
static bool is_bssid_blacklisted(const uint8_t *bssid)
{
#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ struct blacklist_entry *entry;
+
remove_expired_blacklist_entries();
- for (int i = 0; i < g_roaming_app.bssid_blacklist_count; i++) {
- if (memcmp(g_roaming_app.bssid_blacklist[i].bssid, bssid, ETH_ALEN) == 0) {
-#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
- if (g_roaming_app.bssid_blacklist[i].failures >= CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES) {
- return true;
- }
-#else
- return true;
-#endif
- }
- }
+ entry = roaming_app_find_blacklist_entry(bssid);
+ return roaming_app_blacklist_entry_blocks_roaming(entry);
#endif
+ (void) bssid;
return false;
}
static void parse_scan_results_and_roam(void)
{
+ struct timeval now;
+ const wifi_ap_record_t *current_scan_record = g_roaming_app.sta_connected ?
+ roaming_app_find_scan_record(g_roaming_app.current_bss.ap.bssid) :
+ NULL;
int8_t rssi_threshold = g_roaming_app.current_rssi_threshold;
- uint8_t best_rssi_diff = rssi_threshold;
+ int16_t current_rssi_baseline = g_roaming_app.current_bss.ap.rssi;
+ int16_t best_rssi_diff = rssi_threshold;
struct cand_bss *best_ap = NULL;
- int8_t rssi_diff = 0;
+ int16_t rssi_diff = 0;
uint8_t i;
int8_t best_ap_index = -1;
+
+ gettimeofday(&now, NULL);
+
+ if (current_scan_record && current_scan_record->rssi > current_rssi_baseline) {
+ current_rssi_baseline = current_scan_record->rssi;
+ }
+
for (i = 0; i < g_roaming_app.scanned_aps.current_count; i++) {
if (is_bssid_blacklisted(g_roaming_app.scanned_aps.ap_records[i].bssid)) {
ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " is blacklisted, skipping", MAC2STR(g_roaming_app.scanned_aps.ap_records[i].bssid));
continue;
}
- rssi_diff = g_roaming_app.scanned_aps.ap_records[i].rssi - g_roaming_app.current_bss.ap.rssi;
+ rssi_diff = g_roaming_app.scanned_aps.ap_records[i].rssi - current_rssi_baseline;
ESP_LOGD(ROAMING_TAG, "The difference between ("MACSTR", "MACSTR") with rssi (%d,%d) is : %d while the threshold is %d and the best rssi diff yet is %d, thecand_auth is %d",
MAC2STR(g_roaming_app.scanned_aps.ap_records[i].bssid),MAC2STR(g_roaming_app.current_bss.ap.bssid),
- g_roaming_app.scanned_aps.ap_records[i].rssi, g_roaming_app.current_bss.ap.rssi,
+ g_roaming_app.scanned_aps.ap_records[i].rssi, current_rssi_baseline,
rssi_diff, rssi_threshold, best_rssi_diff, g_roaming_app.scanned_aps.ap_records[i].authmode);
if ((memcmp(g_roaming_app.scanned_aps.ap_records[i].bssid, g_roaming_app.current_bss.ap.bssid, ETH_ALEN) != 0) &&
candidate_security_match(g_roaming_app.scanned_aps.ap_records[i]) && rssi_diff > best_rssi_diff ) {
@@ -756,6 +1819,10 @@ static void parse_scan_results_and_roam(void)
}
if (best_ap) {
+ if (!roaming_app_can_act_on_candidate(&now, true)) {
+ os_free(best_ap);
+ return;
+ }
ESP_LOGI(ROAMING_TAG,"Found a better AP "MACSTR" at channel %d", MAC2STR(best_ap->bssid), best_ap->channel);
if (wifi_post_roam_event(best_ap)) {
ESP_LOGE(ROAMING_TAG, "Posting of roaming event failed");
@@ -763,26 +1830,42 @@ static void parse_scan_results_and_roam(void)
os_free(best_ap);
} else {
ESP_LOGI(ROAMING_TAG, "Could not find a better AP with the threshold set to %d", g_roaming_app.current_rssi_threshold + 1);
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ roaming_app_retry_blacklist_recovery_if_needed();
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
}
}
static void scan_done_event_handler(void *arg, ETS_STATUS status)
{
if (status == ETS_OK) {
+ esp_err_t err;
+
ESP_LOGD(ROAMING_TAG, "Scan Done properly");
g_roaming_app.scanned_aps.current_count = MAX_CANDIDATE_COUNT;
- esp_wifi_scan_get_ap_records(&g_roaming_app.scanned_aps.current_count, g_roaming_app.scanned_aps.ap_records);
+ err = esp_wifi_scan_get_ap_records(&g_roaming_app.scanned_aps.current_count, g_roaming_app.scanned_aps.ap_records);
+ if (err != ESP_OK) {
+ g_roaming_app.scanned_aps.current_count = 0;
+ ESP_LOGW(ROAMING_TAG, "Failed to fetch scan results: %s", esp_err_to_name(err));
+ g_roaming_app.scan_ongoing = false;
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ roaming_app_retry_blacklist_recovery_if_needed();
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
+ return;
+ }
+ gettimeofday(&g_roaming_app.scanned_aps.time, NULL);
print_ap_records(&g_roaming_app.scanned_aps);
parse_scan_results_and_roam();
} else {
ESP_LOGD(ROAMING_TAG, "Scan Done with error %d ", status);
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ roaming_app_retry_blacklist_recovery_if_needed();
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
}
g_roaming_app.scan_ongoing = false;
}
static bool conduct_scan(void)
{
- gettimeofday(&g_roaming_app.scanned_aps.time, NULL);
- os_memset(&g_roaming_app.scanned_aps, 0, sizeof(struct scanned_ap_info));
/* Issue scan */
if (esp_wifi_promiscuous_scan_start(&g_roaming_app.config.scan_config, scan_done_event_handler) != ESP_OK) {
ESP_LOGE(ROAMING_TAG, "failed to issue scan");
@@ -795,14 +1878,26 @@ static bool conduct_scan(void)
static void determine_best_ap(int8_t rssi_threshold)
{
struct timeval now;
+
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
gettimeofday(&now, NULL);
+ if (!roaming_app_can_act_on_candidate(&now, true)) {
+ return;
+ }
+
/* If the scan results are recent enough or a scan is already ongoing we should not trigger a new scan */
if (!g_roaming_app.scan_ongoing) {
g_roaming_app.scan_ongoing = true;
g_roaming_app.current_rssi_threshold = rssi_threshold;
- if (time_diff_sec(&now,&g_roaming_app.scanned_aps.time) > SCAN_RESULTS_USABILITY_WINDOW) {
+ if (!roaming_app_scan_cache_is_valid(&now)) {
if (!conduct_scan()) {
g_roaming_app.scan_ongoing = false;
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ roaming_app_retry_blacklist_recovery_if_needed();
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
}
} else {
parse_scan_results_and_roam();
@@ -822,13 +1917,18 @@ static void periodic_scan_roam(struct timeval *now)
* as the results produced by a scan at this time would not be used by
* supplicant to build candidate lists.
* */
- if (time_diff_sec(now, &g_roaming_app.last_roamed_time) < g_roaming_app.config.backoff_time - SUPPLICANT_CANDIDATE_LIST_EXPIRY) {
+ if (roaming_app_roam_backoff_active(now, SUPPLICANT_CANDIDATE_LIST_EXPIRY)) {
return;
}
#endif /*NETWORK_ASSISTED_ROAMING_ENABLED && !LEGACY_ROAM_ENABLED*/
+ if (!roaming_app_connected_roam_method_available()) {
+ return;
+ }
/* If the current RSSI is not worse than the configured threshold
* for station initiated roam, then do not trigger roam */
- roaming_app_get_ap_info(&g_roaming_app.current_bss.ap);
+ if (!roaming_app_get_ap_info(&g_roaming_app.current_bss.ap)) {
+ return;
+ }
ESP_LOGD(ROAMING_TAG, "Connected AP's RSSI=%d", g_roaming_app.current_bss.ap.rssi);
if (g_roaming_app.current_bss.ap.rssi > g_roaming_app.config.scan_rssi_threshold) {
ESP_LOGD(ROAMING_TAG, "Not going for scan, Scan RSSI threshold=%d", g_roaming_app.config.scan_rssi_threshold);
@@ -844,6 +1944,10 @@ static void roaming_app_periodic_rrm_internal_handler(void *data, void *ctx)
{
struct timeval now;
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
if (!g_roaming_app.config.rrm_monitor) {
ESP_LOGI(ROAMING_TAG, "%s:RRM monitor is disabled in config", __func__);
return;
@@ -867,6 +1971,10 @@ static void roaming_app_periodic_scan_internal_handler(void *data, void *ctx)
{
struct timeval now;
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
if (!g_roaming_app.config.scan_monitor) {
ESP_LOGI(ROAMING_TAG, "%s: Scan monitor is disabled in config", __func__);
return;
@@ -884,7 +1992,7 @@ static void roaming_app_periodic_scan_internal_handler(void *data, void *ctx)
}
}
}
-#endif /*PERIODIC_SCAN_ROAM_MONITORING*/
+#endif /*PERIODIC_SCAN_MONITORING*/
static bool validate_scan_chan_list(const char* scan_chan_list)
{
regex_t regex;
@@ -1015,7 +2123,7 @@ static esp_err_t init_scan_config(void)
g_roaming_app.config.scan_config.scan_time.active.min = SCAN_TIME_MIN_DURATION;
g_roaming_app.config.scan_config.scan_time.active.max = SCAN_TIME_MAX_DURATION;
g_roaming_app.config.scan_config.home_chan_dwell_time = HOME_CHANNEL_DWELL_TIME;
- gettimeofday(&g_roaming_app.scanned_aps.time, NULL);
+ roaming_app_refresh_scan_config_filters();
return ESP_OK;
}
@@ -1031,6 +2139,7 @@ void roam_init_app(void)
return;
#endif
memset(&g_roaming_app, 0, sizeof(g_roaming_app));
+ g_roaming_app.app_active = true;
#if LOW_RSSI_ROAMING_ENABLED
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_BSS_RSSI_LOW,
&roaming_app_rssi_low_handler, NULL));
@@ -1044,6 +2153,44 @@ void roam_init_app(void)
ESP_LOGI(ROAMING_TAG, "Roaming app initialization done");
}
+static void roaming_app_cancel_pending_events(void)
+{
+ eloop_cancel_timeout(roaming_app_connected_event_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(roaming_app_disconnected_event_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(roaming_app_trigger_roam_internal_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(enable_reconnect, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(disable_reconnect, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+#if LOW_RSSI_ROAMING_ENABLED
+ eloop_cancel_timeout(roaming_app_rssi_low_internal_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+#endif /*LOW_RSSI_ROAMING_ENABLED*/
+#if PERIODIC_RRM_MONITORING
+ eloop_cancel_timeout(roaming_app_neighbor_report_recv_internal_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+#endif /*PERIODIC_RRM_MONITORING*/
+#if CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST
+ eloop_cancel_timeout(roaming_app_blacklist_recovery_internal_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(roaming_app_blacklist_add_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+ eloop_cancel_timeout(roaming_app_blacklist_remove_handler, ELOOP_ALL_CTX, ELOOP_ALL_CTX);
+#endif /*CONFIG_ESP_WIFI_ROAMING_BSSID_BLACKLIST*/
+ roaming_app_free_tracked_timeout_user_data();
+}
+
+static int roaming_app_deinit_internal(void *ctx, void *data)
+{
+ (void) ctx;
+ (void) data;
+ g_roaming_app.app_active = false;
+ roaming_app_cancel_pending_events();
+ roaming_app_stop_periodic_monitors();
+ roaming_app_reset_connect_hint_state();
+#if PERIODIC_RRM_MONITORING
+ if (g_roaming_app.btm_neighbor_list) {
+ os_free(g_roaming_app.btm_neighbor_list);
+ g_roaming_app.btm_neighbor_list = NULL;
+ }
+#endif /*PERIODIC_RRM_MONITORING*/
+ return ESP_OK;
+}
+
void roam_deinit_app(void)
{
#if !LOW_RSSI_ROAMING_ENABLED && !PERIODIC_SCAN_MONITORING
@@ -1055,25 +2202,23 @@ void roam_deinit_app(void)
ESP_LOGE(ROAMING_TAG, "No roaming trigger enabled. Roaming app cannot be de-initialized");
return;
#endif
+
+ if (!g_roaming_app.app_active) {
+ return;
+ }
+
+ if (roaming_app_run_blocking(roaming_app_deinit_internal, NULL) != ESP_OK) {
+ ESP_LOGW(ROAMING_TAG, "Failed to deinitialize roaming app on Wi-Fi task");
+ return;
+ }
+
#if LOW_RSSI_ROAMING_ENABLED
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_BSS_RSSI_LOW,
&roaming_app_rssi_low_handler));
#endif /*LOW_RSSI_ROAMING_ENABLED*/
-
-#if PERIODIC_SCAN_MONITORING
- g_roaming_app.periodic_scan_active = false;
- eloop_cancel_timeout(roaming_app_periodic_scan_internal_handler, NULL, NULL);
-#endif /*PERIODIC_SCAN_MONITORING*/
#if PERIODIC_RRM_MONITORING
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_NEIGHBOR_REP,
&roaming_app_neighbor_report_recv_handler));
- /* Disabling the periodic scan and RRM events */
- g_roaming_app.periodic_rrm_active = false;
- eloop_cancel_timeout(roaming_app_periodic_rrm_internal_handler, NULL, NULL);
- if (g_roaming_app.btm_neighbor_list) {
- os_free(g_roaming_app.btm_neighbor_list);
- g_roaming_app.btm_neighbor_list = NULL;
- }
#endif /*PERIODIC_RRM_MONITORING*/
}
@@ -1081,30 +2226,30 @@ void roam_deinit_app(void)
static void roaming_app_blacklist_add_handler(void *ctx, void *data)
{
uint8_t *bssid = data;
- if (g_roaming_app.bssid_blacklist_count >= CONFIG_ESP_WIFI_ROAMING_MAX_CANDIDATES) {
+ roaming_app_untrack_timeout_user_data(bssid);
+
+ if (!g_roaming_app.app_active) {
+ if (bssid) {
+ os_free(bssid);
+ }
+ return;
+ }
+
+ if (!roaming_app_add_manual_blacklist_entry(bssid)) {
ESP_LOGE(ROAMING_TAG, "Blacklist is full");
os_free(bssid);
return;
}
- for (int i = 0; i < g_roaming_app.bssid_blacklist_count; i++) {
- if (memcmp(g_roaming_app.bssid_blacklist[i].bssid, bssid, ETH_ALEN) == 0) {
- ESP_LOGD(ROAMING_TAG, "BSSID " MACSTR " already in blacklist", MAC2STR(bssid));
- os_free(bssid);
- return; // Already blacklisted
- }
- }
- memcpy(g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].bssid, bssid, ETH_ALEN);
-#if CONFIG_ESP_WIFI_ROAMING_AUTO_BLACKLISTING
- g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].failures = CONFIG_ESP_WIFI_ROAMING_MAX_CONN_FAILURES;
-#endif
- gettimeofday(&g_roaming_app.bssid_blacklist[g_roaming_app.bssid_blacklist_count].timestamp, NULL);
- g_roaming_app.bssid_blacklist_count++;
ESP_LOGI(ROAMING_TAG, "BSSID " MACSTR " added to blacklist", MAC2STR(bssid));
os_free(bssid);
}
esp_err_t esp_wifi_blacklist_add(const uint8_t *bssid)
{
+ if (!g_roaming_app.app_active) {
+ return ESP_ERR_INVALID_STATE;
+ }
+
if (!bssid) {
return ESP_ERR_INVALID_ARG;
}
@@ -1117,20 +2262,25 @@ esp_err_t esp_wifi_blacklist_add(const uint8_t *bssid)
os_free(bssid_copy);
return ESP_FAIL;
}
+ roaming_app_track_timeout_user_data(bssid_copy);
return ESP_OK;
}
static void roaming_app_blacklist_remove_handler(void *ctx, void *data)
{
uint8_t *bssid = data;
- int found_index = -1;
- for (int i = 0; i < g_roaming_app.bssid_blacklist_count; i++) {
- if (memcmp(g_roaming_app.bssid_blacklist[i].bssid, bssid, ETH_ALEN) == 0) {
- found_index = i;
- break;
+ int found_index;
+ roaming_app_untrack_timeout_user_data(bssid);
+
+ if (!g_roaming_app.app_active) {
+ if (bssid) {
+ os_free(bssid);
}
+ return;
}
+ found_index = roaming_app_find_blacklist_entry_index(bssid);
+
if (found_index != -1) {
// Shift elements to fill the gap
int remaining_entries = g_roaming_app.bssid_blacklist_count - found_index - 1;
@@ -1147,6 +2297,10 @@ static void roaming_app_blacklist_remove_handler(void *ctx, void *data)
esp_err_t esp_wifi_blacklist_remove(const uint8_t *bssid)
{
+ if (!g_roaming_app.app_active) {
+ return ESP_ERR_INVALID_STATE;
+ }
+
if (!bssid) {
return ESP_ERR_INVALID_ARG;
}
@@ -1159,22 +2313,82 @@ esp_err_t esp_wifi_blacklist_remove(const uint8_t *bssid)
os_free(bssid_copy);
return ESP_FAIL;
}
+ roaming_app_track_timeout_user_data(bssid_copy);
return ESP_OK;
}
#endif
-/* No need for this to be done in pptask ctx */
-esp_err_t roam_get_config_params(struct roam_config *config)
+static int roaming_app_get_config_params_internal(void *ctx, void *data)
{
+ (void) ctx;
+ struct roam_config *config = data;
+
memcpy(config, &g_roaming_app.config, sizeof(*config));
+ config->scan_config.ssid = config->scan_filter_ssid[0] != '\0' ? config->scan_filter_ssid : NULL;
+ config->scan_config.bssid = config->scan_filter_bssid_set ? config->scan_filter_bssid : NULL;
return ESP_OK;
}
+esp_err_t roam_get_config_params(struct roam_config *config)
+{
+ if (!config) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ return roaming_app_run_blocking(roaming_app_get_config_params_internal, config);
+}
+
static int update_config_params(void *data)
{
struct roam_config *config = data;
+ uint8_t next_scan_filter_ssid[ROAM_SCAN_FILTER_SSID_LEN] = {0};
+ uint8_t next_scan_filter_bssid[ROAM_SCAN_FILTER_BSSID_LEN] = {0};
+ bool next_scan_filter_bssid_set = false;
+ bool filters_changed;
+
+ if (config->scan_filter_ssid[0] != '\0') {
+ strlcpy((char *) next_scan_filter_ssid,
+ (const char *) config->scan_filter_ssid,
+ sizeof(next_scan_filter_ssid));
+ }
+
+ if (config->scan_filter_bssid_set) {
+ memcpy(next_scan_filter_bssid, config->scan_filter_bssid, sizeof(next_scan_filter_bssid));
+ next_scan_filter_bssid_set = true;
+ }
+
+ filters_changed = strcmp((const char *) g_roaming_app.config.scan_filter_ssid,
+ (const char *) next_scan_filter_ssid) != 0 ||
+ g_roaming_app.config.scan_filter_bssid_set != next_scan_filter_bssid_set ||
+ (next_scan_filter_bssid_set &&
+ memcmp(g_roaming_app.config.scan_filter_bssid,
+ next_scan_filter_bssid,
+ sizeof(next_scan_filter_bssid)) != 0);
+
g_roaming_app.config = *config;
+ memset(g_roaming_app.config.scan_filter_ssid, 0, sizeof(g_roaming_app.config.scan_filter_ssid));
+ memset(g_roaming_app.config.scan_filter_bssid, 0, sizeof(g_roaming_app.config.scan_filter_bssid));
+ g_roaming_app.config.scan_filter_bssid_set = false;
+
+ if (next_scan_filter_ssid[0] != '\0') {
+ strlcpy((char *) g_roaming_app.config.scan_filter_ssid,
+ (const char *) next_scan_filter_ssid,
+ sizeof(g_roaming_app.config.scan_filter_ssid));
+ }
+
+ if (next_scan_filter_bssid_set) {
+ memcpy(g_roaming_app.config.scan_filter_bssid,
+ next_scan_filter_bssid,
+ sizeof(g_roaming_app.config.scan_filter_bssid));
+ g_roaming_app.config.scan_filter_bssid_set = true;
+ }
+
+ roaming_app_refresh_scan_config_filters();
+ if (filters_changed) {
+ roaming_app_invalidate_scan_cache();
+ }
+ roaming_app_apply_runtime_config();
ESP_LOGI(ROAMING_TAG, "Updated Roaming app config :");
@@ -1200,14 +2414,44 @@ static int update_config_params(void *data)
return ESP_OK;
}
+static void roaming_app_normalize_config_filters(struct roam_config *config)
+{
+ if (!config) {
+ return;
+ }
+
+ if (config->scan_filter_ssid[0] == '\0' && config->scan_config.ssid) {
+ strlcpy((char *) config->scan_filter_ssid,
+ (const char *) config->scan_config.ssid,
+ sizeof(config->scan_filter_ssid));
+ }
+
+ if (!config->scan_filter_bssid_set && config->scan_config.bssid) {
+ memcpy(config->scan_filter_bssid,
+ config->scan_config.bssid,
+ sizeof(config->scan_filter_bssid));
+ config->scan_filter_bssid_set = true;
+ }
+
+ config->scan_config.ssid = NULL;
+ config->scan_config.bssid = NULL;
+}
+
esp_err_t roam_set_config_params(struct roam_config *config)
{
wifi_ipc_config_t cfg;
+ struct roam_config config_copy;
+
+ if (!config) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ config_copy = *config;
+ roaming_app_normalize_config_filters(&config_copy);
cfg.fn = update_config_params;
- cfg.arg = config;
- cfg.arg_size = sizeof(*config);
- esp_wifi_ipc_internal(&cfg, false);
+ cfg.arg = &config_copy;
+ cfg.arg_size = sizeof(config_copy);
- return ESP_OK;
+ return esp_wifi_ipc_internal(&cfg, false);
}
diff --git a/components/wpa_supplicant/src/common/ieee802_11_defs.h b/components/wpa_supplicant/src/common/ieee802_11_defs.h
index 86231b09a1b..4242084961f 100644
--- a/components/wpa_supplicant/src/common/ieee802_11_defs.h
+++ b/components/wpa_supplicant/src/common/ieee802_11_defs.h
@@ -939,6 +939,7 @@ enum phy_type {
PHY_TYPE_HT = 7,
PHY_TYPE_DMG = 8,
PHY_TYPE_VHT = 9,
+ PHY_TYPE_HE = 14,
};
/* IEEE P802.11-REVmc/D5.0, 9.4.2.37 - Neighbor Report element */
diff --git a/examples/wifi/roaming/roaming_app/sdkconfig.defaults b/examples/wifi/roaming/roaming_app/sdkconfig.defaults
index 12b72e0e88e..228d8f9e8e2 100644
--- a/examples/wifi/roaming/roaming_app/sdkconfig.defaults
+++ b/examples/wifi/roaming/roaming_app/sdkconfig.defaults
@@ -1,4 +1,3 @@
-CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n
CONFIG_ESP_WIFI_11KV_SUPPORT=y
CONFIG_ESP_WIFI_SCAN_CACHE=y