15 KiB
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. 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"
#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:
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.
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:
ESP_BT_MODE_IDLE
: Bluetooth not runningESP_BT_MODE_BLE
: BLE modeESP_BT_MODE_CLASSIC_BT
: BT Classic modeESP_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.
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.
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.
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.
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()
.
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 structuresble_gap_adv_params
andble_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.
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.
-
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.
-
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.
-
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.
-
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.
-
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
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
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
/* 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:
-
It checks the
notify_state
variable to determine if notifications to the client should continue. Ifnotify_state
is false (indicating that notifications are not enabled), the function stops the heartbeat simulation, resets the heart rate to 90, and exits. -
If notifications are enabled (
notify_state
is true), it constructs a 2-byte arrayhrm
. The first byte is set to 0x06, indicating contact of a sensor, and the second byte is set to the current heart rate value. -
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. -
It creates a memory buffer (
om
) from thehrm
array using the NimBLE functionble_hs_mbuf_from_flat
. -
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. Theconn_handle
andhrs_hrm_handle
parameters specify the connection handle and the handle of the heart rate measurement characteristic. -
The
assert(rc == 0)
statement ensures that the notification operation was successful. -
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.