All About ESP32 Parts
- Part 1: Overview, Tools and blinking LED
- Part 2: Logging and WiFi Basics
- Part 3: TCP and HTTP via WiFi
- Part 4: Bluetooth Low Energy (BLE) Basics
Related Articles
- MQTT 101 Tutorial: Introduction and Hands-on using Eclipse Mosquitto
- Bluetooth Low Energy (BLE) 101 Tutorial: Intensive Introduction
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.
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) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_init(&bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_BLE); esp_bluedroid_init(); esp_bluedroid_enable();
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) { ESP_ERROR_CHECK(nvs_flash_erase()); 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(); esp_bt_controller_init(&bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_BLE);
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.
esp_bluedroid_init(); esp_bluedroid_enable();
The next step is to register callback functions to serve GATT and GAP events, and BLE profiles that we’ve described in the beginning.
esp_ble_gatts_register_callback(gatts_event_handler); esp_ble_gap_register_callback(gap_event_handler); esp_ble_gatts_app_register(PROFILE_A_APP_ID); esp_ble_gatts_app_register(PROFILE_B_APP_ID); esp_ble_gatt_set_local_mtu(500);
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.
esp_ble_gatts_register_callback(gatts_event_handler); esp_ble_gap_register_callback(gap_event_handler);
The other thing to do is to register the two profiles
esp_ble_gatts_app_register(PROFILE_A_APP_ID); esp_ble_gatts_app_register(PROFILE_B_APP_ID);
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", param->reg.app_id, param->reg.status); return; } } /* 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:
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_A_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;
Now, after we connect we can read, write , or subscribe to notifications as the created characteristic in A and B has the following properties:
a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
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:
case ESP_GATTS_READ_EVT: { 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); break;
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:
Available services
Testing reading and writing to services
Conclusion
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.
All About ESP32 Parts
- Part 1: Overview, Tools and blinking LED
- Part 2: Logging and WiFi Basics
- Part 3: TCP and HTTP via WiFi
- Part 4: Bluetooth Low Energy (BLE) Basics
Related Articles