Chapter 63: BLE Scanning and Filtering
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and mechanics of BLE scanning.
- Differentiate between active and passive scanning modes.
- Configure scan parameters like interval, window, type, and duration using ESP-IDF.
- Process and interpret scan results, including advertising data, RSSI, and device addresses.
- Implement application-level filtering of scan results based on various criteria (e.g., name, service UUIDs, manufacturer data).
- Utilize ESP-IDF APIs for starting, stopping, and managing the scanning process.
- Understand the basics of scanning for BLE 5.0 extended advertisements.
- Troubleshoot common issues related to BLE scanning.
Introduction
In the previous chapter on BLE advertising, we learned how a peripheral device makes its presence known. Now, we shift our perspective to the central device, which needs to find these peripherals. This process of listening for advertisements is called scanning. Scanning is the central’s way of “looking around” to see which BLE devices are nearby and what they are offering.
Imagine walking into a large hall where many people are making announcements. Scanning is like listening to these announcements to find the person or service you’re interested in. For an ESP32 acting as a central device (e.g., a hub collecting data from multiple sensors, or a controller for smart devices), effective scanning is the first step towards interaction. It’s not just about finding any device, but often about finding specific devices based on their characteristics or advertised services.
This chapter will explore the intricacies of BLE scanning using ESP-IDF v5.x. We’ll cover how to configure scan operations, process the received advertising data, and implement filtering techniques to identify the target peripherals in a potentially crowded BLE environment.
Theory
Scanning is a fundamental operation for a BLE device acting in the Central role. It allows the Central to discover advertising Peripherals.
The Purpose of Scanning
Scanning serves several key purposes for a Central device:
- Device Discovery: The primary goal is to find nearby BLE devices that are advertising their presence.
- Information Gathering: Advertisements often contain useful information like the device’s name, services it offers, or manufacturer-specific data. This helps the Central decide if it wants to connect.
- Connection Initiation: After discovering a suitable connectable peripheral, the Central can initiate a connection based on the information obtained from the scan result (primarily the device’s address).
- Receiving Broadcast Data: Some peripherals operate in a broadcast-only mode (non-connectable advertising, like beacons). Scanning allows Centrals to receive this broadcast data without establishing a connection.
The Scanning Process
A Central device performs scanning by periodically listening on the three primary advertising channels (37, 38, and 39). The timing of this listening is controlled by two main parameters:
- Scan Interval: The interval at which the scanner starts listening on an advertising channel. It’s the total duration from the beginning of one scan window to the beginning of the next.
- Scan Window: The duration for which the scanner actively listens on an advertising channel within each scan interval.
The scan_window
must be less than or equal to the scan_interval
. The ratio (scan_window / scan_interval)
defines the duty cycle of the scanning operation.
- A 100% duty cycle (
scan_window == scan_interval
) means the scanner is continuously listening (cycling through advertising channels). This provides the fastest discovery but consumes the most power. - A lower duty cycle means the scanner sleeps more between listening periods, saving power but potentially taking longer to discover all nearby advertisers.
%%{init: {"theme": "base", "themeVariables": { "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937", "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif", "git0": "#EDE9FE", "git1": "#DBEAFE", "git2": "#FEF3C7", "git3": "#D1FAE5", "gitBranchLabel0": "#5B21B6", "gitBranchLabel1": "#1E40AF", "gitBranchLabel2": "#92400E", "gitBranchLabel3": "#065F46" }}}%% gantt title BLE Scan Timing dateFormat X axisFormat %S s section Scanning Cycle 1 Scan Interval :interval1, 0, 100 Scan Window :active, 0, 30 Device Idle/Processing :crit, 30, 70 section Scanning Cycle 2 Scan Interval :interval2, 100, 100 Scan Window :active, 100, 30 Device Idle/Processing :crit, 130, 70 section Scanning Cycle 3 (Continuous Example) Scan Interval (100% Duty) :interval3, 200, 50 Scan Window (100% Duty) :active, 200, 50 %% Class Definitions for Colors (using gitN as placeholders for custom colors) %% classDef interval fill:#FEF3C7,stroke:#D97706,color:#92400E %% Decision Node Yellow %% classDef active fill:#DBEAFE,stroke:#2563EB,color:#1E40AF %% Process Node Blue %% classDef idle fill:#F3F4F6,stroke:#9CA3AF,color:#4B5563 %% Lighter Grey %% Notes: %% - Scan Interval: Total time from the start of one scan to the start of the next. %% - Scan Window: Duration within the interval when the device actively listens for advertisements. %% - Scan Window <= Scan Interval. %% - Shaded area (Scan Window) is when the radio is ON for scanning. %% - Unshaded area within Scan Interval (Device Idle/Processing) is when radio might be OFF or processing other tasks. %% - Cycle 3 shows a 100% duty cycle where Scan Window = Scan Interval (continuous scan).
Active vs. Passive Scanning
There are two main modes of scanning:
- Passive Scanning:
- The scanner only listens for advertising packets (like
ADV_IND
,ADV_NONCONN_IND
,ADV_SCAN_IND
) on the advertising channels. - It does not transmit any packets itself during the scanning process.
- It receives only the initial advertising data payload (up to 31 bytes for legacy advertisements).
- Analogy: Silently listening to public announcements without asking for more details.
- The scanner only listens for advertising packets (like
- Active Scanning:
- When the scanner receives an advertisment PDU that is “scannable” (e.g.,
ADV_IND
orADV_SCAN_IND
), it can transmit a Scan Request (SCAN_REQ
) PDU back to the advertiser on the same advertising channel. - The advertiser, upon receiving the
SCAN_REQ
, responds with a Scan Response (SCAN_RSP
) PDU. - This Scan Response packet can contain an additional 31 bytes of data (for legacy advertisements).
- Active scanning allows the Central to gather more information about the peripheral than what is available in the initial advertisement alone.
- Analogy: Hearing an announcement and then asking the announcer for a pamphlet with more details.
- When the scanner receives an advertisment PDU that is “scannable” (e.g.,
Feature | Passive Scanning (BLE_SCAN_TYPE_PASSIVE ) |
Active Scanning (BLE_SCAN_TYPE_ACTIVE ) |
---|---|---|
Scanner Transmits? | ✘ No (Listens only) | ✔ Yes (Sends SCAN_REQ if advertiser is scannable) |
Data Received | Advertising Data (AdvData) only (up to 31 bytes legacy). | Advertising Data (AdvData) + Scan Response Data (ScanRspData) if advertiser provides it (up to 31+31 bytes legacy). |
Information Gathered | Basic information from initial advertisement. | Potentially more detailed information (e.g., full device name, additional service UUIDs). |
Power Consumption | Generally lower (no TX). | Slightly higher (due to TX of SCAN_REQ ). |
Advertiser Interaction | Advertiser is unaware of the passive scanner. | Advertiser receives SCAN_REQ and sends SCAN_RSP . |
Use When | Sufficient info in AdvData, minimizing power, or just presence detection. | Need more info than AdvData provides, device name often in ScanRspData. |
Scan Parameters in ESP-IDF
The esp_ble_scan_params_t
structure in ESP-IDF is used to configure scanning behavior:
typedef struct {
esp_ble_scan_type_t scan_type; /*!< Scan type, active or passive */
esp_ble_addr_type_t own_addr_type; /*!< Owner address type */
esp_ble_scan_filter_t scan_filter_policy; /*!< Scan filter policy */
uint16_t scan_interval; /*!< Scan interval. This is defined as the time interval from when the Controller started its last LE scan until it begins the subsequent LE scan. Range: 0x0004 to 0x4000. Time = N * 0.625 msec. Time Range: 2.5 msec to 10.24 seconds */
uint16_t scan_window; /*!< Scan window. The duration of the LE scan. Range: 0x0004 to 0x4000. Time = N * 0.625 msec. Time Range: 2.5 msec to 10.24 seconds. Scan window shall be less than or equal to scan interval */
esp_ble_scan_dup_t scan_duplicate; /*!< Scan duplicate filter policy */
} esp_ble_scan_params_t;
scan_type
:BLE_SCAN_TYPE_PASSIVE
: Perform passive scanning.BLE_SCAN_TYPE_ACTIVE
: Perform active scanning.
own_addr_type
: The BLE address type the scanner itself will use (e.g.,BLE_ADDR_TYPE_PUBLIC
,BLE_ADDR_TYPE_RPA_PUBLIC
).scan_filter_policy
:BLE_SCAN_FILTER_ALLOW_ALL
: Accept advertising packets from all devices (default). No whitelist is used.BLE_SCAN_FILTER_ALLOW_ONLY_WLST
: Accept advertising packets only from devices present in the whitelist.- (Other options exist for handling directed advertisements and more complex whitelist scenarios).
scan_interval
: As described above. For continuous scanning, setscan_interval
andscan_window
to the same value.scan_window
: As described above.scan_duplicate
: Controls how duplicate advertising packets from the same device are handled by the controller.BLE_SCAN_DUPLICATE_DISABLE
: All advertising PDUs are reported to the host.BLE_SCAN_DUPLICATE_ENABLE
: Duplicate advertising PDUs are filtered out by the controller during a scan period (default). Only the first advertisement from a device (or first after its data changes) is reported.BLE_SCAN_DUPLICATE_MAX
: (ESP-IDF specific extension) Allows configuring the number of duplicate packets to receive before filtering.
Processing Scan Results
When the BLE controller receives an advertising packet that matches the scan parameters, it forwards the information to the host stack. In ESP-IDF, this results in an ESP_GAP_BLE_SCAN_RESULT_EVT
being delivered to the registered GAP callback function.
The esp_ble_gap_cb_param_t->scan_rst
structure within this event contains:
search_evt
: Type of scan result event (e.g.,ESP_GAP_SEARCH_INQ_RES_EVT
for a new advertisement,ESP_GAP_SEARCH_INQ_CMPL_EVT
when scan duration ends).bda
: Bluetooth Device Address (MAC address) of the advertiser.dev_type
: Device type (e.g.,ESP_BT_DEVICE_TYPE_BLE
).ble_addr_type
: Address type of the advertiser (public, random, RPA).rssi
: Received Signal Strength Indicator (in dBm, e.g., -50, -80). A higher value (closer to 0) means a stronger signal.adv_data_len
andble_adv
: Length and pointer to the raw advertising data or scan response data.scan_rsp
: A boolean flag,true
if this event contains scan response data,false
if it’s advertising data.flag
: The value of the Flags AD structure from the advertisement.
Field in scan_result->scan_rst |
Type | Description |
---|---|---|
search_evt |
esp_gap_search_evt_t |
Type of scan result event. Common value: ESP_GAP_SEARCH_INQ_RES_EVT (new advertisement/scan response received). ESP_GAP_SEARCH_INQ_CMPL_EVT (scan duration ended). |
bda |
uint8_t[6] (BD_ADDR_LEN) |
Bluetooth Device Address (MAC address) of the advertising peripheral. |
dev_type |
esp_bt_dev_type_t |
Device type (e.g., ESP_BT_DEVICE_TYPE_BLE , ESP_BT_DEVICE_TYPE_DUMO for dual-mode). |
ble_addr_type |
esp_ble_addr_type_t |
Address type of the advertiser (public, random, RPA etc.). |
rssi |
int8_t |
Received Signal Strength Indicator in dBm (e.g., -50, -80). Higher value (closer to 0) indicates stronger signal. |
adv_data_len |
uint8_t |
Length of the raw advertising data or scan response data in ble_adv . |
ble_adv |
uint8_t[] (pointer) |
Pointer to the raw advertising data or scan response data (sequence of AD Structures). |
scan_rsp |
bool |
true if this event contains scan response data, false if it’s advertising data. |
flag |
uint8_t |
Value of the Flags AD structure from the advertisement (if present). Useful for quick checks on discoverability mode, BR/EDR support. |
Parsing Advertising Data:
The raw ble_adv data is a sequence of AD Structures (Length, Type, Data). The ESP-IDF provides a helper function to extract specific AD types:
uint8_t *esp_ble_resolve_adv_data(uint8_t *adv_data, esp_ble_adv_data_type_t type, uint8_t *length)
adv_data
: Pointer to the raw advertising data buffer.type
: The AD Type to search for (e.g.,ESP_BLE_AD_TYPE_NAME_CMPL
,ESP_BLE_AD_TYPE_16SRV_CMPL
).length
: Output parameter, will be filled with the length of the found data.- Returns: A pointer to the start of the data for the specified AD Type within
adv_data
, orNULL
if not found.
Application-Level Filtering
While the controller offers basic filtering (whitelist, duplicates), applications often need more sophisticated filtering logic based on the content of the advertisements:
Filtering Criterion | Description | How to Implement in ESP_GAP_BLE_SCAN_RESULT_EVT Handler |
---|---|---|
Device Address (BDA/MAC) | Filter for specific, known devices based on their unique Bluetooth address. | Compare scan_result->scan_rst.bda with a target BDA. |
Local Name | Search for devices advertising a particular name (complete or shortened). | Use esp_ble_resolve_adv_data() with ESP_BLE_AD_TYPE_NAME_CMPL or ESP_BLE_AD_TYPE_NAME_SHORT . Compare the extracted name string. |
Service UUIDs | Find devices that offer specific services (e.g., Heart Rate, Battery Service). Very common. | Use esp_ble_resolve_adv_data() with ESP_BLE_AD_TYPE_16SRV_CMPL , ESP_BLE_AD_TYPE_128SRV_CMPL , etc. Iterate through the list of UUIDs and compare. |
Manufacturer Specific Data | Identify devices by custom data from a specific manufacturer (e.g., iBeacons, Eddystone, custom sensor data). | Use esp_ble_resolve_adv_data() with ESP_BLE_AD_TYPE_MFG_DATA . Check Company ID (first 2 bytes) then the custom data. |
Flags AD Type | Filter based on device capabilities advertised in flags (e.g., only discoverable devices, BLE-only devices). | Use esp_ble_resolve_adv_data() with ESP_BLE_AD_TYPE_FLAG or directly check scan_result->scan_rst.flag . Check specific bits (e.g., ESP_BLE_ADV_FLAG_GEN_DISC ). |
RSSI (Signal Strength) | Filter for devices based on proximity (stronger signal = closer). | Compare scan_result->scan_rst.rssi against a threshold (e.g., > -70 dBm for relatively close devices). |
Combination of Criteria | Apply multiple filters together for more precise targeting. | Implement conditional logic combining checks for name, service UUID, RSSI, etc. |
- By Device Address (BDA): Connect only to a specific, known MAC address.
- By Local Name: Search for devices advertising a particular name (e.g., “MySensor”).
- By Service UUIDs: Find devices advertising support for a specific service (e.g., Heart Rate Service
0x180D
). This is very common. - By Manufacturer Specific Data: Look for devices with particular manufacturer data, often used for custom protocols or device identification (e.g., iBeacons use this).
- By RSSI: Filter out devices that are too far away (weak signal).
This filtering is implemented in the host application by inspecting the parameters of the ESP_GAP_BLE_SCAN_RESULT_EVT
.
Scanning Duration and Stopping
esp_ble_gap_start_scanning(uint32_t duration)
: Theduration
parameter is in seconds. If0
, scanning is continuous until explicitly stopped.esp_ble_gap_stop_scanning(void)
: Manually stops an ongoing scan. This is essential before attempting to initiate a connection (esp_ble_gattc_open
).
Scanning for Extended Advertisements (BLE 5.0+)
BLE 5.0 introduced “Extended Advertising,” which allows for larger advertising payloads and advertising on secondary (data) channels.
- Legacy scanners (like the ones configured by
esp_ble_gap_set_scan_params
) primarily listen on the 3 primary advertising channels. - To receive extended advertisements, a BLE 5.0 capable scanner needs to be configured to listen for special PDUs on primary channels that point to extended advertising events on secondary channels.
- ESP-IDF provides separate APIs for this:
esp_ble_gap_set_ext_scan_params(const esp_ble_ext_scan_params_t *params)
esp_ble_gap_start_ext_scan(uint32_t duration, uint16_t period)
esp_ble_gap_stop_ext_scan(void)
- The
esp_ble_gap_cb_param_t->ext_scan_rst
structure inESP_GAP_BLE_EXT_SCAN_RESULT_EVT
provides information about extended advertisements. - This is a more advanced topic, but it’s important to be aware of if interacting with newer BLE 5.0 peripherals.
ESP-IDF APIs for Scanning
Key functions from esp_gap_ble_api.h
:
API Function | Scan Type | Purpose | Key Event Triggered |
---|---|---|---|
esp_ble_gap_register_callback() |
Both | Registers the global GAP event handler for all GAP events. | N/A (Setup) |
esp_ble_gap_set_scan_params() |
Legacy | Configures parameters for legacy BLE scanning (interval, window, type, etc.) using esp_ble_scan_params_t . |
ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT |
esp_ble_gap_start_scanning() |
Legacy | Starts legacy BLE scanning for a specified duration (or continuously if duration is 0). | ESP_GAP_BLE_SCAN_START_COMPLETE_EVT (Results via ESP_GAP_BLE_SCAN_RESULT_EVT ) |
esp_ble_gap_stop_scanning() |
Legacy | Stops an ongoing legacy BLE scan. | ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT |
esp_ble_resolve_adv_data() |
Both | Helper function to parse specific AD (Advertising Data) types from raw advertising or scan response data. | N/A (Utility) |
esp_ble_gap_set_ext_scan_params() |
Extended (BLE 5.0) | Configures parameters for BLE 5.0 extended scanning using esp_ble_ext_scan_params_t . |
ESP_GAP_BLE_EXT_SCAN_PARAM_SET_COMPLETE_EVT |
esp_ble_gap_start_ext_scan() |
Extended (BLE 5.0) | Starts BLE 5.0 extended scanning for a specified duration and period. | ESP_GAP_BLE_EXT_SCAN_START_COMPLETE_EVT (Results via ESP_GAP_BLE_EXT_SCAN_RESULT_EVT ) |
esp_ble_gap_stop_ext_scan() |
Extended (BLE 5.0) | Stops an ongoing BLE 5.0 extended scan. | ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT |
esp_ble_gap_register_callback(esp_gap_ble_cb_t callback)
: Registers the GAP event handler.esp_ble_gap_set_scan_params(esp_ble_scan_params_t *scan_params)
: Configures parameters for legacy scanning. TriggersESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT
.esp_ble_gap_start_scanning(uint32_t duration)
: Starts legacy scanning. TriggersESP_GAP_BLE_SCAN_START_COMPLETE_EVT
.esp_ble_gap_stop_scanning(void)
: Stops legacy scanning. TriggersESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
.esp_ble_resolve_adv_data(uint8_t *adv_data, esp_ble_adv_data_type_t type, uint8_t *length)
: Helper to parse AD structures.
BLE Scanning Lifecycle in ESP-IDF
%%{init: {"theme": "base", "themeVariables": { "primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937", "fontSize": "14px", "fontFamily": "\"Open Sans\", sans-serif" }}}%% graph TD A[/"<b>Start: Initialize BLE Stack</b><br>esp_bt_controller_init/enable<br>esp_bluedroid_init/enable"\]:::start B["Register GAP Callback<br><code class='code-mermaid'>esp_ble_gap_register_callback()</code>"]:::process C["(Optional) Register GATTC Callback & App<br><code class='code-mermaid'>esp_ble_gattc_register_callback()</code><br><code class='code-mermaid'>esp_ble_gattc_app_register()</code><br>(If planning to connect)"]:::process D["Set Scan Parameters<br><code class='code-mermaid'>esp_ble_gap_set_scan_params(&scan_params)</code>"]:::process E{"Scan Param Set Event?<br>(<code class='code-mermaid'>ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT</code>)"}:::decision F["Start Scanning<br><code class='code-mermaid'>esp_ble_gap_start_scanning(duration)</code>"]:::process G{"Scan Start Complete Event?<br>(<code class='code-mermaid'>ESP_GAP_BLE_SCAN_START_COMPLETE_EVT</code>)"}:::decision H[/"<b>Scanning Active</b><br>Receiving <code class='code-mermaid'>ESP_GAP_BLE_SCAN_RESULT_EVT</code>"\]:::success I{"Scan Result Event<br>(<code class='code-mermaid'>ESP_GAP_SEARCH_INQ_RES_EVT</code>)"}:::decision J["Process Scan Result<br>(Parse AdvData, Filter, etc.)"]:::process K{"Scan Duration Ended?<br>(<code class='code-mermaid'>ESP_GAP_SEARCH_INQ_CMPL_EVT</code> on <code class='code-mermaid'>scan_rst.search_evt</code>)"}:::decision L["(Explicit) Stop Scanning<br><code class='code-mermaid'>esp_ble_gap_stop_scanning()</code>"]:::process M{"Scan Stop Complete Event?<br>(<code class='code-mermaid'>ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT</code>)"}:::decision N[/"<b>Scanning Stopped</b>"\]:::endofOp A --> B B --> C C --> D D -->|Success| E E -->|Status OK| F E -->|Status Fail| ErrP{Handle Param Error}:::error F -->|Success| G G -->|Status OK| H G -->|Status Fail| ErrS{Handle Start Error}:::error H --> I I -- Inq Res Evt --> J J --> H H --> K K -- Inq Cmpl Evt / Duration End --> L H -.->|Need to Stop Manually or Connect| L L -->|Success| M M -->|Status OK| N M -->|Status Fail| ErrStop{Handle Stop Error}:::error classDef start 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 success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 classDef endofOp fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B %% Notes: %% - This flowchart outlines the typical sequence for legacy BLE scanning. %% - Event-driven: Operations are chained through completion events. %% - GATTC registration is needed if the ESP32 intends to connect to discovered devices. %% - Error handling at each step is crucial. %% - Scanning continues until duration ends or esp_ble_gap_stop_scanning() is called.
Practical Examples
Prerequisites:
- An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2).
- VS Code with the Espressif IDF Extension.
- At least one other BLE device advertising nearby (e.g., another ESP32 running an advertising example, a smartphone, a fitness tracker).
Example 1: Basic Active Scanning and Displaying Results
This example performs an active scan and prints basic information about discovered devices.
1. Project Setup:
- Create a new ESP-IDF project.
- Run
idf.py menuconfig
:Component config
->Bluetooth
-> Enable[*] Bluetooth
.Component config
->Bluetooth
->Bluetooth Host
-> SelectBluedroid
.Component config
->Bluetooth
->Bluetooth controller
->Controller Mode
->BLE Only
.- Save and exit.
2. Code (main/scanning_main.c
):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h" // For GATTC app registration if planning to connect
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#define SCAN_TAG "BLE_SCANNER"
// Scan parameters
static esp_ble_scan_params_t ble_scan_params = {
.scan_type = BLE_SCAN_TYPE_ACTIVE, // Active scan to get scan response
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0x50, // Corresponds to 50ms. N * 0.625ms = 80 * 0.625ms = 50ms
.scan_window = 0x30, // Corresponds to 30ms. N * 0.625ms = 48 * 0.625ms = 30ms
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE // Report all advertisements
};
static void print_adv_data(uint8_t *adv_data, uint8_t adv_data_len) {
ESP_LOGI(SCAN_TAG, "ADV Data (len %d): ", adv_data_len);
for (int i = 0; i < adv_data_len; i++) {
printf("%02x ", adv_data[i]);
}
printf("\n");
}
// GAP event handler
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
if ((err = param->scan_param_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(SCAN_TAG, "Scan param set failed: %s", esp_err_to_name(err));
break;
}
ESP_LOGI(SCAN_TAG, "Scan parameters set, starting scan...");
// Start scanning for 10 seconds
uint32_t duration = 10;
esp_ble_gap_start_scanning(duration);
break;
}
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(SCAN_TAG, "Scan start failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(SCAN_TAG, "Scan started successfully for 10 seconds.");
}
break;
case ESP_GAP_BLE_SCAN_RESULT_EVT: {
esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
switch (scan_result->scan_rst.search_evt) {
case ESP_GAP_SEARCH_INQ_RES_EVT: // Inquiry result for advertisements / scan responses
ESP_LOGI(SCAN_TAG, "--------------------------------------------------");
ESP_LOGI(SCAN_TAG, "Device found: Addr %02x:%02x:%02x:%02x:%02x:%02x",
scan_result->scan_rst.bda[0], scan_result->scan_rst.bda[1],
scan_result->scan_rst.bda[2], scan_result->scan_rst.bda[3],
scan_result->scan_rst.bda[4], scan_result->scan_rst.bda[5]);
ESP_LOGI(SCAN_TAG, "RSSI: %d dBm", scan_result->scan_rst.rssi);
ESP_LOGI(SCAN_TAG, "Address Type: %d", scan_result->scan_rst.ble_addr_type);
if (scan_result->scan_rst.scan_rsp) {
ESP_LOGI(SCAN_TAG, "Scan Response Packet");
} else {
ESP_LOGI(SCAN_TAG, "Advertising Packet");
}
// print_adv_data(scan_result->scan_rst.ble_adv, scan_result->scan_rst.adv_data_len);
// Parse and print device name
uint8_t *adv_name = NULL;
uint8_t adv_name_len = 0;
adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
if (adv_name != NULL) {
ESP_LOGI(SCAN_TAG, "Complete Name: %.*s", adv_name_len, adv_name);
} else {
adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_NAME_SHORT, &adv_name_len);
if (adv_name != NULL) {
ESP_LOGI(SCAN_TAG, "Short Name: %.*s", adv_name_len, adv_name);
} else {
ESP_LOGI(SCAN_TAG, "No name found.");
}
}
// Parse and print flags
uint8_t *adv_flags_data = NULL;
uint8_t adv_flags_len = 0;
adv_flags_data = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_FLAG, &adv_flags_len);
if (adv_flags_data != NULL && adv_flags_len > 0) {
ESP_LOGI(SCAN_TAG, "Flags: 0x%02x", adv_flags_data[0]);
if (adv_flags_data[0] & ESP_BLE_ADV_FLAG_LIMIT_DISC) ESP_LOGI(SCAN_TAG, " - LE Limited Discoverable Mode");
if (adv_flags_data[0] & ESP_BLE_ADV_FLAG_GEN_DISC) ESP_LOGI(SCAN_TAG, " - LE General Discoverable Mode");
if (adv_flags_data[0] & ESP_BLE_ADV_FLAG_BREDR_NOT_SPT) ESP_LOGI(SCAN_TAG, " - BR/EDR Not Supported");
}
break;
case ESP_GAP_SEARCH_INQ_CMPL_EVT: // Scan duration finished
ESP_LOGI(SCAN_TAG, "Scan completed (duration ended).");
// You might want to restart scanning or process results
break;
default:
break;
}
break;
}
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
if ((err = param->scan_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(SCAN_TAG, "Scan stop failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(SCAN_TAG, "Scan stopped successfully.");
}
break;
default:
break;
}
}
// Dummy GATTC callback for profile registration (if planning to connect later)
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && param->reg.status == ESP_GATT_OK) {
ESP_LOGI(SCAN_TAG, "GATTC App registered, app_id %04x, gattc_if %d", param->reg.app_id, gattc_if);
}
}
void app_main(void) {
esp_err_t ret;
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(SCAN_TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(SCAN_TAG, "Enable controller failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(SCAN_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(SCAN_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
return;
}
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret) {
ESP_LOGE(SCAN_TAG, "GAP register error: %s", esp_err_to_name(ret));
return;
}
// Register GATTC interface (optional if only scanning, but good practice if connecting later)
ret = esp_ble_gattc_register_callback(gattc_event_handler);
if (ret) {
ESP_LOGE(SCAN_TAG, "GATTC register error: %s", esp_err_to_name(ret));
return;
}
ret = esp_ble_gattc_app_register(0); // Dummy app_id for GATTC
if (ret) {
ESP_LOGE(SCAN_TAG, "GATTC app register error: %s", esp_err_to_name(ret));
return;
}
// Set scan parameters
ret = esp_ble_gap_set_scan_params(&ble_scan_params);
if (ret) {
ESP_LOGE(SCAN_TAG, "Set scan params error: %s", esp_err_to_name(ret));
}
ESP_LOGI(SCAN_TAG, "Scanner Initialized. Waiting for scan param set complete...");
}
3. Build, Flash, and Monitor:
idf.py build
idf.py flash monitor
4. Observe Output:
- The serial monitor will show logs from
SCAN_TAG
. - After “Scan parameters set, starting scan…”, you should see “Device found…” messages for nearby BLE peripherals, including their MAC address, RSSI, name (if available), and flags.
- Since it’s an active scan, you might see both “Advertising Packet” and “Scan Response Packet” logs if the peripherals provide scan responses.
- The scan will stop after 10 seconds.
Example 2: Filtering for Devices Advertising a Specific Service UUID
This example modifies the scan result handler to look for devices advertising a specific service (e.g., the Heart Rate Service, UUID 0x180D
).
Modify gap_event_handler
in scanning_main.c
:
// ... (previous includes and definitions) ...
#define TARGET_SERVICE_UUID 0x180D // Heart Rate Service
// ... (inside gap_event_handler, case ESP_GAP_BLE_SCAN_RESULT_EVT, ESP_GAP_SEARCH_INQ_RES_EVT)
// ... (log BDA, RSSI, Name as before) ...
// Filter for a specific service UUID
uint8_t *service_uuid_data = NULL;
uint8_t service_uuid_len = 0;
// Check for complete list of 16-bit service UUIDs
service_uuid_data = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_16SRV_CMPL, &service_uuid_len);
if (service_uuid_data != NULL && service_uuid_len > 0) {
for (int i = 0; i < service_uuid_len; i += 2) { // UUIDs are 2 bytes (16-bit)
uint16_t current_uuid = (service_uuid_data[i+1] << 8) | service_uuid_data[i]; // LSB first
ESP_LOGI(SCAN_TAG, " Found Service UUID (Complete List): 0x%04x", current_uuid);
if (current_uuid == TARGET_SERVICE_UUID) {
ESP_LOGW(SCAN_TAG, ">>>> Target Service (0x%04x) FOUND! Device: %02x:%02x:%02x:%02x:%02x:%02x <<<<",
TARGET_SERVICE_UUID,
scan_result->scan_rst.bda[0], scan_result->scan_rst.bda[1],
scan_result->scan_rst.bda[2], scan_result->scan_rst.bda[3],
scan_result->scan_rst.bda[4], scan_result->scan_rst.bda[5]);
// Here you might decide to stop scanning and connect
// esp_ble_gap_stop_scanning();
// esp_ble_gattc_open(...);
}
}
} else {
// Check for incomplete list of 16-bit service UUIDs
service_uuid_data = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_16SRV_PART, &service_uuid_len);
if (service_uuid_data != NULL && service_uuid_len > 0) {
for (int i = 0; i < service_uuid_len; i += 2) {
uint16_t current_uuid = (service_uuid_data[i+1] << 8) | service_uuid_data[i];
ESP_LOGI(SCAN_TAG, " Found Service UUID (Incomplete List): 0x%04x", current_uuid);
if (current_uuid == TARGET_SERVICE_UUID) {
ESP_LOGW(SCAN_TAG, ">>>> Target Service (0x%04x) FOUND! Device: %02x:%02x:%02x:%02x:%02x:%02x <<<<",
TARGET_SERVICE_UUID,
scan_result->scan_rst.bda[0], scan_result->scan_rst.bda[1],
scan_result->scan_rst.bda[2], scan_result->scan_rst.bda[3],
scan_result->scan_rst.bda[4], scan_result->scan_rst.bda[5]);
}
}
}
}
// ... (rest of the handler) ...
Build, Flash, Observe:
- If you have a device advertising the Heart Rate Service (UUID
0x180D
) nearby, the ESP32 will log a special message when it finds it. - This demonstrates a common application-level filtering technique.
Variant Notes
- ESP32-S2: This variant lacks Bluetooth hardware and thus cannot perform BLE scanning.
- ESP32 (Original): Fully supports legacy scanning as described in the examples.
- ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: These newer variants also fully support legacy scanning. Additionally, they have robust support for BLE 5.0 features, which significantly impacts scanning:
- Scanning for Extended Advertisements: To discover devices using BLE 5.0 extended advertising (which can have larger payloads and advertise on secondary channels), you must use the extended scanning APIs (e.g.,
esp_ble_gap_set_ext_scan_params
,esp_ble_gap_start_ext_scan
). TheESP_GAP_BLE_EXT_SCAN_RESULT_EVT
event is used for these results. - Scanning on LE Coded PHY (Long Range) / LE 2M PHY (Higher Speed): Extended scanning APIs allow specifying which PHYs to scan on. This is necessary to discover devices advertising exclusively on these PHYs.
- The legacy scan APIs (
esp_ble_gap_set_scan_params
,esp_ble_gap_start_scanning
) will typically only discover devices using legacy advertising on the LE 1M PHY over primary advertising channels.
- Scanning for Extended Advertisements: To discover devices using BLE 5.0 extended advertising (which can have larger payloads and advertise on secondary channels), you must use the extended scanning APIs (e.g.,
ESP32 Variant | Legacy Scanning (Primary Channels, LE 1M PHY) | Extended Scanning (BLE 5.0 Features) | Scanning on LE Coded PHY / LE 2M PHY | Notes |
---|---|---|---|---|
ESP32 (Original) | ✔ | Limited* | Limited* | *BLE 5.0 features depend on silicon revision (ECO3+) and ESP-IDF version. Full support is better in newer chips. Primarily uses legacy scanning APIs. |
ESP32-S2 | ✘ | ✘ | ✘ | Does not have Bluetooth hardware. |
ESP32-S3 | ✔ | ✔ | ✔ | Robust support for both legacy and extended scanning APIs (e.g., esp_ble_gap_start_ext_scan() ). |
ESP32-C3 | ✔ | ✔ | ✔ | Good support for legacy and extended scanning. |
ESP32-C6 | ✔ | ✔ | ✔ | Supports BLE 5.0/5.3 features for scanning. Also supports 802.15.4. |
ESP32-H2 | ✔ | ✔ | ✔ | Strong BLE 5.x support for scanning. Primarily focused on 802.15.4 but with full BLE capabilities. |
For applications needing to interact with modern BLE 5.0 devices that leverage extended advertising or different PHYs, you will need to use the extended scanning functions.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Scan Window > Scan Interval | esp_ble_gap_set_scan_params() returns error ESP_ERR_INVALID_ARG or similar. Scan does not start. |
Ensure ble_scan_params.scan_window is less than or equal to ble_scan_params.scan_interval . Both are in units of 0.625 ms. |
Continuous Scan Draining Battery | Device (if battery-powered) loses power much faster than expected when BLE scanning is active. | Use a finite scan duration in esp_ble_gap_start_scanning() if continuous discovery isn’t strictly needed. If continuous, consider a lower duty cycle (e.g., scan_window = 30ms, scan_interval = 300ms) or implement logic to pause/resume scanning periodically. |
Not Stopping Scan Before Connecting | Connection attempt with esp_ble_gattc_open() fails, returns error (e.g. ESP_GATT_NO_RESOURCES , ESP_GATT_INTERNAL_ERROR ), or behaves unpredictably. |
Always call esp_ble_gap_stop_scanning() and wait for the ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT event (confirming status is success) *before* attempting to initiate a connection with esp_ble_gattc_open() . |
Incorrectly Parsing Adv Data / Misinterpreting esp_ble_resolve_adv_data() |
Extracted data is wrong, application crashes (buffer overruns), or expected AD types (name, UUIDs) are not found. | Use esp_ble_resolve_adv_data() correctly. Always check the returned length parameter. For string data (like names), use the returned length to copy into a null-terminated buffer if necessary, don’t assume it’s already null-terminated. For multi-byte data (like UUIDs), iterate using the correct size per element (e.g., 2 bytes for 16-bit UUIDs, 16 bytes for 128-bit UUIDs). |
Filtering Logic Too Strict or Flawed | Application doesn’t “find” or react to desired peripherals, even though they are advertising correctly and visible to generic scanner apps. | Log all discovered devices *without* filters first to verify they are being seen by the ESP32. Then, incrementally add and test filtering logic. Double-check UUID byte order (often LSB first in advertisements). Be aware of “Incomplete List” vs. “Complete List” for service UUIDs. Check for partial name matches if using shortened names. |
No Scan Results Received | Scan starts successfully (ESP_GAP_BLE_SCAN_START_COMPLETE_EVT is OK), but no ESP_GAP_BLE_SCAN_RESULT_EVT events occur. |
1. Check if there are any BLE devices advertising nearby. 2. Verify scan parameters: scan_interval and scan_window might be too short or too far apart. Try with a high duty cycle like scan_interval=0x50 , scan_window=0x50 for testing. 3. Ensure scan_filter_policy is not overly restrictive (e.g., BLE_SCAN_FILTER_ALLOW_ONLY_WLST without a populated whitelist). Start with BLE_SCAN_FILTER_ALLOW_ALL . 4. Check antenna connection if using an external antenna. |
Forgetting GATTC Registration for Connection | Scanning works, device found, but connection attempt fails later. Error might be related to GATTC interface not ready. | If you plan to connect to discovered devices, ensure you register a GATTC application interface using esp_ble_gattc_register_callback() and esp_ble_gattc_app_register() during initialization. The gattc_if obtained is needed for esp_ble_gattc_open() . |
Exercises
- RSSI-Based Filtering:
- Modify Example 1 to only print details for devices with an RSSI value greater (less negative) than a certain threshold (e.g., -70 dBm). This helps filter for closer devices.
- Filter by Multiple Criteria:
- Combine filtering: Scan for devices that advertise a specific Local Name (e.g., “MyPeripheral”) AND a specific 16-bit Service UUID (e.g.,
0xABCD
– you’ll need a device advertising this). Log only when a device matches both criteria.
- Combine filtering: Scan for devices that advertise a specific Local Name (e.g., “MyPeripheral”) AND a specific 16-bit Service UUID (e.g.,
- Scan Duration and Restart:
- Implement a scanner that scans for 5 seconds, then stops for 5 seconds, and then repeats this cycle. Log when scanning starts and stops.
- Manufacturer Data Filter:
- Set up an ESP32 advertiser (from Chapter 62) to include specific Manufacturer Data (e.g., Company ID
0xFFFF
, data0x01 0x02 0x03
). - Implement a scanner that specifically looks for this Company ID and then checks if the subsequent data bytes match
0x01 0x02 0x03
. Log when such a device is found.
- Set up an ESP32 advertiser (from Chapter 62) to include specific Manufacturer Data (e.g., Company ID
Summary
- BLE scanning allows Central devices to discover advertising Peripherals by listening on the 3 primary advertising channels.
- Active scanning involves sending a
SCAN_REQ
to get additionalSCAN_RSP
data, while passive scanning only listens. - Scan behavior is configured using
esp_ble_scan_params_t
(interval, window, type, filter policy, duplicate filtering). - Scan results are delivered via
ESP_GAP_BLE_SCAN_RESULT_EVT
, containing BDA, RSSI, and advertising/scan response data. esp_ble_resolve_adv_data()
helps parse AD structures from the raw advertising payload.- Application-level filtering allows selection of devices based on name, service UUIDs, manufacturer data, RSSI, etc.
- Scanning must be stopped before initiating a connection.
- BLE 5.0 introduced Extended Advertising, requiring different scanning APIs (
esp_ble_gap_set_ext_scan_params
, etc.) for discovery.
Further Reading
- ESP-IDF API Reference –
esp_gap_ble_api.h
: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/esp_gap_ble.html - Bluetooth Core Specification: Volume 3, Part C (Generic Access Profile – Scanning modes) and Volume 6, Part B (Link Layer – Scanning state and PDUs): https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host/generic-access-profile.html
- ESP-IDF
gattc_demo
example: https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/bluedroid/ble/gatt_client - ESP-IDF
ble_scan
example: (If available, or derive fromgattc_demo
) for focused scanning demonstrations.
