Consulting Services
ArticlesEmbedded C & MCUIoT

All about ESP32 – Part 4

Bluetooth Low Energy (BLE) Basics


Bluetooth Low Energy (BLE) is a multi-layer protocol or what’s known, as a terminology, a stack of protocols (Bluetooth stack). Before any practice with ESP32 Bluetooth peripheral, a solid background with most important BLE terms and basic overview is a must. That’s exactly why we wrote “Bluetooth Low Energy (BLE) 101 Tutorial: Intensive Introduction” article.

In this part of “All about ESP32” series we will explain the basics of using BLE. We will use one of ESP32 SDK (ESP-IDF) examples as a starting point. We will cite some content from our BLE introduction article, so try to get a look at it before starting.


ESP-IDF BLE GATT Server Example


This example is an example of a server that contains a set of services that clients will access to them. We will start explaining the most important parts of the example found in the following directory [ESP-IDF-DIR]\examples\bluetooth\gatt_server. This example will define TWO profiles and broadcast advertisements under the name of ESP_GATTS_DEMO. Each profile will contain one service and each service will contain one character. We will see and debug that in an Android Application called NRF Connect

The profile actually, doesn’t reflects any entry to the ATT table in the device. It’s a predefined set of services. In this example, they’ve defined identical service and characteristic in 2 profiles (Profile A & Profile B). This duplication has a design purpose described in the documentation:

In this way, a single design, enabled by different Application Profiles, can behave differently when used by different smartphone apps, allowing the server to react differently according to the client app that is being used. In practice, each profile is seen by the client as an independent BLE service. It is up to the client to discriminate the services that it is interested in.

GATT Server Demo ESP32


Initialization in Main function


Starting from main function with the very basic initialization part

// Initialize NVS.
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        ret = nvs_flash_init();
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();



As we saw before in WiFi part in this series, NVS (Non-Volatile Storage) should be initialized. The BLE stores some non-volatile variables in the NVS partition.

ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        ret = nvs_flash_init();

The code starts by initializing the Controller block of Bluetooth stack using some default parameters, they can be found in esp_bt.h. The Controller, as we discussed in the introduction article, basically manages everything that relates to the physical radio transaction,  and besides the initialization we should enable the Controller with BLE mode.

esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();

As Controller can run in one of the several modes, show below,and as ESP32 is a “Bluetooth Smart Ready” chip, it means that it can run in BLE mode, Classic Bluetooth mode or in dual-mode.

typedef enum
    ESP_BT_MODE_IDLE       = 0x00,   /*!< Bluetooth is not running */
    ESP_BT_MODE_BLE        = 0x01,   /*!< Run BLE mode */ 
    ESP_BT_MODE_CLASSIC_BT = 0x02,   /*!< Run Classic BT mode */ 
    ESP_BT_MODE_BTDM       = 0x03,   /*!< Run dual mode */ } esp_bt_mode_t;

The next thing to do is to enable the Bluetooth software stack, which is the software in the background that does some internal bluetooth operations and controls bluetooth stack layers. ESP32 uses a modified version of the software stack called Bluedroid.



The next step is to register callback functions to serve GATT and GAP events, and BLE profiles that we’ve described in the beginning.


The top two layers in BLE stack that mostly matter for BLE developer are GAP, which is responsible mainly for advertising and broadcasting, and GATT which is responsible for storing and exchanging real data through a set of services that consist of characteristics which hold the concerning data.

Many different events may occur in GAP and GATT, thus each action is different depending on the event type. For this reason, the developer uses an event handler function for GAP and GATT. The BLE software stack calls these callback functions passing some parameters and other event information to be used in the handler.


The other thing to do is to register the two profiles


And finally, to set Maximum Transmission Unit (MTU) to 500.

What MTU defines is the size of Attribute PDU (packet not the bare ATT in the ATT table). To know more, you can check the very handy Punchthrough’s blog entry about MTU. Client and the Server should exchange their MTU when entering connection. Keep in mind that the ATT packets with a size larger than MTU cannot be transferred.


Starting Broadcasting/Advertisement


The first thing to do in the BLE firmware is to enable GAP advertisements, with connectable type packets and under a certain device name. To know more about advertising packets please read the Advertising (Advertiser & Scanner) section from our last article.

Back to the code, when we register the profile, an event will be triggered which is ESP_GATTS_REG_EVT, and by the way, GATT events defined in ESP-IDF can be one of the following:

typedef enum 
    ESP_GATTS_REG_EVT                 = 0,       /*!< When register application id, the event comes */
    ESP_GATTS_READ_EVT                = 1,       /*!< When gatt client request read operation, the event comes */
    ESP_GATTS_WRITE_EVT               = 2,       /*!< When gatt client request write operation, the event comes */
    ESP_GATTS_EXEC_WRITE_EVT          = 3,       /*!< When gatt client request execute write, the event comes */
    ESP_GATTS_MTU_EVT                 = 4,       /*!< When set mtu complete, the event comes */ 
    ESP_GATTS_CONF_EVT                = 5,       /*!< When receive confirm, the event comes */ 
    ESP_GATTS_UNREG_EVT               = 6,       /*!< When unregister application id, the event comes */ 
    ESP_GATTS_CREATE_EVT              = 7,       /*!< When create service complete, the event comes */ 
    ESP_GATTS_ADD_INCL_SRVC_EVT       = 8,       /*!< When add included service complete, the event comes */ 
    ESP_GATTS_ADD_CHAR_EVT            = 9,       /*!< When add characteristic complete, the event comes */
    ESP_GATTS_ADD_CHAR_DESCR_EVT      = 10,      /*!< When add descriptor complete, the event comes */ 
    ESP_GATTS_DELETE_EVT              = 11,      /*!< When delete service complete, the event comes */ 
    ESP_GATTS_START_EVT               = 12,      /*!< When start service complete, the event comes */ 
    ESP_GATTS_STOP_EVT                = 13,      /*!< When stop service complete, the event comes */
    ESP_GATTS_CONNECT_EVT             = 14,      /*!< When gatt client connect, the event comes */ 
    ESP_GATTS_DISCONNECT_EVT          = 15,      /*!< When gatt client disconnect, the event comes */ 
    ESP_GATTS_OPEN_EVT                = 16,      /*!< When connect to peer, the event comes */ 
    ESP_GATTS_CANCEL_OPEN_EVT         = 17,      /*!< When disconnect from peer, the event comes */
    ESP_GATTS_CLOSE_EVT               = 18,      /*!< When gatt server close, the event comes */ 
    ESP_GATTS_LISTEN_EVT              = 19,      /*!< When gatt listen to be connected the event comes */
    ESP_GATTS_CONGEST_EVT             = 20,      /*!< When congest happen, the event comes */
     /* following is extra event */ 
    ESP_GATTS_RESPONSE_EVT            = 21,      /*!< When gatt send response complete, the event comes */ 
    ESP_GATTS_CREAT_ATTR_TAB_EVT      = 22,      /*!< When gatt create table complete, the event comes */ 
    ESP_GATTS_SET_ATTR_VAL_EVT        = 23,      /*!< When gatt set attr value complete, the event comes */ 
    ESP_GATTS_SEND_SERVICE_CHANGE_EVT = 24,      /*!< When gatt send service change indication complete, the event comes */ 
} esp_gatts_cb_event_t;

Now let’s check gatts_event_handler :

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
    /* If event is register event, store the gatts_if for each profile */
    if (event == ESP_GATTS_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
        } else {
            ESP_LOGI(GATTS_TAG, "Reg app failed, app_id %04x, status %d\n",

    /* If the gatts_if equal to profile A, call profile A cb handler,
     * so here call each profile's callback */
    do {
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
                    gatts_if == gl_profile_tab[idx].gatts_if) {
                if (gl_profile_tab[idx].gatts_cb) {
                    gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
    } while (0);

What this function basically does, is to check if the event type is ESP_GATTS_REG_EVT, and if it’s true … first, it stores an interface number for each profile. The documentation says that GATT interface type in a different application on GATT client uses different gatt_if values.

gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;

The second thing to do is to call each profile call-back handler: gatts_profile_a_event_handler and gatts_profile_b_event_handler

gl_profile_tab[idx].gatts_cb(event, gatts_if, param);

Where these call-back functions were assigned while the declaration of gl_profile_tab array.

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_B_APP_ID] = {
        .gatts_cb = gatts_profile_b_event_handler,                   /* This demo does not implement, similar as profile A */
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */

Part of gatts_profile_a_event_handler will set advertisement data using esp_ble_gap_config_adv_data(&adv_data) and esp_ble_gap_set_device_name(TEST_DEVICE_NAME), then the gap_event_handler will start the advertisement using esp_ble_gap_start_advertising function when the event type is ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT.

Now, the last thing to do is to create a service inside the profile using the esp_ble_gatts_create_service function. The function takes 3 parameters one of them is the number of handlers/ATT to allocate for the service as the service may contain more than one characteristic. The very basic service may contain one characteristic, and each characteristic will contain a characteristic declaration, Characteristic value attribute handle and the real value. Thus, the minimum attributes for the service are 4: service declaration,  characteristic declaration, Characteristic value attribute handle, and the real value. For more information, please refer to How Your Device Data Is Stored section in our last article and try to read An Espressif developer’ answer about a related question.


Making a Connection and Sending Read/Write Requests


As we said, first we need a connectable type of advertisement packets to make the Central ask the Peripheral to make a connection. We can see that advertisement packets type is ADV_TYPE_IND.

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,

Let’s recall this table from the previous article.

“Getting Started with Bluetooth Low Energy” Book


Now, when a mobile (Central) connects to our device and specifically to the service of profile A. The connection parameters will be updated to new values making the timeout for the connection without any activity equal to 4000 ms. Look to this switch case inside gatts_profile_a_event_handler:

        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.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;
        //start sent the update connection parameters to the peer device.

Now, after we connect we can read, write , or subscribe to notifications as the created characteristic in A and B has the following properties:


Let’s have a look at what read request will do. It will send a response containing dummy four bytes as described in the code:

        ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %d, handle %d\n", 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;
        esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
                                    ESP_GATT_OK, &rsp);


Finally, The Example in Action


I used NRF Connect Android application to discover this example services and try reading and writing to it. Here are the images:


NRF Connect App ESP GATTS Demo ADVAdvertising under the name of “ESP_GATTS_DEMO”


NRF Connect App ESP GATTS Demo Services

Available services


NRF Connect App ESP GATTS Demo Read/writing request

Testing reading and writing to services




After our last article “Bluetooth Low Energy (BLE) 101 Tutorial: Intensive Introduction” and reading this technical article with explanations to most important parts of the “gatt_server” example from ESP-IDF SDK, you should now have a very basic idea of BLE concept and how to design a very simple BLE device starting from a ready demo like the one in this article. Although the ESP-IDF has made a very good walkthrough on Github, I found that it needs to be rewritten in a way more proper for a starter in BLE world.

In our next article, we will write a simple service to control the built-in LED of an ESP32 breakout board, starting from the same code of this example, but re-organized in a simpler and more straight-forward style.


Yahya Tawil

Embedded Hardware Engineer interested in open hardware and was born in the same year as Linux. Yahya is the editor-in-chief of Atadiat and believes in the importance of sharing free, practical, spam-free and high quality written content with others. His experience with Embedded Systems includes developing firmware with bare-metal C and Arduino, designing PCB&schematic and content creation.


  1. Excellent tutorials!! Please keep up the good work.
    Thank you very much for your time and effort.
    I subscribed to the mailing list awaiting new ESP32 tutorials

    1. I don’t think that you can do anything from firmware side … if the service is not one of the listed one in BLE standard (found on Bluetooth SIG website) then you should tell your mobile App the service UUID has that name. For characteristics you have a Characteristic User description attribute that contains Characteristic name.

  2. I’ve just worked through this example line-by-line (painfully) until I understood it. Your description is much simpler.
    However, why in the example did they generate dummy data for the characteristic value? That seems pointless, The characteristic value attribute is assigned value {0x11, 0x22, 0x33} when it is created and added to the service. So why not read that back? The dummy data confused me for ages.

    1. Yeah, no easy way to understand ESP32 code without some pain, I tried to make the pain less in the 4 parts : )

      If I understand your question correctly … making the characteristic responses with dummy data is just for testing (to make sure all things are working as intended), it could be using a global variable like a sensor value or something else.

  3. Is it possible to read out and dump the entire attribute table?
    I was thinking of a loop:
    Get next handle
    Read attribute(handle)
    Print attribute: handle, Type (UUID), Permissions, Value

    This is for debugging purposes only.

    1. I don’t know for instance an Android App to dump all attributes, but I find NRF connect further enough to find all information about characteristics. However if you have an NRF52 KIT/doungle then you can use connectivity firmware with a simple Python script to do the dump for you. Also don’t forget to check the log menu in NRF connect App it has a lot of useful information.

      1. I was thinking of printing the attribute table from within the application running on the ESP32. This is for debugging and to verify the attributes. also just to learn about the attribute table.
        I do see all the services, characteristics and descriptors etc on the nRF connect app, so that does verify things.
        Thanks for your reply

        1. Well I don’t know exactly if you can do that easily in ESP side if you are not using the raw table way, as there was an example in the SDK called “gatt_server_service_table” they built the GATT table raw by raw in the application, you can get a look for learning purpose if you like. it seems you’re doing that already 🙂

  4. Re-read your explanation. it is really good!
    I’ve been pulling the esp_gatts_table_server example to bits to see how it works. This all seems logical, and a better way of setting up the GATT server.
    However the example only has one service. Do you know how to add another service? I was trying to add a battery service.
    It didn’t work when I added a second service to the profile table. Compiled OK but the GATT server complained when it ran.
    It seems like I need to create a second profile for a second service, with an associated call-back function etc.
    Is this correct? I thought the BLE standard said a Profile can contain multiple services, and each service can contain multiple characteristics.

  5. Hello
    Great article, all is explained in a excellent and pedagogic way : elementary explanations at the beginning with good references allowing to dive deeper in this jungle which is BT for a beginner.
    But I did not found the <>. Not yet written ?
    Anyway thousand thank you for all these articles which are for me the <> to progress in the programming of the ESP32.

    Sorry for my english but i am french.

    1. Most welcome Jack. Happy to hear that the series was written in a way found its way to your journey with ESP32!

      Maybe in the couple few months, I will resume writing in this series. Keep an eye on our website.


  6. Thank you so much for this article and the BLE guide, they are very helpful for a starter like me, great work.

    I’ll keep waiting for the next article to know the “simpler and more straight-forward way”, I want to do a project to control the ESP32 from the mobile and I’m still quite lost on how to connect everything together, please please please!

  7. Hello sir. Have a great day. Thank you for your excellent tutorials. I am working on a project. So, in the end, I want to use ESP32 to send data to the phone app. But I cannot build espressif ide project. If you have time could you help my problem to solve it? Thanks for understanding.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Back to top button