Demo 27: How to use Arduino ESP32 BLE (Bluetooth Low Energy) as a GATT server

1. Introduction
In this tutorial (2 parts: part 1: GATT server and part 2: GATT client), I will show you how to use BLE (Bluetooth Low Energy) in Arduino ESP32. Firstly, we need to know some basic concepts.Or you can refer here.
-  GAP stands for Generic Access Profile. GAP makes your device visible to the other BLE devices (BLE devices can scan your BLE device), and determines how two devices can interact with each other.
- There are 2 kinds of devices in BLE communication: Central devices and Peripheral devices.
+ Peripheral devices are small, low power, resource constrained devices that can connect and give data to to a powerful central device. Example: a heart rate monitor.
+ Central devices are usually the powerful  devices like smart phone or tablet. It can scan and connect to any peripheral device that is advertising information to get data from peripheral device.
- When the connection is established between the peripheral and a central device, the advertising process will generally stop and you will use GATT (Generic Attribute Profile) services and characteristics to communicate in both directions.
- GATT is based on a traditional client-server architecture including GATT Server and GATT Client.
- The peripheral device keeps the role as the GATT Server, and the central device keeps the role as GATT Client, which sends requests to this server. 
- Beside that GATT also has some concepts called Profiles, Services and Characteristics. 
+ Profiles: is a collection of Services.
+ Services: is a collection of characteristic. Service distinguishes from other services by a unique 16-bit numeric ID called a UUID.
+ Characteristics: is data. Characteristic distinguishes from other Characteristics by a unique 16-bit or 128-bit numeric ID called a UUID.
Figure: Example of Hear Rate Profile


- In this tutorial, I will make a demo using GATT to turn ON/OFF a LED. So ESP32 will act as a GATT server and a GATT client (I use Raspbbery Pi3 with BLE or if your laptop is equipped with BLE you can use it).

2. Software
- We will use LightBlue on iOS or on Android for testing or  Bluez Gatttool for Raspberry Pi3 as a GATT client to connect to our ESP32 GATT server.
2.1 Bluez Gatttool for Raspberry Pi3

If the gatttool was not installed on your RPi3 then you can follow these steps to install it:
+ wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.46.tar.xz
+ tar xvf bluez-5.46.tar.xz
+ sudo apt-get install libglib2.0-dev libdbus-1-dev libusb-dev libudev-dev libical-dev systemd libreadline-dev
+ ./configure --enable-library
+ make -j8 && sudo make install
+ sudo cp attrib/gatttool /usr/local/bin/

- Run a BLE scan: sudo hcitool lescan : It will return the MAC address of BLE device
- Connect to the BLE device: sudo gatttool -b BLE_ADDR -I : then type connect to connect. We use sudo hcitool lescan to get BLE_ADDR
Note: If you could not connect after typing connect, you should try typing connect some times.
- List all uuids of services: primary
- List all available handles (Handles are the «connection points» where you can read and write access data): char-desc
- Read from a handle: char-read-hnd
- Write to a handle: char-write-req
2.2 ESP32 GATT server
The role of GATT server:
- Receive the Write command of GATT client and convert it to LED state
- Sending its temperature to GATT client using BLE notification
The name of our ESP32 BLE device is "ESP_GATTS_IOTSHARING" and we will create 1 service GATTS_SERVICE_UUID_TEST_LED with 2 characteristics: GATTS_CHAR_UUID_LED_CTRL and GATTS_CHAR_UUID_TEMP_NOTI.
The characteristic GATTS_CHAR_UUID_LED_CTRL with read and write permission to write the LED control command to it and to read the data from BLE device (For testing the read request only returns "iotsharing.com" string).
The characteristic GATTS_CHAR_UUID_TEMP_NOTI with notification permission that enable the GATT client to register BLE notification to receive the temperature data of the GATT server.

I also wrote many comments in the code, so you can read and map them with theory above.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
#pragma GCC diagnostic push
#pragma GCC diagnostic warning "-fpermissive"

#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 "bt.h"
#include "bta_api.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_main.h"

#include "sdkconfig.h"

#pragma GCC diagnostic pop

/* this function will be invoked to handle incomming events */
static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);

#define LED                          4
#define GATTS_SERVICE_UUID_TEST_LED   0xAABB
#define GATTS_CHAR_UUID_LED_CTRL      0xAA01
#define GATTS_CHAR_UUID_TEMP_NOTI     0xBB01
#define GATTS_NUM_HANDLES     8

#define TEST_DEVICE_NAME            "ESP_GATTS_IOTSHARING"
/* maximum value of a characteristic */
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0xFF

/* profile info */
#define PROFILE_ON_APP_ID 0
/* characteristic ids for led ctrl and temp noti */
#define CHAR_NUM 2
#define CHARACTERISTIC_LED_CTRL_ID    0
#define CHARACTERISTIC_TEMP_NOTI_ID   1

/* value range of a attribute (characteristic) */
uint8_t attr_str[] = {0x00};
esp_attr_value_t gatts_attr_val =
{
    .attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX,
    .attr_len     = sizeof(attr_str),
    .attr_value   = attr_str,
};
/* custom uuid */
static uint8_t service_uuid128[32] = {
    /* LSB <--------------------------------------------------------------------------------> MSB */
    //first uuid, 16bit, [12],[13] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xAB, 0xCD, 0x00, 0x00,
};

static esp_ble_adv_data_t test_adv_data = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = true,
    .min_interval = 0x20,
    .max_interval = 0x40,
    .appearance = 0x00,
    .manufacturer_len = 0,
    .p_manufacturer_data =  NULL,
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = 16,
    .p_service_uuid = service_uuid128,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

//this variable holds advertising parameters
esp_ble_adv_params_t test_adv_params;

//this structure holds the information of characteristic
struct gatts_characteristic_inst{
    esp_bt_uuid_t char_uuid;
    esp_bt_uuid_t descr_uuid;
    uint16_t char_handle;
    esp_gatt_perm_t perm;
    esp_gatt_char_prop_t property;
    uint16_t descr_handle;
};

//this structure holds the information of current BLE connection
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;
    struct gatts_characteristic_inst chars[CHAR_NUM];
};

//this variable holds the information of current BLE connection
static struct gatts_profile_inst test_profile;

/* 
This callback will be invoked when GAP advertising events come.
Refer GAP BLE callback event type here: 
https://github.com/espressif/esp-idf/blob/master/components/bt/bluedroid/api/include/esp_gap_ble_api.h
*/
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_ble_gap_start_advertising(&test_adv_params);
   break;
  case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
   esp_ble_gap_start_advertising(&test_adv_params);
   break;
  case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
   esp_ble_gap_start_advertising(&test_adv_params);
   break;
  case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
   //advertising start complete event to indicate advertising start successfully or failed
   if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
    Serial.printf("\nAdvertising start failed\n");
   }
   break;
  case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
   if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
    Serial.printf("\nAdvertising stop failed\n");
   }
   else {
    Serial.printf("\nStop adv successfully\n");
   }
   break;
  default:
   break;
    }
}

//process BLE write event ESP_GATTS_WRITE_EVT
void process_write_event_env(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param){
    /* check char handle and set LED */
    if(test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].char_handle == param->write.handle){
        if(param->write.len == 1){
            uint8_t state = param->write.value[0];
            digitalWrite(LED, state);
        }
    }
    /* send response if any */
    if (param->write.need_rsp){
        Serial.printf("respond");
        esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
        if (response_err != ESP_OK){
            Serial.printf("\nSend response error\n");
        }
    }
}

/*
This callback will will be invoked when GATT BLE events come.
Refer GATT Server callback function events here: 
https://github.com/espressif/esp-idf/blob/master/components/bt/bluedroid/api/include/esp_gatts_api.h
*/
static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
    switch (event) {
    //When register application id, the event comes
    case ESP_GATTS_REG_EVT:{
        Serial.printf("\nREGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
        test_profile.service_id.is_primary = true;
        test_profile.service_id.id.inst_id = 0x00;
        test_profile.service_id.id.uuid.len = ESP_UUID_LEN_16;
        test_profile.service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_LED;
  //after finishing registering, the ESP_GATTS_REG_EVT event comes, we start the next step is creating service
        esp_ble_gatts_create_service(gatts_if, &test_profile.service_id, GATTS_NUM_HANDLES);
        break;
    }
    case ESP_GATTS_READ_EVT: {
        Serial.printf("\nESP_GATTS_READ_EVT\n");
        esp_gatt_rsp_t rsp;
        memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
        rsp.attr_value.handle = param->read.handle;
        rsp.attr_value.len = 14;
        rsp.attr_value.value[0] = 105;
        rsp.attr_value.value[1] = 111;
        rsp.attr_value.value[2] = 116;
        rsp.attr_value.value[3] = 115;
        rsp.attr_value.value[4] = 104;
        rsp.attr_value.value[5] = 97;
        rsp.attr_value.value[6] = 114;
        rsp.attr_value.value[7] = 105;
        rsp.attr_value.value[8] = 110;
        rsp.attr_value.value[9] = 103;
        rsp.attr_value.value[10] = 46;
        rsp.attr_value.value[11] = 99;
        rsp.attr_value.value[12] = 111;
        rsp.attr_value.value[13] = 109;
  /* 
  When central device send READ request to GATT server, the ESP_GATTS_READ_EVT comes 
  This always responds "iotsharing.com" string 
  */
        esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
                                    ESP_GATT_OK, &rsp);
        break;
    }
 /* 
 When central device send WRITE request to GATT server, the ESP_GATTS_WRITE_EVT comes 
 Invoking process_write_event_env() to process and send response if any
 */
    case ESP_GATTS_WRITE_EVT: {
        Serial.printf("\nESP_GATTS_WRITE_EVT\n");
        process_write_event_env(gatts_if, param);
        break;
    }
    // When create service complete, the event comes
    case ESP_GATTS_CREATE_EVT:{
        Serial.printf("\nstatus %d, service_handle %x, service id %x\n", param->create.status, param->create.service_handle, param->create.service_id.id.uuid.uuid.uuid16);
        // store service handle and add characteristics
        test_profile.service_handle = param->create.service_handle;
        // LED controller characteristic
        esp_ble_gatts_add_char(test_profile.service_handle, &test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].char_uuid,
                               test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].perm,
                               test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].property,
                               &gatts_attr_val, NULL);
        // temperature monitoring characteristic
        esp_ble_gatts_add_char(test_profile.service_handle, 
                               &test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].char_uuid,
                               test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].perm,
                               test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].property,
                               &gatts_attr_val, NULL);
    //and start service
        esp_ble_gatts_start_service(test_profile.service_handle);
        break;
    }
 //When add characteristic complete, the event comes
    case ESP_GATTS_ADD_CHAR_EVT: {
        Serial.printf("\nADD_CHAR_EVT, status %d,  attr_handle %x, service_handle %x, char uuid %x\n",
                param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle, param->add_char.char_uuid.uuid.uuid16);
        /* store characteristic handles for later usage */
        if(param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_LED_CTRL){
            test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].char_handle = param->add_char.attr_handle;
        }else if(param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_TEMP_NOTI){
            test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].char_handle = param->add_char.attr_handle;
        }   
  //if having characteristic description calling esp_ble_gatts_add_char_descr() here 
        break;
    }
 //When add descriptor complete, the event comes
    case ESP_GATTS_ADD_CHAR_DESCR_EVT:{
        Serial.printf("\nESP_GATTS_ADD_CHAR_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
                 param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
        break;
    }
    /* when disconneting, send advertising information again */
    case ESP_GATTS_DISCONNECT_EVT:{
        esp_ble_gap_start_advertising(&test_adv_params);
        break;
    }
    // When gatt client connect, the event comes
    case ESP_GATTS_CONNECT_EVT: {
        Serial.printf("\nESP_GATTS_CONNECT_EVT\n");
        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 = 0x50;    // max_int = 0x50*1.25ms = 100ms
        conn_params.min_int = 0x30;    // min_int = 0x30*1.25ms = 60ms
        conn_params.timeout = 1000;    // timeout = 1000*10ms = 10000ms
        test_profile.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;
    }
    default:
        break;
    }
}

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 the profile */
    if (event == ESP_GATTS_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            test_profile.gatts_if = gatts_if;
        } else {
            Serial.printf("\nReg app failed, app_id %04x, status %d\n",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }
    /* here call each profile's callback */
    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 == test_profile.gatts_if) {
        if (test_profile.gatts_cb) {
            test_profile.gatts_cb(event, gatts_if, param);
        }
    }
}

void setup(){
    Serial.begin(115200);
    pinMode(LED, OUTPUT);
    digitalWrite(LED, LOW);
    /* initialize advertising info */
    test_adv_params.adv_int_min        = 0x20;
    test_adv_params.adv_int_max        = 0x40;
    test_adv_params.adv_type           = ADV_TYPE_IND;
    test_adv_params.own_addr_type      = BLE_ADDR_TYPE_PUBLIC;
    test_adv_params.channel_map        = ADV_CHNL_ALL;
    test_adv_params.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY;
    /* initialize profile and characteristic permission and property*/
    test_profile.gatts_cb = gatts_profile_event_handler;
    test_profile.gatts_if = ESP_GATT_IF_NONE;
    test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].char_uuid.len = ESP_UUID_LEN_16;
    test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_LED_CTRL;
    test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].perm = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
    test_profile.chars[CHARACTERISTIC_LED_CTRL_ID].property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE;
    test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].char_uuid.len = ESP_UUID_LEN_16;
    test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEMP_NOTI;
    test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].perm = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
    test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
    
    esp_err_t ret;
    /* initialize BLE and bluedroid */
    btStart();
    ret = esp_bluedroid_init();
    if (ret) {
        Serial.printf("\n%s init bluetooth failed\n", __func__);
        return;
    }
    ret = esp_bluedroid_enable();
    if (ret) {
        Serial.printf("\n%s enable bluetooth failed\n", __func__);
        return;
    }
    // set BLE name and broadcast advertising info so that the world can see you
    esp_ble_gap_set_device_name(TEST_DEVICE_NAME);
    esp_ble_gap_config_adv_data(&test_adv_data);
    // register callbacks to handle events of GAp and GATT
    esp_ble_gatts_register_callback(gatts_event_handler);
    esp_ble_gap_register_callback(gap_event_handler);
    // register profiles with app id
    esp_ble_gatts_app_register(CHARACTERISTIC_LED_CTRL_ID);
}

long lastMsg = 0;
//send temperature value to registered notification client every 5 seconds via GATT notification
void loop(){
    long now = millis();
    if (now - lastMsg > 5000) {
        lastMsg = now;
        uint8_t temp = random(0, 50);
        esp_ble_gatts_send_indicate(test_profile.gatts_if, 
                                    test_profile.conn_id, 
                                    test_profile.chars[CHARACTERISTIC_TEMP_NOTI_ID].char_handle,
                                    sizeof(temp), &temp, false);
    }
}
3. Result
3.1 Using LightBlue to connect to GATT server
Figure: 2 characteristics with uuid AA01 and BB01
 Fifure: Choose Write to write value to GATT server
 Figure: Choose 1 or 0 to turn ON/OFF LED
 Figure: press Listen for notifications to receive temp data
 Figure: Temp data are 0x25, 0x27, 0x04, ...
3.2 Using Bluez Gatttool + Raspberry Pi3 to connect to GATT server
If using Bluez Gatttool for Raspberry Pi3:
- Using sudo hcitool lescan to get the address of my BLE device. It is 30:AE:A4:02:70:76. Then I used sudo gatttool -b 30:AE:A4:02:70:76 -I to establish connection to it.

- Using char-desc command we will see our characteristic uuids that we set in the code, are aa01 and bb01 with handles are 0x002a and 0x002c. So we just operate on these handles with commands below. 
 Figure: Available characteristics
 Figure: write data to ESP32 BLE

Post a Comment

1 Comments

basti said…
Hi, maybe you can help me. I'm trying to deliver a value through the characteristics of a ble service to be able to communicate with an appleTV. TVOS notifies me when the value changed but I have no idea where to add the value in your great example because I would like to change this value after pressing a button. I hope you are able to help me!