mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
Merge branch 'docs/add_nimble_get_started_v5.2' into 'release/v5.2'
feat(ble/example): Added NimBLE Get started examples (v5.2) See merge request espressif/esp-idf!33029
This commit is contained in:
commit
c9953e3fa0
@ -0,0 +1,6 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(nimble_beacon)
|
@ -0,0 +1,352 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# NimBLE Beacon Example
|
||||
|
||||
## Overview
|
||||
|
||||
This is a pretty simple example, aiming to introduce
|
||||
|
||||
1. How to initialize NimBLE stack
|
||||
2. How to configure advertisement and scan response data
|
||||
3. How to start advertising as a non-connectable beacon
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE host stack.
|
||||
|
||||
To test this demo, any BLE scanner application can be used.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
### Set Target
|
||||
|
||||
Before project configuration and build, be sure to set the correct chip target using:
|
||||
|
||||
``` shell
|
||||
idf.py set-target <chip_name>
|
||||
```
|
||||
|
||||
For example, if you're using ESP32, then input
|
||||
|
||||
``` Shell
|
||||
idf.py set-target esp32
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following command to build, flash and monitor the project.
|
||||
|
||||
``` Shell
|
||||
idf.py -p <PORT> flash monitor
|
||||
```
|
||||
|
||||
For example, if the corresponding serial port is `/dev/ttyACM0`, then it goes
|
||||
|
||||
``` Shell
|
||||
idf.py -p /dev/ttyACM0 flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Code Explained
|
||||
|
||||
### Overview
|
||||
|
||||
1. Initialize NVS flash, NimBLE host stack and GAP service; configure NimBLE host stack and start NimBLE host task thread
|
||||
2. Wait for NimBLE host stack to sync with BLE controller
|
||||
3. Set advertisement and scan response data, then configure advertising parameters and start advertising
|
||||
|
||||
### Entry Point
|
||||
|
||||
`app_main` in `main.c` is the entry point of all ESP32 applications. In general, application initialization should be done here.
|
||||
|
||||
First, call `nvs_flash_init` function to initialize NVS flash, which is the dependency for BLE module to store configurations.
|
||||
|
||||
``` C
|
||||
void app_main(void) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
esp_err_t ret;
|
||||
|
||||
/* NVS flash initialization */
|
||||
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();
|
||||
}
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Then, call `nimble_port_init` function to initialize NimBLE host stack.
|
||||
|
||||
``` C
|
||||
void app_main(void) {
|
||||
...
|
||||
|
||||
/* NimBLE host stack initialization */
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
|
||||
ret);
|
||||
return;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
After that, call `gap_init` defined in `gap.c`. We will initialize GAP service, set GAP device name and appearance in this function.
|
||||
|
||||
``` C
|
||||
int gap_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
|
||||
/* Initialize GAP service */
|
||||
ble_svc_gap_init();
|
||||
|
||||
/* Set GAP device name */
|
||||
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device name to %s, error code: %d",
|
||||
DEVICE_NAME, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Set GAP device appearance */
|
||||
rc = ble_svc_gap_device_appearance_set(BLE_GAP_APPEARANCE_GENERIC_TAG);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device appearance, error code: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
...
|
||||
|
||||
/* GAP service initialization */
|
||||
rc = gap_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And we need to configure some callback functions for NimBLE host stack to call and store the configurations in `nimble_host_config_init` in `main.c`.
|
||||
|
||||
``` C
|
||||
static void nimble_host_config_init(void) {
|
||||
/* Set host callbacks */
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
/* Store host configuration */
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
...
|
||||
|
||||
/* NimBLE host configuration initialization */
|
||||
nimble_host_config_init();
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
So far, initialization has been done. We can call `xTaskCreate` to create `nimble_host_task` thread, and let NimBLE host stack run in the background.
|
||||
|
||||
``` C
|
||||
static void nimble_host_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "nimble host task has been started!");
|
||||
|
||||
/* This function won't return until nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
...
|
||||
|
||||
/* Start NimBLE host task thread and return */
|
||||
xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### On Stack Sync
|
||||
|
||||
Once NimBLE host stack is synced with BLE controller, `on_stack_sync` in `gap.c` will be called by NimBLE host stack, which has been configured in `nimble_host_config_init`.
|
||||
|
||||
In this function, we will call `adv_init` function to ask NimBLE host stack to check if device MAC address is available by `ble_hs_util_ensure_addr` and `ble_hs_id_infer_auto` functions. If so, we will copy the address and try to start advertising by calling `start_advertising` in the same source file.
|
||||
|
||||
``` C
|
||||
static void on_stack_sync(void) {
|
||||
/* On stack sync, do advertising initialization */
|
||||
adv_init();
|
||||
}
|
||||
|
||||
void adv_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Make sure we have proper BT identity address set */
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "device does not have any available bt address!");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Figure out BT address to use while advertising */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy device address to addr_val */
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
format_addr(addr_str, addr_val);
|
||||
ESP_LOGI(TAG, "device address: %s", addr_str);
|
||||
|
||||
/* Start advertising. */
|
||||
start_advertising();
|
||||
}
|
||||
```
|
||||
|
||||
### Start Advertising
|
||||
|
||||
As a beacon device, we're going to start advertising and send scan response if a scan request is received. To make it happen, we need to set advertisement and scan response data before advertising starts. So the following are what we do:
|
||||
|
||||
1. Initialize advertisement and scan response fields structs `adv_fields` and `rsp_fields`, as well as advertising parameters struct `adv_params`
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
const char *name;
|
||||
struct ble_hs_adv_fields adv_fields = {0};
|
||||
struct ble_hs_adv_fields rsp_fields = {0};
|
||||
struct ble_gap_adv_params adv_params = {0};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. Set advertising flags, device name, transmit power, appearance and LE role in `adv_fields`, and call `ble_gap_adv_set_fields`
|
||||
1. For adveritisng flags, `BLE_HS_ADV_F_DISC_GEN` means advertising is general discoverable, and `BLE_HS_ADV_F_BREDR_UNSUP` means BLE support only (BR/EDR refers to Bluetooth Classic)
|
||||
2. For appearance, it is used to tell scanner what does it look like; we use `BLE_GAP_APPEARANCE_GENERIC_TAG` here to make our device identified as a tag
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Set advertising flags */
|
||||
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
/* Set device name */
|
||||
name = ble_svc_gap_device_name();
|
||||
adv_fields.name = (uint8_t *)name;
|
||||
adv_fields.name_len = strlen(name);
|
||||
adv_fields.name_is_complete = 1;
|
||||
|
||||
/* Set device tx power */
|
||||
adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
adv_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
/* Set device appearance */
|
||||
adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
|
||||
adv_fields.appearance_is_present = 1;
|
||||
|
||||
/* Set device LE role */
|
||||
adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
|
||||
adv_fields.le_role_is_present = 1;
|
||||
|
||||
/* Set advertiement fields */
|
||||
rc = ble_gap_adv_set_fields(&adv_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
3. Set device address and URI in `rsp_fields`, and call `ble_gap_adv_rsp_set_fields`
|
||||
1. Since `AdvData` in advertisement packet **should not be longer than 31 bytes**, additional information must be placed in scan response packet
|
||||
2. We put the official website link of espressif into URI field
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Set device address */
|
||||
rsp_fields.device_addr = addr_val;
|
||||
rsp_fields.device_addr_type = own_addr_type;
|
||||
rsp_fields.device_addr_is_present = 1;
|
||||
|
||||
/* Set URI */
|
||||
rsp_fields.uri = esp_uri;
|
||||
rsp_fields.uri_len = sizeof(esp_uri);
|
||||
|
||||
/* Set scan response fields */
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
4. Set advertising mode and discoverable mode to non-connectable and general-discoverable respectively in `adv_params`, and finally, start advertising by calling `ble_gap_adv_start`
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
NULL, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
```
|
||||
|
||||
### Observation
|
||||
|
||||
If everything goes well, you should be able to see `NimBLE_Beacon` on a BLE scanner device, broadcasting a lot of information including an URI of "https://espressif.com" (The official website of espressif), which is exactly what we expect.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For any technical queries, please file an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
|
@ -0,0 +1,4 @@
|
||||
file(GLOB_RECURSE srcs "main.c" "src/*.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "./include")
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
/* Includes */
|
||||
/* STD APIs */
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ESP APIs */
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* FreeRTOS APIs */
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
/* NimBLE stack APIs */
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
|
||||
/* Defines */
|
||||
#define TAG "NimBLE_Beacon"
|
||||
#define DEVICE_NAME "NimBLE_Beacon"
|
||||
|
||||
#endif // COMMON_H
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GAP_SVC_H
|
||||
#define GAP_SVC_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GAP APIs */
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLE_GAP_APPEARANCE_GENERIC_TAG 0x0200
|
||||
#define BLE_GAP_URI_PREFIX_HTTPS 0x17
|
||||
#define BLE_GAP_LE_ROLE_PERIPHERAL 0x00
|
||||
|
||||
/* Public function declarations */
|
||||
void adv_init(void);
|
||||
int gap_init(void);
|
||||
|
||||
#endif // GAP_SVC_H
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "gap.h"
|
||||
|
||||
/* Library function declarations */
|
||||
void ble_store_config_init(void);
|
||||
|
||||
/* Private function declarations */
|
||||
static void on_stack_reset(int reason);
|
||||
static void on_stack_sync(void);
|
||||
static void nimble_host_config_init(void);
|
||||
static void nimble_host_task(void *param);
|
||||
|
||||
/* Private functions */
|
||||
/*
|
||||
* Stack event callback functions
|
||||
* - on_stack_reset is called when host resets BLE stack due to errors
|
||||
* - on_stack_sync is called when host has synced with controller
|
||||
*/
|
||||
static void on_stack_reset(int reason) {
|
||||
/* On reset, print reset reason to console */
|
||||
ESP_LOGI(TAG, "nimble stack reset, reset reason: %d", reason);
|
||||
}
|
||||
|
||||
static void on_stack_sync(void) {
|
||||
/* On stack sync, do advertising initialization */
|
||||
adv_init();
|
||||
}
|
||||
|
||||
static void nimble_host_config_init(void) {
|
||||
/* Set host callbacks */
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
/* Store host configuration */
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
static void nimble_host_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "nimble host task has been started!");
|
||||
|
||||
/* This function won't return until nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
/* NVS flash initialization */
|
||||
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();
|
||||
}
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE host stack initialization */
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
|
||||
ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GAP service initialization */
|
||||
rc = gap_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE host configuration initialization */
|
||||
nimble_host_config_init();
|
||||
|
||||
/* Start NimBLE host task thread and return */
|
||||
xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);
|
||||
return;
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gap.h"
|
||||
#include "common.h"
|
||||
|
||||
/* Private function declarations */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]);
|
||||
static void start_advertising(void);
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t own_addr_type;
|
||||
static uint8_t addr_val[6] = {0};
|
||||
static uint8_t esp_uri[] = {BLE_GAP_URI_PREFIX_HTTPS, '/', '/', 'e', 's', 'p', 'r', 'e', 's', 's', 'i', 'f', '.', 'c', 'o', 'm'};
|
||||
|
||||
/* Private functions */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]) {
|
||||
sprintf(addr_str, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1],
|
||||
addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static void start_advertising(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
const char *name;
|
||||
struct ble_hs_adv_fields adv_fields = {0};
|
||||
struct ble_hs_adv_fields rsp_fields = {0};
|
||||
struct ble_gap_adv_params adv_params = {0};
|
||||
|
||||
/* Set advertising flags */
|
||||
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
/* Set device name */
|
||||
name = ble_svc_gap_device_name();
|
||||
adv_fields.name = (uint8_t *)name;
|
||||
adv_fields.name_len = strlen(name);
|
||||
adv_fields.name_is_complete = 1;
|
||||
|
||||
/* Set device tx power */
|
||||
adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
adv_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
/* Set device appearance */
|
||||
adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
|
||||
adv_fields.appearance_is_present = 1;
|
||||
|
||||
/* Set device LE role */
|
||||
adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
|
||||
adv_fields.le_role_is_present = 1;
|
||||
|
||||
/* Set advertiement fields */
|
||||
rc = ble_gap_adv_set_fields(&adv_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set device address */
|
||||
rsp_fields.device_addr = addr_val;
|
||||
rsp_fields.device_addr_type = own_addr_type;
|
||||
rsp_fields.device_addr_is_present = 1;
|
||||
|
||||
/* Set URI */
|
||||
rsp_fields.uri = esp_uri;
|
||||
rsp_fields.uri_len = sizeof(esp_uri);
|
||||
|
||||
/* Set scan response fields */
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
NULL, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
void adv_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Make sure we have proper BT identity address set */
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "device does not have any available bt address!");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Figure out BT address to use while advertising */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy device address to addr_val */
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
format_addr(addr_str, addr_val);
|
||||
ESP_LOGI(TAG, "device address: %s", addr_str);
|
||||
|
||||
/* Start advertising. */
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
int gap_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
|
||||
/* Initialize GAP service */
|
||||
ble_svc_gap_init();
|
||||
|
||||
/* Set GAP device name */
|
||||
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device name to %s, error code: %d",
|
||||
DEVICE_NAME, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Set GAP device appearance */
|
||||
rc = ble_svc_gap_device_appearance_set(BLE_GAP_APPEARANCE_GENERIC_TAG);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device appearance, error code: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n
|
@ -0,0 +1,6 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(nimble_connection)
|
@ -0,0 +1,269 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# NimBLE Connection Example
|
||||
|
||||
## Overview
|
||||
|
||||
This example is extended from NimBLE Beacon Example, and further introduces
|
||||
|
||||
1. How to advertise as a connectable peripheral device
|
||||
2. How to capture GAP events and handle them
|
||||
3. How to update connection parameters
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE host stack.
|
||||
|
||||
To test this demo, any BLE scanner application can be used.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
### Set Target
|
||||
|
||||
Before project configuration and build, be sure to set the correct chip target using:
|
||||
|
||||
``` shell
|
||||
idf.py set-target <chip_name>
|
||||
```
|
||||
|
||||
For example, if you're using ESP32, then input
|
||||
|
||||
``` Shell
|
||||
idf.py set-target esp32
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following command to build, flash and monitor the project.
|
||||
|
||||
``` Shell
|
||||
idf.py -p <PORT> flash monitor
|
||||
```
|
||||
|
||||
For example, if the corresponding serial port is `/dev/ttyACM0`, then it goes
|
||||
|
||||
``` Shell
|
||||
idf.py -p /dev/ttyACM0 flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Code Explained
|
||||
|
||||
### Overview
|
||||
|
||||
1. Initialize LED, NVS flash, NimBLE host stack and GAP service; configure NimBLE host stack and start NimBLE host task thread; wait for NimBLE host stack to sync with BLE controller
|
||||
2. Set advertisement and scan response data, then configure advertising parameters and start advertising
|
||||
3. On connect event
|
||||
1. Turn on the LED on the dev board
|
||||
2. Print out connection descriptions
|
||||
3. Update connection parameters
|
||||
4. On connection update event
|
||||
1. Print out connection descriptions
|
||||
5. On disconnect event
|
||||
1. Turn off the LED on the dev board
|
||||
2. Print out connection descriptions
|
||||
|
||||
### Entry Point & On Stack Sync
|
||||
|
||||
Please refer to the NimBLE Beacon Example for details.
|
||||
|
||||
### Start Advertising
|
||||
|
||||
There're some slight differences in this example when compared to NimBLE Beacon Example. First, in this example we are constructing a connectable peripheral, so connection mode is set to connectable, that is
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Also, to demonstrate advertising parameters settings, advertising interval parameters are modified to 500ms, and shown in scan response. Please note that the unit of advertising interval is 0.625ms.
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Set advertising interval */
|
||||
rsp_fields.adv_itvl = BLE_GAP_ADV_ITVL_MS(500);
|
||||
rsp_fields.adv_itvl_is_present = 1;
|
||||
|
||||
...
|
||||
|
||||
/* Set advertising interval */
|
||||
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(500);
|
||||
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(510);
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And finally, when calling the advertising start API, a callback function `gap_event_handler` is passed as argument to receive GAP events. We'll talk about it in the next section.
|
||||
|
||||
``` C
|
||||
static void start_advertising(void) {
|
||||
...
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
gap_event_handler, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
```
|
||||
|
||||
### On GAP Events
|
||||
|
||||
To keep it simple, we're interested in 3 GAP events at the moment
|
||||
|
||||
- `BLE_GAP_EVENT_CONNECT` - Connect event
|
||||
- `BLE_GAP_EVENT_DISCONNECT` - Disconnect event
|
||||
- `BLE_GAP_EVENT_CONN_UPDATE` - Connection update event
|
||||
|
||||
#### Connect Event
|
||||
|
||||
When the device is connected to a peer device or a connection failed, a connect event will be passed to `gap_event_handler` by NimBLE host stack. We'll first check the connection status
|
||||
|
||||
- If succeeded
|
||||
- Get connection descriptor by connection handle and print out
|
||||
- Turn on the LED
|
||||
- Try to update connection parameters
|
||||
- If failed
|
||||
- Re-start advertising
|
||||
|
||||
``` C
|
||||
/* Connect event */
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Handle different GAP event */
|
||||
switch (event->type) {
|
||||
|
||||
/* Connect event */
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
ESP_LOGI(TAG, "connection %s; status=%d",
|
||||
event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
|
||||
/* Connection succeeded */
|
||||
if (event->connect.status == 0) {
|
||||
/* Check connection handle */
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print connection descriptor and turn on the LED */
|
||||
print_conn_desc(&desc);
|
||||
led_on();
|
||||
|
||||
/* Try to update connection parameters */
|
||||
struct ble_gap_upd_params params = {.itvl_min = desc.conn_itvl,
|
||||
.itvl_max = desc.conn_itvl,
|
||||
.latency = 3,
|
||||
.supervision_timeout =
|
||||
desc.supervision_timeout};
|
||||
rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"failed to update connection parameters, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* Connection failed, restart advertising */
|
||||
else {
|
||||
start_advertising();
|
||||
}
|
||||
return rc;
|
||||
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Disconnect Event
|
||||
|
||||
On disconnect event, we simply
|
||||
|
||||
1. Print out disconnect reason and connection descriptor
|
||||
2. Turn off the LED
|
||||
3. Re-start advertising
|
||||
|
||||
``` C
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
...
|
||||
|
||||
/* Disconnect event */
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
/* A connection was terminated, print connection descriptor */
|
||||
ESP_LOGI(TAG, "disconnected from peer; reason=%d",
|
||||
event->disconnect.reason);
|
||||
|
||||
/* Turn off the LED */
|
||||
led_off();
|
||||
|
||||
/* Restart advertising */
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### Connection Update Event
|
||||
|
||||
On connection update event, the operation is also very simple
|
||||
|
||||
1. Print out connection status
|
||||
2. Get connection descriptor by connection handle and print out
|
||||
|
||||
``` C
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
...
|
||||
|
||||
/* Connection parameters update event */
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
/* The central has updated the connection parameters. */
|
||||
ESP_LOGI(TAG, "connection updated; status=%d",
|
||||
event->conn_update.status);
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
print_conn_desc(&desc);
|
||||
return rc;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Observation
|
||||
|
||||
If everything goes well, except for what we have seen in NimBLE Beacon example, you should be able to see LED turned on when device is connected, and see LED turned off on disconnection.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For any technical queries, please file an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
|
||||
|
@ -0,0 +1,4 @@
|
||||
file(GLOB_RECURSE srcs "main.c" "src/*.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "./include")
|
@ -0,0 +1,42 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||
|
||||
choice BLINK_LED
|
||||
prompt "Blink LED type"
|
||||
default BLINK_LED_GPIO
|
||||
help
|
||||
Select the LED type. A normal level controlled LED or an addressable LED strip.
|
||||
The default selection is based on the Espressif DevKit boards.
|
||||
You can change the default selection according to your board.
|
||||
|
||||
config BLINK_LED_GPIO
|
||||
bool "GPIO"
|
||||
config BLINK_LED_STRIP
|
||||
bool "LED strip"
|
||||
endchoice
|
||||
|
||||
choice BLINK_LED_STRIP_BACKEND
|
||||
depends on BLINK_LED_STRIP
|
||||
prompt "LED strip backend peripheral"
|
||||
default BLINK_LED_STRIP_BACKEND_RMT if SOC_RMT_SUPPORTED
|
||||
default BLINK_LED_STRIP_BACKEND_SPI
|
||||
help
|
||||
Select the backend peripheral to drive the LED strip.
|
||||
|
||||
config BLINK_LED_STRIP_BACKEND_RMT
|
||||
depends on SOC_RMT_SUPPORTED
|
||||
bool "RMT"
|
||||
config BLINK_LED_STRIP_BACKEND_SPI
|
||||
bool "SPI"
|
||||
endchoice
|
||||
|
||||
config BLINK_GPIO
|
||||
int "Blink GPIO number"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
help
|
||||
GPIO number (IOxx) to blink on and off the LED.
|
||||
Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.
|
||||
|
||||
endmenu
|
@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/led_strip: "^2.4.1"
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
/* Includes */
|
||||
/* STD APIs */
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ESP APIs */
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* FreeRTOS APIs */
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
/* NimBLE stack APIs */
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
|
||||
/* Defines */
|
||||
#define TAG "NimBLE_Connection"
|
||||
#define DEVICE_NAME "NimBLE_CONN"
|
||||
|
||||
#endif // COMMON_H
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GAP_SVC_H
|
||||
#define GAP_SVC_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GAP APIs */
|
||||
#include "host/ble_gap.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLE_GAP_APPEARANCE_GENERIC_TAG 0x0200
|
||||
#define BLE_GAP_URI_PREFIX_HTTPS 0x17
|
||||
#define BLE_GAP_LE_ROLE_PERIPHERAL 0x00
|
||||
|
||||
/* Public function declarations */
|
||||
void adv_init(void);
|
||||
int gap_init(void);
|
||||
|
||||
#endif // GAP_SVC_H
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef LED_H
|
||||
#define LED_H
|
||||
|
||||
/* Includes */
|
||||
/* ESP APIs */
|
||||
#include "driver/gpio.h"
|
||||
#include "led_strip.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLINK_GPIO CONFIG_BLINK_GPIO
|
||||
|
||||
/* Public function declarations */
|
||||
uint8_t get_led_state(void);
|
||||
void led_on(void);
|
||||
void led_off(void);
|
||||
void led_init(void);
|
||||
|
||||
#endif // LED_H
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "gap.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Library function declarations */
|
||||
void ble_store_config_init(void);
|
||||
|
||||
/* Private function declarations */
|
||||
static void on_stack_reset(int reason);
|
||||
static void on_stack_sync(void);
|
||||
static void nimble_host_config_init(void);
|
||||
static void nimble_host_task(void *param);
|
||||
|
||||
/* Private functions */
|
||||
/*
|
||||
* Stack event callback functions
|
||||
* - on_stack_reset is called when host resets BLE stack due to errors
|
||||
* - on_stack_sync is called when host has synced with controller
|
||||
*/
|
||||
static void on_stack_reset(int reason) {
|
||||
/* On reset, print reset reason to console */
|
||||
ESP_LOGI(TAG, "nimble stack reset, reset reason: %d", reason);
|
||||
}
|
||||
|
||||
static void on_stack_sync(void) {
|
||||
/* On stack sync, do advertising initialization */
|
||||
adv_init();
|
||||
}
|
||||
|
||||
static void nimble_host_config_init(void) {
|
||||
/* Set host callbacks */
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
/* Store host configuration */
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
static void nimble_host_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "nimble host task has been started!");
|
||||
|
||||
/* This function won't return until nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
/* LED initialization */
|
||||
led_init();
|
||||
|
||||
/* NVS flash initialization */
|
||||
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();
|
||||
}
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE stack initialization */
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
|
||||
ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GAP service initialization */
|
||||
rc = gap_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE host configuration initialization */
|
||||
nimble_host_config_init();
|
||||
|
||||
/* Start NimBLE host task thread and return */
|
||||
xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);
|
||||
return;
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gap.h"
|
||||
#include "common.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Private function declarations */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]);
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc);
|
||||
static void start_advertising(void);
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg);
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t own_addr_type;
|
||||
static uint8_t addr_val[6] = {0};
|
||||
static uint8_t esp_uri[] = {BLE_GAP_URI_PREFIX_HTTPS, '/', '/', 'e', 's', 'p', 'r', 'e', 's', 's', 'i', 'f', '.', 'c', 'o', 'm'};
|
||||
|
||||
/* Private functions */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]) {
|
||||
sprintf(addr_str, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1],
|
||||
addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc) {
|
||||
/* Local variables */
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Connection handle */
|
||||
ESP_LOGI(TAG, "connection handle: %d", desc->conn_handle);
|
||||
|
||||
/* Local ID address */
|
||||
format_addr(addr_str, desc->our_id_addr.val);
|
||||
ESP_LOGI(TAG, "device id address: type=%d, value=%s",
|
||||
desc->our_id_addr.type, addr_str);
|
||||
|
||||
/* Peer ID address */
|
||||
format_addr(addr_str, desc->peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "peer id address: type=%d, value=%s", desc->peer_id_addr.type,
|
||||
addr_str);
|
||||
|
||||
/* Connection info */
|
||||
ESP_LOGI(TAG,
|
||||
"conn_itvl=%d, conn_latency=%d, supervision_timeout=%d, "
|
||||
"encrypted=%d, authenticated=%d, bonded=%d\n",
|
||||
desc->conn_itvl, desc->conn_latency, desc->supervision_timeout,
|
||||
desc->sec_state.encrypted, desc->sec_state.authenticated,
|
||||
desc->sec_state.bonded);
|
||||
}
|
||||
|
||||
static void start_advertising(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
const char *name;
|
||||
struct ble_hs_adv_fields adv_fields = {0};
|
||||
struct ble_hs_adv_fields rsp_fields = {0};
|
||||
struct ble_gap_adv_params adv_params = {0};
|
||||
|
||||
/* Set advertising flags */
|
||||
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
/* Set device name */
|
||||
name = ble_svc_gap_device_name();
|
||||
adv_fields.name = (uint8_t *)name;
|
||||
adv_fields.name_len = strlen(name);
|
||||
adv_fields.name_is_complete = 1;
|
||||
|
||||
/* Set device tx power */
|
||||
adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
adv_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
/* Set device appearance */
|
||||
adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
|
||||
adv_fields.appearance_is_present = 1;
|
||||
|
||||
/* Set device LE role */
|
||||
adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
|
||||
adv_fields.le_role_is_present = 1;
|
||||
|
||||
/* Set advertiement fields */
|
||||
rc = ble_gap_adv_set_fields(&adv_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set device address */
|
||||
rsp_fields.device_addr = addr_val;
|
||||
rsp_fields.device_addr_type = own_addr_type;
|
||||
rsp_fields.device_addr_is_present = 1;
|
||||
|
||||
/* Set URI */
|
||||
rsp_fields.uri = esp_uri;
|
||||
rsp_fields.uri_len = sizeof(esp_uri);
|
||||
|
||||
/* Set advertising interval */
|
||||
rsp_fields.adv_itvl = BLE_GAP_ADV_ITVL_MS(500);
|
||||
rsp_fields.adv_itvl_is_present = 1;
|
||||
|
||||
/* Set scan response fields */
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
/* Set advertising interval */
|
||||
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(500);
|
||||
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(510);
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
gap_event_handler, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
|
||||
/*
|
||||
* NimBLE applies an event-driven model to keep GAP service going
|
||||
* gap_event_handler is a callback function registered when calling
|
||||
* ble_gap_adv_start API and called when a GAP event arrives
|
||||
*/
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Handle different GAP event */
|
||||
switch (event->type) {
|
||||
|
||||
/* Connect event */
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
ESP_LOGI(TAG, "connection %s; status=%d",
|
||||
event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
|
||||
/* Connection succeeded */
|
||||
if (event->connect.status == 0) {
|
||||
/* Check connection handle */
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print connection descriptor and turn on the LED */
|
||||
print_conn_desc(&desc);
|
||||
led_on();
|
||||
|
||||
/* Try to update connection parameters */
|
||||
struct ble_gap_upd_params params = {.itvl_min = desc.conn_itvl,
|
||||
.itvl_max = desc.conn_itvl,
|
||||
.latency = 3,
|
||||
.supervision_timeout =
|
||||
desc.supervision_timeout};
|
||||
rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"failed to update connection parameters, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* Connection failed, restart advertising */
|
||||
else {
|
||||
start_advertising();
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Disconnect event */
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
/* A connection was terminated, print connection descriptor */
|
||||
ESP_LOGI(TAG, "disconnected from peer; reason=%d",
|
||||
event->disconnect.reason);
|
||||
|
||||
/* Turn off the LED */
|
||||
led_off();
|
||||
|
||||
/* Restart advertising */
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
/* Connection parameters update event */
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
/* The central has updated the connection parameters. */
|
||||
ESP_LOGI(TAG, "connection updated; status=%d",
|
||||
event->conn_update.status);
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
print_conn_desc(&desc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
void adv_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Make sure we have proper BT identity address set */
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "device does not have any available bt address!");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Figure out BT address to use while advertising */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy device address to addr_val */
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
format_addr(addr_str, addr_val);
|
||||
ESP_LOGI(TAG, "device address: %s", addr_str);
|
||||
|
||||
/* Start advertising. */
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
int gap_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
|
||||
/* Initialize GAP service */
|
||||
ble_svc_gap_init();
|
||||
|
||||
/* Set GAP device name */
|
||||
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device name to %s, error code: %d",
|
||||
DEVICE_NAME, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Set GAP device appearance */
|
||||
rc = ble_svc_gap_device_appearance_set(BLE_GAP_APPEARANCE_GENERIC_TAG);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device appearance, error code: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "led.h"
|
||||
#include "common.h"
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t led_state;
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
static led_strip_handle_t led_strip;
|
||||
#endif
|
||||
|
||||
/* Public functions */
|
||||
uint8_t get_led_state(void) { return led_state; }
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
|
||||
void led_on(void) {
|
||||
/* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
|
||||
led_strip_set_pixel(led_strip, 0, 16, 16, 16);
|
||||
|
||||
/* Refresh the strip to send data */
|
||||
led_strip_refresh(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = true;
|
||||
}
|
||||
|
||||
void led_off(void) {
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_strip_clear(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = false;
|
||||
}
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink addressable led!");
|
||||
/* LED strip initialization with the GPIO and pixels number*/
|
||||
led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = CONFIG_BLINK_GPIO,
|
||||
.max_leds = 1, // at least one LED on board
|
||||
};
|
||||
#if CONFIG_BLINK_LED_STRIP_BACKEND_RMT
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||
.flags.with_dma = false,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
#elif CONFIG_BLINK_LED_STRIP_BACKEND_SPI
|
||||
led_strip_spi_config_t spi_config = {
|
||||
.spi_bus = SPI2_HOST,
|
||||
.flags.with_dma = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));
|
||||
#else
|
||||
#error "unsupported LED strip backend"
|
||||
#endif
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_off();
|
||||
}
|
||||
|
||||
#elif CONFIG_BLINK_LED_GPIO
|
||||
|
||||
void led_on(void) { gpio_set_level(CONFIG_BLINK_GPIO, true); }
|
||||
|
||||
void led_off(void) { gpio_set_level(CONFIG_BLINK_GPIO, false); }
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink gpio led!");
|
||||
gpio_reset_pin(CONFIG_BLINK_GPIO);
|
||||
/* Set the GPIO as a push/pull output */
|
||||
gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT);
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported LED type"
|
||||
#endif
|
@ -0,0 +1,6 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n
|
||||
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=8
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_GPIO=5
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_GPIO=6
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=18
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=48
|
@ -0,0 +1,6 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(nimble_gatt_server)
|
@ -0,0 +1,380 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# NimBLE GATT Server Example
|
||||
|
||||
## Overview
|
||||
|
||||
This example is extended from NimBLE Connection Example, and further introduces
|
||||
|
||||
1. How to implement a GATT server using GATT services table
|
||||
2. How to handle characteristic access requests
|
||||
1. Write access demonstrated by LED control
|
||||
2. Read and indicate access demonstrated by heart rate measurement(mocked)
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE host stack.
|
||||
|
||||
To test this demo, any BLE scanner application can be used.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
### Set Target
|
||||
|
||||
Before project configuration and build, be sure to set the correct chip target using:
|
||||
|
||||
``` shell
|
||||
idf.py set-target <chip_name>
|
||||
```
|
||||
|
||||
For example, if you're using ESP32, then input
|
||||
|
||||
``` Shell
|
||||
idf.py set-target esp32
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following command to build, flash and monitor the project.
|
||||
|
||||
``` Shell
|
||||
idf.py -p <PORT> flash monitor
|
||||
```
|
||||
|
||||
For example, if the corresponding serial port is `/dev/ttyACM0`, then it goes
|
||||
|
||||
``` Shell
|
||||
idf.py -p /dev/ttyACM0 flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Code Explained
|
||||
|
||||
### Overview
|
||||
|
||||
1. Initialization
|
||||
1. Initialize LED, NVS flash, NimBLE host stack, GAP service
|
||||
2. Initialize GATT service and add services to registration queue
|
||||
3. Configure NimBLE host stack and start NimBLE host task thread, GATT services will be registered automatically when NimBLE host stack started
|
||||
4. Start heart rate update task thread
|
||||
2. Wait for NimBLE host stack to sync with BLE controller, and start advertising; wait for connection event to come
|
||||
3. After connection established, wait for GATT characteristics access events to come
|
||||
1. On write LED event, turn on or off the LED accordingly
|
||||
2. On read heart rate event, send out current heart rate measurement value
|
||||
3. On indicate heart rate event, enable heart rate indication
|
||||
|
||||
### Entry Point
|
||||
|
||||
In this example, we call GATT `gatt_svr_init` function to initialize GATT server in `app_main` before NimBLE host configuration. This is a custom function defined in `gatt_svc.c`, and basically we just call GATT service initialization API and add services to registration queue.
|
||||
|
||||
And there's another code added in `nimble_host_config_init`, which is
|
||||
|
||||
``` C
|
||||
static void nimble_host_config_init(void) {
|
||||
...
|
||||
|
||||
ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
That is GATT server register callback function. In this case it will only print out some registration information when services, characteristics or descriptors are registered.
|
||||
|
||||
Then, after NimBLE host task thread is created, we'll create another task defined in `heart_rate_task` to update heart rate measurement mock value and send indication if enabled.
|
||||
|
||||
### GAP Service Updates
|
||||
|
||||
`gap_event_handler` is updated with LED control removed, and more event handling branches, when compared to NimBLE Connection Example, including
|
||||
|
||||
- `BLE_GAP_EVENT_ADV_COMPLETE` - Advertising complete event
|
||||
- `BLE_GAP_EVENT_NOTIFY_TX` - Notificate event
|
||||
- `BLE_GAP_EVENT_SUBSCRIBE` - Subscribe event
|
||||
- `BLE_GAP_EVENT_MTU` - MTU update event
|
||||
|
||||
`BLE_GAP_EVENT_ADV_COMPLETE` and `BLE_GAP_EVENT_MTU` events are actually not involved in this example, but we still put them down there for reference. `BLE_GAP_EVENT_NOTIFY_TX` and `BLE_GAP_EVENT_SUBSCRIBE` events will be discussed in the next section.
|
||||
|
||||
### GATT Services Table
|
||||
|
||||
GATT services are defined in `ble_gatt_svc_def` struct array, with a variable name `gatt_svr_svcs` in this demo. We'll call it as the GATT services table in the following content.
|
||||
|
||||
``` C
|
||||
/* Heart rate service */
|
||||
static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D);
|
||||
|
||||
static uint8_t heart_rate_chr_val[2] = {0};
|
||||
static uint16_t heart_rate_chr_val_handle;
|
||||
static const ble_uuid16_t heart_rate_chr_uuid = BLE_UUID16_INIT(0x2A37);
|
||||
|
||||
static uint16_t heart_rate_chr_conn_handle = 0;
|
||||
static bool heart_rate_chr_conn_handle_inited = false;
|
||||
static bool heart_rate_ind_status = false;
|
||||
|
||||
/* Automation IO service */
|
||||
static const ble_uuid16_t auto_io_svc_uuid = BLE_UUID16_INIT(0x1815);
|
||||
static uint16_t led_chr_val_handle;
|
||||
static const ble_uuid128_t led_chr_uuid =
|
||||
BLE_UUID128_INIT(0x23, 0xd1, 0xbc, 0xea, 0x5f, 0x78, 0x23, 0x15, 0xde, 0xef,
|
||||
0x12, 0x12, 0x25, 0x15, 0x00, 0x00);
|
||||
|
||||
/* GATT services table */
|
||||
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
/* Heart rate service */
|
||||
{.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &heart_rate_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{/* Heart rate characteristic */
|
||||
.uuid = &heart_rate_chr_uuid.u,
|
||||
.access_cb = heart_rate_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_INDICATE,
|
||||
.val_handle = &heart_rate_chr_val_handle},
|
||||
{
|
||||
0, /* No more characteristics in this service. */
|
||||
}}},
|
||||
|
||||
/* Automation IO service */
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &auto_io_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){/* LED characteristic */
|
||||
{.uuid = &led_chr_uuid.u,
|
||||
.access_cb = led_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE,
|
||||
.val_handle = &led_chr_val_handle},
|
||||
{0}},
|
||||
},
|
||||
|
||||
{
|
||||
0, /* No more services. */
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
In this table, there are two GATT primary services defined
|
||||
|
||||
- Heart rate service with a UUID of `0x180D`
|
||||
- Automation IO service with a UUID of `0x1815`
|
||||
|
||||
#### Automation IO Service
|
||||
|
||||
Under automation IO service, there's a LED characteristic, with a vendor-specific UUID and write only permission.
|
||||
|
||||
The characteristic is binded with `led_chr_access` callback function, in which the write access event is captured. The LED will be turned on or off according to the write value, quite straight-forward.
|
||||
|
||||
``` C
|
||||
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: LED characteristic is write only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Write characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic write; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG,
|
||||
"characteristic write by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == led_chr_val_handle) {
|
||||
/* Verify access buffer length */
|
||||
if (ctxt->om->om_len == 1) {
|
||||
/* Turn the LED on or off according to the operation bit */
|
||||
if (ctxt->om->om_data[0]) {
|
||||
led_on();
|
||||
ESP_LOGI(TAG, "led turned on!");
|
||||
} else {
|
||||
led_off();
|
||||
ESP_LOGI(TAG, "led turned off!");
|
||||
}
|
||||
} else {
|
||||
goto error;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(TAG,
|
||||
"unexpected access operation to led characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
```
|
||||
|
||||
#### Heart Rate Service
|
||||
|
||||
Under heart rate service, there's a heart rate measurement characteristic, with a UUID of `0x2A37` and read + indicate access permission.
|
||||
|
||||
The characteristic is binded with `heart_rate_chr_access` callback function, in which the read access event is captured. It should be mentioned that in SIG definition, heart rate measurement is a multi-byte data structure, with the first byte indicating the flags
|
||||
|
||||
- Bit 0: Heart rate value type
|
||||
- 0: Heart rate value is `uint8_t` type
|
||||
- 1: Heart rate value is `uint16_t` type
|
||||
- Bit 1: Sensor contact status
|
||||
- Bit 2: Sensor contact supported
|
||||
- Bit 3: Energy expended status
|
||||
- Bit 4: RR-interval status
|
||||
- Bit 5-7: Reserved
|
||||
|
||||
and the rest of bytes are data fields. In this case, we use `uint8_t` type and disable other features, making the characteristic value a 2-byte array. So when characteristic read event arrives, we'll get the latest heart rate value and send it back to peer device.
|
||||
|
||||
``` C
|
||||
static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: Heart rate characteristic is read only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Read characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic read; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "characteristic read by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update access buffer value */
|
||||
heart_rate_chr_val[1] = get_heart_rate();
|
||||
rc = os_mbuf_append(ctxt->om, &heart_rate_chr_val,
|
||||
sizeof(heart_rate_chr_val));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"unexpected access operation to heart rate characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
```
|
||||
|
||||
Indicate access, however, is a bit more complicated. As mentioned in *GAP Service Updates*, we'll handle another 2 events namely `BLE_GAP_EVENT_NOTIFY_TX` and `BLE_GAP_EVENT_SUBSCRIBE` in `gap_event_handler`. In this case, if peer device wants to enable heart rate measurement indication, it will send a subscribe request to the local device, and the request will be captured as a subscribe event in `gap_event_handler`. But from the perspective of software layering, the event should be handled in GATT server, so we just pass the event to GATT server by calling `gatt_svr_subscribe_cb`, as demonstrated in the demo
|
||||
|
||||
``` C
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
...
|
||||
|
||||
/* Subscribe event */
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
/* Print subscription info to log */
|
||||
ESP_LOGI(TAG,
|
||||
"subscribe event; conn_handle=%d attr_handle=%d "
|
||||
"reason=%d prevn=%d curn=%d previ=%d curi=%d",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle,
|
||||
event->subscribe.reason, event->subscribe.prev_notify,
|
||||
event->subscribe.cur_notify, event->subscribe.prev_indicate,
|
||||
event->subscribe.cur_indicate);
|
||||
|
||||
/* GATT subscribe event callback */
|
||||
gatt_svr_subscribe_cb(event);
|
||||
return rc;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Then we'll check connection handle and attribute handle, if the attribute handle matches `heart_rate_chr_val_chandle`, `heart_rate_chr_conn_handle` and `heart_rate_ind_status` will be updated together.
|
||||
|
||||
``` C
|
||||
void gatt_svr_subscribe_cb(struct ble_gap_event *event) {
|
||||
/* Check connection handle */
|
||||
if (event->subscribe.conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "subscribe event; conn_handle=%d attr_handle=%d",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "subscribe by nimble stack; attr_handle=%d",
|
||||
event->subscribe.attr_handle);
|
||||
}
|
||||
|
||||
/* Check attribute handle */
|
||||
if (event->subscribe.attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update heart rate subscription status */
|
||||
heart_rate_chr_conn_handle = event->subscribe.conn_handle;
|
||||
heart_rate_chr_conn_handle_inited = true;
|
||||
heart_rate_ind_status = event->subscribe.cur_indicate;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that heart rate measurement incation is handled in `heart_rate_task` by calling `send_heart_rate_indication` function periodically. Actually, this function will check heart rate indication status and send indication accordingly. In this way, heart rate indication is implemented.
|
||||
|
||||
``` C
|
||||
void send_heart_rate_indication(void) {
|
||||
if (heart_rate_ind_status && heart_rate_chr_conn_handle_inited) {
|
||||
ble_gatts_indicate(heart_rate_chr_conn_handle,
|
||||
heart_rate_chr_val_handle);
|
||||
ESP_LOGI(TAG, "heart rate indication sent!");
|
||||
}
|
||||
}
|
||||
|
||||
static void heart_rate_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "heart rate task has been started!");
|
||||
|
||||
/* Loop forever */
|
||||
while (1) {
|
||||
/* Update heart rate value every 1 second */
|
||||
update_heart_rate();
|
||||
ESP_LOGI(TAG, "heart rate updated to %d", get_heart_rate());
|
||||
|
||||
/* Send heart rate indication if enabled */
|
||||
send_heart_rate_indication();
|
||||
|
||||
/* Sleep */
|
||||
vTaskDelay(HEART_RATE_TASK_PERIOD);
|
||||
}
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
```
|
||||
|
||||
## Observation
|
||||
|
||||
If everything goes well, you should be able to see 4 services when connected to ESP32, including
|
||||
|
||||
- Generic Access
|
||||
- Generic Attribute
|
||||
- Heart Rate
|
||||
- Automation IO
|
||||
|
||||
Click on Automation IO Service, you should be able to see LED characteristic. Click on upload button, you should be able to write `ON` or `OFF` value. Send it to the device, LED will be turned on or off following your instruction.
|
||||
|
||||
Click on Heart Rate Service, you should be able to see Heart Rate Measurement characteristic. Click on download button, you should be able to see the latest heart rate measurement mock value, and it should be consistent with what is shown on serial output. Click on subscribe button, you should be able to see the heart rate measurement mock value updated every second.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For any technical queries, please file an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
|
@ -0,0 +1,4 @@
|
||||
file(GLOB_RECURSE srcs "main.c" "src/*.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "./include")
|
@ -0,0 +1,42 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||
|
||||
choice BLINK_LED
|
||||
prompt "Blink LED type"
|
||||
default BLINK_LED_GPIO
|
||||
help
|
||||
Select the LED type. A normal level controlled LED or an addressable LED strip.
|
||||
The default selection is based on the Espressif DevKit boards.
|
||||
You can change the default selection according to your board.
|
||||
|
||||
config BLINK_LED_GPIO
|
||||
bool "GPIO"
|
||||
config BLINK_LED_STRIP
|
||||
bool "LED strip"
|
||||
endchoice
|
||||
|
||||
choice BLINK_LED_STRIP_BACKEND
|
||||
depends on BLINK_LED_STRIP
|
||||
prompt "LED strip backend peripheral"
|
||||
default BLINK_LED_STRIP_BACKEND_RMT if SOC_RMT_SUPPORTED
|
||||
default BLINK_LED_STRIP_BACKEND_SPI
|
||||
help
|
||||
Select the backend peripheral to drive the LED strip.
|
||||
|
||||
config BLINK_LED_STRIP_BACKEND_RMT
|
||||
depends on SOC_RMT_SUPPORTED
|
||||
bool "RMT"
|
||||
config BLINK_LED_STRIP_BACKEND_SPI
|
||||
bool "SPI"
|
||||
endchoice
|
||||
|
||||
config BLINK_GPIO
|
||||
int "Blink GPIO number"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
help
|
||||
GPIO number (IOxx) to blink on and off the LED.
|
||||
Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.
|
||||
|
||||
endmenu
|
@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/led_strip: "^2.4.1"
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
/* Includes */
|
||||
/* STD APIs */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ESP APIs */
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* FreeRTOS APIs */
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
/* NimBLE stack APIs */
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
|
||||
/* Defines */
|
||||
#define TAG "NimBLE_GATT_Server"
|
||||
#define DEVICE_NAME "NimBLE_GATT"
|
||||
|
||||
#endif // COMMON_H
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GAP_SVC_H
|
||||
#define GAP_SVC_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GAP APIs */
|
||||
#include "host/ble_gap.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLE_GAP_APPEARANCE_GENERIC_TAG 0x0200
|
||||
#define BLE_GAP_URI_PREFIX_HTTPS 0x17
|
||||
#define BLE_GAP_LE_ROLE_PERIPHERAL 0x00
|
||||
|
||||
/* Public function declarations */
|
||||
void adv_init(void);
|
||||
int gap_init(void);
|
||||
|
||||
#endif // GAP_SVC_H
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GATT_SVR_H
|
||||
#define GATT_SVR_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GATT APIs */
|
||||
#include "host/ble_gatt.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
|
||||
/* NimBLE GAP APIs */
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
/* Public function declarations */
|
||||
void send_heart_rate_indication(void);
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
void gatt_svr_subscribe_cb(struct ble_gap_event *event);
|
||||
int gatt_svc_init(void);
|
||||
|
||||
#endif // GATT_SVR_H
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef HEART_RATE_H
|
||||
#define HEART_RATE_H
|
||||
|
||||
/* Includes */
|
||||
/* ESP APIs */
|
||||
#include "esp_random.h"
|
||||
|
||||
/* Defines */
|
||||
#define HEART_RATE_TASK_PERIOD (1000 / portTICK_PERIOD_MS)
|
||||
|
||||
/* Public function declarations */
|
||||
uint8_t get_heart_rate(void);
|
||||
void update_heart_rate(void);
|
||||
|
||||
#endif // HEART_RATE_H
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef LED_H
|
||||
#define LED_H
|
||||
|
||||
/* Includes */
|
||||
/* ESP APIs */
|
||||
#include "driver/gpio.h"
|
||||
#include "led_strip.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLINK_GPIO CONFIG_BLINK_GPIO
|
||||
|
||||
/* Public function declarations */
|
||||
uint8_t get_led_state(void);
|
||||
void led_on(void);
|
||||
void led_off(void);
|
||||
void led_init(void);
|
||||
|
||||
#endif // LED_H
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "gap.h"
|
||||
#include "gatt_svc.h"
|
||||
#include "heart_rate.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Library function declarations */
|
||||
void ble_store_config_init(void);
|
||||
|
||||
/* Private function declarations */
|
||||
static void on_stack_reset(int reason);
|
||||
static void on_stack_sync(void);
|
||||
static void nimble_host_config_init(void);
|
||||
static void nimble_host_task(void *param);
|
||||
|
||||
/* Private functions */
|
||||
/*
|
||||
* Stack event callback functions
|
||||
* - on_stack_reset is called when host resets BLE stack due to errors
|
||||
* - on_stack_sync is called when host has synced with controller
|
||||
*/
|
||||
static void on_stack_reset(int reason) {
|
||||
/* On reset, print reset reason to console */
|
||||
ESP_LOGI(TAG, "nimble stack reset, reset reason: %d", reason);
|
||||
}
|
||||
|
||||
static void on_stack_sync(void) {
|
||||
/* On stack sync, do advertising initialization */
|
||||
adv_init();
|
||||
}
|
||||
|
||||
static void nimble_host_config_init(void) {
|
||||
/* Set host callbacks */
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
/* Store host configuration */
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
static void nimble_host_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "nimble host task has been started!");
|
||||
|
||||
/* This function won't return until nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void heart_rate_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "heart rate task has been started!");
|
||||
|
||||
/* Loop forever */
|
||||
while (1) {
|
||||
/* Update heart rate value every 1 second */
|
||||
update_heart_rate();
|
||||
ESP_LOGI(TAG, "heart rate updated to %d", get_heart_rate());
|
||||
|
||||
/* Send heart rate indication if enabled */
|
||||
send_heart_rate_indication();
|
||||
|
||||
/* Sleep */
|
||||
vTaskDelay(HEART_RATE_TASK_PERIOD);
|
||||
}
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
esp_err_t ret;
|
||||
|
||||
/* LED initialization */
|
||||
led_init();
|
||||
|
||||
/*
|
||||
* NVS flash initialization
|
||||
* Dependency of BLE stack to store configurations
|
||||
*/
|
||||
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();
|
||||
}
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE stack initialization */
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
|
||||
ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GAP service initialization */
|
||||
rc = gap_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GATT server initialization */
|
||||
rc = gatt_svc_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GATT server, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE host configuration initialization */
|
||||
nimble_host_config_init();
|
||||
|
||||
/* Start NimBLE host task thread and return */
|
||||
xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);
|
||||
xTaskCreate(heart_rate_task, "Heart Rate", 4*1024, NULL, 5, NULL);
|
||||
return;
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gap.h"
|
||||
#include "common.h"
|
||||
#include "gatt_svc.h"
|
||||
|
||||
/* Private function declarations */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]);
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc);
|
||||
static void start_advertising(void);
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg);
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t own_addr_type;
|
||||
static uint8_t addr_val[6] = {0};
|
||||
static uint8_t esp_uri[] = {BLE_GAP_URI_PREFIX_HTTPS, '/', '/', 'e', 's', 'p', 'r', 'e', 's', 's', 'i', 'f', '.', 'c', 'o', 'm'};
|
||||
|
||||
/* Private functions */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]) {
|
||||
sprintf(addr_str, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1],
|
||||
addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc) {
|
||||
/* Local variables */
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Connection handle */
|
||||
ESP_LOGI(TAG, "connection handle: %d", desc->conn_handle);
|
||||
|
||||
/* Local ID address */
|
||||
format_addr(addr_str, desc->our_id_addr.val);
|
||||
ESP_LOGI(TAG, "device id address: type=%d, value=%s",
|
||||
desc->our_id_addr.type, addr_str);
|
||||
|
||||
/* Peer ID address */
|
||||
format_addr(addr_str, desc->peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "peer id address: type=%d, value=%s", desc->peer_id_addr.type,
|
||||
addr_str);
|
||||
|
||||
/* Connection info */
|
||||
ESP_LOGI(TAG,
|
||||
"conn_itvl=%d, conn_latency=%d, supervision_timeout=%d, "
|
||||
"encrypted=%d, authenticated=%d, bonded=%d\n",
|
||||
desc->conn_itvl, desc->conn_latency, desc->supervision_timeout,
|
||||
desc->sec_state.encrypted, desc->sec_state.authenticated,
|
||||
desc->sec_state.bonded);
|
||||
}
|
||||
|
||||
static void start_advertising(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
const char *name;
|
||||
struct ble_hs_adv_fields adv_fields = {0};
|
||||
struct ble_hs_adv_fields rsp_fields = {0};
|
||||
struct ble_gap_adv_params adv_params = {0};
|
||||
|
||||
/* Set advertising flags */
|
||||
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
/* Set device name */
|
||||
name = ble_svc_gap_device_name();
|
||||
adv_fields.name = (uint8_t *)name;
|
||||
adv_fields.name_len = strlen(name);
|
||||
adv_fields.name_is_complete = 1;
|
||||
|
||||
/* Set device tx power */
|
||||
adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
adv_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
/* Set device appearance */
|
||||
adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
|
||||
adv_fields.appearance_is_present = 1;
|
||||
|
||||
/* Set device LE role */
|
||||
adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
|
||||
adv_fields.le_role_is_present = 1;
|
||||
|
||||
/* Set advertiement fields */
|
||||
rc = ble_gap_adv_set_fields(&adv_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set device address */
|
||||
rsp_fields.device_addr = addr_val;
|
||||
rsp_fields.device_addr_type = own_addr_type;
|
||||
rsp_fields.device_addr_is_present = 1;
|
||||
|
||||
/* Set URI */
|
||||
rsp_fields.uri = esp_uri;
|
||||
rsp_fields.uri_len = sizeof(esp_uri);
|
||||
|
||||
/* Set advertising interval */
|
||||
rsp_fields.adv_itvl = BLE_GAP_ADV_ITVL_MS(500);
|
||||
rsp_fields.adv_itvl_is_present = 1;
|
||||
|
||||
/* Set scan response fields */
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
/* Set advertising interval */
|
||||
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(500);
|
||||
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(510);
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
gap_event_handler, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
|
||||
/*
|
||||
* NimBLE applies an event-driven model to keep GAP service going
|
||||
* gap_event_handler is a callback function registered when calling
|
||||
* ble_gap_adv_start API and called when a GAP event arrives
|
||||
*/
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Handle different GAP event */
|
||||
switch (event->type) {
|
||||
|
||||
/* Connect event */
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
ESP_LOGI(TAG, "connection %s; status=%d",
|
||||
event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
|
||||
/* Connection succeeded */
|
||||
if (event->connect.status == 0) {
|
||||
/* Check connection handle */
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print connection descriptor */
|
||||
print_conn_desc(&desc);
|
||||
|
||||
/* Try to update connection parameters */
|
||||
struct ble_gap_upd_params params = {.itvl_min = desc.conn_itvl,
|
||||
.itvl_max = desc.conn_itvl,
|
||||
.latency = 3,
|
||||
.supervision_timeout =
|
||||
desc.supervision_timeout};
|
||||
rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"failed to update connection parameters, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* Connection failed, restart advertising */
|
||||
else {
|
||||
start_advertising();
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Disconnect event */
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
/* A connection was terminated, print connection descriptor */
|
||||
ESP_LOGI(TAG, "disconnected from peer; reason=%d",
|
||||
event->disconnect.reason);
|
||||
|
||||
/* Restart advertising */
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
/* Connection parameters update event */
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
/* The central has updated the connection parameters. */
|
||||
ESP_LOGI(TAG, "connection updated; status=%d",
|
||||
event->conn_update.status);
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
print_conn_desc(&desc);
|
||||
return rc;
|
||||
|
||||
/* Advertising complete event */
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
/* Advertising completed, restart advertising */
|
||||
ESP_LOGI(TAG, "advertise complete; reason=%d",
|
||||
event->adv_complete.reason);
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
/* Notification sent event */
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
if ((event->notify_tx.status != 0) &&
|
||||
(event->notify_tx.status != BLE_HS_EDONE)) {
|
||||
/* Print notification info on error */
|
||||
ESP_LOGI(TAG,
|
||||
"notify event; conn_handle=%d attr_handle=%d "
|
||||
"status=%d is_indication=%d",
|
||||
event->notify_tx.conn_handle, event->notify_tx.attr_handle,
|
||||
event->notify_tx.status, event->notify_tx.indication);
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Subscribe event */
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
/* Print subscription info to log */
|
||||
ESP_LOGI(TAG,
|
||||
"subscribe event; conn_handle=%d attr_handle=%d "
|
||||
"reason=%d prevn=%d curn=%d previ=%d curi=%d",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle,
|
||||
event->subscribe.reason, event->subscribe.prev_notify,
|
||||
event->subscribe.cur_notify, event->subscribe.prev_indicate,
|
||||
event->subscribe.cur_indicate);
|
||||
|
||||
/* GATT subscribe event callback */
|
||||
gatt_svr_subscribe_cb(event);
|
||||
return rc;
|
||||
|
||||
/* MTU update event */
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
/* Print MTU update info to log */
|
||||
ESP_LOGI(TAG, "mtu update event; conn_handle=%d cid=%d mtu=%d",
|
||||
event->mtu.conn_handle, event->mtu.channel_id,
|
||||
event->mtu.value);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/* Public functions */
|
||||
void adv_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Make sure we have proper BT identity address set (random preferred) */
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "device does not have any available bt address!");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Figure out BT address to use while advertising (no privacy for now) */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Printing ADDR */
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
format_addr(addr_str, addr_val);
|
||||
ESP_LOGI(TAG, "device address: %s", addr_str);
|
||||
|
||||
/* Start advertising. */
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
int gap_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
|
||||
/* Call NimBLE GAP initialization API */
|
||||
ble_svc_gap_init();
|
||||
|
||||
/* Set GAP device name */
|
||||
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device name to %s, error code: %d",
|
||||
DEVICE_NAME, rc);
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gatt_svc.h"
|
||||
#include "common.h"
|
||||
#include "heart_rate.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Private function declarations */
|
||||
static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
/* Private variables */
|
||||
/* Heart rate service */
|
||||
static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D);
|
||||
|
||||
static uint8_t heart_rate_chr_val[2] = {0};
|
||||
static uint16_t heart_rate_chr_val_handle;
|
||||
static const ble_uuid16_t heart_rate_chr_uuid = BLE_UUID16_INIT(0x2A37);
|
||||
|
||||
static uint16_t heart_rate_chr_conn_handle = 0;
|
||||
static bool heart_rate_chr_conn_handle_inited = false;
|
||||
static bool heart_rate_ind_status = false;
|
||||
|
||||
/* Automation IO service */
|
||||
static const ble_uuid16_t auto_io_svc_uuid = BLE_UUID16_INIT(0x1815);
|
||||
static uint16_t led_chr_val_handle;
|
||||
static const ble_uuid128_t led_chr_uuid =
|
||||
BLE_UUID128_INIT(0x23, 0xd1, 0xbc, 0xea, 0x5f, 0x78, 0x23, 0x15, 0xde, 0xef,
|
||||
0x12, 0x12, 0x25, 0x15, 0x00, 0x00);
|
||||
|
||||
/* GATT services table */
|
||||
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
/* Heart rate service */
|
||||
{.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &heart_rate_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{/* Heart rate characteristic */
|
||||
.uuid = &heart_rate_chr_uuid.u,
|
||||
.access_cb = heart_rate_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_INDICATE,
|
||||
.val_handle = &heart_rate_chr_val_handle},
|
||||
{
|
||||
0, /* No more characteristics in this service. */
|
||||
}}},
|
||||
|
||||
/* Automation IO service */
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &auto_io_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){/* LED characteristic */
|
||||
{.uuid = &led_chr_uuid.u,
|
||||
.access_cb = led_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE,
|
||||
.val_handle = &led_chr_val_handle},
|
||||
{0}},
|
||||
},
|
||||
|
||||
{
|
||||
0, /* No more services. */
|
||||
},
|
||||
};
|
||||
|
||||
/* Private functions */
|
||||
static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: Heart rate characteristic is read only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Read characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic read; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "characteristic read by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update access buffer value */
|
||||
heart_rate_chr_val[1] = get_heart_rate();
|
||||
rc = os_mbuf_append(ctxt->om, &heart_rate_chr_val,
|
||||
sizeof(heart_rate_chr_val));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"unexpected access operation to heart rate characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: LED characteristic is write only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Write characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic write; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG,
|
||||
"characteristic write by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == led_chr_val_handle) {
|
||||
/* Verify access buffer length */
|
||||
if (ctxt->om->om_len == 1) {
|
||||
/* Turn the LED on or off according to the operation bit */
|
||||
if (ctxt->om->om_data[0]) {
|
||||
led_on();
|
||||
ESP_LOGI(TAG, "led turned on!");
|
||||
} else {
|
||||
led_off();
|
||||
ESP_LOGI(TAG, "led turned off!");
|
||||
}
|
||||
} else {
|
||||
goto error;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(TAG,
|
||||
"unexpected access operation to led characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
void send_heart_rate_indication(void) {
|
||||
if (heart_rate_ind_status && heart_rate_chr_conn_handle_inited) {
|
||||
ble_gatts_indicate(heart_rate_chr_conn_handle,
|
||||
heart_rate_chr_val_handle);
|
||||
ESP_LOGI(TAG, "heart rate indication sent!");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle GATT attribute register events
|
||||
* - Service register event
|
||||
* - Characteristic register event
|
||||
* - Descriptor register event
|
||||
*/
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
char buf[BLE_UUID_STR_LEN];
|
||||
|
||||
/* Handle GATT attributes register events */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Service register event */
|
||||
case BLE_GATT_REGISTER_OP_SVC:
|
||||
ESP_LOGD(TAG, "registered service %s with handle=%d",
|
||||
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf),
|
||||
ctxt->svc.handle);
|
||||
break;
|
||||
|
||||
/* Characteristic register event */
|
||||
case BLE_GATT_REGISTER_OP_CHR:
|
||||
ESP_LOGD(TAG,
|
||||
"registering characteristic %s with "
|
||||
"def_handle=%d val_handle=%d",
|
||||
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
|
||||
ctxt->chr.def_handle, ctxt->chr.val_handle);
|
||||
break;
|
||||
|
||||
/* Descriptor register event */
|
||||
case BLE_GATT_REGISTER_OP_DSC:
|
||||
ESP_LOGD(TAG, "registering descriptor %s with handle=%d",
|
||||
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf),
|
||||
ctxt->dsc.handle);
|
||||
break;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
assert(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* GATT server subscribe event callback
|
||||
* 1. Update heart rate subscription status
|
||||
*/
|
||||
|
||||
void gatt_svr_subscribe_cb(struct ble_gap_event *event) {
|
||||
/* Check connection handle */
|
||||
if (event->subscribe.conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "subscribe event; conn_handle=%d attr_handle=%d",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "subscribe by nimble stack; attr_handle=%d",
|
||||
event->subscribe.attr_handle);
|
||||
}
|
||||
|
||||
/* Check attribute handle */
|
||||
if (event->subscribe.attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update heart rate subscription status */
|
||||
heart_rate_chr_conn_handle = event->subscribe.conn_handle;
|
||||
heart_rate_chr_conn_handle_inited = true;
|
||||
heart_rate_ind_status = event->subscribe.cur_indicate;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* GATT server initialization
|
||||
* 1. Initialize GATT service
|
||||
* 2. Update NimBLE host GATT services counter
|
||||
* 3. Add GATT services to server
|
||||
*/
|
||||
int gatt_svc_init(void) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* 1. GATT service initialization */
|
||||
ble_svc_gatt_init();
|
||||
|
||||
/* 2. Update GATT services counter */
|
||||
rc = ble_gatts_count_cfg(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 3. Add GATT services */
|
||||
rc = ble_gatts_add_svcs(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "heart_rate.h"
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t heart_rate;
|
||||
|
||||
/* Public functions */
|
||||
uint8_t get_heart_rate(void) { return heart_rate; }
|
||||
|
||||
void update_heart_rate(void) { heart_rate = 60 + (uint8_t)(esp_random() % 21); }
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "led.h"
|
||||
#include "common.h"
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t led_state;
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
static led_strip_handle_t led_strip;
|
||||
#endif
|
||||
|
||||
/* Public functions */
|
||||
uint8_t get_led_state(void) { return led_state; }
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
|
||||
void led_on(void) {
|
||||
/* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
|
||||
led_strip_set_pixel(led_strip, 0, 16, 16, 16);
|
||||
|
||||
/* Refresh the strip to send data */
|
||||
led_strip_refresh(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = true;
|
||||
}
|
||||
|
||||
void led_off(void) {
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_strip_clear(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = false;
|
||||
}
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink addressable led!");
|
||||
/* LED strip initialization with the GPIO and pixels number*/
|
||||
led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = CONFIG_BLINK_GPIO,
|
||||
.max_leds = 1, // at least one LED on board
|
||||
};
|
||||
#if CONFIG_BLINK_LED_STRIP_BACKEND_RMT
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||
.flags.with_dma = false,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
#elif CONFIG_BLINK_LED_STRIP_BACKEND_SPI
|
||||
led_strip_spi_config_t spi_config = {
|
||||
.spi_bus = SPI2_HOST,
|
||||
.flags.with_dma = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));
|
||||
#else
|
||||
#error "unsupported LED strip backend"
|
||||
#endif
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_off();
|
||||
}
|
||||
|
||||
#elif CONFIG_BLINK_LED_GPIO
|
||||
|
||||
void led_on(void) { gpio_set_level(CONFIG_BLINK_GPIO, true); }
|
||||
|
||||
void led_off(void) { gpio_set_level(CONFIG_BLINK_GPIO, false); }
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink gpio led!");
|
||||
gpio_reset_pin(CONFIG_BLINK_GPIO);
|
||||
/* Set the GPIO as a push/pull output */
|
||||
gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT);
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported LED type"
|
||||
#endif
|
@ -0,0 +1,6 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n
|
||||
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=8
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_GPIO=5
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_GPIO=6
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=18
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=48
|
@ -0,0 +1,6 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(nimble_security)
|
@ -0,0 +1,224 @@
|
||||
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-H2 | ESP32-S3 |
|
||||
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
|
||||
|
||||
# NimBLE Security Example
|
||||
|
||||
## Overview
|
||||
|
||||
This example is extended from NimBLE GATT Server Example, and further introduces
|
||||
|
||||
1. How to set random non-resolvable private address for device
|
||||
2. How to ask for connection encryption from peripheral side on characteristic access
|
||||
3. How to bond with peer device using a random generated 6-digit passkey
|
||||
|
||||
It uses ESP32's Bluetooth controller and NimBLE host stack.
|
||||
|
||||
To test this demo, any BLE scanner application can be used.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
### Set Target
|
||||
|
||||
Before project configuration and build, be sure to set the correct chip target using:
|
||||
|
||||
``` shell
|
||||
idf.py set-target <chip_name>
|
||||
```
|
||||
|
||||
For example, if you're using ESP32, then input
|
||||
|
||||
``` Shell
|
||||
idf.py set-target esp32
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following command to build, flash and monitor the project.
|
||||
|
||||
``` Shell
|
||||
idf.py -p <PORT> flash monitor
|
||||
```
|
||||
|
||||
For example, if the corresponding serial port is `/dev/ttyACM0`, then it goes
|
||||
|
||||
``` Shell
|
||||
idf.py -p /dev/ttyACM0 flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Code Explained
|
||||
|
||||
### Overview
|
||||
|
||||
The following is additional content compared to the NimBLE GATT Server example.
|
||||
|
||||
1. Initialization procedure is generally similar to NimBLE GATT Server Example, but we'll initialize random number generator in the beginning, and in `nimble_host_config_init` we will configure security manager to enable related features
|
||||
2. On stack sync, a random non-resolvable private address is generated and set as the device address
|
||||
3. Characteristics' permission modified to require connection encryption when accessing
|
||||
4. 3 more GAP event branches added in `gap_event_handler` to handle encryption related events
|
||||
|
||||
### Entry Point
|
||||
|
||||
In `nimble_host_config_init` function, we're going to enable some security manager features, including
|
||||
|
||||
- Bonding
|
||||
- Man-in-the-middle protection
|
||||
- Key distribution
|
||||
|
||||
Also, we're going to set the IO capability to `BLE_HS_IO_DISPLAY_ONLY`, since it's possible to print out the passkey to serial output.
|
||||
|
||||
``` C
|
||||
static void nimble_host_config_init(void) {
|
||||
...
|
||||
|
||||
/* Security manager configuration */
|
||||
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### GATT Server Updates
|
||||
|
||||
For heart rate characteristic and LED characteristic, `BLE_GATT_CHR_F_READ_ENC` and `BLE_GATT_CHR_F_WRITE_ENC` flag are added respectively to require connection encryption when GATT client tries to access the characteristic. Thanks to NimBLE host stack, the connection encryption will be initiated automatically by adding these flags.
|
||||
|
||||
However, heart rate characteristic is also indicatable, and NimBLE host stack does not offer an implementation for indication access to require connection encryption, so we need to do it ourselves. For GATT server, we simply check connection security status by calling an external function `is_connection_encrypted` in `send_heart_rate_indication` function to determine if the indication should be sent. This external function is defined in GAP layer, and we'll talk about it in *GAP Event Handler Updates* section.
|
||||
|
||||
``` C
|
||||
void send_heart_rate_indication(void) {
|
||||
/* Check if connection handle is initialized */
|
||||
if (!heart_rate_chr_conn_handle_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check indication and security status */
|
||||
if (heart_rate_ind_status &&
|
||||
is_connection_encrypted(heart_rate_chr_conn_handle)) {
|
||||
ble_gatts_indicate(heart_rate_chr_conn_handle,
|
||||
heart_rate_chr_val_handle);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Random Address
|
||||
|
||||
In the following function, we can generate a random non-resolvable private address and set as the device address. We will call it in `adv_init` function before ensuring address availability.
|
||||
|
||||
``` C
|
||||
static void set_random_addr(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
ble_addr_t addr;
|
||||
|
||||
/* Generate new non-resolvable private address */
|
||||
rc = ble_hs_id_gen_rnd(0, &addr);
|
||||
assert(rc == 0);
|
||||
|
||||
/* Set address */
|
||||
rc = ble_hs_id_set_rnd(addr.val);
|
||||
assert(rc == 0);
|
||||
}
|
||||
```
|
||||
|
||||
### Check Connection Encryption Status
|
||||
|
||||
By connection handle, we can fetch connection descriptor from NimBLE host stack, and there's a flag indicating connection encryption status, check the following codes
|
||||
|
||||
``` C
|
||||
bool is_connection_encrypted(uint16_t conn_handle) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return desc.sec_state.encrypted;
|
||||
}
|
||||
```
|
||||
|
||||
### GAP Event Handler Updates
|
||||
|
||||
3 more GAP event branches are added in `gap_event_handler`, which are
|
||||
|
||||
- `BLE_GAP_EVENT_ENC_CHANGE` - Encryption change event
|
||||
- `BLE_GAP_EVENT_REPEAT_PAIRING` - Repeat pairing event
|
||||
- `BLE_GAP_EVENT_PASSKEY_ACTION` - Passkey action event
|
||||
|
||||
On encryption change event, we're going to print the encryption change status to output.
|
||||
|
||||
``` C
|
||||
/* Encryption change event */
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
/* Encryption has been enabled or disabled for this connection. */
|
||||
if (event->enc_change.status == 0) {
|
||||
ESP_LOGI(TAG, "connection encrypted!");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "connection encryption failed, status: %d",
|
||||
event->enc_change.status);
|
||||
}
|
||||
return rc;
|
||||
```
|
||||
|
||||
On repeat pairing event, to make it simple, we will just delete the old bond and repeat pairing.
|
||||
|
||||
``` C
|
||||
/* Repeat pairing event */
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
||||
/* Delete the old bond */
|
||||
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection, error code %d", rc);
|
||||
return rc;
|
||||
}
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
|
||||
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
|
||||
* continue with pairing operation */
|
||||
ESP_LOGI(TAG, "repairing...");
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
```
|
||||
|
||||
On passkey action event, a random 6-digit passkey is generated, and you are supposed to enter the same passkey on pairing. If the input is consistent with the generated passkey, you should be able to bond with the device.
|
||||
|
||||
``` C
|
||||
/* Passkey action event */
|
||||
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
||||
/* Display action */
|
||||
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
|
||||
/* Generate passkey */
|
||||
struct ble_sm_io pkey = {0};
|
||||
pkey.action = event->passkey.params.action;
|
||||
pkey.passkey = 100000 + esp_random() % 900000;
|
||||
ESP_LOGI(TAG, "enter passkey %" PRIu32 " on the peer side",
|
||||
pkey.passkey);
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to inject security manager io, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
```
|
||||
|
||||
## Observation
|
||||
|
||||
If everything goes well, pairing will be required when you try to access any of the characteristics, that is, read or indicate heart rate characteristic, or write LED characteristic.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For any technical queries, please file an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.
|
@ -0,0 +1,4 @@
|
||||
file(GLOB_RECURSE srcs "main.c" "src/*.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
INCLUDE_DIRS "./include")
|
@ -0,0 +1,42 @@
|
||||
menu "Example Configuration"
|
||||
|
||||
orsource "$IDF_PATH/examples/common_components/env_caps/$IDF_TARGET/Kconfig.env_caps"
|
||||
|
||||
choice BLINK_LED
|
||||
prompt "Blink LED type"
|
||||
default BLINK_LED_GPIO
|
||||
help
|
||||
Select the LED type. A normal level controlled LED or an addressable LED strip.
|
||||
The default selection is based on the Espressif DevKit boards.
|
||||
You can change the default selection according to your board.
|
||||
|
||||
config BLINK_LED_GPIO
|
||||
bool "GPIO"
|
||||
config BLINK_LED_STRIP
|
||||
bool "LED strip"
|
||||
endchoice
|
||||
|
||||
choice BLINK_LED_STRIP_BACKEND
|
||||
depends on BLINK_LED_STRIP
|
||||
prompt "LED strip backend peripheral"
|
||||
default BLINK_LED_STRIP_BACKEND_RMT if SOC_RMT_SUPPORTED
|
||||
default BLINK_LED_STRIP_BACKEND_SPI
|
||||
help
|
||||
Select the backend peripheral to drive the LED strip.
|
||||
|
||||
config BLINK_LED_STRIP_BACKEND_RMT
|
||||
depends on SOC_RMT_SUPPORTED
|
||||
bool "RMT"
|
||||
config BLINK_LED_STRIP_BACKEND_SPI
|
||||
bool "SPI"
|
||||
endchoice
|
||||
|
||||
config BLINK_GPIO
|
||||
int "Blink GPIO number"
|
||||
range ENV_GPIO_RANGE_MIN ENV_GPIO_OUT_RANGE_MAX
|
||||
default 8
|
||||
help
|
||||
GPIO number (IOxx) to blink on and off the LED.
|
||||
Some GPIOs are used for other purposes (flash connections, etc.) and cannot be used to blink.
|
||||
|
||||
endmenu
|
@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/led_strip: "^2.4.1"
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
/* Includes */
|
||||
/* STD APIs */
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ESP APIs */
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* FreeRTOS APIs */
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
/* NimBLE stack APIs */
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/ble.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
|
||||
/* Defines */
|
||||
#define TAG "NimBLE_Security"
|
||||
#define DEVICE_NAME "NimBLE_SEC"
|
||||
|
||||
#endif // COMMON_H
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GAP_SVC_H
|
||||
#define GAP_SVC_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GAP APIs */
|
||||
#include "host/ble_gap.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLE_GAP_APPEARANCE_GENERIC_TAG 0x0200
|
||||
#define BLE_GAP_URI_PREFIX_HTTPS 0x17
|
||||
#define BLE_GAP_LE_ROLE_PERIPHERAL 0x00
|
||||
|
||||
/* Public function declarations */
|
||||
void adv_init(void);
|
||||
bool is_connection_encrypted(uint16_t conn_handle);
|
||||
int gap_init(void);
|
||||
|
||||
#endif // GAP_SVC_H
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef GATT_SVR_H
|
||||
#define GATT_SVR_H
|
||||
|
||||
/* Includes */
|
||||
/* NimBLE GATT APIs */
|
||||
#include "host/ble_gatt.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
|
||||
/* NimBLE GAP APIs */
|
||||
#include "host/ble_gap.h"
|
||||
|
||||
/* Public function declarations */
|
||||
void send_heart_rate_indication(void);
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
int gatt_svr_subscribe_cb(struct ble_gap_event *event);
|
||||
int gatt_svc_init(void);
|
||||
|
||||
#endif // GATT_SVR_H
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef HEART_RATE_H
|
||||
#define HEART_RATE_H
|
||||
|
||||
/* Includes */
|
||||
/* ESP APIs */
|
||||
#include "esp_random.h"
|
||||
|
||||
/* Defines */
|
||||
#define HEART_RATE_TASK_PERIOD (1000 / portTICK_PERIOD_MS)
|
||||
|
||||
/* Public function declarations */
|
||||
uint8_t get_heart_rate(void);
|
||||
void update_heart_rate(void);
|
||||
|
||||
#endif // HEART_RATE_H
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#ifndef LED_H
|
||||
#define LED_H
|
||||
|
||||
/* Includes */
|
||||
/* ESP APIs */
|
||||
#include "driver/gpio.h"
|
||||
#include "led_strip.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* Defines */
|
||||
#define BLINK_GPIO CONFIG_BLINK_GPIO
|
||||
|
||||
/* Public function declarations */
|
||||
uint8_t get_led_state(void);
|
||||
void led_on(void);
|
||||
void led_off(void);
|
||||
void led_init(void);
|
||||
|
||||
#endif // LED_H
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "gap.h"
|
||||
#include "gatt_svc.h"
|
||||
#include "heart_rate.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Library function declarations */
|
||||
void ble_store_config_init(void);
|
||||
|
||||
/* Private function declarations */
|
||||
static void nimble_host_config_init(void);
|
||||
static void nimble_host_task(void *param);
|
||||
|
||||
/* Private functions */
|
||||
/*
|
||||
* Stack event callback functions
|
||||
* - on_stack_reset is called when host resets BLE stack due to errors
|
||||
* - on_stack_sync is called when host has synced with controller
|
||||
*/
|
||||
static void on_stack_reset(int reason) {
|
||||
/* On reset, print reset reason to console */
|
||||
ESP_LOGI(TAG, "nimble stack reset, reset reason: %d", reason);
|
||||
}
|
||||
|
||||
static void on_stack_sync(void) {
|
||||
/* On stack sync, do advertising initialization */
|
||||
adv_init();
|
||||
}
|
||||
|
||||
static void nimble_host_config_init(void) {
|
||||
/* Set host callbacks */
|
||||
ble_hs_cfg.reset_cb = on_stack_reset;
|
||||
ble_hs_cfg.sync_cb = on_stack_sync;
|
||||
ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
/* Security manager configuration */
|
||||
ble_hs_cfg.sm_io_cap = BLE_HS_IO_DISPLAY_ONLY;
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
||||
|
||||
/* Store host configuration */
|
||||
ble_store_config_init();
|
||||
}
|
||||
|
||||
static void nimble_host_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "nimble host task has been started!");
|
||||
|
||||
/* This function won't return until nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void heart_rate_task(void *param) {
|
||||
/* Task entry log */
|
||||
ESP_LOGI(TAG, "heart rate task has been started!");
|
||||
|
||||
/* Loop forever */
|
||||
while (1) {
|
||||
/* Update heart rate value every 1 second */
|
||||
update_heart_rate();
|
||||
ESP_LOGI(TAG, "heart rate updated to %d", get_heart_rate());
|
||||
|
||||
/* Send heart rate indication if enabled */
|
||||
send_heart_rate_indication();
|
||||
|
||||
/* Sleep */
|
||||
vTaskDelay(HEART_RATE_TASK_PERIOD);
|
||||
}
|
||||
|
||||
/* Clean up at exit */
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
uint32_t seed = esp_random();
|
||||
esp_err_t ret;
|
||||
|
||||
/* LED initialization */
|
||||
led_init();
|
||||
|
||||
/* Random generator initialization */
|
||||
srand(seed);
|
||||
|
||||
/*
|
||||
* NVS flash initialization
|
||||
* Dependency of BLE stack to store configurations
|
||||
*/
|
||||
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();
|
||||
}
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE stack initialization */
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
|
||||
ret);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GAP service initialization */
|
||||
rc = gap_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GATT server initialization */
|
||||
rc = gatt_svc_init();
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to initialize GATT server, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* NimBLE host configuration initialization */
|
||||
nimble_host_config_init();
|
||||
|
||||
/* Start NimBLE host task thread and return */
|
||||
xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);
|
||||
xTaskCreate(heart_rate_task, "Heart Rate", 4*1024, NULL, 5, NULL);
|
||||
return;
|
||||
}
|
@ -0,0 +1,386 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gap.h"
|
||||
#include "common.h"
|
||||
#include "gatt_svc.h"
|
||||
|
||||
/* Private function declarations */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]);
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc);
|
||||
static void start_advertising(void);
|
||||
static void set_random_addr(void);
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg);
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t own_addr_type;
|
||||
static uint8_t addr_val[6] = {0};
|
||||
static uint8_t esp_uri[] = {BLE_GAP_URI_PREFIX_HTTPS, '/', '/', 'e', 's', 'p', 'r', 'e', 's', 's', 'i', 'f', '.', 'c', 'o', 'm'};
|
||||
|
||||
/* Private functions */
|
||||
inline static void format_addr(char *addr_str, uint8_t addr[]) {
|
||||
sprintf(addr_str, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1],
|
||||
addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
static void print_conn_desc(struct ble_gap_conn_desc *desc) {
|
||||
/* Local variables */
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Connection handle */
|
||||
ESP_LOGI(TAG, "connection handle: %d", desc->conn_handle);
|
||||
|
||||
/* Local ID address */
|
||||
format_addr(addr_str, desc->our_id_addr.val);
|
||||
ESP_LOGI(TAG, "device id address: type=%d, value=%s",
|
||||
desc->our_id_addr.type, addr_str);
|
||||
|
||||
/* Peer ID address */
|
||||
format_addr(addr_str, desc->peer_id_addr.val);
|
||||
ESP_LOGI(TAG, "peer id address: type=%d, value=%s", desc->peer_id_addr.type,
|
||||
addr_str);
|
||||
|
||||
/* Connection info */
|
||||
ESP_LOGI(TAG,
|
||||
"conn_itvl=%d, conn_latency=%d, supervision_timeout=%d, "
|
||||
"encrypted=%d, authenticated=%d, bonded=%d\n",
|
||||
desc->conn_itvl, desc->conn_latency, desc->supervision_timeout,
|
||||
desc->sec_state.encrypted, desc->sec_state.authenticated,
|
||||
desc->sec_state.bonded);
|
||||
}
|
||||
|
||||
static void set_random_addr(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
ble_addr_t addr;
|
||||
|
||||
/* Generate new non-resolvable private address */
|
||||
rc = ble_hs_id_gen_rnd(0, &addr);
|
||||
assert(rc == 0);
|
||||
|
||||
/* Set address */
|
||||
rc = ble_hs_id_set_rnd(addr.val);
|
||||
assert(rc == 0);
|
||||
}
|
||||
|
||||
static void start_advertising(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
const char *name;
|
||||
struct ble_hs_adv_fields adv_fields = {0};
|
||||
struct ble_hs_adv_fields rsp_fields = {0};
|
||||
struct ble_gap_adv_params adv_params = {0};
|
||||
|
||||
/* Set advertising flags */
|
||||
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
/* Set device name */
|
||||
name = ble_svc_gap_device_name();
|
||||
adv_fields.name = (uint8_t *)name;
|
||||
adv_fields.name_len = strlen(name);
|
||||
adv_fields.name_is_complete = 1;
|
||||
|
||||
/* Set device tx power */
|
||||
adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
adv_fields.tx_pwr_lvl_is_present = 1;
|
||||
|
||||
/* Set device appearance */
|
||||
adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
|
||||
adv_fields.appearance_is_present = 1;
|
||||
|
||||
/* Set device LE role */
|
||||
adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
|
||||
adv_fields.le_role_is_present = 1;
|
||||
|
||||
/* Set advertiement fields */
|
||||
rc = ble_gap_adv_set_fields(&adv_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set device address */
|
||||
rsp_fields.device_addr = addr_val;
|
||||
rsp_fields.device_addr_type = own_addr_type;
|
||||
rsp_fields.device_addr_is_present = 1;
|
||||
|
||||
/* Set URI */
|
||||
rsp_fields.uri = esp_uri;
|
||||
rsp_fields.uri_len = sizeof(esp_uri);
|
||||
|
||||
/* Set advertising interval */
|
||||
rsp_fields.adv_itvl = BLE_GAP_ADV_ITVL_MS(500);
|
||||
rsp_fields.adv_itvl_is_present = 1;
|
||||
|
||||
/* Set scan response fields */
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set non-connetable and general discoverable mode to be a beacon */
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
/* Set advertising interval */
|
||||
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(500);
|
||||
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(510);
|
||||
|
||||
/* Start advertising */
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
||||
gap_event_handler, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "advertising started!");
|
||||
}
|
||||
|
||||
/*
|
||||
* NimBLE applies an event-driven model to keep GAP service going
|
||||
* gap_event_handler is a callback function registered when calling
|
||||
* ble_gap_adv_start API and called when a GAP event arrives
|
||||
*/
|
||||
static int gap_event_handler(struct ble_gap_event *event, void *arg) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Handle different GAP event */
|
||||
switch (event->type) {
|
||||
|
||||
/* Connect event */
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
ESP_LOGI(TAG, "connection %s; status=%d",
|
||||
event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
|
||||
/* Connection succeeded */
|
||||
if (event->connect.status == 0) {
|
||||
/* Check connection handle */
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Print connection descriptor */
|
||||
print_conn_desc(&desc);
|
||||
|
||||
/* Try to update connection parameters */
|
||||
struct ble_gap_upd_params params = {.itvl_min = desc.conn_itvl,
|
||||
.itvl_max = desc.conn_itvl,
|
||||
.latency = 3,
|
||||
.supervision_timeout =
|
||||
desc.supervision_timeout};
|
||||
rc = ble_gap_update_params(event->connect.conn_handle, ¶ms);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"failed to update connection parameters, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* Connection failed, restart advertising */
|
||||
else {
|
||||
start_advertising();
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Disconnect event */
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
/* A connection was terminated, print connection descriptor */
|
||||
ESP_LOGI(TAG, "disconnected from peer; reason=%d",
|
||||
event->disconnect.reason);
|
||||
|
||||
/* Restart advertising */
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
/* Connection parameters update event */
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
/* The central has updated the connection parameters. */
|
||||
ESP_LOGI(TAG, "connection updated; status=%d",
|
||||
event->conn_update.status);
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
print_conn_desc(&desc);
|
||||
return rc;
|
||||
|
||||
/* Advertising complete event */
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
/* Advertising completed, restart advertising */
|
||||
ESP_LOGI(TAG, "advertise complete; reason=%d",
|
||||
event->adv_complete.reason);
|
||||
start_advertising();
|
||||
return rc;
|
||||
|
||||
/* Notification sent event */
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
if ((event->notify_tx.status != 0) &&
|
||||
(event->notify_tx.status != BLE_HS_EDONE)) {
|
||||
/* Print notification info on error */
|
||||
ESP_LOGI(TAG,
|
||||
"notify event; conn_handle=%d attr_handle=%d "
|
||||
"status=%d is_indication=%d",
|
||||
event->notify_tx.conn_handle, event->notify_tx.attr_handle,
|
||||
event->notify_tx.status, event->notify_tx.indication);
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Subscribe event */
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
/* Print subscription info to log */
|
||||
ESP_LOGI(TAG,
|
||||
"subscribe event; conn_handle=%d attr_handle=%d "
|
||||
"reason=%d prevn=%d curn=%d previ=%d curi=%d",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle,
|
||||
event->subscribe.reason, event->subscribe.prev_notify,
|
||||
event->subscribe.cur_notify, event->subscribe.prev_indicate,
|
||||
event->subscribe.cur_indicate);
|
||||
|
||||
/* GATT subscribe event callback */
|
||||
rc = gatt_svr_subscribe_cb(event);
|
||||
if (rc == BLE_ATT_ERR_INSUFFICIENT_AUTHEN) {
|
||||
/* Request connection encryption */
|
||||
return ble_gap_security_initiate(event->subscribe.conn_handle);
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* MTU update event */
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
/* Print MTU update info to log */
|
||||
ESP_LOGI(TAG, "mtu update event; conn_handle=%d cid=%d mtu=%d",
|
||||
event->mtu.conn_handle, event->mtu.channel_id,
|
||||
event->mtu.value);
|
||||
return rc;
|
||||
|
||||
/* Encryption change event */
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
/* Encryption has been enabled or disabled for this connection. */
|
||||
if (event->enc_change.status == 0) {
|
||||
ESP_LOGI(TAG, "connection encrypted!");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "connection encryption failed, status: %d",
|
||||
event->enc_change.status);
|
||||
}
|
||||
return rc;
|
||||
|
||||
/* Repeat pairing event */
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
||||
/* Delete the old bond */
|
||||
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection, error code %d", rc);
|
||||
return rc;
|
||||
}
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
|
||||
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
|
||||
* continue with pairing operation */
|
||||
ESP_LOGI(TAG, "repairing...");
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
|
||||
/* Passkey action event */
|
||||
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
||||
/* Display action */
|
||||
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
|
||||
/* Generate passkey */
|
||||
struct ble_sm_io pkey = {0};
|
||||
pkey.action = event->passkey.params.action;
|
||||
pkey.passkey = 100000 + esp_random() % 900000;
|
||||
ESP_LOGI(TAG, "enter passkey %" PRIu32 " on the peer side",
|
||||
pkey.passkey);
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"failed to inject security manager io, error code: %d",
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
void adv_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
char addr_str[12] = {0};
|
||||
|
||||
/* Make sure we have proper BT identity address set */
|
||||
set_random_addr();
|
||||
rc = ble_hs_util_ensure_addr(1);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "device does not have any available bt address!");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Figure out BT address to use while advertising */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Copy device address to addr_val */
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
|
||||
return;
|
||||
}
|
||||
format_addr(addr_str, addr_val);
|
||||
ESP_LOGI(TAG, "device address: %s", addr_str);
|
||||
|
||||
/* Start advertising. */
|
||||
start_advertising();
|
||||
}
|
||||
|
||||
bool is_connection_encrypted(uint16_t conn_handle) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
struct ble_gap_conn_desc desc;
|
||||
|
||||
/* Print connection descriptor */
|
||||
rc = ble_gap_conn_find(conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to find connection by handle, error code: %d",
|
||||
rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return desc.sec_state.encrypted;
|
||||
}
|
||||
|
||||
int gap_init(void) {
|
||||
/* Local variables */
|
||||
int rc = 0;
|
||||
|
||||
/* Call NimBLE GAP initialization API */
|
||||
ble_svc_gap_init();
|
||||
|
||||
/* Set GAP device name */
|
||||
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "failed to set device name to %s, error code: %d",
|
||||
DEVICE_NAME, rc);
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "gatt_svc.h"
|
||||
#include "common.h"
|
||||
#include "gap.h"
|
||||
#include "heart_rate.h"
|
||||
#include "led.h"
|
||||
|
||||
/* Private function declarations */
|
||||
static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
/* Private variables */
|
||||
/* Heart rate service */
|
||||
static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D);
|
||||
|
||||
static uint8_t heart_rate_chr_val[2] = {0};
|
||||
static uint16_t heart_rate_chr_val_handle;
|
||||
static const ble_uuid16_t heart_rate_chr_uuid = BLE_UUID16_INIT(0x2A37);
|
||||
|
||||
static uint16_t heart_rate_chr_conn_handle = 0;
|
||||
static bool heart_rate_chr_conn_handle_inited = false;
|
||||
static bool heart_rate_ind_status = false;
|
||||
|
||||
/* Automation IO service */
|
||||
static const ble_uuid16_t auto_io_svc_uuid = BLE_UUID16_INIT(0x1815);
|
||||
static uint16_t led_chr_val_handle;
|
||||
static const ble_uuid128_t led_chr_uuid =
|
||||
BLE_UUID128_INIT(0x23, 0xd1, 0xbc, 0xea, 0x5f, 0x78, 0x23, 0x15, 0xde, 0xef,
|
||||
0x12, 0x12, 0x25, 0x15, 0x00, 0x00);
|
||||
|
||||
/* GATT services table */
|
||||
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
/* Heart rate service */
|
||||
{.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &heart_rate_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
{/* Heart rate characteristic */
|
||||
.uuid = &heart_rate_chr_uuid.u,
|
||||
.access_cb = heart_rate_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_INDICATE |
|
||||
BLE_GATT_CHR_F_READ_ENC,
|
||||
.val_handle = &heart_rate_chr_val_handle},
|
||||
{
|
||||
0, /* No more characteristics in this service. */
|
||||
}}},
|
||||
|
||||
/* Automation IO service */
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &auto_io_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){
|
||||
/* LED characteristic */
|
||||
{.uuid = &led_chr_uuid.u,
|
||||
.access_cb = led_chr_access,
|
||||
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_ENC,
|
||||
.val_handle = &led_chr_val_handle},
|
||||
{0}},
|
||||
},
|
||||
|
||||
{
|
||||
0, /* No more services. */
|
||||
},
|
||||
};
|
||||
|
||||
/* Private functions */
|
||||
static int heart_rate_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: Heart rate characteristic is read only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Read characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic read; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "characteristic read by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update access buffer value */
|
||||
heart_rate_chr_val[1] = get_heart_rate();
|
||||
rc = os_mbuf_append(ctxt->om, &heart_rate_chr_val,
|
||||
sizeof(heart_rate_chr_val));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(
|
||||
TAG,
|
||||
"unexpected access operation to heart rate characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
struct ble_gatt_access_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* Handle access events */
|
||||
/* Note: LED characteristic is write only */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Write characteristic event */
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
/* Verify connection handle */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ESP_LOGI(TAG, "characteristic write; conn_handle=%d attr_handle=%d",
|
||||
conn_handle, attr_handle);
|
||||
} else {
|
||||
ESP_LOGI(TAG,
|
||||
"characteristic write by nimble stack; attr_handle=%d",
|
||||
attr_handle);
|
||||
}
|
||||
|
||||
/* Verify attribute handle */
|
||||
if (attr_handle == led_chr_val_handle) {
|
||||
/* Verify access buffer length */
|
||||
if (ctxt->om->om_len == 1) {
|
||||
/* Turn the LED on or off according to the operation bit */
|
||||
if (ctxt->om->om_data[0]) {
|
||||
led_on();
|
||||
ESP_LOGI(TAG, "led turned on!");
|
||||
} else {
|
||||
led_off();
|
||||
ESP_LOGI(TAG, "led turned off!");
|
||||
}
|
||||
} else {
|
||||
goto error;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
goto error;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
error:
|
||||
ESP_LOGE(TAG,
|
||||
"unexpected access operation to led characteristic, opcode: %d",
|
||||
ctxt->op);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/* Public functions */
|
||||
void send_heart_rate_indication(void) {
|
||||
/* Check if connection handle is initialized */
|
||||
if (!heart_rate_chr_conn_handle_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check indication and security status */
|
||||
if (heart_rate_ind_status &&
|
||||
is_connection_encrypted(heart_rate_chr_conn_handle)) {
|
||||
ble_gatts_indicate(heart_rate_chr_conn_handle,
|
||||
heart_rate_chr_val_handle);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle GATT attribute register events
|
||||
* - Service register event
|
||||
* - Characteristic register event
|
||||
* - Descriptor register event
|
||||
*/
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) {
|
||||
/* Local variables */
|
||||
char buf[BLE_UUID_STR_LEN];
|
||||
|
||||
/* Handle GATT attributes register events */
|
||||
switch (ctxt->op) {
|
||||
|
||||
/* Service register event */
|
||||
case BLE_GATT_REGISTER_OP_SVC:
|
||||
ESP_LOGD(TAG, "registered service %s with handle=%d",
|
||||
ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf),
|
||||
ctxt->svc.handle);
|
||||
break;
|
||||
|
||||
/* Characteristic register event */
|
||||
case BLE_GATT_REGISTER_OP_CHR:
|
||||
ESP_LOGD(TAG,
|
||||
"registering characteristic %s with "
|
||||
"def_handle=%d val_handle=%d",
|
||||
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf),
|
||||
ctxt->chr.def_handle, ctxt->chr.val_handle);
|
||||
break;
|
||||
|
||||
/* Descriptor register event */
|
||||
case BLE_GATT_REGISTER_OP_DSC:
|
||||
ESP_LOGD(TAG, "registering descriptor %s with handle=%d",
|
||||
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf),
|
||||
ctxt->dsc.handle);
|
||||
break;
|
||||
|
||||
/* Unknown event */
|
||||
default:
|
||||
assert(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* GATT server subscribe event callback
|
||||
* 1. Update heart rate subscription status
|
||||
*/
|
||||
|
||||
int gatt_svr_subscribe_cb(struct ble_gap_event *event) {
|
||||
/* Check attribute handle */
|
||||
if (event->subscribe.attr_handle == heart_rate_chr_val_handle) {
|
||||
/* Update heart rate subscription status */
|
||||
heart_rate_chr_conn_handle = event->subscribe.conn_handle;
|
||||
heart_rate_chr_conn_handle_inited = true;
|
||||
heart_rate_ind_status = event->subscribe.cur_indicate;
|
||||
|
||||
/* Check security status */
|
||||
if (!is_connection_encrypted(event->subscribe.conn_handle)) {
|
||||
ESP_LOGE(TAG, "failed to subscribe to heart rate measurement, "
|
||||
"connection not encrypted!");
|
||||
return BLE_ATT_ERR_INSUFFICIENT_AUTHEN;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* GATT server initialization
|
||||
* 1. Initialize GATT service
|
||||
* 2. Update NimBLE host GATT services counter
|
||||
* 3. Add GATT services to server
|
||||
*/
|
||||
int gatt_svc_init(void) {
|
||||
/* Local variables */
|
||||
int rc;
|
||||
|
||||
/* 1. GATT service initialization */
|
||||
ble_svc_gatt_init();
|
||||
|
||||
/* 2. Update GATT services counter */
|
||||
rc = ble_gatts_count_cfg(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 3. Add GATT services */
|
||||
rc = ble_gatts_add_svcs(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "common.h"
|
||||
#include "heart_rate.h"
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t heart_rate;
|
||||
|
||||
/* Public functions */
|
||||
uint8_t get_heart_rate(void) { return heart_rate; }
|
||||
|
||||
void update_heart_rate(void) { heart_rate = 60 + (uint8_t)(esp_random() % 21); }
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
/* Includes */
|
||||
#include "led.h"
|
||||
#include "common.h"
|
||||
|
||||
/* Private variables */
|
||||
static uint8_t led_state;
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
static led_strip_handle_t led_strip;
|
||||
#endif
|
||||
|
||||
/* Public functions */
|
||||
uint8_t get_led_state(void) { return led_state; }
|
||||
|
||||
#ifdef CONFIG_BLINK_LED_STRIP
|
||||
|
||||
void led_on(void) {
|
||||
/* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
|
||||
led_strip_set_pixel(led_strip, 0, 16, 16, 16);
|
||||
|
||||
/* Refresh the strip to send data */
|
||||
led_strip_refresh(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = true;
|
||||
}
|
||||
|
||||
void led_off(void) {
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_strip_clear(led_strip);
|
||||
|
||||
/* Update LED state */
|
||||
led_state = false;
|
||||
}
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink addressable led!");
|
||||
/* LED strip initialization with the GPIO and pixels number*/
|
||||
led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = CONFIG_BLINK_GPIO,
|
||||
.max_leds = 1, // at least one LED on board
|
||||
};
|
||||
#if CONFIG_BLINK_LED_STRIP_BACKEND_RMT
|
||||
led_strip_rmt_config_t rmt_config = {
|
||||
.resolution_hz = 10 * 1000 * 1000, // 10MHz
|
||||
.flags.with_dma = false,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
|
||||
#elif CONFIG_BLINK_LED_STRIP_BACKEND_SPI
|
||||
led_strip_spi_config_t spi_config = {
|
||||
.spi_bus = SPI2_HOST,
|
||||
.flags.with_dma = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(
|
||||
led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));
|
||||
#else
|
||||
#error "unsupported LED strip backend"
|
||||
#endif
|
||||
/* Set all LED off to clear all pixels */
|
||||
led_off();
|
||||
}
|
||||
|
||||
#elif CONFIG_BLINK_LED_GPIO
|
||||
|
||||
void led_on(void) { gpio_set_level(CONFIG_BLINK_GPIO, true); }
|
||||
|
||||
void led_off(void) { gpio_set_level(CONFIG_BLINK_GPIO, false); }
|
||||
|
||||
void led_init(void) {
|
||||
ESP_LOGI(TAG, "example configured to blink gpio led!");
|
||||
gpio_reset_pin(CONFIG_BLINK_GPIO);
|
||||
/* Set the GPIO as a push/pull output */
|
||||
gpio_set_direction(CONFIG_BLINK_GPIO, GPIO_MODE_OUTPUT);
|
||||
}
|
||||
|
||||
#else
|
||||
#error "unsupported LED type"
|
||||
#endif
|
@ -0,0 +1,6 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n
|
||||
|
||||
CONFIG_BLINK_LED_GPIO=y
|
||||
CONFIG_BLINK_GPIO=8
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_GPIO=5
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_GPIO=6
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=18
|
@ -0,0 +1,2 @@
|
||||
CONFIG_BLINK_LED_STRIP=y
|
||||
CONFIG_BLINK_GPIO=48
|
Loading…
Reference in New Issue
Block a user