Chapter 57: BLE GATT Server Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the role and responsibilities of a GATT Server in BLE.
- Define and create custom BLE Services and Characteristics using ESP-IDF.
- Add Descriptors to Characteristics, including the Client Characteristic Configuration Descriptor (CCCD).
- Implement handlers for read and write requests from a GATT Client.
- Send notifications and indications to subscribed GATT Clients.
- Initialize and manage the GATT server attribute table.
- Use ESP-IDF APIs to register and start services.
- Test your GATT server implementation using a standard BLE scanner application.
Introduction
In Chapter 56, we explored the overall architecture of Bluetooth Low Energy. We learned that BLE devices can act as Peripherals (typically GATT Servers) or Centrals (typically GATT Clients). This chapter dives into the practical implementation of a GATT Server on an ESP32.
A GATT (Generic Attribute Profile) Server is the device that holds the data and makes it available to other devices. Think of a fitness tracker: it acts as a GATT server, hosting services like “Heart Rate Service” which in turn contains characteristics like “Heart Rate Measurement.” A smartphone (the GATT Client) can then connect to the tracker to read this heart rate data.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% sequenceDiagram participant Client as GATT Client (e.g., Smartphone) participant Server as GATT Server (e.g., ESP32 Fitness Tracker) Client->>Server: Initiates Connection (GAP Discover & Connect) activate Server Server-->>Client: Connection Established deactivate Server Client->>Server: Request: Discover Services activate Server Server-->>Client: Response: List of Services (e.g., Heart Rate Service) deactivate Server Client->>Server: Request: Discover Characteristics (for Heart Rate Service) activate Server Server-->>Client: Response: List of Characteristics (e.g., Heart Rate Measurement) deactivate Server Client->>Server: Request: Read Characteristic Value (Heart Rate Measurement) activate Server Server-->>Client: Response: Value (e.g., "60 bpm") deactivate Server opt Client enables notifications for Heart Rate Client->>Server: Write Request: Enable Notifications (to CCCD of Heart Rate Measurement) activate Server Server-->>Client: Write Response: OK deactivate Server end loop Server-initiated updates alt Value Changes Server->>Client: Send Notification: New Heart Rate (e.g., "62 bpm") end end Client->>Server: Initiates Disconnection activate Server Server-->>Client: Disconnection Complete deactivate Server %% Styling Classes (already defined in prompt, re-iterating for clarity in this block) %% classDef Client fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF %% classDef Server fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
As an ESP32 developer, creating a GATT server allows your device to expose sensor readings, control points, device information, and much more in a standardized, low-power way. This chapter will guide you through defining your data structure (services, characteristics, and descriptors) and handling interactions with connected clients using the ESP-IDF v5.x.
Theory
Recall from Chapter 56 that the GATT profile defines how attributes (data) are exchanged between connected BLE devices. The GATT Server is the entity that stores these attributes and provides them to a GATT Client upon request.
GATT Server Responsibilities
A GATT Server is responsible for:
- Defining the Attribute Database: This involves specifying a collection of services, characteristics, and descriptors that represent the data and functionality the server offers.
- Storing Attribute Values: Maintaining the current values for all defined characteristics.
- Handling Client Requests: Responding to read and write requests for characteristic values and descriptor values from connected GATT Clients.
- Sending Notifications/Indications: Pushing updates of characteristic values to clients that have subscribed to them.
- Managing Security: Enforcing appropriate permissions for attribute access (though detailed security is covered in Chapter 60).
Building the Attribute Table
The core of a GATT server is its attribute table (also known as the GATT database or profile). This table is a hierarchical structure:
- Services: Group related functionality. A service is a collection of characteristics.
- UUID: Each service is identified by a Universally Unique Identifier (UUID). Standard services defined by the Bluetooth SIG use 16-bit UUIDs (e.g.,
0x180F
for Battery Service), while custom services require full 128-bit UUIDs. - Type: Services can be primary (a primary function of the device) or secondary (included by another service). Most services are primary.
- UUID: Each service is identified by a Universally Unique Identifier (UUID). Standard services defined by the Bluetooth SIG use 16-bit UUIDs (e.g.,
UUID Type | Size | Example (SIG Standard) | Usage |
---|---|---|---|
16-bit UUID | 2 bytes (16 bits) | 0x180F (Battery Service) | Used for standard services and characteristics defined by the Bluetooth SIG. Derived from a base 128-bit Bluetooth Base UUID. |
128-bit UUID | 16 bytes (128 bits) | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | Required for custom services and characteristics not defined by the Bluetooth SIG. Ensures global uniqueness. |
- Characteristics: Represent individual data points or control points within a service.
- UUID: Each characteristic also has a UUID (16-bit for standard, 128-bit for custom).
- Value: The actual data held by the characteristic.
- Properties: Define how a characteristic’s value can be accessed (e.g., Read, Write, Write Without Response, Notify, Indicate).
- Permissions: Define the security level required for accessing the characteristic’s value (e.g., Read Encrypted, Write Authenticated).
Property | ESP-IDF Flag (Partial Example) | Description |
---|---|---|
Read | ESP_GATT_CHAR_PROP_BIT_READ | Allows the client to read the characteristic’s value. |
Write | ESP_GATT_CHAR_PROP_BIT_WRITE | Allows the client to write the characteristic’s value (with response). |
Write Without Response | ESP_GATT_CHAR_PROP_BIT_WRITE_NR | Allows the client to write the characteristic’s value (without server response/acknowledgment). |
Notify | ESP_GATT_CHAR_PROP_BIT_NOTIFY | Allows the server to send updates to the client without acknowledgment when the value changes (if client subscribed). |
Indicate | ESP_GATT_CHAR_PROP_BIT_INDICATE | Allows the server to send updates to the client with acknowledgment when the value changes (if client subscribed). |
Broadcast | ESP_GATT_CHAR_PROP_BIT_BROADCAST | Allows the characteristic value to be broadcast in advertising packets (less common for GATT server interaction). |
Authenticated Signed Writes | ESP_GATT_CHAR_PROP_BIT_AUTH | Allows signed data to be written to the characteristic value. |
Extended Properties | ESP_GATT_CHAR_PROP_BIT_EXT_PROP | Indicates that an Extended Properties Descriptor is present. |
- Descriptors: Provide additional metadata about a characteristic.
- UUID: Each descriptor has a UUID. Common standard descriptors include:
- Client Characteristic Configuration Descriptor (CCCD – UUID
0x2902
): Allows a client to enable/disable notifications or indications for a characteristic. This is crucial for server-initiated updates. - Characteristic User Description (UUID
0x2901
): A human-readable string describing the characteristic. - Characteristic Presentation Format (UUID
0x2904
): Defines the format, exponent, and unit of the characteristic value.
- Client Characteristic Configuration Descriptor (CCCD – UUID
- Value: The descriptor’s own value (e.g., the CCCD value indicates if notifications are enabled).
- Permissions: Define security for accessing the descriptor.
- UUID: Each descriptor has a UUID. Common standard descriptors include:
Descriptor Name | UUID | ESP-IDF UUID Macro (Example) | Purpose |
---|---|---|---|
Client Characteristic Configuration Descriptor (CCCD) | 0x2902 | ESP_GATT_UUID_CHAR_CLIENT_CONFIG | Allows a client to enable/disable notifications or indications for a characteristic. Essential for server-initiated updates. |
Characteristic User Description | 0x2901 | ESP_GATT_UUID_CHAR_DESCRIPTION | Provides a human-readable string describing the characteristic (e.g., “LED Control Switch”). |
Characteristic Presentation Format | 0x2904 | ESP_GATT_UUID_CHAR_PRESENT_FORMAT | Defines the format, exponent, unit, and namespace of the characteristic value. |
Characteristic Aggregate Format | 0x2905 | ESP_GATT_UUID_CHAR_AGG_FORMAT | Used when a characteristic value is an aggregate of multiple characteristics. Lists attribute handles of those characteristics. |
Server Characteristic Configuration | 0x2903 | ESP_GATT_UUID_CHAR_SRVR_CONFIG | Allows a client to configure broadcast options for the characteristic on the server. |
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD subgraph GATT_PROFILE [GATT Profile / Attribute Table] direction LR S1[Service 1: e.g., Heart Rate Service] S2[Service 2: e.g., Custom Sensor Service] subgraph Service_1_Details [Service 1] direction LR S1_DECL["<b>Service Declaration</b><br>UUID: 0x180D (Heart Rate)<br>Type: Primary"] S1_C1[Characteristic 1: Heart Rate Measurement] S1_C2[Characteristic 2: Body Sensor Location] end subgraph S1_C1_Details [Characteristic: Heart Rate Measurement] direction LR S1_C1_DECL["<b>Char. Declaration</b><br>Properties: Notify<br>Handle: 0x002A<br>UUID: 0x2A37"] S1_C1_VAL["<b>Char. Value</b><br>Handle: 0x002B<br>Data: (e.g., 60 bpm)"] S1_C1_DESC1["<b>Descriptor: CCCD</b><br>Handle: 0x002C<br>UUID: 0x2902<br>Value: (e.g., 0x0001 for Notify)"] end subgraph S1_C2_Details [Characteristic: Body Sensor Location] direction LR S1_C2_DECL["<b>Char. Declaration</b><br>Properties: Read<br>Handle: 0x002E<br>UUID: 0x2A38"] S1_C2_VAL["<b>Char. Value</b><br>Handle: 0x002F<br>Data: (e.g., Chest)"] end subgraph Service_2_Details [Service 2] direction LR S2_DECL["<b>Service Declaration</b><br>UUID: (Custom 128-bit UUID)<br>Type: Primary"] S2_C1[Characteristic A: Custom Data Point] end subgraph S2_C1_Details [Characteristic: Custom Data Point] direction LR S2_C1_DECL["<b>Char. Declaration</b><br>Properties: Read, Write<br>Handle: 0x0035<br>UUID: (Custom 128-bit UUID)"] S2_C1_VAL["<b>Char. Value</b><br>Handle: 0x0036<br>Data: (e.g., 0x01, 0xFF)"] S2_C1_DESC1["<b>Descriptor: User Description</b><br>Handle: 0x0037<br>UUID: 0x2901<br>Value: <b>My Custom Data</b>"] end S1 --> S1_DECL S1 --> S1_C1 S1 --> S1_C2 S1_C1 --> S1_C1_DECL S1_C1 --> S1_C1_VAL S1_C1 --> S1_C1_DESC1 S1_C2 --> S1_C2_DECL S1_C2 --> S1_C2_VAL S2 --> S2_DECL S2 --> S2_C1 S2_C1 --> S2_C1_DECL S2_C1 --> S2_C1_VAL S2_C1 --> S2_C1_DESC1 end classDef service fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF; classDef characteristic fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef attribute fill:#EDE9FE,stroke:#5B21B6,stroke-width:1px,color:#5B21B6; classDef descriptor fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46; class S1,S2 service; class S1_C1,S1_C2,S2_C1 characteristic; class S1_DECL,S2_DECL,S1_C1_DECL,S1_C2_DECL,S2_C1_DECL attribute; class S1_C1_VAL,S1_C2_VAL,S2_C1_VAL attribute; class S1_C1_DESC1,S2_C1_DESC1 descriptor;
ESP-IDF GATT Server APIs
The ESP-IDF provides a rich set of APIs, primarily in esp_gatts_api.h
, to implement a GATT server. Key steps and functions include:
- Registering Application Profile:
- An application profile is essentially your GATT server instance. You register it with
esp_ble_gatts_app_register(uint16_t app_id)
. Theapp_id
is chosen by you (e.g.,0
to0xFFFF
). This call triggers anESP_GATTS_REG_EVT
in your GATTS callback.
- An application profile is essentially your GATT server instance. You register it with
- Registering GATTS Callback:
esp_ble_gatts_register_callback(esp_gatts_cb_t callback)
: This is crucial. All GATT server events (connections, read/write requests, etc.) are delivered to this callback function.
- Creating Services:
esp_ble_gatts_create_service(esp_gatt_if_t gatts_if, esp_gatt_srvc_id_t *service_id, uint16_t num_handle)
:gatts_if
: The interface ID received inESP_GATTS_REG_EVT
.service_id
: A structure specifying the service UUID and whether it’s primary or secondary.num_handle
: The total number of attribute handles needed for this service (service declaration + characteristics + descriptors). This needs careful calculation.
- This triggers an
ESP_GATTS_CREATE_EVT
containing the service handle.
- Adding Characteristics:
esp_ble_gatts_add_char(uint16_t service_handle, esp_bt_uuid_t *char_uuid, esp_gatt_perm_t perm, esp_gatt_char_prop_t property, esp_attr_value_t *char_val, esp_attr_control_t *control)
:service_handle
: Obtained fromESP_GATTS_CREATE_EVT
.char_uuid
: The UUID of the characteristic.perm
: Attribute permissions (e.g.,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE
).property
: Characteristic properties (e.g.,ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY
).char_val
(optional): Initial value of the characteristic. Can be set to automatically respond to read requests ifcontrol->auto_rsp
is set.control
(optional): Defines if the stack should automatically respond to read requests (ESP_GATT_AUTO_RSP
) or if the application will respond manually (ESP_GATT_RSP_BY_APP
).
- This triggers an
ESP_GATTS_ADD_CHAR_EVT
containing the characteristic attribute handle.
- Adding Descriptors:
esp_ble_gatts_add_char_descr(uint16_t service_handle, esp_bt_uuid_t *descr_uuid, esp_gatt_perm_t perm, esp_attr_value_t *char_descr_val, esp_attr_control_t *control)
:- Similar to adding a characteristic, but adds a descriptor to the last added characteristic.
- Crucial for adding CCCDs (
ESP_GATT_UUID_CHAR_CLIENT_CONFIG
) to enable notifications/indications. CCCD permissions are typicallyESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE
.
- This triggers an
ESP_GATTS_ADD_CHAR_DESCR_EVT
.
- Starting Services:
esp_ble_gatts_start_service(uint16_t service_handle)
: Makes the service and its attributes visible to clients.- This triggers an
ESP_GATTS_START_EVT
.
- Handling GATT Events in Callback:The GATTS callback function you register will receive events like:
ESP_GATTS_CONNECT_EVT
: A client has connected. Storeconn_id
andgatts_if
.ESP_GATTS_DISCONNECT_EVT
: A client has disconnected.ESP_GATTS_READ_EVT
: Client requests to read an attribute.- If not using
ESP_GATT_AUTO_RSP
, the application must prepare a responseesp_gatt_rsp_t
and send it usingesp_ble_gatts_send_response()
.
- If not using
ESP_GATTS_WRITE_EVT
: Client writes to an attribute (characteristic or descriptor).- Crucial for handling writes to CCCDs to enable/disable notifications.
- If
need_rsp
is true in the event parameters, the application must send a response usingesp_ble_gatts_send_response()
.
ESP_GATTS_CONF_EVT
: Confirmation that an indication sent by the server was received by the client.ESP_GATTS_MTU_EVT
: MTU has been exchanged/updated.
- Sending Notifications/Indications:
esp_ble_gatts_send_indicate(esp_gatt_if_t gatts_if, uint16_t conn_id, uint16_t attr_handle, uint16_t value_len, uint8_t *value, bool need_confirm)
:gatts_if
,conn_id
: Identify the client.attr_handle
: The attribute handle of the characteristic value to send.value
: Pointer to the data.need_confirm
:false
for notifications,true
for indications (which require client confirmation viaESP_GATTS_CONF_EVT
).
- Advertising:
- A GATT server needs to advertise itself so clients can discover and connect to it. This involves GAP (Generic Access Profile) functions like
esp_ble_gap_config_adv_data()
andesp_ble_gap_start_advertising()
, covered in more detail in Chapter 62. For now, we’ll use basic advertising.
- A GATT server needs to advertise itself so clients can discover and connect to it. This involves GAP (Generic Access Profile) functions like
API Function | Purpose | Key Triggered Event in GATTS Callback |
---|---|---|
esp_ble_gatts_app_register(app_id) | Registers the application profile (GATT server instance) with the BLE stack. | ESP_GATTS_REG_EVT |
esp_ble_gatts_register_callback(callback) | Registers the master GATTS callback function to handle all GATT server events. | (N/A – This sets up the handler for all other events) |
esp_ble_gatts_create_service(gatts_if, service_id, num_handle) | Creates a new service in the attribute table. | ESP_GATTS_CREATE_EVT |
esp_ble_gatts_add_char(service_handle, char_uuid, perm, property, char_val, control) | Adds a characteristic to a previously created service. | ESP_GATTS_ADD_CHAR_EVT |
esp_ble_gatts_add_char_descr(service_handle, descr_uuid, perm, descr_val, control) | Adds a descriptor to the last added characteristic. | ESP_GATTS_ADD_CHAR_DESCR_EVT |
esp_ble_gatts_start_service(service_handle) | Makes the service and its attributes visible and accessible to clients. | ESP_GATTS_START_EVT |
esp_ble_gatts_stop_service(service_handle) | Stops a service, making it unavailable to clients. | ESP_GATTS_STOP_EVT |
esp_ble_gatts_delete_service(service_handle) | Deletes a service from the attribute table. | ESP_GATTS_DELETE_EVT |
esp_ble_gatts_send_response(gatts_if, conn_id, trans_id, status, rsp) | Sends a response to a client’s read or write request when manual response (ESP_GATT_RSP_BY_APP) is configured. | (N/A – Action taken by server) |
esp_ble_gatts_send_indicate(gatts_if, conn_id, attr_handle, value_len, value, need_confirm) | Sends a notification (need_confirm=false) or indication (need_confirm=true) to a subscribed client. | ESP_GATTS_CONF_EVT (for indications) |
esp_ble_gatts_set_attr_value(attr_handle, length, value) | Sets the value of an attribute in the server’s local database. | (N/A – Action taken by server) |
esp_ble_gatts_get_attr_value(attr_handle, length, value) | Gets the value of an attribute from the server’s local database. | (N/A – Action taken by server) |
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% graph TD A[Start: Initialize BLE Stack] --> B{esp_bluedroid_init/enable}; B --> C["esp_ble_gatts_register_callback() <br> (Register GATTS Event Handler)"]; C --> D["esp_ble_gatts_app_register() <br> (Register App Profile ID)"]; D -- ESP_GATTS_REG_EVT --> E{"Store gatts_if <br> from event params"}; E --> F["esp_ble_gatts_create_service() <br> (Define Service UUID, type, num_handles)"]; F -- ESP_GATTS_CREATE_EVT --> G{"Store service_handle <br> from event params"}; G --> H["esp_ble_gatts_add_char() <br> (Define Char UUID, props, perms, value*)"]; H -- ESP_GATTS_ADD_CHAR_EVT --> I{"Store char_handle <br> from event params"}; I --> J{Need Descriptors for this Char?}; J -- Yes --> K["esp_ble_gatts_add_char_descr() <br> (e.g., CCCD, User Desc.)"]; K -- ESP_GATTS_ADD_CHAR_DESCR_EVT --> L{"Store descr_handle <br> from event params"}; L --> M{More Descriptors for this Char?}; M -- Yes --> K; M -- No --> N; J -- No --> N; N{More Characteristics for this Service?}; N -- Yes --> H; N -- No --> O["esp_ble_gatts_start_service() <br> (Use stored service_handle)"]; O -- ESP_GATTS_START_EVT --> P{Service Started Successfully?}; P -- Yes --> Q[Service is Active]; P -- No --> R[Error: Service Start Failed]; Q --> S["esp_ble_gap_config_adv_data() <br> esp_ble_gap_start_advertising()"]; S --> T[Device Advertising with GATT Service]; classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef eventNode fill:#FEF9C3,stroke:#CA8A04,stroke-width:1px,color:#713F12; 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,S,T startNode; class C,D,E,F,G,H,I,K,L,O,Q processNode; class D,F,H,K,O eventNode; class J,M,N,P decisionNode; class R errorNode; class Q endNode;
Attribute Handles
Each attribute (service declaration, characteristic declaration, characteristic value, descriptor) in the GATT table is assigned a unique handle. This handle is a 16-bit integer that the client uses to refer to a specific attribute when making read/write requests. The server also uses these handles when sending notifications. You obtain these handles from the ESP_GATTS_ADD_CHAR_EVT
and ESP_GATTS_ADD_CHAR_DESCR_EVT
events.
Practical Examples
Let’s build a simple GATT server. Ensure you have your ESP-IDF v5.x environment set up with VS Code.
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 scanner application on your smartphone or computer (e.g., “nRF Connect for Mobile” by Nordic Semiconductor, “LightBlue Explorer”).
Example 1: Basic GATT Server with a Read-Only Characteristic
This example creates a GATT server with one service: “Device Information Service” (standard UUID 0x180A
) and one read-only characteristic: “Manufacturer Name String” (standard UUID 0x2A29
).
1. Project Setup:
- Create a new ESP-IDF project or use an existing one.
- Run
idf.py menuconfig
:Component config
->Bluetooth
-> Enable[*] Bluetooth
.Component config
->Bluetooth
->Bluetooth Host
-> SelectBluedroid
.Component config
->Bluetooth
->Bluetooth controller
->Controller Mode
->BLE Only
(orBR/EDR/BLE Combined
if you need Classic BT later).- Save and exit.
2. Code (main/gatt_server_main.c
):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.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_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"
#define GATTS_TAG "GATT_SERVER_EXAMPLE"
// Declare the GATTS profile interface
#define GATTS_SERVICE_UUID_TEST 0x00FF // Example custom service UUID
#define GATTS_CHAR_UUID_TEST_A 0xFF01 // Example custom characteristic UUID
#define GATTS_DESCR_UUID_TEST_A 0x3333 // Example custom descriptor UUID (not used in this basic example)
#define GATTS_NUM_HANDLE_TEST_A 4 // Service Decl + Char Decl + Char Value + CCCD (if notifying)
#define TEST_DEVICE_NAME "ESP32_GATT_SERVER"
#define TEST_MANUFACTURER_DATA_LEN 17
// Attribute value
static uint8_t char1_str[] = {0x11, 0x22, 0x33}; // Example characteristic value
static esp_attr_value_t gatts_demo_char1_val = {
.attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX, // Define this if needed, or use fixed size
.attr_len = sizeof(char1_str),
.attr_value = char1_str,
};
// Advertising data
static esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = true,
.min_interval = 0x20, // range from 0x0020 to 0x4000
.max_interval = 0x40, // range from 0x0020 to 0x4000
.appearance = 0x00,
.manufacturer_len = 0, // TEST_MANUFACTURER_DATA_LEN,
.p_manufacturer_data = NULL, // &test_manufacturer[0],
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0, // Set this if advertising service UUIDs
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
// Scan response data (optional)
static esp_ble_adv_data_t scan_rsp_data = {
.set_scan_rsp = true,
.include_name = true,
.include_txpower = true,
.appearance = 0x00,
.manufacturer_len = 0,
.p_manufacturer_data = NULL,
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0,
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
static esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x20,
.adv_int_max = 0x40,
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
#define PROFILE_NUM 1
#define PROFILE_A_APP_ID 0
struct gatts_profile_inst {
esp_gatts_cb_t gatts_cb;
uint16_t gatts_if;
uint16_t app_id;
uint16_t conn_id;
uint16_t service_handle;
esp_gatt_srvc_id_t service_id;
uint16_t char_handle;
esp_bt_uuid_t char_uuid;
esp_gatt_perm_t perm;
esp_gatt_char_prop_t property;
uint16_t descr_handle;
esp_bt_uuid_t descr_uuid;
};
// Forward declaration of GATTS event handler
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
// One GATTS profile instance for this application
static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE, // Will be set after registration
}
};
// Manufacturer Name String Characteristic Value
static const uint8_t manufacturer_name[] = "Espressif Systems";
// Service UUID: Device Information (0x180A)
static const uint16_t GATTS_SERVICE_UUID_DEVICE_INFO = ESP_GATT_UUID_DEVICE_INFO_SVC;
// Characteristic UUID: Manufacturer Name String (0x2A29)
static const uint16_t GATTS_CHAR_UUID_MANUFACTURER_NAME = ESP_GATT_UUID_MANU_NAME;
// Attribute handles
uint16_t dis_handle_table[HRS_IDX_NB]; // HRS_IDX_NB should be defined based on attributes
// GATTS Profile A Event Handler
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT: // Application profile registered
ESP_LOGI(GATTS_TAG, "REG_EVT, status %d, app_id %d", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_A_APP_ID].gatts_if = gatts_if; // Store the GATTS interface ID
// Set device name
esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(TEST_DEVICE_NAME);
if (set_dev_name_ret){
ESP_LOGE(GATTS_TAG, "set device name failed, error code = %x", set_dev_name_ret);
}
// Configure advertising data
esp_err_t adv_config_ret = esp_ble_gap_config_adv_data(&adv_data);
if (adv_config_ret){
ESP_LOGE(GATTS_TAG, "config adv data failed, error code = %x", adv_config_ret);
}
// Configure scan response data (optional)
// esp_err_t scan_rsp_config_ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
// if (scan_rsp_config_ret){
// ESP_LOGE(GATTS_TAG, "config scan response data failed, error code = %x", scan_rsp_config_ret);
// }
// Create the service
esp_gatt_srvc_id_t service_id;
service_id.is_primary = true;
service_id.id.uuid.len = ESP_UUID_LEN_16;
service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_DEVICE_INFO;
// Calculate number of handles: Service Decl + (Char Decl + Char Value) for Manufacturer Name
// For simple read-only char with auto_rsp, 3 handles are typical.
esp_ble_gatts_create_service(gatts_if, &service_id, 4); // Service + Char Decl + Char Value
break;
case ESP_GATTS_CREATE_EVT: // Service created
ESP_LOGI(GATTS_TAG, "CREATE_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].service_handle = param->create.service_handle;
// Start the service
esp_ble_gatts_start_service(param->create.service_handle);
// Add Manufacturer Name Characteristic
esp_bt_uuid_t char_uuid;
char_uuid.len = ESP_UUID_LEN_16;
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_MANUFACTURER_NAME;
esp_attr_value_t char_val;
char_val.attr_max_len = sizeof(manufacturer_name);
char_val.attr_len = sizeof(manufacturer_name);
char_val.attr_value = (uint8_t *)manufacturer_name;
esp_attr_control_t control; // To use auto response
control.auto_rsp = ESP_GATT_AUTO_RSP; // Stack handles read requests automatically
esp_ble_gatts_add_char(param->create.service_handle,
&char_uuid,
ESP_GATT_PERM_READ, // Read-only permission
ESP_GATT_CHAR_PROP_BIT_READ, // Read property
&char_val, // Initial value
&control); // Auto response
break;
case ESP_GATTS_ADD_CHAR_EVT: // Characteristic added
ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].char_handle = param->add_char.attr_handle;
// If you had descriptors, you would add them here, after the characteristic is added.
// For example, if this characteristic supported notifications, you'd add a CCCD.
break;
case ESP_GATTS_ADD_CHAR_DESCR_EVT: // Descriptor added
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",
param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].descr_handle = param->add_char_descr.attr_handle;
break;
case ESP_GATTS_START_EVT: // Service started
ESP_LOGI(GATTS_TAG, "START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
break;
case ESP_GATTS_CONNECT_EVT: // Client connected
ESP_LOGI(GATTS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",
param->connect.conn_id,
param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]);
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
// Stop advertising on connection if desired (or allow multiple connections if configured)
// esp_ble_gap_stop_advertising();
break;
case ESP_GATTS_DISCONNECT_EVT: // Client disconnected
ESP_LOGI(GATTS_TAG, "DISCONNECT_EVT, conn_id %d, reason %d", param->disconnect.conn_id, param->disconnect.reason);
// Restart advertising to allow new connections
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GATTS_READ_EVT: // Read request from client
ESP_LOGI(GATTS_TAG, "READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d, offset %d, is_long %d, need_rsp %d",
param->read.conn_id, param->read.trans_id, param->read.handle,
param->read.offset, param->read.is_long, param->read.need_rsp);
// Note: If auto_rsp was ESP_GATT_RSP_BY_APP, you would construct and send a response here:
// esp_gatt_rsp_t rsp;
// memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
// rsp.attr_value.handle = param->read.handle;
// rsp.attr_value.len = ...;
// memcpy(rsp.attr_value.value, ..., rsp.attr_value.len);
// esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp);
break;
case ESP_GATTS_WRITE_EVT: // Write request from client
ESP_LOGI(GATTS_TAG, "WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d, offset %d, len %d, is_prep %d, need_rsp %d",
param->write.conn_id, param->write.trans_id, param->write.handle,
param->write.offset, param->write.len, param->write.is_prep, param->write.need_rsp);
ESP_LOGI(GATTS_TAG, "Value written: %.*s", param->write.len, (char*)param->write.value);
// If need_rsp is true and auto_rsp is ESP_GATT_RSP_BY_APP, send response
if (param->write.need_rsp && !param->write.is_prep) { // Don't respond to prepare writes yet
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
}
break;
// Other events like ESP_GATTS_EXEC_WRITE_EVT, ESP_GATTS_MTU_EVT, ESP_GATTS_CONF_EVT, etc.
// would be handled here.
default:
break;
}
}
// GAP Event Handler
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
ESP_LOGI(GATTS_TAG, "Advertising data set complete");
esp_ble_gap_start_advertising(&adv_params); // Start advertising after data is set
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
ESP_LOGI(GATTS_TAG, "Scan response data set complete");
// If using scan response, advertising might be started here or after adv_data_set
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Advertising start failed, error status = %x", param->adv_start_cmpl.status);
} else {
ESP_LOGI(GATTS_TAG, "Advertising started successfully");
}
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Advertising stop failed, error status = %x", param->adv_stop_cmpl.status);
} else {
ESP_LOGI(GATTS_TAG, "Advertising stopped successfully");
}
break;
// Other GAP events like ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
default:
break;
}
}
void app_main(void) {
esp_err_t ret;
// Initialize NVS (Non-Volatile Storage)
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);
// Initialize Bluetooth controller
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); // Release classic BT memory if not used
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(GATTS_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); // Enable BLE mode
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
// Initialize Bluedroid stack
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
// Register GATTS callback function
ret = esp_ble_gatts_register_callback(gatts_profile_a_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
return;
}
// Register GAP callback function
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
return;
}
// Register GATTS application profile
ret = esp_ble_gatts_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
// Set MTU size (optional, default is 23)
// esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
// if (local_mtu_ret){
// ESP_LOGE(GATTS_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
// }
ESP_LOGI(GATTS_TAG, "GATT Server Example Initialized");
}
Note: The above code provides a basic structure. The
GATTS_DEMO_CHAR_VAL_LEN_MAX
andHRS_IDX_NB
are placeholders and would need proper definition in a full application. The advertising data is minimal; more complex advertising is covered in Chapter 62. This example usesESP_GATT_AUTO_RSP
for simplicity.
Common ESP_GATTS Events
Event Name (in esp_gatts_cb_event_t) | Description | Key Parameters in esp_ble_gatts_cb_param_t |
---|---|---|
ESP_GATTS_REG_EVT | Triggered after esp_ble_gatts_app_register(). Indicates registration status. | reg.status, reg.app_id, reg.gatts_if (store this) |
ESP_GATTS_CONNECT_EVT | A GATT client has connected to the server. | connect.conn_id (store this), connect.gatts_if, connect.remote_bda |
ESP_GATTS_DISCONNECT_EVT | A GATT client has disconnected. | disconnect.conn_id, disconnect.gatts_if, disconnect.reason |
ESP_GATTS_READ_EVT | A client has requested to read an attribute value (characteristic or descriptor). | read.conn_id, read.trans_id, read.handle, read.offset, read.need_rsp |
ESP_GATTS_WRITE_EVT | A client has written a value to an attribute (characteristic or descriptor, e.g., CCCD). | write.conn_id, write.trans_id, write.handle, write.offset, write.len, write.value, write.need_rsp, write.is_prep |
ESP_GATTS_EXEC_WRITE_EVT | A client has requested to execute a prepared write sequence. | exec_write.conn_id, exec_write.trans_id, exec_write.exec_write_flag |
ESP_GATTS_MTU_EVT | The MTU (Maximum Transmission Unit) size has been negotiated/updated with a client. | mtu.conn_id, mtu.mtu |
ESP_GATTS_CONF_EVT | Confirmation received from the client that an indication sent by the server was successfully received. | conf.conn_id, conf.status, conf.handle |
ESP_GATTS_CREATE_EVT | Triggered after esp_ble_gatts_create_service(). Provides service handle. | create.status, create.service_handle, create.service_id |
ESP_GATTS_ADD_CHAR_EVT | Triggered after esp_ble_gatts_add_char(). Provides characteristic attribute handle. | add_char.status, add_char.attr_handle, add_char.service_handle, add_char.char_uuid |
ESP_GATTS_ADD_CHAR_DESCR_EVT | Triggered after esp_ble_gatts_add_char_descr(). Provides descriptor attribute handle. | add_char_descr.status, add_char_descr.attr_handle, add_char_descr.service_handle, add_char_descr.descr_uuid |
ESP_GATTS_START_EVT | Triggered after esp_ble_gatts_start_service(). Indicates if service started successfully. | start.status, start.service_handle |
3. Build, Flash, and Monitor:
- Connect your ESP32 board.
- Run
idf.py build
- Run
idf.py flash monitor
4. Observe Output & Test with BLE Scanner:
- The serial monitor will show logs from
GATTS_TAG
. You should see events for registration, service creation, characteristic addition, and advertising start. - Open your BLE scanner app (e.g., nRF Connect for Mobile).
- Scan for devices. You should see a device named “ESP32_GATT_SERVER”.
- Connect to it.
- The scanner app should display the services and characteristics. You should find the “Device Information” service (UUID
0x180A
). - Within this service, you should find the “Manufacturer Name String” characteristic (UUID
0x2A29
). - Read this characteristic. The app should display “Espressif Systems”.
- The ESP32 serial monitor will show
ESP_GATTS_CONNECT_EVT
when you connect andESP_GATTS_READ_EVT
when you read (though withESP_GATT_AUTO_RSP
, the application code doesn’t explicitly handle the read response for this characteristic).
Example 2: Writable Characteristic and Notifications
Let’s enhance the server. We’ll add a custom service with:
- A writable characteristic (e.g., to control an LED – simulated here).
- A characteristic that sends notifications (e.g., a counter that increments).
(This example would involve significantly more code, including defining new UUIDs, handling ESP_GATTS_WRITE_EVT
to update a local variable for the writable characteristic, adding a CCCD for the notifying characteristic, handling writes to the CCCD, and a FreeRTOS task or timer to periodically update and notify the counter characteristic. Due to length constraints for this response, a full, runnable code for this second example is extensive. The principles are extensions of Example 1:
- Define new 128-bit UUIDs for the custom service and characteristics.
- Writable Characteristic:
- Properties:
ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE
. - Permissions:
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE
. - In
ESP_GATTS_WRITE_EVT
: Check thehandle
to identify the characteristic. Update a global variable withparam->write.value
. Ifparam->write.need_rsp
is true, callesp_ble_gatts_send_response()
.
- Properties:
- Notifying Characteristic:
- Properties:
ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_NOTIFY
. - Permissions:
ESP_GATT_PERM_READ
. - Add CCCD: After adding this characteristic, call
esp_ble_gatts_add_char_descr()
withESP_GATT_UUID_CHAR_CLIENT_CONFIG
and permissionsESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE
. - In
ESP_GATTS_WRITE_EVT
: Ifparam->write.handle
matches the CCCD handle, checkparam->write.value
. If0x0001
(notifications enabled), store this state (e.g., per connection ID). If0x0000
(disabled), update state. - Sending Notifications: When the characteristic’s value changes (e.g., via a timer), if notifications are enabled for a connected client, call esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle, value_len, value, false). false means notification, not indication.)
- Properties:
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%% sequenceDiagram participant Client participant Server_ESP32 Note over Client, Server_ESP32: Scenario: Client writes to a control characteristic & subscribes to a sensor characteristic. Client->>Server_ESP32: 1. Write Request (to Control_Char_Handle, Value: 0x01 - e.g. "Turn ON LED") activate Server_ESP32 Server_ESP32-->>Client: 2. Write Response (Status: OK) Note right of Server_ESP32: ESP32 processes write,<br>updates internal LED state. deactivate Server_ESP32 Client->>Server_ESP32: 3. Write Request (to Sensor_Char_CCCD_Handle, Value: 0x0001 - Enable Notifications) activate Server_ESP32 Server_ESP32-->>Client: 4. Write Response (Status: OK) Note right of Server_ESP32: ESP32 records that client<br>is subscribed for notifications<br>on Sensor_Char. deactivate Server_ESP32 loop Periodic Sensor Update or Event Note over Server_ESP32: Internal event: Sensor value changes (e.g., counter increments) Server_ESP32->>Client: 5. Send Notification (Sensor_Char_Handle, New Value: "Count: 1") end Note over Client, Server_ESP32: Later... Client disables notifications Client->>Server_ESP32: 6. Write Request (to Sensor_Char_CCCD_Handle, Value: 0x0000 - Disable Notifications) activate Server_ESP32 Server_ESP32-->>Client: 7. Write Response (Status: OK) Note right of Server_ESP32: ESP32 records that client<br>is no longer subscribed. deactivate Server_ESP32 %% Apply styles based on prompt %% Client is a generic "Process Node" %% Server_ESP32 is a "Primary/Start Node" as it's the focus device %% Interactions are "Process" steps
Tip: Refer to the
gatt_server_service_table
example in the ESP-IDFexamples/bluetooth/bluedroid/ble
directory for a more comprehensive illustration of creating multiple services and characteristics, including notifications and custom UUIDs.
Variant Notes
ESP32 Variant | Bluetooth LE (BLE) Support | GATT Server Capability | Typical BLE Version | Key Notes |
---|---|---|---|---|
ESP32 (Original, ECO3+) | Yes | Yes | BLE 4.2 / BLE 5.0 (ECO3+) | Full GATT server support. ECO3 and later revisions offer BLE 5.0 features. |
ESP32-S2 | No | No | N/A | Does not have Bluetooth hardware. Wi-Fi only. |
ESP32-S3 | Yes | Yes | BLE 5.0 | Full GATT server support with BLE 5.0 features (e.g., LE 2M PHY, Coded PHY, Advertising Extensions). Also has Wi-Fi. |
ESP32-C3 | Yes | Yes | BLE 5.0 | RISC-V core. Full GATT server support with BLE 5.0 features. Also has Wi-Fi. |
ESP32-C6 | Yes | Yes | BLE 5.0 | RISC-V core. Supports BLE, Wi-Fi 6, and 802.15.4 (Thread/Zigbee). Full GATT server support. |
ESP32-H2 | Yes | Yes | BLE 5.2 | RISC-V core. Focus on 802.15.4 (Thread/Zigbee) and BLE. Full GATT server support. May not have Wi-Fi depending on module. |
The fundamental process of defining services, characteristics, handling events, and sending responses/notifications remains the same.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect Handle Calculation for esp_ble_gatts_create_service | esp_ble_gatts_add_char or esp_ble_gatts_add_char_descr fails with ESP_GATT_INSUF_RESOURCE or similar errors. Service might not appear fully. | Carefully count all attributes: Service Declaration (1) + Characteristic Declaration (1 per char) + Characteristic Value (1 per char) + Descriptors (1 per descr). Sum these for num_handle. E.g., Service + 1 Char (Value only) + 1 CCCD = 1+1+1+1 = 4 handles. |
Forgetting to Send Response for need_rsp Writes/Reads | Client times out, operations fail, or unexpected disconnections. Client app might hang or report errors. | Always check param->write.need_rsp or param->read.need_rsp. If true (and not using ESP_GATT_AUTO_RSP for reads), call esp_ble_gatts_send_response() with ESP_GATT_OK and appropriate data (for reads) or NULL (for writes). |
Mishandling CCCD Writes | Notifications/indications are not sent, or sent when they shouldn’t be. Client cannot enable/disable updates. | In ESP_GATTS_WRITE_EVT, if param->write.handle matches your CCCD handle:
1. Check param->write.len (should be 2). 2. Extract 16-bit value: uint16_t cccd_value = param->write.value[0] | (param->write.value[1] << 8); 3. Store state (0x0001 for notify, 0x0002 for indicate, 0x0000 for disable) per conn_id. 4. Send response if need_rsp is true. |
Characteristic Value Buffer Management | Corrupted data read by client, crashes due to buffer overflows (attr_value too small), incorrect string termination. | Ensure esp_attr_value_t.attr_max_len is sufficient. For strings, ensure null-termination if client expects it. Use correct length when updating values for notifications (esp_ble_gatts_send_indicate). |
Not Starting Advertising or Service | ESP32 not discoverable by BLE scanners. If discoverable, services/characteristics are not visible or accessible. | Ensure esp_ble_gatts_start_service() is called for each service (typically after all its attributes are added). Ensure esp_ble_gap_start_advertising() is called after GATTS and GAP setup. Check return codes of these functions. |
Incorrect UUID Usage | Service/characteristic not found by client, or conflicts if using standard UUIDs for custom purposes. | Use 16-bit UUIDs (e.g., 0x180A) for SIG-defined attributes. Use unique 128-bit UUIDs for all custom services and characteristics. Generate these properly. |
Permissions vs. Properties Mismatch | Client gets “Read Not Permitted”, “Write Not Permitted” errors despite characteristic having the property. | Ensure attribute permissions (perm in esp_ble_gatts_add_char) align with characteristic properties. E.g., if property is READ, permission must include ESP_GATT_PERM_READ. For secure connections, also consider encryption/authentication permissions. |
Exercises
- Add Firmware Version Characteristic:
- Modify Example 1. Add another characteristic to the “Device Information Service” for “Firmware Revision String” (UUID
0x2A26
). Make it read-only and display a static version string (e.g., “1.0.0”). - Verify with a BLE scanner.
- Modify Example 1. Add another characteristic to the “Device Information Service” for “Firmware Revision String” (UUID
- Read/Write LED State:
- Create a new custom service (use a 128-bit UUID). Add a characteristic “LED State” (custom 128-bit UUID).
- Make this characteristic readable and writable.
- When read, it should return the current (simulated) LED state (0 for OFF, 1 for ON).
- When written, it should update the (simulated) LED state. Log the new state to the console.
- Implement
ESP_GATTS_READ_EVT
andESP_GATTS_WRITE_EVT
handling (responding by application, i.e., notESP_GATT_AUTO_RSP
).
- Temperature Sensor Notifications:
- Create a “Temperature Service” (custom 128-bit UUID) with a “Temperature Measurement” characteristic (custom 128-bit UUID).
- Make this characteristic readable and support notifications. Add a CCCD.
- Simulate temperature readings: In a FreeRTOS task, generate a random temperature value (e.g., between 20.0 and 30.0 °C) every 5 seconds.
- If a client has enabled notifications for this characteristic, send the new temperature value.
- Format the temperature as a float, then convert to a byte array for sending (e.g.,
uint8_t temp_data[4]; memcpy(temp_data, &float_temp, sizeof(float_temp));
).
- Add User Description Descriptor:
- To one of the characteristics created in the previous exercises (e.g., “LED State” or “Temperature Measurement”), add a “Characteristic User Description” descriptor (UUID
0x2901
). - Set its value to a human-readable string like “Controls the onboard LED” or “Current room temperature”. Make it readable.
- Verify with your BLE scanner that the descriptor is present and readable.
- To one of the characteristics created in the previous exercises (e.g., “LED State” or “Temperature Measurement”), add a “Characteristic User Description” descriptor (UUID
- Write With Response:
- Modify the “LED State” characteristic from Exercise 2. Change its write property to require “Write With Response” (
ESP_GATT_WRITE_TYPE_RSP
). - Ensure your
ESP_GATTS_WRITE_EVT
handler correctly sends a response usingesp_ble_gatts_send_response()
when this characteristic is written to. - Verify with your BLE scanner that write operations are acknowledged. Some scanners might indicate if a write was “with response.”
- Modify the “LED State” characteristic from Exercise 2. Change its write property to require “Write With Response” (
Summary
- A GATT Server on the ESP32 defines and hosts data (attributes) structured into Services, Characteristics, and Descriptors.
- The ESP-IDF provides
esp_gatts_api.h
for creating services (esp_ble_gatts_create_service
), adding characteristics (esp_ble_gatts_add_char
), and descriptors (esp_ble_gatts_add_char_descr
). - All GATT server interactions are event-driven, handled through a registered callback function (
esp_gatts_cb_t
). - Key events include client connect/disconnect (
ESP_GATTS_CONNECT_EVT
,ESP_GATTS_DISCONNECT_EVT
), read requests (ESP_GATTS_READ_EVT
), and write requests (ESP_GATTS_WRITE_EVT
). - For characteristics that support notifications or indications, a Client Characteristic Configuration Descriptor (CCCD, UUID
0x2902
) must be added. - The server sends notifications/indications using
esp_ble_gatts_send_indicate()
. - Proper handling of attribute permissions, characteristic properties, and client response requirements (
need_rsp
) is crucial. - A GATT server must also perform GAP advertising to be discoverable.
Further Reading
- ESP-IDF GATT Server Example:
examples/bluetooth/bluedroid/ble/gatt_server_service_table
in your ESP-IDF installation. This is an excellent, more complete example. - ESP-IDF API Reference: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/esp_gatt_defs.html
- Bluetooth SIG – Services Specification: https://www.bluetooth.com/specifications/specs/
- Bluetooth SIG – Core Specification Supplement: https://www.bluetooth.com/specifications/specs/core-specification-supplement-9/
