9 GATT - 客户端
9.1 概览
GATT 客户端的主要功能是发现服务,读写和订阅特征。每个连接使用独立的 GATT 客户端实例。
GATT 客户端的操作几乎都需要先发出数据再等待服务器发回响应,需要较长的时间,所以也引入了会话的概念:
如果上一个会话未结束,那么就不允许发起新的会话。每次发起会话时,都要提供一个专门的回调函数。
当这个回调函数收到 GATT_EVENT_QUERY_COMPLETE
事件时,会话结束。
使用 GATT 客户端 API 时需要注意检查返回值,常见的返回值有:
\(0\):正常、成功;
BTSTACK_MEMORY_ALLOC_FAILED
:内存不足,无法创建 GATT 客户端实例GATT_CLIENT_IN_WRONG_STATE
:会话冲突解决方法:等待上一个会话完成后再发起新的会话。通过
gatt_client_is_ready
可以查询当前是否可以发起新的会话。GATT_CLIENT_VALUE_TOO_LONG
:要写入的值超出 MTU 的限制解决方法:缩短值的长度;或者检查对比设备的 MTU 能力。
BTSTACK_ACL_BUFFERS_FULL
:内部缓存已满,数据未进入发送队列解决方法:参考 L2CAP 传输队列。
可选地,开发者可以设置 GATT 客户端事件回调:
void gatt_client_register_handler(
); btstack_packet_handler_t handler
这个回调函数目前只会收到一个事件36:
GATT_EVENT_MTU
这个事件表示 MTU 协商完成。有两个解析函数:
gatt_event_mtu_get_handle(packet)
获得连接句柄;
gatt_event_mtu_get_mtu(packet)
获得 MTU 大小。
9.2 使用说明
9.2.1 创建客户端
调用 GATT 客户端的绝大多数 API 时,都会按需自动创建客户端实例。不会触发创建动作的 API:
gatt_client_listen_for_characteristic_value_updates
通过 gatt_client_is_ready()
可“手动”创建客户端实例。
9.2.2 发现服务
下列 API 用来发现服务:
gatt_client_discover_primary_services
:发现所有服务。gatt_client_discover_primary_services_by_uuid16
:发现指定的服务。gatt_client_discover_primary_services_by_uuid128
:发现指定的服务。
下列 API 用来发现服务内部的特征:
gatt_client_discover_characteristics_for_service
:发现一个服务里的所有特征gatt_client_discover_characteristics_for_handle_range_by_uuid16
:在一定句柄范围内发现指定的特征gatt_client_discover_characteristics_for_handle_range_by_uuid16
:在一定句柄范围内发现指定的特征gatt_client_discover_characteristic_descriptors
:发现特征的所有描述符。
以 gatt_client_discover_primary_services
的使用为例说明使用方法。这个 API 的原型如下:
uint8_t gatt_client_discover_primary_services(
// 回调
,
user_packet_handler_t callback// 连接句柄
); hci_con_handle_t con_handle
其回调函数的例子:
static void service_discovery_callback(
// 事件包类型(忽略)
uint8_t packet_type,
// 连接句柄(这个句柄也可以从事件内部获得,故忽略此参数)
uint16_t _,
// 事件包
const uint8_t *packet,
// 事件包大小
uint16_t size)
{
uint16_t con_handle;
switch (packet[0])
{
// 对于发现的每个服务都有一个 QUERY_RESULT
case GATT_EVENT_SERVICE_QUERY_RESULT:
{
const gatt_event_service_query_result_t *result =
(packet);
gatt_event_service_query_result_parse// ....
}
break;
case GATT_EVENT_QUERY_COMPLETE:
// 会话完成
break;
}
}
为了简化开发,SDK 提供了 gatt_client_util 模块,只调用一个函数就可以完成服务发现:
struct gatt_client_discoverer *gatt_client_util_discover_all(
// 连接句柄
,
hci_con_handle_t con_handle// 发现完成后的回调
,
f_on_fully_discovered on_fully_discovered// 传递给黑调的用户数据
void *user_data);
下面的回调函数演示了如何遍历所有服务、特征:
void my_on_fully_discovered(
// 第一个服务
*first,
service_node_t // 用户数据
void *user_data,
// 错误码
int err_code)
{
if (err_code) ...
*s = first;
service_node_t // 遍历服务
while (s)
{
*c = s->chars;
char_node_t // 遍历服务的所有特征
while (c)
{
*d = c->descs;
desc_node_t // 遍历特征的所有描述符
while (d)
{
= d->next;
d }
= c->next;
c }
= s->next;
s }
}
9.2.3 读取特征
可以通过特征的句柄或者 UUID 读取值:
gatt_client_read_value_of_characteristic_using_value_handle
gatt_client_read_value_of_characteristics_by_uuid16
gatt_client_read_value_of_characteristics_by_uuid128
也可以指定多个句柄批量读取:
gatt_client_read_multiple_characteristic_values
基于句柄的分块读取:
gatt_client_read_long_value_of_characteristic_using_value_handle
gatt_client_read_long_value_of_characteristic_using_value_handle_with_offset
以 gatt_client_read_value_of_characteristic_using_value_handle
说明使用方法。其原型为:
uint8_t gatt_client_read_value_of_characteristic_using_value_handle(
// 回调
,
btstack_packet_handler_t callback// 连接句柄
,
hci_con_handle_t con_handle// 特征句柄
uint16_t characteristic_value_handle);
其回调函数的例子:
void read_characteristic_value_callback(
// 事件包类型(忽略)
uint8_t packet_type,
// 连接句柄(这个句柄也可以从事件内部获得,故忽略此参数)
uint16_t _,
// 事件包
const uint8_t *packet,
// 事件包大小
uint16_t size)
{
switch (packet[0])
{
case GATT_EVENT_CHARACTERISTIC_VALUE_QUERY_RESULT:
{
uint16_t value_size;
const gatt_event_value_packet_t *value =
(
gatt_event_characteristic_value_query_result_parse, size, &value_size);
packet// value->handle 为特征句柄
// value_size 为值的长度
// value->value 是指向值的指针
}
break;
case GATT_EVENT_QUERY_COMPLETE:
// 会话完成
break;
}
}
9.2.4 写入特征
通过特征的句柄写入值:
gatt_client_write_value_of_characteristic
:有响应的写入gatt_client_write_value_of_characteristic_without_response
:无响应的写入
基于句柄的分块写入:
gatt_client_write_long_value_of_characteristic
gatt_client_write_long_value_of_characteristic_with_offset
其中 gatt_client_write_value_of_characteristic
的原型为:
uint8_t gatt_client_write_value_of_characteristic(
// 回调
,
btstack_packet_handler_t callback// 连接句柄
,
hci_con_handle_t con_handle// 特征句柄
uint16_t characteristic_value_handle,
// 值的长度
uint16_t length,
// 指向值的指针
const uint8_t * data);
其回调函数的例子:
void write_characteristic_value_callback(
uint8_t packet_type, uint16_t _,
const uint8_t *packet, uint16_t size)
{
switch (packet[0])
{
case GATT_EVENT_QUERY_COMPLETE:
("特征写入完成。状态码:: %d\n",
platform_printf(packet)->status);
gatt_event_query_complete_parsebreak;
}
}
状态码的定义见 ATT 错误码。
与之相比,无响应的写入不需要提供回调函数,没有“会话”的概念,下一次写入不需要等待上次完成,数据吞吐率更高。
uint8_t gatt_client_write_value_of_characteristic_without_response(
// 连接句柄
,
hci_con_handle_t con_handle// 特征句柄
uint16_t characteristic_value_handle,
// 值的长度
uint16_t length,
// 指向值的指针
const uint8_t * data);
9.2.5 订阅特征
开发者需要完成两个步骤:
调用
gatt_client_listen_for_characteristic_value_updates
添加值更新时的回调函数void gatt_client_listen_for_characteristic_value_updates( // 开发者提供一个回调函数结构体 * notification, gatt_client_notification_t // 回调函数 , btstack_packet_handler_t packet_handler// 连接句柄 , hci_con_handle_t con_handle// 特征的值的句柄 uint16_t value_handle);
由于
notification
会被添加到 GATT 客户端实例的回调链表中,所以必须指向一块全局内存,一定不能在栈上分配。notification
所指向的内容不需要初始化。如果 notification 是从动态内存(如堆)里分配的,那么当连接断开时记得释放这块内存,以免泄露。
调用
gatt_client_write_characteristic_descriptor_using_descriptor_handle
向 CCCD 写入使能标志这个函数的原型及使用方法与
gatt_client_write_value_of_characteristic
完全一致:uint8_t gatt_client_write_characteristic_descriptor_using_descriptor_handle( // 回调函数 , btstack_packet_handler_t callback// 连接句柄 , hci_con_handle_t con_handle// CCCD 的句柄 uint16_t descriptor_handle, // 数据长度(固定为 2) uint16_t length, // 值为 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION // 或 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_INDICATION // 或 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NONE uint8_t * data);
或者调用
gatt_client_write_client_characteristic_configuration
向 CCCD 写入使能标志。 这个函数的原型如下,它会自动发现 CCCD 的句柄而不需要通过参数传入:uint8_t gatt_client_write_client_characteristic_configuration( // 回调函数 , btstack_packet_handler_t callback// 连接句柄 , hci_con_handle_t con_handle// 待订阅的特征 * characteristic, gatt_client_characteristic_t // 值为 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION // 或 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_INDICATION // 或 GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NONE uint16_t configuration);
9.2.6 ATT_MTU
除了通过监听 GATT_EVENT_MTU
事件以外,通过 gatt_client_get_mtu
可以随时获取 ATT_MTU:
// 返回错误码
uint8_t gatt_client_get_mtu(
// 连接句柄
,
hci_con_handle_t con_handle// 输出 MTU
uint16_t * mtu);
特征的值的最大长度为 \((ATT\_MTU - 3)\)。调用 gatt_client_write_value_of_characteristic_without_response
写入值时,
如果超过最大长度,会返回错误码:GATT_CLIENT_VALUE_TOO_LONG
。
以及转发过来的
L2CAP_EVENT_CAN_SEND_NOW
事件。↩︎