BTstack - GATT Server

BTstack - GATT Server

.gatt 文件

首先我们需要了解一下 BTstack 特有的 GATT 表示文件 .gatt,它是一种类似 csv 格式的表示法,如下所示:

// import service_name
#import <service_name.gatt>

PRIMARY_SERVICE, {SERVICE_UUID}
CHARACTERISTIC, {ATTRIBUTE_TYPE_UUID}, {PROPERTIES}, {VALUE}
CHARACTERISTIC, {ATTRIBUTE_TYPE_UUID}, {PROPERTIES}, {VALUE}
...
PRIMARY_SERVICE, {SERVICE_UUID}
CHARACTERISTIC, {ATTRIBUTE_TYPE_UUID}, {PROPERTIES}, {VALUE}
...

每一行代表一个 ATT 里规定的 Attribute,PRIMARY_SERVICE 表明定义一个 primary service,并且其后面的 SERVICE_UUID 代表了此 service 的 uuid,可以是 16bit (1800) ,也可以是 128bit (00001234-0000-1000-8000-00805F9B34FB)。

CHARACTERISTIC 表明定义一个 characteristic,ATTRIBUTE_TYPE_UUID 就是该 characteristic 的 uuid,VALUE 可以是一个字符串(“this is a string”),也可以是一个 16 进制的字节序( 01 02 03)。PROPERTIES 表明此 characteristic 的属性,多个属性可以用 | 操作连接,常用的有:

Property Meaning
READ Characteristic can be read
WRITE Characteristic can be written using Write Request
WRITE_WITHOUT_RESPONSE Characteristic can be written using Write Command
NOTIFY Characteristic allows notifications by server
INDICATE Characteristic allows indication by server
DYNAMIC Read or writes to Characteristic are handled by application

上述的属性只有 DYNAMIC 需要着重理解一下,该属性并不是 Spec 里定义的,而是 BTstack 协议栈自定义的属性,添加了 DYNAMIC 属性后,当对该 characteristic value 进行读写操作时,协议栈会通过 att_servet_*() 函数注册的回调函数通知应用程序处理,适用于 value 值需要动态变化的场景,例如 Battery Service 里的电池电量会不断变化。

当然,为了方便 .gatt 文件的服用,BTstack 提供了 #import <service_name.gatt> 语法,其类似于 C 语言的 #include,将 service_name.gatt 文件里的内容复制到 import 的地方。

如何快速生成标准 GATT Service 的 .gatt 文件呢?BTstack 也提供了相应的工具 tool/convert_gatt_service.py ,在命令行直接运行该工具,会列出一系列的标准 Service,如下所示:

jackis@jackis-VirtualBox:~/btstack$ tool/convert_gatt_service.py 
List of services cached from https://www.bluetooth.com/specifications/gatt/services/ on 2020-07-22

Specification Type                                     | Specification Name            | UUID
-------------------------------------------------------+-------------------------------+-----------
org.bluetooth.service.alert_notification               | Alert Notification Service    | 0x1811
org.bluetooth.service.automation_io                    | Automation IO                 | 0x1815
org.bluetooth.service.battery_service                  | Battery Service               | 0x180F
org.bluetooth.service.blood_pressure                   | Blood Pressure                | 0x1810
......

To convert a service into a .gatt file template, please call the script again with the requested Specification Type and the output file name
Usage: tool/convert_gatt_service.py SPECIFICATION_TYPE [service_name.gatt]

并且在最后会提示使用命令

Usage: tool/convert_gatt_service.py SPECIFICATION_TYPE [service_name.gatt]

生成 .gatt 文件,以 Battery Service 为例:

jackis@jackis-VirtualBox:~/btstack$ tool/convert_gatt_service.py org.bluetooth.service.battery_service battery_service.gatt
Fetching org.bluetooth.service.battery_service from https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.battery_service.xml

Service Battery Service
- Characteristic Battery Level - properties ['Read', 'Notify']
-- Descriptor Characteristic Presentation Format       - TODO: Please set values
-- Descriptor Client Characteristic Configuration     

Service successfully created battery_service.gatt
Please check for TODOs in the .gatt file

打开生成的 battery_service.gatt 文件:

// Specification Type org.bluetooth.service.battery_service
// https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.battery_service.xml

// Battery Service 180F
PRIMARY_SERVICE, ORG_BLUETOOTH_SERVICE_BATTERY_SERVICE
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_BATTERY_LEVEL, DYNAMIC | READ | NOTIFY,
// TODO: Characteristic Presentation Format: please set values
#TODO CHARACTERISTIC_FORMAT, READ, _format_, _exponent_, _unit_, _name_space_, _description_
CLIENT_CHARACTERISTIC_CONFIGURATION, READ | WRITE,

可以看见它已经按照标准定义了相应的 service,characteristic 和 characteristic configuration。

battery service server

BTstack 只提供了 ATT Server 的实现,GATT Server 的逻辑通过一个 GATT 编译器实现,该编译器将上述的 *.gatt 文件转换成了一个 *.h 文件,并在里面存储了 ATT Server 需要的 attribute table。

以 BTstack 实现的 /ble/gatt-service/battery_service_server.c 为例,学习一下如何实现一个 GATT Service 。

根据 BAS_SPEC_V10 的规定,Battery Service 的 UUID 为 «Battery Service» - 0x180F,其有一个 Characteristic 为 Battery Level,UUID 为 ,属性为 Read,Notify(可选)。因此来看 /ble/gatt-service/battery_service.gatt 文件:

// Specification Type org.bluetooth.service.battery_service
// https://www.bluetooth.com/api/gatt/xmlfile?xmlFileName=org.bluetooth.service.battery_service.xml

// Battery Service 180F
PRIMARY_SERVICE, ORG_BLUETOOTH_SERVICE_BATTERY_SERVICE
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_BATTERY_LEVEL, DYNAMIC | READ | NOTIFY,

简单定义了一个 PRIMARY_SERVICECHARACTERISTIC,GATT 编译器会将该 .gatt 文件编译生成 battery_service.h 文件,里面包含:

const uint8_t profile_data[] =
{
    // ATT DB Version
    1,
 
    // add Battery Service
    // #import <battery_service.gatt> -- BEGIN
    // Specification Type org.bluetooth.service.battery_service
    // https://www.bluetooth.com/api/gatt/xmlfile?xmlFileName=org.bluetooth.service.battery_service.xml
    // Battery Service 180F

    // 0x0004 PRIMARY_SERVICE-ORG_BLUETOOTH_SERVICE_BATTERY_SERVICE
    0x0a, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x28, 0x0f, 0x18, 
    // 0x0005 CHARACTERISTIC-ORG_BLUETOOTH_CHARACTERISTIC_BATTERY_LEVEL-DYNAMIC | READ | NOTIFY
    0x0d, 0x00, 0x02, 0x00, 0x05, 0x00, 0x03, 0x28, 0x12, 0x06, 0x00, 0x19, 0x2a, 
    // 0x0006 VALUE-ORG_BLUETOOTH_CHARACTERISTIC_BATTERY_LEVEL-DYNAMIC | READ | NOTIFY-''
    // READ_ANYBODY
    0x08, 0x00, 0x02, 0x01, 0x06, 0x00, 0x19, 0x2a, 
    // 0x0007 CLIENT_CHARACTERISTIC_CONFIGURATION
    // READ_ANYBODY, WRITE_ANYBODY
    0x0a, 0x00, 0x0e, 0x01, 0x07, 0x00, 0x02, 0x29, 0x00, 0x00, 
    // #import <battery_service.gatt> -- END

    // END
    0x00, 0x00, 
};

这里的 profile_data[] 数组就是 ATT 里使用的 attribute table,包含了 Battery Service 的实现。

battery_service_server 仅仅提供了 2 个接口,调用 battery_service_server_init() 函数初始化 server 并设置当前电量值,当电池电量发生变化时,调用 battery_service_server_set_battery_value() 函数更新值即可,内部细节被封装的很好,使用很方便。我们来看看这些接口是如何实现的,所涉及到的 ATT 接口都在 att_server.h 文件里。

在 init 函数里,首先调用 gatt_server_get_get_handle_range_for_service_with_uuid16() 函数获取指定 service uuid 在 attribute table 里的 start handle 和 end handle。因为 attribute table 是我们用户自己定义的,不同 service 的顺序可能不一样,所以需要调用该接口。然后利用接口 gatt_server_get_value_handle_for_characteristic_with_uuid16() 函数获取在 [start handle, end handle] 里指定 uuid 的 characteritic 的 vaule handle ,用于 ATT 读写。还可以利用 gatt_server_get_client_configuration_handle_for_characteristic_with_uuid16() 函数获取 client configuration 的 handle,供 ATT 读写。最后利用 att_server_register_service_handler() 函数注册了一个读写回调函数,

// register service with ATT Server
battery_service.start_handle   = start_handle;
battery_service.end_handle     = end_handle;
battery_service.read_callback  = &battery_service_read_callback;
battery_service.write_callback = &battery_service_write_callback;
att_server_register_service_handler(&battery_service);

当 ATT 读写 [start_handle, end_handle] 之间的 attribute 时,都会回调注册的函数。先看看注册的写回调函数:

static int battery_service_write_callback(hci_con_handle_t con_handle, uint16_t attribute_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
    UNUSED(transaction_mode);
    UNUSED(offset);
    UNUSED(buffer_size);

    if (attribute_handle == battery_value_handle_client_configuration){
        battery_value_client_configuration = little_endian_read_16(buffer, 0);
        battery_value_client_configuration_connection = con_handle;
    }
    return 0;
}

当本次写的 attribute handle 是之前存储的 client configuration handle 时,从写入数据 buffer 读取了 16bit 的数据。这就是写入的 client configuration value 值。

读回调函数里主要处理了读取的数据:

static uint16_t battery_service_read_callback(hci_con_handle_t con_handle, uint16_t attribute_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size){
    UNUSED(con_handle);

    if (attribute_handle == battery_value_handle_value){
        return att_read_callback_handle_byte(battery_value, offset, buffer, buffer_size);
    }
    if (attribute_handle == battery_value_handle_client_configuration){
        return att_read_callback_handle_little_endian_16(battery_value_client_configuration, offset, buffer, buffer_size);
    }
    return 0;
}

当读取的 attribute handle 是之前存储的 battery value characteristic handle 时,利用接口 att_read_callback_handle_byte()buffer 里写入一个字节的数据。当然还可以写入 16bit,32bit 和任意bit 的数据,接口为 att_read_callback_handle_*()

battery service server 提供的另一个接口用于更新电池电量:

void battery_service_server_set_battery_value(uint8_t value){
    battery_value = value;
    if (battery_value_client_configuration != 0){
        battery_callback.callback = &battery_service_can_send_now;
        battery_callback.context  = (void*) (uintptr_t) battery_value_client_configuration_connection;
        att_server_register_can_send_now_callback(&battery_callback, battery_value_client_configuration_connection);
    }
}

由于 BTstack 内部设计,不是每一个时刻都可以发生数据,需要等待协议栈内部准备好,因此这里用 att_server_register_can_send_now_callback() 函数注册了一个回调函数,当协议栈内部好可以发送 ATT 数据时会回调它:

static void battery_service_can_send_now(void * context){
    hci_con_handle_t con_handle = (hci_con_handle_t) (uintptr_t) context;
    att_server_notify(con_handle, battery_value_handle_value, &battery_value, 1);
}

在回调函数里利用 att_server_notify() 函数发送一个 notify 。

att_server

本小节从整体角度考虑 att_server.h 里的接口该如何使用。

首先通过 att_server_init() 初始化 ATT Server,该函数可传入一个读回调函数,一个写回调函数,用于 ATT 读写一条 arrtibute 的情况。而 att_server_register_service_handler() 函数也可以注册读写回调函数,不过其优先级更高,而且只对 [start_handle, end_handle] 之间的 attribute 读写有效。

由于 BTstack 并不能在任意时刻发送数据,因此需要注册回调函数,等待合适的时候协议栈回调该函数,执行发送动作。共有两种类型的接口:

  1. 利用 ATT_EVENT_CAN_SEND_NOW 事件的发送方式。
  2. 利用注册的特定回调函数完成发送。

类型 1 的接口需要通过 att_server_register_packet_handler() 函数注册一个回调函数,在该回调函数里判断是否收到 ATT_EVENT_CAN_SEND_NOW 事件,若收到则进行发送操作,例如:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
    UNUSED(channel);
    UNUSED(size);

    switch (packet_type) {
        case HCI_EVENT_PACKET:
            switch (hci_event_packet_get_type(packet)) {
                case ATT_EVENT_CAN_SEND_NOW:
                    att_server_notify(con_handle, ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE, (uint8_t*) counter_string, counter_string_len);
                    break;
            }
            break;
    }
}

当然配套使用的接口 att_server_request_can_send_now_event() 用于发送 ATT_EVENT_CAN_SEND_NOW 事件。

类型 2 的接口 att_server_register_can_send_now_callback()att_server_request_to_send_notification()att_server_request_to_send_indication() 这些接口注册一个回调函数,该函数回调时可以用 att_server_notify()att_server_indicate() 发送数据。


参考:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容