ESP32學習筆記(30)——BLE GATT服務端自定義服務和特徵

語言: CN / TW / HK

highlight: vs2015

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第30天,點選檢視活動詳情

一、簡介

1.1 低功耗藍芽(BLE)協議棧

鏈路層(LL) 控制裝置的射頻狀態,有五個裝置狀態:待機、廣播、掃描、初始化和連線。

廣播 為廣播資料包,而 掃描 則是監聽廣播。

GAP通訊中角色,中心裝置(Central - 主機) 用來掃描和連線 外圍裝置(Peripheral - 從機)

大部分情況下外圍裝置通過廣播自己來讓中心裝置發現自己,並建立 GATT 連線,從而進行更多的資料交換。

也有些情況是不需要連線的,只要外設廣播自己的資料即可,用這種方式主要目的是讓外圍裝置,把自己的資訊傳送給多箇中心裝置。

1.2 通用屬性協議(GATT)

GATT是用Attribute Protocal(屬性協議)定義的一個service(服務)框架。這個框架定義了Services以及它們的Characteristics的格式和規程。規程就是定義了包括髮現、讀、寫、通知、指示以及配置廣播的characteristics。

為實現配置檔案(Profile)的裝置定義了兩種角色:Client(客戶端)、Server(伺服器)。esp32的ble一般就處於Server模式。

一旦兩個裝置建立了連線,GATT就開始發揮效用,同時意味著GAP協議管理的廣播過程結束了。

1.2.1 Profile(規範)

profile 可以理解為一種規範,建立的藍芽應用任務,藍芽任務實際上分為兩類:標準藍芽任務規範 profile(公有任務),非標準藍芽任務規範 profile(私有任務)。

  • 標準藍芽任務規範 profile:指的是從藍芽特別興趣小組 SIG 的官網上已經發布的 GATT 規範列表,包括警告通知(alert notification),血壓測量(blood pressure),心率(heart rate),電池(battery)等等。它們都是針對具體的低功耗藍芽的應用例項來設計的。目前藍芽技術聯盟還在不斷的制定新的規範,並且釋出。

  • 非標準藍芽任務規範 profile:指的是供應商自定義的任務,在藍芽 SIG 小組內未定義的任務規範。

1.2.2 Service(服務)

service 可以理解為一個服務,在 BLE 從機中有多個服務,例如:電量資訊服務、系統資訊服務等; 每個 service 中又包含多個 characteristic 特徵值; 每個具體的 characteristic 特徵值才是 BLE 通訊的主題,比如當前的電量是 80%,電量的 characteristic 特徵值存在從機的 profile 裡,這樣主機就可以通過這個 characteristic 來讀取 80% 這個資料。 GATT 服務一般包含幾個具有相關的功能,比如特定感測器的讀取和設定,人機介面的輸入輸出。組織具有相關的特性到服務中既實用又有效,因為它使得邏輯上和使用者資料上的邊界變得更加清晰,同時它也有助於不同應用程式間程式碼的重用。

1.2.3 Characteristic(特徵)

characteristic 特徵,BLE 主從機的通訊均是通過 characteristic 來實現,可以理解為一個標籤,通過這個標籤可以獲取或者寫入想要的內容。

1.2.4 UUID(通用唯一識別碼)

uuid 通用唯一識別碼,我們剛才提到的 service 和 characteristic 都需要一個唯一的 uuid 來標識; 每個從機都會有一個 profile,不管是自定義的 simpleprofile,還是標準的防丟器 profile,他們都是由一些 service 組成,每個 service 又包含了多個 characteristic,主機和從機之間的通訊,均是通過characteristic來實現。

1.3 ESP32藍芽應用結構

藍芽是⼀種短距通訊系統,其關鍵特性包括魯棒性、低功耗、低成本等。藍芽系統分為兩種不同的技術:經典藍芽 (Classic Bluetooth) 和藍芽低功耗 (Bluetooth Low Energy)。 ESP32 支援雙模藍芽,即同時支援經典藍芽和藍芽低功耗。

從整體結構上,藍芽可分為控制器 (Controller) 和主機 (Host) 兩⼤部分:控制器包括了 PHY、Baseband、Link Controller、Link Manager、Device Manager、HCI 等模組,用於硬體接⼝管理、鏈路管理等等;主機則包括了 L2CAP、SMP、SDP、ATT、GATT、GAP 以及各種規範,構建了嚮應用層提供介面的基礎,方便應用層對藍芽系統的訪問。主機可以與控制器執行在同⼀個宿主上,也可以分佈在不同的宿主上。ESP32 可以支援上述兩種方式。

1.4 Bluedroid主機架構

在 ESP-IDF 中,使用經過大量修改後的 BLUEDROID 作為藍芽主機 (Classic BT + BLE)。BLUEDROID 擁有較為完善的功能,⽀持常用的規範和架構設計,同時也較為複雜。經過大量修改後,BLUEDROID 保留了大多數 BTA 層以下的程式碼,幾乎完全刪去了 BTIF 層的程式碼,使用了較為精簡的 BTC 層作為內建規範及 Misc 控制層。修改後的 BLUEDROID 及其與控制器之間的關係如下圖:

二、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.hbt/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通訊實現過程

  1. 掃描藍芽BLE終端裝置,對應esp32就是廣播給大家供掃描
  2. 連線藍芽BLE終端裝置,pad掃描到後去連線
  3. 啟動服務發現,連線到esp32後獲取相應的服務。 連線成功後,我們就要去尋找我們所需要的服務,這裡需要先啟動服務發現。
  4. 獲取Characteristic 之前我們說過,我們的最終目的就是獲取Characteristic來進行通訊,正常情況下,我們可以從硬體工程師那邊得到serviceUUID和characteristicUUID,也就是我們所比喻的班級號和學號,以此來獲得我們的characteristic。
  5. 開始通訊 我們在得到Characteristic後,就可以開始讀寫操作進行通訊了。 a. 對於讀操作來說,讀取BLE終端裝置返回的資料會通過回撥方法mGattCallback中的onCharacteristicChanged函式返回。 b. 對於寫操作來說,可以通過向Characteristic寫入指令以此來達到控制BLE終端裝置的目的

四、Demo程式GATT啟動流程

使用 esp-idf\examples\bluetooth\bluedroid\ble\gatt_server 中的例程 ```cpp ......... //esp_bt_controller_config_t是藍芽控制器配置結構體,這裡使用了一個預設的引數 esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); //初始化藍芽控制器,此函式只能被呼叫一次,且必須在其他藍芽功能被呼叫之前呼叫 ret = esp_bt_controller_init(&bt_cfg); if (ret) { ESP_LOGE(GATTS_TAG, "%s initialize controller failed: %s\n", func, esp_err_to_name(ret)); return; }

//使能藍芽控制器,mode是藍芽模式,如果想要動態改變藍芽模式不能直接呼叫該函式,
//應該先用disable關閉藍芽再使用該API來改變藍芽模式
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
    ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
    return;
}
//初始化藍芽並分配系統資源,它應該被第一個呼叫
/*
藍芽棧bluedroid stack包括了BT和BLE使用的基本的define和API
初始化藍芽棧以後並不能直接使用藍芽功能,
還需要用FSM管理藍芽連線情況
*/
ret = esp_bluedroid_init();
if (ret) {
    ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
    return;
}
//使能藍芽棧
ret = esp_bluedroid_enable();
if (ret) {
    ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
    return;
}

//建立藍芽的FSM(有限狀態機)
//這裡使用回撥函式來控制每個狀態下的響應,需要將其在GATT和GAP層的回撥函式註冊
/*gatts_event_handler和gap_event_handler處理藍芽棧可能發生的所有情況,達到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;
}

//下面建立了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;
}
//下面建立了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中最大能夠交換的資料量)。
例如:主裝置發出一個1000位元組的MTU請求,但是從裝置迴應的MTU是500位元組,那麼今後雙方要以較小的值500位元組作為以後的MTU。
即主從雙方每次在做資料傳輸時不超過這個最大資料單元。
*/
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);
}

....... ```

五、服務資料結構體設定

一個GATT 伺服器應用程式架構(由Application Profiles組織起來)如下:

每個Profile定義為一個結構體,結構體成員依賴於該Application Profile 實現的services服務和characteristic特徵。結構體成員還包括GATT interface(GATT 介面)、Application ID(應用程式ID)和處理profile事件的回撥函式。

每個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),它就必須是實現CCCD(Client Characteristic Configuration Descriptor)----這是額外的ATT。描述符有一個控制代碼和UUID。如: cpp struct gatts_profile_inst { esp_gatts_cb_t gatts_cb; //GATT的回撥函式 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儲存在陣列中,並分配相應的回撥函式gatts_profile_a_event_handler()gatts_profile_b_event_handler()。GATT 客戶端上的不同應用程式使用不同的介面,由 gatts_if 引數表示。對於初始化,此引數設定為ESP_GATT_IF_NONE,這意味著應用程式配置檔案尚未連結到任何客戶端。 cpp /* 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 */ }, }; 這是兩個元素的陣列。可以用Application ID來註冊Application Profiles,Application ID是由應用程式分配的用來標識每個Profile。 通過這種方法,可以在一個Server中擁有多個Application Profile。 cpp esp_ble_gatts_app_register (PROFILE_A_APP_ID); esp_ble_gatts_app_register (PROFILE_B_APP_ID);

六、GATT事件處理程式

其作用就是建立了藍芽GATT的FSM(有限狀態機),callback回撥函式處理從BLE堆疊推送到應用程式的所有事件。

回撥函式的引數: * event: esp_gatts_cb_event_t 這是一個列舉型別,表示呼叫該回調函式時的事件(或藍芽的狀態) * 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 指向回撥函式的引數,是個聯合體型別,不同的事件型別採用聯合體內不同的成員結構體。

```cpp 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);

} ```

七、註冊建立服務

當呼叫esp_ble_gatts_app_register()註冊一個應用程式Profile(Application Profile),將觸發ESP_GATTS_REG_EVT事件,除了可以完成對應profile的gatts_if的註冊,還可以呼叫esp_bel_create_attr_tab()來建立profile Attributes 表或建立一個服務esp_ble_gatts_create_service()。 ```cpp 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;

… } 控制代碼數定義為4:cpp

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 可用於區分具有相同 UUID 的多個服務。在此示例中,由於每個應用程式配置檔案只有一個服務並且服務具有不同的 UUID,因此在配置檔案 A 和 B 中可以將服務例項 ID 定義為 0。但是,如果只有一個應用程式配置檔案具有兩個服務使用相同的 UUID,則有必要使用不同的例項 ID 來引用一個或另一個服務。

demo中的gatts_event_handler()回撥函式—呼叫esp_ble_gatts_app_register(),觸發ESP_GATTS_REG_EVT時,完成對每個profile 的gatts_if 的註冊。 cpp gl_profile_tab[param->reg.app_id].gatts_if = gatts_if; 如果gatts_if == 某個Profile的gatts_if時,呼叫對應profile的回撥函式處理事情。 cpp 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); } }

八、啟動服務和建立特徵

8.1 啟動服務

當一個服務service建立成功後,由該profile GATT handler 管理的 ESP_GATTS_CREATE_EVT事件被觸發,在這個事件可以啟動服務和新增特徵characteristics到服務中。呼叫esp_ble_gatts_start_service()來啟動指定服務。 ```cpp 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 建立特徵

Characteristic是在GATT規範中最小的邏輯資料單元,由一個Value和多個描述特性的Desciptior組成。實際上,在與藍芽裝置打交道,主要就是讀寫Characteristic的value來完成。 同樣的,Characteristic也是通過16bit或128bit的UUID唯一標識。

我們根據藍芽裝置的協議用對應的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。初始值定義如下: ```cpp

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, }; ``` 特徵初始值必須是非空物件並且特徵長度必須始終大於零,否則堆疊將返回錯誤。

最後,特性被配置為每次讀取或寫入特性時都需要手動傳送響應,而不是讓堆疊自動響應。這是通過將esp_ble_gatts_add_char()函式的最後一個引數(表示屬性響應控制引數)設定為ESP_GATT_RSP_BY_APP或 NULL 來配置的。

七、建立特徵描述符

當特徵新增到service中成功時,觸發ESP_GATTS_ADD_CHAR_EVT事件。該事件返回由堆疊為剛剛新增的特徵生成的控制代碼。該事件包括以下引數: cpp esp_gatt_status_t狀態; /* !< 操作狀態*/ uint16_t attr_handle; /* !< 特徵屬性控制代碼*/ uint16_t service_handle; /* !< 服務屬性控制代碼*/ esp_bt_uuid_t char_uuid; /* !< 特徵 uuid */ 事件返回的屬性控制代碼儲存在配置檔案表中,並且還設定了特徵描述符長度和 UUID。使用該esp_ble_gatts_get_attr_value()函式讀取特徵長度和值,然後列印以供參考。最後,使用該esp_ble_gatts_add_char_descr()函式新增特徵描述。使用的引數是服務控制代碼、描述符 UUID、寫入和讀取許可權、初始值和自動響應設定。特徵描述符的初始值可以是空指標,自動響應引數也設定為空,這意味著需要響應的請求必須手動回覆。 ```cpp 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`觸發事件,在此示例中用於列印資訊訊息。cpp 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 更新連線引數

一個ESP_GATTS_CONNECT_EVT當客戶端已連線到伺服器GATT被觸發。此事件用於更新連線引數,例如延遲、最小連線間隔、最大連線間隔和超時。連線引數儲存在一個esp_ble_conn_update_params_t結構中,然後傳遞給esp_ble_gap_update_conn_params()函式。更新連線引數過程只需執行一次,因此配置檔案 B 連線事件處理程式不包含該esp_ble_gap_update_conn_params()函式。最後,事件返回的連線 ID 儲存在配置檔案表中。

配置檔案 A 連線事件: cpp 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 連線事件: cpp 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()函式觸發一個 GAP 事件ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT,用於列印連線資訊: cpp 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時,觸發ESP_GATTS_MTU_EVT事件,其列印如下圖所示

ESP_GATTS_MTU_EVT事件對應的回撥函式中引數param的結構體為gatts_mtu_evt_param(包括連線id和MTU大小) cpp /** * @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中最大能夠交換的資料量)。例如:主裝置發出一個150位元組的MTU請求,但是從裝置迴應的MTU是23位元組,那麼今後雙方要以較小的值23位元組作為以後的MTU。即主從雙方每次在做資料傳輸時不超過這個最大資料單元。 MTU交換通常發生在主從雙方建立連線後。MTU比較小,就是為什麼BLE不能傳輸大資料的原因所在。

參照一分鐘讀懂低功耗(BLE)MTU交換資料包 這篇文章就可以瞭解MTU交換過程。

MTU交換請求用於client通知server關於client最大接收MTU大小並請求server響應它的最大接收MTU大小。

Client的接收MTU 應該大於或等於預設ATT_MTU(23).這個請求已建立連線就由client發出。這個Client Rx MTU引數應該設定為client可以接收的attribute protocol PDU最大尺寸。

MTU交換應答傳送用於接收到一個Exchange MTU請求

這個應答由server發出,server的接收MTU必須大於或等於預設ATT_MTU大小。這裡的Server Rx MTU應該設定為 伺服器可以接收的attribute protocol PDU 最大尺寸。

Server和Client應該設定ATT_MTU為Client Rx MTU和Server Rx MTU兩者的較小值。

這個ATT_MTU在server在發出這個應答後,在發其他屬性協議PDU之前生效;在client收到這個應答並在發其他屬性協議PDU之前生效。

十、管理讀取事件

現在已經建立並啟動了服務和特徵,程式可以接收讀寫事件。讀取操作由ESP_GATTS_READ_EVT事件表示,它具有以下引數: cpp 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中,響應是用虛擬資料構造的,並使用事件給定的相同控制代碼傳送回主機。除了響應之外,GATT 介面、連線 ID 和傳輸 ID 也作為引數包含在esp_ble_gatts_send_response()函式中。如果在建立特徵或描述符時將自動響應位元組設定為 NULL,則此功能是必需的。 cpp 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,它具有以下引數: cpp 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中實現了兩種型別的寫事件,寫特徵值和寫長特徵值。當特徵值可以容納在一個屬性協議最大傳輸單元 (ATT MTU) 中時,使用第一種型別的寫入,該單元通常為 23 位元組長。當要寫入的屬性長於單個 ATT 訊息中可以傳送的屬性時使用第二種型別,通過使用準備寫入響應將資料分成多個塊,然後使用執行寫入請求來確認或取消完整的寫入請求. 此行為在藍芽規範版本 4.2,第 3 卷,G 部分,第 4.9 節中定義。寫長特徵訊息流如下圖所示。

當觸發寫入事件時,此示例列印日誌訊息,然後執行example_write_event_env()函式。 cpp 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()函式包含寫長特徵過程的邏輯: ```cpp 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_SIZEsizeof(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);
    }
}

} 當客戶端傳送寫請求或準備寫請求時,伺服器應響應。但是,如果客戶端傳送 Write without Response 命令,則伺服器不需要回復響應。這是在寫入過程中通過檢查 的值來檢查的`write.need_rsp parameter`。如果需要響應,程式繼續做響應準備,如果不存在,客戶端不需要響應,因此程式結束。響應的話會影響資料傳輸速度,在需要大資料量的場合是否合適需要試驗?cpp 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){ … 然後該函式檢查是否`write.is_prep`設定了由 表示的 Prepare Write Request 引數,這意味著客戶端正在請求 Write Long Characteristic。如果存在,該過程繼續準備多個寫響應,如果不存在,則伺服器簡單地發回單個寫響應。cpp … if (param->write.is_prep){ … }else{ esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL); } … 為了處理長特徵寫入,定義並例項化了一個準備緩衝區結構:cpp 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; 為了使用準備緩衝區,為其分配了一些記憶體空間。如果由於記憶體不足導致分配失敗,則會列印錯誤:cpp 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認證型別設定為`ESP_GATT_AUTH_REQ_NONE`,這意味著客戶端可以寫入該特性而無需先進行身份驗證。一旦傳送響應,分配給它使用的記憶體就會被釋放。cpp 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; } 最後,傳入的資料被複制到建立的緩衝區中,其長度按偏移量遞增:cpp 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函式:cpp 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;

} 執行寫入用於確認或取消之前完成的寫入過程,由長特徵寫入過程。為此,該函式會檢查`exec_write_flag`隨事件接收到的引數中的 。如果標誌等於 表示的執行標誌`exec_write_flag`,則確認寫入並在日誌中列印緩衝區;如果不是,則表示取消寫入並刪除所有已寫入的資料。cpp 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"); } 最後,為儲存來自長寫操作的資料塊而建立的緩衝區結構被釋放,並將其指標設定為 NULL 以使其為下一個長寫過程做好準備。cpp 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並讀取藍芽發過來的資料,開啟這個後我們就能實時獲取藍芽發過來的值了。

使能通知(notify enable)的列印如下所示,開啟通知實際上的一個WRITE。

如果write.handle和descr_handle相同,且長度==2,確定descr_value描述值,根據描述值開啟/關閉 通知notify/indicate。 cpp //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); 該函式將notify或indicate發給GATT的客戶端;

need_confirm = false,則傳送的是notification通知;

==true,傳送的是指示indication。

其他引數: 服務端訪問介面;連線id; 屬性控制代碼,value_len; 值


• 由 Leung 寫於 2021 年 7 月 7 日

• 參考:ESP32學習筆記(7)藍芽GATT服務應用

Gatt 伺服器示例演練