esp-idf/examples/bluetooth/nimble/blecent/tutorial/blecent_walkthrough.md

50 KiB

BLE Central Example Walkthrough

Introduction

In this tutorial, we will explore the blecent example code provided by Espressif's ESP-IDF framework. The primary goal of the blecent example is to illustrate how a BLE Central device can interact with multiple BLE Peripheral devices in the vicinity. The Central device initiates the communication by scanning for nearby Peripherals and establishing connections with them. By the end of this tutorial, you will have a comprehensive understanding of how the blecent example code operates as a BLE Central application.

Includes

This example is located in the examples folder of the ESP-IDF under the blecent/main. The main.c file located in the main folder contains all the functionality that we are going to review. The header files contained in main.c are:

#include "esp_log.h"
#include "nvs_flash.h"
/* BLE */
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "console/console.h"
#include "services/gap/ble_svc_gap.h"
#include "blecent.h"

These includes are required for the FreeRTOS and underlying system components to run, including the logging functionality and a library to store data in non-volatile flash memory. We are interested in “nimble_port.h”, “nimble_port_freertos.h”, "ble_hs.h", “ble_svc_gap.h” and “blecent.h” which expose the BLE APIs required to implement this example.

  • nimble_port.h: Includes the declaration of functions required for the initialization of the nimble stack.
  • nimble_port_freertos.h: Initializes and enables nimble host task.
  • ble_hs.h: Defines the functionalities to handle the host event.
  • ble_svc_gap.h: Defines the macros for device name, and device appearance and declares the function to set them.
  • blecent.h: Provides necessary definitions and forward declarations for the blecent example's functionality, specifically for interacting with BLE services and characteristics related to Alert Notifications.

Main Entry Point

The program's entry point is the app_main() function:

void
app_main(void)
{
    int rc;
    /* Initialize NVS — it is used to store PHY calibration data */
    esp_err_t ret = nvs_flash_init();
    if  (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ret = nimble_port_init();
    if (ret != ESP_OK) {
        ESP_LOGE(tag, "Failed to init nimble %d ", ret);
        return;
    }

    /* Configure the host. */
    ble_hs_cfg.reset_cb = blecent_on_reset;
    ble_hs_cfg.sync_cb = blecent_on_sync;
    ble_hs_cfg.store_status_cb = ble_store_util_status_rr;

    /* Initialize data structures to track connected peers. */
    rc = peer_init(MYNEWT_VAL(BLE_MAX_CONNECTIONS), 64, 64, 64);
    assert(rc == 0);

    /* Set the default device name. */
    rc = ble_svc_gap_device_name_set("nimble-blecent");
    assert(rc == 0);

    /* XXX Need to have template for store */
    ble_store_config_init();

    nimble_port_freertos_init(blecent_host_task);

#if CONFIG_EXAMPLE_INIT_DEINIT_LOOP
    stack_init_deinit();
#endif

}

The main function starts by initializing the non-volatile storage library. This library allows us to save the key-value pairs in flash memory. nvs_flash_init() stores the PHY calibration data. In a Bluetooth Low Energy (BLE) device, cryptographic keys used for encryption and authentication are often stored in Non-Volatile Storage (NVS). BLE stores the peer keys, CCCD keys, peer records, etc on NVS. By storing these keys in NVS, the BLE device can quickly retrieve them when needed, without the need for time-consuming key generations.

esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

BT Controller and Stack Initialization

The main function calls nimble_port_init() to initialize BT Controller and nimble stack. This function initializes the BT controller by first creating its configuration structure named esp_bt_controller_config_t with default settings generated by the BT_CONTROLLER_INIT_CONFIG_DEFAULT() macro. It implements the Host Controller Interface (HCI) on the controller side, the Link Layer (LL), and the Physical Layer (PHY). The BT Controller is invisible to the user applications and deals with the lower layers of the BLE stack. The controller configuration includes setting the BT controller stack size, priority, and HCI baud rate. With the settings created, the BT controller is initialized and enabled with the esp_bt_controller_init() and esp_bt_controller_enable() functions:

esp_bt_controller_config_t config_opts = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&config_opts);

Next, the controller is enabled in BLE Mode.

ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);

The controller should be enabled in ESP_BT_MODE_BLE if you want to use the BLE mode.

There are four Bluetooth modes supported:

  1. ESP_BT_MODE_IDLE: Bluetooth not running
  2. ESP_BT_MODE_BLE: BLE mode
  3. ESP_BT_MODE_CLASSIC_BT: BT Classic mode
  4. ESP_BT_MODE_BTDM: Dual mode (BLE + BT Classic)

After the initialization of the BT controller, the nimble stack, which includes the common definitions and APIs for BLE, is initialized by using esp_nimble_init():

esp_err_t esp_nimble_init(void)
{
#if !SOC_ESP_NIMBLE_CONTROLLER
    /* Initialize the function pointers for OS porting */
    npl_freertos_funcs_init();

    npl_freertos_mempool_init();

    if(esp_nimble_hci_init() != ESP_OK) {
        ESP_LOGE(NIMBLE_PORT_LOG_TAG, "hci inits failed\n");
        return ESP_FAIL;
    }

    /* Initialize default event queue */
    ble_npl_eventq_init(&g_eventq_dflt);
    /* Initialize the global memory pool */
    os_mempool_module_init();
    os_msys_init();

#endif
    /* Initialize the host */
    ble_transport_hs_init();

    return ESP_OK;
}

The host is configured by setting up the callbacks for Stack-reset, Stack-sync, and Storage status.

ble_hs_cfg.reset_cb = blecent_on_reset;
ble_hs_cfg.sync_cb = blecent_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;

The main function invokes peer_init() to initialize memory pools to manage peer, service, characteristics, and descriptor objects in BLE.

rc = peer_init(MYNEWT_VAL(BLE_MAX_CONNECTIONS), 64, 64, 64);

The main function calls ble_svc_gap_device_name_set() to set the default device name.

rc = ble_svc_gap_device_name_set("nimble-blecent");

The main function calls ble_store_config_init() to configure the host by setting up the storage callbacks which handle the read, write, and deletion of security material.

/* XXX Need to have a template for store */
    ble_store_config_init();

The main function ends by creating a task where nimble will run using nimble_port_freertos_init(). This enables the nimble stack by using esp_nimble_enable().

nimble_port_freertos_init(blecent_host_task);

esp_nimble_enable() create a task where the nimble host will run. It is not strictly necessary to have a separate task for the nimble host, but to handle the default queue, it is easier to create a separate task.

CONFIG_EXAMPLE_INIT_DEINIT_LOOP

#if CONFIG_EXAMPLE_INIT_DEINIT_LOOP
/* This function showcases stack init and deinit procedure. */
static void stack_init_deinit(void)
{
    int rc;
    while(1) {

        vTaskDelay(1000);

        ESP_LOGI(tag, "Deinit host");

        rc = nimble_port_stop();
        if (rc == 0) {
            nimble_port_deinit();
        } else {
            ESP_LOGI(tag, "Nimble port stop failed, rc = %d", rc);
            break;
        }

        vTaskDelay(1000);

        ESP_LOGI(tag, "Init host");

        rc = nimble_port_init();
        if (rc != ESP_OK) {
            ESP_LOGI(tag, "Failed to init nimble %d ", rc);
            break;
        }

        nimble_port_freertos_init(blecent_host_task);

        ESP_LOGI(tag, "Waiting for 1 second");
    }
}
#endif

The stack_init_deinit function provides a demonstration of the initialization and deinitialization procedure for the NimBLE stack. It operates within a loop that showcases the following steps:

  • Delay: The function starts by introducing a delay of 1000 tick periods using the vTaskDelay function. This allows time for certain operations to complete before moving on.

  • Deinitialization: The function logs that the host deinitialization process is beginning. It calls nimble_port_stop to halt the NimBLE stack's operation. If the stop operation is successful (returning 0), the function then calls nimble_port_deinit to deinitialize the NimBLE stack. If the stop operation fails, an error message is logged, and the loop breaks.

  • Delay: Another 1000 tick periods delay is introduced before proceeding with initialization.

  • Initialization: The function logs that the host initialization process is starting. It calls nimble_port_init to initialize the NimBLE stack. If the initialization fails (returning anything other than ESP_OK), an error message is logged, and the loop breaks.

  • FreeRTOS Initialization: After successful NimBLE stack initialization, the function calls nimble_port_freertos_init and provides the blecent_host_task as a parameter. This step sets up the NimBLE stack to work within the FreeRTOS environment.

blecent_scan()

static void
blecent_scan(void)
{
    uint8_t own_addr_type;
    struct ble_gap_disc_params disc_params;
    int rc;

    /* Figure out address to use while advertising (no privacy for now) */
    rc = ble_hs_id_infer_auto(0, &own_addr_type);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
        return;
    }

    /* Tell the controller to filter duplicates; we don't want to process
     * repeated advertisements from the same device.
     */
    disc_params.filter_duplicates = 1;

    /**
     * Perform a passive scan.  I.e., don't send follow-up scan requests to
     * each advertiser.
     */
    disc_params.passive = 1;

    /* Use defaults for the rest of the parameters. */
    disc_params.itvl = 0;
    disc_params.window = 0;
    disc_params.filter_policy = 0;
    disc_params.limited = 0;

    rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
                      blecent_gap_event, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "Error initiating GAP discovery procedure; rc=%d\n",
                    rc);
    }
}

The function blecent_scan() initiates the General Discovery Procedure for scanning nearby BLE devices. The function starts by declaring several variables used in the scanning process. These variable include:

  • own_addr_type: A uint8_t variable that stores the type of address (public or random) that the device will use while scanning.
  • disc_params: A struct of type ble_gap_disc_params that holds the parameters for the GAP (Generic Access Profile) discovery procedure.

The function uses ble_hs_id_infer_auto() to determine the address type (public or random) that the device should use for scanning. The result is stored in the own_addr_type variable.

Configure Discovery Parameters: The function configures the disc_params struct with the following settings:

  • filter_duplicates: Set to 1, indicating that the controller should filter out duplicate advertisements from the same device. This reduces unnecessary processing of repeated advertisements.
  • passive: Set to 1, indicating that the scan will be a passive scan. In a passive scan, the scanning device only listens for advertisements without sending any follow-up scan requests to advertisers. It's used for general device discovery.

The function sets some other parameters in the disc_params struct to their default values:

  • itvl: The scan interval is set to 0, using the default value.
  • window: The scan window is set to 0, using the default value.
  • filter_policy: The filter policy is set to 0, using the default value.
  • limited: The limited discovery mode is set to 0, using the default value.

The function then calls ble_gap_disc() to initiate the BLE scanning procedure. It passes the following parameters:

  • own_addr_type: The address type to use for scanning (determined earlier).
  • BLE_HS_FOREVER: The duration for which the scan should continue (in this case, indefinitely).
  • &disc_params: A pointer to the ble_gap_disc_params struct containing the scan parameters.
  • blecent_gap_event: The callback function to handle the scan events (such as receiving advertisements from nearby devices).
  • NULL: The argument for the callback context, which is not used in this example.

If an error occurs during the initiation of the scanning procedure, the function prints an error message.

blecent_gap_event

The function blecent_gap_event is in responsible of managing various GAP (Generic Access Profile) events that arise during the BLE communication.

The function employs a switch statement to manage diverse types of GAP events that can be received.

  • BLE_GAP_EVENT_DISC: This case is activated when a new advertisement report is detected during scanning. The function extracts the advertisement data using ble_hs_adv_parse_fields and then displays the advertisement fields using the print_adv_fields function. It subsequently verifies if the discovered device is of interest and attempts to establish a connection with it.

  • BLE_GAP_EVENT_CONNECT: This case is triggered when a new connection is established or when a connection attempt fails. If the Connection was established then the connection descriptor is initiated using the ble_gap_conn_find() method else advertisement is resumed. If the connection is successful, it displays the connection descriptor through print_conn_desc and stores the peer information. Additionally, it handles optional features such as BLE power control, vendor-specific commands, and security initiation. In the event of a connection attempt failure, it resumes scanning.

  • BLE_GAP_EVENT_DISCONNECT: This case is activated when a connection is terminated. It prints the reason for disconnection and the connection descriptor before removing information about the peer and resuming scanning.

  • BLE_GAP_EVENT_DISC_COMPLETE: This case is triggered upon the completion of the GAP discovery process. It displays the reason for the discovery's completion.

  • BLE_GAP_EVENT_ENC_CHANGE: This case is activated when encryption is enabled or disabled for a connection. It displays the status of the encryption change and the connection descriptor. If encryption is enabled (when CONFIG_EXAMPLE_ENCRYPTION is defined), it initiates service discovery.

  • BLE_GAP_EVENT_NOTIFY_RX: This case is triggered when the Central device receives a notification or indication from the Peripheral device. It displays information about the received data.

  • BLE_GAP_EVENT_MTU: This case is activated when the Maximum Transmission Unit (MTU) is updated for a connection. It prints the new MTU value and related information.

  • BLE_GAP_EVENT_REPEAT_PAIRING: This case is triggered when the Peripheral device attempts to establish a new secure link with the Central despite an existing bond. The app prioritizes convenience over security and deletes the old bond, accepting the new link.

So, event handler function effectively manages various GAP events, handles connection-related tasks, initiates security procedures, performs service discovery, and accommodates optional features based on the configuration settings.

blecent_should_connect

static int
blecent_should_connect(const struct ble_gap_disc_desc *disc)
{
    struct ble_hs_adv_fields fields;
    int rc;
    int i;

    /* The device has to be advertising connectability. */
    if (disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_ADV_IND &&
            disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_DIR_IND) {

        return 0;
    }

    rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
    if (rc != 0) {
        return 0;
    }

    if (strlen(CONFIG_EXAMPLE_PEER_ADDR) && (strncmp(CONFIG_EXAMPLE_PEER_ADDR, "ADDR_ANY", strlen("ADDR_ANY")) != 0)) {
        ESP_LOGI(tag, "Peer address from menuconfig: %s", CONFIG_EXAMPLE_PEER_ADDR);
        /* Convert string to address */
        sscanf(CONFIG_EXAMPLE_PEER_ADDR, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
               &peer_addr[5], &peer_addr[4], &peer_addr[3],
               &peer_addr[2], &peer_addr[1], &peer_addr[0]);
        if (memcmp(peer_addr, disc->addr.val, sizeof(disc->addr.val)) != 0) {
            return 0;
        }
    }

    /* The device has to advertise support for the Alert Notification
     * service (0x1811).
     */
    for (i = 0; i < fields.num_uuids16; i++) {
        if (ble_uuid_u16(&fields.uuids16[i].u) == BLECENT_SVC_ALERT_UUID) {
            return 1;
        }
    }

    return 0;
}

This function is responsible for determining whether the Central device should establish a connection with a discovered Peripheral device based on the advertisement data received during scanning. Let's break down the code step by step:

  • Local Variables: The function declares several local variables, including fields of type struct ble_hs_adv_fields to hold the parsed advertisement data, rc to store the return code of function calls, and i for loop iteration.

  • Checking Advertisement Event Type: The function begins by checking the event type of the advertisement report stored in the disc structure. It verifies whether the event type indicates that the device is advertising its connectability. Specifically, it checks for two event types: BLE_HCI_ADV_RPT_EVTYPE_ADV_IND (Connectable Undirected Advertising) and BLE_HCI_ADV_RPT_EVTYPE_DIR_IND (Directed Advertising). If the event type is neither of these, it means the device is not connectable, and the function returns 0, indicating that the Central device should not attempt to connect.

  • Parsing Advertisement Data: The function then attempts to parse the advertisement data using the ble_hs_adv_parse_fields function. The advertisement data is stored in the disc->data buffer, and its length is specified by disc->length_data. The parsed data is then stored in the fields struct.

  • Checking Peer Address: This section of the code handles an optional feature where the Central device can specify a specific peer address it wants to connect to. It checks whether the configuration option CONFIG_EXAMPLE_PEER_ADDR is set and not equal to ADDR_ANY (a special value). If this configuration is set, it means the Central device wants to connect to a specific device based on its address. The function converts the specified peer address from a string representation to a byte array peer_addr using the sscanf function. If the received device's address (in disc->addr.val) does not match the specified peer address, the function returns 0, indicating that the Central device should not connect to this device.

  • Checking for Alert Notification Service: Next, the function examines the parsed advertisement data to check for the presence of the Alert Notification service (UUID: 0x1811). The Alert Notification service is used to notify the Central device about alerts or notifications from the Peripheral device. If the advertised service is found in the advertisement data, the function returns 1, indicating that the Central device should proceed with connecting to this device.

  • Returning 0: If none of the conditions mentioned above are met (i.e., the event type is not connectable, the peer address check is not enabled, and the Alert Notification service is not advertised), the function returns 0, signaling that the Central device should not establish a connection with this device.

blecent_connect_if_interesting

/**
 * Connects to the sender of the specified advertisement of it looks
 * interesting.  A device is "interesting" if it advertises connectability and
 * support for the Alert Notification service.
 */
static void
blecent_connect_if_interesting(void *disc)
{
    uint8_t own_addr_type;
    int rc;
    ble_addr_t *addr;

    /* Don't do anything if we don't care about this advertiser. */
#if CONFIG_EXAMPLE_EXTENDED_ADV
    if (!ext_blecent_should_connect((struct ble_gap_ext_disc_desc *)disc)) {
        return;
    }
#else
    if (!blecent_should_connect((struct ble_gap_disc_desc *)disc)) {
        return;
    }
#endif

    /* Scanning must be stopped before a connection can be initiated. */
    rc = ble_gap_disc_cancel();
    if (rc != 0) {
        MODLOG_DFLT(DEBUG, "Failed to cancel scan; rc=%d\n", rc);
        return;
    }

    /* Figure out address to use for connect (no privacy for now) */
    rc = ble_hs_id_infer_auto(0, &own_addr_type);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
        return;
    }

    /* Try to connect the the advertiser.  Allow 30 seconds (30000 ms) for
     * timeout.
     */
#if CONFIG_EXAMPLE_EXTENDED_ADV
    addr = &((struct ble_gap_ext_disc_desc *)disc)->addr;
#else
    addr = &((struct ble_gap_disc_desc *)disc)->addr;
#endif

    rc = ble_gap_connect(own_addr_type, addr, 30000, NULL,
                         blecent_gap_event, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "Error: Failed to connect to device; addr_type=%d "
                    "addr=%s; rc=%d\n",
                    addr->type, addr_str(addr->val), rc);
        return;
    }
}

Function blecent_connect_if_interesting is to handle the connection process to a BLE Peripheral device if certain criteria are met, making the device interesting for the Central to connect to. Let's summarize the code step by step:

  • Local Variables: The function declares local variables, including own_addr_type of type uint8_t to store the own address type of the Central device, rc to store the return code of function calls, and addr of type ble_addr_t* to store the address of the discovered advertiser.

  • Checking Device Interest: The function checks if the advertiser meets the criteria for an interesting device to connect to. The criteria are evaluated using either blecent_should_connect or ext_blecent_should_connect, depending on the presence of the CONFIG_EXAMPLE_EXTENDED_ADV configuration. If the advertiser doesn't meet the criteria, the function returns without attempting a connection.

  • Stopping Scanning: Before initiating a connection, the function cancels any ongoing BLE scanning using ble_gap_disc_cancel(). Scanning must be halted to establish a connection.

  • Determining Address Type: The function infers the address type of the Central device for the connection by calling ble_hs_id_infer_auto, and the inferred type is stored in own_addr_type. If there's an error during address type determination, the function exits without attempting a connection.

  • Initiating Connection: With scanning stopped and the address type determined, the function is ready to initiate the connection. It retrieves the advertiser's address from the provided disc argument (of type struct ble_gap_disc_desc* or struct ble_gap_ext_disc_desc*, depending on configuration).

  • Connecting to the Advertiser: The function uses ble_gap_connect to establish the connection with the advertiser device. It provides the Central's own address type, the advertiser's address, and sets a timeout of 30 seconds (30000 ms) for the connection attempt. Additionally, it designates the blecent_gap_event function as the event callback to manage connection-related events.

  • Error Handling: If the connection attempt fails (rc != 0), the function logs an error message with details about the failed connection attempt and returns.

blecent_on_disc_complete

/**
 * Called when service discovery of the specified peer has completed.
 */
static void
blecent_on_disc_complete(const struct peer *peer, int status, void *arg)
{

    if (status != 0) {
        /* Service discovery failed.  Terminate the connection. */
        MODLOG_DFLT(ERROR, "Error: Service discovery failed; status=%d "
                    "conn_handle=%d\n", status, peer->conn_handle);
        ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
        return;
    }

    /* Service discovery has completed successfully.  Now we have a complete
     * list of services, characteristics, and descriptors that the peer
     * supports.
     */
    MODLOG_DFLT(INFO, "Service discovery complete; status=%d "
                "conn_handle=%d\n", status, peer->conn_handle);

    /* Now perform three GATT procedures against the peer: read,
     * write, and subscribe to notifications for the ANS service.
     */
    blecent_read_write_subscribe(peer);
}

The function blecent_on_disc_complete is essential for managing service discovery completion and GATT procedures. It's invoked after service discovery on a peer device concludes. The function takes three arguments: a pointer to the peer structure (containing peer information), an integer status indicating success or failure, and a generic arg pointer for potential extra data (not used here).

  • Handling Discovery Status: Upon initiation, the function checks the status of the service discovery operation. Non-zero status indicates failure. In such cases, an error message is logged using MODLOG_DFLT(ERROR, ...) and the connection with the peer is terminated via ble_gap_terminate(), assigning the error reason code BLE_ERR_REM_USER_CONN_TERM. This ensures a proper connection termination due to the unsuccessful service discovery.

  • Success Path: For a status of zero, denoting successful service discovery, a log message is generated, signifying the completion of service discovery. It includes the status and the peer's connection handle (conn_handle).

  • GATT Procedures: Upon successful service discovery, the function proceeds with three GATT (Generic Attribute Profile) procedures performed on the peer. These encompass reading, writing, and subscribing to notifications for the Alert Notification Service (ANS). These specific GATT actions are typically implemented within the blecent_read_write_subscribe() function.

blecent_read_write_subscribe

/**
 * Performs three GATT operations against the specified peer:
 * 1. Reads the ANS Supported New Alert Category characteristic.
 * 2. After read is completed, writes the ANS Alert Notification Control Point characteristic.
 * 3. After write is completed, subscribes to notifications for the ANS Unread Alert Status
 *    characteristic.
 *
 * If the peer does not support a required service, characteristic, or
 * descriptor, then the peer lied when it claimed support for the alert
 * notification service!  When this happens, or if a GATT procedure fails,
 * this function immediately terminates the connection.
 */
static void
blecent_read_write_subscribe(const struct peer *peer)
{
    const struct peer_chr *chr;
    int rc;

    /* Read the supported-new-alert-category characteristic. */
    chr = peer_chr_find_uuid(peer,
                             BLE_UUID16_DECLARE(BLECENT_SVC_ALERT_UUID),
                             BLE_UUID16_DECLARE(BLECENT_CHR_SUP_NEW_ALERT_CAT_UUID));
    if (chr == NULL) {
        MODLOG_DFLT(ERROR, "Error: Peer doesn't support the Supported New "
                    "Alert Category characteristic\n");
        goto err;
    }

    rc = ble_gattc_read(peer->conn_handle, chr->chr.val_handle,
                        blecent_on_read, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "Error: Failed to read characteristic; rc=%d\n",
                    rc);
        goto err;
    }

    return;
err:
    /* Terminate the connection. */
    ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

This function executes the following sequence of actions:

  • Reading Supported New Alert Category Characteristic: It initiates by attempting to read the "Supported New Alert Category" characteristic within the "Alert Notification Service" (ANS). To accomplish this, it employs the peer_chr_find_uuid function, which searches for the specified characteristic using its UUID. If the characteristic isn't located within the peer's attributes, the function logs an error message and discontinues the connection.

  • Carrying Out Characteristic Read: If the "Supported New Alert Category" characteristic is successfully identified, the function endeavors to read its value utilizing the ble_gattc_read function. In the event of a failed read operation, an error is logged, and the connection is terminated.

blecent_on_custom_read

/**
 * Application Callback. Called when the custom subscribable chatacteristic
 * in the remote GATT server is read.
 * Expect to get the recently written data.
 **/
static int
blecent_on_custom_read(uint16_t conn_handle,
                       const struct ble_gatt_error *error,
                       struct ble_gatt_attr *attr,
                       void *arg)
{
    MODLOG_DFLT(INFO,
                "Read complete for the subscribable characteristic; "
                "status=%d conn_handle=%d", error->status, conn_handle);
    if (error->status == 0) {
        MODLOG_DFLT(INFO, " attr_handle=%d value=", attr->handle);
        print_mbuf(attr->om);
    }
    MODLOG_DFLT(INFO, "\n");

    return 0;
}

blecent_on_custom_read function is triggered when data is read from a custom subscribable characteristic in a distant Generic Attribute Profile (GATT) server.

  • Function Definition: The callback function blecent_on_custom_read is defined with specific parameters. These parameters are supplied by the BLE stack when the callback is invoked:

    • uint16_t conn_handle: The BLE connection handle associated with the read operation.
    • const struct ble_gatt_error *error: A pointer to a structure that holds error information, if any, during the read.
    • struct ble_gatt_attr *attr: A pointer to the read attribute, encompassing its handle and data.
    • void *arg: An optional argument that can be included when registering the callback (not used in this example).
  • Logging Usage: The function utilizes the MODLOG_DFLT macro to log insights regarding the read operation. This includes details such as the connection handle, operation status (via error->status), and attribute handle (via attr->handle).

  • Successful Read Check: The code assesses the read operation's success by inspecting the status stored in error->status. A status of 0 indicates a successful read with no errors.

  • Value Logging: If the read operation succeeds, the function logs the attribute's value. This is achieved by employing the print_mbuf function, which prints the content of attr->om (mbuf) data, representing the read attribute's value.

blecent_on_custom_write

/**
 * Application Callback. Called when the custom subscribable characteristic
 * in the remote GATT server is written to.
 * Client has previously subscribed to this characeteristic,
 * so expect a notification from the server.
 **/
static int
blecent_on_custom_write(uint16_t conn_handle,
                        const struct ble_gatt_error *error,
                        struct ble_gatt_attr *attr,
                        void *arg)
{
    const struct peer_chr *chr;
    const struct peer *peer;
    int rc;

    MODLOG_DFLT(INFO,
                "Write to the custom subscribable characteristic complete; "
                "status=%d conn_handle=%d attr_handle=%d\n",
                error->status, conn_handle, attr->handle);

    peer = peer_find(conn_handle);
    chr = peer_chr_find_uuid(peer,
                             remote_svc_uuid,
                             remote_chr_uuid);
    if (chr == NULL) {
        MODLOG_DFLT(ERROR,
                    "Error: Peer doesn't have the custom subscribable characteristic\n");
        goto err;
    }

    /*** Performs a read on the characteristic, the result is handled in blecent_on_new_read callback ***/
    rc = ble_gattc_read(conn_handle, chr->chr.val_handle,
                        blecent_on_custom_read, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR,
                    "Error: Failed to read the custom subscribable characteristic; "
                    "rc=%d\n", rc);
        goto err;
    }

    return 0;
err:
    /* Terminate the connection */
    return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

blecent_on_custom_write function is invoked when a remote GATT server's (Generic Attribute Profile) custom subscribable characteristic is successfully written to by the Central device.

  • Parameters:

    • conn_handle: BLE connection handle where the write operation took place.
    • error: Contains write operation details, including the status code.
    • attr: Represents the written attribute (characteristic).
    • arg: Optional user-defined argument (unused here).
  • Logging: This function logs write operation completion, displaying write status, connection handle, and attribute handle.

  • Locating Peer and Characteristic:

    • The function seeks the relevant peer using the connection handle through peer_find.
    • It also searches for a chr (characteristic) within the peer's attributes using remote_svc_uuid and remote_chr_uuid.
    • If the characteristic isn't found, an error message is logged, and the function proceeds to the error handling section.
  • Initiating Characteristic Read:

    • Upon locating the characteristic, the function begins a read operation using ble_gattc_read.
    • It supplies connection handle, characteristic's value handle (chr->chr.val_handle), read handler (blecent_on_custom_read), and no user-defined argument (NULL).
  • Error Handling:

    • If the read operation faces an issue (non-zero return value), an error message is logged.
    • The function then navigates to the error handling section (err), ending the BLE connection with the remote device via ble_gap_terminate.
    • The termination rationale is marked as BLE_ERR_REM_USER_CONN_TERM.
  • Return Value:

    • Successful write and subsequent read return 0.
    • Write or read errors lead to connection termination, with the result of ble_gap_terminate.

blecent_on_custom_subscribe

/**
 * Application Callback. Called when the custom subscribable characteristic
 * is subscribed to.
 **/
static int
blecent_on_custom_subscribe(uint16_t conn_handle,
                            const struct ble_gatt_error *error,
                            struct ble_gatt_attr *attr,
                            void *arg)
{
    const struct peer_chr *chr;
    uint8_t value;
    int rc;
    const struct peer *peer;

    MODLOG_DFLT(INFO,
                "Subscribe to the custom subscribable characteristic complete; "
                "status=%d conn_handle=%d", error->status, conn_handle);

    if (error->status == 0) {
        MODLOG_DFLT(INFO, " attr_handle=%d value=", attr->handle);
        print_mbuf(attr->om);
    }
    MODLOG_DFLT(INFO, "\n");

    peer = peer_find(conn_handle);
    chr = peer_chr_find_uuid(peer,
                             remote_svc_uuid,
                             remote_chr_uuid);
    if (chr == NULL) {
        MODLOG_DFLT(ERROR, "Error: Peer doesn't have the subscribable characteristic\n");
        goto err;
    }

    /* Write 1 byte to the new characteristic to test if it notifies after subscribing */
    value = 0x19;
    rc = ble_gattc_write_flat(conn_handle, chr->chr.val_handle,
                              &value, sizeof(value), blecent_on_custom_write, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR,
                    "Error: Failed to write to the subscribable characteristic; "
                    "rc=%d\n", rc);
        goto err;
    }

    return 0;
err:
    /* Terminate the connection */
    return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

blecent_on_custom_subscribe function is triggered when a custom subscribable characteristic is subscribed to. Here's a concise breakdown of the code's key aspects:

  • Function and Parameters: The function receives parameters including the connection handle conn_handle, an error structure error for status details, a GATT attribute structure attr representing the subscribed attribute, and a generic argument arg.

  • Subscription Log: Information about the subscription process is logged, including the subscription status, connection handle, and attribute handle. If the subscription is successful (error->status == 0), the subscribed attribute's value is printed using the print_mbuf function.

  • Peer and Characteristic Search: The function searches for a peer structure using conn_handle and attempts to locate a specific characteristic within that peer. The desired characteristic is identified by remote_svc_uuid and remote_chr_uuid.

  • Characteristic Validation: If the characteristic isn't found within the peer, an error message is logged to indicate the absence of the expected subscribable characteristic.

  • Characteristic Write: Assuming the characteristic is found, the function tries to write a single byte (0x19) to it. This test is performed to determine if the characteristic will notify the Central device after subscribing. The ble_gattc_write_flat function handles this operation.

  • Write Outcome Check: In case the write operation fails (rc != 0), an error message is logged to indicate the failure in writing to the subscribable characteristic.

  • Return Value: The function returns 0 to signal the completion of processing. In case of errors, connection termination is initiated within the error-handling segments.

blecent_custom_gatt_operations

/**
 * Performs 3 operations on the remote GATT server.
 * 1. Subscribes to a characteristic by writing 0x10 to it's CCCD.
 * 2. Writes to the characteristic and expect a notification from remote.
 * 3. Reads the characteristic and expect to get the recently written information.
 **/
static void
blecent_custom_gatt_operations(const struct peer* peer)
{
    const struct peer_dsc *dsc;
    int rc;
    uint8_t value[2];

    dsc = peer_dsc_find_uuid(peer,
                             remote_svc_uuid,
                             remote_chr_uuid,
                             BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16));
    if (dsc == NULL) {
        MODLOG_DFLT(ERROR, "Error: Peer lacks a CCCD for the subscribable characterstic\n");
        goto err;
    }

    /*** Write 0x00 and 0x01 (The subscription code) to the CCCD ***/
    value[0] = 1;
    value[1] = 0;
    rc = ble_gattc_write_flat(peer->conn_handle, dsc->dsc.handle,
                              value, sizeof(value), blecent_on_custom_subscribe, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR,
                    "Error: Failed to subscribe to the subscribable characteristic; "
                    "rc=%d\n", rc);
        goto err;
    }

    return;
err:
    /* Terminate the connection */
    ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

blecent_custom_gatt_operations function demonstrates various GATT interactions, including subscribing to a characteristic, writing to it, and reading from it.

  • Finding CCCD Descriptor: The function begins by searching for a specific Client Characteristic Configuration Descriptor (CCCD) associated with a subscribable characteristic. This search employs the peer_dsc_find_uuid function using provided UUIDs.

  • Checking CCCD Availability: If the CCCD is not found (dsc is NULL), an error message is logged, and the connection is terminated.

  • Subscribing to Characteristic: The function assembles a two-byte value array with value[0] indicating subscription (set to 1) and value[1] set to 0. It utilizes ble_gattc_write_flat to write this value to the CCCD, subscribing to characteristic notifications. If the write operation fails (returns non-zero), an error message is logged, and the connection is terminated.

  • Successful Execution: If the subscription write succeeds, the function returns without further actions.

  • Error Handling: In case of any failures, the code proceeds to the err label, terminating the BLE connection using ble_gap_terminate with the relevant error code.

blecent_on_subscribe

/**
 * Application callback.  Called when the attempt to subscribe to notifications
 * for the ANS Unread Alert Status characteristic has completed.
 */
static int
blecent_on_subscribe(uint16_t conn_handle,
                     const struct ble_gatt_error *error,
                     struct ble_gatt_attr *attr,
                     void *arg)
{
    struct peer *peer;

    MODLOG_DFLT(INFO, "Subscribe complete; status=%d conn_handle=%d "
                "attr_handle=%d\n",
                error->status, conn_handle, attr->handle);

    peer = peer_find(conn_handle);
    if (peer == NULL) {
        MODLOG_DFLT(ERROR, "Error in finding peer, aborting...");
        ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
    }
    /* Subscribe to, write to, and read the custom characteristic*/
    blecent_custom_gatt_operations(peer);

    return 0;
}

blecent_on_subscribe function's purpose is to handle the completion of subscribing to notifications for a specific characteristic in a BLE Peripheral device.

  • Parameters:

    • conn_handle: Corresponds to the BLE connection's handle.
    • error: Points to a struct ble_gatt_error indicating the outcome of the subscription attempt.
    • attr: Points to the GATT attribute linked with the subscribed characteristic.
    • arg: Represents a user-defined argument passed into the callback.
  • Logging: The function logs subscription completion details utilizing MODLOG_DFLT(INFO, ...). It reports subscription status, connection handle, and attribute handle.

  • Peer Identification: The function endeavors to locate the associated peer (Peripheral device) linked with the provided conn_handle. If the peer can't be located, it logs an error message and terminates the BLE connection with the peripheral using ble_gap_terminate, coupled with error code BLE_ERR_REM_USER_CONN_TERM.

  • Custom GATT Operations: Presuming the peer is successfully identified, the code implies the existence of a function named blecent_custom_gatt_operations. This function likely performs extra custom GATT (Generic Attribute Profile) operations on the peripheral. Such operations could encompass writing to or reading from custom characteristics or attributes.

blecent_on_write

/**
 * Application callback.  Called when the write to the ANS Alert Notification
 * Control Point characteristic has completed.
 */
static int
blecent_on_write(uint16_t conn_handle,
                 const struct ble_gatt_error *error,
                 struct ble_gatt_attr *attr,
                 void *arg)
{
    MODLOG_DFLT(INFO,
                "Write complete; status=%d conn_handle=%d attr_handle=%d\n",
                error->status, conn_handle, attr->handle);

    /* Subscribe to notifications for the Unread Alert Status characteristic.
     * A central enables notifications by writing two bytes (1, 0) to the
     * characteristic's client-characteristic-configuration-descriptor (CCCD).
     */
    const struct peer_dsc *dsc;
    uint8_t value[2];
    int rc;
    const struct peer *peer = peer_find(conn_handle);

    dsc = peer_dsc_find_uuid(peer,
                             BLE_UUID16_DECLARE(BLECENT_SVC_ALERT_UUID),
                             BLE_UUID16_DECLARE(BLECENT_CHR_UNR_ALERT_STAT_UUID),
                             BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16));
    if (dsc == NULL) {
        MODLOG_DFLT(ERROR, "Error: Peer lacks a CCCD for the Unread Alert "
                    "Status characteristic\n");
        goto err;
    }

    value[0] = 1;
    value[1] = 0;
    rc = ble_gattc_write_flat(conn_handle, dsc->dsc.handle,
                              value, sizeof value, blecent_on_subscribe, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "Error: Failed to subscribe to characteristic; "
                    "rc=%d\n", rc);
        goto err;
    }

    return 0;
err:
    /* Terminate the connection. */
    return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

This function gets triggered upon the completion of a write operation to the ANS (Alert Notification Service) Alert Notification Control Point characteristic. Here's a breakdown of the code's components:

  • Subscription to Notifications: The primary objective of the function is to subscribe to notifications for the Unread Alert Status characteristic. Notifications are enabled by writing two bytes (1, 0) to the Client Characteristic Configuration Descriptor (CCCD) of the characteristic.

  • Locating CCCD Descriptor: The peer_dsc_find_uuid function is employed to find the CCCD descriptor associated with the Unread Alert Status characteristic. This entails locating a specific descriptor with a particular UUID linked to CCCD. In case the descriptor isn't found, an error message is logged, and the code proceeds to the err label.

  • Preparing Subscription Value: The code prepares the subscription value as an array of two bytes: [1, 0]. This value's purpose is to enable notifications for the characteristic.

  • Initiating Write Operation: The function then uses ble_gattc_write_flat to execute a flat write operation. It writes the subscription value to the CCCD descriptor's handle. The blecent_on_subscribe callback function is designated to manage the completion of the subscription write operation.

  • Error Handling for Write: If the write operation encounters a failure (as indicated by a non-zero return value from ble_gattc_write_flat), an error message is logged, and the code proceeds to the err label.

blecent_on_read

/**
 * Application callback.  Called when the read of the ANS Supported New Alert
 * Category characteristic has completed.
 */
static int
blecent_on_read(uint16_t conn_handle,
                const struct ble_gatt_error *error,
                struct ble_gatt_attr *attr,
                void *arg)
{
    MODLOG_DFLT(INFO, "Read complete; status=%d conn_handle=%d", error->status,
                conn_handle);
    if (error->status == 0) {
        MODLOG_DFLT(INFO, " attr_handle=%d value=", attr->handle);
        print_mbuf(attr->om);
    }
    MODLOG_DFLT(INFO, "\n");

    /* Write two bytes (99, 100) to the alert-notification-control-point
     * characteristic.
     */
    const struct peer_chr *chr;
    uint8_t value[2];
    int rc;
    const struct peer *peer = peer_find(conn_handle);

    chr = peer_chr_find_uuid(peer,
                             BLE_UUID16_DECLARE(BLECENT_SVC_ALERT_UUID),
                             BLE_UUID16_DECLARE(BLECENT_CHR_ALERT_NOT_CTRL_PT));
    if (chr == NULL) {
        MODLOG_DFLT(ERROR, "Error: Peer doesn't support the Alert "
                    "Notification Control Point characteristic\n");
        goto err;
    }

    value[0] = 99;
    value[1] = 100;
    rc = ble_gattc_write_flat(conn_handle, chr->chr.val_handle,
                              value, sizeof value, blecent_on_write, NULL);
    if (rc != 0) {
        MODLOG_DFLT(ERROR, "Error: Failed to write characteristic; rc=%d\n",
                    rc);
        goto err;
    }

    return 0;
err:
    /* Terminate the connection. */
    return ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

The blecent_on_read function is called when a read operation for a certain characteristic is completed.

  • Logging Read Status: The function logs information regarding the read operation, including its status and the connection handle. If the read operation is successful (status equals 0), it also logs the attribute handle and the value read from the characteristic using the print_mbuf function.

  • Writing to a Characteristic: The code proceeds to write two bytes (99 and 100) to the Alert Notification Control Point characteristic. It locates the characteristic using peer_chr_find_uuid based on its UUID values. If the characteristic is not found (i.e., chr is NULL), it logs an error message and proceeds to the err label.

  • Write Operation: When the Alert Notification Control Point characteristic is found, the code prepares the data to be written (an array called value containing two bytes) and employs ble_gattc_write_flat to execute the write operation. The completion of the write operation triggers the blecent_on_write callback function. If the write operation encounters an error (return code rc is non-zero), the code logs an error message and proceeds to the err label.

  • Handling Errors: If an error occurs (such as not finding the characteristic or a failed write operation), the code terminates the connection using ble_gap_terminate with the reason code BLE_ERR_REM_USER_CONN_TERM.

In essence, this callback function handles reading a characteristic's value, logs the result, and potentially performs a subsequent write operation to another characteristic.

Conclusion

In this example code, we've explored how to initiate BLE scanning, discover nearby Peripheral devices, establish connections, and interact with their services and characteristics.

Key Takeaways:

  • Initialization: The code initializes the BLE stack and sets up event handlers.
  • Scanning: The Central device scans for nearby BLE Peripheral devices and filters interesting devices based on criteria like service UUIDs or peer addresses.
  • Connection Management: When a suitable Peripheral is found, the Central initiates a connection and handles connection events.
  • Service Discovery: After connection, the Central performs service discovery to identify the services and characteristics supported by the Peripheral.
  • GATT Procedures: The code showcases read, write, and subscription procedures with the discovered services and characteristics.
  • Error Handling: Proper error handling ensures termination of connections in case of failures.