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}
...
每一行代表一個(gè) ATT 里規(guī)定的 Attribute掸刊,PRIMARY_SERVICE
表明定義一個(gè) primary service,并且其后面的 SERVICE_UUID
代表了此 service 的 uuid,可以是 16bit (1800) ,也可以是 128bit (00001234-0000-1000-8000-00805F9B34FB)阵翎。
CHARACTERISTIC
表明定義一個(gè) characteristic,ATTRIBUTE_TYPE_UUID
就是該 characteristic 的 uuid之剧,VALUE
可以是一個(gè)字符串(“this is a string”)郭卫,也可以是一個(gè) 16 進(jìn)制的字節(jié)序( 01 02 03)。PROPERTIES
表明此 characteristic 的屬性背稼,多個(gè)屬性可以用 |
操作連接贰军,常用的有:
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 協(xié)議棧自定義的屬性谓形,添加了 DYNAMIC
屬性后,當(dāng)對(duì)該 characteristic value 進(jìn)行讀寫操作時(shí)疆前,協(xié)議棧會(huì)通過(guò) att_servet_*()
函數(shù)注冊(cè)的回調(diào)函數(shù)通知應(yīng)用程序處理寒跳,適用于 value 值需要?jiǎng)討B(tài)變化的場(chǎng)景,例如 Battery Service 里的電池電量會(huì)不斷變化竹椒。
當(dāng)然童太,為了方便 .gatt
文件的服用,BTstack 提供了 #import <service_name.gatt>
語(yǔ)法胸完,其類似于 C 語(yǔ)言的 #include
书释,將 service_name.gatt
文件里的內(nèi)容復(fù)制到 import 的地方。
如何快速生成標(biāo)準(zhǔn) GATT Service 的 .gatt
文件呢赊窥?BTstack 也提供了相應(yīng)的工具 tool/convert_gatt_service.py
爆惧,在命令行直接運(yùn)行該工具,會(huì)列出一系列的標(biāo)準(zhǔn) 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]
并且在最后會(huì)提示使用命令
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
打開(kāi)生成的 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,
可以看見(jiàn)它已經(jīng)按照標(biāo)準(zhǔn)定義了相應(yīng)的 service,characteristic 和 characteristic configuration址遇。
battery service server
BTstack 只提供了 ATT Server 的實(shí)現(xiàn)熄阻,GATT Server 的邏輯通過(guò)一個(gè) GATT 編譯器實(shí)現(xiàn),該編譯器將上述的 *.gatt
文件轉(zhuǎn)換成了一個(gè) *.h
文件倔约,并在里面存儲(chǔ)了 ATT Server 需要的 attribute table秃殉。
以 BTstack 實(shí)現(xiàn)的 /ble/gatt-service/battery_service_server.c 為例,學(xué)習(xí)一下如何實(shí)現(xiàn)一個(gè) GATT Service 。
根據(jù) BAS_SPEC_V10 的規(guī)定钾军,Battery Service 的 UUID 為 ?Battery Service? - 0x180F鳄袍,其有一個(gè) Characteristic 為 Battery Level,UUID 為 吏恭,屬性為 Read拗小,Notify(可選)。因此來(lái)看 /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,
簡(jiǎn)單定義了一個(gè) PRIMARY_SERVICE
和 CHARACTERISTIC
砸泛,GATT 編譯器會(huì)將該 .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[]
數(shù)組就是 ATT 里使用的 attribute table蛆封,包含了 Battery Service 的實(shí)現(xiàn)唇礁。
battery_service_server 僅僅提供了 2 個(gè)接口,調(diào)用 battery_service_server_init()
函數(shù)初始化 server 并設(shè)置當(dāng)前電量值惨篱,當(dāng)電池電量發(fā)生變化時(shí)盏筐,調(diào)用 battery_service_server_set_battery_value()
函數(shù)更新值即可,內(nèi)部細(xì)節(jié)被封裝的很好砸讳,使用很方便琢融。我們來(lái)看看這些接口是如何實(shí)現(xiàn)的,所涉及到的 ATT 接口都在 att_server.h 文件里簿寂。
在 init 函數(shù)里漾抬,首先調(diào)用 gatt_server_get_get_handle_range_for_service_with_uuid16()
函數(shù)獲取指定 service uuid 在 attribute table 里的 start handle 和 end handle。因?yàn)?attribute table 是我們用戶自己定義的常遂,不同 service 的順序可能不一樣纳令,所以需要調(diào)用該接口。然后利用接口 gatt_server_get_value_handle_for_characteristic_with_uuid16()
函數(shù)獲取在 [start handle, end handle] 里指定 uuid 的 characteritic 的 vaule handle 克胳,用于 ATT 讀寫平绩。還可以利用 gatt_server_get_client_configuration_handle_for_characteristic_with_uuid16()
函數(shù)獲取 client configuration 的 handle,供 ATT 讀寫漠另。最后利用 att_server_register_service_handler()
函數(shù)注冊(cè)了一個(gè)讀寫回調(diào)函數(shù)捏雌,
// 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);
當(dāng) ATT 讀寫 [start_handle, end_handle] 之間的 attribute 時(shí),都會(huì)回調(diào)注冊(cè)的函數(shù)笆搓。先看看注冊(cè)的寫回調(diào)函數(shù):
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;
}
當(dāng)本次寫的 attribute handle 是之前存儲(chǔ)的 client configuration handle 時(shí)性湿,從寫入數(shù)據(jù) buffer
讀取了 16bit 的數(shù)據(jù)。這就是寫入的 client configuration value 值满败。
讀回調(diào)函數(shù)里主要處理了讀取的數(shù)據(jù):
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;
}
當(dāng)讀取的 attribute handle 是之前存儲(chǔ)的 battery value characteristic handle 時(shí)窘奏,利用接口 att_read_callback_handle_byte()
向 buffer
里寫入一個(gè)字節(jié)的數(shù)據(jù)。當(dāng)然還可以寫入 16bit葫录,32bit 和任意bit 的數(shù)據(jù)着裹,接口為 att_read_callback_handle_*()
。
battery service server 提供的另一個(gè)接口用于更新電池電量:
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 內(nèi)部設(shè)計(jì),不是每一個(gè)時(shí)刻都可以發(fā)生數(shù)據(jù)骇扇,需要等待協(xié)議棧內(nèi)部準(zhǔn)備好摔竿,因此這里用 att_server_register_can_send_now_callback()
函數(shù)注冊(cè)了一個(gè)回調(diào)函數(shù),當(dāng)協(xié)議棧內(nèi)部好可以發(fā)送 ATT 數(shù)據(jù)時(shí)會(huì)回調(diào)它:
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);
}
在回調(diào)函數(shù)里利用 att_server_notify()
函數(shù)發(fā)送一個(gè) notify 少孝。
att_server
本小節(jié)從整體角度考慮 att_server.h 里的接口該如何使用继低。
首先通過(guò) att_server_init()
初始化 ATT Server,該函數(shù)可傳入一個(gè)讀回調(diào)函數(shù)稍走,一個(gè)寫回調(diào)函數(shù)袁翁,用于 ATT 讀寫一條 arrtibute 的情況。而 att_server_register_service_handler()
函數(shù)也可以注冊(cè)讀寫回調(diào)函數(shù)婿脸,不過(guò)其優(yōu)先級(jí)更高粱胜,而且只對(duì) [start_handle, end_handle] 之間的 attribute 讀寫有效。
由于 BTstack 并不能在任意時(shí)刻發(fā)送數(shù)據(jù)狐树,因此需要注冊(cè)回調(diào)函數(shù)焙压,等待合適的時(shí)候協(xié)議棧回調(diào)該函數(shù)抑钟,執(zhí)行發(fā)送動(dòng)作涯曲。共有兩種類型的接口:
- 利用 ATT_EVENT_CAN_SEND_NOW 事件的發(fā)送方式。
- 利用注冊(cè)的特定回調(diào)函數(shù)完成發(fā)送在塔。
類型 1 的接口需要通過(guò) att_server_register_packet_handler()
函數(shù)注冊(cè)一個(gè)回調(diào)函數(shù)幻件,在該回調(diào)函數(shù)里判斷是否收到 ATT_EVENT_CAN_SEND_NOW 事件,若收到則進(jìn)行發(fā)送操作蛔溃,例如:
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;
}
}
當(dāng)然配套使用的接口 att_server_request_can_send_now_event()
用于發(fā)送 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()
這些接口注冊(cè)一個(gè)回調(diào)函數(shù)城榛,該函數(shù)回調(diào)時(shí)可以用 att_server_notify()
和 att_server_indicate()
發(fā)送數(shù)據(jù)揪利。
參考: