Chapter 35: Advanced WiFi Scan Features and Filtering
Volume 2: Connectivity Fundamentals – WiFi
Chapter Objectives
By the end of this chapter, you will be able to:
- Utilize the
wifi_scan_config_t
structure to customize scan behavior. - Filter scan results based on SSID, BSSID, or channel.
- Perform passive WiFi scans and understand their characteristics.
- Configure the ESP32 to scan for hidden networks.
- Adjust scan timing parameters and understand their impact.
- Implement targeted scanning strategies for specific use cases.
Introduction
In the previous chapter, we learned the fundamentals of WiFi scanning using default parameters, allowing the ESP32 to discover all nearby visible Access Points. While effective for general network discovery, this broad approach isn’t always optimal. Sometimes, we need more control:
- What if we only care about a specific network name (SSID)?
- What if we need to find a particular Access Point based on its MAC address (BSSID)?
- What if we want to minimize scan time by only checking a specific channel?
- What if we need to find a network that doesn’t broadcast its SSID (a “hidden” network)?
- Can we perform a less intrusive scan or adjust how long the ESP32 listens on each channel?
This chapter explores the advanced configuration options available for WiFi scanning in ESP-IDF. By leveraging the wifi_scan_config_t
structure, we can tailor the scanning process to be more efficient, targeted, and suitable for diverse application requirements, such as faster connections, specific AP monitoring, or reduced power consumption during scans.
Theory
The key to customizing the scan process lies in the wifi_scan_config_t
structure, which is passed as the first argument to esp_wifi_scan_start()
. Let’s examine its fields in detail:
typedef struct {
uint8_t *ssid; /**< Pointer to the SSID to scan for (NULL = all SSIDs) */
uint8_t *bssid; /**< Pointer to the BSSID (MAC address) to scan for (NULL = all BSSIDs) */
uint8_t channel; /**< Channel to scan (0 = all channels) */
bool show_hidden; /**< Send probe requests for hidden APs (true) or not (false) */
wifi_scan_type_t scan_type; /**< Scan type: WIFI_SCAN_TYPE_ACTIVE or WIFI_SCAN_TYPE_PASSIVE */
wifi_scan_time_t scan_time; /**< Structure defining scan time per channel */
} wifi_scan_config_t;
%%{ init: { 'flowchart': { 'curve': 'basis' } } }%% graph TD %% Node Styles classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef config fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0C4A6E; %% For config struct classDef result fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; A[<b>Start</b>: Need to Perform Customized Scan]:::primary B["Declare <code class='code-like'>wifi_scan_config_t scan_config;</code>"]:::config C["Initialize <code class='code-like'>scan_config</code> fields:<br>e.g., <code class='code-like'>memset(&scan_config, 0, sizeof(scan_config));</code> (Good practice)"]:::process D{Filter by SSID?}:::decision D -- "Yes" --> D_Y["Set <code class='code-like'>scan_config.ssid = (uint8_t*)\<i>TargetSSID\</i>;</code>"]:::config D -- "No" --> D_N["Set <code class='code-like'>scan_config.ssid = NULL;</code>"]:::config E{Filter by BSSID?}:::decision E -- "Yes" --> E_Y["Set <code class='code-like'>scan_config.bssid = target_bssid_mac_array;</code>"]:::config E -- "No" --> E_N["Set <code class='code-like'>scan_config.bssid = NULL;</code>"]:::config F{Scan Specific Channel?}:::decision F -- "Yes" --> F_Y["Set <code class='code-like'>scan_config.channel = target_channel_num;</code>"]:::config F -- "No" --> F_N["Set <code class='code-like'>scan_config.channel = 0;</code> (All Channels)"]:::config G{Scan for Hidden APs?}:::decision G -- "Yes" --> G_Y["Set <code class='code-like'>scan_config.show_hidden = true;</code><br>(Ensure <code class='code-like'>scan_config.ssid</code> is set for specific hidden AP)"]:::config G -- "No" --> G_N["Set <code class='code-like'>scan_config.show_hidden = false;</code>"]:::config H{Choose Scan Type?}:::decision H -- "Passive" --> H_P["Set <code class='code-like'>scan_config.scan_type = WIFI_SCAN_TYPE_PASSIVE;</code><br>Optional: Set <code class='code-like'>scan_config.scan_time.passive</code>"]:::config H -- "Active (Default)" --> H_A["Set <code class='code-like'>scan_config.scan_type = WIFI_SCAN_TYPE_ACTIVE;</code><br>Optional: Set <code class='code-like'>scan_config.scan_time.active</code>"]:::config I["Call <code class='code-like'>esp_wifi_scan_start(&scan_config, block_mode);</code>"]:::process J["Process Scan Results<br>(Get AP num, Get AP records, etc.)"]:::result A --> B --> C; C --> D; D_Y --> E; D_N --> E; E_Y --> F; E_N --> F; F_Y --> G; F_N --> G; G_Y --> H; G_N --> H; H_P --> I; H_A --> I; I --> J;
Filtering Scan Results
ssid
(uint8_t *): If you provide a pointer to a specific SSID (as a null-terminated byte array), the scan results returned byesp_wifi_scan_get_ap_records()
will only include APs matching that exact SSID. If set toNULL
, APs with any SSID are included. This is useful for quickly finding your target network without sifting through neighbors’ networks.bssid
(uint8_t *): If you provide a pointer to a specific 6-byte BSSID (MAC address), the scan will target only that specific Access Point. This is highly specific and useful if you know the exact AP you want information about, perhaps for monitoring or diagnostics. If set toNULL
, APs with any BSSID are included.channel
(uint8_t): If set to a specific channel number (typically 1-13 or 1-14 depending on region), the scan will only be performed on that single channel. This significantly speeds up the scan process if you know the channel your target network operates on, but it will miss all APs on other channels. If set to0
, the ESP32 scans all available channels according to its regulatory domain configuration.
Tip: You can combine filters. For example, setting both ssid
and channel
will find APs with the specified SSID only on the specified channel.
Scan Type (scan_type
)
This field determines how the ESP32 looks for networks:
Feature | Active Scan (WIFI_SCAN_TYPE_ACTIVE ) |
Passive Scan (WIFI_SCAN_TYPE_PASSIVE ) |
---|---|---|
Mechanism | Device transmits “Probe Request” frames on each channel and listens for “Probe Response” frames from APs. | Device listens on each channel for a period, waiting to receive “Beacon” frames periodically broadcast by APs. |
Discovery Speed |
|
|
Power Consumption |
|
|
Network Intrusiveness |
|
|
Hidden Network Discovery |
|
|
Typical Use Cases | Default scan type for most devices. Good for quick discovery and when hidden networks might be a target. | Power-sensitive applications. Scenarios where minimal RF interference is desired. Background monitoring. |
Default in ESP-IDF | Yes (if wifi_scan_config_t.scan_type is not set or config is NULL). |
No (must be explicitly set). |
WIFI_SCAN_TYPE_ACTIVE
(Default): The ESP32 transmits “Probe Request” frames on each channel and listens for “Probe Response” frames from APs.- Pros: Generally faster discovery, especially for networks that aren’t currently broadcasting beacons frequently. Can discover hidden networks if
show_hidden
is true. - Cons: Requires transmission, consuming slightly more power. Slightly more “noisy” on the RF spectrum.
- Pros: Generally faster discovery, especially for networks that aren’t currently broadcasting beacons frequently. Can discover hidden networks if
WIFI_SCAN_TYPE_PASSIVE
: The ESP32 simply listens on each channel for a defined period, waiting to receive periodic “Beacon” frames naturally broadcast by APs.- Pros: Lower power consumption as no transmission is required. Less intrusive (“quieter”).
- Cons: Slower, as it depends on passively receiving beacons. Might miss APs if the listen time per channel is too short or if APs beacon infrequently. Cannot discover hidden networks (as they don’t beacon their SSID).
Hidden Networks (show_hidden
)
Standard APs broadcast their SSID in beacon frames. “Hidden” networks (more accurately, networks with SSID broadcasting disabled) do not include the SSID in their beacons.
show_hidden = false
(Default): During an active scan, the ESP32 sends a generic Probe Request. Only non-hidden APs will respond with their SSID. Passive scans will also only reveal non-hidden SSIDs found in beacons.show_hidden = true
: During an active scan, the ESP32 sends directed Probe Requests, including the specific SSID if thessid
field inwifi_scan_config_t
is also set. If an AP with that SSID (even if hidden) receives this directed probe, it is required by the standard to respond, thus revealing itself in the scan results. Ifssid
is NULL butshow_hidden
is true, the ESP32 sends a probe request with a wildcard SSID, which might elicit responses from hidden networks, but behavior can vary between AP vendors. Note: This only works withWIFI_SCAN_TYPE_ACTIVE
. Passive scans cannot find hidden networks.
Warning: “Hiding” an SSID provides minimal security benefit and is primarily security through obscurity. It doesn’t encrypt the connection or prevent targeted discovery.
Scan Timing (scan_time
)
The wifi_scan_time_t
structure within wifi_scan_config_t
allows fine-tuning how long the ESP32 spends on each channel during a scan.
typedef struct {
uint32_t min; /**< Minimum duration in milliseconds */
uint32_t max; /**< Maximum duration in milliseconds */
} wifi_active_scan_time_t;
typedef uint32_t wifi_passive_scan_time_t; /**< Duration in milliseconds */
typedef struct {
wifi_active_scan_time_t active; /**< Configuration for active scan */
wifi_passive_scan_time_t passive; /**< Configuration for passive scan */
} wifi_scan_time_t;
active.min
/active.max
(ms): For active scans, the ESP32 stays on a channel for at leastmin
milliseconds. If APs respond quickly, it might move on. It will stay up tomax
milliseconds waiting for responses before moving to the next channel. Default values are typically around 120ms max.passive
(ms): For passive scans, this defines the fixed duration the ESP32 listens on each channel for beacons. Default is often around 120ms.
Impact of Timing:
- Shorter times: Faster overall scan completion. Risk of missing APs, especially in noisy environments or if AP beacons/responses are delayed.
- Longer times: Increased likelihood of discovering all APs, including weaker or slower ones. Slower overall scan completion. Increased power consumption for passive scans (longer listening).
Parameter Field in wifi_scan_time_t |
Description | Impact of Shorter Times | Impact of Longer Times | Default (Typical) |
---|---|---|---|---|
For Active Scan (scan_type = WIFI_SCAN_TYPE_ACTIVE ) |
||||
active.min (ms) |
Minimum duration the ESP32 stays on a channel listening for probe responses after sending a probe request. | Faster channel switching if APs respond quickly. May miss slower APs. | More time to receive responses, but can slow down scan if APs respond fast anyway. | ~20-30 ms |
active.max (ms) |
Maximum duration the ESP32 stays on a channel waiting for probe responses before moving to the next channel. | Faster overall scan. Higher risk of missing APs, especially in noisy environments or if APs are slow to respond. | Increased likelihood of discovering all APs, including weaker or slower ones. Slower overall scan completion. | ~120-300 ms |
For Passive Scan (scan_type = WIFI_SCAN_TYPE_PASSIVE ) |
||||
passive (ms) |
Fixed duration the ESP32 listens on each channel for beacon frames. | Faster overall scan. Higher risk of missing APs if their beacon interval is long or if beacons are missed during the short listen window. | Increased likelihood of receiving beacons from all APs on the channel. Slower overall scan completion. Increased power consumption (longer radio ON time). | ~120-300 ms |
Practical Examples
These examples build upon the basic scan code from Chapter 34. We’ll focus on modifying the wifi_scan_config_t
structure. Assume the wifi_init
, NVS initialization, app_main
, and get_auth_mode_str
function remain similar to the previous chapter’s example unless noted.
Example 1: Scanning for a Specific SSID
This example scans only for networks named “MyTargetNetwork”.
1. Modify wifi_scan
function:
// Inside the wifi_scan function, before calling esp_wifi_scan_start:
// Target SSID (replace with your actual target network name)
const char *target_ssid = "MyTargetNetwork";
wifi_scan_config_t scan_config = {
.ssid = (uint8_t *)target_ssid, // Point to the target SSID
.bssid = NULL, // Any BSSID
.channel = 0, // All channels
.show_hidden = false, // Standard scan for visible networks
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
// .scan_time = { ... } // Default timing
};
ESP_LOGI(TAG, "Starting scan specifically for SSID: %s", target_ssid);
// Initiate Scan (Blocking) - Pass the config structure
esp_err_t scan_result = esp_wifi_scan_start(&scan_config, true);
// ... (rest of the scan result processing code remains the same)
2. Build, Flash, Monitor:
Run the code. The output should now only list Access Points with the SSID “MyTargetNetwork”. If none are found, it will report 0 access points.
I (XXX) WIFI_SCAN: Starting scan specifically for SSID: MyTargetNetwork
...
I (XXX) WIFI_SCAN: Scan completed successfully.
I (XXX) WIFI_SCAN: Found 1 access points: // Or 0 or more, if multiple APs share the SSID
I (XXX) WIFI_SCAN: Retrieved 1 records (expected 1):
I (XXX) WIFI_SCAN: [0] SSID: MyTargetNetwork
I (XXX) WIFI_SCAN: BSSID: xx:xx:xx:xx:xx:xx
I (XXX) WIFI_SCAN: Channel: 6
I (XXX) WIFI_SCAN: RSSI: -60 dBm
I (XXX) WIFI_SCAN: Auth Mode: WPA2_PSK
I (XXX) WIFI_SCAN: --------------------
...
Example 2: Scanning on a Specific Channel
This example scans only channel 6.
1. Modify wifi_scan
function:
// Inside the wifi_scan function, before calling esp_wifi_scan_start:
uint8_t target_channel = 6;
wifi_scan_config_t scan_config = {
.ssid = NULL, // Any SSID
.bssid = NULL, // Any BSSID
.channel = target_channel, // Scan ONLY channel 6
.show_hidden = false,
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
// .scan_time = { ... } // Default timing
};
ESP_LOGI(TAG, "Starting scan only on channel: %d", target_channel);
// Initiate Scan (Blocking) - Pass the config structure
esp_err_t scan_result = esp_wifi_scan_start(&scan_config, true);
// ... (rest of the scan result processing code)
2. Build, Flash, Monitor:
The output will now only show APs operating on channel 6. The scan should complete noticeably faster as it only checks one channel.
I (XXX) WIFI_SCAN: Starting scan only on channel: 6
...
I (XXX) WIFI_SCAN: Scan completed successfully.
I (XXX) WIFI_SCAN: Found 2 access points: // Example: 2 APs found on channel 6
I (XXX) WIFI_SCAN: Retrieved 2 records (expected 2):
I (XXX) WIFI_SCAN: [0] SSID: NetworkOnCh6_A
I (XXX) WIFI_SCAN: BSSID: ...
I (XXX) WIFI_SCAN: Channel: 6 // Should always be 6
I (XXX) WIFI_SCAN: RSSI: -58 dBm
I (XXX) WIFI_SCAN: Auth Mode: WPA2_PSK
I (XXX) WIFI_SCAN: --------------------
I (XXX) WIFI_SCAN: [1] SSID: NetworkOnCh6_B
I (XXX) WIFI_SCAN: BSSID: ...
I (XXX) WIFI_SCAN: Channel: 6 // Should always be 6
I (XXX) WIFI_SCAN: RSSI: -72 dBm
I (XXX) WIFI_SCAN: Auth Mode: WPA_WPA2_PSK
I (XXX) WIFI_SCAN: --------------------
...
Example 3: Performing a Passive Scan
This example uses passive scanning across all channels.
1. Modify wifi_scan
function:
// Inside the wifi_scan function, before calling esp_wifi_scan_start:
wifi_scan_config_t scan_config = {
.ssid = NULL,
.bssid = NULL,
.channel = 0, // All channels
.show_hidden = false, // Passive scan cannot find hidden networks
.scan_type = WIFI_SCAN_TYPE_PASSIVE, // Set scan type to PASSIVE
.scan_time.passive = 200 // Optional: Listen for 200ms per channel (default ~120ms)
};
ESP_LOGI(TAG, "Starting PASSIVE WiFi scan...");
// Initiate Scan (Blocking) - Pass the config structure
esp_err_t scan_result = esp_wifi_scan_start(&scan_config, true);
// ... (rest of the scan result processing code)
2. Build, Flash, Monitor:
Observe the output. The scan might take slightly longer than the default active scan. Depending on the environment and timing, it might occasionally miss some APs compared to an active scan. Hidden networks will not appear.
Example 4: Scanning for Hidden Networks
This example attempts to find a specific hidden network named “MyHiddenNet”.
Important: You must know the SSID of the hidden network you are looking for.
1. Modify wifi_scan
function:
// Inside the wifi_scan function, before calling esp_wifi_scan_start:
// Target SSID (replace with the actual hidden network name)
const char *hidden_ssid = "MyHiddenNet";
wifi_scan_config_t scan_config = {
.ssid = (uint8_t *)hidden_ssid, // MUST provide the SSID to probe for
.bssid = NULL, // Any BSSID
.channel = 0, // All channels
.show_hidden = true, // Enable probing for hidden networks
.scan_type = WIFI_SCAN_TYPE_ACTIVE, // MUST be active scan
// .scan_time = { ... } // Default timing
};
ESP_LOGI(TAG, "Starting ACTIVE scan for specific (possibly hidden) SSID: %s", hidden_ssid);
// Initiate Scan (Blocking) - Pass the config structure
esp_err_t scan_result = esp_wifi_scan_start(&scan_config, true);
// ... (rest of the scan result processing code)
2. Build, Flash, Monitor:
If an AP with the exact SSID “MyHiddenNet” is nearby (even if its SSID broadcast is disabled), it should respond to the directed probe request and appear in the scan results. If the network doesn’t exist or is out of range, 0 APs will be reported.
I (XXX) WIFI_SCAN: Starting ACTIVE scan for specific (possibly hidden) SSID: MyHiddenNet
...
I (XXX) WIFI_SCAN: Scan completed successfully.
I (XXX) WIFI_SCAN: Found 1 access points: // If the hidden network responded
I (XXX) WIFI_SCAN: Retrieved 1 records (expected 1):
I (XXX) WIFI_SCAN: [0] SSID: MyHiddenNet // The SSID is revealed in the result
I (XXX) WIFI_SCAN: BSSID: zz:yy:xx:ww:vv:uu
I (XXX) WIFI_SCAN: Channel: 11
I (XXX) WIFI_SCAN: RSSI: -65 dBm
I (XXX) WIFI_SCAN: Auth Mode: WPA2_PSK
I (XXX) WIFI_SCAN: --------------------
...
Variant Notes
- ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6: All advanced scanning features described in this chapter, including filtering, scan types, hidden network scanning, and timing adjustments via
wifi_scan_config_t
, are fully supported and function consistently across these WiFi-enabled variants. - ESP32-H2: As this variant lacks 802.11 WiFi hardware, this chapter is not applicable.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Solution / Best Practice |
---|---|---|
Incorrect ssid or bssid Pointer/Data |
Scan fails (esp_wifi_scan_start error), crashes due to invalid memory access, or filtering doesn’t work. |
Ensure ssid points to a valid, null-terminated string. Ensure bssid points to a valid 6-byte MAC array. Memory must remain valid during scan. |
Passive Scan Expecting Hidden APs | Hidden networks are never found, even if show_hidden = true is set. |
Passive scans cannot find hidden networks. Use WIFI_SCAN_TYPE_ACTIVE , show_hidden = true , AND provide the specific ssid of the hidden network. |
Scan Times Too Short | Scan completes quickly but frequently misses APs, especially those further away or in noisy environments. | Use default timings or tune cautiously. Start with values around 100-150ms for active.max or passive time. Test thoroughly. |
Channel Filter Misses Target AP | Specifying a channel different from the AP’s actual operating channel results in the AP not being found. | Only filter by channel if certain of the AP’s channel. Otherwise, scan all channels (channel = 0 ) first to discover the AP and its current channel. |
Using show_hidden = true Without Specific SSID |
Expecting all hidden networks to appear. Results may be unreliable or vendor-dependent for wildcard hidden scans. | For reliable hidden AP discovery, provide the exact ssid in wifi_scan_config_t along with show_hidden = true and active scan type. |
Misunderstanding scan_time.active.min vs .max |
Setting min too high can slow down scans unnecessarily if APs respond quickly. Setting max too low can miss APs. |
min is how long to listen at least; max is the upper limit. Balance responsiveness with discovery reliability. |
Exercises
- Combined Filtering: Modify the scan configuration to find only APs that match a specific
ssid
and operate on a specificchannel
. Test this by configuring an AP you control or by observing your local environment. - Active vs. Passive Comparison: Write code that performs an active scan (all channels, default settings), prints the results and the time taken (use
esp_timer_get_time()
before and afteresp_wifi_scan_start
). Then, immediately perform a passive scan (all channels, e.g., 150ms passive time), print its results and time taken. Compare the list of found APs and the duration for both scan types in your environment. - Scan Time Tuning: Experiment with the
scan_time.active.max
setting for active scans. Start with a low value (e.g., 50ms), run the scan, and note the number of APs found. Gradually increase the value (e.g., 100ms, 150ms, 300ms) and repeat the scan. Observe how the number of discovered APs changes (if at all) in your environment as the scan time per channel increases.
Summary
- The
wifi_scan_config_t
structure provides fine-grained control over the WiFi scanning process. - Scans can be filtered by specific
ssid
,bssid
, orchannel
to target specific networks or speed up the process. scan_type
allows choosing betweenWIFI_SCAN_TYPE_ACTIVE
(faster, requires transmission, can find hidden) andWIFI_SCAN_TYPE_PASSIVE
(slower, lower power, cannot find hidden).- Setting
show_hidden = true
along with a specificssid
andWIFI_SCAN_TYPE_ACTIVE
enables discovery of known hidden networks. scan_time
parameters control the duration spent on each channel, affecting scan speed versus discovery reliability.- Advanced scanning techniques allow for more efficient and targeted network discovery tailored to application needs.
Further Reading
- ESP-IDF API Reference –
esp_wifi.h
: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/network/esp_wifi.html (Focus onwifi_scan_config_t
and its members) - ESP-IDF WiFi Scan Example: https://github.com/espressif/esp-idf/tree/v5.4/examples/wifi/scan (Review the example for usage of scan configuration)