Chapter 59: BLE GATT Client Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the role and responsibilities of a GATT Client in BLE.
- Implement BLE scanning to discover nearby peripheral devices using ESP-IDF.
- Establish a connection to a target BLE peripheral.
- Perform service discovery to find services offered by a connected peripheral.
- Discover characteristics and descriptors within a specific service.
- Read values from and write values to characteristics and descriptors.
- Enable and handle notifications and indications from a GATT server.
- Manage attribute handles obtained during the discovery process.
- Use ESP-IDF GATT Client APIs (
esp_gattc_api.h
) effectively.
Introduction
In the preceding chapters, we’ve explored the architecture of BLE and learned how to create a GATT Server, allowing an ESP32 to offer data and services. Now, we turn our attention to the other crucial role in BLE communication: the GATT Client. A GATT Client is a device that discovers, connects to, and consumes the data and services offered by a GATT Server.
Many ESP32 applications require interaction with external BLE peripherals – reading data from sensors (like heart rate monitors, temperature sensors), controlling actuators (like smart locks, lights), or exchanging information with other BLE-enabled devices. To achieve this, the ESP32 must act as a GATT Client.
This chapter will provide a comprehensive guide to implementing GATT client functionality on an ESP32 using ESP-IDF v5.x. We will cover the entire lifecycle: scanning for devices, establishing connections, discovering the server’s capabilities (services, characteristics, descriptors), and interacting with those attributes by reading, writing, and receiving notifications.
Theory
Recall that a BLE device acting as a Central typically takes on the role of a GATT Client. The GATT Client initiates communication with a Peripheral (GATT Server) and requests operations on the server’s attributes.
GATT Client Operational Flow
The typical sequence of operations for a GATT Client is as follows:
- Initialization: Initialize the Bluetooth controller and host stack (Bluedroid). Register GAP and GATTC event handlers and application profiles.
- Scanning (GAP operation):
- Configure scan parameters (scan interval, window, scan type – passive/active).
- Start scanning for advertising peripherals.
- Process scan results received in the GAP callback (
ESP_GAP_BLE_SCAN_RESULT_EVT
). This includes device address, advertising data (name, service UUIDs), and RSSI. - Select a target peripheral based on its advertising data.
- Connecting (GAP operation):
- Stop scanning (usually).
- Initiate a connection to the target peripheral using
esp_ble_gattc_open()
. This function takes the remote device address and address type. - Await connection confirmation via
ESP_GATTC_OPEN_EVT
orESP_GATTC_CONNECT_EVT
in the GATTC callback.
- MTU Exchange (Optional but Recommended):
- After connection, the client can request a larger ATT MTU (Maximum Transmission Unit) using
esp_ble_gattc_send_mtu_req()
. The default is 23 bytes. A larger MTU allows more data per packet. - The server responds, and the negotiated MTU is reported via
ESP_GATTC_CFG_MTU_EVT
.
- After connection, the client can request a larger ATT MTU (Maximum Transmission Unit) using
- Service Discovery:
- Once connected, the client needs to discover what services the server offers.
- Use
esp_ble_gattc_search_service()
to discover all primary services or a specific primary service by UUID. - Service discovery results are received via
ESP_GATTC_SEARCH_RES_EVT
(one event per discovered service, providing service UUID, start handle, and end handle). ESP_GATTC_SEARCH_CMPL_EVT
indicates the completion of the service search.
- Characteristic Discovery:
- For each interesting service discovered, the client discovers its characteristics.
- Use
esp_ble_gattc_get_all_char()
oresp_ble_gattc_get_char_by_uuid()
within the service’s handle range. - Characteristic information (UUID, properties, value handle) is received via
ESP_GATTC_GET_CHAR_EVT
. Multiple events may arrive if multiple characteristics are found.
- Descriptor Discovery:
- For each interesting characteristic, the client may discover its descriptors (e.g., CCCD, User Description).
- Use
esp_ble_gattc_get_all_descr()
oresp_ble_gattc_get_descr_by_uuid()
within the characteristic’s handle range (typically from its value handle to the service end handle or next characteristic handle). - Descriptor information (UUID, handle) is received via
ESP_GATTC_GET_DESCR_EVT
.
- Interacting with Characteristics/Descriptors:
- Reading:
- Use
esp_ble_gattc_read_char()
oresp_ble_gattc_read_char_descr()
specifying the attribute handle. - The read value is received in
ESP_GATTC_READ_CHAR_EVT
orESP_GATTC_READ_DESCR_EVT
.
- Use
- Writing:
- Use
esp_ble_gattc_write_char()
oresp_ble_gattc_write_char_descr()
. Specify the handle, value, length, and write type (with/without response). - Confirmation for writes with response comes via
ESP_GATTC_WRITE_CHAR_EVT
orESP_GATTC_WRITE_DESCR_EVT
.
- Use
- Enabling Notifications/Indications:
- Discover the CCCD handle for the target characteristic.
- Write
0x0001
(for notifications) or0x0002
(for indications) to the CCCD handle usingesp_ble_gattc_write_char_descr()
. - Register interest in receiving these updates using
esp_ble_gattc_register_for_notify()
with the characteristic’s value handle.
- Handling Notifications/Indications:
- Incoming notifications/indications are received via
ESP_GATTC_NOTIFY_EVT
. The event parameters include the characteristic handle, value, and length.
- Incoming notifications/indications are received via
- Reading:
- Disconnecting:
- Use
esp_ble_gattc_close()
to terminate the connection. - Confirmation via
ESP_GATTC_CLOSE_EVT
.
- Use
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD A["Start: Init BLE Stack & Register Callbacks<br><span class='func-small'>esp_bluedroid_init(), esp_ble_gap_register_callback(),<br>esp_ble_gattc_register_callback(), esp_ble_gattc_app_register()</span>"] --> B(Scanning for Peripherals); B -- Found Target Device --> C(Stop Scan & Initiate Connection); C -- <span class='func-small'>esp_ble_gattc_open(target_bda)</span> --> D{Connection Event?<br><span class='event-small'>ESP_GATTC_OPEN_EVT</span>}; D -- Success --> E(Optional: MTU Exchange); D -- Failure --> B; E -- <span class='func-small'>esp_ble_gattc_send_mtu_req()</span> --> F{MTU Configured?<br><span class='event-small'>ESP_GATTC_CFG_MTU_EVT</span>}; F --> G(Discover Services); G -- <span class='func-small'>esp_ble_gattc_search_service()</span> --> H{Services Found?<br><span class='event-small'>ESP_GATTC_SEARCH_RES_EVT</span><br><span class='event-small'>ESP_GATTC_SEARCH_CMPL_EVT</span>}; H -- Yes --> I(For Each Service: Discover Characteristics); I -- <span class='func-small'>esp_ble_gattc_get_all_char()</span> --> J{Characteristics Found?<br><span class='event-small'>ESP_GATTC_GET_CHAR_EVT</span>}; J -- Yes --> K(For Each Char: Discover Descriptors); K -- <span class='func-small'>esp_ble_gattc_get_all_descr()</span> --> L{Descriptors Found?<br><span class='event-small'>ESP_GATTC_GET_DESCR_EVT</span>}; L -- Yes / No more Descriptors --> M(Store All Handles); M --> N{Interact with Attributes}; N -- Read --> O_READ["Read Char/Descr<br><span class='func-small'>esp_ble_gattc_read_char()</span><br>Event: <span class='event-small'>ESP_GATTC_READ_CHAR_EVT</span>"]; N -- Write --> O_WRITE["Write Char/Descr<br><span class='func-small'>esp_ble_gattc_write_char()</span><br>Event: <span class='event-small'>ESP_GATTC_WRITE_CHAR_EVT</span>"]; N -- Notify/Indicate --> O_NOTIFY["Enable Notifications/Indications<br>(Write CCCD, <span class='func-small'>register_for_notify()</span>)<br>Event: <span class='event-small'>ESP_GATTC_NOTIFY_EVT</span>"]; O_READ --> N; O_WRITE --> N; O_NOTIFY --> N; N -- Done Interacting / Error --> P(Disconnect); P -- <span class='func-small'>esp_ble_gattc_close()</span> --> Q{Disconnected?<br><span class='event-small'>ESP_GATTC_CLOSE_EVT</span>}; Q --> R[End / Cleanup]; H -- No / Error --> P; J -- No / Error --> P; K -- No / Error --> P; classDef func-small fill:#fff,stroke:#333,color:#333,fontSize:10px; classDef event-small fill:#fff,stroke:#333,color:#333,fontSize:10px,fontStyle:italic; classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef ioNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; classDef endNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A,R startNode; class B,C,E,G,I,K,M,P processNode; class D,F,H,J,L,N,Q decisionNode; class O_READ,O_WRITE,O_NOTIFY ioNode;
BLE Device Scanning Process:
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD A[Start: Init BLE Stack & Register GAP Callback] --> B(Set Scan Parameters); B -- <span class='func-small'>esp_ble_gap_set_scan_params()</span> --> C{Scan Params Set?<br><span class='event-small'>ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT</span>}; C -- Success --> D(Start Scanning); D -- <span class='func-small'>esp_ble_gap_start_scanning(duration)</span> --> E{Scan Started?<br><span class='event-small'>ESP_GAP_BLE_SCAN_START_COMPLETE_EVT</span>}; E -- Success --> F{Scanning Loop...}; F -- Advertising Packet Received --> G{Scan Result Event<br><span class='event-small'>ESP_GAP_BLE_SCAN_RESULT_EVT</span>}; G --> H(Process Scan Result: <br>BDA, RSSI, Adv Data, Name?); H --> I{Target Device Found?}; I -- Yes --> J(Stop Scanning); I -- No --> F; F -- Scan Duration Elapsed / Timeout --> J; J -- <span class='func-small'>esp_ble_gap_stop_scanning()</span> --> K{Scan Stopped?<br><span class='event-small'>ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT</span>}; K -- Success --> L[End Scan Phase / Proceed to Connect]; C -- Failure --> M[Error Handling]; E -- Failure --> M; K -- Failure --> M; classDef func-small fill:#fff,stroke:#333,color:#333,fontSize:10px; classDef event-small fill:#fff,stroke:#333,color:#333,fontSize:10px,fontStyle:italic; classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A startNode; class B,D,F,H,J processNode; class C,E,G,I,K decisionNode; class L endNode; class M errorNode;
BLE Connection Process:
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD A["Start: Target BDA Known, <br>GATTC App Registered (<span class='code-small'>client_gattc_if</span> valid)"] --> B(Initiate Connection); B -- <span class='func-small'>esp_ble_gattc_open(client_gattc_if, remote_bda, ...)</span> --> C{GATT Connection Open Event?<br><span class='event-small'>ESP_GATTC_OPEN_EVT</span>}; C -- Status OK --> D(Connection Successful!); D --> E["Store: <span class='code-small'>conn_id</span>, <span class='code-small'>remote_bda</span>, <span class='code-small'>gattc_if</span><br>Server MTU: <span class='code-small'>param->open.mtu</span>"]; E --> F{Optional: MTU Exchange?}; F -- Yes --> G["Send MTU Request<br><span class='func-small'>esp_ble_gattc_send_mtu_req(conn_id)</span>"]; G --> H{MTU Configured?<br><span class='event-small'>ESP_GATTC_CFG_MTU_EVT</span>}; H -- Status OK --> I["Store Negotiated MTU<br><span class='code-small'>param->cfg_mtu.mtu</span>"]; F -- No --> J[Proceed to Service Discovery]; I --> J; C -- Status Not OK (Failure) --> K["Handle Connection Error<br>(e.g., Log, Retry, Rescan)"]; H -- Status Not OK --> L[MTU Exchange Failed, Use Server MTU]; L --> J; classDef code-small fill:#fff,stroke:#333,color:#333,fontSize:10px; classDef func-small fill:#fff,stroke:#333,color:#333,fontSize:10px; classDef event-small fill:#fff,stroke:#333,color:#333,fontSize:10px,fontStyle:italic; classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef successNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A startNode; class B,E,G,I,L processNode; class C,F,H decisionNode; class D,J successNode; class K errorNode;
ESP-IDF GATT Client APIs (esp_gattc_api.h
)
The ESP-IDF provides a comprehensive set of APIs for GATT client operations. These primarily reside in esp_gattc_api.h
.
- Callback Registration:
esp_ble_gattc_register_callback(esp_gattc_cb_t callback)
: Registers a global callback function to handle all GATTC events. This is the central point for asynchronous event processing.
- Application Profile Registration:
esp_ble_gattc_app_register(uint16_t app_id)
: Registers a client application profile. Each profile gets a uniquegattc_if
(GATT client interface ID) delivered inESP_GATTC_REG_EVT
. Multiple client profiles can exist.
- Connection Management:
esp_ble_gattc_open(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, esp_ble_addr_type_t remote_addr_type, bool is_direct)
: Initiates a connection.esp_ble_gattc_close(esp_gatt_if_t gattc_if, uint16_t conn_id)
: Closes a connection.
- MTU Configuration:
esp_ble_gatt_set_local_mtu(uint16_t mtu)
: Sets the client’s desired MTU size before connection.esp_ble_gattc_send_mtu_req(esp_gatt_if_t gattc_if, uint16_t conn_id)
: Sends an MTU exchange request after connection. (Often, setting local MTU is sufficient, and the exchange happens automatically).
- Discovery Functions:
esp_ble_gattc_search_service(esp_gatt_if_t gattc_if, uint16_t conn_id, esp_bt_uuid_t *filter_srvc_uuid)
: Searches for services.filter_srvc_uuid
can beNULL
to find all primary services.esp_ble_gattc_get_all_char(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t start_handle, uint16_t end_handle)
: Gets all characteristics within a service’s handle range.esp_ble_gattc_get_char_by_uuid(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t start_handle, uint16_t end_handle, esp_bt_uuid_t char_uuid, esp_gattc_char_filter_t *filter)
: Gets characteristics matching a UUID.esp_ble_gattc_get_all_descr(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t char_handle, uint16_t end_handle)
: Gets all descriptors for a characteristic.char_handle
here is the characteristic value handle.esp_ble_gattc_get_descr_by_uuid(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t char_handle, esp_bt_uuid_t descr_uuid)
: Gets a descriptor by UUID.
- Attribute Read/Write:
esp_ble_gattc_read_char(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, esp_gatt_auth_req_t auth_req)
: Reads a characteristic value.esp_ble_gattc_read_char_descr(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, esp_gatt_auth_req_t auth_req)
: Reads a descriptor value.esp_ble_gattc_write_char(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, uint16_t value_len, uint8_t *value, esp_gatt_write_type_t write_type, esp_gatt_auth_req_t auth_req)
: Writes to a characteristic.esp_ble_gattc_write_char_descr(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, uint16_t value_len, uint8_t *value, esp_gatt_write_type_t write_type, esp_gatt_auth_req_t auth_req)
: Writes to a descriptor.
- Notifications/Indications:
esp_ble_gattc_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t server_bda, uint16_t handle)
: Registers to receive notifications/indications for a characteristic handle.esp_ble_gattc_unregister_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t server_bda, uint16_t handle)
: Unregisters.
API Function | Purpose | Key Triggered/Related Event(s) in GATTC Callback |
---|---|---|
esp_ble_gattc_register_callback(cb) | Registers the master GATTC callback function. | (N/A – This sets up the handler for all other events) |
esp_ble_gattc_app_register(app_id) | Registers a GATT client application profile. | ESP_GATTC_REG_EVT |
esp_ble_gattc_open(if, bda, type, direct) | Initiates a connection to a remote peripheral. | ESP_GATTC_OPEN_EVT |
esp_ble_gattc_close(if, conn_id) | Closes an existing connection. | ESP_GATTC_CLOSE_EVT |
esp_ble_gattc_send_mtu_req(if, conn_id) | Requests an ATT MTU exchange with the server. | ESP_GATTC_CFG_MTU_EVT |
esp_ble_gattc_search_service(if, conn_id, filter_uuid) | Discovers services on the connected server. | ESP_GATTC_SEARCH_RES_EVT (per service), ESP_GATTC_SEARCH_CMPL_EVT (completion) |
esp_ble_gattc_get_all_char(if, conn_id, start_h, end_h) | Discovers all characteristics within a service’s handle range. | ESP_GATTC_GET_CHAR_EVT (per characteristic/completion) |
esp_ble_gattc_get_char_by_uuid(…) | Discovers characteristics matching a specific UUID. | ESP_GATTC_GET_CHAR_EVT |
esp_ble_gattc_get_all_descr(if, conn_id, char_h, end_h) | Discovers all descriptors for a characteristic. | ESP_GATTC_GET_DESCR_EVT (per descriptor/completion) |
esp_ble_gattc_read_char(if, conn_id, handle, auth) | Reads a characteristic’s value. | ESP_GATTC_READ_CHAR_EVT |
esp_ble_gattc_read_char_descr(…) | Reads a descriptor’s value. | ESP_GATTC_READ_DESCR_EVT |
esp_ble_gattc_write_char(if, conn_id, handle, len, val, type, auth) | Writes to a characteristic’s value. | ESP_GATTC_WRITE_CHAR_EVT (for write with response) |
esp_ble_gattc_write_char_descr(…) | Writes to a descriptor’s value (e.g., CCCD). | ESP_GATTC_WRITE_DESCR_EVT (for write with response) |
esp_ble_gattc_register_for_notify(if, bda, handle) | Registers to receive notifications/indications for a characteristic. | ESP_GATTC_REG_FOR_NOTIFY_EVT, then ESP_GATTC_NOTIFY_EVT for incoming data. |
esp_ble_gattc_unregister_for_notify(…) | Unregisters from receiving notifications/indications. | ESP_GATTC_UNREG_FOR_NOTIFY_EVT |
Managing Discovered Attribute Handles
A crucial part of GATT client implementation is storing the attribute handles (service start/end handles, characteristic value handles, descriptor handles) discovered from the server. These handles are needed for subsequent read, write, and notification operations. You’ll typically use a data structure (an array of structs, a linked list, etc.) to store this information, often associating it with the connection ID (conn_id
) and GATT interface (gattc_if
).

GATTC Event Handling
The GATTC callback function is central to the client’s operation. It must handle a variety of events:
Event Name (in esp_gattc_cb_event_t) | Description & Significance | Key Parameters in esp_ble_gattc_cb_param_t union |
---|---|---|
ESP_GATTC_REG_EVT | Client application profile registered. Store gattc_if. | reg.status, reg.app_id, reg.gattc_if |
ESP_GATTC_OPEN_EVT | Connection attempt completed (success or failure). If success, store conn_id, remote_bda. Initiate MTU exchange or service discovery. | open.status, open.conn_id, open.gattc_if, open.remote_bda, open.mtu (initial MTU) |
ESP_GATTC_CONNECT_EVT | Physical link established (often precedes ESP_GATTC_OPEN_EVT for GATT-level connection). | connect.conn_id, connect.gattc_if, connect.remote_bda |
ESP_GATTC_DISCONNECT_EVT | Physical link disconnected. | disconnect.conn_id, disconnect.gattc_if, disconnect.remote_bda, disconnect.reason |
ESP_GATTC_CLOSE_EVT | GATT connection closed. Clean up resources. | close.status, close.conn_id, close.remote_bda |
ESP_GATTC_CFG_MTU_EVT | MTU exchange procedure completed. Store negotiated MTU. | cfg_mtu.status, cfg_mtu.conn_id, cfg_mtu.mtu |
ESP_GATTC_SEARCH_RES_EVT | A service is found during discovery. Store service UUID, start/end handles. | search_res.conn_id, search_res.srvc_id, search_res.start_handle, search_res.end_handle |
ESP_GATTC_SEARCH_CMPL_EVT | Service discovery process is complete. Initiate characteristic discovery. | search_cmpl.status, search_cmpl.conn_id |
ESP_GATTC_GET_CHAR_EVT | A characteristic is found (or discovery for a service completes). Store char UUID, value handle, properties. Initiate descriptor discovery. | get_char.status, get_char.conn_id, get_char.srvc_id, get_char.char_id, get_char.char_handle, get_char.char_prop |
ESP_GATTC_GET_DESCR_EVT | A descriptor is found (or discovery for a char completes). Store descr UUID and handle. | get_descr.status, get_descr.conn_id, get_descr.srvc_id, get_descr.char_id, get_descr.descr_id, get_descr.handle |
ESP_GATTC_READ_CHAR_EVT | Result of a characteristic read operation. Process received value. | read.status, read.conn_id, read.handle, read.value, read.value_len |
ESP_GATTC_READ_DESCR_EVT | Result of a descriptor read operation. | read.status, read.conn_id, read.handle (descriptor handle), read.value, read.value_len |
ESP_GATTC_WRITE_CHAR_EVT | Confirmation of a characteristic write operation (if write with response). | write.status, write.conn_id, write.handle |
ESP_GATTC_WRITE_DESCR_EVT | Confirmation of a descriptor write operation (e.g., to CCCD). | write.status, write.conn_id, write.handle |
ESP_GATTC_NOTIFY_EVT | A notification or indication is received from the server. Process the value. | notify.conn_id, notify.remote_bda, notify.handle (char value handle), notify.value_len, notify.value, notify.is_notify (true for notify, false for indicate) |
ESP_GATTC_REG_FOR_NOTIFY_EVT | Confirmation that registration for notifications was successful. | reg_for_notify.status, reg_for_notify.handle |
A state machine is often useful within the client application to manage the sequential nature of discovery and interaction.
Practical Examples
Prerequisites:
- An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2. Not ESP32-S2).
- VS Code with the Espressif IDF Extension.
- A BLE peripheral device to connect to. This could be:
- Another ESP32 running the GATT server code from Chapter 57.
- A standard BLE device (e.g., a fitness tracker, heart rate monitor).
- A smartphone app acting as a BLE peripheral (e.g., “nRF Connect for Mobile” can simulate peripherals).
Example 1: Scanning for BLE Peripherals
This example demonstrates how to initialize BLE and scan for nearby advertising 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/gatt_client_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"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"
static const char* GATTC_TAG = "GATT_CLIENT_SCAN_EXAMPLE";
// Scan parameters
static esp_ble_scan_params_t ble_scan_params = {
.scan_type = BLE_SCAN_TYPE_ACTIVE, // Active scan
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0x50, // N * 0.625ms
.scan_window = 0x30, // N * 0.625ms
.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE // Process all advertising packets
};
// GAP event handler
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
uint8_t *adv_name = NULL;
uint8_t adv_name_len = 0;
switch (event) {
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
ESP_LOGI(GATTC_TAG, "Scan parameters set, starting scan...");
// The duration of the scan, 0 for continuous scan
uint32_t duration = 30; // Scan for 30 seconds
esp_ble_gap_start_scanning(duration);
break;
}
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTC_TAG, "Scan start failed, error status = %x", param->scan_start_cmpl.status);
} else {
ESP_LOGI(GATTC_TAG, "Scan started successfully");
}
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
ESP_LOGI(GATTC_TAG, "Device found: Addr %02x:%02x:%02x:%02x:%02x:%02x, RSSI: %d",
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],
scan_result->scan_rst.rssi);
// Try to get device name from advertising data
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(GATTC_TAG, "Device 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(GATTC_TAG, "Short Device Name: %.*s", adv_name_len, adv_name);
} else {
ESP_LOGI(GATTC_TAG, "No device name found in advertisement");
}
}
// Here you could decide to connect to this device based on its address or name
// For example: if (memcmp(scan_result->scan_rst.bda, target_bda, ESP_BD_ADDR_LEN) == 0) { esp_ble_gattc_open(...); }
break;
case ESP_GAP_SEARCH_INQ_CMPL_EVT: // Inquiry complete
ESP_LOGI(GATTC_TAG, "Scan completed");
// You might restart scanning or proceed with a connection if a target was found
break;
default:
break;
}
break;
}
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(GATTC_TAG, "Scan stop failed, error status = %x", param->scan_stop_cmpl.status);
} else {
ESP_LOGI(GATTC_TAG, "Scan stopped successfully");
}
break;
default:
break;
}
}
// GATTC event handler (minimal for this example)
static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
ESP_LOGI(GATTC_TAG, "GATTC EVT %d, gattc_if %d", event, gattc_if);
// Further GATTC event handling will be added in subsequent examples
}
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(GATTC_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(GATTC_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTC_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTC_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
// Register GAP callback
ret = esp_ble_gap_register_callback(esp_gap_cb);
if (ret) {
ESP_LOGE(GATTC_TAG, "gap register error, error code = %x", ret);
return;
}
// Register GATTC callback
ret = esp_ble_gattc_register_callback(esp_gattc_cb);
if (ret) {
ESP_LOGE(GATTC_TAG, "gattc register error, error code = %x", ret);
return;
}
// Register GATTC application profile (app_id can be any value)
ret = esp_ble_gattc_app_register(0);
if (ret) {
ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
return;
}
// Set scan parameters
ret = esp_ble_gap_set_scan_params(&ble_scan_params);
if (ret) {
ESP_LOGE(GATTC_TAG, "set scan params error, error code = %x", ret);
}
ESP_LOGI(GATTC_TAG, "GATT Client Scan Example Initialized");
}
3. Build, Flash, and Monitor:
- Connect your ESP32 board.
- Run
idf.py build
. - Run
idf.py flash monitor
.
4. Observe Output:
- The serial monitor will show logs from
GATTC_TAG
. - After “Scan parameters set, starting scan…”, you should see “Device found…” messages for nearby BLE peripherals, along with their MAC addresses, RSSI, and names (if available in the advertisement).
- The scan will run for 30 seconds (as set by
duration
) and then stop.
Example 2: Connecting and Discovering Services
This example builds upon Example 1 to connect to a specific device (you’ll need to hardcode its MAC address or modify the logic to select one from scan results) and then discover its services.
Conceptual Additions to esp_gattc_cb
for Example 2 & 3:
// ... (previous includes and definitions) ...
// Store remote BDA of the device to connect to
static esp_bd_addr_t remote_bda_to_connect = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}; // TODO: Replace with actual MAC
static bool connect_to_device = false; // Set to true when target device is found
// GATTC Profile ID and GATTC Interface
static uint16_t gattc_profile_app_id = 0; // Matches app_register
static esp_gatt_if_t client_gattc_if = ESP_GATT_IF_NONE;
static uint16_t current_conn_id = 0xFFFF;
// State for discovery
typedef enum {
CLIENT_IDLE,
CLIENT_CONNECTING,
CLIENT_CONNECTED,
CLIENT_DISCOVERING_SERVICES,
CLIENT_DISCOVERING_CHARS,
CLIENT_DISCOVERING_DESCRS,
CLIENT_READY_TO_INTERACT
} client_state_t;
static client_state_t g_client_state = CLIENT_IDLE;
// Placeholder for storing discovered service info
#define MAX_SERVICES 10
typedef struct {
esp_bt_uuid_t uuid;
uint16_t start_handle;
uint16_t end_handle;
// You'd add storage for characteristics and descriptors here too
} discovered_service_t;
static discovered_service_t g_services[MAX_SERVICES];
static uint8_t g_service_count = 0;
static uint8_t g_current_service_idx_for_char_discovery = 0;
// Modified GAP callback to initiate connection
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
// ... (scan result handling from Example 1) ...
case ESP_GAP_BLE_SCAN_RESULT_EVT: {
esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
ESP_LOGI(GATTC_TAG, "Device found: Addr %02x:%02x:%02x:%02x:%02x:%02x", /* ... */);
// Example: Connect if MAC address matches
if (memcmp(scan_result->scan_rst.bda, remote_bda_to_connect, ESP_BD_ADDR_LEN) == 0) {
ESP_LOGI(GATTC_TAG, "Target device found. Stopping scan and attempting to connect.");
esp_ble_gap_stop_scanning();
connect_to_device = true; // Flag to connect after scan stops
// Actual connect call will be in ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
// or directly here if you ensure gattc_if is valid
}
}
break;
}
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// ...
if (connect_to_device && client_gattc_if != ESP_GATT_IF_NONE) {
ESP_LOGI(GATTC_TAG, "Connecting to remote device...");
esp_ble_gattc_open(client_gattc_if, remote_bda_to_connect, BLE_ADDR_TYPE_PUBLIC, true);
g_client_state = CLIENT_CONNECTING;
connect_to_device = false; // Reset flag
}
break;
// ...
}
// Modified GATTC callback for connection and discovery
static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
ESP_LOGI(GATTC_TAG, "GATTC EVT %d, gattc_if %d, status %d", event, gattc_if, param->reg.status);
switch (event) {
case ESP_GATTC_REG_EVT:
if (param->reg.status == ESP_GATT_OK) {
ESP_LOGI(GATTC_TAG, "GATTC app registered successfully, app_id %d, gattc_if %d", param->reg.app_id, gattc_if);
client_gattc_if = gattc_if; // Store the interface
} else {
ESP_LOGE(GATTC_TAG, "GATTC app registration failed");
}
break;
case ESP_GATTC_OPEN_EVT:
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(GATTC_TAG, "Connected successfully to %02x:%02x:%02x:%02x:%02x:%02x, conn_id %d",
param->open.remote_bda[0], param->open.remote_bda[1], param->open.remote_bda[2],
param->open.remote_bda[3], param->open.remote_bda[4], param->open.remote_bda[5],
param->open.conn_id);
current_conn_id = param->open.conn_id;
g_client_state = CLIENT_CONNECTED;
// Optional: Configure MTU
// esp_ble_gattc_send_mtu_req(gattc_if, param->open.conn_id);
// Start service discovery
ESP_LOGI(GATTC_TAG, "Starting service discovery...");
esp_ble_gattc_search_service(gattc_if, param->open.conn_id, NULL); // NULL for all primary services
g_client_state = CLIENT_DISCOVERING_SERVICES;
g_service_count = 0;
} else {
ESP_LOGE(GATTC_TAG, "Connection failed, status %d", param->open.status);
g_client_state = CLIENT_IDLE;
// Optionally, restart scanning
// esp_ble_gap_start_scanning(0);
}
break;
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status == ESP_GATT_OK) {
ESP_LOGI(GATTC_TAG, "MTU configured to %d", param->cfg_mtu.mtu);
} else {
ESP_LOGE(GATTC_TAG, "MTU configuration failed, status %d", param->cfg_mtu.status);
}
break;
case ESP_GATTC_SEARCH_RES_EVT: { // Service found
ESP_LOGI(GATTC_TAG, "Service found: UUID %04x, start_handle %d, end_handle %d",
param->search_res.srvc_id.uuid.uuid.uuid16, // Assuming 16-bit UUID for simplicity
param->search_res.start_handle, param->search_res.end_handle);
if (g_service_count < MAX_SERVICES) {
g_services[g_service_count].uuid = param->search_res.srvc_id.uuid;
g_services[g_service_count].start_handle = param->search_res.start_handle;
g_services[g_service_count].end_handle = param->search_res.end_handle;
g_service_count++;
}
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: // Service discovery complete
ESP_LOGI(GATTC_TAG, "Service discovery complete, found %d services.", g_service_count);
if (param->search_cmpl.status == ESP_GATT_OK) {
if (g_service_count > 0) {
// Start characteristic discovery for the first service
g_current_service_idx_for_char_discovery = 0;
ESP_LOGI(GATTC_TAG, "Discovering characteristics for service UUID %04x", g_services[0].uuid.uuid.uuid16);
esp_ble_gattc_get_all_char(gattc_if, current_conn_id,
g_services[0].start_handle,
g_services[0].end_handle);
g_client_state = CLIENT_DISCOVERING_CHARS;
} else {
ESP_LOGI(GATTC_TAG, "No services found.");
g_client_state = CLIENT_READY_TO_INTERACT; // Or disconnect
}
} else {
ESP_LOGE(GATTC_TAG, "Service discovery failed, status %d", param->search_cmpl.status);
g_client_state = CLIENT_IDLE;
}
break;
case ESP_GATTC_GET_CHAR_EVT: // Characteristic found
if (param->get_char.status == ESP_GATT_OK) {
ESP_LOGI(GATTC_TAG, "Characteristic found: UUID %04x, handle %d, props %02x",
param->get_char.char_id.uuid.uuid.uuid16, // Assuming 16-bit
param->get_char.char_handle,
param->get_char.char_prop);
// TODO: Store characteristic info (handle, UUID, properties)
// TODO: If this is the last char for current service, move to next service or desc discovery
// For simplicity, we are not doing full recursive discovery here.
// You would typically discover descriptors for this characteristic now.
// esp_ble_gattc_get_all_descr(gattc_if, current_conn_id, param->get_char.char_handle, g_services[g_current_service_idx_for_char_discovery].end_handle);
// g_client_state = CLIENT_DISCOVERING_DESCRS;
// Example: If it's a known characteristic, try to read it
// if (param->get_char.char_id.uuid.uuid.uuid16 == ESP_GATT_UUID_MANU_NAME) {
// esp_ble_gattc_read_char(gattc_if, current_conn_id, param->get_char.char_handle, ESP_GATT_AUTH_REQ_NONE);
// }
} else {
ESP_LOGE(GATTC_TAG, "Get characteristic failed for service %d, status %d", g_current_service_idx_for_char_discovery, param->get_char.status);
}
// Logic to move to next characteristic or next service for discovery
// This part needs careful state management. If all chars for current service are done:
// g_current_service_idx_for_char_discovery++;
// if (g_current_service_idx_for_char_discovery < g_service_count) { ... start char discovery for next service ...}
// else { g_client_state = CLIENT_READY_TO_INTERACT; }
break;
// TODO: Add handlers for ESP_GATTC_GET_DESCR_EVT, ESP_GATTC_READ_CHAR_EVT,
// ESP_GATTC_WRITE_CHAR_EVT, ESP_GATTC_NOTIFY_EVT, ESP_GATTC_DISCONNECT_EVT etc.
default:
break;
}
}
Note: The above code for connection and discovery is significantly simplified. A robust implementation requires careful state management to proceed through service, characteristic, and descriptor discovery sequentially, and to store all discovered attribute handles. Refer to the
gattc_demo
orble_spp_client
examples in ESP-IDF for more complete implementations.
Example 3: Reading a Characteristic and Handling Notifications
This would further extend Example 2.
- Reading: After discovering a characteristic (e.g., “Manufacturer Name String” UUID
0x2A29
), callesp_ble_gattc_read_char()
using its handle. The result comes inESP_GATTC_READ_CHAR_EVT
. - Notifications:
- Discover a notifiable characteristic and its CCCD (UUID
0x2902
). - Write
0x0001
to the CCCD handle usingesp_ble_gattc_write_char_descr()
. - On successful write (
ESP_GATTC_WRITE_DESCR_EVT
), callesp_ble_gattc_register_for_notify()
with the characteristic’s value handle. - Handle incoming data in
ESP_GATTC_NOTIFY_EVT
.
- Discover a notifiable characteristic and its CCCD (UUID
Variant Notes
- ESP32-S2: This variant does not have Bluetooth hardware and cannot operate as a BLE GATT client.
- ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2: All these variants provide full support for BLE GATT client functionality. The ESP-IDF GATTC APIs are consistent across these chips.
- Impact of BLE 5.x Features (from a Client Perspective):
- Larger ATT_MTU: As a client, your ESP32 can request a larger MTU from the server using
esp_ble_gattc_send_mtu_req()
(or by setting local MTU withesp_ble_gatt_set_local_mtu()
which often triggers the exchange). This allows receiving larger characteristic values in a single operation. - LE 2M PHY / Coded PHY: If both the ESP32 client and the peripheral server support these PHYs, the connection can achieve higher throughput (2M PHY) or longer range (Coded PHY). The client doesn’t usually explicitly select the PHY during connection for GATT operations; the stack often handles negotiation or uses defaults. PHY preferences can be set for scanning and connection initiation.
- Scanning for Extended Advertisements: If peripherals use BLE 5.0 extended advertising, the client needs to be configured to scan for these to discover them.
- Larger ATT_MTU: As a client, your ESP32 can request a larger MTU from the server using
The core GATTC operations (connect, discover, read, write, notify) are fundamentally the same. BLE 5.x features enhance the underlying link capabilities.
Common Mistakes & Troubleshooting Tips
Mistake / Issue (Client Focus) | Symptom(s) | Fix / Best Practice |
---|---|---|
Incorrect State Management During Discovery | API calls fail (e.g., ESP_GATT_ILLEGAL_PARAMETER), discovery gets stuck, app crashes. Calling GATTC functions in wrong order/context. | Implement a clear state machine. E.g., only start char discovery in ESP_GATTC_SEARCH_CMPL_EVT. Store handles properly. |
Not Handling All Necessary GATTC Events | App doesn’t proceed to next step, doesn’t clean up on disconnect, misses crucial info. | Ensure GATTC callback covers all relevant events for your workflow. Log unhandled events during development. |
Mismanaging Attribute Handles | Operations target wrong attribute, fail with ESP_GATT_INVALID_HANDLE, data misinterpreted. | Carefully store handles from _GET_CHAR_EVT, _GET_DESCR_EVT. Map handles to services/chars correctly. |
Issues with Notifications/Indications | ESP_GATTC_NOTIFY_EVT never triggered despite server sending. | 1. Discover CCCD handle. 2. Write 0x0001 (notify) or 0x0002 (indicate) to CCCD. 3. Wait for ESP_GATTC_WRITE_DESCR_EVT. 4. Call esp_ble_gattc_register_for_notify() with char value handle. |
Connection Failures / Unexpected Disconnects | esp_ble_gattc_open() fails, or connection drops. | Check remote BDA & address type. Ensure peripheral is connectable. Log disconnect reasons from ESP_GATTC_DISCONNECT_EVT. Check supervision timeout. |
Forgetting to Register GATTC App Profile | GATTC API calls fail early, gattc_if is invalid. | Call esp_ble_gattc_app_register() and wait for ESP_GATTC_REG_EVT to get a valid gattc_if before other GATTC operations. |
Blocking in Callbacks | BLE stack becomes unresponsive, other events missed, watchdog timers may trigger. | Keep GATTC (and GAP) callback functions non-blocking. Offload lengthy operations to separate tasks using queues or event groups. |
Exercises
- Targeted Device Connection:
- Modify Example 1 (scanning) to automatically connect to a device that advertises a specific name (e.g., “ESP32_GATT_SERVER” from Chapter 57).
- Once connected, log a success message and then disconnect.
- Read All Characteristics of a Service:
- Connect to a device known to have the “Device Information Service” (UUID
0x180A
). - Discover this service.
- Discover all characteristics within this service.
- For each discovered characteristic, attempt to read its value and log the UUID and the read value (if successful and readable).
- Connect to a device known to have the “Device Information Service” (UUID
- Write to a Writable Characteristic:
- Set up the GATT server from Chapter 57 (or a similar server) that has a writable characteristic (e.g., the “LED State” from Chapter 57’s exercises).
- Implement a GATT client that connects to this server, discovers the specific writable characteristic by its UUID.
- Write a new value to the characteristic (e.g., toggle the LED state).
- Verify on the server side (via logs or actual LED) that the write was successful.
- Full Interaction with Custom Sensor:
- Assume you have a BLE peripheral with the “Custom Environment Sensing Service” designed in Chapter 58 (Example 1), which has a notifiable “Temperature Measurement” characteristic and a readable “Humidity Measurement” characteristic.
- Write an ESP32 GATT client application that:
- Scans and connects to this specific peripheral (identify it by name or advertised service UUID).
- Discovers the “Custom Environment Service.”
- Discovers the “Temperature Measurement” and “Humidity Measurement” characteristics.
- Reads the “Humidity Measurement” value and prints it.
- Enables notifications for the “Temperature Measurement” characteristic.
- Prints any received temperature notifications to the console for 30 seconds.
- Disables notifications and disconnects.
Summary
- A GATT Client discovers and interacts with services and characteristics offered by a GATT Server.
- The process involves scanning, connecting, discovering services, characteristics, and descriptors, and then performing operations like read, write, and subscribe/handle notifications.
- ESP-IDF provides
esp_gattc_api.h
for all GATTC operations, which are event-driven via a registered callback. - Proper state management and careful handling of attribute handles are crucial for successful GATT client implementation.
- Key GATTC events include
ESP_GATTC_OPEN_EVT
,ESP_GATTC_SEARCH_RES_EVT
,ESP_GATTC_SEARCH_CMPL_EVT
,ESP_GATTC_GET_CHAR_EVT
,ESP_GATTC_READ_CHAR_EVT
,ESP_GATTC_WRITE_CHAR_EVT
, andESP_GATTC_NOTIFY_EVT
. - To receive notifications/indications, the client must write to the server’s CCCD for the specific characteristic and then register for notifications locally.
Further Reading
- ESP-IDF GATT Client Example: https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/bluedroid/ble/gatt_client/tutorial/Gatt_Client_Example_Walkthrough.md
- ESP-IDF
ble_spp_client
Example:examples/bluetooth/bluedroid/ble/ble_spp_client
demonstrates another client application. - ESP-IDF API Reference –
esp_gattc_api.h
: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/esp_gattc.html - Bluetooth SIG Website (bluetooth.com): For specifications on GATT, ATT, and standard profiles: https://www.bluetooth.com/bluetooth-resources/intro-to-bluetooth-gap-gatt/