Merge branch 'feature/walkthrough_for_spp_client' into 'master'

doc(nimble): Added the tutorial for spp_client example.

See merge request espressif/esp-idf!26030
This commit is contained in:
Rahul Tank 2023-09-25 19:08:34 +08:00
commit b953afe030

View File

@ -0,0 +1,549 @@
# BLE SPP Client Example Walkthrough
## Introduction
In this tutorial, we will explore the spp_client example code provided by Espressif's ESP-IDF framework. This tutorial guides you in building a BLE Serial Port Profile (SPP) client using the NimBLE stack. BLE SPP enables wireless serial communication, making it valuable for IoT and embedded applications. By following this tutorial, you will learn how to scan for nearby BLE devices advertising the SPP service, establish connections, and create a virtual serial link for data exchange.
## Includes
This example is located in the examples folder of the ESP-IDF under the [ble_spp/spp_client/main](../main). The [main.c](../main/main.c) file located in the main folder contains all the functionality that we are going to review. The header files contained in [main.c](../main/main.c) are:
```c
#include "esp_log.h"
#include "nvs_flash.h"
/* BLE */
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "console/console.h"
#include "services/gap/ble_svc_gap.h"
#include "ble_spp_client.h"
#include "driver/uart.h"
```
These `includes` are required for the FreeRTOS and underlying system components to run, including the logging functionality and a library to store data in non-volatile flash memory. We are interested in `“nimble_port.h”`, `“nimble_port_freertos.h”`, `"ble_hs.h"`, `“ble_svc_gap.h”` and `“ble_spp_client.h”` which expose the BLE APIs required to implement this example.
* `nimble_port.h`: Includes the declaration of functions required for the initialization of the nimble stack.
* `nimble_port_freertos.h`: Initializes and enables nimble host task.
* `ble_hs.h`: Defines the functionalities to handle the host event.
* `ble_svc_gap.h`: Defines the macros for device name, and device appearance and declares the function to set them.
* `ble_spp_client.h`: Defines constants and structures for a BLE SPP client using the NimBLE stack. It includes UUIDs and data structures for BLE communication in an ESP-IDF project.
## Main Entry Point
The program's entry point is the app_main() function:
```c
void
app_main(void)
{
int rc;
/* Initialize NVS — it is used to store PHY calibration data */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ret = nimble_port_init();
if (ret != ESP_OK) {
MODLOG_DFLT(ERROR, "Failed to init nimble %d \n", ret);
return;
}
/* Initialize UART driver and start uart task */
ble_spp_uart_init();
/* Configure the host. */
ble_hs_cfg.reset_cb = ble_spp_client_on_reset;
ble_hs_cfg.sync_cb = ble_spp_client_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
/* Initialize data structures to track connected peers. */
rc = peer_init(MYNEWT_VAL(BLE_MAX_CONNECTIONS), 64, 64, 64);
assert(rc == 0);
/* Set the default device name. */
rc = ble_svc_gap_device_name_set("nimble-ble-spp-client");
assert(rc == 0);
/* XXX Need to have template for store */
ble_store_config_init();
nimble_port_freertos_init(ble_spp_client_host_task);
}
```
The main function starts by initializing the non-volatile storage library. This library allows us to save the key-value pairs in flash memory. `nvs_flash_init()` stores the PHY calibration data. In a Bluetooth Low Energy (BLE) device, cryptographic keys used for encryption and authentication are often stored in Non-Volatile Storage (NVS). BLE stores the peer keys, CCCD keys, peer records, etc on NVS. By storing these keys in NVS, the BLE device can quickly retrieve them when needed, without the need for time-consuming key generations.
```c
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
```
## BT Controller and Stack Initialization
The main function calls `nimble_port_init()` to initialize BT Controller and nimble stack. This function initializes the BT controller by first creating its configuration structure named `esp_bt_controller_config_t` with default settings generated by the `BT_CONTROLLER_INIT_CONFIG_DEFAULT()` macro. It implements the Host Controller Interface (HCI) on the controller side, the Link Layer (LL), and the Physical Layer (PHY). The BT Controller is invisible to the user applications and deals with the lower layers of the BLE stack. The controller configuration includes setting the BT controller stack size, priority, and HCI baud rate. With the settings created, the BT controller is initialized and enabled with the `esp_bt_controller_init()` and `esp_bt_controller_enable()` functions:
```c
esp_bt_controller_config_t config_opts = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&config_opts);
```
Next, the controller is enabled in BLE Mode.
```c
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
```
The controller should be enabled in `ESP_BT_MODE_BLE` if you want to use the BLE mode.
There are four Bluetooth modes supported:
1. `ESP_BT_MODE_IDLE`: Bluetooth not running
2. `ESP_BT_MODE_BLE`: BLE mode
3. `ESP_BT_MODE_CLASSIC_BT`: BT Classic mode
4. `ESP_BT_MODE_BTDM`: Dual mode (BLE + BT Classic)
After the initialization of the BT controller, the nimble stack, which includes the common definitions and APIs for BLE, is initialized by using `esp_nimble_init()`:
```c
esp_err_t esp_nimble_init(void)
{
#if !SOC_ESP_NIMBLE_CONTROLLER
/* Initialize the function pointers for OS porting */
npl_freertos_funcs_init();
npl_freertos_mempool_init();
if(esp_nimble_hci_init() != ESP_OK) {
ESP_LOGE(NIMBLE_PORT_LOG_TAG, "hci inits failed\n");
return ESP_FAIL;
}
/* Initialize default event queue */
ble_npl_eventq_init(&g_eventq_dflt);
/* Initialize the global memory pool */
os_mempool_module_init();
os_msys_init();
#endif
/* Initialize the host */
ble_transport_hs_init();
return ESP_OK;
}
```
The main function calls `ble_spp_uart_init`, which is responsible for initializing the UART (Universal Asynchronous Receiver-Transmitter) communication for the BLE SPP client application.
```c
static void ble_spp_uart_init(void)
{
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_RTS,
.rx_flow_ctrl_thresh = 122,
.source_clk = UART_SCLK_DEFAULT,
};
//Install UART driver, and get the queue.
uart_driver_install(UART_NUM_0, 4096, 8192, 10, &spp_common_uart_queue, 0);
//Set UART parameters
uart_param_config(UART_NUM_0, &uart_config);
//Set UART pins
uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
xTaskCreate(ble_client_uart_task, "uTask", 4096, (void *)UART_NUM_0, 8, NULL);
}
```
* It defines a configuration structure (`uart_config_t`) to specify the UART communication settings, such as `baud rate`, `data bits`, `parity`, `stop bits`, `flow control`, and more.
* `uart_driver_install` installs the UART driver, allocating memory for the UART communication buffers and creating a queue (`spp_common_uart_queue`) to manage UART events.
* `uart_param_config` sets the UART parameters based on the configuration structure defined earlier.
* `uart_set_pin` configures the UART pins for communication. In this case, it uses default pin configurations.
* Finally, it creates a FreeRTOS task (`ble_client_uart_task`) that will handle UART communication. This task is responsible for sending and receiving data over UART.
```c
/* Configure the host. */
ble_hs_cfg.reset_cb = ble_spp_client_on_reset;
ble_hs_cfg.sync_cb = ble_spp_client_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
```
The host is configured by setting up the callbacks for Stack-reset, Stack-sync, and Storage status.
```c
/* Initialize data structures to track connected peers. */
rc = peer_init(MYNEWT_VAL(BLE_MAX_CONNECTIONS), 64, 64, 64);
assert(rc == 0);
```
The main function invokes `peer_init()` to initialize memory pools to manage peer, service, characteristics, and descriptor objects in BLE.
```c
/* Set the default device name. */
rc = ble_svc_gap_device_name_set("nimble-ble-spp-client");
assert(rc == 0);
```
The main function calls `ble_svc_gap_device_name_set()` to set the default device name.
```c
/* XXX Need to have template for store */
ble_store_config_init();
```
The main function calls `ble_store_config_init()` to configure the host by setting up the storage callbacks which handle the read, write, and deletion of security material.
```c
nimble_port_freertos_init(ble_spp_client_host_task);
```
The main function ends by creating a task where nimble will run using `nimble_port_freertos_init()`. This enables the nimble stack by using `esp_nimble_enable()`.
## ble_client_uart_task
```c
void ble_client_uart_task(void *pvParameters)
{
ESP_LOGI(tag, "BLE client UART task started");
int rc;
int i;
uart_event_t event;
for (;;) {
//Waiting for UART event.
if (xQueueReceive(spp_common_uart_queue, (void * )&event, (TickType_t)portMAX_DELAY)) {
switch (event.type) {
//Event of UART receving data
case UART_DATA:
if (event.size) {
/* Writing characteristics */
uint8_t *temp = NULL;
temp = (uint8_t *)malloc(sizeof(uint8_t) * event.size);
if (temp == NULL) {
ESP_LOGE(tag, "malloc failed,%s L#%d", __func__, __LINE__);
break;
}
memset(temp, 0x0, event.size);
uart_read_bytes(UART_NUM_0, temp, event.size, portMAX_DELAY);
for ( i = 0; i <= CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++) {
if (attribute_handle[i] != 0) {
rc = ble_gattc_write_flat(i, attribute_handle[i], temp, event.size, NULL, NULL);
if (rc == 0) {
ESP_LOGI(tag, "Write in uart task success!");
} else {
ESP_LOGI(tag, "Error in writing characteristic rc=%d", rc);
}
vTaskDelay(10);
}
}
free(temp);
}
break;
default:
break;
}
}
}
vTaskDelete(NULL);
}
```
This function, `ble_client_uart_task` manages the transfer of data between the UART interface and the Bluetooth connections, allowing for bidirectional communication over BLE.
1. **Initialization**: It starts by logging the initiation of the BLE client UART task. This task handles UART communication for the SPP client.
2. **Event Loop**: The function enters an infinite loop, where it continuously checks for UART events.
3. **Waiting for UART Event**: It waits for a UART event using `xQueueReceive` with an indefinite delay (`portMAX_DELAY`). This allows it to block until there's UART data to process.
4. **Event Type Check**: Once an event is received, it checks the event type. In this context, it's looking for UART data events (`UART_DATA`).
5. **Data Processing**: If UART data is received (i.e., `event.size` is nonzero), it proceeds to handle the data.
- It dynamically allocates memory for a temporary buffer (`temp`) to store the incoming data.
- It reads the UART data into the `temp` buffer using `uart_read_bytes`.
- It then iterates through potential BLE connections (`CONFIG_BT_NIMBLE_MAX_CONNECTIONS`) and checks if there's a valid attribute handle associated with the connection.
- For each valid connection, it attempts to write the data from the `temp` buffer to the corresponding BLE characteristic using `ble_gattc_write_flat`.
- If the write operation is successful, it logs a success message; otherwise, it logs an error message.
- A small delay (`vTaskDelay`) is introduced between write attempts (e.g., to avoid flooding the BLE connection).
- Finally, it frees the temporary buffer (`temp`) to release the allocated memory.
6. **Task Deletion**: After processing, the task is deleted (`vTaskDelete(NULL)`) to free up system resources.
## ble_spp_client_scan()
```c
static void
ble_spp_client_scan(void)
{
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
int rc;
/* Figure out address to use while advertising (no privacy for now) */
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
return;
}
/* Tell the controller to filter duplicates; we don't want to process
* repeated advertisements from the same device.
*/
disc_params.filter_duplicates = 1;
/**
* Perform a passive scan. I.e., don't send follow-up scan requests to
* each advertiser.
*/
disc_params.passive = 1;
/* Use defaults for the rest of the parameters. */
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
ble_spp_client_gap_event, NULL);
if (rc != 0) {
MODLOG_DFLT(ERROR, "Error initiating GAP discovery procedure; rc=%d\n",
rc);
}
}
```
The function `ble_spp_client_scan()` initiates the General Discovery Procedure for scanning nearby BLE devices. The function starts by declaring several variables used in the scanning process. These variable include:
* `own_addr_type`: A uint8_t variable that stores the type of address (public or random) that the device will use while scanning.
* `disc_params`: A struct of type `ble_gap_disc_params` that holds the parameters for the GAP (Generic Access Profile) discovery procedure.
The function uses `ble_hs_id_infer_auto()` to determine the address type (public or random) that the device should use for scanning. The result is stored in the own_addr_type variable.
Configure Discovery Parameters: The function configures the disc_params struct with the following settings:
* `filter_duplicates`: Set to 1, indicating that the controller should filter out duplicate advertisements from the same device. This reduces unnecessary processing of repeated advertisements.
* `passive`: Set to 1, indicating that the scan will be a passive scan. In a passive scan, the scanning device only listens for advertisements without sending any follow-up scan requests to advertisers. It's used for general device discovery.
The function sets some other parameters in the disc_params struct to their default values:
* `itvl`: The scan interval is set to 0, using the default value.
* `window`: The scan window is set to 0, using the default value.
* `filter_policy`: The filter policy is set to 0, using the default value.
* `limited`: The limited discovery mode is set to 0, using the default value.
The function then calls `ble_gap_disc()` to initiate the BLE scanning procedure. It passes the following parameters:
* `own_addr_type`: The address type to use for scanning (determined earlier).
* `BLE_HS_FOREVER`: The duration for which the scan should continue (in this case, indefinitely).
* `&disc_params`: A pointer to the ble_gap_disc_params struct containing the scan parameters.
* `blecent_gap_event`: The callback function to handle the scan events (such as receiving advertisements from nearby devices).
* `NULL`: The argument for the callback context, which is not used in this example.
If an error occurs during the initiation of the scanning procedure, the function prints an error message.
## ble_spp_client_gap_event
The function `ble_spp_client_gap_event` is in responsible of managing various GAP (Generic Access Profile) events that arise during the BLE communication.
The function employs a switch statement to manage diverse types of GAP events that can be received.
* `BLE_GAP_EVENT_DISC`: This case is activated when a new advertisement report is detected during scanning. The function extracts the advertisement data using `ble_hs_adv_parse_fields` and then displays the advertisement fields using the print_adv_fields function. It subsequently verifies if the discovered device is of interest and attempts to establish a connection with it.
* `BLE_GAP_EVENT_CONNECT`: This case is triggered when a new connection is established or when a connection attempt fails. If the Connection was established then the connection descriptor is initiated using the `ble_gap_conn_find()` method else advertisement is resumed. If the connection is successful, it displays the connection descriptor through `print_conn_desc` and stores the peer information. Additionally, it handles optional features such as BLE power control, vendor-specific commands, and security initiation. In the event of a connection attempt failure, it resumes scanning.
* `BLE_GAP_EVENT_DISCONNECT`: This case is activated when a connection is terminated. It prints the reason for disconnection and the connection descriptor before removing information about the peer and resuming scanning.
* `BLE_GAP_EVENT_DISC_COMPLETE`: This case is triggered upon the completion of the GAP discovery process. It displays the reason for the discovery's completion.
* `BLE_GAP_EVENT_NOTIFY_RX`: This case is triggered when the Central device receives a notification or indication from the Peripheral device. It displays information about the received data.
* `BLE_GAP_EVENT_MTU`: This case is activated when the Maximum Transmission Unit (MTU) is updated for a connection. It prints the new MTU value and related information.
## ble_spp_client_should_connect
```c
static int
ble_spp_client_should_connect(const struct ble_gap_disc_desc *disc)
{
struct ble_hs_adv_fields fields;
int rc;
int i;
/* Check if device is already connected or not */
for ( i = 0; i <= CONFIG_BT_NIMBLE_MAX_CONNECTIONS; i++) {
if (memcmp(&connected_addr[i].val, disc->addr.val, PEER_ADDR_VAL_SIZE) == 0) {
MODLOG_DFLT(DEBUG, "Device already connected");
return 0;
}
}
/* The device has to be advertising connectability. */
if (disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_ADV_IND &&
disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_DIR_IND) {
return 0;
}
rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
if (rc != 0) {
return 0;
}
/* The device has to advertise support for the SPP
* service (0xABF0).
*/
for (i = 0; i < fields.num_uuids16; i++) {
if (ble_uuid_u16(&fields.uuids16[i].u) == GATT_SPP_SVC_UUID) {
return 1;
}
}
return 0;
}
```
This code defines a function `ble_spp_client_should_connect` responsible for determining whether the BLE client should connect to a discovered BLE device. Here's an explanation of its functionality:
1. **Device Connection Check**: It first checks if the device is already connected by comparing the device's address (`disc->addr.val`) with the addresses of connected devices stored in the `connected_addr` array. If a match is found, it logs a message and returns 0, indicating that the device should not be connected again.
2. **Advertisement Type Check**: It checks the type of advertisement. The function expects the device to be advertising in either "ADV_IND" (undirected advertising) or "DIR_IND" (directed advertising) mode. If the advertisement type is different, it returns 0, indicating that the device should not be connected.
3. **Advertisement Data Parsing**: The function then parses the advertisement data using the `ble_hs_adv_parse_fields` function to extract information about the advertised services and characteristics.
4. **SPP Service Check**: It checks if the device advertises support for the SPP service by iterating through the list of 16-bit UUIDs (`fields.uuids16`) present in the advertisement data. If it finds a match with the SPP service UUID (0xABF0), it returns 1, indicating that the device should be connected.
In summary, this function checks if a discovered BLE device is eligible for connection based on whether it's already connected, the type of advertisement, and if it advertises support for the SPP service. It returns 1 if the device is eligible for connection and 0 otherwise.
## ble_spp_client_connect_if_interesting
```c
static void
ble_spp_client_connect_if_interesting(const struct ble_gap_disc_desc *disc)
{
uint8_t own_addr_type;
int rc;
/* Don't do anything if we don't care about this advertiser. */
if (!ble_spp_client_should_connect(disc)) {
return;
}
/* Scanning must be stopped before a connection can be initiated. */
rc = ble_gap_disc_cancel();
if (rc != 0) {
MODLOG_DFLT(DEBUG, "Failed to cancel scan; rc=%d\n", rc);
return;
}
/* Figure out address to use for connect (no privacy for now) */
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
return;
}
/* Try to connect the the advertiser. Allow 30 seconds (30000 ms) for
* timeout.
*/
rc = ble_gap_connect(own_addr_type, &disc->addr, 30000, NULL,
ble_spp_client_gap_event, NULL);
if (rc != 0) {
MODLOG_DFLT(ERROR, "Error: Failed to connect to device; addr_type=%d "
"addr=%s; rc=%d\n",
disc->addr.type, addr_str(disc->addr.val), rc);
return;
}
}
```
This function, `ble_spp_client_connect_if_interesting` decides whether to connect to a BLE device based on its advertisement. Here's an explanation of what it does:
1. It takes as input a `struct ble_gap_disc_desc` called `disc`, which represents an advertisement from a nearby BLE device.
2. It checks if the advertisement indicates support for the intended service (in this case, the SPP service) and proceeds to initiate connection, else rejects.
3. If the advertisement is found to be advertising the SPP service, it cancels the ongoing BLE scanning process using `ble_gap_disc_cancel`. This is necessary because you can't initiate a connection while scanning.
4. It determines the address type (`own_addr_type`) to be used for the connection, typically with no privacy considerations for now.
5. Finally, it attempts to establish a connection with the advertiser using `ble_gap_connect`. It specifies a timeout of 30 seconds (30000 ms) for the connection attempt. If the connection attempt fails, it logs an error message.
## ble_spp_client_on_disc_complete
```c
static void
ble_spp_client_on_disc_complete(const struct peer *peer, int status, void *arg)
{
if (status != 0) {
/* Service discovery failed. Terminate the connection. */
MODLOG_DFLT(ERROR, "Error: Service discovery failed; status=%d "
"conn_handle=%d\n", status, peer->conn_handle);
ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
return;
}
/* Service discovery has completed successfully. Now we have a complete
* list of services, characteristics, and descriptors that the peer
* supports.
*/
MODLOG_DFLT(INFO, "Service discovery complete; status=%d "
"conn_handle=%d\n", status, peer->conn_handle);
ble_spp_client_set_handle(peer);
#if CONFIG_BT_NIMBLE_MAX_CONNECTIONS > 1
ble_spp_client_scan();
#endif
}
```
In this code snippet, a function named `ble_spp_client_on_disc_complete` is defined, which is a callback function called when the service discovery of a BLE peer device is completed. Here's a breakdown of what this function does:
1. It takes three parameters:
- `peer`: A pointer to the BLE peer structure representing the connected device.
- `status`: An integer indicating the status of the service discovery operation. A non-zero value typically indicates a failure, while a value of 0 signifies success.
- `arg`: A pointer to an optional user-defined argument (not used in this function).
2. If the `status` is not zero, it means that the service discovery has failed. In this case, the function logs an error message using the `MODLOG_DFLT` macro, indicating the failure and including the `status` and connection handle (`conn_handle`) of the peer device.
3. If the service discovery is successful (i.e., `status` is zero), the function logs an informational message indicating that the service discovery is complete, again including the `status` and `conn_handle`.
4. After a successful service discovery, the `ble_spp_client_set_handle` function is called to set the attribute handle for the peer device. This handle is essential for subsequent BLE operations.
5. Finally, in cases where the application supports multiple connections (`CONFIG_BT_NIMBLE_MAX_CONNECTIONS > 1`), the function `ble_spp_client_scan` is called to initiate scanning for additional BLE devices. This allows the application to connect to multiple devices concurrently.
In summary, this function handles the completion of service discovery for a BLE peer device, taking appropriate actions based on the success or failure of the operation and optionally initiating scanning for more devices when applicable.
## Conclusion
In this tutorial, we have explored a BLE Serial Port Profile (SPP) client using the NimBLE stack. We started by setting up the NimBLE stack and initializing the necessary components. Then, we implemented the client's functionality, including scanning for nearby BLE devices advertising the SPP service, establishing connections, and handling data communication.
**Key Takeaways**:
- **Initialization**: Initialize the NimBLE stack and set up a BLE SPP client application.
- **Scanning**: Scan for nearby BLE devices and filter those advertising the SPP service.
- **Service Discovery**: Establish connections with discovered devices and perform service discovery.
- **Communication**: Handle data communication over the BLE connection, emulating a serial port.