上文中创建了GATT客户端YQS_C,本文深入代码,理解它的实现脉络。

阅读本文前,请先阅读“解读自定义GATT服务”一文。

(1)通用和定制

nrf connect是一个通用的主机程序,它能够扫描、连接任何从机设备,并能发现、解析任意的GATT服务。

实现这样的通用主机程序,逻辑比较复杂,需要依次发现所有的服务,再发现每个服务下的特征,再发现每个特征下的描述符。

实际中更常见的是针对某个GATT服务所做的GATT客户的,比如nus对应的nus_c,hrs对应的hrs_c。

实现这样的定制主机程序,逻辑比较简单,而且可以充分利用SDK中的ble_db_discovery库函数。

我们这里讨论的是定制的主机程序,针对yqs设计相应的yqs_c程序。

(2)UUID

先确定基础UUID,与YQS的基础UUID一致。

利用函数sd_ble_uuid_vs_add写入协议栈,并利用函数ble_db_discovery_evt_register注册到ble_db_discovery中。

(3)Discovery事件处理

Discovery事件会订阅到main.c –> db_disc_handler中。

处理函数为ble_yqs_c_on_db_disc_evt。实际运行时,仅关注一个事件:BLE_DB_DISCOVERY_COMPLETE,即发现GATT完毕事件。

在该事件中,可以读取YQS从设备的GATT服务和特征的全部信息,包括:

  • 服务的UUID
  • 该服务的句柄范围(该范围的第一个值为服务句柄)
  • 特征的数目
  • 各个特征的句柄、UUID、属性、值
  • 描述符的句柄

(4)基础事件处理

所谓基础事件包括连接、断开等。

(5)发送数据

发送数据即对指定的Char Handle执行write:sd_ble_gattc_write,其中的write_op参数将决定是执行write 还是write no response。

(6)使能CCCD

实际上,使能CCCD就是向CCCD Handle执行write操作。write的数据内容为{0x00, 0x01}。

(7)接收数据

只有使能的从机的CCCD以后,才可以接收数据。

接收数据后主机端产生BLE_GATTC_EVT_HVX事件,进一步触发on_hvx回调函数。

在该函数中可以获得收到的数据内容。

(8)声明YQS_C

与GATT服务端类似,也需要先进行声明:BLE_YQS_C_DEF(m_ble_yqs_c);

(9)初始化YQS_C

在系统初始化阶段,初始化YQS_C:yqs_c_init

初始化过程中会关联事件回调函数,以告诉系统一些关键事件节点。

(10)初始化Discovery

对于主机而言,Discovery是一个特殊操作, 在函数db_discovery_init中进行专门的初始化。

(完)

前面创建了一个YQS服务,属于GAP Server,也称为从机。本文创建一个GAP Client,即主机,来连接和发现YQS设备。

阅读本文之前,请先阅读前文

(1)工程框架

打开nus_c工程<SDK Dir>\14.2.0\examples\ble_central\ble_app_nus_c。

编译下载到开发板中,确保工程能够正常运行。

通过RTT Viewer可以看到相关打印日志。

(2)添加yqs_c

复制以下两个文件:

  • <SDK Dir>\14.2.0\components\ble\ble_services\ble_nus\ble_nus_c.c
  • <SDK Dir>\14.2.0\components\ble\ble_services\ble_nus\ble_nus_c.h

粘贴到ble_app_nus_c目录下,放在main.c的同级目录。

将该目录的相对路径(../../../)添加到工程的User Include Directory中,如下:

将文件重命名为:

  • ble_yqs_c.c
  • ble_yqs_c.h

并将两个文件拖放到nRF_BLE_Services中,如下图:

(3)移植YQS

打开ble_yqs_c.c,做全局替换,注意区分大小写:

  • NUS –> YQS
  • nus –> yqs

对ble_yqs_c.h做同样操作。

将YQS_BASE_UUID中的0x9E改成0x9D,如下:

{{0x9D, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x00, 0x00, 0x40, 0x6E}}

打开sdk_config.h,在BLE_TPS_ENABLED下方增加一个项目,如下:

// <q> BLE_TPS_ENABLED  - ble_tps - TX Power Service
#ifndef BLE_TPS_ENABLED
#define BLE_TPS_ENABLED 0
#endif

// <q> BLE_YQS_C_ENABLED  - ble_yqs_c - Youqun Client
#ifndef BLE_YQS_C_ENABLED
#define BLE_YQS_C_ENABLED 1
#endif

在BLE_TPS_BLE_OBSERVER_PRIO下方添加一个项目如下:

// <o> BLE_TPS_BLE_OBSERVER_PRIO  
// <i> Priority with which BLE events are dispatched to the TX Power Service.
#ifndef BLE_TPS_BLE_OBSERVER_PRIO
#define BLE_TPS_BLE_OBSERVER_PRIO 2
#endif

// <o> BLE_YQS_C_BLE_OBSERVER_PRIO  
// <i> Priority with which BLE events are dispatched to the Youqun Client.
#ifndef BLE_YQS_C_BLE_OBSERVER_PRIO
#define BLE_YQS_C_BLE_OBSERVER_PRIO 2
#endif 

同时修改禁用BLE_NUS_C_ENABLE,如下:

// <q> BLE_NUS_C_ENABLED  - ble_nus_c - Nordic UART Central Service

#ifndef BLE_NUS_C_ENABLED
#define BLE_NUS_C_ENABLED 0
#endif

找到NRF_SDH_BLE_VS_UUID_COUNT,确保其值为1,如下:

// <o> NRF_SDH_BLE_VS_UUID_COUNT - The number of vendor-specific UUIDs. 
#ifndef NRF_SDH_BLE_VS_UUID_COUNT
#define NRF_SDH_BLE_VS_UUID_COUNT 1
#endif

(4)调用YQS

这个工程实际上是一个可以运行的nus主机工程,我们只要将所有的nus替换成yqs即可。

做全局替换,区分大小写:

  • NUS –> YQS
  • nus –> yqs

此时编译通过,应该可以正常运行。在RTT Viewer中可以看到相应的输出。

但是如果在另一个开发板中跑YQS从机工程,会发现它们无法建立连接及通信。

(5)设备连接

yqs主机发现设备广播数据后,如果检测到了YQS UUID,则连接设备,否则继续扫描。

但是我们的YQS基于ble_app_template实现的,广播包中包含的UUID是0x180A,而不是YQS UUID。这个问题与GATT 服务运行机制无关,这里我们简单的修改一下,越过这个问题。

找到m_yqs_uuid,并做如下修改:

/**@brief YQS uuid. */
static ble_uuid_t const m_yqs_uuid =
{
    .uuid = 0x180A/*BLE_UUID_YQS_SERVICE*/,
    .type = BLE_UUID_TYPE_BLE/*YQS_SERVICE_UUID_TYPE*/
};

这时候再编译运行,即可发现和连接YQS从机设备。

通过RTT Viewer可以看到相关信息。

(完)

上文中实现了一个GATT服务,其实就是将NUS改了改名字,本文深入到ble_yqs代码中,梳理它的实现脉络。

(1)UUID

作为一个自定义服务,一定会有一个128-bit的UUID。

在ble_yqs.c中,定义了一个基础UUID(YQS_BASE_UUID):

#define YQS_BASE_UUID  {{0x9D, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x00, 0x00, 0x40, 0x6E}} /**< Used vendor specific UUID. */

注意倒数第三、四位是0x0000,这两个字节为占位符,运行时将根据Service UUID和Characteristic UUID进行填充。

初始化时候,要先将这个基础UUID写入协议栈:

sd_ble_uuid_vs_add(&yqs_base_uuid, &p_yqs->uuid_type);

同时定义了双字节的Service和Characteristic的UUID:

#define BLE_UUID_YQS_SERVICE 0x0001            /**< The UUID of the Nordic UART Service. */
#define BLE_UUID_YQS_RX_CHARACTERISTIC 0x0002  /**< The UUID of the RX Characteristic. */
#define BLE_UUID_YQS_TX_CHARACTERISTIC 0x0003  /**< The UUID of the TX Characteristic. */

最终生成的UUID如下:

Service UUID={0x9D, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E}

RX Char UUID={0x9D, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x40, 0x6E}

TX Char UUID={0x9D, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x40, 0x6E}

(2)添加服务

sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,&ble_uuid,&p_yqs->service_handle);

传入参数为服务的类型和UUID,最后一个参数为传出参数,记录这个服务的句柄。

在该服务下添加特征都将围绕这个服务句柄(service_handle)进行。

(3)添加特征

sd_ble_gatts_characteristic_add(p_yqs->service_handle,&char_md,&attr_char_value,&p_yqs->rx_handles);

传入参数为服务句柄、特征的特性以及特征值,最后一个参数为传出参数,记录这个特征的句柄。

特征的相关参数比较繁杂,至少需要了解以下知识点:

  • write/write_no_response/notify/indicate/read
  • CCCD
  • authorization required or not
  • encryption with MITM or not
  • value length and offset
  • MTU size

解释清楚这些细节需要一篇专门的文章,这里略过。

而在ble_yqs.c中,RX Characteristic实现了write和write no reponse两个写属性,表示主机(Central)可以对该特征进行写操作。TX Characteristic实现了notify的属性,表示主机可以接收该特征发出的数据。

(4)接收数据

YQS接收数据是指主机向RX Characteristic发送数据,将产生BLE_GATTS_EVT_WRITE事件,进一步触发on_write()回调函数。

在回调函数中,根据特征句柄的不同,处理两件事:

  • 处理CCCD使能信号
  • 处理用户write数据

用户的write数据进一步产生BLE_YQS_EVT_RX_DATA事件,触发外部回调事件。

(5)发送数据

发送数据即执行notify操作。主机端使能notify以后,才能够执行notify。

notify采用API:sd_ble_gatts_hvx函数,值得注意的是,发送数据的长度要小于MTU Size,否则将触发NRF_ERROR_INVALID_PARAM错误。

所有数据发送完毕触发BLE_GATTS_EVT_HVN_TX_COMPLETE事件。

(6)声明YQS

从SDK 14起,各种服务都称为一个个“观察者(Observer)”,声明观察者的操作都是宏的形式,如下:

BLE_YQS_DEF(m_yqs);

它需要放在全局变量位置。

(7)初始化YQS

所有的GATT服务都放在services_init()中初始化。

初始化时候要指定事件处理的回调函数,这里是yqs_data_handler

(8)事件处理

在回调函数yqs_data_handler中,处理在ble_yqs.c中定义好的事件,从而告诉主程序数据收发的节点。

 

(完)