# BLE HeartRate Example Walkthrough ## Introduction In this tutorial, we will explore the ble heartrate example code provided by Espressif's ESP-IDF framework. It advertises heart rate data, simulating heartbeats, and allows clients to subscribe to updates. The code handles BLE events such as connection establishment, disconnection, and subscriptions, making it a foundational component for building BLE-based heart rate monitoring applications on ESP32 devices. In this example, a GATT server is created to show how a standard heart rate measuring service works. When the notifications are turned on, it replicates the measurement of the heart rate and notifies the client. ## Includes This example is located in the examples folder of the ESP-IDF under the [blehr/main](../main). The [main.c](../main/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](../main/main.c) are: ```c #include "esp_log.h" #include "nvs_flash.h" #include "freertos/FreeRTOSConfig.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 "blehr_sens.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. * `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. * `blehr_sens.h`: Defines the UUIDs, handles, and functions for implementing a Heart Rate Sensor (HRS) service in a BLE application. ## Main Entry Point The program's entry point is the app_main() function: ```c 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) { MODLOG_DFLT(ERROR, "Failed to init nimble %d \n", ret); return; } /* Initialize the NimBLE host configuration */ ble_hs_cfg.sync_cb = blehr_on_sync; ble_hs_cfg.reset_cb = blehr_on_reset; /* name, period/time, auto reload, timer ID, callback */ blehr_tx_timer = xTimerCreate("blehr_tx_timer", pdMS_TO_TICKS(1000), pdTRUE, (void *)0, blehr_tx_hrate); rc = gatt_svr_init(); assert(rc == 0); /* Set the default device name */ rc = ble_svc_gap_device_name_set(device_name); assert(rc == 0); /* Start the task */ nimble_port_freertos_init(blehr_host_task); } ``` 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. ```c 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: ```c 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. ```c 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()`: The host is configured by setting up the callbacks for Stack-reset and Stack-sync. ```c ble_hs_cfg.sync_cb = blehr_on_sync; ble_hs_cfg.reset_cb = blehr_on_reset; ``` The main function creates a timer named `blehr_tx_timer` with a periodic interval of 1000 tick periods. It's set to automatically reload itself and is associated with the callback function `blehr_tx_hrate`. This timer is typically used to periodically trigger the `blehr_tx_hrate` function, which simulates heart rate updates and sends notifications to BLE clients. ```c blehr_tx_timer = xTimerCreate("blehr_tx_timer", pdMS_TO_TICKS(1000), pdTRUE, (void *)0, blehr_tx_hrate); ``` ## GATT Server Init The main function invokes `gatt_svr_init` to initializes the Generic Attribute (GATT) server. It initializes the Generic Access Profile (GAP) service and the Generic Attribute (GATT) service. It calls the `ble_gatts_count_cfg` function to count the number of GATT services configured in the `gatt_svr_svcs` array. ```c int gatt_svr_init(void) { int rc; ble_svc_gap_init(); ble_svc_gatt_init(); rc = ble_gatts_count_cfg(gatt_svr_svcs); if (rc != 0) { return rc; } rc = ble_gatts_add_svcs(gatt_svr_svcs); if (rc != 0) { return rc; } return 0; } ``` The main function calls `ble_svc_gap_device_name_set()` to set the default device name. ```c rc = ble_svc_gap_device_name_set(device_name); ``` 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()`. ```c nimble_port_freertos_init(blehr_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. ## BLE Heart Rate Advertisement `blehr_advertise` function, responsible for initiating BLE advertising of the heart rate sensor device. It uses the following function to start an advertisement. * `blehr_advertise` start by creating the instances of structures `ble_gap_adv_params` and `ble_hs_adv_fields`. Advertising parameters such as connecting modes, discoverable modes, advertising intervals, channel map advertising filter policy, and high duty cycle for directed advertising are defined in these structures. * `ble_hs_adv_fields` provides a structure to store advertisement data in a BLE application. It contains various data members, such as flags (indicating advertisement type and other general information), advertising transmit power, device name, and 16-bit service UUIDs (such as alert notifications), etc. * Then, it sets these advertisement data fields and starts the BLE advertising process in general discoverable and undirected connectable modes, allowing other devices to discover and potentially connect to this heart rate sensor device. If any errors occur during the setup or advertising process, it logs an error message. ```c static void blehr_advertise(void) { struct ble_gap_adv_params adv_params; struct ble_hs_adv_fields fields; int rc; /* * Set the advertisement data included in our advertisements: * o Flags (indicates advertisement type and other general info) * o Advertising tx power * o Device name */ memset(&fields, 0, sizeof(fields)); /* * Advertise two flags: * o Discoverability in forthcoming advertisement (general) * o BLE-only (BR/EDR unsupported) */ fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; /* * Indicate that the TX power level field should be included; have the * stack fill this value automatically. This is done by assigning the * special value BLE_HS_ADV_TX_PWR_LVL_AUTO. */ fields.tx_pwr_lvl_is_present = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; fields.name = (uint8_t *)device_name; fields.name_len = strlen(device_name); fields.name_is_complete = 1; rc = ble_gap_adv_set_fields(&fields); if (rc != 0) { MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc); return; } /* Begin advertising */ memset(&adv_params, 0, sizeof(adv_params)); adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; rc = ble_gap_adv_start(blehr_addr_type, NULL, BLE_HS_FOREVER, &adv_params, blehr_gap_event, NULL); if (rc != 0) { MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc); return; } } ``` ## GAP Events in blehr The function `blehr_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. 1. `BLE_GAP_EVENT_CONNECT`: - This event is triggered when a new BLE connection is established or a connection attempt fails. - It logs the connection status (established or failed) and handles connection failure by resuming advertising. - Stores the connection handle for future reference. 2. `BLE_GAP_EVENT_DISCONNECT`: - Triggered when a BLE connection is terminated, and it logs the disconnection reason. - Resumes advertising to make the device discoverable again after disconnection. 3. `BLE_GAP_EVENT_ADV_COMPLETE`: - Occurs when advertising is complete, typically after a connection is established. - It restarts advertising to allow for new connections once the previous one is completed. 4. `BLE_GAP_EVENT_SUBSCRIBE`: - This event indicates a client's subscription to a characteristic, potentially enabling notifications. - It logs the current notification status and the handle of the subscribed attribute. - Depending on the attribute, it may reset or stop a timer used for sending notifications. 5. `BLE_GAP_EVENT_MTU`: - Triggered when the Maximum Transmission Unit (MTU) of the BLE connection is updated. - Logs the updated MTU value and the associated connection handle. ## blehr_tx_hrate_stop ```c static void blehr_tx_hrate_stop(void) { xTimerStop( blehr_tx_timer, 1000 / portTICK_PERIOD_MS ); } ``` This function stops a timer (`blehr_tx_timer`). It uses the FreeRTOS `xTimerStop` function to halt the timer. The timer is configured to stop after a 1000 tick periods interval defined by `1000 / portTICK_PERIOD_MS`. This function is typically called when there's no need to send heart rate updates, such as when the client unsubscribes from heart rate notifications. ## blehr_tx_hrate_reset ```c static void blehr_tx_hrate_reset(void) { int rc; if (xTimerReset(blehr_tx_timer, 1000 / portTICK_PERIOD_MS ) == pdPASS) { rc = 0; } else { rc = 1; } assert(rc == 0); } ``` This function resets the heart rate measurement timer (`blehr_tx_timer`) for periodic transmission. It uses the FreeRTOS `xTimerReset` function to restart the timer with the same 1000 tick periods interval. If the reset operation succeeds, it sets rc to 0; otherwise, it sets rc to 1. An assertion is used to ensure that the reset operation always succeeds (rc should be 0). This function is usually called to resume periodic heart rate updates after a stop event. ## blehr_tx_hrate ```c /* This function simulates heart beat and notifies it to the client */ static void blehr_tx_hrate(TimerHandle_t ev) { static uint8_t hrm[2]; int rc; struct os_mbuf *om; if (!notify_state) { blehr_tx_hrate_stop(); heartrate = 90; return; } hrm[0] = 0x06; /* contact of a sensor */ hrm[1] = heartrate; /* storing dummy data */ /* Simulation of heart beats */ heartrate++; if (heartrate == 160) { heartrate = 90; } om = ble_hs_mbuf_from_flat(hrm, sizeof(hrm)); rc = ble_gatts_notify_custom(conn_handle, hrs_hrm_handle, om); assert(rc == 0); blehr_tx_hrate_reset(); } ``` `blehr_tx_hrate` responsible for simulating heartbeats and notifying this simulated heart rate data to a connected BLE client. Here's a breakdown of what this code does: 1. It checks the `notify_state` variable to determine if notifications to the client should continue. If `notify_state` is false (indicating that notifications are not enabled), the function stops the heartbeat simulation, resets the heart rate to 90, and exits. 2. If notifications are enabled (`notify_state` is true), it constructs a 2-byte array `hrm`. The first byte is set to 0x06, indicating contact of a sensor, and the second byte is set to the current heart rate value. 3. It simulates heartbeats by incrementing the `heartrate` variable. If the heart rate reaches 160, it resets it to 90 to simulate a continuous heartbeat cycle. 4. It creates a memory buffer (`om`) from the `hrm` array using the NimBLE function `ble_hs_mbuf_from_flat`. 5. It uses the `ble_gatts_notify_custom` function to send a custom notification to the BLE client. This notification contains the heart rate data constructed in step 2. The `conn_handle` and `hrs_hrm_handle` parameters specify the connection handle and the handle of the heart rate measurement characteristic. 6. The `assert(rc == 0)` statement ensures that the notification operation was successful. 7. Finally, it calls the `blehr_tx_hrate_reset` function to reset the timer for the next heart rate simulation. Overall, this code is responsible for generating simulated heart rate data, notifying it to a connected BLE client, and ensuring that the heart rate simulation continues at regular intervals.