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:

  1. Defining the Attribute Database: This involves specifying a collection of services, characteristics, and descriptors that represent the data and functionality the server offers.
  2. Storing Attribute Values: Maintaining the current values for all defined characteristics.
  3. Handling Client Requests: Responding to read and write requests for characteristic values and descriptor values from connected GATT Clients.
  4. Sending Notifications/Indications: Pushing updates of characteristic values to clients that have subscribed to them.
  5. 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 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.
    • Value: The descriptor’s own value (e.g., the CCCD value indicates if notifications are enabled).
    • Permissions: Define security for accessing the descriptor.
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:

  1. 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). The app_id is chosen by you (e.g., 0 to 0xFFFF). This call triggers an ESP_GATTS_REG_EVT in your GATTS callback.
  2. 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.
  3. 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 in ESP_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.
  4. 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 from ESP_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 if control->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.
  5. 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 typically ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE.
    • This triggers an ESP_GATTS_ADD_CHAR_DESCR_EVT.
  6. 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.
  7. Handling GATT Events in Callback:The GATTS callback function you register will receive events like:
    • ESP_GATTS_CONNECT_EVT: A client has connected. Store conn_id and gatts_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 response esp_gatt_rsp_t and send it using esp_ble_gatts_send_response().
    • 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 using esp_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.
  8. 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 via ESP_GATTS_CONF_EVT).
  9. 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() and esp_ble_gap_start_advertising(), covered in more detail in Chapter 62. For now, we’ll use basic advertising.
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 -> Select Bluedroid.
    • Component config -> Bluetooth -> Bluetooth controller -> Controller Mode -> BLE Only (or BR/EDR/BLE Combined if you need Classic BT later).
    • Save and exit.

2. Code (main/gatt_server_main.c):

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 and HRS_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 uses ESP_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 and ESP_GATTS_READ_EVT when you read (though with ESP_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:

  1. A writable characteristic (e.g., to control an LED – simulated here).
  2. 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 the handle to identify the characteristic. Update a global variable with param->write.value. If param->write.need_rsp is true, call esp_ble_gatts_send_response().
  • 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() with ESP_GATT_UUID_CHAR_CLIENT_CONFIG and permissions ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE.
    • In ESP_GATTS_WRITE_EVT: If param->write.handle matches the CCCD handle, check param->write.value. If 0x0001 (notifications enabled), store this state (e.g., per connection ID). If 0x0000 (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.)
%%{ 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-IDF examples/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

  1. 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.
  2. 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 and ESP_GATTS_WRITE_EVT handling (responding by application, i.e., not ESP_GATT_AUTO_RSP).
  3. 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));).
  4. 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.
  5. 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 using esp_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.”

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top