1. 程式人生 > >bluetooth LE HOGP profile的程式碼大致實現流程(HOGP+GATT)

bluetooth LE HOGP profile的程式碼大致實現流程(HOGP+GATT)

之前一段時間學習了HOGP profile。Specification寫得很簡單,主要是說明它的一些基本要求。而在程式碼方面,它的內容也並不是非常多。正如它的名字一樣——HID over GATT Profile,它是利用LE的基本協議GATT(通用屬性協議)來實現HID host與Device的互動的。總的來說,工作在LE模式的HID裝置從連線建立到通訊的過程大致是這樣的:

1.在裝置發現階段,HID(以下均指LE的HID裝置)device需要在它的advertisement中包含HID的相關資訊,如HID service的UUID、裝置Appearance、裝置Local Name和Class of Device等;這樣,HID host在掃描中就可以得知對端為HID裝置了;

2.在連線建立階段,使用者通過UI觸發連線的建立過程;由於前面檢測到對方為HID裝置,連線建立會進入到HID的state machine當中,以建立HID連線為最終目標;這一過程又分為下面幾個階段:

1)GATT連線建立階段;既然是HID over GATT Profile,必須先由GATT來探探對方的虛實,看看它是否符合HOGP的規範。這裡會涉及到L2CAP的連線建立與GATT的disovery過程;

2)SMP通道加密階段;其實這是每對LE裝置建立連線的必經之路,好歹得加密防止簡單的攻擊。由於L2CAP連線建立是由HOGP觸發的,因此在這個過程中會有SMP的加密過程。之前一篇部落格層介紹過,SMP可以在createBond的過程中觸發,實現整個配對、加密過程,但那是在非HID裝置的情況下進行的;

3)HID服務搜尋階段與配置;雖然第一階段中GATT已經完成了service discovery,但是HID還需要將必要的屬性從GATT的cache中提取出來,存到自己的database中;此外還有一些額外的屬性值(characteristic value)需要讀取,如用於解析按鍵的Report Map;最後,對某些屬性的configuration descriptor進行配置,設定如notification、indication等;

4)HID設備註冊階段;這是HID程式碼向系統註冊uhid裝置的過程。說實話這一部分我還不是很瞭解,大概就是註冊了一個虛擬裝置節點,同時備案了HID的Report Map;以後每次有HID訊息過來,只要將它作為Input事件寫入這個節點的檔案描述符即可,剩下的事件處理kernel會來幫我們完成;

5)HID裝置通訊階段;這裡將根據之前的第三階段的配置結果進行。如hid device將事件通過notification的方式傳送給hid host,而host根據第三階段的搜尋結果進行相應的處理。

下面,我以一個LE鍵盤為例,簡單介紹下Bluedroid(不是bluez!!!)中HOGP的程式碼流程。

首先,請允許我放出createBond過程中的一段程式碼,一切皆是由它引起的

  1. static void btif_dm_cb_create_bond(bt_bdaddr_t *bd_addr)

  2. {

  3. BOOLEAN is_hid = check_cod(bd_addr, COD_HID_POINTING);

  4. bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_BONDING);

  5. if (is_hid){

  6. int status;

  7. status = btif_hh_connect(bd_addr);

  8. if(status != BT_STATUS_SUCCESS)

  9. bond_state_changed(status, bd_addr, BT_BOND_STATE_NONE);

  10. }

  11. else

  12. {

  13. #if BLE_INCLUDED == TRUE

  14. int device_type;

  15. int addr_type;

  16. bdstr_t bdstr;

  17. bd2str(bd_addr, &bdstr);

  18. if(btif_config_get_int("Remote", (char const *)&bdstr,"DevType", &device_type) &&

  19. (btif_storage_get_remote_addr_type(bd_addr, &addr_type) == BT_STATUS_SUCCESS) &&

  20. (device_type == BT_DEVICE_TYPE_BLE))

  21. {

  22. BTA_DmAddBleDevice(bd_addr->address, addr_type, BT_DEVICE_TYPE_BLE);

  23. }

  24. #endif

  25. BTA_DmBond ((UINT8 *)bd_addr->address);

  26. }

  27. /* Track originator of bond creation */

  28. pairing_cb.is_local_initiated = TRUE;

  29. }

看到了吧,程式碼第七行對hid型別進行了判斷。若對端為hid device,則程式碼直接進入btif_hh_connect建立HID的連線,而不會進入下面的BTA_DmBond。

接下來,函式BTA_HhOpen將會被呼叫,從而進入HID的內部state machine處理過程。HID的事件處理函式在BTA_HhEnable中註冊過,主要是bta_hh_reg的第一個成員bta_hh_hdl_event,不過它其實也只是個殼子,除了對HID的使能、禁能做出處理,剩下的都是交給bta_hh_sm_execute。這函式一看名字就知道是幹啥的,它將根據當前的狀態和到來的事件,決定一下步的動作。首先是以下幾組狀態:

  1. const tBTA_HH_ST_TBL bta_hh_st_tbl[] =

  2. {

  3. bta_hh_st_idle,

  4. bta_hh_st_w4_conn,

  5. bta_hh_st_connected

  6. #if (defined BTA_HH_LE_INCLUDED && BTA_HH_LE_INCLUDED == TRUE)

  7. ,bta_hh_st_w4_sec

  8. #endif

  9. };

他們分別為idle、wait for(w4)connection、connected和wait for(w4)security,每組都有自己的狀態機,決定事件的處理函式和下一個狀態。剛開始為idle狀態,而前面BTA_HhOpen傳過來的是事件BTA_HH_API_OPEN_EVT,此時的處理函式則是bta_hh_start_sdp。瞭解bluetooth LE裝置的小夥伴也許會感到奇怪,LE怎麼會使用SDP呢?你沒有看錯,它確實是這個名字,只不過有一段巨集決定的處理流程:

  1. #if (BTA_HH_LE_INCLUDED == TRUE)

  2. if (bta_hh_is_le_device(p_cb, p_data->api_conn.bd_addr))

  3. {

  4. bta_hh_le_open_conn(p_cb, p_data->api_conn.bd_addr);

  5. return;

  6. }

  7. #endif

這樣看Android還是考慮得蠻周到的,會先檢測本地是否為LE裝置。而函式bta_hh_le_open_conn比較短,貼出來瞅瞅:

  1. void bta_hh_le_open_conn(tBTA_HH_DEV_CB *p_cb, BD_ADDR remote_bda)

  2. {

  3. APPL_TRACE_DEBUG1("%s", __FUNCTION__);

  4. /* update cb_index[] map */

  5. p_cb->hid_handle = BTA_HH_GET_LE_DEV_HDL(p_cb->index);

  6. memcpy(p_cb->addr, remote_bda, BD_ADDR_LEN);

  7. bta_hh_cb.le_cb_index[BTA_HH_GET_LE_CB_IDX(p_cb->hid_handle)] = p_cb->index;

  8. p_cb->in_use = TRUE;

  9. BTA_GATTC_Open(bta_hh_cb.gatt_if, remote_bda, TRUE);

  10. }

你看,最後它是呼叫了BTA_GATTC_Open函式,這有兩個含義。首先,確實與HOGP相符,HID裝置藉由GATT來實現通訊;其次,GATTC中的“C”代表咋們是GATT的Cient,要通過discovery過程搜尋對端的GATT Service。

接下來就進入GATT部分了,這裡不會貼太多的程式碼。GATT service discovery過程很漫長,但是和SMP一樣,它就是一步一步按照bluetooth core specification的規定,不斷的與service互動,並由內部的state machine決定如何處理事件。它最主要的內容如下:

首先是GATT的運作方式。GATT分為server和client,client可以查詢server所提供的服務。在server的GATT database中,以handle作為序號,標誌著多組primary service及其所包含的include service、characteristic及其value、descriptor。舉個例子,假如service A的handle範圍為hdl_start至hdl_end,則hdl_start一定是該primary service,而在hdl_star與hdl_end之間則分佈著該primary service的include service、characteristic及其descriptor。一般來說,include service 排在characteristic之前。characteristic本身佔一個handle,儲存著它的可讀寫性等資訊;緊跟其後的一個handle則存放著它的value。descriptor不是每個characteristic必有的,若有則在本characteristic value之後下一個characteristic之前。

接下來是幾個處理函式:

1)gatt_le_connect_cback和gatt_le_data_ind;他們分別是GATT連線建立處理函式和GATT訊息處理函式,由於GATT使用固定的L2CAP channel(CID = 4,準確來說是ATT的Channel ID),因此它們是fixed_channel的處理函式;

2)bta_gattc_cl_cback;這是一個GATT client的回撥函式列表,每當某個動作執行完畢時,它的成員函式就會被呼叫,包括bta_gattc_conn_cback,bta_gattc_cmpl_cback,bta_gattc_disc_res_cback,,bta_gattc_disc_cmpl_cback,bta_gattc_enc_cmpl_cback,分表處理連線建立、某個動作完畢、搜尋結果訊息、整個搜尋過程完成與加密過程完成。每當看程式碼看到形如(pfunc)(params)的地方時,多半是這些函式在被呼叫了。

3)bta_gattc_hdl_event和bta_gattc_sm_execute;前者僅僅處理部分事件,其他大部分還是會轉交給後者根據狀態機進行處理。同HID一樣,GATT也有一個狀態機陣列:

  1. const tBTA_GATTC_ST_TBL bta_gattc_st_tbl[] =

  2. {

  3. bta_gattc_st_idle,

  4. bta_gattc_st_w4_conn,

  5. bta_gattc_st_connected,

  6. bta_gattc_st_discover

  7. };

分別處理idle、wait for connection、connected和discovery;當HOGP呼叫GATT時,狀態為idle;準備建立L2CAP連線,從而進入w4_conn;L2CAP連線建立後開始搜尋服務,進入discover;最後回到connected狀態,表明GATT連線已經建立,對方GATT資料庫中的service我們已經大致清楚;

4)bta_hh_gattc_callback和bta_hh_le_encrypt_callback;這裡我把GATT和SMP對於HOGP的回撥函式放在一起,他們的功能是類似的。只要各自的任務完成,就可以呼叫HOGP給的回撥函式通知HOGP;

除了處理函式,GATT的大致過程也簡單說下,就兩個階段:

1)搜尋所有的primary service(也就是HID、Battery、Device Information之類的Service),得到每個service(包含其include service、characteristic及其value、characteristic descriptor)的handle範圍;

2)逐個搜尋每個service的include service和characteristic的handle;若characteristic有相應的descriptor,也會進行搜尋;

此外,幾個cmd也簡單說明一下,對照著過程來:

1)primary service的搜尋:Read by Group Type Request,返回其handle的範圍

2)include service和characteristic的搜尋:Read By Type Request,返回characteristic的讀寫等屬性、其本身和value的handle

3)descriptor的搜尋:Find Information Request,返回handle-UUID二元組

4)characteristic的讀取:Read Request和Read Blob Request(當需要多次讀取時使用),返回其value

5)descriptor的配置:Write Request

好了,GATT的部分到此結束。SMP過程在另外一篇部落格中有介紹,它和GATT一樣,也是在LE中L2CAP的固定channel中處理的,過程也類似。當GATT與SMP都完成時,HOGP才會繼續它自己的工作,包括:

1)搜尋HID服務;這一步在smp完成之後立即展開,bta_hh_le_pri_service_discovery負責去執行,並設定UUID為HID。最終,它會再次進入GATT中,並且由於這時候已經有了local cache,僅在本地進行查詢;

2)搜尋HID的include service和其characteristic;在我的裝置中,HID include Battery,並且電池電量被HID讀取了;

3)搜尋HID的一些characteristic,具體是一個數組:

  1. static const UINT16 bta_hh_le_disc_char_uuid[BTA_HH_LE_DISC_CHAR_NUM] =

  2. {

  3. GATT_UUID_HID_INFORMATION,

  4. GATT_UUID_HID_REPORT_MAP,

  5. GATT_UUID_HID_CONTROL_POINT,

  6. GATT_UUID_HID_REPORT,

  7. GATT_UUID_HID_BT_KB_INPUT,

  8. GATT_UUID_HID_BT_KB_OUTPUT,

  9. GATT_UUID_HID_BT_MOUSE_INPUT,

  10. GATT_UUID_HID_PROTO_MODE /* always make sure this is the last attribute to discover */

  11. };

以上均需要在local cache中查詢,而有些還需要read characteristic value,如HID_REPORT_MAP,否則裝置的訊息無法再host端進行處理。整個列表中需要重點關注的是Report ,它可能存在多個,並且自身可讀寫性會有不同(也可能完全相同);系統會根據這些可讀寫性來區分Input Report、Output Report和Feature Report。當載著Report的notification訊息到來時,系統就會根據該report的value handle(其實最終是根據hid host端儲存的該value handle對應的Report ID)在本地database查詢其對應的uhid裝置,然後將資訊寫入該裝置的檔案描述符,由kernel進行處理。

4)設定屬性configuration型別的descriptor,重點是設定訊息的通知方式為notification。

5)開啟裝置節點;一切資訊就緒後,HID會在系統中開啟一個uhid的裝置節點,呼叫最基本的uhid_write之類的函式註冊裝置及其附加資訊(如Report Map)。

好了,HOGP的過程完成,其狀態也轉到bta_hh_st_connected,等待hid device的notification到來。Notification的處理也很簡單,就是將Report value寫入uhid的檔案描述符中。