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}
...

每一行代表一個(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_SERVICECHARACTERISTIC砸泛,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)作涯曲。共有兩種類型的接口:

  1. 利用 ATT_EVENT_CAN_SEND_NOW 事件的發(fā)送方式。
  2. 利用注冊(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ù)揪利。


參考:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狠持,隨后出現(xiàn)的幾起案子疟位,更是在濱河造成了極大的恐慌,老刑警劉巖喘垂,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甜刻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡正勒,警方通過(guò)查閱死者的電腦和手機(jī)得院,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)章贞,“玉大人祥绞,你說(shuō)我怎么就攤上這事。” “怎么了蜕径?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵两踏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我兜喻,道長(zhǎng)梦染,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任朴皆,我火速辦了婚禮帕识,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘遂铡。我一直安慰自己肮疗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布忧便。 她就那樣靜靜地躺著族吻,像睡著了一般帽借。 火紅的嫁衣襯著肌膚如雪珠增。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天砍艾,我揣著相機(jī)與錄音蒂教,去河邊找鬼。 笑死脆荷,一個(gè)胖子當(dāng)著我的面吹牛凝垛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜓谋,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼梦皮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了桃焕?” 一聲冷哼從身側(cè)響起剑肯,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎观堂,沒(méi)想到半個(gè)月后让网,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡师痕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年溃睹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胰坟。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡因篇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惜犀,我是刑警寧澤铛碑,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站虽界,受9級(jí)特大地震影響汽烦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莉御,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一撇吞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧礁叔,春花似錦牍颈、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至涣易,卻和暖如春画机,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背新症。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工步氏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人徒爹。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓荚醒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親隆嗅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子界阁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容