Merge branch 'docs/add_ble_get_started_eng' into 'master'

Docs: Added BLE English version of BLE Get Started

See merge request espressif/esp-idf!34013

(cherry picked from commit 35fd0412888e5b891ff06b55f4f2088239326a3c)

42601a7a docs(ble): Added ENG version of ble-introduction.rst
615282c4 docs(ble): Added ENG version of ble-device-discovery.rst
645e68f5 docs(ble): Added initial ENG version of ble-device-discovery.rst
d2d95a52 docs(ble): Added ENG version of ble-data-exchange.rst
00ca4dc7 docs(ble):Updated Details table in ble-device-discovery.rst
8e03c200 fix(ble): Removed trailing white space, and fixed some format issues
5b304485 docs(ble):Fixed a indentation in ble-data-exchange.rst
b4c51e7b docs(ble): Deleted some chinese character in ble-data-exchange.rst ENG version
d01d7efe docs(ble):Revised some expression in ENG version of BLE Get Started
a24762c5 docs(ble): Removed a trailing whitespace in ble-introduction.rst
afbe5a1f Apply 44 suggestion(s) to 4 file(s)
4ddab725 docs(ble): Fixed a description in ble-introduction.rst
203dfa03 Apply 5 suggestion(s) to 3 file(s)

Co-authored-by: Island <island@espressif.com>
This commit is contained in:
Wei Yu Han 2024-10-23 10:16:47 +08:00
parent 45c893d5c5
commit c0b22cb37b
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!

View File

@ -11,7 +11,7 @@
- 学习连接的基本概念
- 学习连接相关的参数
- 学习 NimBLE_Connection 例程的代码结构
- 学习 :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` 例程的代码结构
连接的基本概念
@ -23,7 +23,7 @@
*在 Bluetooth LE 5.0 引入扩展广播特性以后, Legacy ADV 和 Extended ADV 对应的连接建立过程略有差异,下以 Legacy ADV 对应的连接建立过程为例。*
当扫描者在某一个广播信道接收到一个广播数据包时,若该广播者是可连接的,那么扫描者可以在同一广播信道发送连接请求 (Connection Request)。对于广播者来说,它可以设置 *接受列表 (Accept List)* 以过滤不受信任的设备,或接受任一扫描者的连接请求。随后,广播者转变为外围设备,扫描者转变为中央设备,两者之间可以在数据信道进行双向通信。
当扫描者在某一个广播信道接收到一个广播数据包时,若该广播者是可连接的,那么扫描者可以在同一广播信道发送连接请求 (Connection Request)。对于广播者来说,它可以设置 *接受列表 (Filter Accept List)* 以过滤不受信任的设备,或接受任一扫描者的连接请求。随后,广播者转变为外围设备,扫描者转变为中央设备,两者之间可以在数据信道进行双向通信。
:ref:`扫描请求与扫描响应 <scan_request_and_scan_response>` 所述,广播者在每一个信道的广播结束以后,都会短暂进入 RX 模式,以接收可能的扫描请求。实际上,这个 RX 过程中还可以接受连接请求。所以对于扫描者来说,发送连接请求的时间窗口和发送扫描请求的时间窗口是类似的。
@ -73,7 +73,7 @@
外围设备延迟 (Peripheral Latency) 规定了外围设备在无需发送数据的前提下,最多可忽略的连接事件数量。
为了理解这个连接参数的作用,让我们以蓝牙鼠标为例,分析其应用场景。用户在使用键盘的过程中,鼠标并没有需要发送的有效数据,此时最好降低数据包发送的频率以节省电量;在使用鼠标的过程中,我们希望鼠标能够尽可能快地发送数据,以降低使用延迟。也就是说,蓝牙鼠标的数据发送是间歇性高频率的。此时,如果仅靠连接间隔参数进行连接调节,则那么较低的连接间隔会导致高能耗,较高的连接间隔会导致高延迟。
为了理解这个连接参数的作用,让我们以蓝牙鼠标为例。用户在使用键盘的过程中,鼠标并没有需要发送的有效数据,此时最好降低数据包发送的频率以节省电量;在使用鼠标的过程中,我们希望鼠标能够尽可能快地发送数据,以降低使用延迟。也就是说,蓝牙鼠标的数据发送是间歇性高频率的。此时,如果仅靠连接间隔参数进行连接调节,则那么较低的连接间隔会导致高能耗,较高的连接间隔会导致高延迟。
在这种场景下,外围设备延迟机制将是一个完美的解决方案。为了降低蓝牙鼠标的延迟,我们可以将连接间隔设为一个较小的值,例如 10 ms ,那么在密集使用时数据交换频率可达 100 Hz ;随后,我们将外围设备延迟设定为 100 ,那么蓝牙鼠标在不使用的状态下,实际的数据交换频率可降低至 1 Hz 。通过这种设计,我们在不调整连接参数的前提下,实现了可变的数据交换频率,在最大程度上提升了用户体验。
@ -132,17 +132,17 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
例程实践
-------------------------------------------
在掌握了连接的相关知识以后,接下来让我们结合 NimBLE_Connection 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的外围设备,对学到的知识进行实践。
在掌握了连接的相关知识以后,接下来让我们结合 :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的外围设备,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
1. 一块 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
3. 在手机上安装 **nRF Connect for Mobile** 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`。
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`IDF 快速入门 <../../../get-started/index>`。
动手试试
@ -173,7 +173,7 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
$ idf.py set-target <chip-name>
你应该能看到命令行
你应该能看到以下命令行
.. code-block:: shell
@ -188,7 +188,7 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
$ idf.py flash monitor
你应该能看到命令行以
你应该能看到以下命令行以
.. code-block:: shell
@ -201,7 +201,7 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
连接,然后断开
##############################
打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_CONN 设备,如下图所示
打开手机上的 **nRF Connect for Mobile** 程序,在 **SCANNER** 标签页中下拉刷新,找到 NimBLE_CONN 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-connection-device-list.jpg
:align: center
@ -211,9 +211,9 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_CONN 设备。
:ref:`NimBLE_Beacon <nimble_beacon_details>` 相比,可以观察到大部分广播数据是一致的,但多了一项 `Advertising Interval` 数据,其值为 500 ms ;在 `CONNECT` 按钮下方,确实也可以观察到广播间隔为 510 ms 左右。
:ref:`NimBLE_Beacon <nimble_beacon_details>` 相比,可以观察到大部分广播数据是一致的,但多了一项 `Advertising Interval` 数据,其值为 500 ms ;在 **CONNECT** 按钮下方,确实也可以观察到广播间隔为 510 ms 左右。
点击 `CONNECT` 按钮连接到设备,在手机上应能够看到 GAP 服务,如下
点击 **CONNECT** 按钮连接到设备,在手机上应能够看到 GAP 服务,如下
.. figure:: ../../../../_static/ble/ble-connection-connected.jpg
:align: center
@ -221,7 +221,7 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
连接到 NimBLE_CONN 设备
此时应该还能观察到开发板上的 LED 亮起。点击 `DISCONNECT`,断开与设备的连接,此时应能观察到开发板上的 LED 熄灭。
此时应该还能观察到开发板上的 LED 亮起。点击 **DISCONNECT**,断开与设备的连接,此时应能观察到开发板上的 LED 熄灭。
若你的开发板上没有电源指示灯以外的 LED ,你应该能在日志输出中观察到对应的状态指示。
@ -279,7 +279,7 @@ MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以
.. _nimble_connection_project_structure:
NimBLE_Connection 的根目录结构与 :ref:`NimBLE_Beacon <nimble_beacon_project_structure>` 完全一致,不过在完成了固件的构建以后,你可能会观察到根目录下多了一个 `managed_components` 目录,里面含有固件构建时自动引入的依赖;本例中为 `led_strip` 组件,用于控制开发板的 LED。该依赖项在 `main/idf_component.yml` 文件中被引入。
:example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` 的根目录结构与 :ref:`NimBLE_Beacon <nimble_beacon_project_structure>` 完全一致,不过在完成了固件的构建以后,你可能会观察到根目录下多了一个 `managed_components` 目录,里面含有固件构建时自动引入的依赖;本例中为 `led_strip` 组件,用于控制开发板的 LED。该依赖项在 `main/idf_component.yml` 文件中被引入。
另外,在 `main` 文件夹中引入了 LED 控制相关的源代码。
@ -472,6 +472,6 @@ GAP 事件处理
总结
----------------
通过本教程,你了解了连接的基本概念,并通过 NimBLE_Connection 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE 外围设备的方法。
通过本教程,你了解了连接的基本概念,并通过 :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>` 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE 外围设备的方法。
你可以尝试对例程中的参数进行修改,并在日志输出中观察修改结果。例如,你可以修改外围设备延迟或连接超时参数,观察连接参数的修改是否能够触发连接更新事件。

View File

@ -3,7 +3,7 @@
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其四,旨在对 Bluetooth LE 连接中的数据交换过程进行简要介绍。随后,本教程会结合 NimBLE_GATT_Server 例程,基于 NimBLE 主机层协议栈,对 GATT 服务器的代码实现进行介绍。
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其四,旨在对 Bluetooth LE 连接中的数据交换过程进行简要介绍。随后,本教程会结合 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 例程,基于 NimBLE 主机层协议栈,对 GATT 服务器的代码实现进行介绍。
学习目标
@ -11,7 +11,7 @@
- 学习特征数据和服务的数据结构细节
- 学习 GATT 的不同数据访问操作
- 学习 NimBLE_GATT_Server 例程的代码结构
- 学习 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 例程的代码结构
GATT 数据特征与服务
@ -203,7 +203,7 @@ CCCD 的 UUID 是 `0x2902`,属性值中仅含 2 比特信息。第一个比特
.. _attribute_table:
下面以 NimBLE_GATT_Server 为例,展示一个 GATT 服务器可能的属性表形态。例程中含有两个服务,分别是 Heart Rate Service 和 Automation IO Service ;前者含有一个 Heart Rate Measurement 特征数据,后者含有一个 LED 特征数据。整个 GATT 服务器有属性表如下
下面以 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 为例,展示一个 GATT 服务器可能的属性表形态。例程中含有两个服务,分别是 Heart Rate Service 和 Automation IO Service ;前者含有一个 Heart Rate Measurement 特征数据,后者含有一个 LED 特征数据。整个 GATT 服务器有属性表如下
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| Handle | UUID | Permissions | Value | Attribute Type |
@ -232,7 +232,9 @@ CCCD 的 UUID 是 `0x2902`,属性值中仅含 2 比特信息。第一个比特
| | | +-------------------------------------------------+ |
| | | | UUID = `0x00001525-1212-EFDE-1523-785FEABCD123` | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 6 | `0x00001525-1212-EFDE-1523-785FEABCD123` | Write-only | LED status | Characteristic Value |
| 6 | `0x00001525-1212-EFDE-` |Write-only | LED status |Characteristic Value |
| | `1523-785FE` | | | |
| | `ABCD123` | | | |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
GATT 客户端在与 GATT 服务器初次建立通信时,会从 GATT 服务器拉取属性表中的元信息,从而获取 GATT 服务器上可用的服务以及数据特征。这一过程被称为 *服务发现 (Service Discovery)*
@ -243,26 +245,23 @@ GATT 数据操作
.. _gatt_data_operation:
数据操作指的是对 GATT 服务器上的特征数据进行访问的操作,主要可以分为
数据操作指的是对 GATT 服务器上的特征数据进行访问的操作,主要可以分为以下两类:
1. 由客户端发起的操作
2. 由服务器发起的操作
两类。
由客户端发起的操作
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
由客户端发起的操作有以下三种
1. 读 (Read)
2. 写 (Write)
3. 写(无需响应) (Write without response)
读操作比较简单,单纯是从 GATT 服务器上拉取某一特征数据的当前值。
写操作分两种。普通的写操作要求 GATT 服务器在收到客户端的写请求以及对应数据以后,进行确认响应;快速写操作则不需要服务器进行确认响应。
- **读 (Read)**
- 从 GATT 服务器上拉取某一特征数据的当前值。
- **写 (Write)**
- 普通的写操作要求 GATT 服务器在收到客户端的写请求以及对应数据以后,进行确认响应。
- **写(无需响应) (Write without response)**
- 快速写操作则不需要服务器进行确认响应。
由服务器发起的操作
@ -270,10 +269,10 @@ GATT 数据操作
由服务器发起的操作分两种
1. 通知 (Notify)
2. 指示 (Indicate)
通知和指示都是 GATT 服务器主动向客户端推送数据的操作,区别在于通知无需客户端回复确认响应,而指示需要。所以,指示的数据推送速度比通知慢。
- **通知 (Notify)**
- 通知是 GATT 服务器主动向客户端推送数据的操作,不需要客户端回复确认响应。
- **指示 (Indicate)**
- 与通知相似,区别在于指示需要客户端回复确认,因此数据推送速度比通知慢。
虽然通知和指示都是由服务器发起的操作,但是服务器发起操作的前提是,客户端启用了通知或指示。所以,本质上 GATT 的数据交换过程总是以客户端请求数据开始。
@ -281,23 +280,23 @@ GATT 数据操作
例程实践
-------------------------------------------
在掌握了 GATT 数据交换的相关知识以后,接下来让我们结合 NimBLE_GATT_Server 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 GATT 服务器,对学到的知识进行实践。
在掌握了 GATT 数据交换的相关知识以后,接下来让我们结合 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 GATT 服务器,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
1. 一块 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`。
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`IDF 快速入门 <../../../get-started/index>`。
动手试试
^^^^^^^^^^^^^^^^^^
请参考 :ref:`动手试试 <nimble_gatt_server_practice>` 。
请参考 :ref:`BLE 介绍 动手试试 <nimble_gatt_server_practice>` 。
代码详解
@ -307,7 +306,7 @@ GATT 数据操作
工程结构综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
NimBLE_GATT_Server 的根目录结构与 :ref:`NimBLE_Connection <nimble_connection_project_structure>` 完全一致。另外,在 `main` 文件夹中引入了与 GATT 服务以及模拟心率生成相关的源代码。
:example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 的根目录结构与 :ref:`NimBLE_Connection <nimble_connection_project_structure>` 完全一致。另外,在 `main` 文件夹中引入了与 GATT 服务以及模拟心率生成相关的源代码。
程序行为综述
@ -659,6 +658,4 @@ LED 特征数据的访问通过 `led_chr_access` 回调函数管理,相关代
总结
----------------------------
通过本教程,你了解了如何通过服务表创建 GATT 服务以及相应的特征数据,并掌握了 GATT 特征数据的访问管理方式,包括读、写和订阅操作的实现。你可以在 NimBLE_GATT_Server 例程的基础上,开发更加复杂的 GATT 服务应用。
通过本教程,你了解了如何通过服务表创建 GATT 服务以及相应的特征数据,并掌握了 GATT 特征数据的访问管理方式,包括读、写和订阅操作的实现。你可以在 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 例程的基础上,开发更加复杂的 GATT 服务应用。

View File

@ -3,7 +3,7 @@
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其二,旨在对 Bluetooth LE 设备发现过程进行简要介绍,包括广播与扫描相关的基本概念。随后,本教程会结合 NimBLE_Beacon 例程,基于 NimBLE 主机层协议栈,对 Bluetooth LE 广播的代码实现进行介绍。
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其二,旨在对 Bluetooth LE 设备发现过程进行简要介绍,包括广播与扫描相关的基本概念。随后,本教程会结合 :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` 例程,基于 NimBLE 主机层协议栈,对 Bluetooth LE 广播的代码实现进行介绍。
学习目标
@ -11,7 +11,7 @@
- 学习广播的基本概念
- 学习扫描的基本概念
- 学习 NimBLE_Beacon 例程的代码结构
- 学习 :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` 例程的代码结构
广播 (Advertising) 与扫描 (Scanning) 是 Bluetooth LE 设备在进入连接前在设备发现 (Device Discovery) 阶段的工作状态。下面,我们先了解与广播有关的基本概念。
@ -33,7 +33,7 @@
蓝牙的无线电频段
################################
第一个问题指向的是,广播数据包应发送到哪一无线电频段。这个回答由蓝牙核心规范给出,答案是 2.4 GHz ISM 频段。选择该频段的理由是, 2.4 GHz ISM 频段是一个全球可用的免费无线电频段,不被任何国家以军事用途等理由管控,也无需向任何组织支付许可费用,因此该频段的可用性极高,且没有任何使用成本。不过,这也意味着 2.4 GHz ISM 频段非常拥挤,可能会与其他无线通信协议发生数据冲突,如 2.4 GHz WiFi。
第一个问题指向的是,广播数据包应发送到哪一无线电频段。这个回答由蓝牙核心规范给出,答案是 2.4 GHz ISM 频段。2.4 GHz ISM 频段是一个全球可用的免费无线电频段,不被任何国家以军事用途等理由管控,也无需向任何组织支付许可费用,因此该频段的可用性极高,且没有任何使用成本。不过,这也意味着 2.4 GHz ISM 频段非常拥挤,可能会与其他无线通信协议发生数据冲突,如 2.4 GHz WiFi。
蓝牙信道
@ -87,7 +87,7 @@ Bluetooth LE 4.2 标准中,广播数据包允许搭载最多 31 字节广播
- 254
- 1650
扩展广播数据包由 ADV_EXT_IND 和 AUX_ADV_IND 组成,分别在主广播信道 (Primary Advertising Channel) 和次广播信道 (Secondary Advertising Channel) 上传输。其中,主广播信道对应于信道 37-39 ,次广播信道对应于信道 0-36 。由于接收方总是在主广播信道中接收广播数据,因此发送方在发送扩展广播数据包时,应在主广播信道中发送 ADV_EXT_IND ,在次广播信道中发送 AUX_ADV_IND ,并在 ADV_EXT_IND 中指示 AUX_ADV_IND 所在的次广播信道;通过这种机制,接收方能够在接收到主广播信道的 ADV_EXT_IND 以后,根据指示到指定的次广播信道去接收 AUX_ADV_IND ,从而得到完整的扩展广播数据包。
扩展广播数据包由 `ADV_EXT_IND``AUX_ADV_IND` 组成,分别在主广播信道 (Primary Advertising Channel) 和次广播信道 (Secondary Advertising Channel) 上传输。其中,主广播信道对应于信道 37-39 ,次广播信道对应于信道 0-36 。由于接收方总是在主广播信道中接收广播数据,因此发送方在发送扩展广播数据包时,应在主广播信道中发送 `ADV_EXT_IND` ,在次广播信道中发送 `AUX_ADV_IND` ,并在 `ADV_EXT_IND` 中指示 `AUX_ADV_IND` 所在的次广播信道;通过这种机制,接收方能够在接收到主广播信道的 `ADV_EXT_IND` 以后,根据指示到指定的次广播信道去接收 `AUX_ADV_IND` ,从而得到完整的扩展广播数据包。
.. list-table::
:align: center
@ -99,10 +99,10 @@ Bluetooth LE 4.2 标准中,广播数据包允许搭载最多 31 字节广播
- 作用
* - 主广播信道 (Primary Advertising Channel)
- 37-39
- 用于传输扩展广播数据包的 ADV_EXT_IND
- 用于传输扩展广播数据包的 `ADV_EXT_IND`
* - 次广播信道 (Secondary Advertising Channel)
- 0-36
- 用于传输扩展广播数据包的 AUX_ADV_IND
- 用于传输扩展广播数据包的 `AUX_ADV_IND`
发送广播数据包的周期取多久?
@ -111,11 +111,11 @@ Bluetooth LE 4.2 标准中,广播数据包允许搭载最多 31 字节广播
广播间隔
##################
对于第二个问题,即发送广播数据包的周期怎么取,蓝牙标准中也给出了一个明确的参数定义,即广播间隔 (Advertising Interval)。广播间隔可取的范围为 20 ms 到 10.24 s ,取值步长为 0.625 ms。
对于第二个问题,即发送广播数据包的周期怎么取,蓝牙标准中也给出了一个明确的参数定义,即广播间隔 Advertising Interval。广播间隔可取的范围为 20 ms 到 10.24 s ,取值步长为 0.625 ms。
广播间隔的取值决定了广播者的可发现性 (Discoverability)以及设备功耗。当广播间隔取得太长时,广播数据包被接收方接收到的概率就会变得很低,此时广播者的可发现性就会变差。同时,广播间隔也不宜取得太短,因此频繁发送广播数据需要消耗更多的电量。所以,广播者需要在可发现性和能耗之间进行取舍,根据应用场景的需求选择最合适的广播间隔。
广播间隔的取值决定了广播者的可发现性 Discoverability 以及设备功耗。当广播间隔取得太长时,广播数据包被接收方接收到的概率就会变得很低,此时广播者的可发现性就会变差。同时,广播间隔也不宜取得太短,因此频繁发送广播数据需要消耗更多的电量。所以,广播者需要在可发现性和能耗之间进行取舍,根据应用场景的需求选择最合适的广播间隔。
值得一提的是,如果在同一空间中存在两个广播间隔相同的广播者,那么有概率出现重复性的撞包 (Packet Collision) 现象,即两个广播者总是在同一时刻向同一信道发送广播数据。由于广播是一个只发不收的过程,广播者无法得知是否发生了广播撞包。为了降低上述问题的发生概率,广播者应在每一次广播事件后添加 0-10 ms 的随机时延。
值得一提的是,如果在同一空间中存在两个广播间隔相同的广播者,那么有概率出现重复性的撞包 Packet Collision 现象,即两个广播者总是在同一时刻向同一信道发送广播数据。由于广播是一个只发不收的过程,广播者无法得知是否发生了广播撞包。为了降低上述问题的发生概率,广播者应在每一次广播事件后添加 0-10 ms 的随机时延。
广播数据包里包含哪些信息?
@ -201,7 +201,7 @@ PDU 头中含有较多信息,可以分为以下六个部分
* - 序号
- 名称
- 位数
- 比特位数
- 备注
* - 1
- PDU 类型 (PDU Type)
@ -222,7 +222,7 @@ PDU 头中含有较多信息,可以分为以下六个部分
* - 5
- 接收地址类型 (Rx Address, **RxAdd**)
- 1
- 同上
- 0/1 分别表示公共地址/随机地址
* - 6
- 有效负载长度 (Payload Length)
- 8
@ -336,7 +336,7 @@ PDU 有效负载也分为两部分
* - 不可解析随机私有地址 (Non-resolvable Random Private Address)
- 完全随机的地址,仅用于防止设备被追踪,非常少用
然后看广播数据。一个广播数据结构的格式定义如下
然后看**广播数据**。一个广播数据结构的格式定义如下
.. list-table::
:align: center
@ -370,7 +370,8 @@ PDU 有效负载也分为两部分
2. 多久扫描一次?一次扫描多久? (When?)
3. 扫描的过程中需要做什么? (What?)
第一个问题已经在广播的介绍中说明了。对于 Bluetooth LE 4.2 设备来说,广播者只会在广播信道,即编号为 37-39 的三个信道发送广播数据;对于 Bluetooth LE 5.0 设备来说,如果广播者启用了扩展广播特性,则会在主广播信道发送 ADV_EXT_IND ,在次广播信道发送 AUX_ADV_IND ,并在 ADV_EXT_IND 指示 AUX_ADV_IND 所在的次广播信道。所以相应的,对于 Bluetooth LE 4.2 设备来说,扫描者只需在广播信道接收广播数据包即可。对于 Bluetooth LE 5.0 设备来说,扫描者应在主广播信道接收主广播数据包和扩展广播数据包的 ADV_EXT_IND 若扫描者接收到了 ADV_EXT_IND ,且 ADV_EXT_IND 指示了一个次广播信道,那么还需要到对应的次广播信道去接收 AUX_ADV_IND ,以获取完整的扩展广播数据包。
第一个问题已经在广播的介绍中说明了。对于 Bluetooth LE 4.2 设备来说,广播者只会在广播信道,即编号为 37-39 的三个信道发送广播数据;对于 Bluetooth LE 5.0 设备来说,如果广播者启用了扩展广播特性,则会在主广播信道发送 `ADV_EXT_IND` ,在次广播信道发送 `AUX_ADV_IND` ,并在 `ADV_EXT_IND` 指示 `AUX_ADV_IND` 所在的次广播信道。
所以相应的,对于 Bluetooth LE 4.2 设备来说,扫描者只需在广播信道接收广播数据包即可。对于 Bluetooth LE 5.0 设备来说,扫描者应在主广播信道接收主广播数据包和扩展广播数据包的 `ADV_EXT_IND` 若扫描者接收到了 `ADV_EXT_IND` ,且 `ADV_EXT_IND` 指示了一个次广播信道,那么还需要到对应的次广播信道去接收 `AUX_ADV_IND` ,以获取完整的扩展广播数据包。
扫描窗口与扫描间隔
@ -378,9 +379,9 @@ PDU 有效负载也分为两部分
第二个问题分别指向扫描窗口 (Scan Window) 和 扫描间隔 (Scan Interval) 概念。
首先对扫描窗口进行说明。扫描窗口指的是扫描者在同一个 RF 信道持续接收蓝牙数据包的持续时间,例如扫描窗口参数设定为 50 ms 时,扫描者在每个 RF 信道都会不间断地扫描 50 ms。
- **扫描窗口**扫描者在同一个 RF 信道持续接收蓝牙数据包的持续时间,例如扫描窗口参数设定为 50 ms 时,扫描者在每个 RF 信道都会不间断地扫描 50 ms。
扫描间隔则指的是相邻两个扫描窗口开始时刻之间的时间间隔,所以扫描间隔必然大于等于扫描窗口。
- **扫描间隔**相邻两个扫描窗口开始时刻之间的时间间隔,所以扫描间隔必然大于等于扫描窗口。
下图在时间轴上展示了扫描者的广播数据包接收过程,其中扫描者的扫描间隔为 100 ms ,扫描窗口为 50 ms ;广播者的广播间隔为 50 ms ,广播数据包的发送时长仅起到示意作用。可以看到,第一个扫描窗口对应 37 信道,此时扫描者恰好接收到了广播者第一次在 37 信道发送的广播数据包,以此类推。
@ -419,17 +420,17 @@ PDU 有效负载也分为两部分
例程实践
-------------------------------------------
在掌握了广播与扫描的相关知识以后,接下来让我们结合 NimBLE_Beacon 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 Beacon 设备,对学到的知识进行实践。
在掌握了广播与扫描的相关知识以后,接下来让我们结合 :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 Beacon 设备,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
1. 一块 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
3. 在手机上安装 **nRF Connect for Mobile** 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`。
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`IDF 快速入门 <../../../get-started/index>`。
动手试试
@ -460,7 +461,7 @@ PDU 有效负载也分为两部分
$ idf.py set-target <chip-name>
你应该能看到命令行
你应该能看到以下命令行
.. code-block:: shell
@ -475,7 +476,7 @@ PDU 有效负载也分为两部分
$ idf.py flash monitor
你应该能看到命令行
你应该能看到以下命令行
.. code-block:: shell
@ -490,7 +491,7 @@ PDU 有效负载也分为两部分
.. _nimble_beacon_details:
打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_Beacon 设备,如下图所示
打开手机上的 **nRF Connect for Mobile** 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_Beacon 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-scan-list-nimble-beacon.jpg
:align: center
@ -501,7 +502,7 @@ PDU 有效负载也分为两部分
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_Beacon 设备。
观察到 NimBLE Beacon 设备下带有丰富的设备信息,甚至还带有乐鑫的网址(这就是信标广告功能的体现)。点击右下角的 `RAW` 按钮,可以看到广播数据包的原始信息,如下
观察到 NimBLE Beacon 设备下带有丰富的设备信息,甚至还带有乐鑫的网址(这就是信标广告功能的体现)。点击右下角的 **RAW** 按钮,可以看到广播数据包的原始信息,如下
.. figure:: ../../../../_static/ble/ble-adv-packet-raw-data.jpg
:align: center
@ -510,7 +511,7 @@ PDU 有效负载也分为两部分
广播数据包原始信息
Details 表格即广播数据包和扫描响应数据包中的所有广播数据结构,可以整理如下
**Details** 表格即广播数据包和扫描响应数据包中的所有广播数据结构,可以整理如下
.. list-table::
:align: center
@ -523,37 +524,37 @@ Details 表格即广播数据包和扫描响应数据包中的所有广播数据
- 原始数据
- 解析值
* - 标志位
- 2
- 2 Bytes
- `0x01`
- `0x06`
- General Discoverable, BR/EDR Not Supported
* - 完整设备名称
- 14
- 14 Bytes
- `0x09`
- `0x4E696D424C455F426561636F6E`
- NimBLE_Beacon
* - 发送功率等级
- 2
- 2 Bytes
- `0x0A`
- `0x09`
- 9 dBm
* - 设备外观
- 3
- 3 Bytes
- `0x19`
- `0x0002`
- 通用标签
* - LE 角色
- 2
- 2 Bytes
- `0x1C`
- `0x00`
- 仅支持外设设备
* - 设备地址
- 8
- 8 Bytes
- `0x1B`
- `0x46F506BDF5F000`
- `F0:F5:BD:06:F5:46`
* - URI
- 17
- 17 Bytes
- `0x24`
- `0x172F2F6573707265737369662E636F6D`
- `https://espressif.com`
@ -562,7 +563,7 @@ Details 表格即广播数据包和扫描响应数据包中的所有广播数据
你可能还注意到,对应于设备外观的 Raw Data 为 `0x0002`,而代码中对 Generic Tag 的定义是 `0x0200`;还有,设备地址的 Raw Data 除了最后一个字节 `0x00` 以外,似乎与实际地址完全颠倒。这是因为, Bluetooth LE 的空中数据包遵循小端 (Little Endian) 传输的顺序,所以低字节的数据反而会在靠前的位置。
另外,注意到 nRF Connect for Mobile 程序并没有为我们提供 `CONNECT` 按钮以连接至此设备。这符合我们的预期,因为 Beacon 设备本来就应该是不可连接的。下面,让我们深入代码细节,看看这样的一个 Beacon 设备是怎样实现的。
另外,注意到 **nRF Connect for Mobile** 程序并没有为我们提供 **CONNECT** 按钮以连接至此设备。这符合我们的预期,因为 Beacon 设备本来就应该是不可连接的。下面,让我们深入代码细节,看看这样的一个 Beacon 设备是怎样实现的。
代码详解
@ -574,7 +575,7 @@ Details 表格即广播数据包和扫描响应数据包中的所有广播数据
.. _nimble_beacon_project_structure:
NimBLE_Beacon 的根目录大致分为以下几部分
:example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` 的根目录大致分为以下几部分
- `README*.md`
- 工程的说明文档
@ -611,7 +612,7 @@ NimBLE_Beacon 的根目录大致分为以下几部分
2. 初始化 GAP 服务
3. 启动 NimBLE 主机层的 FreeRTOS 线程
ESP32 的蓝牙协议栈使用 NVS Flash 存储相关配置,所以在初始化蓝牙协议栈之前,必须调用 `nvs_flash_init` API 以初始化 NVS Flash ,某些情况下需要调用 `nvs_flash_erase` API 对 NVS Flash 进行擦除后再初始化。
{IDF_TARGET_NAME} 的蓝牙协议栈使用 NVS Flash 存储相关配置,所以在初始化蓝牙协议栈之前,必须调用 `nvs_flash_init` API 以初始化 NVS Flash ,某些情况下需要调用 `nvs_flash_erase` API 对 NVS Flash 进行擦除后再初始化。
.. code-block:: C
@ -879,6 +880,6 @@ ESP32 的蓝牙协议栈使用 NVS Flash 存储相关配置,所以在初始化
总结
---------
通过本教程,你了解了广播和扫描的基本概念,并通过 NimBLE_Beacon 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE Beacon 设备的方法。
通过本教程,你了解了广播和扫描的基本概念,并通过 :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>` 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE Beacon 设备的方法。
你可以尝试对例程中的数据进行修改,并在 nRF Connect for Mobile 调试工具中查看修改结果。例如,你可以尝试修改 `adv_fields``rsp_fields` 结构体,以修改被填充的广播数据结构,或者交换广播数据包和扫描响应数据包中的广播数据结构。但需要注意的一点是,广播数据包和扫描响应数据包的广播数据上限为 31 字节,若设定的广播数据结构大小超过该限值,调用 `ble_gap_adv_start` API 将会失败。

View File

@ -47,7 +47,7 @@ Bluetooth LE 协议定义了三层软件结构,自上而下分别是
Bluetooth LE 分层结构
作为应用开发者,在开发过程中我们主要与主机层提供的 API 接口打交道,这要求我们对主机层中的蓝牙协议有一定的了解。接下来,我们会从连接和数据交两个角度,对 GAP 和 GATT/ATT 层的基本概念进行介绍。
作为应用开发者,在开发过程中我们主要与主机层提供的 API 接口打交道,这要求我们对主机层中的蓝牙协议有一定的了解。接下来,我们会从连接和数据交两个角度,对 GAP 和 GATT/ATT 层的基本概念进行介绍。
GAP 层 - 定义设备的连接
@ -71,7 +71,7 @@ GAP 中共定义了三种设备的连接状态以及五种不同的设备角色
- 外围设备 (Peripheral)
- 中央设备 (Central)
广播者向外广播的数据中包含设备地址等信息,用于向外界设备表明广播者的存在,并告知其他设备是否可以连接。扫描者则持续接收环境中的广播数据包。若某一个扫描者发现了一个可连接的广播者,并希望与之建立连接,可以将角色切换为连接发起者。当连接发起者再次收到该广播者的广播数据,会立即发起连接请求 (Connection Request);在广播者未开启白名单 (White List, 又称 Accept List) 或连接发起者在广播者的白名单之中时,连接将被成功建立。
广播者向外广播的数据中包含设备地址等信息,用于向外界设备表明广播者的存在,并告知其他设备是否可以连接。扫描者则持续接收环境中的广播数据包。若某一个扫描者发现了一个可连接的广播者,并希望与之建立连接,可以将角色切换为连接发起者。当连接发起者再次收到该广播者的广播数据,会立即发起连接请求 (Connection Request);在广播者未开启白名单 (Filter Accept List, 又称 White List) 或连接发起者在广播者的白名单之中时,连接将被成功建立。
进入连接以后,原广播者转变为外围设备(旧称从设备 Slave ),原扫描者或连接初始化者转变为中央设备(旧称主设备 Master )。
@ -112,7 +112,7 @@ GATT/ATT 层定义了进入连接状态后,设备之间的数据交换方式
ATT 层
#############
ATT 的全称是属性协议 (Attribute Protocol, ATT),定义了一种称为属性 (Attribute) 的基本数据结构,以及基于服务器/客户端架构的数据访问方式。
ATT 的全称是属性协议 (Attribute Protocol, ATT),定义了一种称为**属性 (Attribute)** 的基本数据结构,以及基于服务器/客户端架构的数据访问方式。
简单来说,数据以属性的形式存储在服务器上,等待客户端的访问。以智能开关为例,开关量作为数据,以属性的形式存储在智能开关内的蓝牙芯片(服务器)中,此时用户可以通过手机(客户端)访问智能开关蓝牙芯片(服务器)上存放的开关量属性,获取当前的开关状态(读访问),或控制开关的闭合与断开(写访问)。
@ -123,7 +123,7 @@ ATT 的全称是属性协议 (Attribute Protocol, ATT),定义了一种称为
- 值 (Value)
- 访问权限 (Permissions)
在协议栈实现中,属性一般被放在称为属性表 (Attribute Table) 的结构体数组中管理。一个属性在这张表中的索引,就是属性的句柄,常为一无符号整型。
在协议栈实现中,属性一般被放在称为**属性表 (Attribute Table)** 的结构体数组中管理。一个属性在这张表中的索引,就是属性的句柄,常为一无符号整型。
属性的类型由 UUID 表示,可以分为 16 位、32 位与 128 位 UUID 三类。 16 位 UUID 由蓝牙技术联盟 (Bluetooth Special Interest Group, Bluetooth SIG) 统一定义,可以在其公开发布的 `Assigned Numbers <https://www.bluetooth.com/specifications/assigned-numbers/>`__ 文件中查询;其他两种长度的 UUID 用于表示厂商自定义的属性类型,其中 128 位 UUID 较为常用。
@ -155,7 +155,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
除此以外,特征数据中还可能包含若干可选的描述符属性 (Characteristic Descriptor Attribute)。
一个服务本身也由一个属性进行描述,称为服务声明属性 (Service Declaration Attribute)。一个服务中可以存在一个或多个特征数据,它们之间体现为从属关系。另外,一个服务可以通过 Include 机制引用另一个服务,复用其特性定义,避免如设备名称、制造商信息等相同特性的重复定义。
一个服务本身也由一个属性进行描述,称为服务声明属性 (Service Declaration Attribute)。一个服务中可以存在一个或多个特征数据,它们之间体现为从属关系。另外,一个服务可以通过 `Include` 机制引用另一个服务,复用其特性定义,避免如设备名称、制造商信息等相同特性的重复定义。
规范是一个预定义的服务集合,实现了某规范中所定义的所有服务的设备即满足该规范。例如 Heart Rate Profile 规范由 Heart Rate Service 和 Device Information Service 两个服务组成,那么可以称实现了 Heart Rate Service 和 Device Information Service 服务的设备符合 Heart Rate Profile 规范。
@ -178,11 +178,11 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
1. 一块 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
3. 在手机上安装 **nRF Connect for Mobile** 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`。
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`IDF 快速入门 <../../../get-started/index>`。
动手试试
@ -214,7 +214,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
$ idf.py set-target <chip-name>
你应该能看到命令行
你应该能看到以下命令行
.. code-block:: shell
@ -229,7 +229,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
$ idf.py flash monitor
你应该能看到命令行
你应该能看到以下命令行
.. code-block:: shell
@ -243,7 +243,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
连接到开发板
#######################
现在开发板已准备就绪。接下来,打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_GATT 设备,如下图所示
现在开发板已准备就绪。接下来,打开手机上的 **nRF Connect for Mobile** 程序,在 **SCANNER** 标签页中下拉刷新,找到 NimBLE_GATT 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-get-started-connect-brief.jpg
:align: center
@ -254,7 +254,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_GATT 设备。
点击 NimBLE_GATT 设备条目,可以展开看到广播数据的详细信息。
点击 **NimBLE_GATT** 设备条目,可以展开看到广播数据的详细信息。
.. figure:: ../../../../_static/ble/ble-get-started-connect-details.jpg
:align: center
@ -263,7 +263,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
广播数据详情
点击右侧的 CONNECT 按钮,在手机连接的同时,可以在开发板的串口输出中观察到许多与连接相关的日志信息。随后,手机上会显示 NimBLE_GATT 标签页,左上角应有 CONNECTED 状态,说明手机已成功通过 Bluetooth LE 协议连接至开发板。在 CLIENT 子页中,你应该能够看到四个 GATT 服务,如图所示
点击右侧的 **CONNECT** 按钮,在手机连接的同时,可以在开发板的串口输出中观察到许多与连接相关的日志信息。随后,手机上会显示 NimBLE_GATT 标签页,左上角应有 **CONNECTED** 状态,说明手机已成功通过 Bluetooth LE 协议连接至开发板。在 CLIENT 子页中,你应该能够看到四个 GATT 服务,如图所示
.. figure:: ../../../../_static/ble/ble-get-started-gatt-services-list.jpg
:align: center
@ -274,13 +274,13 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
前两个服务是 GAP 服务和 GATT 服务,这两个服务是 Bluetooth LE 应用中的基础服务。后两个服务是 Bluetooth SIG 定义的 Heart Rate Service 服务和 Automation IO Service 服务,分别提供心率数据读取和 LED 控制功能。
在服务名的下方,对应有各个服务的 UUID 以及服务主次标识。如 Heart Rate Service 服务的 UUID 为 `0x180D`,是一个主服务 (Primary Service)。需要注意的是,服务的名称是通过 UUID 解析得到的。以 nRF Connect for Mobile 为例,在实现 GATT 客户端时,开发者会将 Bluetooth SIG 定义的服务,以及开发商 Nordic Semiconductor 自定义的服务预先写入数据库中,然后根据 GATT 服务的 UUID 进行服务信息解析。所以,假如某一服务的 UUID 不在数据库中,那么该服务的服务信息就无法被解析,服务名称将会显示为未知服务 (Unknown Service)。
在服务名的下方,对应有各个服务的 UUID 以及服务主次标识。如 Heart Rate Service 服务的 UUID 为 `0x180D`,是一个主服务 (Primary Service)。需要注意的是,服务的名称是通过 UUID 解析得到的。以 nRF Connect for Mobile 为例,在实现 GATT 客户端时,开发者会将 Bluetooth SIG 定义的服务,以及例程自定义的服务预先写入数据库中,然后根据 GATT 服务的 UUID 进行服务信息解析。所以,假如某一服务的 UUID 不在数据库中,那么该服务的服务信息就无法被解析,服务名称将会显示为未知服务 (Unknown Service)。
把灯点亮!
##################
下面体验一下本例程的功能。首先,点击 Automation IO Service 服务,可以看到该服务下有一个 LED 特征数据。
下面体验一下本例程的功能。首先,点击 **Automation IO Service** 服务,可以看到该服务下有一个 LED 特征数据。
.. figure:: ../../../../_static/ble/ble-get-started-automation-io-service-details.jpg
:align: center
@ -289,7 +289,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
Automation IO Service
如图,该 LED 特征数据的 UUID 为 128 位的厂商自定义 UUID 。实际上,这是 Nordic Semiconductor 自定义的 LED 特征数据,在 nRF Connect for Mobile 上有专门的控制页面适配。点击右侧的上传按钮,可以对该特征数据进行写访问,如下图所示。
如图,该 LED 特征数据的 UUID 为 128 位的厂商自定义 UUID 。 点击右侧的**UPLOAD**按钮,可以对该特征数据进行写访问,如下图所示。
.. figure:: ../../../../_static/ble/ble-get-started-led-write.jpg
:align: center
@ -298,7 +298,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
对 LED 特征数据进行写访问
选择 ON 选项,然后发送,你应该能看到开发板上的 LED 被点亮了。选择 OFF 选项,然后发送,你应该能观察到开发板上的 LED 又熄灭了。
选择 **ON** 选项,然后发送,你应该能看到开发板上的 LED 被点亮了。选择 **OFF** 选项,然后发送,你应该能观察到开发板上的 LED 又熄灭了。
若你的开发板上没有电源指示灯以外的 LED ,你应该能在日志输出中观察到对应的状态指示。
@ -306,7 +306,7 @@ GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基
接收心率数据
#######################
接下来,点击 Heart Rate Service 服务,可以看到该服务下有一个 Heart Rate Measurement 特征数据。
接下来,点击 **Heart Rate Service** 服务,可以看到该服务下有一个 Heart Rate Measurement 特征数据。
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-service-details.jpg
:align: center
@ -339,4 +339,4 @@ Heart Rate Measurement 特征数据的 UUID 是 `0x2A37`,这是一个 Bluetoot
总结
---------
通过本教程,你了解了 Bluetooth LE 的分层架构、Bluetooth LE 协议栈中主机层和控制器层的基本功能以及 GAP 层与 GATT/ATT 层的作用。随后,通过 NimBLE_GATT_Server 例程,你掌握了如何使用 ESP-IDF 开发框架进行 Bluetooth LE 应用的构建与烧录,能够在手机上使用 nRF Connect for Mobile 调试程序,远程控制开发板上 LED 的点亮与熄灭,以及接收随机生成的心率数据。你已经迈出了走向 Bluetooth LE 开发者的第一步,恭喜!
通过本教程,你了解了 Bluetooth LE 的分层架构、Bluetooth LE 协议栈中主机层和控制器层的基本功能以及 GAP 层与 GATT/ATT 层的作用。随后,通过 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>` 例程,你掌握了如何使用 ESP-IDF 开发框架进行 Bluetooth LE 应用的构建与烧录,能够在手机上使用 **nRF Connect for Mobile** 调试程序,远程控制开发板上 LED 的点亮与熄灭,以及接收随机生成的心率数据。你已经迈出了走向 Bluetooth LE 开发者的第一步,恭喜!