ESP32 Bluetooth Low Energy Data Exchange Tutorial with ESP-IDF: Menuconfig and Code Implementation Explained | Part 2

Bluetooth Low Energy Data Exchange Tutorial with ESP-IDF | Part - 2 | Innovate Yourself
3
1

Introduction

Welcome back to the second part of our ESP32 BLE Data Exchange Tutorial series. In the previous installment, we discussed the fundamentals of Bluetooth Low Energy (BLE) and got our development environment set up. Now, it’s time to delve deeper into ESP-IDF (Espressif IoT Development Framework) and explore how to exchange data seamlessly between your ESP32 board and other BLE devices. In this tutorial, we’ll cover four key topics:

  1. Setting Up Data Characteristics
  2. Maintaining Persistent BLE Connections
  3. Making Your Device Discoverable or Non-Discoverable (on press of boot button for 3 seconds)
  4. Controlling LEDs with BLE Commands

So, without further ado, let’s jump right in!

Setting Up Data Characteristics

In BLE communication, data characteristics define the type of data your device can send and receive. They act as containers for information and provide essential details about the data they hold. To set up characteristics in ESP-IDF, follow these steps:

Step 1: Define Your Custom Characteristic

In your ESP-IDF project, navigate to the relevant source file and define your custom data characteristic. For instance, let’s say we want to exchange temperature data. We can define a temperature characteristic as follows:

// Array of pointers to other service definitions
// UUID - Universal Unique Identifier
static const struct ble_gatt_svc_def gatt_svcs[] = {
    {.type = BLE_GATT_SVC_TYPE_PRIMARY,
     .uuid = BLE_UUID16_DECLARE(0x180), // Define UUID for device type
     .characteristics = (struct ble_gatt_chr_def[]){
         {.uuid = BLE_UUID16_DECLARE(0xFEF4), // Define UUID for reading
          .flags = BLE_GATT_CHR_F_READ,
          .access_cb = device_read},
         {.uuid = BLE_UUID16_DECLARE(0xDEAD), // Define UUID for writing
          .flags = BLE_GATT_CHR_F_WRITE,
          .access_cb = device_write},
         {.uuid = BLE_UUID16_DECLARE(0xDEAD), // Define UUID for writing
          .flags = BLE_GATT_CHR_F_READ,
          .access_cb = device_read2},
         {0}}},
    {0}};

Step 2: Register the Characteristic

Next, you need to register the characteristic with the ESP-IDF BLE stack. This step ensures that your device can advertise and accept data related to this characteristic.

ble_gatts_count_cfg(gatt_svcs);            // Initialize NimBLE configuration - config gatt services
ble_gatts_add_svcs(gatt_svcs);             // Initialize NimBLE configuration - queues gatt services.
    

Now that we’ve set up our data characteristic, let’s explore how to maintain persistent BLE connections.

Maintaining Persistent BLE Connections

Maintaining a stable and persistent connection is crucial in BLE applications. It ensures that your ESP32 board stays connected to other devices reliably. Here’s how to achieve this:

Step 1: Set Connection Parameters

In your ESP-IDF project, configure the connection parameters to suit your application. These parameters include connection interval, slave latency, and supervision timeout.

esp_ble_gap_set_conn_params(&conn_params);

Step 2: Handle Connection Events

Implement callback functions to handle connection events. You can use these callbacks to perform actions like data exchange or notifications when a connection is established or terminated.

esp_ble_gatts_register_callback(gatts_event_handler);
esp_ble_gap_register_callback(gap_event_handler);

By handling connection events effectively, you can ensure that your ESP32 maintains stable connections.

Making Your Device Discoverable or Non-Discoverable

BLE devices can be either discoverable or non-discoverable, depending on your application’s requirements. Let’s explore how to control this behavior:

Step 1: Define Discoverable or Non-Discoverable Mode

You can define your device’s discoverability mode in your ESP-IDF project. For example, to make your device discoverable:

struct ble_gap_adv_params adv_params;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable
ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);

And to make it non-discoverable:

ble_gap_adv_stop();

By toggling between these modes, you can control when your ESP32 board is visible to other BLE devices.

Controlling LEDs with BLE Commands

Now, let’s get practical and see how to control LEDs on your ESP32 using BLE commands from another device. This can be a fun way to demonstrate the capabilities of your BLE-enabled project.

Step 1: Define LED Characteristics

Define characteristics for controlling LEDs. For example, you can define a characteristic to turn an LED on and off:

static uint8_t led_state1 = 0; // 0 for OFF, 1 for ON
static uint8_t led_state2 = 0; // 0 for OFF, 1 for ON

Step 2: Handle Write Events

Implement callback functions to handle write events. When the connected device sends a BLE command to turn the LED on or off, your ESP32 should respond accordingly:

static int ble_gap_event(struct ble_gap_event *event, void *arg){
    // Handle write events here
}

By capturing write events, you can control the state of LEDs on your ESP32 using BLE commands.

Full Code

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "sdkconfig.h"
#include "driver/gpio.h"
char *TAG = "BLE-Server";
uint8_t ble_addr_type;
struct ble_gap_adv_params adv_params;
bool status = false;
void ble_app_advertise(void);
// Write data to ESP32 defined as server
static int device_write(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    // printf("Data from the client: %.*s\n", ctxt->om->om_len, ctxt->om->om_data);
    char *data = (char *)ctxt->om->om_data;
    printf("%d %s\n", strcmp(data, (char *)"LIGHT ON") == 0, data);
    if (strcmp(data, (char *)"LIGHT ON\0") == 0)
    {
        printf("LIGHT ON\n");
        gpio_set_level(GPIO_NUM_2, 1);
    }
    else if (strcmp(data, (char *)"LIGHT OFF\0") == 0)
    {
        printf("LIGHT OFF\n");
        gpio_set_level(GPIO_NUM_2, 0);
    }
    else if (strcmp(data, (char *)"LED ON\0") == 0)
    {
        printf("LED ON\n");
        gpio_set_level(GPIO_NUM_4, 1);
    }
    else if (strcmp(data, (char *)"LED OFF\0") == 0)
    {
        printf("LED OFF\n");
        gpio_set_level(GPIO_NUM_4, 0);
    }
    // else
    // {
    //     printf("Data from the client: %.*s\n", ctxt->om->om_len, ctxt->om->om_data);
    // }
    memset(data, 0, strlen(data));
    return 0;
}
// Read data from ESP32 defined as server
static int device_read(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    os_mbuf_append(ctxt->om, "Data from the server", strlen("Data from the server"));
    return 0;
}
static int device_read2(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    os_mbuf_append(ctxt->om, "Message from Innovate Yourself", strlen("Message from Innovate Yourself"));
    return 0;
}
// Array of pointers to other service definitions
// UUID - Universal Unique Identifier
static const struct ble_gatt_svc_def gatt_svcs[] = {
    {.type = BLE_GATT_SVC_TYPE_PRIMARY,
     .uuid = BLE_UUID16_DECLARE(0x180), // Define UUID for device type
     .characteristics = (struct ble_gatt_chr_def[]){
         {.uuid = BLE_UUID16_DECLARE(0xFEF4), // Define UUID for reading
          .flags = BLE_GATT_CHR_F_READ,
          .access_cb = device_read},
         {.uuid = BLE_UUID16_DECLARE(0xDEAD), // Define UUID for writing
          .flags = BLE_GATT_CHR_F_WRITE,
          .access_cb = device_write},
         {.uuid = BLE_UUID16_DECLARE(0xDEAD), // Define UUID for writing
          .flags = BLE_GATT_CHR_F_READ,
          .access_cb = device_read2},
         {0}}},
    {0}};
// BLE event handling
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
    switch (event->type)
    {
    // Advertise if connected
    case BLE_GAP_EVENT_CONNECT:
        ESP_LOGI("GAP", "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK!" : "FAILED!");
        if (event->connect.status != 0)
        {
            ble_app_advertise();
        }
        break;
    // Advertise again after completion of the event
    case BLE_GAP_EVENT_DISCONNECT:
        ESP_LOGI("GAP", "BLE GAP EVENT DISCONNECTED");
        if (event->connect.status != 0)
        {
            ble_app_advertise();
        }
        break;
    case BLE_GAP_EVENT_ADV_COMPLETE:
        ESP_LOGI("GAP", "BLE GAP EVENT");
        ble_app_advertise();
        break;
    default:
        break;
    }
    return 0;
}
// Define the BLE connection
void ble_app_advertise(void)
{
    // GAP - device name definition
    struct ble_hs_adv_fields fields;
    const char *device_name;
    memset(&fields, 0, sizeof(fields));
    device_name = ble_svc_gap_device_name(); // Read the BLE device name
    fields.name = (uint8_t *)device_name;
    fields.name_len = strlen(device_name);
    fields.name_is_complete = 1;
    ble_gap_adv_set_fields(&fields);
    // GAP - device connectivity definition
    // struct ble_gap_adv_params adv_params;
    memset(&adv_params, 0, sizeof(adv_params));
    // adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; // connectable or non-connectable
    // adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable
    // ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);
}
// The application
void ble_app_on_sync(void)
{
    ble_hs_id_infer_auto(0, &ble_addr_type); // Determines the best address type automatically
    ble_app_advertise();                     // Define the BLE connection
}
// The infinite task
void host_task(void *param)
{
    nimble_port_run(); // This function will return only when nimble_port_stop() is executed
}
void connect_ble(void)
{
    nvs_flash_init(); // 1 - Initialize NVS flash using
    // esp_nimble_hci_and_controller_init();      // 2 - Initialize ESP controller
    nimble_port_init();                        // 3 - Initialize the host stack
    ble_svc_gap_device_name_set("BLE-Server"); // 4 - Initialize NimBLE configuration - server name
    ble_svc_gap_init();                        // 4 - Initialize NimBLE configuration - gap service
    ble_svc_gatt_init();                       // 4 - Initialize NimBLE configuration - gatt service
    ble_gatts_count_cfg(gatt_svcs);            // 4 - Initialize NimBLE configuration - config gatt services
    ble_gatts_add_svcs(gatt_svcs);             // 4 - Initialize NimBLE configuration - queues gatt services.
    ble_hs_cfg.sync_cb = ble_app_on_sync;      // 5 - Initialize application
    nimble_port_freertos_init(host_task);      // 6 - Run the thread
}
void boot_creds_clear(void *param)
{
    // printf("%lld\n", n - m);
    int64_t m = esp_timer_get_time();
    while (1)
    {
        if (!gpio_get_level(GPIO_NUM_0))
        {
            int64_t n = esp_timer_get_time();
            if ((n - m) / 1000 >= 2000)
            {
                ESP_LOGI("BOOT BUTTON:", "Button Pressed FOR 3 SECOND\n");
                adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; // connectable or non-connectable
                adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable
                ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);
                status = true;
                vTaskDelay(100);
                m = esp_timer_get_time();
            }
        }
        else
        {
            m = esp_timer_get_time();
        }
        vTaskDelay(10);
        if (status)
        {
            // ESP_LOGI("GAP", "BLE GAP status");
            adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; // connectable or non-connectable
            adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable
            ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);
        }
    }
}
void app_main()
{
    gpio_set_direction(GPIO_NUM_0, GPIO_MODE_INPUT); // boot button
    gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
    gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT);
    connect_ble();
    xTaskCreate(boot_creds_clear, "boot_creds_clear", 2048, NULL, 5, NULL);
}
  • Github Repository | Bluetooth Low Energy Data Exchange Tutorial with ESP-IDF | Innovate Yourself
  • Github Repository | Bluetooth Low Energy Data Exchange Tutorial with ESP-IDF | Innovate Yourself

DOWNLOAD FULL CODE

Conclusion

In this second part of our ESP32 BLE Data Exchange Tutorial, we’ve explored crucial topics related to setting up data characteristics, maintaining persistent BLE connections, making your device discoverable or non-discoverable, and controlling LEDs with BLE commands. Armed with this knowledge, you’re well on your way to becoming a pro in Python and ESP-IDF development.

Stay tuned for the next part of our series, where we’ll dive even deeper into advanced BLE features and real-world applications.

Also, check out our other playlist Rasa ChatbotInternet of thingsDockerPython ProgrammingMQTTTech NewsESP-IDF etc.
Become a member of our social family on youtube here.
Stay tuned and Happy Learning. ✌🏻😃
Happy coding! ❤️🔥

Leave a Reply