How to Create Profiles, Services, and Characteristics in ESP-IDF’s GATT Server Example Using Tables

Tabrez Ahmed
19 min readOct 5, 2024

--

BLE Connect Application Showing output of 3rd program

Hello, In this article i will guide you through the implementation of a GATT server using ESP-IDF, specifically focusing on the gatts_demo.c file located in the directory:

~/esp/esp-idf/examples/bluetooth/bluedroid/ble/gatt_server/main/

I will not be going into the details of how things work but what code changes are needed to be done to create profiles, services and characteristics using tables.

Here is the github repo for this project.

If you want to jump straight into the editing of codebase here is the anchor

1 .Adding Service

2. Adding Characteristic

3. Adding a Profile

Lets quickly get familiarized with the structures and functions being used in this code base.

This is a BLE (Bluetooth Low Energy) GATT (Generic Attribute Profile) server code for an ESP32 device. The code defines a GATT server with various services and characteristics. The BLE GATT server is responsible for managing connections, receiving requests from a client (like a mobile app), and sending notifications or responses back.

This is the header part of the code, it creates 1 service with 2 characteristics with write and notify permissions respectively.

Definitions and Macros

  • GATTS_TAG: This is just a tag used for logging. It's like a label that helps identify logs related to this specific code module when debugging.
  • CHAR_DECLARATION_SIZE: This defines the size of the characteristic declaration, which is one byte. Characteristics are used to send and receive data over BLE.
  • TEST_DEVICE_NAME: This defines the name of the device that will be shown when other devices (like smartphones) scan for nearby BLE devices. In this case, the name is "FACTORY_TEST."
  • PROFILE_NUM and PROFILE_A_APP_ID: These macros define the number of profiles (sets of services and characteristics) and the specific profile ID we're working with. BLE devices can have multiple profiles, but this code has one.

GATT Profile Structure

  • gatts_profile_inst structure: This structure holds important information about the BLE profile, such as which functions to call (callbacks), the connection ID, and handles for the service and characteristics. Each profile gets its own instance of this structure.
  • gatts_cb: This is the event handler for the GATT server, which is triggered when specific BLE events (like connection or data write) happen.
  • service_handle, char_handle: These are like addresses or identifiers for the service and characteristics. They help the code know where to send or receive data.

Characteristic Properties and Permissions

  • char1_properties and char2_properties: These define how a characteristic can be used. For example, char1_properties is for writing data (e.g., the client sends data to the server), and char2_properties is for sending notifications from the server to the client.
  • Characteristic: This is a piece of data (like temperature, LED status, etc.) that a BLE server can send or receive.
  • Permissions: Each characteristic has permissions such as read or write. The client can only perform actions that the permissions allow.

GATT Database (gatt_db_a)

  • The GATT database is where all services, characteristics, and descriptors are defined. It’s like a blueprint that tells the BLE server what features it supports.
  • Service Declaration: This declares a new service, which is a group of characteristics. Services usually represent some functionality, like an LED controller or temperature sensor.
  • Characteristic Declarations and Values: These declare the characteristics within the service. The characteristic has a UUID (unique identifier) and defines whether it’s read, written to, or used for notifications.
  • Descriptor: A descriptor provides additional information about a characteristic, like if notifications are enabled.

Advertisement Data

  • adv_data and scan_rsp_data: These define what information is sent when the device advertises itself (when other devices scan for it). For example, it includes the device name, whether the device can be discovered, and if it’s connectable.

Advertising Parameters

  • adv_params: This defines how often the device advertises itself (how frequently it sends out signals that it is available). Advertising is what lets a BLE client (like a phone) discover the server.

Event Handlers and Callbacks

  • Event Handler: In the BLE world, things happen asynchronously, meaning that actions like connecting to a device or receiving data don’t happen immediately after you write the code. Event handlers or callbacks are functions that get triggered when something happens, like when a BLE client writes data to the server or when a new connection is made.
  • gatts_profile_a_event_handler: This function is called whenever an event occurs for the GATT profile. Events include things like "A client connected" or "A client wrote some data."
  • example_write_event_env: This function handles when a client writes data to the server. It stores the data received in a buffer for further use.

Functions vs Callbacks

  • Function: A block of code that runs when you call it. For example, you write code to call example_write_event_env when a specific event happens.
  • Callback: A function that the system calls automatically when something happens. You don’t directly call it; instead, the BLE library or framework will call it when the event it’s tied to (like writing data) occurs.

For instance:

  • The gatts_profile_a_event_handler is a callback. The BLE library calls this when it needs to handle a BLE event for this profile.
  • The example_write_event_env is a function that you would call to manage a specific event, like processing data from a client.

Prepare Write Events

  • a_prepare_write_env and b_prepare_write_env: These are used to handle large data writes. BLE writes usually send small amounts of data, but if you want to send more data (like a long string), you need to split it into multiple parts. These structures help handle that process.

GAP Event Handler: gap_event_handler

This function handles Bluetooth GAP (Generic Access Profile) events. It processes different events triggered by the BLE GAP layer, such as advertising data setup, starting and stopping advertising, and updating connection parameters.

How it Works:

  • Advertising Data Setup: When the device prepares its advertising data, the events ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT and ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT get triggered. The function checks if both the advertisement data and scan response data are set before starting advertising. In this case, adv_config_done is used as a flag to ensure all configurations are done before starting the advertising process.

Advertising Start and Stop Events:

  • ESP_GAP_BLE_ADV_START_COMPLETE_EVT: This event checks if advertising started successfully. If it fails, an error message is logged.
  • ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: This event checks if advertising stopped successfully and logs the result.

Connection Parameter Update Event:

  • ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: When connection parameters are updated (such as connection interval or latency), this event logs the new parameters and their status.

Writing Data to GATT: example_write_event_env

This function handles GATT write events, specifically for “prepare write” operations. In BLE, prepare writes allow writing large amounts of data in chunks. The example_write_event_env function handles the request, assembles the data, and sends a response to the client.

Key Points:

  • Handling Prepare Writes: If the client requests a response (need_rsp) and the write is a "prepare write" (is_prep), the server allocates memory for storing the incoming data. It then checks if the offset and data length are valid. Once the data is received, a response is sent back to the client.
if (param->write.is_prep){
if (prepare_write_env->prepare_buf == NULL) {
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
}
// Store incoming data in prepare_buf
memcpy(prepare_write_env->prepare_buf + param->write.offset, param->write.value, param->write.len);
prepare_write_env->prepare_len += param->write.len;
}

Sending a Response: A response is sent back using esp_ble_gatts_send_response. This informs the client whether the write was successful.

Execute Write: example_exec_write_event_env

This function handles the execution of the previously prepared write operations. Once all the chunks of data are received and the client requests the final execution (ESP_GATT_PREP_WRITE_EXEC), the server processes the data.

Key Points:

  • Finalizing the Write: If the write execution is confirmed, the data stored in prepare_buf is logged or processed. If the write is canceled (ESP_GATT_PREP_WRITE_CANCEL), the data is discarded.
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
}else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}

Cleaning Up: After the execution (or cancellation), the memory allocated for storing the prepared write data (prepare_buf) is freed to avoid memory leaks.

These functions together handle key events in the BLE communication process:

  • The GAP event handler manages advertising and connection parameters.
  • The write event function manages incoming write requests, especially for larger data through prepare writes.
  • The execute write function finalizes or cancels the prepared write operations.

The Front Desk Manager:

  1. Client enters (ESP_GATTS_REG_EVT):
    When a client (like a new device) enters, the manager sets up basic details like the building name (device name) and advertisement strategy (how the building presents itself to others). It may set up multiple banners (advertisement data and scan response data) to ensure visibility.
  • If the name or advertisement setup fails, the manager logs an error.
  • After setting up the name and advertisements, it proceeds to set up services in the building (create attribute tables).

2.Guest checks out an item (ESP_GATTS_READ_EVT):
This happens when a guest (client) asks for information about an item in the building (reading a characteristic). The manager provides a small package of data to the guest and logs that the information has been sent.

3. Guest submits a form (ESP_GATTS_WRITE_EVT):
When a guest fills out a form (write request), the manager checks which department (characteristic) the form is meant for and processes it accordingly. It may handle simple forms directly or set up a process for handling longer ones (prepare write).

  • Depending on the content of the form (data written), the system performs different actions (e.g., triggering LEDs based on the value).

4. Form final submission (ESP_GATTS_EXEC_WRITE_EVT):
After a guest has filled out all sections of a form, they submit it for final review. The manager reviews the full form (execute write) and either processes or discards it, logging the decision.

5. Manager updates guest rules (ESP_GATTS_MTU_EVT):
The building’s rules (MTU size) for data transfer are updated, allowing more or less data to be sent per message. The manager logs the new rule.

6. New guest enters (ESP_GATTS_CONNECT_EVT):
When a new guest connects (device connection), the manager updates the guest’s preferences (connection parameters) and ensures everything is working smoothly for them.

7. Guest leaves (ESP_GATTS_DISCONNECT_EVT):
When a guest leaves the building (disconnects), the manager resumes advertising to attract new guests, ensuring that the building remains visible to others.

Lets do a deeper dive

1. ESP_GATTS_REG_EVT — Register Event

When this event occurs, it signals the successful registration of a GATT server application. Here’s what happens:

  • Set Device Name:
    The handler attempts to set the BLE device name using the function esp_ble_gap_set_device_name(TEST_DEVICE_NAME). If this fails, it logs an error with ESP_LOGE.
  • Configure Advertisement Data:
    Depending on the configuration (CONFIG_SET_RAW_ADV_DATA), the handler configures either raw advertising data or standard advertising and scan response data using esp_ble_gap_config_adv_data().
    The idea here is to let the BLE device announce its presence, and potentially its services, to nearby devices looking to connect.
  • Create Attribute Table:
    The final part of this event creates the attribute table for the services defined by gatt_db_a. In BLE, an attribute table contains all the GATT service characteristics and descriptors.
esp_ble_gatts_create_attr_tab(gatt_db_a, gatts_if, 8, 0);

2. ESP_GATTS_READ_EVT — Read Event

This event is triggered when a client reads a characteristic from the BLE server. Here’s the breakdown:

  • The handler identifies the characteristic being read using param->read.handle.
  • A response structure esp_gatt_rsp_t is initialized, where you set:
  • Handle: The characteristic handle the client is reading.
  • Length: The length of data to send back (in this case, 4 bytes).
  • Value: The actual data to return, in this example, 4 bytes: {0xde, 0xed, 0xbe, 0xef}.
  • The function then uses esp_ble_gatts_send_response() to send this data back to the client. If sending fails, an error is logged.

This event allows the BLE server to respond to read requests initiated by clients (e.g., mobile apps, remotes).

3. ESP_GATTS_WRITE_EVT — Write Event

The write event is one of the most important in BLE communications. It is triggered when a client writes data to a characteristic. Here’s the process:

  • The handler logs the write operation with information such as conn_id, trans_id, and handle.
  • Handle Identification:
    The handle in the write request tells the handler which characteristic the client is writing to. In this case, we have:
  • char1_write_handle: The first characteristic (e.g., used for general commands).
  • char1_notify_handle: Another characteristic (e.g., notifications).
  • char3_write_handle: A third characteristic that might be used for special commands.
  • Based on the handle, the server processes the data accordingly. For example, in your code, if char3_write_handle is written with a value of 1, 2, or 3, the handler logs it and performs some logic based on the value received.
  • Prepare Writes:
    If the data is larger or involves multi-part transactions, it might be a prepared write. This allows the BLE server to accumulate data across multiple writes before processing it.

Others are self explanatory.

  • Client initiates a GATT write request
  • GATT Write Event Triggered: ESP_GATTS_WRITE_EVT
    Function: example_write_event_env()
  • Checks if a response is required (need_rsp)
  • If a Prepare Write (is_prep):
  • Allocate memory for buffer if needed
  • Copy received data to buffer
  • Create a response and send it to the client
  • Else, directly sends a response to the client
  • Execute Write Event: ESP_GATTS_EXEC_WRITE_EVT
    Function: example_exec_write_event_env()
  • Check if the client wants to execute the prepared write (flag = ESP_GATT_PREP_WRITE_EXEC)
  • If yes, logs buffer data
  • If no, cancels the prepared write
  • Free the allocated memory for the buffer, if any

What happens during a write event?:

  1. ESP_GATTS_WRITE_EVT: This is the entry point for the write event in the GATT server.
  2. Log the write operation: The connection ID, transaction ID, and handle are logged for debugging and tracking purposes.
  3. Check which handle was written to: The function checks the specific handle that the client wrote to.
  4. Handle == char1_write_handle: If the handle corresponds to char1_write_handle, the server processes data and executes the necessary actions associated with this characteristic.
  5. Handle == char1_notify_handle: If the handle is for notifications, the server sends a notification to the client if applicable.
  6. Handle == char3_write_handle: If the handle is for char3_write_handle, the server processes the data based on the value received, which may involve logging or executing specific logic.
  7. Check for prepared writes: If the server is set up for prepared writes, it checks if this is part of a multi-part write transaction.
  8. Send response to client with status: Finally, the server sends a response back to the client indicating the status of the write operation (e.g., ESP_GATT_OK)

What happens during a read event?

Entry Point: Read Event Triggered

  • The handler receives the ESP_GATTS_READ_EVT event, indicating that a client has requested to read a characteristic.

Log the Read Operation

  • The function logs the details of the read operation, including:
  • conn_id: The connection ID of the client that initiated the read.
  • trans_id: The transaction ID, which helps track this specific request.
  • handle: The handle of the characteristic that is being read.
  • This logging is crucial for debugging and understanding the flow of operations in the GATT server.

Prepare the Response Structure

  • An esp_gatt_rsp_t structure (rsp) is initialized to prepare the response that will be sent back to the client.
  • The structure is cleared to ensure no old data remains.

Set Response Attributes

  • The rsp structure is populated with the following details:
  • rsp.attr_value.handle: The handle of the characteristic being read, which is obtained from the param structure.
  • rsp.attr_value.len: The length of the data to be sent back to the client. In our example, it’s set to 4, meaning four bytes of data will be sent.
  • rsp.attr_value.value: The actual data to be sent back to the client. In this case, the values [0xde, 0xed, 0xbe, 0xef] are hard-coded to be sent as the response.

Send Response to Client

  • The esp_ble_gatts_send_response function is called to send the response back to the client. The parameters passed include:
  • gatts_if: The GATT interface for the profile.
  • param->read.conn_id: The connection ID for the response.
  • param->read.trans_id: The transaction ID to identify the specific read request.
  • ESP_GATT_OK: The status code indicating the read operation was successful.
  • &rsp: A pointer to the response structure prepared earlier.

Check Response Status

  • After sending the response, the function checks if the status returned by esp_ble_gatts_send_response is not equal to ESP_OK. If there's an error, it logs the error code for debugging.
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)

This code primarily consists of two functions: gatts_event_handler and app_main. The gatts_event_handler function handles various GATT server events, while the app_main function initializes the BLE stack and registers the necessary callbacks and application IDs.

Explanation

Function Declaration:

  • This function is declared as static, meaning it is only accessible within this translation unit (source file).
  • It takes three parameters:
  • event: The GATT event that has occurred.
  • gatts_if: The GATT interface identifier for the profile.
  • param: A pointer to the parameters associated with the event.

Register Event Handling:

  • The first if statement checks if the event is a registration event (ESP_GATTS_REG_EVT).
  • If it is, it checks the status of the registration:
  • If successful (ESP_GATT_OK), it stores the gatts_if for the corresponding profile in gl_profile_tab and logs this information.
  • If registration fails, it logs an error message and exits the function.

Profile Callback Invocation:

  • The do { ... } while (0); construct is used for a structured flow of execution.
  • It iterates over all profiles (from 0 to PROFILE_NUM).
  • If gatts_if is ESP_GATT_IF_NONE, it indicates that all profiles need to be processed.
  • If gatts_if matches the stored GATT interface of a profile, the respective callback function (gl_profile_tab[idx].gatts_cb) is called with the event, interface, and parameters.

App main:

NVS Initialization:

  • The function starts by initializing the NVS (Non-Volatile Storage) using nvs_flash_init(). This is essential for storing persistent data across reboots.
  • If there are no free pages or a new version of NVS is found, it erases the old data and initializes it again.

Bluetooth Controller Configuration:

  • It releases memory allocated for Classic Bluetooth using esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT) because only BLE (Bluetooth Low Energy) is needed in this application.
  • Initializes the Bluetooth controller with default settings and checks for errors.

Enable BLE Mode:

  • The BLE controller is enabled using esp_bt_controller_enable(ESP_BT_MODE_BLE).

Bluedroid Initialization:

  • Initializes the Bluedroid stack and enables it. Bluedroid is the Bluetooth stack used by ESP-IDF.

Callback Registration:

  • Registers the GATT event handler (gatts_event_handler) to handle GATT-related events.
  • Registers a GAP (Generic Access Profile) event handler (gap_event_handler) to handle connection and disconnection events.

Application Registration:

  • Registers an application for a specific profile ID (PROFILE_A_APP_ID).

Set Local MTU:

  • Sets the local MTU (Maximum Transmission Unit) to 500 bytes. The MTU defines the maximum size of data that can be transmitted in a single packet

The initialization process leads to the overall operation by first registering the event handler in the app_main function with a call to esp_ble_gatts_register_callback(gatts_event_handler), which designates gatts_event_handler as the callback for GATT events. When a GATT event occurs (such as registration, read, or write), the ESP-IDF triggers the gatts_event_handler.

If the event is a registration event (ESP_GATTS_REG_EVT), the corresponding profile's GATT interface is updated in gl_profile_tab. For other events like read or write, the handler checks if the GATT interface matches any profile in gl_profile_tab, and if it does, it invokes the respective callback function. Inside the gatts_event_handler, the function iterates through gl_profile_tab to find the matching GATT interface, and when it finds one, it calls the corresponding callback, allowing each profile to handle its specific GATT events as defined in the respective callback functions.

Adding a Service to a profile

The github repo has only 1 profile you can follow these steps to create a new service

1 . Define UUIDs , service handle and characteristic handles for new service

Also create a new function for handling write services for second service

Make sure you update number of profiles macro.

static const uint16_t GATTS_SERVICE_UUID_TEST_B = 0x00EE;
static const uint16_t GATTS_CHAR1_UUID_TEST_B = 0xEE01;
static const uint16_t GATTS_CHAR2_UUID_TEST_B = 0xEE02;
static const uint16_t GATTS_DESCR_UUID_TEST_B = 0xEE21;

static uint16_t service2_handle;
static uint16_t char2_write_handle;
static uint16_t char2_notify_handle;
static uint16_t char2_notify_desc_handle_B;

static prepare_type_env_t b_prepare_write_env;
#define PROFILE_B_APP_ID 1
#define PROFILE_NUM 2

2. Set Up Attribute Database

3. Define Service Declaration in the database

4. Add Characteristic Declarations

5. Define Characteristic Values

6. Add Descriptors (if needed)

static const esp_gatts_attr_db_t gatt_db_b[] = {
// Service B Declaration
[0] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST_B), (uint8_t *)&GATTS_SERVICE_UUID_TEST_B}
},

// Characteristic 1 Declaration for Service B (Write)
[1] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char1_properties}
},

// Characteristic 1 Value for Service B
[2] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR1_UUID_TEST_B, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(gatts_demo_char1_val), (uint8_t *)&gatts_demo_char1_val}
},

// Characteristic 2 Declaration for Service B (Notify)
[3] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char2_properties}
},

// Characteristic 2 Value for Service B
[4] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR2_UUID_TEST_B, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(gatts_demo_char1_val), (uint8_t *)&gatts_demo_char1_val}
},

// Descriptor for Characteristic 2 (Notify) in Service B
[5] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&descriptor_declaration_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(GATTS_DESCR_UUID_TEST_B), (uint8_t *)&GATTS_DESCR_UUID_TEST_B}
},
};

7. Add the table to in ESP_GATT_REG_EVT

//Add to REG_EVT of event handler a:
esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db_b, gatts_if, 7, 0);
if (create_attr_ret) {
ESP_LOGE(GATTS_TAG, "create attr table failed, error code = %x", create_attr_ret);
}//to create new service first create a table

We create new tables for new services.

Add a characteristic:

  1. variables and UUID unique to new characteristic
static uint16_t char31_write_handle;
static const uint16_t GATTS_CHAR3_UUID_TEST_B = 0xEE03;

2. Add in Table

// Characteristic 3 Declaration for Service 2(WRITE, READ AND NOTIFY PERMISSIONS)

[5] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char3_properties}
},

// Characteristic 3 Value for Service 2
[6] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR3_UUID_TEST_A, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(gatts_demo_char1_val), (uint8_t *)&gatts_demo_char1_val}
},

3. Handle the charcateristic permissions like read, notify, write

ADD in ESP_GATTS_WRITE_EVT:

        if (param->write.handle == char32_write_handle) {
ESP_LOGE(GATTS_TAG, "service 2 Characteristic 1 write event, value: %.*s",
param->write.len, param->write.value);

// Make sure value length is 1 for single-byte comparison
if (param->write.len == 1) {
uint8_t received_value = param->write.value[0]; // Dereference the pointer to get the value

if (received_value == 1) {
printf("data is : %d\n", (int)received_value);
} else if (received_value == 2) {
printf("data is : %d\n", (int)received_value);
} else if (received_value == 3) {
printf("data is : %d\n", (int)received_value);
}
} else {
ESP_LOGE(GATTS_TAG, "Unexpected write value length: %d", param->write.len);
}
}

// Handle write to Characteristic 1.1

4.Updateattribute table in ESP_GATT_EG_EVT

esp_err_t create_attr_ret =  esp_ble_gatts_create_attr_tab(gatt_db_a, gatts_if, 9, 0);
if (create_attr_ret) {
ESP_LOGE(GATTS_TAG, "create attr table failed, error code = %x", create_attr_ret);
}//to create new service first create a table

create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db_b, gatts_if, 8, 0);
if (create_attr_ret) {
ESP_LOGE(GATTS_TAG, "create attr table failed, error code = %x", create_attr_ret);
}//to create new service first create a table

5. Update ESP_GATTS_CONNECT_EVT

                if (param->add_attr_tab.num_handle == 9) {
service1_handle =param->add_attr_tab.handles[0];
char11_write_handle = param->add_attr_tab.handles[2];
char11_notify_handle = param->add_attr_tab.handles[4];
char31_write_handle = param->add_attr_tab.handles[6];
char21_notify_desc_handle_A =param->add_attr_tab.handles[7];

esp_ble_gatts_start_service(service1_handle);

}//register table of index 7 do similar for new services

else if (param->add_attr_tab.num_handle ==8) {
service2_handle =param->add_attr_tab.handles[0];
char12_write_handle = param->add_attr_tab.handles[2];
char12_notify_handle = param->add_attr_tab.handles[4];
char32_write_handle = param->add_attr_tab.handles[6];
char22_notify_desc_handle_B =param->add_attr_tab.handles[7];

esp_ble_gatts_start_service(service2_handle);

}
else {
ESP_LOGE(GATTS_TAG, "Create attribute table abnormally, the number handle = %d\n", param->add_attr_tab.num_handle);
}
}
break;
}

Adding a Profile

In BLE, each profile is intended to represent a separate set of services and characteristics that are logically grouped. However, two profiles cannot share the exact same service and characteristic UUIDs at the same time. BLE works by advertising distinct services and characteristics with unique identifiers, so reusing the same UUIDs for multiple profiles will create conflicts and errors during the GATT service creation.

  1. Define New UUIDs and new service for new profile
static const uint16_t GATTS_SERVICE_UUID_TEST_C = 0x00DD;
static const uint16_t GATTS_CHAR1_UUID_TEST_C = 0xDD01;
static const uint16_t GATTS_DESCR_UUID_TEST_C = 0xDD21;

static uint16_t service3_handle;
static uint16_t char13_write_handle;
static uint16_t char13_write_desc_handle_C;
static uint16_t char32_write_handle;

2 .Set Up Attribute Database

static const esp_gatts_attr_db_t gatt_db_c[] = {
// Service C Declaration
[0] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST_C), (uint8_t *)&GATTS_SERVICE_UUID_TEST_C}
},


// Characteristic 1 Declaration for Service B (Write)
[1] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char3_properties}
},

// Characteristic 1 Value for Service B
[2] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR1_UUID_TEST_C, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(gatts_demo_char1_val), (uint8_t *)&gatts_demo_char1_val}
},

[3] = {
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&descriptor_declaration_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(GATTS_DESCR_UUID_TEST_C), (uint8_t *)&GATTS_DESCR_UUID_TEST_C}
},

};

3. Create a New Event Handler

static void gatts_profile_c_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:

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);
}
#ifdef CONFIG_SET_RAW_ADV_DATA
esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
if (raw_adv_ret){
ESP_LOGE(GATTS_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
}
adv_config_done |= adv_config_flag;
esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
if (raw_scan_ret){
ESP_LOGE(GATTS_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
}
adv_config_done |= scan_rsp_config_flag;
#else
//config adv data
esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);
if (ret){
ESP_LOGE(GATTS_TAG, "config adv data failed, error code = %x", ret);
}
adv_config_done |= adv_config_flag;
//config scan response data
ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
if (ret){
ESP_LOGE(GATTS_TAG, "config scan response data failed, error code = %x", ret);
}
adv_config_done |= scan_rsp_config_flag;

#endif

esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db_c, gatts_if, 4, 0);
if (create_attr_ret) {
ESP_LOGE(GATTS_TAG, "create attr table failed, error code = %x", create_attr_ret);
}//to create new service first create a table

break;

case ESP_GATTS_READ_EVT: {
ESP_LOGI(GATTS_TAG, "Characteristic read, conn_id %d, trans_id %" PRIu32 ", handle %d",
param->read.conn_id, param->read.trans_id, param->read.handle);

esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = 4;
rsp.attr_value.value[0] = 0xde;
rsp.attr_value.value[1] = 0xed;
rsp.attr_value.value[2] = 0xbe;
rsp.attr_value.value[3] = 0xef;

// Make sure you use the correct connection ID here
esp_err_t status = esp_ble_gatts_send_response(gl_profile_tab[PROFILE_C_APP_ID].gatts_if,
param->read.conn_id,
param->read.trans_id,
ESP_GATT_OK,
&rsp);

if (status != ESP_OK) {
ESP_LOGE(GATTS_TAG, "Send response failed, error code = %x", status);
}
break;
}

case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
//we did mainly changes here
if (param->write.handle == char13_write_handle) {
ESP_LOGE(GATTS_TAG, "service 3 profile C Characteristic 3 write event, value: %.*s", param->write.len, param->write.value);
if (param->write.len == 1) {
uint8_t received_value = param->write.value[0]; // Dereference the pointer to get the value

if (received_value == 1) {
printf("data is : %d\n", (int)received_value);
} else if (received_value == 2) {
printf("data is : %d\n", (int)received_value);
} else if (received_value == 3) {
printf("data is : %d\n", (int)received_value);
}
} else {
ESP_LOGE(GATTS_TAG, "Unexpected write value length: %d", param->write.len);
}
}

}
example_write_event_env(gatts_if, &c_prepare_write_env, param);
break;
}
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGI(GATTS_TAG,"ESP_GATTS_EXEC_WRITE_EVT");
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
example_exec_write_event_env(&c_prepare_write_env, param);
break;
case ESP_GATTS_MTU_EVT:
ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
break;
case ESP_GATTS_UNREG_EVT:
break;
case ESP_GATTS_ADD_INCL_SRVC_EVT:
break;
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
gl_profile_tab[PROFILE_C_APP_ID].descr_handle = param->add_char_descr.attr_handle;
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
break;
case ESP_GATTS_DELETE_EVT:
break;
case ESP_GATTS_START_EVT:
ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d\n",
param->start.status, param->start.service_handle);
break;
case ESP_GATTS_STOP_EVT:
break;
case ESP_GATTS_CONNECT_EVT: {
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
// For the IOS system, please reference the apple official documents about the ble connection parameters restrictions.
conn_params.latency = 0;
conn_params.max_int = 0x20; // max_int = 0x20*1.25ms = 40ms
conn_params.min_int = 0x10; // min_int = 0x10*1.25ms = 20ms
conn_params.timeout = 400; // timeout = 400*10ms = 4000ms
ESP_LOGI(GATTS_TAG, "ESP_GATTS_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_C_APP_ID].conn_id = param->connect.conn_id;
//start sent the update connection parameters to the peer device.
esp_ble_gap_update_conn_params(&conn_params);
break;
}case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
if (param->add_attr_tab.status != ESP_GATT_OK) {
ESP_LOGE(GATTS_TAG, "Create attribute table failed, error code=0x%x", param->add_attr_tab.status);
} else {
ESP_LOGI(GATTS_TAG, "Create attribute table successfully, the number handle = %d\n", param->add_attr_tab.num_handle);
//second main changes happend here
if (param->add_attr_tab.num_handle == 4) {
service3_handle =param->add_attr_tab.handles[0];
char13_write_handle = param->add_attr_tab.handles[2];
// char1_notify_handle = param->add_attr_tab.handles[];
//char3_write_handle = param->add_attr_tab.handles[];
char13_write_desc_handle_C =param->add_attr_tab.handles[3];

esp_ble_gatts_start_service(service3_handle);

}//register table of index 7 do similar for new services

else {
ESP_LOGE(GATTS_TAG, "Create attribute table abnormally, the number handle = %d\n", param->add_attr_tab.num_handle);
}
}
break;
}
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(GATTS_TAG, "ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x%x", param->disconnect.reason);
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GATTS_CONF_EVT:
ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT, status %d attr_handle %d", param->conf.status, param->conf.handle);
if (param->conf.status != ESP_GATT_OK){
esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);
}
break;
case ESP_GATTS_OPEN_EVT:
case ESP_GATTS_CANCEL_OPEN_EVT:
case ESP_GATTS_CLOSE_EVT:
case ESP_GATTS_LISTEN_EVT:
case ESP_GATTS_CONGEST_EVT:
default:
break;
}
}

Make sure you use Profile C where ever profile A was used.

4. Add Profile to the 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, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},

[PROFILE_C_APP_ID] = {
.gatts_cb = gatts_profile_c_event_handler,
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
};

5. Register the New Profile(in app_main)

esp_err_t ret = esp_ble_gatts_app_register(PROFILE_C_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error for Profile C, error code = %x", ret);
return;
}

6. Handle Attribute Table Creation(already doen in event handler)

esp_err_t create_attr_ret =  esp_ble_gatts_create_attr_tab(gatt_db_c, gatts_if, 4, 0);
if (create_attr_ret) {
ESP_LOGE(GATTS_TAG, "create attr table failed, error code = %x", create_attr_ret);
}//to create new service first create a table

7. Test the firmware by flashing onto esp32

--

--

Tabrez Ahmed
Tabrez Ahmed

Written by Tabrez Ahmed

Electronics & Communication Engineering graduate RV College of Engineering.

Responses (1)