Docs: Added BLE English version of BLE Get Started

This commit is contained in:
Wei Yu Han
2024-10-23 10:14:53 +08:00
committed by Island
parent 12398c0f25
commit 738b2de9ab
8 changed files with 2457 additions and 99 deletions

View File

@@ -1 +1,477 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-connection.rst
Connection
===================
:link_to_translation:`zh_CN:[中文]`
This document is the third tutorial in the Getting Started series on Bluetooth Low Energy (Bluetooth LE), aiming to provide a brief overview of the connection process. Subsequently, the tutorial introduces the code implementation of peripheral devices using the :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` example based on the NimBLE host layer stack.
Learning Objectives
----------------------------------
- Understand the basic concepts of connection
- Learn about connection-related parameters
- Explore the code structure of the :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` example
Basic Concepts
---------------------------------
Initiating a Connection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*With the introduction of extended advertising features in Bluetooth LE 5.0, there are slight differences in the connection establishment process between Legacy ADV and Extended ADV. Below, we take the Legacy ADV connection establishment process as an example.*
When a scanner receives an advertising packet on a specific advertising channel, if the advertiser is connectable, the scanner can send a connection request on the same advertising channel. The advertiser can set a *Filter Accept List* to filter out untrusted devices or accept connection requests from any scanner. Afterward, the advertiser becomes the peripheral device, and the scanner becomes the central device, allowing for bidirectional communication over the data channel.
As described in the section :ref:`Scan Requests and Scan Responses <scan_request_and_scan_response>`, after each advertising period on a channel, the advertiser briefly enters RX mode to receive possible scan requests. In fact, this RX phase can also accept connection requests. Thus, for the scanner, the time window for sending a connection request is similar to that for sending a scan request.
.. figure:: ../../../../_static/ble/ble-advertiser-rx-connection-request.png
:align: center
:scale: 30%
:alt: Initiating a Connection
Initiating a Connection
Connection Interval and Connection Event
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
During a connection, the central and peripheral devices periodically exchange data, with this data exchange cycle referred to as the Connection Interval. The connection interval is one of the connection parameters determined during the initial connection request and can be modified afterward. The step size for the connection interval is 1.25 ms, with a range from 7.5 ms (6 steps) to 4.0 s (3200 steps).
A single data exchange process is termed Connection Event. During a connection event, there can be one or more data packet exchanges (when the data volume is large, it may need to be fragmented). In a data packet exchange, the central device first sends a packet to the peripheral device, followed by a packet from the peripheral device back to the central device. Even if either party does not need to send data at the start of a connection interval, it must send an empty packet to maintain the connection.
The timing relationship between the connection interval and connection event can be referenced in the diagram below.
.. figure:: ../../../../_static/ble/ble-connection-event-and-connection-interval.png
:align: center
:scale: 30%
:alt: Connection Interval and Connection Event
Connection Interval and Connection Event
It's worth noting that if a connection event requires sending a large amount of data, causing the duration of the connection event to exceed the connection interval, the connection event must be split into multiple events. This means that if there isn't enough remaining time in the connection interval to complete the next packet exchange, the next packet exchange must wait until the next connection interval begins.
When the required data exchange frequency is low, a longer connection interval can be set; during the connection interval, the device can sleep outside of connection events to reduce power consumption.
Connection Parameters
------------------------------
As mentioned earlier, the connection interval is a connection parameter whose initial value is given by the central device in the connection request and can be modified in subsequent connections. In addition to the connection interval, there are many other important connection parameters. Below, we will explain some of these key parameters.
Supervision Timeout
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Supervision Timeout defines the maximum time allowed between two successful connection events. If a successful connection event is followed by a period longer than the supervision timeout without another successful connection event, the connection is considered to be disconnected. This parameter is critical for maintaining connection status; for example, if one party unexpectedly loses power or moves out of range, the other party can determine whether to disconnect to conserve communication resources by checking for a timeout.
Peripheral Latency
^^^^^^^^^^^^^^^^^^^^^^^^^^
Peripheral Latency specifies the maximum number of connection events that the peripheral device can skip when there is no data to send.
To understand the purpose of this parameter, consider a Bluetooth mouse as an example. When a user is typing on a keyboard, the mouse may not have any data to send, so its preferable to reduce the frequency of data packet transmissions to save power. Conversely, during mouse usage, we want the mouse to send data as quickly as possible to minimize latency. This means that the data transmission from the Bluetooth mouse is intermittently high-frequency. If we rely solely on the connection interval for adjustments, a lower connection interval would lead to high energy consumption, while a higher connection interval would result in high latency.
In this scenario, the peripheral latency mechanism is a perfect solution. To reduce the latency of a Bluetooth mouse, we can set a smaller connection interval, such as 10 ms, which allows a data exchange frequency of up to 100 Hz during intensive use. We can then set the peripheral latency to 100, allowing the mouse to effectively reduce the data exchange frequency to 1 Hz when idle. This design achieves variable data exchange frequency without adjusting connection parameters, maximizing user experience.
Maximum Transmission Unit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Maximum Transmission Unit (MTU) refers to the maximum byte size of a single ATT data packet. Before discussing the MTU parameter, it's essential to describe the structure of the Data Channel Packet.
The structure of the Data Channel Packet is similar to that of the :ref:`Advertising Packet <adv_packet_structure>`, with differences in the PDU structure. The data PDU can be divided into three parts:
.. list-table::
:align: center
:widths: 10 30 20 40
:header-rows: 1
* - No.
- Name
- Byte Size
- Notes
* - 1
- Header
- 2
-
* - 2
- Payload
- 0-27 / 0-251
- Before Bluetooth LE 4.2, the maximum payload was 27 bytes; Bluetooth LE 4.2 introduced Data Length Extension (DLE), allowing a maximum payload of 251 bytes.
* - 3
- Message Integrity Check, MIC
- 4
- Optional
The payload of the data PDU can be further divided into:
.. list-table::
:align: center
:widths: 10 70 20
:header-rows: 1
* - No.
- Name
- Byte Size
* - 1
- L2CAP Header
- 4
* - 2
- ATT Header + ATT Data
- 0-23 / 0-247
The default MTU value is 23 bytes, which matches the maximum ATT data byte size that can be carried in a single data PDU before Bluetooth LE 4.2.
MTU can be set to larger values, such as 140 bytes. Before Bluetooth LE 4.2, with a maximum of 23 bytes carrying ATT data in the payload, a complete ATT data packet would need to be split across multiple data PDUs. After Bluetooth LE 4.2, a single data PDU can carry up to 247 bytes of ATT data, so an MTU of 140 bytes can still be accommodated in a single data PDU.
Hands-On Practice
--------------------------
Having understood the concepts related to connections, lets move on to the :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` example code to learn how to build a simple peripheral device using the NimBLE stack.
Prerequisites
^^^^^^^^^^^^^^^^^^^^
1. An {IDF_TARGET_NAME} development board
2. ESP-IDF development environment
3. The **nRF Connect for Mobile** app installed on your phone
If you have not yet completed the ESP-IDF development environment setup, please refer to :doc:`IDF Get Started <../../../get-started/index>`.
Try It Out
^^^^^^^^^^^^^^^^^^^^^^
Building and Flashing
##########################
The reference example for this tutorial is :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>`.
You can navigate to the example directory using the following command:
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_Connection
Please replace `<ESP-IDF Path>` with your local ESP-IDF folder path. Then, you can open the NimBLE_Connection project using VSCode or another IDE you prefer. For example, after navigating to the example directory via the command line, you can open the project in VSCode using the following command:
.. code-block:: shell
$ code .
Next, enter the ESP-IDF environment in the command line and set the target chip:
.. code-block:: shell
$ idf.py set-target <chip-name>
You should see messages like:
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
These messages indicate that the chip has been successfully configured. Then, connect the development board to your computer and run the following command to build the firmware, flash it to the board, and monitor the serial output from the {IDF_TARGET_NAME} development board:
.. code-block:: shell
$ idf.py flash monitor
You should see messages like:
.. code-block:: shell
...
main_task: Returned from app_main()
Wait until the notification ends.
Connect and Disconnect
############################
Open the **nRF Connect for Mobile** app on your phone, pull down to refresh in the **SCANNER** tab, and locate the NimBLE_CONN device as shown in the image below.
.. figure:: ../../../../_static/ble/ble-connection-device-list.jpg
:align: center
:scale: 30%
Locate NimBLE_CONN Device
If the device list is long, it's recommended to filter by the keyword "NimBLE" to quickly find the NimBLE_CONN device.
Compared to :ref:`NimBLE_Beacon <nimble_beacon_details>`, you can observe that most of the advertising data is consistent, but there is an additional Advertising Interval data with a value of 500 ms. Below the **CONNECT** button, you should also see that the advertising interval is around 510 ms.
Click the **CONNECT** button to connect to the device, and you should be able to see the GAP service on your phone as shown below.
.. figure:: ../../../../_static/ble/ble-connection-connected.jpg
:align: center
:scale: 30%
Connected to NimBLE_CONN Device
At this point, you should also see the LED on the development board light up. Click **DISCONNECT** to disconnect from the device, and the LED on the development board should turn off.
If your development board does not have any other LEDs except the one for the power indicator, you should be able to observe the corresponding status indicators in the log output.
Viewing Log Output
##########################
When connected to the device, you should see logs similar to the following:
.. code-block::
I (36367) NimBLE_Connection: connection established; status=0
I (36367) NimBLE_Connection: connection handle: 0
I (36367) NimBLE_Connection: device id address: type=0, value=CE:4E:F7:F9:55:60
I (36377) NimBLE_Connection: peer id address: type=1, value=7F:BE:AD:66:6F:45
I (36377) NimBLE_Connection: conn_itvl=36, conn_latency=0, supervision_timeout=500, encrypted=0, authenticated=0, bonded=0
I (36397) NimBLE: GAP procedure initiated:
I (36397) NimBLE: connection parameter update; conn_handle=0 itvl_min=36 itvl_max=36 latency=3 supervision_timeout=500 min_ce_len=0 max_ce_len=0
I (36407) NimBLE:
I (37007) NimBLE_Connection: connection updated; status=0
I (37007) NimBLE_Connection: connection handle: 0
I (37007) NimBLE_Connection: device id address: type=0, value=CE:4E:F7:F9:55:60
I (37007) NimBLE_Connection: peer id address: type=1, value=7F:BE:AD:66:6F:45
I (37017) NimBLE_Connection: conn_itvl=36, conn_latency=3, supervision_timeout=500, encrypted=0, authenticated=0, bonded=0
The first part of the log shows the connection information output by the device when the connection is established, including the connection handle, the Bluetooth addresses of both the device and the mobile phone, as well as the connection parameters. Here, `conn_itvl` refers to the connection interval, `conn_latency` indicates the peripheral latency, and `supervision_timeout` is the connection timeout parameter. Other parameters can be temporarily ignored.
The second part indicates that the device initiated an update to the connection parameters, requesting to set the peripheral latency to 3.
The third part of the log displays the connection information after the update, showing that the peripheral latency has been successfully updated to 3, while other connection parameters remain unchanged.
When the device disconnects, you should see logs similar to the following:
.. code-block::
I (63647) NimBLE_Connection: disconnected from peer; reason=531
I (63647) NimBLE: GAP procedure initiated: advertise;
I (63647) NimBLE: disc_mode=2
I (63647) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=800 adv_itvl_max=801
I (63657) NimBLE:
I (63657) NimBLE_Connection: advertising started!
You can observe that the device outputs the reason for disconnection when the connection is terminated, and then it initiates advertising again.
Code Details
------------------------------
Project Structure Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_project_structure:
The root directory structure of :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` is identical to that of :ref:`NimBLE_Beacon <nimble_beacon_project_structure>`. However, after building the firmware, you may notice an additional `managed_components` directory in the root, which contains dependencies automatically included during firmware construction; in this case, it's the `led_strip` component used to control the development board's LED. This dependency is referenced in the `main/idf_component.yml` file.
Additionally, LED control-related source code has been introduced in the `main` folder.
Program Behavior Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_program_behavior:
The behavior of this example is mostly consistent with that of :ref:`NimBLE_Beacon <nimble_beacon_program_behavior>`, with the key difference being that this example can accept scan requests from scanners and enter a connected state after entering advertising mode. Furthermore, it utilizes a callback function, `gap_event_handler`, to handle connection events and respond accordingly, such as turning on the LED when a connection is established and turning it off when the connection is terminated.
Entry Function
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_entry_point:
The entry function of this example is nearly the same as that of :ref:`NimBLE_Beacon <nimble_beacon_entry_point>`, except that before initializing NVS Flash, we call the `led_init` function to initialize the LED.
Starting Advertising
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The process for initiating advertising is largely similar to that of :ref:`NimBLE_Beacon <nimble_beacon_start_advertising>`, but there are some details to note.
First, we've added the advertising interval parameter in the scan response. We want to set the advertising interval to 500 ms, and since the unit for the advertising interval is 0.625 ms, we need to set it to `0x320`. However, NimBLE provides a unit conversion macro `BLE_GAP_ADV_ITVL_MS`, which allows us to avoid manual calculations, as shown below:
.. code-block:: 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;
...
}
Next, we want the device to be connectable, so we need to modify the advertising mode from non-connectable to connectable. Additionally, the advertising interval parameter set in the scan response serves only to inform other devices and does not affect the actual advertising interval. This parameter must be set in the advertising parameter structure to take effect. Here, we set the minimum and maximum values of the advertising interval to 500 ms and 510 ms, respectively. Finally, we want to handle GAP events using the callback function `gap_event_handler`, so we pass this callback to the API `ble_gap_adv_start` that starts advertising. The relevant code is as follows:
.. code-block:: 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;
/* 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!");
...
}
When the return value of `ble_gap_adv_start` is 0, it indicates that the device has successfully initiated advertising. Subsequently, the NimBLE protocol stack will call the `gap_event_handler` callback function whenever a GAP event is triggered, passing the corresponding GAP event.
GAP Event Handling
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In this example, we handle three different types of GAP events:
- Connection Event `BLE_GAP_EVENT_CONNECT`
- Disconnection Event `BLE_GAP_EVENT_DISCONNECT`
- Connection Update Event `BLE_GAP_EVENT_CONN_UPDATE`
The connection event is triggered when a connection is successfully established or when a connection attempt fails. If the connection fails, we will restart advertising. If the connection is successful, we will log the connection information, turn on the LED, and initiate a connection parameter update to set the peripheral latency parameter to 3. Heres how the code looks:
.. code-block:: C
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, &params);
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;
...
}
return rc;
}
The disconnection event is triggered when either party disconnects from the connection. At this point, we log the reason for the disconnection, turn off the LED, and restart advertising. Heres the code:
.. code-block:: 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;
...
}
The connection update event is triggered when the connection parameters are updated. At this point, we log the updated connection information. Heres the code:
.. code-block:: 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;
...
}
Summary
----------------------
Through this tutorial, you have learned the basic concepts of connections and how to use the NimBLE host stack to build a Bluetooth LE peripheral device using the :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` example.
You can try to modify parameters in the example and observe the results in the log output. For instance, you can change the peripheral latency or connection timeout parameters to see if the modifications trigger connection update events.

View File

@@ -1 +1,660 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-data-exchange.rst
Data Exchange
========================
:link_to_translation:`zh_CN:[中文]`
This document is the fourth tutorial in the Getting Started series on Bluetooth Low Energy (Bluetooth LE), aiming to provide a brief overview of the data exchange process within Bluetooth LE connections. Subsequently, this tutorial introduces the code implementation of a GATT server, using the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` example based on the NimBLE host layer stack.
Learning Objectives
----------------------------
- Understand the data structure details of characteristic data and services
- Learn about different data access operations in GATT
- Learn about the code structure of the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` example
GATT Data Characteristics and Services
--------------------------------------------------
GATT services are the infrastructure for data exchange between two devices in a Bluetooth LE connection, with the minimum data unit being an attribute. In the section on :ref:`Data Representation and Exchange <gatt_att_introduction>`, we briefly introduced the attributes at the ATT layer and the characteristic data, services, and specifications at the GATT layer. Below are details regarding the attribute-based data structure.
Attributes
^^^^^^^^^^^^^^^^^^^^^^
An attribute consists of the following four parts:
.. list-table::
:align: center
:widths: 10 30 60
:header-rows: 1
* - No.
- Name
- Description
* - 1
- Handle
- A 16-bit unsigned integer representing the index of the attribute in the :ref:`attribute table <attribute_table>`
* - 2
- Type
- ATT attributes use UUID (Universally Unique Identifier) to differentiate types
* - 3
- Access Permission
- Indicates whether encryption/authorization is needed; whether it is readable or writable
* - 4
- Value
- Actual user data or metadata of another attribute
There are two types of UUIDs in Bluetooth LE:
1. 16-bit UUIDs defined by SIG
2. 128-bit UUIDs customized by manufacturers
Common characteristic and service UUIDs are provided in SIG's `Assigned Numbers <https://www.bluetooth.com/specifications/assigned-numbers/>`_ standard document, such as:
.. list-table::
:align: center
:widths: 20 60 20
:header-rows: 1
* - Category
- Type Name
- UUID
* - Service
- Blood Pressure Service
- `0x1810`
* - Service
- Common Audio Service
- `0x1853`
* - Characteristic Data
- Age
- `0x2A80`
* - Characteristic Data
- Appearance
- `0x2A01`
In fact, the definitions of these services and characteristic data are also provided by the SIG. For example, the value of the Heart Rate Measurement must include a flag field and a heart rate measurement field, and may include fields such as energy expended, RR-interval, and transmission interval, among others. Therefore, these definitions from SIG allow Bluetooth LE devices from different manufacturers to recognize each other's services or characteristic data, enabling cross-manufacturer communication.
Manufacturers' customized 128-bit UUIDs are used for proprietary services or data characteristics, such as the UUID for the LED characteristic in this example: `0x00001525-1212-EFDE-1523-785FEABCD123`.
Characteristic Data
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _characteristic_attributes:
A characteristic data item typically consists of the following attributes:
.. list-table::
:align: center
:widths: 10 30 30 30
:header-rows: 1
* - No.
- Type
- Function
- Notes
* - 1
- Characteristic Declaration
- Contains properties, handle, and UUID info for the characteristic value
- UUID is 0x2803, read-only
* - 2
- Characteristic Value
- user data
- UUID identifies the characteristic type
* - 3
- Characteristic Descriptor
- Additional description for the characteristic data
- Optional attribute
Relationship between Characteristic Declaration and Characteristic Value
################################################################################
Using the Heart Rate Measurement as an example, the relationship between the characteristic declaration and characteristic value is illustrated as follows:
The table below is an attribute table, containing two attributes of the Heart Rate Measurement characteristic. Let's first look at the attribute with handle 0. Its UUID is `0x2803`, and the access permission is read-only, indicating that this is a characteristic declaration attribute. The attribute value shows that the read/write property is read-only, and the handle points to 1, indicating that the attribute with handle 1 is the value attribute for this characteristic. The UUID is `0x2A37`, meaning that this characteristic type is Heart Rate Measurement.
Now, let's examine the attribute with handle 1. Its UUID is `0x2A37`, and the access permission is also read-only, corresponding directly with the characteristic declaration attribute. The value of this attribute consists of flag bits and measurement values, which complies with the SIG specification for Heart Rate Measurement characteristic data.
+-------------+--------------+-----------------+-------------------------+----------------------------+
| Handle | UUID | Permissions | Value | Attribute Type |
+=============+==============+=================+=========================+============================+
| 0 | `0x2803` | Read-only | Properties = Read-only | Characteristic Declaration |
| | | +-------------------------+ |
| | | | Handle = 1 | |
| | | +-------------------------+ |
| | | | UUID = `0x2A37` | |
+-------------+--------------+-----------------+-------------------------+----------------------------+
| 1 | `0x2A37` | Read-only | Flags | Characteristic Value |
| | | +-------------------------+ |
| | | | Measurement value | |
+-------------+--------------+-----------------+-------------------------+----------------------------+
Characteristic Descriptors
#########################################
Characteristic descriptors provide supplementary information about characteristic data. The most common is the Client Characteristic Configuration Descriptor (CCCD). When a characteristic supports server-initiated :ref:`data operations <gatt_data_operation>` (notifications or indications), CCCD must be used to describe the relevant information. This is a read-write attribute that allows the GATT client to inform the server whether notifications or indications should be enabled. Writing to this value is also referred to as subscribing or unsubscribing.
The UUID for CCCD is `0x2902`, and its attribute value contains only 2 bits of information. The first bit indicates whether notifications are enabled, and the second bit indicates whether indications are enabled. By adding the CCCD to the attribute table and providing indication access permissions for the Heart Rate Measurement characteristic data, we obtain the complete form of the Heart Rate Measurement characteristic data in the attribute table as follows:
+-------------+--------------+-----------------+-----------------------------+----------------------------+
| Handle | UUID | Permissions | Value | Attribute Type |
+=============+==============+=================+=============================+============================+
| 0 | `0x2803` | Read-only | Properties = Read/Indicate | Characteristic Declaration |
| | | +-----------------------------+ |
| | | | Handle = 1 | |
| | | +-----------------------------+ |
| | | | UUID = `0x2A37` | |
+-------------+--------------+-----------------+-----------------------------+----------------------------+
| 1 | `0x2A37` | Read/Indicate | Flags | Characteristic Value |
| | | +-----------------------------+ |
| | | | Measurement value | |
+-------------+--------------+-----------------+-----------------------------+----------------------------+
| 2 | `0x2902` | Read/Write | Notification status | Characteristic Descriptor |
| | | +-----------------------------+ |
| | | | Indication status | |
+-------------+--------------+-----------------+-----------------------------+----------------------------+
Services
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The data structure of a service can be broadly divided into two parts:
.. list-table::
:align: center
:widths: 20 80
:header-rows: 1
* - No.
- Name
* - 1
- Service Declaration Attribute
* - 2
- Characteristic Definition Attributes
The three characteristic data attributes mentioned in the :ref:`Characteristic Data <characteristic_attributes>` belong to characteristic definition attributes. In essence, the data structure of a service consists of several characteristic data attributes along with a service declaration attribute.
The UUID for the service declaration attribute is 0x2800, which is read-only and holds the UUID identifying the service type. For example, the UUID for the Heart Rate Service is `0x180D`, so its service declaration attribute can be represented as follows:
.. list-table::
:align: center
:widths: 10 20 20 20 30
:header-rows: 1
* - Handle
- UUID
- Permissions
- Value
- Attribute Type
* - 0
- `0x2800`
- Read-only
- `0x180D`
- Service Declaration
Attribute Example
^^^^^^^^^^^^^^^^^^^^^^^^
.. _attribute_table:
The following is an example of a possible attribute table for a GATT server, using the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` as an illustration. The example includes two services: the Heart Rate Service and the Automation IO Service. The former contains a Heart Rate Measurement characteristic, while the latter includes an LED characteristic. The complete attribute table for the GATT server is as follows:
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| Handle | UUID | Permissions | Value | Attribute Type |
+=============+==========================================+=================+=================================================+============================+
| 0 | `0x2800` | Read-only | UUID = `0x180D` | Service Declaration |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 1 | `0x2803` | Read-only | Properties = Read/Indicate | Characteristic Declaration |
| | | +-------------------------------------------------+ |
| | | | Handle = 2 | |
| | | +-------------------------------------------------+ |
| | | | UUID = `0x2A37` | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 2 | `0x2A37` | Read/Indicate | Flags | Characteristic Value |
| | | +-------------------------------------------------+ |
| | | | Measurement value | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 3 | `0x2902` | Read/Write | Notification status | Characteristic Descriptor |
| | | +-------------------------------------------------+ |
| | | | Indication status | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 4 | `0x2800` | Read-only | UUID = `0x1815` | Service Declaration |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 5 | `0x2803` | Read-only | Properties = Write-only | Characteristic Declaration |
| | | +-------------------------------------------------+ |
| | | | Handle = 6 | |
| | | +-------------------------------------------------+ |
| | | | UUID = `0x00001525-1212-EFDE-1523-785FEABCD123` | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 6 | `0x00001525-1212-EFDE-` |Write-only | LED status |Characteristic Value |
| | `1523-785FE` | | | |
| | `ABCD123` | | | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
When a GATT client first establishes communication with a GATT server, it pulls metadata from the server's attribute table to discover the available services and characteristics. This process is known as *Service Discovery*.
GATT Data Operations
------------------------------
.. _gatt_data_operation:
Data operations refer to accessing characteristic data on a GATT server, which can be mainly categorized into two types:
1. Client-initiated operations
2. Server-initiated operations
Client-initiated Operations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Client-initiated operations include the following three types:
- **Read**
- A straightforward operation to pull the current value of a specific characteristic from the GATT server.
- **Write**
- Standard write operations require confirmation from the GATT server upon receiving the client's write request and data.
- **Write without response**
- This is another form of write operation that does not require server acknowledgment.
Server-Initiated Operations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Server-initiated operations are divided into two types:
- **Notify**
- A GATT server actively pushes data to the client without requiring a confirmation response.
- **Indicate**
- Similar to notifications, but this requires confirmation from the client, which makes indication slower than notification.
Although both notifications and indications are initiated by the server, the prerequisite for these operations is that the client has enabled notifications or indications. Therefore, the data exchange process in GATT essentially begins with a client request for data.
Hands-On Practice
----------------------------
Having grasped the relevant knowledge of GATT data exchange, lets combine the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` example code to learn how to build a simple GATT server using the NimBLE protocol stack and put our knowledge into practice.
Prerequisites
^^^^^^^^^^^^^^^^^^
1. An {IDF_TARGET_NAME} development board
2. ESP-IDF development environment
3. The nRF Connect for Mobile application installed on your phone
If you have not completed the ESP-IDF development environment setup, please refer to :doc:`IDF Get Started <../../../get-started/index>`.
Try It Out
^^^^^^^^^^^^^^^^^^
Please refer to :ref:`BLE Introduction Try It Out <nimble_gatt_server_practice>`
Code Explanation
-------------------------------
Project Structure Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The root directory structure of :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` is identical to that of :ref:`NimBLE_Connection <nimble_connection_project_structure>`. Additionally, the `main` folder includes source code related to the GATT service and simulated heart rate generation.
Program Behavior Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The program behavior of this example is largely consistent with that of :ref:`NimBLE_Connection <nimble_connection_project_structure>`, with the difference being that this example adds GATT services and handles access to GATT characteristic data through corresponding callback functions.
Entry Function
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_gatt_server_entry_point:
Based on :ref:`NimBLE_Connection <nimble_connection_entry_point>`, a process to initialize the GATT service by calling the `gatt_svc_init` function has been added. Moreover, in addition to the NimBLE thread, a new `heart_rate_task` thread has been introduced, responsible for the random generation of simulated heart rate measurement data and indication handling. Relevant code is as follows:
.. code-block:: C
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) {
...
xTaskCreate(heart_rate_task, "Heart Rate", 4*1024, NULL, 5, NULL);
return;
}
The `heart_rate_task` thread runs at a frequency of 1 Hz, as `HEART_RATE_TASK_PERIOD` is defined as 1000 ms. Each time it executes, the thread calls the `update_heart_rate` function to randomly generate a new heart rate measurement and then calls `send_heart_rate_indication` to handle the indication operation.
GATT Service Initialization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In the `gatt_svc.c` file, there is a GATT service initialization function as follows:
.. code-block:: C
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;
}
This function first calls the `ble_svc_gatt_init` API to initialize the GATT Service. It's important to note that this GATT Service is a special service with the UUID `0x1801`, which is used by the GATT server to notify clients when services change (i.e., when GATT services are added or removed). In such cases, the client will re-execute the service discovery process to update its service information.
Next, the function calls `ble_gatts_count_cfg` and `ble_gatts_add_svcs` APIs to add the services and characteristic data defined in the `gatt_svr_svcs` service table to the GATT server.
GATT Service Table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The `gatt_svr_svcs service` table is a crucial data structure in this example, defining all services and characteristic data used. The relevant code is as follows:
.. code-block:: C
/* Heart rate service */
static const ble_uuid16_t heart_rate_svc_uuid = BLE_UUID16_INIT(0x180D);
...
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;
...
/* 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. */
},
};
The macros `BLE_UUID16_INIT` and `BLE_UUID128_INIT` provided by the NimBLE protocol stack allow for convenient conversion of 16-bit and 128-bit UUIDs from raw data into `ble_uuid16_t` and `ble_uuid128_t` type variables.
The `gatt_svr_svcs` is an array of structures of type `ble_gatt_svc_def`. The `ble_gatt_svc_def` structure defines a service, with key fields being `type`, `uuid`, and `characteristics`. The `type` field indicates whether the service is primary or secondary, with all services in this example being primary. The `uuid` field represents the UUID of the service. The `characteristics` field is an array of `ble_gatt_chr_def` structures that stores the characteristics associated with the service.
The `ble_gatt_chr_def` structure defines the characteristics, with key fields being `uuid`, `access_cb`, `flags`, and `val_handle`. The `uuid` field is the UUID of the characteristic. The `access_cb` field points to the access callback function for that characteristic. The `flags` field indicates the access permissions for the characteristic data. The `val_handle` field points to the variable handle address for the characteristic value.
It's important to note that when the `BLE_GATT_CHR_F_INDICATE` flag is set for a characteristic, the NimBLE protocol stack automatically adds the CCCD, so there's no need to manually add the descriptor.
Based on variable naming, it's clear that `gatt_svr_svcs` implements all property definitions in the :ref:`attribute table <attribute_table>`. Additionally, access to the Heart Rate Measurement characteristic is managed through the `heart_rate_chr_access` callback function, while access to the LED characteristic is managed through the `led_chr_access` callback function.
Characteristic Data Access Management
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LED Access Management
#####################################
Access to the LED characteristic data is managed through the `led_chr_access` callback function, with the relevant code as follows:
.. code-block:: 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;
}
When the GATT client initiates access to the LED characteristic data, the NimBLE protocol stack will call the `led_chr_access` callback function, passing in the handle information and access context. The `op` field of `ble_gatt_access_ctxt` is used to identify different access events. Since the LED is a write-only characteristic, we only handle the `BLE_GATT_ACCESS_OP_WRITE_CHR` event.
In this processing branch, we first validate the attribute handle to ensure that the client is accessing the LED characteristic. Then, based on the `om` field of `ble_gatt_access_ctxt`, we verify the length of the access data. Finally, we check if the data in `om_data` is equal to 1 to either turn the LED on or off.
If any other access events occur, they are considered unexpected, and we proceed to the error branch to return.
Heart Rate Measurement Read Access Management
######################################################
The heart rate measurement is a readable and indicative characteristic. The read access initiated by the client for heart rate measurement values is managed by the `heart_rate_chr_access` callback function, with the relevant code as follows:
.. code-block:: 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;
}
Similar to the LED access management, we use the `op` field of the `ble_gatt_access_ctxt` access context to determine the access event, handling the `BLE_GATT_ACCESS_OP_READ_CHR` event.
In the handling branch, we first validate the attribute handle to confirm that the client is accessing the heart rate measurement attribute. Then, we call the `get_heart_rate` function to retrieve the latest heart rate measurement, storing it in the measurement area of the `heart_rate_chr_val` array. Finally, we copy the data from `heart_rate_chr_val` into the `om` field of the `ble_gatt_access_ctxt` access context. The NimBLE protocol stack will send the data in this field to the client after the current callback function ends, thus achieving read access to the Heart Rate Measurement characteristic value.
Heart Rate Measurement Indication
#############################################
When the client enables indications for heart rate measurements, the processing flow is a bit more complicated. First, enabling or disabling the heart rate measurement indications is a subscription or unsubscription event at the GAP layer, so we need to add a handling branch for subscription events in the `gap_event_handler` callback function, as follows:
.. code-block:: 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;
}
The subscription event is represented by `BLE_GAP_EVENT_SUBSCRIBE`. In this handling branch, we do not process the subscription event directly; instead, we call the `gatt_svr_subscribe_cb` callback function to handle the subscription event. This reflects the layered design philosophy of software, as the subscription event affects the GATT server's behavior in sending characteristic data and is not directly related to the GAP layer. Thus, it should be passed to the GATT layer for processing.
Next, let's take a look at the operations performed in the `gatt_svr_subscribe_cb` callback function.
.. code-block:: 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;
}
}
In this example, the callback handling is quite simple: it checks whether the attribute handle in the subscription event corresponds to the heart rate measurement attribute handle. If it does, it saves the corresponding connection handle and updates the indication status requested by the client.
As mentioned in :ref:`Entry Function <nimble_gatt_server_entry_point>`, the `send_heart_rate_indication` function is called by the `heart_rate_task` thread at a frequency of 1 Hz. The implementation of this function is as follows:
.. code-block:: 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!");
}
}
The `ble_gatts_indicate` function is an API provided by the NimBLE protocol stack for sending indications. This means that when the indication status for the heart rate measurement is true and the corresponding connection handle is available, calling the `send_heart_rate_indication` function will send the heart rate measurement to the GATT client.
To summarize, when a GATT client subscribes to heart rate measurements, the `gap_event_handler` receives the subscription event and passes it to the `gatt_svr_subscribe_cb` callback function, which updates the subscription status for heart rate measurements. In the `heart_rate_task` thread, it checks the subscription status every second; if the status is true, it sends the heart rate measurement to the client.
Summary
--------------------------------
Through this tutorial, you have learned how to create GATT services and their corresponding characteristic data using a service table, and you mastered the management of access to GATT characteristic data, including read, write, and subscription operations. You can now build more complex GATT service applications based on the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` example.

View File

@@ -1 +1,885 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-device-discovery.rst
Device Discovery
=======================
:link_to_translation:`zh_CN:[中文]`
This document is the second tutorial in the Getting Started series on Bluetooth Low Energy (Bluetooth LE), aiming to provide a brief overview of the Bluetooth LE device discovery process, including basic concepts related to advertising and scanning. Following this, the tutorial introduces the code implementation of Bluetooth LE advertising, using the :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` example based on the NimBLE host layer stack.
Learning Objectives
-----------------------
- Understand the basic concepts of Advertising
- Understand the basic concepts of Scanning
- Learn about the code structure of the :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` example
Advertising and Scanning are the states of Bluetooth LE devices during the device discovery phase before establishing a connection. First, lets understand the basic concepts related to advertising.
Basic Concepts of Advertising
---------------------------------
Advertising is the process where a device sends out advertising packets via its Bluetooth antenna. Since the advertiser does not know whether there is a receiver in the environment or when the receiver will activate its antenna, it needs to send advertising packets periodically until a device responds. During this process, there are several questions for the advertiser to consider:
1. Where should the advertising packets be sent? (Where?)
2. How long should the interval between advertising packets be? (When?)
3. What information should be included in the advertising packets? (What?)
Where to Send Advertising Packets?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Bluetooth Radio Frequency Band
########################################
The first question pertains to which radio frequency band the advertising packets should be sent on. The answer is provided by the Bluetooth Core Specification: the 2.4 GHz ISM band. This band is a globally available, license-free radio frequency band that is not controlled by any country for military or other purposes, and does not require payment to any organization. Thus, it has high availability and no usage costs. However, this also means the 2.4 GHz ISM band is very crowded and may interfere with other wireless communication protocols such as 2.4 GHz WiFi.
Bluetooth Channels
############################
Similar to Bluetooth Classic, the Bluetooth SIG has adopted Adaptive Frequency Hopping (AFH) in Bluetooth LE to address data collision issues. This technology can assess the congestion of RF channels and avoid crowded channels through frequency hopping to improve communication quality. However, unlike Bluetooth Classic, Bluetooth LE uses the 2.4 GHz ISM band divided into 40 RF channels, each with a 2 MHz bandwidth, ranging from 2402 MHz to 2480 MHz, while Bluetooth Classic uses 79 RF channels, each with a 1 MHz bandwidth.
In the Bluetooth LE 4.2 standard, RF channels are categorized into two types, as follows:
.. list-table::
:align: center
:widths: 30 20 20 30
:header-rows: 1
* - Type
- Quantity
- Index
- Purpose
* - Advertising Channel
- 3
- 37-39
- Used for sending advertising packets and scan response packets
* - Data Channel
- 37
- 0-36
- Used for sending data channel packets
During advertising, the advertiser will send advertising packets on the three advertising channels (37-39). Once the advertising packets have been sent on all three channels, the advertising process is considered complete, and the advertiser will repeat the process at the next advertising interval.
Extended Advertising Features
##################################
In the Bluetooth LE 4.2 standard, advertising packets are limited to a maximum of 31 bytes, which restricts the functionality of advertising. To enhance the capability of advertising, Bluetooth 5.0 introduced the Extended Advertising feature. This feature divides advertising packets into:
.. list-table::
:align: center
:widths: 40 20 20 20
:header-rows: 1
* - Type
- Abbreviation
- Max Payload Size per Packet (Bytes
- Max Total Payload Size (Bytes)
* - Primary Advertising Packet
- Legacy ADV
- 31
- 31
* - Extended Advertising Packet
- Extended ADV
- 254
- 1650
Extended advertising packets are composed of `ADV_EXT_IND` and `AUX_ADV_IND`, transmitted on the primary and secondary advertising channels, respectively. The primary advertising channels correspond to channels 37-39, while the secondary advertising channels correspond to channels 0-36. Since the receiver always receives advertising data on the primary advertising channels, the advertiser must send `ADV_EXT_IND` on the primary advertising channels and `AUX_ADV_IND` on the secondary advertising channels. `ADV_EXT_IND` will indicate the secondary advertising channels where `AUX_ADV_IND` is transmitted. This mechanism allows the receiver to obtain the complete extended advertising packet by first receiving `ADV_EXT_IND` on the primary advertising channels and then going to the specified secondary advertising channels to receive `AUX_ADV_IND`.
.. list-table::
:align: center
:widths: 30 40 30
:header-rows: 1
* - Type
- Channels
- Purpose
* - Primary Advertising Channel
- 37-39
- Used to transmit `ADV_EXT_IND` of the extended advertising packet
* - Secondary Advertising Channel
- 0-36
- Used to transmit `AUX_ADV_IND` of the extended advertising packet
How long should the advertising interval be?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Advertising Interval
#########################
For the second question, regarding the period for sending advertising packets, the Bluetooth standard provides a clear parameter definition: Advertising Interval. The advertising interval can range from 20 ms to 10.24 s, with a step size of 0.625 ms.
The choice of advertising interval affects both the discoverability of the advertiser and the devices power consumption. If the advertising interval is too long, the probability of the advertising packets being received by a receiver becomes very low, which decreases the advertisers discoverability. Conversely, if the advertising interval is too short, frequent advertising consumes more power. Therefore, the advertiser needs to balance between discoverability and power consumption and choose the most appropriate advertising interval based on the application's needs.
It is worth noting that if there are two advertisers with the same advertising interval in the same space, packet collision may occur, meaning both advertisers are sending advertising data to the same channel at the same time. Since advertising is a one-way process with no reception, the advertiser cannot know if a packet collision has occurred. To reduce the likelihood of such collisions, advertisers should add a random delay of 0-10 ms after each advertising event.
What information is included in the advertising packet?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Advertising Packet Structure
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For the third question, regarding the information contained in the advertising packet, the Bluetooth LE 4.2 standard defines the format of the advertising packet, as shown in the diagram below:
.. _adv_packet_structure:
.. figure:: ../../../../_static/ble/ble-4.2-adv-packet-structure.png
:align: center
:scale: 35%
:alt: Advertising Packet Structure
Bluetooth LE 4.2 Advertising Packet Structure
Lets break it down step by step. The outer layer of an advertising packet contains four parts, which are:
.. list-table::
:align: center
:widths: 10 40 10 40
:header-rows: 1
* - No.
- Name
- Byte Size
- Function
* - 1
- Preamble
- 1
- A special bit sequence used for device clock synchronization
* - 2
- Access Address
- 4
- Marks the address of the advertising packet
* - 3
- Protocol Data Unit, PDU
- 2-39
- The area where the actual data is stored
* - 4
- Cyclic Redundancy Check, CRC
- 3
- Used for cyclic redundancy checking
The advertising packet is a type of Bluetooth packet, and its nature is determined by the type of PDU. Now, let's take a detailed look at the PDU.
PDU
##########################
The PDU segment is where the actual data is stored. Its structure is as follows:
.. list-table::
:align: center
:widths: 10 50 40
:header-rows: 1
* - No.
- Name
- Byte Size
* - 1
- Header
- 2
* - 2
- Payload
- 0-37
PDU Header
##############
The PDU header contains various pieces of information, which can be broken down into six parts:
.. list-table::
:align: center
:widths: 10 40 10 40
:header-rows: 1
* - No.
- Name
- Bit Size
- Notes
* - 1
- PDU Type
- 4
-
* - 2
- Reserved for Future Use, **RFU**
- 1
-
* - 3
- Channel Selection Bit, **ChSel**
- 1
- Indicates whether the advertiser supports the *LE Channel Selection Algorithm #2*
* - 4
- TX Address, **TxAdd**
- 1
- 0/1 indicates Public Address/Random Address
* - 5
- Rx Address, **RxAdd**
- 1
- 0/1 indicates Public Address/Random Address
* - 6
- Payload Length
- 8
-
The PDU Type bit reflects the advertising behavior of the device. In the Bluetooth protocol, there are three pairs of advertising behaviors:
- *Connectable* vs. *Non-connectable*:
- Whether the device accepts connection requests from others.
- *Scannable* vs. *Non-scannable*:
- Whether the device accepts scan requests from others.
- *Undirected* vs. *Directed*:
- Whether the advertising packet is sent to a specific device.
These advertising behaviors can be combined into four common types of advertising, corresponding to four different PDU types:
.. list-table::
:align: center
:widths: 10 10 10 30 40
:header-rows: 1
* - Connectable
- Scannable
- Undirected
- PDU Type
- Purpose
* - Y
- Y
- Y
- `ADV_IND`
- The most common advertising type
* - Y
- N
- N
- `ADV_DIRECT_IND`
- Commonly used for reconnecting with known devices
* - N
- N
- Y
- `ADV_NONCONN_IND`
- Used by beacon devices to advertising data without connection
* - N
- Y
- Y
- `ADV_SCAN_IND`
- Used by beacons to advertise additional data via a scan response when packet length is insufficient
PDU Payload
#####################
The PDU Payload is divided into two parts:
.. list-table::
:align: center
:widths: 10 50 10 30
:header-rows: 1
* - No.
- Name
- Byte Size
- Notes
* - 1
- Advertisement Address, **AdvA**
- 6
- The 48-bit Bluetooth address of the advertiser
* - 2
- Advertisement Data, **AdvData**
- 0-31
- Consists of multiple Advertisement Data Structures
The Advertisement Address can be either a:
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - Type
- Description
* - Public Address
- A globally unique fixed device address that manufacturers must register and pay fees to IEEE for
* - Random Address
- A randomly generated address
Random addresses are further divided into two categories:
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - Type
- Description
* - Random Static Address
- Can be either fixed in firmware or randomly generated at startup but must not change during operation. Often used as an alternative to a Public Address.
* - Random Private Address
- Periodically changes to prevent device tracking.
For devices using random private addresses to communicate with trusted devices, an Identity Resolving Key (IRK) should be used to generate the random address. Devices with the same IRK can resolve and obtain the true address. There are two types of random private addresses:
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - Type
- Description
* - Resolvable Random Private Address
- Can be resolved with an IRK to obtain the devices true address
* - Non-resolvable Random Private Address
- Completely random and rarely used, as it cannot be resolved and is only meant to prevent tracking
Let's look at the **advertising data**. The format of an advertising data structure is defined as follows:
.. list-table::
:align: center
:widths: 10 40 20 30
:header-rows: 1
* - No.
- Name
- Byte Size
- Notes
* - 1
- AD Length
- 1
-
* - 2
- AD Type
- n
- Most types take 1 byte
* - 3
- AD Data
- (AD Length - n)
-
Basic Concepts of Scanning
^^^^^^^^^^^^^^^^^^^^^^^^^^
Similar to the advertising process, scanning also raises three questions:
1. Where to scan? (Where?)
2. When to scan and for how long? (When?)
3. What to do during scanning? (What?)
For Bluetooth LE 4.2 devices, the advertiser only sends data on the advertising channels, which are channels 37-39. For Bluetooth LE 5.0 devices, if the advertiser has enabled extended advertising, it sends `ADV_EXT_IND` on the primary advertising channels and `AUX_ADV_IND` on the secondary advertising channels.
Thus, for Bluetooth LE 4.2 devices, scanners only need to receive advertising data on advertising channels. For Bluetooth LE 5.0 devices, scanners must first receive the `ADV_EXT_IND` on the primary advertising channels and, if it indicates a secondary channel, move to the corresponding secondary channel to receive the `AUX_ADV_IND`.
Scan Window and Scan Interval
#####################################
The second question refers to the concepts of the Scan Window and the Scan Interval.
- **Scan Window**: the duration for which the scanner continuously receives packets on a single RF channel. For example, if the scan window is set to 50 ms, the scanner continuously scans for 50 ms on each RF channel.
- **Scan Interval**: the time between the start of two consecutive scan windows, which means the scan interval is always greater than or equal to the scan window.
The diagram below illustrates the process of a scanner receiving advertising packets on a timeline. The scanner's scan interval is 100 ms, and the scan window is 50 ms; the advertiser's advertising interval is 50 ms, and the duration of the advertising packet transmission is for illustrative purposes only. As shown, the first scan window corresponds to channel 37, where the scanner successfully receives the advertiser's first broadcasting packet sent on channel 37, and this pattern continues.
.. figure:: ../../../../_static/ble/ble-advertise-and-scan-sequence.png
:align: center
:scale: 30%
:alt: Advertising and Scanning Timing Diagram
Advertising and Scanning Timing Diagram
.. _scan_request_and_scan_response:
Scan Request and Scan Response
#####################################
From the current introduction, it might seem that the advertiser only transmits and the scanner only receives during the advertising process. However, scanning behavior is divided into two types:
- **Passive Scanning**:
- The scanner only receives advertising packets.
- **Active Scanning**:
- After receiving an advertising packet, the scanner sends a scan request to a scannable advertiser.
When a scannable advertiser receives a scan request, it sends a scan response packet, providing more advertising information to the interested scanner. The structure of the scan response packet is identical to the advertising packet, with the difference being the PDU type in the PDU header.
In scenarios where the advertiser operates in scannable advertising mode and the scanner in active scanning mode, the data transmission timing between the advertiser and the scanner becomes more complex. For the scanner, after a scan window ends, it briefly switches to TX mode to send a scan request, then quickly switches back to RX mode to receive a possible scan response. For the advertiser, after each advertising, it briefly switches to RX mode to receive any scan requests, and upon receiving one, it switches to TX mode to send the scan response.
.. figure:: ../../../../_static/ble/ble-advertiser-rx-scan-request.png
:align: center
:scale: 30%
:alt: Scan Request Reception and Scan Response Transmission
Scan Request Reception and Scan Response Transmission
Hands-On Practice
--------------------------
After learning the relevant concepts of advertising and scanning, let's apply this knowledge in practice using the :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` example to create a simple beacon device.
Prerequisites
^^^^^^^^^^^^^^^^^^^^^^^
1. An {IDF_TARGET_NAME} development board
2. ESP-IDF development environment
3. The **nRF Connect for Mobile** app installed on your phone
If you haven't set up the ESP-IDF development environment yet, please refer to :doc:`IDF Get Started <../../../get-started/index>`.
Try It Out
^^^^^^^^^^^^^^^^^^^^^^
Building and Flashing
##########################
The reference example for this tutorial is :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>`.
You can navigate to the example directory using the following command:
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_Beacon
Please replace `<ESP-IDF Path>` with your local ESP-IDF folder path. Then, you can open the NimBLE_Beacon project using VSCode or another IDE you prefer. For example, after navigating to the example directory via the command line, you can open the project in VSCode using the following command:
.. code-block:: shell
$ code .
Next, enter the ESP-IDF environment in the command line and set the target chip:
.. code-block:: shell
$ idf.py set-target <chip-name>
You should see messages like:
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
These messages indicate that the chip has been successfully configured. Then, connect the development board to your computer and run the following command to build the firmware, flash it to the board, and monitor the serial output from the {IDF_TARGET_NAME} development board:
.. code-block:: shell
$ idf.py flash monitor
You should see messages like:
.. code-block:: shell
...
main_task: Returned from app_main()
Wait until the notification ends.
Viewing Beacon Device Information
#########################################
.. _nimble_beacon_details:
Open the **nRF Connect for Mobile** app on your phone, go to the **SCANNER** tab, and pull down to refresh. Locate the NimBLE_Beacon device, as shown in the figure below.
.. figure:: ../../../../_static/ble/ble-scan-list-nimble-beacon.jpg
:align: center
:scale: 30%
:alt: NimBLE Beacon
Locate NimBLE Beacon Device
If the device list is long, it is recommended to filter by the keyword NimBLE in the device name to quickly find the NimBLE_Beacon device.
You will notice that the NimBLE Beacon device contains rich information, including the Espressif website (this demonstrates the beacon advertising feature). Click the **RAW** button in the lower-right corner to view the raw advertising packet data, as shown below.
.. figure:: ../../../../_static/ble/ble-adv-packet-raw-data.jpg
:align: center
:scale: 30%
:alt: ADV Packet Raw Data
Advertising Packet Raw Data
**Details** table summarizes all advertising data structures in the advertising data packet and the scan response data packet:
.. list-table::
:align: center
:widths: 30 10 10 30 20
:header-rows: 1
* - Name
- Length
- Type
- Raw Data
- Resolved Information
* - Flags
- 2 Bytes
- `0x01`
- `0x06`
- General Discoverable, BR/EDR Not Supported
* - Complete Local Device Name
- 14 Bytes
- `0x09`
- `0x4E696D424C455F426561636F6E`
- NimBLE_Beacon
* - TX Power Level
- 2 Bytes
- `0x0A`
- `0x09`
- 9 dBm
* - Appearance
- 3 Bytes
- `0x19`
- `0x0002`
- Generic Tag (Generic category)
* - LE Role
- 2 Bytes
- `0x1C`
- `0x00`
- Only Peripheral Role supported
* - LE Bluetooth Device Address
- 8 Bytes
- `0x1B`
- `0x46F506BDF5F000`
- `F0:F5:BD:06:F5:46`
* - URI
- 17 Bytes
- `0x24`
- `0x172F2F6573707265737369662E636F6D`
- `https://espressif.com`
It is worth mentioning that the total length of the first five advertising data structures is 28 bytes, leaving only 3 bytes of space in the advertising data packet, which is not enough to accommodate the last two data structures. Therefore, the last two advertising data structures must be placed in the scan response data packet.
You may also notice that the Raw Data for the Device Appearance is `0x0002`, while in the code, the definition for Generic Tag is `0x0200`. Additionally, the Raw Data for the Device Address appears to be completely reversed, except for the last byte (`0x00`). This is because Bluetooth LE air packets follow a little-endian transmission order, meaning the lower bytes are placed at the front.
Also, note that the **nRF Connect for Mobile** app does not provide a **CONNECT** button to connect to this device, which aligns with our expectations since a Beacon device is inherently non-connectable. Now, let's dive into the code details to see how such a Beacon device is implemented.
Code Explanation
---------------------------
Project Structure Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_project_structure:
The root directory of :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` is roughly divided into the following parts:
- `README*.md`
- Documentation for the project
- `sdkconfig.defaults*`
- Default configurations for different chip development boards
- `CMakeLists.txt`
- Used to include the ESP-IDF build environment
- `main`
- The main project folder containing the source code, header files, and build configurations
Program Behavior Overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_program_behavior:
Before diving into the code details, let's first get a macro understanding of the program behavior.
First, we initialize the various modules used in the program, mainly including NVS Flash, the NimBLE Host Stack, and the GAP service.
After the NimBLE Host Stack synchronizes with the Bluetooth controller, we confirm the Bluetooth address is available, then initiate an undirected, non-connectable, and scannable advertisement.
The device remains in advertising mode continuously until a reboot occurs.
Entry Function
^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_entry_point:
As with other projects, the entry function of the application is the `app_main` function in the `main/main.c` file, where we typically initialize the modules. In this example, we mainly do the following:
1. Initialize NVS Flash and the NimBLE Host Stack
2. Initialize the GAP service
3. Start the FreeRTOS task for the NimBLE Host Stack
The {IDF_TARGET_NAME} Bluetooth stack uses NVS Flash to store related configurations, so before initializing the Bluetooth stack, we must call the `nvs_flash_init` API to initialize NVS Flash. In some cases, we may need to call the `nvs_flash_erase` API to erase NVS Flash before initialization.
.. code-block:: C
void app_main(void) {
...
/* 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;
}
...
}
Next, you can call `nimble_port_init` API to initialize NimBLE host stack.
.. code-block:: 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;
}
...
}
Then, we call the `gap_init` function defined in the `gap.c` file to initialize the GAP service and set the device name and appearance.
.. code-block:: C
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;
}
...
}
Next, we configure the NimBLE host stack, which mainly involves setting some callback functions, including callbacks for when the stack is reset and when synchronization is complete, and then saving the configuration.
.. code-block:: 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();
...
}
Finally, start the FreeRTOS thread for the NimBLE host stack.
.. code-block:: 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);
...
}
Start Advertising
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_start_advertising:
When developing applications using the NimBLE host stack, the programming model is event-driven.
For example, after the NimBLE host stack synchronizes with the Bluetooth controller, a synchronization completion event will be triggered, invoking the `ble_hs_cfg.sync_cb` function. When setting up the callback function, we point the function pointer to the `on_stack_sync` function, which is the actual function called upon synchronization completion.
In the `on_stack_sync` function, we call the `adv_init` function to initialize advertising operations. In `adv_init`, we first call the `ble_hs_util_ensure_addr` API to confirm that a usable Bluetooth address is available. Then, we call the `ble_hs_id_infer_auto` API to obtain the optimal Bluetooth address type.
.. code-block:: C
static void on_stack_sync(void) {
/* On stack sync, do advertising initialization */
adv_init();
}
void adv_init(void) {
...
/* 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;
}
...
}
Next, we copy the Bluetooth address data from the NimBLE stack's memory space into the local `addr_val` array, preparing it for subsequent use.
.. code-block:: C
void adv_init(void) {
...
/* 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);
...
}
Finally, we call the `start_advertising` function to initiate advertising. Within the `start_advertising` function, we first populate the advertising data structures, including the advertising flags, complete device name, transmission power level, device appearance, and LE role, into the advertising packet as follows:
.. code-block:: C
static void start_advertising(void) {
/* Local variables */
int rc = 0;
const char *name;
struct ble_hs_adv_fields adv_fields = {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;
}
...
}
The `ble_hs_adv_fields` structure predefines some commonly used advertising data types. After completing the data setup, we can enable the corresponding advertising data structures by setting the relevant is_present field to 1 or by assigning a non-zero value to the corresponding length field (len). For example, in the code above, we configure the device's transmission power with `adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;`, and then enable that advertising data structure by setting `adv_fields.tx_pwr_lvl_is_present = 1;`. If we only configure the transmission power without setting the corresponding is_present field, the advertising data structure becomes invalid. Similarly, we configure the device name with `adv_fields.name = (uint8_t *)name;` and set the name's length with `adv_fields.name_len = strlen(name);` to add the device name as an advertising data structure to the advertising packet. If we only configure the device name without specifying its length, the advertising data structure will also be invalid.
Finally, we call the `ble_gap_adv_set_fields` API to finalize the setup of the advertising data structures in the advertising packet.
In the same way, we can fill in the device address and URI into the scan response packet as follows:
.. code-block:: C
static void start_advertising(void) {
...
struct ble_hs_adv_fields rsp_fields = {0};
...
/* 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;
}
...
}
Finally, we set the advertising parameters and initiate the advertising by calling the `ble_gap_adv_start` API.
.. code-block:: C
static void start_advertising(void) {
...
struct ble_gap_adv_params adv_params = {0};
...
/* 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!");
}
Summary
---------------------
Through this tutorial, you have learned the basic concepts of advertising and scanning, and you mastered the method of building a Bluetooth LE Beacon device using the NimBLE host stack through the :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` example.
You can try to modify the data in the example and observe the changes in the **nRF Connect for Mobile** app. For instance, you might modify the `adv_fields` or `rsp_fields` structures to change the populated advertising data structures, or swap the advertising data structures between the advertising packet and the scan response packet. However, keep in mind that the maximum size for the advertising data in both the advertising packet and the scan response packet is 31 bytes; if the size of the advertising data structure exceeds this limit, calling the `ble_gap_adv_start` API will fail.

View File

@@ -1 +1,342 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-introduction.rst
Introduction
===================
:link_to_translation:`zh_CN:[中文]`
This document is the first tutorial in the Getting Started series on Bluetooth Low Energy (Bluetooth LE). It introduces the basic concepts of Bluetooth LE and guides users through flashing a Bluetooth LE example onto an {IDF_TARGET_NAME} development board. The tutorial also instructs users on how to use the **nRF Connect for Mobile** app to control an LED and read heart rate data from the board. The tutorial offers a hands-on approach to understanding Bluetooth LE and working with the ESP-IDF framework for Bluetooth LE applications.
Learning Objectives
-----------------------
- Understand the layered architecture of Bluetooth LE
- Learn the basic functions of each layer in Bluetooth LE
- Understand the functions of GAP and GATT/ATT layers
- Master the method of flashing Bluetooth LE examples on {IDF_TARGET_NAME} development board and interacting with it via a mobile phone
Preface
-----------------
Most people have experienced Bluetooth technology in their daily lives—perhaps you are even wearing Bluetooth headphones right now, listening to audio from your phone or computer. However, audio transmission is a typical use case of Bluetooth Classic, while Bluetooth LE is a Bluetooth protocol that is not compatible with Bluetooth Classic and was introduced in Bluetooth 4.0. As the name suggests, Bluetooth LE is a low-power Bluetooth protocol with a lower data transfer rate compared to Bluetooth Classic. It is typically used in data communication for the Internet of Things (IoT), such as smart switches or sensors, as shown in the example in this tutorial. However, before diving into the example project, let's first understand the basic concepts of Bluetooth LE to help you get started.
Layered Architecture of Bluetooth LE
------------------------------------
The Bluetooth LE protocol defines a three-layer software architecture, listed from top to bottom:
- Application Layer
- Host Layer
- Controller Layer
The Application Layer is where applications are built using Bluetooth LE as the underlying communication technology, relying on the API interfaces provided by the Host Layer.
The Host Layer implements low-level Bluetooth protocols such as L2CAP, GATT/ATT, SMP, and GAP, providing API interfaces to the Application Layer above and communicating with the Controller Layer below via the Host Controller Interface (HCI).
The Controller Layer consists of the Physical Layer (PHY) and the Link Layer (LL), which directly interacts with the hardware below and communicates with the Host Layer above through the HCI.
Its worth mentioning that the Bluetooth Core Specification allows the Host Layer and Controller Layer to be physically separated, in which case the HCI is realized as a physical interface, including SDIO, USB, and UART, among others. However, the Host and Controller Layers can also coexist on the same chip for higher integration, in which case the HCI is referred to as the Virtual Host Controller Interface (VHCI). Generally, the Host Layer and Controller Layer together make up the Bluetooth LE Stack.
The diagram below shows the layered structure of Bluetooth LE.
.. figure:: ../../../../_static/ble/ble-architecture.png
:align: center
:scale: 50%
:alt: Bluetooth LE Layered Architecture
Layered Architecture of Bluetooth LE
As an application developer, during the development process, we primarily interact with the APIs provided by the Host Layer, which requires a certain understanding of the Bluetooth protocols within the Host Layer. Next, we will introduce the basic concepts of the GAP and GATT/ATT layers from two perspectives: connection and data exchange.
GAP Layer - Defining Device Connections
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The GAP (Generic Access Profile) layer defines the connection behaviors between Bluetooth LE devices and the roles they play in the connection.
GAP States and Roles
#######################
The GAP layer defines three connection states and five different device roles, as follows:
- Idle
- In this state, the device is in a standby state without any role.
- Device Discovery
- Advertiser
- Scanner
- Initiator
- Connection
- Peripheral
- Central
The advertising data contains information such as the device address, indicating the advertiser's presence to external devices and informing them whether they are connectable. A scanner continuously receives advertising packets in the environment. If a scanner detects a connectable advertiser and wishes to establish a connection, it can switch its role to initiator. When the initiator receives another advertising data from the same advertiser, it immediately sends a Connection Request. If the advertiser has not enabled a Filter Accept List (also known as White List), or if the initiator is included in the advertiser's Filter Accept List, the connection will be successfully established.
Once connected, the original advertiser becomes the peripheral device (formerly known as the slave device), and the original scanner or connection initiator becomes the central device (formerly known as the master device).
The diagram below shows the relationship between the GAP roles.
.. figure:: ../../../../_static/ble/ble-gap-state-diagram.png
:align: center
:scale: 50%
:alt: GAP roles relationship
GAP Roles Relationship
Bluetooth LE Network Topology
################################
Bluetooth LE devices can connect to multiple Bluetooth LE devices simultaneously, playing multiple peripheral or central device roles, or acting as both a peripheral and a central device at the same time. For example, a Bluetooth LE gateway can act as a central device to connect with peripheral devices such as smart switches, while also functioning as a peripheral device to connect with central devices like smartphones, serving as a data intermediary.
In a Bluetooth LE network, if all devices are connected to at least one other device and each plays only one type of role, this is referred to as a Connected Topology. If at least one device plays both peripheral and central roles simultaneously, the network is called a Multi-role Topology.
Bluetooth LE also supports a connectionless network topology known as Broadcast Topology. In such a network, there are two roles: the device sending the data is called the Broadcaster, and the device receiving the data is called the Observer. The broadcaster only sends data and does not accept connections, while the observer only receives advertising data and does not initiate connections. For example, in a network where a sensor's data is shared by multiple devices, maintaining multiple connections can be costly, so advertising sensor data to all devices in the network is a more suitable approach.
Learn More
##################
If you want to learn more about device discovery and connection, please refer to :doc:`Device Discovery <./ble-device-discovery>` and :doc:`Connection <./ble-connection>`.
GATT/ATT Layer - Data Representation and Exchange
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _gatt_att_introduction:
The GATT/ATT layer defines the methods for data exchange between devices once they are connected, including how data is represented and the process of exchanging it.
ATT Layer
#############
ATT stands for Attribute Protocol (ATT), which defines a basic data structure called **Attribute** and data access methods based on a server/client architecture.
In simple terms, data is stored on a server as attributes, awaiting access by the client. For example, in a smart switch, the switch state is stored in the Bluetooth chip (server) of the smart switch as data in the form of an attribute. The user can then access the switch state attribute stored in the smart switch's Bluetooth chip (server) via their smartphone (client), to either read the current state (read access) or open and close the switch (write access).
The attribute data structure typically consists of the following three parts:
- Handle
- Type
- Value
- Permissions
In the protocol stack implementation, attributes are generally managed in an array-like structure called an **Attribute Table**. The index of an attribute in this table is its handle, usually an unsigned integer.
The type of an attribute is represented by a UUID and can be divided into three categories: 16-bit, 32-bit, and 128-bit UUIDs. The 16-bit UUIDs are universally defined by the Bluetooth Special Interest Group (Bluetooth SIG) and can be found in their publicly available `Assigned Numbers <https://www.bluetooth.com/specifications/assigned-numbers/>`__ document. The other two lengths of UUIDs are used for vendor-defined attribute types, with the 128-bit UUID being the most commonly used.
GATT Layer
###############
GATT stands for Generic Attribute Profile (GATT), and it builds on ATT by defining the following three concepts:
- Characteristic
- Service
- Profile
The hierarchical relationship between these three concepts is shown in the diagram below.
.. figure:: ../../../../_static/ble/ble-gatt-architecture.png
:align: center
:scale: 30%
:alt: GATT Hierarchical Architecture
GATT Hierarchical Architecture
.. _characteristic_structure:
Both characteristics and services are composite data structures based on attributes. A characteristic is often described by two or more attributes, including:
- Characteristic Declaration Attribute
- Characteristic Value Attribute
In addition, a characteristic may also include several optional Characteristic Descriptor Attributes.
A service itself is also described by an attribute, called the Service Declaration Attribute. A service can contain one or more characteristics, with a dependency relationship between them. Additionally, a service can reference another service using the `Include` mechanism, reusing its characteristic definitions to avoid redundant definitions for common characteristics, such as device names or manufacturer information.
A profile is a predefined set of services. A device that implements all the services defined in a profile is said to comply with that profile. For example, the Heart Rate Profile includes the Heart Rate Service and the Device Information Service. Thus, a device that implements both the Heart Rate Service and Device Information Service is considered compliant with the Heart Rate Profile.
Broadly speaking, any device that stores and manages characteristics is called a GATT Server, while any device that accesses the GATT Server to retrieve characteristics is called a GATT Client.
Learn More
####################
If you'd like to learn more about data representation and exchange, please refer to :doc:`Data Exchange <./ble-data-exchange>`.
Hands-On Practice
--------------------------
After learning the basic concepts of Bluetooth LE, let's load a simple Bluetooth LE example onto the {IDF_TARGET_NAME} development board to experience the functionalities of LED control and heart rate data reading, and gain an intuitive understanding of Bluetooth LE technology.
Prerequisites
^^^^^^^^^^^^^^^^
1. An {IDF_TARGET_NAME} development board
2. ESP-IDF development environment
3. The **nRF Connect for Mobile** app installed on your phone
If you haven't set up the ESP-IDF development environment yet, please refer to :doc:`IDF Get Started <../../../get-started/index>`.
Try It Out
^^^^^^^^^^^^^^^^^^
.. _nimble_gatt_server_practice:
Building and Flashing
##########################
The reference example for this tutorial is :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>`.
You can navigate to the example directory using the following command:
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_GATT_Server
Please replace `<ESP-IDF Path>` with your local ESP-IDF folder path. Then, you can open the NimBLE_GATT_Server project using VSCode or another IDE you prefer. For example, after navigating to the example directory via the command line, you can open the project in VSCode using the following command:
.. code-block:: shell
$ code .
Next, enter the ESP-IDF environment in the command line and set the target chip:
.. code-block:: shell
$ idf.py set-target <chip-name>
You should see messages like:
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
These messages indicate that the chip has been successfully configured. Then, connect the development board to your computer and run the following command to build the firmware, flash it to the board, and monitor the serial output from the {IDF_TARGET_NAME} development board:
.. code-block:: shell
$ idf.py flash monitor
You should see messages like:
.. code-block:: shell
...
main_task: Returned from app_main()
NimBLE_GATT_Server: Heart rate updated to 70
The heart rate data will update at a frequency of about 1 Hz, fluctuating between 60 and 80.
Connecting to the Development Board
#######################################
Now the development board is ready. Next, open the **nRF Connect for Mobile** app on your phone, refresh the **SCANNER** tab, and find the NimBLE_GATT device, as shown in the image below.
.. figure:: ../../../../_static/ble/ble-get-started-connect-brief.jpg
:align: center
:scale: 20%
:alt: Device Scan
Device Scan
If the device list is long, it is recommended to filter the device names using NimBLE as a keyword to quickly find the NimBLE_GATT device.
Click on the **NimBLE_GATT** device entry to expand and view the detailed advertising data.
.. figure:: ../../../../_static/ble/ble-get-started-connect-details.jpg
:align: center
:scale: 20%
:alt: Advertising Data Details
Advertising Data Details
Click the **CONNECT** button on the right. While the phone is connecting, you can observe many connection-related log messages in the serial output of the development board. Then, the NimBLE_GATT tab will appear on the phone, and there should be a **CONNECTED** status in the upper left corner, indicating that the phone has successfully connected to the development board via the Bluetooth LE protocol. On the CLIENT subpage, you should be able to see four GATT services, as shown in the figure.
.. figure:: ../../../../_static/ble/ble-get-started-gatt-services-list.jpg
:align: center
:scale: 20%
:alt: GATT Services List
GATT Services List
The first two services are the GAP service and GATT service, which are foundational services in Bluetooth LE applications. The other two services are the Heart Rate Service and Automation IO Service, both defined by the Bluetooth SIG. They provide heart rate data reading and LED control functionality, respectively.
Below the service names, you can see the corresponding UUIDs and the primary/secondary service designation. For example, the UUID for the Heart Rate Service is `0x180D`, which is a primary service. Its important to note that the service names are derived from the UUIDs. In **nRF Connect for Mobile**, when implementing a GATT client, the developer preloads the database with services defined by the Bluetooth SIG or other customized services. Based on the GATT service UUID, service information is parsed. Therefore, if a service's UUID is not in the database, its information cannot be parsed, and the service name will be displayed as Unknown Service.
Lets Light Up the LED!
###############################
Now, let's try out the functionality of this example. First, click on the **Automation IO Service**, and you will see an LED characteristic under this service.
.. figure:: ../../../../_static/ble/ble-get-started-automation-io-service-details.jpg
:align: center
:scale: 20%
:alt: Automation IO Service
Automation IO Service
As shown in the figure, the UUID of this LED characteristic is a 128-bit vendor-specific UUID. Click the **UPLOAD** button on the right to perform a write operation on this characteristic, as shown in the figure.
.. figure:: ../../../../_static/ble/ble-get-started-led-write.jpg
:align: center
:scale: 20%
:alt: Write to LED Characteristic Data
Write to LED Characteristic Data
Select the **ON** option and send it. You should see the LED on the development board light up. Select the **OFF** option and send it, and you should observe the LED on the development board turning off again.
If your development board does not have other LED except the one for the power indicator, you should be able to observe the corresponding status indication in the log output.
Receiving Heart Rate Data
#############################
Next, click on the **Heart Rate Service**. You will see a Heart Rate Measurement characteristic under this service.
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-service-details.jpg
:align: center
:scale: 20%
:alt: Heart Rate Service
Heart Rate Service
The UUID of the Heart Rate Measurement characteristic is `0x2A37`, which is a Bluetooth SIG-defined characteristic. Click the download button on the right to perform a read operation on the heart rate characteristic. You should see the latest heart rate measurement data appear in the `Value` field of the characteristic data section, as shown in the figure.
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-read.jpg
:align: center
:scale: 20%
:alt: Read Heart Rate Characteristic Data
Read Heart Rate Characteristic Data
In the application, it is best for heart rate data to be synchronized to the GATT client immediately when the measurement is updated. To achieve this, we can click the **SUBSCRIPTION** button on the far right to request the heart rate characteristic to perform an indication operation. At this point, you should be able to see the heart rate measurement data continuously updating, as shown in the figure.
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-indicate.jpg
:align: center
:scale: 20%
:alt: Subscribe to the heart rate characteristic data
Subscribe to Heart Rate Characteristic Data
You might have noticed that under the heart rate characteristic, there is a descriptor named *Client Characteristic Configuration* (often abbreviated as CCCD), with a UUID of `0x2902`. When you click the subscribe button, the value of this descriptor changes, which indicates that the characteristic's indications are enabled. Indeed, this descriptor is used to indicate the status of notifications or indications for the characteristic data. When you unsubscribe, the descriptor's value changes to indicate that notifications and indications are disabled.
Summary
-----------
Through this tutorial, you have learned about the layered architecture of Bluetooth LE, the basic functions of the host and controller layers in the Bluetooth LE protocol stack, and the roles of the GAP and GATT/ATT layers. Additionally, using the :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` example, you have mastered how to build and flash Bluetooth LE applications with the ESP-IDF framework, debug the application on your phone using **nRF Connect for Mobile**, remotely control the LED on the development board, and receive randomly generated heart rate data. You've taken the first step towards becoming a Bluetooth LE developer—congratulations!