[BT-3811] Added BLE Get Started

This commit is contained in:
Zhou Xiao 2024-08-27 09:41:50 +08:00 committed by Wei Yu Han
parent c9df77efbf
commit f62f21dafb
31 changed files with 2399 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
docs/_static/ble/ble-architecture.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

View File

@ -28,6 +28,10 @@ BLE_DOCS = ['api-guides/ble/index.rst',
'api-guides/ble/ble-feature-support-status.rst',
'api-guides/ble/host-feature-support-status.rst',
'api-reference/bluetooth/bt_le.rst',
'api-guides/ble/get-started/ble-introduction.rst',
'api-guides/ble/get-started/ble-device-discovery.rst',
'api-guides/ble/get-started/ble-connection.rst',
'api-guides/ble/get-started/ble-data-exchange.rst',
'api-reference/bluetooth/esp_gap_ble.rst',
'api-reference/bluetooth/esp_gatt_defs.rst',
'api-reference/bluetooth/esp_gatts.rst',

View File

@ -0,0 +1 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-connection.rst

View File

@ -0,0 +1 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-data-exchange.rst

View File

@ -0,0 +1 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-device-discovery.rst

View File

@ -0,0 +1 @@
.. include:: ../../../../zh_CN/api-guides/ble/get-started/ble-introduction.rst

View File

@ -13,6 +13,18 @@ Overview
overview
ble-feature-support-status
***************
Get Started
***************
.. toctree::
:maxdepth: 1
get-started/ble-introduction
get-started/ble-device-discovery
get-started/ble-connection
get-started/ble-data-exchange
**********
Profile
**********

View File

@ -0,0 +1,477 @@
连接
===================
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其三,旨在对 Bluetooth LE 的连接过程进行简要介绍。随后,本教程会结合 NimBLE_Connection 例程,基于 NimBLE 主机层协议栈,对外围设备的代码实现进行介绍。
学习目标
----------------------------
- 学习连接的基本概念
- 学习连接相关的参数
- 学习 NimBLE_Connection 例程的代码结构
连接的基本概念
-----------------------------
连接的发起
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*在 Bluetooth LE 5.0 引入扩展广播特性以后, Legacy ADV 和 Extended ADV 对应的连接建立过程略有差异,下以 Legacy ADV 对应的连接建立过程为例。*
当扫描者在某一个广播信道接收到一个广播数据包时,若该广播者是可连接的,那么扫描者可以在同一广播信道发送连接请求 (Connection Request)。对于广播者来说,它可以设置 *接受列表 (Accept List)* 以过滤不受信任的设备,或接受任一扫描者的连接请求。随后,广播者转变为外围设备,扫描者转变为中央设备,两者之间可以在数据信道进行双向通信。
:ref:`扫描请求与扫描响应 <scan_request_and_scan_response>` 所述,广播者在每一个信道的广播结束以后,都会短暂进入 RX 模式,以接收可能的扫描请求。实际上,这个 RX 过程中还可以接受连接请求。所以对于扫描者来说,发送连接请求的时间窗口和发送扫描请求的时间窗口是类似的。
.. figure:: ../../../../_static/ble/ble-advertiser-rx-connection-request.png
:align: center
:scale: 30%
:alt: 连接的发起
连接的发起
连接间隔与连接事件
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
在连接中,中央设备与外围设备会周期性地进行数据交换,这个数据交换的周期被称为连接间隔 (Connection Interval)。连接间隔作为连接参数之一,在连接请求中被首次确定,后续也可以进行修改。连接间隔的取值步长 (Step Size) 为 1.25 ms ,取值范围为 7.5 ms (6 steps) - 4.0 s (3200 steps) 。
一次数据交换的过程被称为连接事件 (Connection Event)。一次连接事件中,存在一次或多次数据包交换(数据量比较大时需要分包发送);一次数据包交换的过程是,中央设备先给外围设备发送一个数据包,随后外围设备给中央设备发送一个数据包。即便连接中任意一方在连接间隔开始时无需发送数据,也必须发送空数据包以维持连接。
连接间隔与连接事件在连接中的时序关系可以参考下图。
.. figure:: ../../../../_static/ble/ble-connection-event-and-connection-interval.png
:align: center
:scale: 30%
:alt: 连接间隔与连接事件
连接间隔与连接事件
值得一提的是,若一次连接事件中需要发送的数据很多,导致连接事件时长超过了连接间隔,那么必须将一次连接事件拆分成多次连接事件;这意味着,假如连接间隔的剩余时间不足以完成下一次数据包交换,那么下一次数据包交换必须等到下一次连接间隔开始时才能进行。
当需求的数据交换频率比较低时,可以设定较长的连接间隔;在连接间隔中,设备可以在除连接事件以外的时间休眠,以降低能耗。
连接参数
-----------------------------------
前文提到,连接间隔是一种连接参数,其初始值由中央设备在连接请求中给出,也支持在后续的连接中进行修改。除此以外,连接中还存在许多其他的连接参数,下面我们挑选其中的一些重要参数进行讲解。
超时参数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
连接超时参数 (Supervision Timeout) 规定了两次成功连接事件之间的最长时间。若在一次成功的连接事件之后,经过了连接超时时间却仍没有完成另一次成功的连接事件,则可以认为连接已断开。这个参数对于连接状态的维护是非常重要的,例如连接中的其中一方突然意外断电,或离开了通信范围,那么连接中的另一方可以通过判断连接是否超时,决定是否需要断开连接以节省通信资源。
外围设备延迟
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
外围设备延迟 (Peripheral Latency) 规定了外围设备在无需发送数据的前提下,最多可忽略的连接事件数量。
为了理解这个连接参数的作用,让我们以蓝牙鼠标为例,分析其应用场景。用户在使用键盘的过程中,鼠标并没有需要发送的有效数据,此时最好降低数据包发送的频率以节省电量;在使用鼠标的过程中,我们希望鼠标能够尽可能快地发送数据,以降低使用延迟。也就是说,蓝牙鼠标的数据发送是间歇性高频率的。此时,如果仅靠连接间隔参数进行连接调节,则那么较低的连接间隔会导致高能耗,较高的连接间隔会导致高延迟。
在这种场景下,外围设备延迟机制将是一个完美的解决方案。为了降低蓝牙鼠标的延迟,我们可以将连接间隔设为一个较小的值,例如 10 ms ,那么在密集使用时数据交换频率可达 100 Hz ;随后,我们将外围设备延迟设定为 100 ,那么蓝牙鼠标在不使用的状态下,实际的数据交换频率可降低至 1 Hz 。通过这种设计,我们在不调整连接参数的前提下,实现了可变的数据交换频率,在最大程度上提升了用户体验。
最大传输单元
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
最大传输单元 (Maximum Transmission Unit, MTU) 指的是单个 ATT 数据包的最大字节数。在介绍 MTU 参数之前,有必要先对数据通道数据包 (Data Channel Packet) 的结构进行说明。
数据通道数据包和 :ref:`广播数据包 <adv_packet_structure>` 的最外层结构一致,区别在于 PDU 的结构。数据 PDU 可以分为三部分,如下
.. list-table::
:align: center
:widths: 10 30 20 40
:header-rows: 1
* - 序号
- 名称
- 字节数
- 备注
* - 1
- 头 (Header)
- 2
-
* - 2
- 有效负载 (Payload)
- 0-27 / 0-251
- 在 Bluetooth LE 4.2 以前,有效负载最大值为 27 字节; Bluetooth LE 4.2 引入了数据长度扩展 (Data Length Extension, DLE) 特性,有效负载的最大值可达 251 字节
* - 3
- 消息完整性检查 (Message Integrity Check, MIC)
- 4
- 可选
数据 PDU 的有效负载可以分为两部分,如下
.. list-table::
:align: center
:widths: 10 70 20
:header-rows: 1
* - 序号
- 名称
- 字节数
* - 1
- L2CAP 头 (L2CAP Header)
- 4
* - 2
- ATT 数据 (ATT Header + ATT Data)
- 0-23 / 0-247
MTU 的默认值为 23 字节,恰为 Bluetooth LE 4.2 之前单个数据 PDU 的最大可承载 ATT 数据字节数。
MTU 可以设定为更大的值,例如 140 字节。在 Bluetooth LE 4.2 以前,由于有效负载中最多只有 23 字节可以承载 ATT 数据,所以必须将完整的一包 ATT 数据包拆分成若干份,分散到多个数据 PDU 中。在 Bluetooth LE 4.2 以后,单个数据 PDU 最多可以承载 247 字节 ATT 数据,所以 MTU 为 140 字节时仍然可以使用单个数据 PDU 承载。
例程实践
-------------------------------------------
在掌握了连接的相关知识以后,接下来让我们结合 NimBLE_Connection 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的外围设备,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`
动手试试
^^^^^^^^^^^^^^^^^^
构建与烧录
#################
本教程对应的参考例程为 :example:`NimBLE_Connection <bluetooth/ble_get_started/nimble/NimBLE_Connection>`
你可以通过以下命令进入例程目录
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_Connection
注意,请将 `<ESP-IDF Path>` 替换为你本地的 ESP-IDF 文件夹路径。随后,你可以通过 VSCode 或其他你常用的 IDE 打开 NimBLE_Connection 工程。以 VSCode 为例,你可以在使用命令行进入例程目录后,通过以下命令打开工程
.. code-block:: shell
$ code .
随后,在命令行中进入 ESP-IDF 环境,完成芯片设定
.. code-block:: shell
$ idf.py set-target <chip-name>
你应该能看到命令行以
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
等提示结束,这说明芯片设定完成。接下来,连接开发板至电脑,随后运行以下命令,构建固件并烧录至开发板,同时监听 {IDF_TARGET_NAME} 开发板的串口输出
.. code-block:: shell
$ idf.py flash monitor
你应该能看到命令行以
.. code-block:: shell
...
main_task: Returned from app_main()
等提示结束。
连接,然后断开
##############################
打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_CONN 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-connection-device-list.jpg
:align: center
:scale: 30%
找到 NimBLE_CONN 设备
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_CONN 设备。
:ref:`NimBLE_Beacon <nimble_beacon_details>` 相比,可以观察到大部分广播数据是一致的,但多了一项 `Advertising Interval` 数据,其值为 500 ms ;在 `CONNECT` 按钮下方,确实也可以观察到广播间隔为 510 ms 左右。
点击 `CONNECT` 按钮连接到设备,在手机上应能够看到 GAP 服务,如下
.. figure:: ../../../../_static/ble/ble-connection-connected.jpg
:align: center
:scale: 30%
连接到 NimBLE_CONN 设备
此时应该还能观察到开发板上的 LED 亮起。点击 `DISCONNECT`,断开与设备的连接,此时应能观察到开发板上的 LED 熄灭。
若你的开发板上没有电源指示灯以外的 LED ,你应该能在日志输出中观察到对应的状态指示。
查看日志输出
#################################
将视线转移到日志输出窗口。在连接到设备时,应能观察到如下日志
.. 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
上述日志的第一部分是连接建立时,设备输出的连接信息,包括连接句柄、设备和手机的蓝牙地址以及连接参数信息。其中 `conn_itvl` 指的是连接间隔, `conn_latency` 指的是外围设备延迟, `supervision_timeout` 是连接超时参数,其他参数暂时忽略。
第二部分是设备发起了连接参数的更新,可以观察到设备请求将外围设备延迟参数更新至 3 。
第三部分是连接更新时,设备输出的连接信息。可以观察到,外围设备延迟参数成功更新至 3 ,其他连接参数不变。
当断开与设备的连接时,应能观察到如下日志
.. 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!
可以观察到,设备在连接断开时输出了连接断开原因,随后再次发起广播。
代码详解
-----------------------------------------------------
工程结构综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_project_structure:
NimBLE_Connection 的根目录结构与 :ref:`NimBLE_Beacon <nimble_beacon_project_structure>` 完全一致,不过在完成了固件的构建以后,你可能会观察到根目录下多了一个 `managed_components` 目录,里面含有固件构建时自动引入的依赖;本例中为 `led_strip` 组件,用于控制开发板的 LED。该依赖项在 `main/idf_component.yml` 文件中被引入。
另外,在 `main` 文件夹中引入了 LED 控制相关的源代码。
程序行为综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_program_behavior:
本例程的程序行为与 :ref:`NimBLE_Beacon <nimble_beacon_program_behavior>` 的程序行为基本一致,区别在于本例程进入广播状态以后,可以接受来自扫描者的扫描请求并进入连接状态。此外,本例程通过一个回调函数 `gap_event_handler` 接收连接事件,并做出相应的行为,如在连接建立时点亮 LED ,在连接断开时熄灭 LED 等。
入口函数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_connection_entry_point:
本例程的入口函数与 :ref:`NimBLE_Beacon <nimble_beacon_entry_point>` 基本一致,区别在于,在初始化 NVS Flash 前,通过调用 `led_init` 函数,对 LED 进行初始化。
开始广播
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
广播的发起过程与 :ref:`NimBLE_Beacon <nimble_beacon_start_advertising>` 基本一致,但存在一些细节上的区别。
首先,我们在扫描响应中添加了广播间隔参数。我们希望设置广播间隔为 500 ms ,而广播间隔的单位为 0.625 ms ,所以这里应将广播间隔设置为 `0x320`,不过 NimBLE 提供了一个单位转换的宏 `BLE_GAP_ADV_ITVL_MS`,我们可以借助这个宏避免手动运算,如下
.. 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;
...
}
其次,我们希望设备是可连接的,所以需要将广播模式从不可连接修改为可连接;另外,在扫描响应中设定的广播间隔参数仅仅起到告知扫其他设备的作用,不影响实际的广播间隔,该参数必须设定到广播参数结构中才能真正生效,这里我们将广播间隔的最小值与最大值分别设为 500 ms 和 510 ms ;最后,我们希望用回调函数 `gap_event_handler` 处理 GAP 事件,所以将该回调函数传入对应于开始广播的 API `ble_gap_adv_start` 中。相关代码如下
.. 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!");
...
}
`ble_gap_adv_start` 的返回值为 0 ,说明设备成功发起广播。此后, NimBLE 协议栈将会在任意 GAP 事件触发时调用 `gap_event_handler` 回调函数,并传入对应的 GAP 事件。
GAP 事件处理
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
本例程中,我们对三种不同的 GAP 事件进行处理,分别是
- 连接事件 `BLE_GAP_EVENT_CONNECT`
- 连接断开事件 `BLE_GAP_EVENT_DISCONNECT`
- 连接更新事件 `BLE_GAP_EVENT_CONN_UPDATE`
连接事件在一个连接成功建立或连接建立失败时被触发。当连接建立失败时,我们重新开始发起广播;当连接建立成功时,我们将连接的信息输出到日志,点亮 LED ,并发起一次连接参数更新,旨在将外围设备延迟参数更新至 3 ,代码如下
.. 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;
}
连接断开事件在连接任意一方断开连接时被触发,此时我们将连接断开的原因输出至日志,熄灭 LED 并重新开始广播,代码如下
.. 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;
...
}
连接更新事件在连接参数更新时被触发,此时我们将更新后的连接信息输出至日志,代码如下
.. 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;
...
}
总结
----------------
通过本教程,你了解了连接的基本概念,并通过 NimBLE_Connection 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE 外围设备的方法。
你可以尝试对例程中的参数进行修改,并在日志输出中观察修改结果。例如,你可以修改外围设备延迟或连接超时参数,观察连接参数的修改是否能够触发连接更新事件。

View File

@ -0,0 +1,664 @@
数据交换
===================
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其四,旨在对 Bluetooth LE 连接中的数据交换过程进行简要介绍。随后,本教程会结合 NimBLE_GATT_Server 例程,基于 NimBLE 主机层协议栈,对 GATT 服务器的代码实现进行介绍。
学习目标
---------------------------
- 学习特征数据和服务的数据结构细节
- 学习 GATT 的不同数据访问操作
- 学习 NimBLE_GATT_Server 例程的代码结构
GATT 数据特征与服务
----------------------------------
GATT 服务是 Bluetooth LE 连接中两个设备进行数据交换的基础设施,其最小数据单元是属性。在 :ref:`数据表示与交换 <gatt_att_introduction>` 中,我们对 ATT 层的属性以及 GATT 层的特征数据、服务与规范进行了简要介绍。下面我们对基于属性的数据结构细节进行说明。
属性
^^^^^^^^^^^^^^^^^^^^^^^^^^^
属性由以下四部分组成
.. list-table::
:align: center
:widths: 10 30 60
:header-rows: 1
* - 序号
- 名称
- 说明
* - 1
- 句柄 (Handle)
- 16 位无符号整型,表示属性在 :ref:`属性表 <attribute_table>` 中的索引
* - 2
- 类型 (Type)
- ATT 属性使用 UUID (Universally Unique ID) 对类型进行区分
* - 3
- 访问权限
- 是否需要加密/授权?可读或可写?
* - 4
- 值
- 实际用户数据或另一属性的元数据
Bluetooth LE 中存在两种类型的 UUID ,如下
1. SIG 定义的 16 位 UUID
2. 厂商自定义的 128 位 UUID
在 SIG 官方提供的 `Assigned Numbers <https://www.bluetooth.com/specifications/assigned-numbers/>`_ 标准文件中,给出了一些常用特征数据和服务的 UUID ,例如
.. list-table::
:align: center
:widths: 20 60 20
:header-rows: 1
* - 分类
- 类型名称
- UUID
* - 服务
- 血压服务 (Blood Pressure Service)
- `0x1810`
* - 服务
- 通用音频服务 (Common Audio Service)
- `0x1853`
* - 特征数据
- 年龄 (Age)
- `0x2A80`
* - 特征数据
- 外观 (Appearance)
- `0x2A01`
事实上,这些服务和特征数据的定义也由 SIG 一并给出。例如心率测量值 (Heart Rate Measurement) 的值中必须含有标志位、心率测量值场,可以含有能量拓展场、 RR-间隔场以及传输间隔场等。所以,使用 SIG 定义的 UUID 使得不同厂商的 Bluetooth LE 设备之间可以识别对方的服务或特征数据,实现跨厂商的 Bluetooth LE 设备通信。
厂商自定义的 128 位 UUID 则用于满足厂商开发私有服务或数据特征的需求,例如本例程中 LED 特征数据的 UUID 为 `0x00001525-1212-EFDE-1523-785FEABCD123`,是一个厂商自定义的 128 位 UUID 。
特征数据
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _characteristic_attributes:
一个特征数据常由以下几个属性组成
.. list-table::
:align: center
:widths: 10 30 30 30
:header-rows: 1
* - 序号
- 名称
- 作用
- 备注
* - 1
- 特征数据声明 (Characteristic Declaration)
- 含有特征数据值的读写属性 (Properties)、句柄以及 UUID 信息
- UUID 为 `0x2803`,只读属性
* - 2
- 特征数据值 (Characteristic Value)
- 实际的用户数据
- UUID 标识特征数据的类型
* - 3
- 特征数据描述符 (Characteristic Descriptor)
- 特征数据的其他描述信息
- 可选属性
特征数据声明和特征数据值之间的关系
##################################################################
下面以心率测量值 (Heart Rate Measurement) 为例,说明特征数据声明和特征数据值之间的关系。
下表为一属性表,含心率测量值数据特征的两个属性。首先来看句柄为 0 的属性,其 UUID 为 `0x2803`,访问权限为只读,说明这是一个特征数据声明属性。属性值中,读写属性为只读,句柄指向 1 ,说明句柄为 1 的属性为该特征数据的值属性; UUID 为 `0x2A37`,说明这个特征数据类型为心率测量值。
接下来看句柄为 1 的属性,其 `UUID``0x2A37`,访问权限为只读,与特征数据声明属性的值一一对应。该属性的值由标志位和测量值两部分组成,符合 SIG 规范对心率测量值特征数据的定义。
+-------------+--------------+-----------------+-------------------------+----------------------------+
| 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 | |
+-------------+--------------+-----------------+-------------------------+----------------------------+
特征数据描述符
##################################################################
特征数据描述符起到对特征数据进行补充说明的作用。最常见的特征数据描述符是客户端特征数据配置描述符 (Client Characteristic Configuration Descriptor, CCCD),下由 CCCD 代指。当特征数据支持由服务器端发起的 :ref:`数据操作 <gatt_data_operation>` (通知或指示)时,必须使用 CCCD 描述相关信息;这是一个可读写属性,用于 GATT 客户端告知服务器是否需要启用通知或指示,写值操作也被称为订阅 (Subscribe) 或取消订阅。
CCCD 的 UUID 是 `0x2902`,属性值中仅含 2 比特信息。第一个比特用于表示通知是否启用,第二个比特用于表示指示是否启用。我们将 CCCD 也添加到属性表中,并为心率测量值特征数据添加指示 (Indicate) 访问权限,就可以得到完整的心率测量值特征数据在属性表中的形态,如下
+-------------+--------------+-----------------+-----------------------------+----------------------------+
| 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 | |
+-------------+--------------+-----------------+-----------------------------+----------------------------+
服务
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
服务的数据结构大致可以分为两部分
.. list-table::
:align: center
:widths: 20 80
:header-rows: 1
* - 序号
- 名称
* - 1
- 服务声明属性 (Service Declaration Attribute)
* - 2
- 特征数据定义属性 (Characteristic Definition Attributes)
:ref:`特征数据 <characteristic_attributes>` 中提到的三种特征数据属性都属于特征数据定义属性。也就是说,服务的数据结构在本质上就是一些特征数据属性加上一个服务声明属性。
服务声明属性的 UUID 为 `0x2800`,访问权限为只读,值为标识服务类型的 UUID ,例如 Heart Rate Service 的 UUID 为 `0x180D`,那么其服务声明属性就可以表示为
.. 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_table:
下面以 NimBLE_GATT_Server 为例,展示一个 GATT 服务器可能的属性表形态。例程中含有两个服务,分别是 Heart Rate Service 和 Automation IO Service ;前者含有一个 Heart Rate Measurement 特征数据,后者含有一个 LED 特征数据。整个 GATT 服务器有属性表如下
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
| 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-1523-785FEABCD123` | Write-only | LED status | Characteristic Value |
+-------------+------------------------------------------+-----------------+-------------------------------------------------+----------------------------+
GATT 客户端在与 GATT 服务器初次建立通信时,会从 GATT 服务器拉取属性表中的元信息,从而获取 GATT 服务器上可用的服务以及数据特征。这一过程被称为 *服务发现 (Service Discovery)*
GATT 数据操作
-----------------------------------------
.. _gatt_data_operation:
数据操作指的是对 GATT 服务器上的特征数据进行访问的操作,主要可以分为
1. 由客户端发起的操作
2. 由服务器发起的操作
两类。
由客户端发起的操作
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
由客户端发起的操作有以下三种
1. 读 (Read)
2. 写 (Write)
3. 写(无需响应) (Write without response)
读操作比较简单,单纯是从 GATT 服务器上拉取某一特征数据的当前值。
写操作分两种。普通的写操作要求 GATT 服务器在收到客户端的写请求以及对应数据以后,进行确认响应;快速写操作则不需要服务器进行确认响应。
由服务器发起的操作
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
由服务器发起的操作分两种
1. 通知 (Notify)
2. 指示 (Indicate)
通知和指示都是 GATT 服务器主动向客户端推送数据的操作,区别在于通知无需客户端回复确认响应,而指示需要。所以,指示的数据推送速度比通知慢。
虽然通知和指示都是由服务器发起的操作,但是服务器发起操作的前提是,客户端启用了通知或指示。所以,本质上 GATT 的数据交换过程总是以客户端请求数据开始。
例程实践
-------------------------------------------
在掌握了 GATT 数据交换的相关知识以后,接下来让我们结合 NimBLE_GATT_Server 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 GATT 服务器,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`
动手试试
^^^^^^^^^^^^^^^^^^
请参考 :ref:`动手试试 <nimble_gatt_server_practice>`
代码详解
---------------------------------------------------
工程结构综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
NimBLE_GATT_Server 的根目录结构与 :ref:`NimBLE_Connection <nimble_connection_project_structure>` 完全一致。另外,在 `main` 文件夹中引入了与 GATT 服务以及模拟心率生成相关的源代码。
程序行为综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
本例程的程序行为与 :ref:`NimBLE_Connection <nimble_connection_project_structure>` 的程序行为基本一致,区别在于本例程添加了 GATT 服务,通过对应的回调函数对 GATT 数据特征的访问进行处理。
入口函数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_gatt_server_entry_point:
:ref:`NimBLE_Connection <nimble_connection_entry_point>` 的基础上,新增了调用 `gatt_svc_init` 函数对 GATT 服务进行初始化的过程。另外,除了 NimBLE 线程以外,本例新增了 `heart_rate_task` 线程,负责心率测量模拟数据的随机生成以及指示处理,相关代码如下
.. 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;
}
`heart_rate_task` 线程以 1 Hz 的频率运行,因为 `HEART_RATE_TASK_PERIOD` 被定义为 1000 ms 。每次执行时,线程都会调用 `update_heart_rate` 函数随机生成一个新的心率测量模拟数据,并调用 `send_heart_rate_indication` 处理指示操作。
GATT 服务初始化
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`gatt_svc.c` 文件中,有 GATT 服务初始化函数如下
.. 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;
}
该函数先调用 `ble_svc_gatt_init` API ,对 GATT Service 进行初始化。需要注意,这里的 GATT Service 是一个特殊的 GATT 服务,服务的 UUID 为 `0x1801` ,用于 GATT 服务器在服务发生变更时(添加或删除 GATT 服务)通知客户端,此时客户端会重新执行服务发现流程,以更新服务信息。
接下来,通过调用 `ble_gatts_count_cfg``ble_gatts_add_svcs` API ,将 `gatt_svr_svcs` 服务表中的服务以及特征数据添加到 GATT 服务器。
GATT 服务表
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`gatt_svr_svcs` 服务表是本例程中非常关键的数据结构,定义了本例程的所有服务与特征数据,相关代码如下
.. 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. */
},
};
`BLE_UUID16_INIT``BLE_UUID128_INIT` 是 NimBLE 协议栈提供的宏,可以便捷地将 16 或 128 位 UUID 由原始数据转换为 `ble_uuid16_t``ble_uuid128_t` 类型变量。
`gatt_svr_svcs` 是一个 `ble_gatt_svc_def` 类型的结构体数组。 `ble_gatt_svc_def` 即定义服务的结构体,关键字段为 `type``uuid` 以及 `characteristics``type` 字段用于标识当前服务的主次类型,本例中均为主服务。 `uuid` 字段即服务的 UUID 。 `characteristics` 字段是 `ble_gatt_chr_def` 类型的结构体数组,用于存放对应服务下的特征数据。
`ble_gatt_chr_def` 即定义特征数据的结构体,关键字段为 `uuid``access_cb``flags` 以及 `val_handle``uuid` 字段即特征数据的 UUID 。 `access_cb` 字段用于指向该特征数据的访问回调函数。 `flags` 字段用于标识特征数据的访问权限。 `val_handle` 字段用于指向该特征数据值的变量句柄地址。
需要说明的是,当为特征数据设定了 `BLE_GATT_CHR_F_INDICATE` 标志位时, NimBLE 协议栈会自动为该特征数据添加 CCCD ,所以我们无需手动添加描述符。
结合变量命名,不难发现, `gatt_svr_svcs` 实现了 :ref:`属性表 <attribute_table>` 中的所有属性定义。另外,对于 Heart Rate Measurement 特征数据,其访问通过 `heart_rate_chr_access` 回调函数管理;对于 LED 特征数据,其访问通过 `led_chr_access` 回调函数管理。
特征数据访问管理
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LED 访问管理
#################################################
LED 特征数据的访问通过 `led_chr_access` 回调函数管理,相关代码如下
.. 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;
}
当 GATT 客户端发起对 LED 特征数据的访问时, NimBLE 协议栈将会调用 `led_chr_access` 回调函数,并将句柄信息与访问上下文等信息传入。 `ble_gatt_access_ctxt``op` 字段用于标识不同的访问事件。由于 LED 是一个只写的特征数据,因此我们仅对 `BLE_GATT_ACCESS_OP_WRITE_CHR` 事件进行处理。
在这个处理分支中,我们先对属性句柄进行验证,确认客户端访问的是 LED 特征数据;随后根据 `ble_gatt_access_ctxt``om` 字段,验证访问数据的长度;最后根据 `om_data` 中的数据是否为 1 ,对 LED 进行点亮或熄灭操作。
若出现了其他访问事件,则认为是意料外的访问,直接走 `error` 分支返回。
心率测量值读访问管理
#################################################
心率测量值是可读且可指示的特征数据,其中客户端对心率测量值发起的读访问,由 `heart_rate_chr_access` 回调函数管理,相关代码如下
.. 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;
}
和 LED 的访问管理类似的,我们通过 `ble_gatt_access_ctxt` 访问上下文的 `op` 字段判断访问事件,对 `BLE_GATT_ACCESS_OP_READ_CHR` 事件进行处理。
在处理分支中,我们同样先对属性句柄进行验证,确认客户端访问的是心率测量值属性;然后,调用 `get_heart_rate` 接口获取最新的心率测量值,并存到 `heart_rate_chr_val` 数组的测量值区域中;最后,将 `heart_rate_chr_val` 的数据复制到 `ble_gatt_access_ctxt` 访问上下文的 `om` 字段中, NimBLE 协议栈会在当前回调函数结束后,将该字段中的数据发送至客户端,从而实现了对 Heart Rate Measurement 特征数据值的读访问。
心率测量值指示
###############################################
当客户端启用心率测量值的指示时,处理流程相对麻烦一些。首先,客户端启用或禁用心率测量值的指示是 GAP 层的订阅或取消订阅事件,所以我们必须在 `gap_event_handler` 回调函数中增加对订阅事件的处理分支,如下
.. 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;
}
订阅事件为 `BLE_GAP_EVENT_SUBSCRIBE` 。在这个处理分支中,我们不直接对订阅事件进行处理,而是调用 `gatt_svr_subscribe_cb` 回调函数处理订阅事件。这里体现了软件分层设计的思想,因为订阅事件影响的是 GATT 服务器对特征数据的发送行为,与 GAP 层无关,因此应直接将这个事件传递至 GATT 层进行处理。
下面,我们看一下 `gatt_svr_subscribe_cb` 回调函数中都进行哪些操作
.. 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;
}
}
本例中的回调处理非常简单:判断订阅事件中的属性句柄是否为心率测量值的属性句柄,若是,则保存对应的连接句柄,并更新客户端要求的指示状态。
:ref:`入口函数 <nimble_gatt_server_entry_point>` 中提到, `send_heart_rate_indication` 函数以 1 Hz 的频率被 `heart_rate_task` 线程调用。这个函数的实现如下
.. 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!");
}
}
`ble_gatts_indicate` 是 NimBLE 协议栈提供的指示发送 API 。也就是说,当心率测量值的指示状态为真,且对应连接句柄可用的情况下,调用 `send_heart_rate_indication` 函数就会发送一次心率测量值至 GATT 客户端。
简单总结一下,当 GATT 客户端订阅心率测量值时, `gap_event_handler` 将会接收到订阅事件,并将订阅事件传递至 `gatt_svr_subscribe_cb` 回调函数,随后更新心率测量值的订阅状态。在 `heart_rate_task` 线程中,每秒都会检查一次心率测量值的订阅状态,若订阅状态为真,则将心率测量值发送至客户端。
总结
----------------------------
通过本教程,你了解了如何通过服务表创建 GATT 服务以及相应的特征数据,并掌握了 GATT 特征数据的访问管理方式,包括读、写和订阅操作的实现。你可以在 NimBLE_GATT_Server 例程的基础上,开发更加复杂的 GATT 服务应用。

View File

@ -0,0 +1,884 @@
设备发现
==========================
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门教程其二,旨在对 Bluetooth LE 设备发现过程进行简要介绍,包括广播与扫描相关的基本概念。随后,本教程会结合 NimBLE_Beacon 例程,基于 NimBLE 主机层协议栈,对 Bluetooth LE 广播的代码实现进行介绍。
学习目标
------------------
- 学习广播的基本概念
- 学习扫描的基本概念
- 学习 NimBLE_Beacon 例程的代码结构
广播 (Advertising) 与扫描 (Scanning) 是 Bluetooth LE 设备在进入连接前在设备发现 (Device Discovery) 阶段的工作状态。下面,我们先了解与广播有关的基本概念。
广播的基本概念
----------------------------------
广播是设备通过蓝牙天线,向外发送广播数据包的过程。由于广播者在广播时并不知道环境中是否存在接收方,也不知道接收方会在什么时候启动天线,所以需要周期性地发送广播数据包,直到有设备响应。在上述过程中,对于广播者来说存在以下几个问题,让我们一起来思考一下
1. 向哪里发送广播数据包? (Where?)
2. 发送广播数据包的周期取多久? (When?)
3. 广播数据包里包含哪些信息? (What?)
向哪里发送广播数据包?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
蓝牙的无线电频段
################################
第一个问题指向的是,广播数据包应发送到哪一无线电频段。这个回答由蓝牙核心规范给出,答案是 2.4 GHz ISM 频段。选择该频段的理由是, 2.4 GHz ISM 频段是一个全球可用的免费无线电频段,不被任何国家以军事用途等理由管控,也无需向任何组织支付许可费用,因此该频段的可用性极高,且没有任何使用成本。不过,这也意味着 2.4 GHz ISM 频段非常拥挤,可能会与其他无线通信协议发生数据冲突,如 2.4 GHz WiFi。
蓝牙信道
#######################################
与经典蓝牙相同,蓝牙技术联盟为了解决数据冲突的问题,在 Bluetooth LE 上也应用了自适应跳频技术 (Adaptive Frequency Hopping, AFH) ,该技术可以判断 RF 信道的拥挤程度,通过跳频避开拥挤的 RF 信道,以提高通信质量。不过 Bluetooth LE 与经典蓝牙的不同之处在于,所使用的 2.4 GHz ISM 频段被划分为 40 个 2 MHz 带宽的射频 (Radio Frequency, RF) 信道,中心频率范围为 2402 MHz - 2480 MHz ,而经典蓝牙则是将这一频段划分为 79 个 1MHz 带宽的 RF 信道。
在 Bluetooth LE 4.2 标准中, RF 信道分为两种类型,如下
.. list-table::
:align: center
:widths: 30 20 20 30
:header-rows: 1
* - 类型
- 数量
- 编号
- 作用
* - 广播信道 (Advertising Channel)
- 3
- 37-39
- 用于发送广播数据包和扫描响应数据包
* - 数据信道 (Data Channel)
- 37
- 0-36
- 用于发送数据通道数据包
广播者在广播时,会在 37-39 这三个广播信道中进行广播数据包的发送。在三个广播信道的广播数据包均发送完毕后,可以认为一次广播结束,广播者会在下一次广播时刻到来时重复上述过程。
扩展广播特性
################################
Bluetooth LE 4.2 标准中,广播数据包允许搭载最多 31 字节广播数据,这无疑限制了广播的功能。为了提高广播的可用性,蓝牙 5.0 标准引入了 扩展广播 (Extended Advertising) 特性,这一特性将广播数据包分为
.. list-table::
:align: center
:widths: 40 20 20 20
:header-rows: 1
* - 类型
- 简称
- 单包最大广播数据字节数
- 最大广播数据字节数
* - 主广播数据包 (Primary Advertising Packet)
- Legacy ADV
- 31
- 31
* - 扩展广播数据包 (Extended Advertising Packet)
- Extended ADV
- 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 ,从而得到完整的扩展广播数据包。
.. list-table::
:align: center
:widths: 30 40 30
:header-rows: 1
* - 类型
- 信道
- 作用
* - 主广播信道 (Primary Advertising Channel)
- 37-39
- 用于传输扩展广播数据包的 ADV_EXT_IND
* - 次广播信道 (Secondary Advertising Channel)
- 0-36
- 用于传输扩展广播数据包的 AUX_ADV_IND
发送广播数据包的周期取多久?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
广播间隔
##################
对于第二个问题,即发送广播数据包的周期怎么取,蓝牙标准中也给出了一个明确的参数定义,即广播间隔 (Advertising Interval)。广播间隔可取的范围为 20 ms 到 10.24 s ,取值步长为 0.625 ms。
广播间隔的取值决定了广播者的可发现性 (Discoverability)以及设备功耗。当广播间隔取得太长时,广播数据包被接收方接收到的概率就会变得很低,此时广播者的可发现性就会变差。同时,广播间隔也不宜取得太短,因此频繁发送广播数据需要消耗更多的电量。所以,广播者需要在可发现性和能耗之间进行取舍,根据应用场景的需求选择最合适的广播间隔。
值得一提的是,如果在同一空间中存在两个广播间隔相同的广播者,那么有概率出现重复性的撞包 (Packet Collision) 现象,即两个广播者总是在同一时刻向同一信道发送广播数据。由于广播是一个只发不收的过程,广播者无法得知是否发生了广播撞包。为了降低上述问题的发生概率,广播者应在每一次广播事件后添加 0-10 ms 的随机时延。
广播数据包里包含哪些信息?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
广播数据包结构
##########################
对于第三个问题,即广播数据包内含有什么信息,在 Bluetooth LE 4.2 标准给出了广播数据包的格式定义,如下图所示
.. _adv_packet_structure:
.. figure:: ../../../../_static/ble/ble-4.2-adv-packet-structure.png
:align: center
:scale: 35%
:alt: 广播数据包结构
Bluetooth LE 4.2 广播数据包结构
看起来非常复杂,让我们来逐层分解。广播数据包的最外层包含四个部分,分别是
.. list-table::
:align: center
:widths: 10 40 10 40
:header-rows: 1
* - 序号
- 名称
- 字节数
- 功能
* - 1
- 预置码 (Preamble)
- 1
- 特殊的比特序列,用于设备时钟同步
* - 2
- 访问地址 (Access Address)
- 4
- 标记广播数据包的地址
* - 3
- 协议数据单元 (Protocol Data Unit, PDU)
- 2-39
- 有效数据的存放区域
* - 4
- 循环冗余校验和 (Cyclic Redundancy Check, CRC)
- 3
- 用于循环冗余校验
广播数据包是蓝牙数据包的一种类型,由 PDU 类型决定。下面我们将对 PDU 展开详细的介绍
PDU
##########################
PDU 段为有效数据存放的区域,其结构如下
.. list-table::
:align: center
:widths: 10 50 40
:header-rows: 1
* - 序号
- 名称
- 字节数
* - 1
- 头 (Header)
- 2
* - 2
- 有效负载 (Payload)
- 0-37
PDU 头
##########################
PDU 头中含有较多信息,可以分为以下六个部分
.. list-table::
:align: center
:widths: 10 40 10 40
:header-rows: 1
* - 序号
- 名称
- 位数
- 备注
* - 1
- PDU 类型 (PDU Type)
- 4
-
* - 2
- 保留位 (Reserved for Future Use, **RFU**)
- 1
-
* - 3
- 通道选择位 (Channel Selection Bit, **ChSel**)
- 1
- 标记广播者是否支持 *LE Channel Selection Algorithm #2* 通道选择算法
* - 4
- 发送地址类型 (Tx Address, **TxAdd**)
- 1
- 0/1 分别表示公共地址/随机地址
* - 5
- 接收地址类型 (Rx Address, **RxAdd**)
- 1
- 同上
* - 6
- 有效负载长度 (Payload Length)
- 8
-
PDU 类型位反映了设备的广播行为。在蓝牙标准中,共有以下三对广播行为
- *可连接 (Connectable)**不可连接 (Non-connectable)*
- 是否接受其他设备的连接请求
- *可扫描 (Scannable)**不可扫描 (Non-scannable)*
- 是否接受其他设备的扫描请求
- *不定向 (Undirected)**定向 (Directed)*
- 是否发送广播数据至指定设备
上述广播行为可以组合成以下四种常见的广播类型,对应四种不同的 PDU 类型
.. list-table::
:align: center
:widths: 10 10 10 30 40
:header-rows: 1
* - 可连接?
- 可扫描?
- 不定向?
- PDU 类型
- 作用
* - 是
- 是
- 是
- `ADV_IND`
- 最常见的广播类型
* - 是
- 否
- 否
- `ADV_DIRECT_IND`
- 常用于已知设备重连
* - 否
- 否
- 是
- `ADV_NONCONN_IND`
- 作为信标设备,仅向外发送广播数据
* - 否
- 是
- 是
- `ADV_SCAN_IND`
- 作为信标设备,一般用于广播数据包长度不足的情况,此时可以通过扫描响应向外发送额外的数据
PDU 有效负载
##########################
PDU 有效负载也分为两部分
.. list-table::
:align: center
:widths: 10 50 10 30
:header-rows: 1
* - 序号
- 名称
- 字节数
- 备注
* - 1
- 广播地址 (Advertisement Address, **AdvA**)
- 6
- 广播设备的 48 位蓝牙地址
* - 2
- 广播数据 (Advertisement Data, **AdvData**)
- 0-31
- 由若干广播数据结构 (Advertisement Data Structure) 组成
先看广播地址,即蓝牙地址,可以分为
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - 类型
- 说明
* - 公共地址 (Public Address)
- 全球范围内独一无二的固定设备地址,厂商必须为此到 IEEE 组织注册并缴纳一定费用
* - 随机地址 (Random Address)
- 随机生成的地址
随机地址又根据用途分为两类
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - 类型
- 说明
* - 随机静态地址 (Random Static Address)
- 可以随固件固化于设备,也可以在设备启动时随机生成,但在设备运行过程中不得变更;常作为公共地址的平替
* - 随机私有地址 (Random Private Address)
- 可在设备运行过程中周期性变更,避免被其他设备追踪
若使用随机私有地址的设备要与其他受信任的设备通信,则应使用身份解析秘钥 (Identity Resolving Key, IRK) 生成随机地址,此时其他持有相同 IRK 的设备可以解析并得到设备的真实地址。此时,随机私有地址又可以分为两类
.. list-table::
:align: center
:widths: 40 60
:header-rows: 1
* - 类型
- 说明
* - 可解析随机私有地址 (Resolvable Random Private Address)
- 可通过 IRK 解析得到设备真实地址
* - 不可解析随机私有地址 (Non-resolvable Random Private Address)
- 完全随机的地址,仅用于防止设备被追踪,非常少用
然后看广播数据。一个广播数据结构的格式定义如下
.. list-table::
:align: center
:widths: 10 40 20 30
:header-rows: 1
* - 序号
- 名称
- 字节数
- 备注
* - 1
- 数据长度 (AD Length)
- 1
-
* - 2
- 数据类型 (AD Type)
- n
- 大部分数据类型占用 1 字节
* - 3
- 数据 (AD Data)
- (AD Length - n)
-
扫描的基本概念
^^^^^^^^^^^^^^^^^^^^^^^^^^
在广播章节,我们通过回答与广播过程相关的三个问题,了解了广播的相关基本概念。事实上,扫描过程中也存在类似的三个问题,让我们一起思考一下
1. 到什么地方去扫描? (Where?)
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 ,以获取完整的扩展广播数据包。
扫描窗口与扫描间隔
################################
第二个问题分别指向扫描窗口 (Scan Window) 和 扫描间隔 (Scan Interval) 概念。
首先对扫描窗口进行说明。扫描窗口指的是扫描者在同一个 RF 信道持续接收蓝牙数据包的持续时间,例如扫描窗口参数设定为 50 ms 时,扫描者在每个 RF 信道都会不间断地扫描 50 ms。
扫描间隔则指的是相邻两个扫描窗口开始时刻之间的时间间隔,所以扫描间隔必然大于等于扫描窗口。
下图在时间轴上展示了扫描者的广播数据包接收过程,其中扫描者的扫描间隔为 100 ms ,扫描窗口为 50 ms ;广播者的广播间隔为 50 ms ,广播数据包的发送时长仅起到示意作用。可以看到,第一个扫描窗口对应 37 信道,此时扫描者恰好接收到了广播者第一次在 37 信道发送的广播数据包,以此类推。
.. figure:: ../../../../_static/ble/ble-advertise-and-scan-sequence.png
:align: center
:scale: 30%
:alt: 广播与扫描时序示意
广播与扫描时序示意图
.. _scan_request_and_scan_response:
扫描请求与扫描响应
########################################
从目前的介绍来看,似乎广播过程中广播者只发不收,扫描过程中扫描者只收不发。事实上,扫描行为分为以下两种
- 被动扫描 (Passive Scanning)
- 扫描者只接收广播数据包
- 主动扫描 (Active Scanning)
- 扫描者在接收广播数据包以后,还向可扫描广播者发送扫描请求 (Scan Request)
可扫描广播者在接收到扫描请求之后,会广播扫描响应 (Scan Response) 数据包,以向感兴趣的扫描者发送更多的广播信息。扫描响应数据包的结构与广播数据包完全一致,区别在于 PDU 头中的 PDU 类型不同。
在广播者处于可扫描广播模式、扫描者处于主动扫描模式的场景下,广播者和扫描者的数据发送时序变得更加复杂。对于扫描者来说,在扫描窗口结束后会短暂进入 TX 模式,向外发送扫描请求,随后马上进入 RX 模式以接收可能的扫描响应;对于广播者来说,每一次广播结束后都会短暂进入 RX 模式以接收可能的扫描请求,并在接收到扫描请求后进入 TX 模式,发送扫描响应。
.. figure:: ../../../../_static/ble/ble-advertiser-rx-scan-request.png
:align: center
:scale: 30%
:alt: 扫描请求的接收与扫描响应的发送
扫描请求的接收与扫描响应的发送
例程实践
-------------------------------------------
在掌握了广播与扫描的相关知识以后,接下来让我们结合 NimBLE_Beacon 例程代码,学习如何使用 NimBLE 协议栈构建一个简单的 Beacon 设备,对学到的知识进行实践。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`
动手试试
^^^^^^^^^^^^^^^^^^
构建与烧录
#################
本教程对应的参考例程为 :example:`NimBLE_Beacon <bluetooth/ble_get_started/nimble/NimBLE_Beacon>`
你可以通过以下命令进入例程目录
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_Beacon
注意,请将 `<ESP-IDF Path>` 替换为你本地的 ESP-IDF 文件夹路径。随后,你可以通过 VSCode 或其他你常用的 IDE 打开 NimBLE_Beacon 工程。以 VSCode 为例,你可以在使用命令行进入例程目录后,通过以下命令打开工程
.. code-block:: shell
$ code .
随后,在命令行中进入 ESP-IDF 环境,完成芯片设定
.. code-block:: shell
$ idf.py set-target <chip-name>
你应该能看到命令行以
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
等提示结束,这说明芯片设定完成。接下来,连接开发板至电脑,随后运行以下命令,构建固件并烧录至开发板,同时监听 {IDF_TARGET_NAME} 开发板的串口输出
.. code-block:: shell
$ idf.py flash monitor
你应该能看到命令行以
.. code-block:: shell
...
main_task: Returned from app_main()
等提示结束。
查看 Beacon 设备信息
#######################################
.. _nimble_beacon_details:
打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_Beacon 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-scan-list-nimble-beacon.jpg
:align: center
:scale: 30%
:alt: NimBLE Beacon
找到 NimBLE Beacon 设备
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_Beacon 设备。
观察到 NimBLE Beacon 设备下带有丰富的设备信息,甚至还带有乐鑫的网址(这就是信标广告功能的体现)。点击右下角的 `RAW` 按钮,可以看到广播数据包的原始信息,如下
.. figure:: ../../../../_static/ble/ble-adv-packet-raw-data.jpg
:align: center
:scale: 30%
:alt: ADV Packet Raw Data
广播数据包原始信息
Details 表格即广播数据包和扫描响应数据包中的所有广播数据结构,可以整理如下
.. list-table::
:align: center
:widths: 30 10 10 30 20
:header-rows: 1
* - 名称
- 长度
- 类型
- 原始数据
- 解析值
* - 标志位
- 2
- `0x01`
- `0x06`
- General Discoverable, BR/EDR Not Supported
* - 完整设备名称
- 14
- `0x09`
- `0x4E696D424C455F426561636F6E`
- NimBLE_Beacon
* - 发送功率等级
- 2
- `0x0A`
- `0x09`
- 9 dBm
* - 设备外观
- 3
- `0x19`
- `0x0002`
- 通用标签
* - LE 角色
- 2
- `0x1C`
- `0x00`
- 仅支持外设设备
* - 设备地址
- 8
- `0x1B`
- `0x46F506BDF5F000`
- `F0:F5:BD:06:F5:46`
* - URI
- 17
- `0x24`
- `0x172F2F6573707265737369662E636F6D`
- `https://espressif.com`
值得一提的是,前五项广播数据结构长度之和为 28 字节,此时广播数据包仅空余 3 字节,无法继续装载后续的两项广播数据结构。所以后两项广播数据结构必须装填至扫描响应数据包。
你可能还注意到,对应于设备外观的 Raw Data 为 `0x0002`,而代码中对 Generic Tag 的定义是 `0x0200`;还有,设备地址的 Raw Data 除了最后一个字节 `0x00` 以外,似乎与实际地址完全颠倒。这是因为, Bluetooth LE 的空中数据包遵循小端 (Little Endian) 传输的顺序,所以低字节的数据反而会在靠前的位置。
另外,注意到 nRF Connect for Mobile 程序并没有为我们提供 `CONNECT` 按钮以连接至此设备。这符合我们的预期,因为 Beacon 设备本来就应该是不可连接的。下面,让我们深入代码细节,看看这样的一个 Beacon 设备是怎样实现的。
代码详解
----------------------------------------------
工程结构综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_project_structure:
NimBLE_Beacon 的根目录大致分为以下几部分
- `README*.md`
- 工程的说明文档
- `sdkconfig.defaults*`
- 不同芯片对应开发板的默认配置
- `CMakeLists.txt`
- 用于引入 ESP-IDF 构建环境
- `main`
- 工程主文件夹,含本工程的源码、头文件以及构建配置
程序行为综述
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_program_behavior:
在深入代码细节前,我们先对程序的行为有一个宏观的认识。
第一步,我们会对程序中使用到的各个模块进行初始化,主要包括 NVS Flash、NimBLE 主机层协议栈以及 GAP 服务的初始化。
第二步,在 NimBLE 主机层协议栈与蓝牙控制器完成同步时,我们先确认蓝牙地址可用,然后发起不定向、不可连接、可扫描的广播。
之后持续处于广播状态,直到设备重启。
入口函数
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_entry_point:
与其他工程一样,应用程序的入口函数为 `main/main.c` 文件中的 `app_main` 函数,我们一般在这个函数中进行各模块的初始化。本例中,我们主要做以下几件事情
1. 初始化 NVS Flash 与 NimBLE 主机层协议栈
2. 初始化 GAP 服务
3. 启动 NimBLE 主机层的 FreeRTOS 线程
ESP32 的蓝牙协议栈使用 NVS Flash 存储相关配置,所以在初始化蓝牙协议栈之前,必须调用 `nvs_flash_init` API 以初始化 NVS Flash ,某些情况下需要调用 `nvs_flash_erase` API 对 NVS Flash 进行擦除后再初始化。
.. 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;
}
...
}
随后,可以调用 `nimble_port_init` API 以初始化 NimBLE 主机层协议栈。
.. 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;
}
...
}
然后,我们调用 `gap.c` 文件中定义的 `gap_init` 函数,初始化 GAP 服务,并设定设备名称与外观。
.. 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;
}
...
}
接下来,设定 NimBLE 主机层协议栈的配置,这里主要涉及到一些回调函数的设定,包括协议栈重置时刻的回调、完成同步时刻的回调等,然后保存配置。
.. 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();
...
}
最后,启动 NimBLE 主机层的 FreeRTOS 线程。
.. 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);
...
}
开始广播
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _nimble_beacon_start_advertising:
使用 NimBLE 主机层协议栈进行应用开发时的编程模型为事件驱动编程 (Event-driven Programming)。
例如,在 NimBLE 主机层协议栈与蓝牙控制器完成同步以后,将会触发同步完成事件,调用 `ble_hs_cfg.sync_cb` 函数。在回调函数设定时,我们令该函数指针指向 `on_stack_sync` 函数,所以这是同步完成时实际被调用的函数。
`on_stack_sync` 函数中,我们调用 `adv_init` 函数,进行广播操作的初始化。在 `adv_init` 中,我们先调用 `ble_hs_util_ensure_addr` API ,确认设备存在可用的蓝牙地址;随后,调用 `ble_hs_id_infer_auto` API ,获取最优的蓝牙地址类型。
.. 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;
}
...
}
接下来,将蓝牙地址数据从 NimBLE 协议栈的内存空间拷贝到本地的 `addr_val` 数组中,等待后续调用。
.. 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);
...
}
最后,调用 `start_advertising` 函数发起广播。在 `start_advertising` 函数中,我们先将广播标志位、完整设备名、发射功率、设备外观和 LE 角色等广播数据结构填充到广播数据包中,如下
.. 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;
}
...
}
`ble_hs_adv_fields` 结构体预定义了一些常用的广播数据类型。我们可以在完成数据设置后,通过令对应的 `is_present` 字段为 1 ,或将对应的长度字段 `len` 设定为非零值,以启用对应的广播数据结构。例如在上述代码中,我们通过 `adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;` 来配置设备发送功率,然后通过 `adv_fields.tx_pwr_lvl_is_present = 1;` 以启用该广播数据结构;若仅配置设备发送功率而不对相应的 `is_present` 字段置位,则该广播数据结构无效。同理,我们通过 `adv_fields.name = (uint8_t *)name;` 配置设备名,然后通过 `adv_fields.name_len = strlen(name);` 配置设备名的长度,从而将设备名这一广播数据结构添加到广播数据包中;若仅配置设备名而不配置设备名的长度,则该广播数据结构无效。
最后,调用 `ble_gap_adv_set_fields` API ,完成广播数据包的广播数据结构设定。
同理,我们可以将设备地址与 URI 填充到扫描响应数据包中,如下
.. 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;
}
...
}
最后,设置广播参数,并通过调用 `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!");
}
总结
---------
通过本教程,你了解了广播和扫描的基本概念,并通过 NimBLE_Beacon 例程掌握了使用 NimBLE 主机层协议栈构建 Bluetooth LE Beacon 设备的方法。
你可以尝试对例程中的数据进行修改,并在 nRF Connect for Mobile 调试工具中查看修改结果。例如,你可以尝试修改 `adv_fields``rsp_fields` 结构体,以修改被填充的广播数据结构,或者交换广播数据包和扫描响应数据包中的广播数据结构。但需要注意的一点是,广播数据包和扫描响应数据包的广播数据上限为 31 字节,若设定的广播数据结构大小超过该限值,调用 `ble_gap_adv_start` API 将会失败。

View File

@ -0,0 +1,342 @@
介绍
===================
:link_to_translation:`en:[English]`
本文档为低功耗蓝牙 (Bluetooth Low Energy, Bluetooth LE) 入门系列教程其一,旨在对 Bluetooth LE 的基本概念进行简要介绍,并引导读者烧录一个完整的 Bluetooth LE 例程至 {IDF_TARGET_NAME} 开发板;随后,指导读者在手机上使用 nRF Connect for Mobile 应用程序,控制开发板上 LED 的开关并读取开发板上随机生成的心率数据。本教程希望帮助读者了解如何使用 ESP-IDF 开发框架对 {IDF_TARGET_NAME} 开发板进行 Bluetooth LE 应用烧录,并通过体验例程功能,对 Bluetooth LE 的功能建立感性认知。
学习目标
---------------
- 认识 Bluetooth LE 的分层架构
- 了解 Bluetooth LE 各层基本功能
- 了解 GAP 以及 GATT/ATT 层的功能
- 掌握在 {IDF_TARGET_NAME} 开发板上烧录 Bluetooth LE 例程的方法,并在手机上与之交互
引言
-----------------
大多数人在生活中都接触过蓝牙,可能屏幕前的你现在正佩戴着蓝牙耳机,收听来自手机或电脑的音频。不过,音频传输是经典蓝牙 (Bluetooth Classic) 的典型应用场景,而 Bluetooth LE 是一种与经典蓝牙不兼容的蓝牙通信协议,在蓝牙 4.0 中被引入。顾名思义, Bluetooth LE 是一种功耗非常低的蓝牙协议,通信速率也比经典蓝牙更低一些,其典型应用场景是物联网 (Internet of Things, IoT) 中的数据通信,例如智能开关或智能传感器,这也是本教程中引用的 Bluetooth LE 例程所实现的功能。不过,在体验例程功能以前,让我们来了解一下 Bluetooth LE 的基本概念,以帮助你更好地入门。
Bluetooth LE 的分层架构
-------------------------
Bluetooth LE 协议定义了三层软件结构,自上而下分别是
- 应用层 (Application Layer)
- 主机层 (Host Layer)
- 控制器层 (Controller Layer)
应用层即以 Bluetooth LE 为底层通信技术所构建的应用,依赖于主机层向上提供的 API 接口。
主机层负责实现 L2CAP、GATT/ATT、SMP、GAP 等底层蓝牙协议,向上对应用层提供 API 接口,向下通过主机控制器接口 (Host Controller Interface, HCI) 与控制器层通信。
控制器层包括物理层 (Physical Layer, PHY) 和链路层 (Link Layer, LL) 两层,向下直接与控制器硬件进行交互,向上通过 HCI 与主机层进行通信。
值得一提的是,蓝牙核心规范 (Core Specification) 允许主机层和控制器层在物理上分离,此时 HCI 体现为物理接口,包括 SDIO、USB 以及 UART 等;当然,主机层和控制器层可以共存于同一芯片,以实现更高的集成度,此时 HCI 体现为逻辑接口,常被称为虚拟主机控制器接口 (Virtual Host Controller Interface, VHCI)。一般认为,主机层和控制器层组成了 Bluetooth LE 协议栈 (Bluetooth LE Stack)。
下图展示了 Bluetooth LE 的分层结构。
.. figure:: ../../../../_static/ble/ble-architecture.png
:align: center
:scale: 50%
:alt: Bluetooth LE 分层结构
Bluetooth LE 分层结构
作为应用开发者,在开发过程中我们主要与主机层提供的 API 接口打交道,这要求我们对主机层中的蓝牙协议有一定的了解。接下来,我们会从连接和数据交互两个角度,对 GAP 和 GATT/ATT 层的基本概念进行介绍。
GAP 层 - 定义设备的连接
^^^^^^^^^^^^^^^^^^^^^^^
GAP 层的全称为通用访问规范 (Generic Access Profile, GAP),定义了 Bluetooth LE 设备之间的连接行为以及设备在连接中所扮演的角色。
GAP 状态与角色
###################
GAP 中共定义了三种设备的连接状态以及五种不同的设备角色,如下
- 空闲 (Idle)
- 此时设备无角色,处于就绪状态 (Standby)
- 设备发现 (Device Discovery)
- 广播者 (Advertiser)
- 扫描者 (Scanner)
- 连接发起者 (Initiator)
- 连接 (Connection)
- 外围设备 (Peripheral)
- 中央设备 (Central)
广播者向外广播的数据中包含设备地址等信息,用于向外界设备表明广播者的存在,并告知其他设备是否可以连接。扫描者则持续接收环境中的广播数据包。若某一个扫描者发现了一个可连接的广播者,并希望与之建立连接,可以将角色切换为连接发起者。当连接发起者再次收到该广播者的广播数据,会立即发起连接请求 (Connection Request);在广播者未开启白名单 (White List, 又称 Accept List) 或连接发起者在广播者的白名单之中时,连接将被成功建立。
进入连接以后,原广播者转变为外围设备(旧称从设备 Slave ),原扫描者或连接初始化者转变为中央设备(旧称主设备 Master )。
GAP 角色之间的转换关系如下图所示
.. figure:: ../../../../_static/ble/ble-gap-state-diagram.png
:align: center
:scale: 50%
:alt: GAP 角色转换关系
GAP 角色转换关系
Bluetooth LE 网络拓扑
########################
Bluetooth LE 设备可以同时与多个 Bluetooth LE 设备建立连接,扮演多个外围设备或中央设备角色,或同时作为外围设备和中央设备。以 Bluetooth LE 网关为例,这种设备可以作为中央设备,与智能开关等外围设备连接,同时作为外围设备,与形如手机等中央设备连接,实现数据中转。
在一个 Bluetooth LE 网络中,若所有设备都在至少一个连接中,且仅扮演一种类型的角色,则称这种网络为连接拓扑 (Connected Topology);若存在至少一个设备同时扮演外围设备和中央设备,则称这种网络为多角色拓扑 (Multi-role Topology)。
Bluetooth LE 同时也支持无连接的网络拓扑,即广播拓扑 (Broadcast Topology)。在这种网络中,存在两种角色,其中发送数据的被称为广播者 (Broadcaster),接收数据的被称为观察者 (Observer)。广播者只广播数据,不接受连接;观察者仅接受广播数据,不发起连接。例如,某个智能传感器的数据可能在一个网络中被多个设备共用,此时维护多个连接的成本相对较高,直接向网络中的所有设备广播传感器数据更加合适。
了解更多
####################
如果你想了解更多设备发现与连接的相关信息,请参考 :doc:`设备发现 <./ble-device-discovery>`:doc:`连接 <./ble-connection>`
GATT/ATT 层 - 数据表示与交换
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _gatt_att_introduction:
GATT/ATT 层定义了进入连接状态后,设备之间的数据交换方式,包括数据的表示与交换过程。
ATT 层
#############
ATT 的全称是属性协议 (Attribute Protocol, ATT),定义了一种称为属性 (Attribute) 的基本数据结构,以及基于服务器/客户端架构的数据访问方式。
简单来说,数据以属性的形式存储在服务器上,等待客户端的访问。以智能开关为例,开关量作为数据,以属性的形式存储在智能开关内的蓝牙芯片(服务器)中,此时用户可以通过手机(客户端)访问智能开关蓝牙芯片(服务器)上存放的开关量属性,获取当前的开关状态(读访问),或控制开关的闭合与断开(写访问)。
属性这一数据结构一般由以下三部分构成
- 句柄 (Handle)
- 类型 (Type)
- 值 (Value)
- 访问权限 (Permissions)
在协议栈实现中,属性一般被放在称为属性表 (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 较为常用。
GATT 层
#################
GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基础上,定义了以下三个概念
- 特征数据 (Characteristic)
- 服务 (Service)
- 规范 (Profile)
这三个概念之间的层次关系如下图所示
.. figure:: ../../../../_static/ble/ble-gatt-architecture.png
:align: center
:scale: 30%
:alt: GATT 中的层次关系
GATT 中的层次关系
.. _characteristic_structure:
特征数据和服务都是以属性为基本数据结构的复合数据结构。一个特征数据往往由两个以上的属性描述,包括
- 特征数据声明属性 (Characteristic Declaration Attribute)
- 特征数据值属性 (Characteristic Value Attribute)
除此以外,特征数据中还可能包含若干可选的描述符属性 (Characteristic Descriptor Attribute)。
一个服务本身也由一个属性进行描述,称为服务声明属性 (Service Declaration Attribute)。一个服务中可以存在一个或多个特征数据,它们之间体现为从属关系。另外,一个服务可以通过 Include 机制引用另一个服务,复用其特性定义,避免如设备名称、制造商信息等相同特性的重复定义。
规范是一个预定义的服务集合,实现了某规范中所定义的所有服务的设备即满足该规范。例如 Heart Rate Profile 规范由 Heart Rate Service 和 Device Information Service 两个服务组成,那么可以称实现了 Heart Rate Service 和 Device Information Service 服务的设备符合 Heart Rate Profile 规范。
广义上,我们可以称所有存储并管理特征数据的设备为 GATT 服务器,称所有访问 GATT 服务器以访问特征数据的设备为 GATT 客户端。
了解更多
#########################
如果你想了解更多数据表示与交换的信息,请参考 :doc:`数据交换 <./ble-data-exchange>`
例程实践
----------------------
在了解了 Bluetooth LE 的基础概念以后,让我们往 {IDF_TARGET_NAME} 开发板中烧录一个简单的 Bluetooth LE 例程,体验 LED 开关与心率数据读取功能,建立对 Bluetooth LE 技术的感性认识。
前提条件
^^^^^^^^^^^^^^^
1. 一块支持 Bluetooth LE 的 {IDF_TARGET_NAME} 开发板
2. ESP-IDF 开发环境
3. 在手机上安装 nRF Connect for Mobile 应用程序
若你尚未完成 ESP-IDF 开发环境的配置,请参考 :doc:`API 参考 <../../../get-started/index>`
动手试试
^^^^^^^^^^^^^^^^^^
.. _nimble_gatt_server_practice:
构建与烧录
#################
本教程对应的参考例程为 :example:`NimBLE_GATT_Server <bluetooth/ble_get_started/nimble/NimBLE_GATT_Server>`
你可以通过以下命令进入例程目录
.. code-block:: shell
$ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_GATT_Server
注意,请将 `<ESP-IDF Path>` 替换为你本地的 ESP-IDF 文件夹路径。随后,你可以通过 VSCode 或其他你常用的 IDE 打开 NimBLE_GATT_Server 工程。以 VSCode 为例,你可以在使用命令行进入例程目录后,通过以下命令打开工程
.. code-block:: shell
$ code .
随后,在命令行中进入 ESP-IDF 环境,完成芯片设定
.. code-block:: shell
$ idf.py set-target <chip-name>
你应该能看到命令行以
.. code-block:: shell
...
-- Configuring done
-- Generating done
-- Build files have been written to ...
等提示结束,这说明芯片设定完成。接下来,连接开发板至电脑,随后运行以下命令,构建固件并烧录至开发板,同时监听 {IDF_TARGET_NAME} 开发板的串口输出
.. code-block:: shell
$ idf.py flash monitor
你应该能看到命令行以
.. code-block:: shell
...
main_task: Returned from app_main()
NimBLE_GATT_Server: Heart rate updated to 70
等提示结束。并且,心率数据以 1 Hz 左右的频率在 60-80 范围内更新。
连接到开发板
#######################
现在开发板已准备就绪。接下来,打开手机上的 nRF Connect for Mobile 程序,在 SCANNER 标签页中下拉刷新,找到 NimBLE_GATT 设备,如下图所示
.. figure:: ../../../../_static/ble/ble-get-started-connect-brief.jpg
:align: center
:scale: 20%
:alt: 扫描设备
扫描设备
若设备列表较长,建议以 NimBLE 为关键字进行设备名过滤,快速找到 NimBLE_GATT 设备。
点击 NimBLE_GATT 设备条目,可以展开看到广播数据的详细信息。
.. figure:: ../../../../_static/ble/ble-get-started-connect-details.jpg
:align: center
:scale: 20%
:alt: 广播数据详情
广播数据详情
点击右侧的 CONNECT 按钮,在手机连接的同时,可以在开发板的串口输出中观察到许多与连接相关的日志信息。随后,手机上会显示 NimBLE_GATT 标签页,左上角应有 CONNECTED 状态,说明手机已成功通过 Bluetooth LE 协议连接至开发板。在 CLIENT 子页中,你应该能够看到四个 GATT 服务,如图所示
.. figure:: ../../../../_static/ble/ble-get-started-gatt-services-list.jpg
:align: center
:scale: 20%
:alt: GATT 服务列表
GATT 服务列表
前两个服务是 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)。
把灯点亮!
##################
下面体验一下本例程的功能。首先,点击 Automation IO Service 服务,可以看到该服务下有一个 LED 特征数据。
.. figure:: ../../../../_static/ble/ble-get-started-automation-io-service-details.jpg
:align: center
:scale: 20%
:alt: Automation IO Service
Automation IO Service
如图,该 LED 特征数据的 UUID 为 128 位的厂商自定义 UUID 。实际上,这是 Nordic Semiconductor 自定义的 LED 特征数据,在 nRF Connect for Mobile 上有专门的控制页面适配。点击右侧的上传按钮,可以对该特征数据进行写访问,如下图所示。
.. figure:: ../../../../_static/ble/ble-get-started-led-write.jpg
:align: center
:scale: 20%
:alt: 对 LED 特征数据进行写访问
对 LED 特征数据进行写访问
选择 ON 选项,然后发送,你应该能看到开发板上的 LED 被点亮了。选择 OFF 选项,然后发送,你应该能观察到开发板上的 LED 又熄灭了。
若你的开发板上没有电源指示灯以外的 LED ,你应该能在日志输出中观察到对应的状态指示。
接收心率数据
#######################
接下来,点击 Heart Rate Service 服务,可以看到该服务下有一个 Heart Rate Measurement 特征数据。
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-service-details.jpg
:align: center
:scale: 20%
:alt: Heart Rate Service
Heart Rate Service
Heart Rate Measurement 特征数据的 UUID 是 `0x2A37`,这是一个 Bluetooth SIG 定义的特征数据。点击右侧的下载按钮,对心率特征数据进行读访问,应该能够看到特征数据栏中的 `Value` 条目后出现了最新的心率测量数据,如图
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-read.jpg
:align: center
:scale: 20%
:alt: 对心率特征数据进行读访问
对心率特征数据进行读访问
在应用中,心率数据最好能够在测量值更新时,马上同步到 GATT 客户端。为此,我们可以点击最右侧的订阅按钮,要求心率特征数据进行指示操作,此时应该能够看到心率测量数据不断更新,如图
.. figure:: ../../../../_static/ble/ble-get-started-heart-rate-indicate.jpg
:align: center
:scale: 20%
:alt: 订阅心率特征数据
订阅心率特征数据
你可能注意到了,心率特征数据下有一个名为 *Client Characteristic Configuration* 的描述符 (Characteristic Descriptor),常简称为 CCCD ,其 UUID 为 `0x2902`。在点击订阅按钮时,这个描述符的值发生了变化,提示特征数据的指示已启用 (Indications enabled)。的确,这个描述符就是用来指示特征数据的指示或通知状态的;当我们取消订阅时,这个描述符的值将变为,特征数据的指示和通知已禁用 (Notifications and indications disabled)。
总结
---------
通过本教程,你了解了 Bluetooth LE 的分层架构、Bluetooth LE 协议栈中主机层和控制器层的基本功能以及 GAP 层与 GATT/ATT 层的作用。随后,通过 NimBLE_GATT_Server 例程,你掌握了如何使用 ESP-IDF 开发框架进行 Bluetooth LE 应用的构建与烧录,能够在手机上使用 nRF Connect for Mobile 调试程序,远程控制开发板上 LED 的点亮与熄灭,以及接收随机生成的心率数据。你已经迈出了走向 Bluetooth LE 开发者的第一步,恭喜!

View File

@ -13,6 +13,18 @@
overview
ble-feature-support-status
**********
快速入门
**********
.. toctree::
:maxdepth: 1
get-started/ble-introduction
get-started/ble-device-discovery
get-started/ble-connection
get-started/ble-data-exchange
**********
蓝牙规范
**********