一、簡介
1.1 低功耗藍牙(BLE)協(xié)議棧
鏈路層(LL) 控制設備的射頻狀態(tài)具则,有五個設備狀態(tài):待機、廣播具帮、掃描博肋、初始化和連接。
廣播 為廣播數(shù)據(jù)包蜂厅,而 掃描 則是監(jiān)聽廣播匪凡。
GAP通信中角色,中心設備(Central - 主機) 用來掃描和連接 外圍設備(Peripheral - 從機)掘猿。
大部分情況下外圍設備通過廣播自己來讓中心設備發(fā)現(xiàn)自己病游,并建立 GATT 連接,從而進行更多的數(shù)據(jù)交換稠通。
也有些情況是不需要連接的衬衬,只要外設廣播自己的數(shù)據(jù)即可,用這種方式主要目的是讓外圍設備改橘,把自己的信息發(fā)送給多個中心設備滋尉。
1.2 通用屬性協(xié)議(GATT)
GATT是用Attribute Protocal(屬性協(xié)議)定義的一個service(服務)框架。這個框架定義了Services以及它們的Characteristics的格式和規(guī)程飞主。規(guī)程就是定義了包括發(fā)現(xiàn)狮惜、讀、寫碌识、通知碾篡、指示以及配置廣播的characteristics。
為實現(xiàn)配置文件(Profile)的設備定義了兩種角色:Client(客戶端)筏餐、Server(服務器)开泽。esp32的ble一般就處于Server模式。
一旦兩個設備建立了連接胖烛,GATT就開始發(fā)揮效用眼姐,同時意味著GAP協(xié)議管理的廣播過程結束了。
1.2.1 Profile(規(guī)范)
profile 可以理解為一種規(guī)范佩番,建立的藍牙應用任務众旗,藍牙任務實際上分為兩類:標準藍牙任務規(guī)范 profile(公有任務),非標準藍牙任務規(guī)范 profile(私有任務)趟畏。
標準藍牙任務規(guī)范 profile:指的是從藍牙特別興趣小組 SIG 的官網(wǎng)上已經發(fā)布的 GATT 規(guī)范列表贡歧,包括警告通知(alert notification),血壓測量(blood pressure),心率(heart rate)利朵,電池(battery)等等律想。它們都是針對具體的低功耗藍牙的應用實例來設計的。目前藍牙技術聯(lián)盟還在不斷的制定新的規(guī)范绍弟,并且發(fā)布技即。
非標準藍牙任務規(guī)范 profile:指的是供應商自定義的任務,在藍牙 SIG 小組內未定義的任務規(guī)范樟遣。
1.2.2 Service(服務)
service 可以理解為一個服務而叼,在 BLE 從機中有多個服務,例如:電量信息服務豹悬、系統(tǒng)信息服務等葵陵;
每個 service 中又包含多個 characteristic 特征值;
每個具體的 characteristic 特征值才是 BLE 通信的主題瞻佛,比如當前的電量是 80%脱篙,電量的 characteristic 特征值存在從機的 profile 里,這樣主機就可以通過這個 characteristic 來讀取 80% 這個數(shù)據(jù)伤柄。
GATT 服務一般包含幾個具有相關的功能绊困,比如特定傳感器的讀取和設置,人機接口的輸入輸出响迂。組織具有相關的特性到服務中既實用又有效考抄,因為它使得邏輯上和用戶數(shù)據(jù)上的邊界變得更加清晰,同時它也有助于不同應用程序間代碼的重用蔗彤。
1.2.3 Characteristic(特征)
characteristic 特征川梅,BLE 主從機的通信均是通過 characteristic 來實現(xiàn),可以理解為一個標簽然遏,通過這個標簽可以獲取或者寫入想要的內容贫途。
1.2.4 UUID(通用唯一識別碼)
uuid 通用唯一識別碼,我們剛才提到的 service 和 characteristic 都需要一個唯一的 uuid 來標識待侵;
每個從機都會有一個 profile丢早,不管是自定義的 simpleprofile,還是標準的防丟器 profile秧倾,他們都是由一些 service 組成怨酝,每個 service 又包含了多個 characteristic,主機和從機之間的通信那先,均是通過characteristic來實現(xiàn)农猬。
1.3 ESP32藍牙應用結構
藍牙是?種短距通信系統(tǒng),其關鍵特性包括魯棒性售淡、低功耗斤葱、低成本等慷垮。藍牙系統(tǒng)分為兩種不同的技術:經典藍牙 (Classic Bluetooth) 和藍牙低功耗 (Bluetooth Low Energy)。
ESP32 支持雙模藍牙揍堕,即同時支持經典藍牙和藍牙低功耗闸迷。
從整體結構上犯戏,藍牙可分為控制器 (Controller) 和主機 (Host) 兩?部分:控制器包括了 PHY、Baseband变姨、Link Controller墓臭、Link Manager采郎、Device Manager粟誓、HCI 等模塊枷邪,用于硬件接?管理、鏈路管理等等抖部;主機則包括了 L2CAP、SMP议惰、SDP慎颗、ATT、GATT言询、GAP 以及各種規(guī)范俯萎,構建了向應用層提供接口的基礎,方便應用層對藍牙系統(tǒng)的訪問运杭。主機可以與控制器運行在同?個宿主上夫啊,也可以分布在不同的宿主上。ESP32 可以支持上述兩種方式辆憔。
1.4 Bluedroid主機架構
在 ESP-IDF 中撇眯,使用經過大量修改后的 BLUEDROID 作為藍牙主機 (Classic BT + BLE)。BLUEDROID 擁有較為完善的功能虱咧,?持常用的規(guī)范和架構設計熊榛,同時也較為復雜。經過大量修改后腕巡,BLUEDROID 保留了大多數(shù) BTA 層以下的代碼玄坦,幾乎完全刪去了 BTIF 層的代碼,使用了較為精簡的 BTC 層作為內置規(guī)范及 Misc 控制層绘沉。修改后的 BLUEDROID 及其與控制器之間的關系如下圖:
1.5 ESP32的GATT服務器服務表示例
使用類似表格的數(shù)據(jù)結構來定義服務器服務和特性煎楣,因此,它展示了一種定義服務器的實用方法功能集中在一處车伞,而不是一一添加服務和特性择懂。
二、API說明
以下控制器和虛擬 HCI 接口位于 bt/include/esp32/include/esp_bt.h帖世。
2.1 esp_bt_controller_mem_release
2.2 esp_bt_controller_init
2.3 esp_bt_controller_enable
以下 GATT 接口位于 bt/host/bluedroid/api/include/api/esp_bt_main.h 和 bt/host/bluedroid/api/include/api/esp_gatts_api.h休蟹。
2.4 esp_bluedroid_init
2.5 esp_bluedroid_enable
2.6 esp_ble_gatts_register_callback
2.7 esp_ble_gatts_app_register
2.8 esp_ble_gatts_create_service
2.9 esp_ble_gatts_add_char
2.10 esp_ble_gatts_add_char_descr
2.11 esp_ble_gatts_start_service
2.12 esp_ble_gatts_send_indicate
2.13 esp_ble_gatts_send_response
2.14 esp_ble_gatts_get_attr_value
三沸枯、藍牙4.0通信實現(xiàn)過程
- 掃描藍牙BLE終端設備,對應esp32就是廣播給大家供掃描
- 連接藍牙BLE終端設備赂弓,pad掃描到后去連接
- 啟動服務發(fā)現(xiàn)绑榴,連接到esp32后獲取相應的服務。
連接成功后盈魁,我們就要去尋找我們所需要的服務翔怎,這里需要先啟動服務發(fā)現(xiàn)。 - 獲取Characteristic
之前我們說過杨耙,我們的最終目的就是獲取Characteristic來進行通信赤套,正常情況下,我們可以從硬件工程師那邊得到serviceUUID和characteristicUUID珊膜,也就是我們所比喻的班級號和學號容握,以此來獲得我們的characteristic。 - 開始通信
我們在得到Characteristic后车柠,就可以開始讀寫操作進行通信了剔氏。
a. 對于讀操作來說,讀取BLE終端設備返回的數(shù)據(jù)會通過回調方法mGattCallback中的onCharacteristicChanged函數(shù)返回竹祷。
b. 對于寫操作來說谈跛,可以通過向Characteristic寫入指令以此來達到控制BLE終端設備的目的
四、Demo程序GATT啟動流程
使用 esp-idf\examples\bluetooth\bluedroid\ble\gatt_server_service_table 中的例程
.........
//esp_bt_controller_config_t是藍牙控制器配置結構體塑陵,這里使用了一個默認的參數(shù)
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
//初始化藍牙控制器感憾,此函數(shù)只能被調用一次,且必須在其他藍牙功能被調用之前調用
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
//使能藍牙控制器令花,mode是藍牙模式阻桅,如果想要動態(tài)改變藍牙模式不能直接調用該函數(shù),
//應該先用disable關閉藍牙再使用該API來改變藍牙模式
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
//初始化藍牙并分配系統(tǒng)資源彭则,它應該被第一個調用
/*
藍牙棧bluedroid stack包括了BT和BLE使用的基本的define和API
初始化藍牙棧以后并不能直接使用藍牙功能鳍刷,
還需要用FSM管理藍牙連接情況
*/
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
//使能藍牙棧
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
//建立藍牙的FSM(有限狀態(tài)機)
//這里使用回調函數(shù)來控制每個狀態(tài)下的響應,需要將其在GATT和GAP層的回調函數(shù)注冊
/*gatts_event_handler和gap_event_handler處理藍牙椄┒叮可能發(fā)生的所有情況输瓜,達到FSM的效果*/
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
return;
}
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
return;
}
//下面創(chuàng)建了BLE GATT服務A,相當于1個獨立的應用程序
ret = esp_ble_gatts_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
//下面創(chuàng)建了BLE GATT服務B芬萍,相當于1個獨立的應用程序
ret = esp_ble_gatts_app_register(PROFILE_B_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
/*
設置了MTU的值(經過MTU交換尤揣,從而設置一個PDU中最大能夠交換的數(shù)據(jù)量)。
例如:主設備發(fā)出一個1000字節(jié)的MTU請求柬祠,但是從設備回應的MTU是500字節(jié)北戏,那么今后雙方要以較小的值500字節(jié)作為以后的MTU。
即主從雙方每次在做數(shù)據(jù)傳輸時不超過這個最大數(shù)據(jù)單元漫蛔。
*/
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
if (local_mtu_ret){
ESP_LOGE(GATTS_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
}
.......
五嗜愈、服務數(shù)據(jù)結構體設置
一個GATT 服務器應用程序架構(由Application Profiles組織起來)如下:
每個Profile定義為一個結構體旧蛾,結構體成員依賴于該Application Profile 實現(xiàn)的services服務和characteristic特征。結構體成員還包括GATT interface(GATT 接口)蠕嫁、Application ID(應用程序ID)和處理profile事件的回調函數(shù)锨天。
每個profile包括GATT interface(GATT 接口)、Application ID(應用程序ID)剃毒、 Connection ID(連接ID)病袄、Service Handle(服務句柄)、Service ID(服務ID)赘阀、Characteristic handle(特征句柄)益缠、Characteristic UUID(特征UUID)、ATT權限基公、Characteristic Properties幅慌、描述符句柄、描述符UUID轰豆。
如果Characteristic支持通知(notifications)或指示(indicatons)欠痴,它就必須是實現(xiàn)CCCD(Client Characteristic Configuration Descriptor)----這是額外的ATT。描述符有一個句柄和UUID秒咨。如:
struct gatts_profile_inst {
esp_gatts_cb_t gatts_cb; //GATT的回調函數(shù)
uint16_t gatts_if; //GATT的接口
uint16_t app_id; //應用的ID
uint16_t conn_id; //連接的ID
uint16_t service_handle; //服務Service句柄
esp_gatt_srvc_id_t service_id; //服務Service ID
uint16_t char_handle; //特征Characteristic句柄
esp_bt_uuid_t char_uuid; //特征Characteristic的UUID
esp_gatt_perm_t perm; //特征屬性Attribute 授權
esp_gatt_char_prop_t property; //特征Characteristic的特性
uint16_t descr_handle; //描述descriptor句柄
esp_bt_uuid_t descr_uuid; //描述descriptorUUID
};
配置文件Application Profile存儲在heart_rate_profile_tab
數(shù)組中,由于本示例中只有一個配置文件掌挚,因此一個元素存儲在數(shù)組中雨席,索引為零,如HEART_PROFILE_APP_IDX
吠式。此外陡厘,還初始化了配置文件事件處理程序回調函數(shù)gatts_profile_event_handler
。GATT 服務端上的不同應用程序使用不同的接口特占,由 gatts_if 參數(shù)表示糙置。對于初始化,此參數(shù)設置為ESP_GATT_IF_NONE
是目,這意味著應用程序配置文件尚未鏈接到任何客戶端谤饭。
/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
[PROFILE_B_APP_ID] = {
.gatts_cb = gatts_profile_b_event_handler, /* This demo does not implement, similar as profile A */
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
};
這是兩個元素的數(shù)組“媚桑可以用Application ID來注冊Application Profiles揉抵,Application ID是由應用程序分配的用來標識每個Profile。 通過這種方法嗤疯,可以在一個Server中擁有多個Application Profile冤今。
esp_ble_gatts_app_register (PROFILE_A_APP_ID);
esp_ble_gatts_app_register (PROFILE_B_APP_ID);
六、GATT事件處理程序
其作用就是建立了藍牙GATT的FSM(有限狀態(tài)機)茂缚,callback回調函數(shù)處理從BLE堆棧推送到應用程序的所有事件戏罢。
回調函數(shù)的參數(shù):
- event: esp_gatts_cb_event_t 這是一個枚舉類型屋谭,表示調用該回調函數(shù)時的事件(或藍牙的狀態(tài))
-
gatts_if: esp_gatt_if_t (uint8_t) 這是GATT訪問接口類型,通常在GATT客戶端上不同的應用程序用不同的gatt_if(不同的Application profile對應不同的gatts_if) 龟糕,調用
esp_ble_gatts_app_register()
時桐磁,注冊Application profile 就會有一個gatts_if。 - param: esp_ble_gatts_cb_param_t 指向回調函數(shù)的參數(shù)翩蘸,是個聯(lián)合體類型所意,不同的事件類型采用聯(lián)合體內不同的成員結構體。
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
/*如果事件是注冊事件催首,則為每個配置文件存儲 gatts_if */
if (event == ESP_GATTS_REG_EVT) {
if (param->reg.status == ESP_GATT_OK) {
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
} else {
ESP_LOGI(GATTS_TAG, "Reg app failed, app_id %04x, status %d\n",
param->reg.app_id,
param->reg.status);
return;
}
}
/*如果 gatts_if 等于 profile A扶踊,則調用 profile A cb handler,
* 所以這里調用每個 profile 的回調*/
do {
int idx;
for (idx = 0; idx < PROFILE_NUM; idx++) {
if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
gatts_if == gl_profile_tab[idx].gatts_if) {
if (gl_profile_tab[idx].gatts_cb) {
gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
}
}
}
} while (0);
}
七郎任、注冊創(chuàng)建服務
當調用esp_ble_gatts_app_register()
注冊一個應用程序Profile(Application Profile)秧耗,將觸發(fā)ESP_GATTS_REG_EVT
事件,除了可以完成對應profile的gatts_if的注冊,還可以調用esp_bel_create_attr_tab()
來創(chuàng)建profile Attributes 表或創(chuàng)建一個服務esp_ble_gatts_create_service()
舶治。
static void gatts_profile_b_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_B_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_B);
break;
…
}
句柄數(shù)定義為4:
#define GATTS_NUM_HANDLE_TEST_B 4
句柄是:
- 服務句柄
GATTS_SERVICE_UUID_TEST_B 0x00EE
- 特征手柄
GATTS_CHAR_UUID_TEST_B 0xEE01
- 特征值句柄
- 特征描述符句柄
GATTS_DESCR_UUID_TEST_B 0x2222
該服務被定義為具有 16 位 UUID 長度的主要服務分井。服務 ID 使用實例 ID = 0 和由 定義的 UUID 進行初始化GATTS_SERVICE_UUID_TEST_A。
服務實例 ID 可用于區(qū)分具有相同 UUID 的多個服務霉猛。在此示例中尺锚,由于每個應用程序配置文件只有一個服務并且服務具有不同的 UUID,因此在配置文件 A 和 B 中可以將服務實例 ID 定義為 0惜浅。但是瘫辩,如果只有一個應用程序配置文件具有兩個服務使用相同的 UUID,則有必要使用不同的實例 ID 來引用一個或另一個服務坛悉。
demo中的gatts_event_handler()
回調函數(shù)—調用esp_ble_gatts_app_register()
伐厌,觸發(fā)ESP_GATTS_REG_EVT
時,完成對每個profile 的gatts_if 的注冊裸影。
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
如果gatts_if == 某個Profile的gatts_if時挣轨,調用對應profile的回調函數(shù)處理事情。
if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
gatts_if == gl_profile_tab[idx].gatts_if) {
if (gl_profile_tab[idx].gatts_cb) {
gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
}
}
八轩猩、啟動服務和創(chuàng)建特征
8.1 啟動服務
當一個服務service創(chuàng)建成功后卷扮,由該profile GATT handler 管理的 ESP_GATTS_CREATE_EVT
事件被觸發(fā),在這個事件可以啟動服務和添加特征characteristics到服務中界轩。調用esp_ble_gatts_start_service()
來啟動指定服務画饥。
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d\n", param->create.status, param->create.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].service_handle = param->create.service_handle;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;
esp_ble_gatts_start_service(gl_profile_tab[PROFILE_A_APP_ID].service_handle);
a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_err_t add_char_ret =
esp_ble_gatts_add_char(gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].char_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
a_property,
&gatts_demo_char1_val,
NULL);
if (add_char_ret){
ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
}
break;
首先,由BLE堆棧生成生成的服務句柄(service handle)存儲在配置文件Profile表中浊猾,應用層將用服務句柄來引用這個服務抖甘。調用esp_ble_gatts_start_service()
和先前產生服務句柄來啟動服務。
8.2 創(chuàng)建特征
Characteristic是在GATT規(guī)范中最小的邏輯數(shù)據(jù)單元葫慎,由一個Value和多個描述特性的Desciptior組成衔彻。實際上薇宠,在與藍牙設備打交道,主要就是讀寫Characteristic的value來完成艰额。 同樣的澄港,Characteristic也是通過16bit或128bit的UUID唯一標識。
我們根據(jù)藍牙設備的協(xié)議用對應的Characteristci進行讀寫即可達到與其通信的目的柄沮。
添加特征到service中回梧,調用esp_ble_gatts_add_char()
來添加characteristics連同characteristic權限和property(屬性)到服務service中。
權限:
-
ESP_GATT_PERM_READ
: 允許讀取特征值 -
ESP_GATT_PERM_WRITE
: 允許寫入特征值
特性:
-
ESP_GATT_CHAR_PROP_BIT_READ
: 可以讀取特性 -
ESP_GATT_CHAR_PROP_BIT_WRITE
: 特征可寫 -
ESP_GATT_CHAR_PROP_BIT_NOTIFY
: 特性可以通知值的變化
同時擁有讀寫權限和屬性似乎是多余的祖搓。但是狱意,屬性的讀寫屬性是向客戶端顯示的信息,目的是讓客戶端知道服務器是否接受讀寫請求拯欧。從這個意義上說详囤,這些屬性充當客戶端正確訪問服務器資源的提示。另一方面镐作,權限是授予客戶端讀取或寫入該屬性的授權藏姐。例如,如果客戶端嘗試寫入它沒有寫入權限的屬性该贾,即使設置了寫入屬性羔杨,服務器也會拒絕該請求。
此外杨蛋,demo還為表示特征提供了一個初始值gatts_demo_char1_val问畅。初始值定義如下:
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40
uint8_t char1_str[] = {0x11,0x22,0x33};
esp_attr_value_t gatts_demo_char1_val =
{
. attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX,
. attr_len = sizeof (char1_str),
. attr_value = char1_str,
};
特征初始值必須是非空對象并且特征長度必須始終大于零,否則堆棧將返回錯誤六荒。
最后,特性被配置為每次讀取或寫入特性時都需要手動發(fā)送響應矾端,而不是讓堆棧自動響應掏击。這是通過將esp_ble_gatts_add_char()
函數(shù)的最后一個參數(shù)(表示屬性響應控制參數(shù))設置為ESP_GATT_RSP_BY_APP
或 NULL 來配置的。
七秩铆、創(chuàng)建特征描述符
當特征添加到service中成功時砚亭,觸發(fā)ESP_GATTS_ADD_CHAR_EVT
事件。該事件返回由堆棧為剛剛添加的特征生成的句柄殴玛。該事件包括以下參數(shù):
esp_gatt_status_t狀態(tài)捅膘; /* !< 操作狀態(tài)*/
uint16_t attr_handle; /* !< 特征屬性句柄*/
uint16_t service_handle; /* !< 服務屬性句柄*/
esp_bt_uuid_t char_uuid; /* !< 特征 uuid */
事件返回的屬性句柄存儲在配置文件表中,并且還設置了特征描述符長度和 UUID滚粟。使用該esp_ble_gatts_get_attr_value()
函數(shù)讀取特征長度和值寻仗,然后打印以供參考。最后凡壤,使用該esp_ble_gatts_add_char_descr()
函數(shù)添加特征描述署尤。使用的參數(shù)是服務句柄耙替、描述符 UUID、寫入和讀取權限曹体、初始值和自動響應設置俗扇。特征描述符的初始值可以是空指針,自動響應參數(shù)也設置為空箕别,這意味著需要響應的請求必須手動回復铜幽。
case ESP_GATTS_ADD_CHAR_EVT: {
uint16_t length = 0;
const uint8_t *prf_char;
ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].char_handle = param->add_char.attr_handle;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
esp_err_t get_attr_ret = esp_ble_gatts_get_attr_value(param->add_char.attr_handle, &length, &prf_char);
if (get_attr_ret == ESP_FAIL){
ESP_LOGE(GATTS_TAG, "ILLEGAL HANDLE");
}
ESP_LOGI(GATTS_TAG, "the gatts demo char length = %x\n", length);
for(int i = 0; i < length; i++){
ESP_LOGI(GATTS_TAG, "prf_char[%x] = %x\n",i,prf_char[i]);
}
esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(
gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].descr_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
NULL,NULL);
if (add_descr_ret){
ESP_LOGE(GATTS_TAG, "add char descr failed, error code = %x", add_descr_ret);
}
break;
}
添加描述符后,將ESP_GATTS_ADD_CHAR_DESCR_EVT
觸發(fā)事件串稀,在此示例中用于打印信息消息除抛。
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle,
param->add_char.service_handle);
break;
九、連接事件
9.1 更新連接參數(shù)
一個ESP_GATTS_CONNECT_EVT
當客戶端已連接到服務器GATT被觸發(fā)厨诸。此事件用于更新連接參數(shù)镶殷,例如延遲、最小連接間隔微酬、最大連接間隔和超時绘趋。連接參數(shù)存儲在一個esp_ble_conn_update_params_t
結構中,然后傳遞給esp_ble_gap_update_conn_params()
函數(shù)颗管。更新連接參數(shù)過程只需執(zhí)行一次陷遮,因此配置文件 B 連接事件處理程序不包含該esp_ble_gap_update_conn_params()
函數(shù)。最后垦江,事件返回的連接 ID 存儲在配置文件表中帽馋。
配置文件 A 連接事件:
case ESP_GATTS_CONNECT_EVT: {
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
/* For the IOS system, please reference the apple official documents about the ble connection parameters restrictions. */
conn_params.latency = 0;
conn_params.max_int = 0x30; // max_int = 0x30*1.25ms = 40ms
conn_params.min_int = 0x10; // min_int = 0x10*1.25ms = 20ms
conn_params.timeout = 400; // timeout = 400*10ms = 4000ms
ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
//start sent the update connection parameters to the peer device.
esp_ble_gap_update_conn_params(&conn_params);
break;
}
配置文件 B 連接事件:
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(GATTS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d\n",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_B_APP_ID].conn_id = param->connect.conn_id;
break;
該esp_ble_gap_update_conn_params()
函數(shù)觸發(fā)一個 GAP 事件ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
,用于打印連接信息:
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
ESP_LOGI(GATTS_TAG, "update connection params status = %d, min_int = %d, max_int = %d,
conn_int = %d,latency = %d, timeout = %d",
param->update_conn_params.status,
param->update_conn_params.min_int,
param->update_conn_params.max_int,
param->update_conn_params.conn_int,
param->update_conn_params.latency,
param->update_conn_params.timeout);
break;
9.2 確定MTU大小
當有手機(client客戶端)連上server時比吭,觸發(fā)ESP_GATTS_MTU_EVT事件绽族,其打印如下圖所示
ESP_GATTS_MTU_EVT事件對應的回調函數(shù)中參數(shù)param的結構體為gatts_mtu_evt_param(包括連接id和MTU大小)
/**
* @brief ESP_GATTS_MTU_EVT
*/
struct gatts_mtu_evt_param {
uint16_t conn_id; /*!< Connection id */
uint16_t mtu; /*!< MTU size */
} mtu; /*!< Gatt server callback param of ESP_GATTS_MTU_EVT */
在例子中設置本地的MTU大小為500衩藤,代碼如下所示:
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
如上所述吧慢,設置了MTU的值(經過MTU交換,從而設置一個PDU中最大能夠交換的數(shù)據(jù)量)赏表。例如:主設備發(fā)出一個150字節(jié)的MTU請求检诗,但是從設備回應的MTU是23字節(jié),那么今后雙方要以較小的值23字節(jié)作為以后的MTU瓢剿。即主從雙方每次在做數(shù)據(jù)傳輸時不超過這個最大數(shù)據(jù)單元逢慌。 MTU交換通常發(fā)生在主從雙方建立連接后。MTU比較小间狂,就是為什么BLE不能傳輸大數(shù)據(jù)的原因所在攻泼。
參照一分鐘讀懂低功耗(BLE)MTU交換數(shù)據(jù)包 這篇文章就可以了解MTU交換過程。
MTU交換請求用于client通知server關于client最大接收MTU大小并請求server響應它的最大接收MTU大小。
Client的接收MTU 應該大于或等于默認ATT_MTU(23).這個請求已建立連接就由client發(fā)出坠韩。這個Client Rx MTU參數(shù)應該設置為client可以接收的attribute protocol PDU最大尺寸距潘。
MTU交換應答發(fā)送用于接收到一個Exchange MTU請求
這個應答由server發(fā)出,server的接收MTU必須大于或等于默認ATT_MTU大小只搁。這里的Server Rx MTU應該設置為 服務器可以接收的attribute protocol PDU 最大尺寸音比。
Server和Client應該設置ATT_MTU為Client Rx MTU和Server Rx MTU兩者的較小值。
這個ATT_MTU在server在發(fā)出這個應答后氢惋,在發(fā)其他屬性協(xié)議PDU之前生效洞翩;在client收到這個應答并在發(fā)其他屬性協(xié)議PDU之前生效。
十焰望、管理讀取事件
現(xiàn)在已經創(chuàng)建并啟動了服務和特征骚亿,程序可以接收讀寫事件。讀取操作由ESP_GATTS_READ_EVT
事件表示熊赖,它具有以下參數(shù):
uint16_t conn_id; /* !< 連接 ID */
uint32_t trans_id; /* !< 傳輸 ID */
esp_bd_addr_t bda; /* !< 讀取的藍牙設備地址*/
uint16_t handle; /* !< 屬性句柄*/
uint16_t offset; /* !< 值的偏移量来屠,如果值太長*/
bool is_long; /* !< 值是否過長*/
bool need_rsp; /*!<讀操作需要做響應*/
demo中,響應是用虛擬數(shù)據(jù)構造的震鹉,并使用事件給定的相同句柄發(fā)送回主機俱笛。除了響應之外,GATT 接口传趾、連接 ID 和傳輸 ID 也作為參數(shù)包含在esp_ble_gatts_send_response()
函數(shù)中迎膜。如果在創(chuàng)建特征或描述符時將自動響應字節(jié)設置為 NULL,則此功能是必需的浆兰。
case ESP_GATTS_READ_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %d, handle %d\n",
param->read.conn_id, param->read.trans_id, param->read.handle);
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = 4;
rsp.attr_value.value[0] = 0xde;
rsp.attr_value.value[1] = 0xed;
rsp.attr_value.value[2] = 0xbe;
rsp.attr_value.value[3] = 0xef;
esp_ble_gatts_send_response(gatts_if,
param->read.conn_id,
param->read.trans_id,
ESP_GATT_OK, &rsp);
break;
}
十一磕仅、管理寫入事件
寫入事件由事件表示ESP_GATTS_WRITE_EVT
,它具有以下參數(shù):
uint16_t conn_id; /* !< 連接 ID */
uint32_t trans_id; /* !< 傳輸 ID */
esp_bd_addr_t bda; /* !< 寫入的藍牙設備地址*/
uint16_t handle; /* !< 屬性句柄*/
uint16_t offset; /* !< 值的偏移量簸呈,如果值太長*/
bool need_rsp; /* !< 寫操作需要做響應*/
bool is_prep; /*!< 這個寫操作是prepare write */
uint16_t len; /* !< 寫入屬性值長度*/
uint8_t *value; /* !< 寫入屬性值*/
demo中實現(xiàn)了兩種類型的寫事件榕订,寫特征值和寫長特征值。當特征值可以容納在一個屬性協(xié)議最大傳輸單元 (ATT MTU) 中時蜕便,使用第一種類型的寫入卸亮,該單元通常為 23 字節(jié)長。當要寫入的屬性長于單個 ATT 消息中可以發(fā)送的屬性時使用第二種類型玩裙,通過使用準備寫入響應將數(shù)據(jù)分成多個塊,然后使用執(zhí)行寫入請求來確認或取消完整的寫入請求. 此行為在藍牙規(guī)范版本 4.2段直,第 3 卷吃溅,G 部分,第 4.9 節(jié)中定義鸯檬。寫長特征消息流如下圖所示决侈。
當觸發(fā)寫入事件時,此示例打印日志消息,然后執(zhí)行example_write_event_env()
函數(shù)赖歌。
case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %d, handle %d\n", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
if (gl_profile_tab[PROFILE_B_APP_ID].descr_handle == param->write.handle && param->write.len == 2){
uint16_t descr_value= param->write.value[1]<<8 | param->write.value[0];
if (descr_value == 0x0001){
if (b_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY){
ESP_LOGI(GATTS_TAG, "notify enable");
uint8_t notify_data[15];
for (int i = 0; i < sizeof(notify_data); ++i)
{
notify_data[i] = i%0xff;
}
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(notify_data),
notify_data, false);
}
}else if (descr_value == 0x0002){
if (b_property & ESP_GATT_CHAR_PROP_BIT_INDICATE){
ESP_LOGI(GATTS_TAG, "indicate enable");
uint8_t indicate_data[15];
for (int i = 0; i < sizeof(indicate_data); ++i)
{
indicate_data[i] = i % 0xff;
}
//the size of indicate_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(indicate_data),
indicate_data, true);
}
}
else if (descr_value == 0x0000){
ESP_LOGI(GATTS_TAG, "notify/indicate disable ");
}else{
ESP_LOGE(GATTS_TAG, "unknown value");
}
}
}
example_write_event_env(gatts_if, &a_prepare_write_env, param);
break;
}
該example_write_event_env()
函數(shù)包含寫長特征過程的邏輯:
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
if (param->write.is_prep){
if (prepare_write_env->prepare_buf == NULL){
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem\n");
status = ESP_GATT_NO_RESOURCES;
}
} else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
}
}
當客戶端發(fā)送寫請求或準備寫請求時枉圃,服務器應響應。但是庐冯,如果客戶端發(fā)送 Write without Response 命令孽亲,則服務器不需要回復響應。這是在寫入過程中通過檢查 的值來檢查的write.need_rsp parameter
展父。如果需要響應返劲,程序繼續(xù)做響應準備,如果不存在栖茉,客戶端不需要響應篮绿,因此程序結束。響應的話會影響數(shù)據(jù)傳輸速度吕漂,在需要大數(shù)據(jù)量的場合是否合適需要試驗亲配?
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env,
esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
…
然后該函數(shù)檢查是否write.is_prep
設置了由 表示的 Prepare Write Request 參數(shù),這意味著客戶端正在請求 Write Long Characteristic惶凝。如果存在吼虎,該過程繼續(xù)準備多個寫響應,如果不存在梨睁,則服務器簡單地發(fā)回單個寫響應鲸睛。
…
if (param->write.is_prep){
…
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
…
為了處理長特征寫入,定義并實例化了一個準備緩沖區(qū)結構:
typedef struct {
uint8_t *prepare_buf;
int prepare_len;
} prepare_type_env_t;
static prepare_type_env_t a_prepare_write_env;
static prepare_type_env_t b_prepare_write_env;
為了使用準備緩沖區(qū)坡贺,為其分配了一些內存空間官辈。如果由于內存不足導致分配失敗,則會打印錯誤:
else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
該過程現(xiàn)在準備esp_gatt_rsp_t
要發(fā)送回客戶端的類型響應遍坟。它使用寫入請求的相同參數(shù)構造的響應拳亿,例如長度、句柄和偏移量愿伴。另外肺魁,寫入該特性所需的GATT認證類型設置為ESP_GATT_AUTH_REQ_NONE
,這意味著客戶端可以寫入該特性而無需先進行身份驗證隔节。一旦發(fā)送響應鹅经,分配給它使用的內存就會被釋放。
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
最后怎诫,傳入的數(shù)據(jù)被復制到創(chuàng)建的緩沖區(qū)中瘾晃,其長度按偏移量遞增:
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGI(GATTS_TAG,"ESP_GATTS_EXEC_WRITE_EVT");
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
example_exec_write_event_env(&a_prepare_write_env, param);
break;
我們來看看Executive Write函數(shù):
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
#### prepare_write_env->prepare_len = 0;
}
執(zhí)行寫入用于確認或取消之前完成的寫入過程,由長特征寫入過程幻妓。為此蹦误,該函數(shù)會檢查exec_write_flag
隨事件接收到的參數(shù)中的 。如果標志等于 表示的執(zhí)行標志exec_write_flag
,則確認寫入并在日志中打印緩沖區(qū)强胰;如果不是舱沧,則表示取消寫入并刪除所有已寫入的數(shù)據(jù)。
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG,
prepare_write_env->prepare_buf,
prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
最后偶洋,為存儲來自長寫操作的數(shù)據(jù)塊而創(chuàng)建的緩沖區(qū)結構被釋放熟吏,并將其指針設置為 NULL 以使其為下一個長寫過程做好準備。
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
11.1 使能通知
使能notify并讀取藍牙發(fā)過來的數(shù)據(jù)涡真,開啟這個后我們就能實時獲取藍牙發(fā)過來的值了分俯。
使能通知(notify enable)的打印如下所示,打開通知實際上的一個WRITE哆料。
如果write.handle和descr_handle相同缸剪,且長度==2,確定descr_value描述值东亦,根據(jù)描述值開啟/關閉 通知notify/indicate杏节。
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,
sizeof(notify_data), notify_data, false);
該函數(shù)將notify或indicate發(fā)給GATT的客戶端;
need_confirm = false,則發(fā)送的是notification通知典阵;
==true奋渔,發(fā)送的是指示indication。
其他參數(shù): 服務端訪問接口壮啊;連接id嫉鲸; 屬性句柄,value_len; 值
? 由 Leung 寫于 2021 年 7 月 7 日