8 GATT - 服务器
8.1 概览
GATT 服务器32为客户端提供服务。 协议栈支持多个连接,每个连接的配置(Profile)可以独立设置。 需要注意,GATT 服务器和客户端这两个角色与主、从两个角色没有任何关联:一个连接的主角色既可以充当 GATT 的客户端,也可以充当服务器, 还可以两种角色一起扮演;一个连接的从角色也是如此。
要使用 GATT 服务器,开发者需要做三件事33:
初试化:设置事件回调
void att_server_register_packet_handler( btstack_packet_handler_t handler);初始化:提供回调函数
void att_server_init( // 特征的读回调 att_read_callback_t read_callback, // 特征的写回调 att_write_callback_t write_callback);读回调的类型如下:
typedef uint16_t (*att_read_callback_t)( // 连接句柄 hci_con_handle_t con_handle, // 特征句柄 uint16_t attribute_handle, // 数据偏移 uint16_t offset, // 缓存 uint8_t *buffer, // 缓存的大小 uint16_t buffer_size);写回调的类型如下:
typedef int (*att_write_callback_t)( // 连接句柄 hci_con_handle_t con_handle, // 特征句柄 uint16_t attribute_handle, // 会话模式 uint16_t transaction_mode, // 数据偏移 uint16_t offset, // 缓存 const uint8_t *buffer, // 缓存的大小 uint16_t buffer_size);将
con_handle和attribute_handle组合到一起,回调函数就可以确定是在访问哪个 Profile 里的哪个特征。 对于长度超过\((ATT\_MTU - 3)\)的长值,BLE 支持分块读写模式,相应地,两个回调函数都有一个offset参数。关于会话模式
transaction_mode的说明见后文。适时提供 Profile 数据
void att_set_db( // 要关联的句柄 hci_con_handle_t con_handle, // Profile 数据库 const uint8_t *db);
协议栈内的 GATT 服务器可以通过 2 种方式获得特征的值,然后传输到客户端:
保存在 Profile 数据库内部的值
这种方式适用于值不改变的情况,服务器可能自动将值传输到客户端,不需要开发者参与。
借助回调函数
att_read_callback_t这种方式适用于值动态改变的情况。 每当客户端读取值时,协议栈会立即调用回调函数,且
buffer为NULL。 这一次调用是为了获取数值长度。回调函数的处理流程又可以分为两种情况:如果 App 可以立即准备好数据,那么直接返回数值的总长;
之后,协议栈准备内存空间并立即再次调用函数,此时
buffer参数非NULL, 回调函数将数据写入buffer所指向的内存,读取完成;如果 App 无法立即准备好数据,那么返回
ATT_DEFERRED_READ进入延迟读取模式;待数据就绪之后,App 调用
att_server_deferred_read_response将数据传给协议栈,读取完成。
每个特性具有若干属性,见表 8.1。
| 属性 | 说明 |
|---|---|
| ATT_PROPERTY_BROADCAST | 允许广播该特性的值 |
| ATT_PROPERTY_READ | 允许读取 |
| ATT_PROPERTY_WRITE_WITHOUT_RESPONSE | 允许无响应写入 |
| ATT_PROPERTY_WRITE | 允许(有响应的)写入 |
| ATT_PROPERTY_NOTIFY | 允许通知(Notification) |
| ATT_PROPERTY_INDICATE | 允许指示(Indication) |
| ATT_PROPERTY_AUTHENTICATED_SIGNED_WRITE | 允许带签名的写入 |
| ATT_PROPERTY_EXTENDED_PROPERTIES | 支持扩展属性 |
| ATT_PROPERTY_DYNAMIC | 为动态特性(即需要使用回调函数) |
其中 DYNAMIC 为协议栈自定义的属性,只有加上了这个属性,对特性的读写操作才会交由回调函数处理。对于支持写入的特征,
由于总是需要通过回调函数处理,必须加上此属性。
对于支持通知(Notification)和/或指示(Indication)的特征,必须带有 Client Characteristic Configuration 描述符(常被简称为 CCCD):
- 将
GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION写入 CCCD 就可以使能通知, - 将
GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_INDICATION写入 CCCD 就可以使能指示, - 将
GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NONE写入 CCCD 就可以关闭通知和指示。
通知相当于无响应的上报,而指示相当于有响应的上报。
8.2 使用说明
8.2.1 Profile 数据
Profile 数据34 使用了协议栈自定义的数据结构。 开发者不需要了解这个数据结构的定义,而是借助图形化工具或者编程地生成。
使用图形化的 Profile 编辑器。
请参考 《SDK 用户手册》。
使用
att_db_util模块。调用
att_db_util_init告知 Profile 数据的存储空间。 调用att_db_util_add_service_uuid??添加服务,然后通过att_db_util_add_characteristic_uuid??添加若干特性。 重复上述步骤可以添加多个服务。最后调用att_db_util_get_size查看整个 Profile 数据的大小,相应调整存储空间的大小,再重新编译程序。上面的
....._uuid??函数都包含uuid16和uuid128等两种形式,分别对应 16-bit 的简短 UUID 和 16 字节的完整 UUID。att_db_util_add_characteristic_uuid??函数返回的是特性的值的句柄。 调用att_db_util_add_characteristic_uuid??时需要注意根据情况添加ATT_PROPERTY_DYNAMIC属性。 对于带有ATT_PROPERTY_NOTIFY和/或ATT_PROPERTY_INDICATE属性的特性,att_db_util_add_characteristic_uuid??函数会自动添加 CCCD。 设特性的值句柄为 \(N\),那么 CCCD 的句柄为 \(N + 1\)。
8.2.2 实现读回调
一个典型的读回调函数大概是这种样子:
static uint16_t att_read_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t offset,
uint8_t * buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_0:
if (buffer)
{
memcpy(buffer, ...)
return buffer_size;
}
else
return size of value;
//...
default:
return 0;
}
}延迟读取的情况:
static uint16_t att_read_callback(hci_con_handle_t connection_handle,
uint16_t att_handle, uint16_t offset,
uint8_t * buffer, uint16_t buffer_size)
{
switch (att_handle)
{
case HANDLE_0:
//...
return ATT_DEFERRED_READ;
//...
default:
return 0;
}
}延迟读取的数据通过 att_server_deferred_read_response 传递给协议栈:
int att_server_deferred_read_response(
// 连接句柄
hci_con_handle_t con_handle,
// 特征句柄
uint16_t attribute_handle,
// 指向值的指针
const uint8_t *value,
// 值的长度
uint16_t value_len);SDK GATT Relay 演示了延迟读取的具体用法。
8.2.3 实现写回调
当写特性时,可能触发服务器执行某个动作。BLE 为了精度控制动作的执行引入了会话35概念:
一个会话包含对 1 个或多个特性的写入,最后是一个显式的“执行”命令通知服务器开始执行。此外还有一个“取消”命令,通知服务器会话不要执行动作、直接终止。相应地,
写回调的会话模式 transaction_mode 参数包含四种值:
// 无会话的普通写入
#define ATT_TRANSACTION_MODE_NONE ...
// 带会话的写入
#define ATT_TRANSACTION_MODE_ACTIVE ...
// 执行
#define ATT_TRANSACTION_MODE_EXECUTE ...
// 取消
#define ATT_TRANSACTION_MODE_CANCEL ...对于 NONE 和 ACTIVE 两种模式,都会传入要写入的数据,而 EXECUTE 和 CANCEL 则既不会传入特征句柄,也不传入数据,
也就是说这两个命令只关联到连接,施加于整个服务器,而不针对某个特征。
一个典型的写回调函数大概是这种样子:
static int att_write_callback(
hci_con_handle_t connection_handle, uint16_t att_handle,
uint16_t transaction_mode,
uint16_t offset, const uint8_t *buffer, uint16_t buffer_size)
{
处理 EXECUTE 和 CANCEL 两种会话模式并返回;
//
switch (att_handle)
{
case HANDLE_0:
//...
return 0;
//...
default:
return 0;
}
}如果发现错误,可以返回非 0 值告知客户端36。
8.2.4 发送通知(Notification)
通过 att_server_notify 发送通知:
int att_server_notify(
// 连接句柄
hci_con_handle_t con_handle,
// 特征句柄
uint16_t attribute_handle,
// 指向值的指针
const uint8_t *value,
// 值的长度
uint16_t value_len);该函数返回的状态代码及含义如下:
- 0:数据正常进入发送队列;
BTSTACK_LE_CHANNEL_NOT_EXIST(0x59):参数con_handle指定的连接不存在;BTSTACK_ACL_BUFFERS_FULL(0x57):内部缓存已满,数据未进入发送队列37。
8.2.5 发送指示(Indication)
通过 att_server_indicate 发送指示:
int att_server_indicate(
// 连接句柄
hci_con_handle_t con_handle,
// 特征句柄
uint16_t attribute_handle,
// 指向值的指针
const uint8_t *value,
// 值的长度
uint16_t value_len);指示的响应通过 ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE 事件通知 App,事件里的状态码的定义见 ATT 错误码。
该函数返回的状态代码及含义如下:
- 0:数据正常进入发送队列;
BTSTACK_LE_CHANNEL_NOT_EXIST(0x59):参数con_handle指定的连接不存在;BTSTACK_ACL_BUFFERS_FULL(0x57):内部缓存已满,数据未进入发送队列38;ATT_HANDLE_VALUE_INDICATION_IN_PROGRESS(0x90):在收到ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE事件前, 不能发送新的指示,否则att_server_indicate将返回此错误码。
8.2.6 响应事件
GATT 服务器模块会弹出以下事件。
ATT_EVENT_MTU_EXCHANGE_COMPLETE这个事件表示 MTU 协商完成。有两个解析函数:
att_event_mtu_exchange_complete_get_handle(packet)获得连接句柄;att_event_mtu_exchange_complete_get_MTU(packet)获得 MTU 大小。
ATT_EVENT_HANDLE_VALUE_INDICATION_COMPLETE这个事件表示指示发送完成。有三个解析函数:
att_event_handle_value_indication_complete_get_conn_handle(packet)获得连接句柄;att_event_handle_value_indication_complete_get_attribute_handle(packet)获得特征句柄;att_event_handle_value_indication_complete_get_status(packet)获得响应状态。共有两种状态:\(0\) 表示成功送达,
ATT_HANDLE_VALUE_INDICATION_TIMEOUT(0x91) 表示超时。
在不引起混淆的前提下,本手册混用 ATT 服务器、GATT 服务器,代码里也用
att_server代指gatt_server。↩︎事实上,这几件事已由 Wizard 工具代劳。↩︎
在手册、工具、代码的不同位置可能使用了不同的名词,如 Profile 数据库、GATT 数据库、GATT 数据等。↩︎
协议栈里称为“会话”,规范里称为“队列”。↩︎
仅适用于有响应的写入,无响应的写入无效。↩︎
解决方法参考 L2CAP 传输队列。↩︎
解决方法参考 L2CAP 传输队列。↩︎