之前一段时间学习了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过程中的一段代码,一切皆是由它引起的
static void btif_dm_cb_create_bond(bt_bdaddr_t *bd_addr)
{
BOOLEAN is_hid = check_cod(bd_addr, COD_HID_POINTING);
bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_BONDING);
if (is_hid){
int status;
status = btif_hh_connect(bd_addr);
if(status != BT_STATUS_SUCCESS)
bond_state_changed(status, bd_addr, BT_BOND_STATE_NONE);
}
else
{
#if BLE_INCLUDED == TRUE
int device_type;
int addr_type;
bdstr_t bdstr;
bd2str(bd_addr, &bdstr);
if(btif_config_get_int("Remote", (char const *)&bdstr,"DevType", &device_type) &&
(btif_storage_get_remote_addr_type(bd_addr, &addr_type) == BT_STATUS_SUCCESS) &&
(device_type == BT_DEVICE_TYPE_BLE))
{
BTA_DmAddBleDevice(bd_addr->address, addr_type, BT_DEVICE_TYPE_BLE);
}
#endif
BTA_DmBond ((UINT8 *)bd_addr->address);
}
/* Track originator of bond creation */
pairing_cb.is_local_initiated = TRUE;
}
看到了吧,代码第七行对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。这函数一看名字就知道是干啥的,它将根据当前的状态和到来的事件,决定一下步的动作。首先是以下几组状态:
const tBTA_HH_ST_TBL bta_hh_st_tbl[] =
{
bta_hh_st_idle,
bta_hh_st_w4_conn,
bta_hh_st_connected
#if (defined BTA_HH_LE_INCLUDED && BTA_HH_LE_INCLUDED == TRUE)
,bta_hh_st_w4_sec
#endif
};
他们分别为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呢?你没有看错,它确实是这个名字,只不过有一段宏决定的处理流程:
#if (BTA_HH_LE_INCLUDED == TRUE)
if (bta_hh_is_le_device(p_cb, p_data->api_conn.bd_addr))
{
bta_hh_le_open_conn(p_cb, p_data->api_conn.bd_addr);
return;
}
#endif
这样看Android还是考虑得蛮周到的,会先检测本地是否为LE设备。而函数bta_hh_le_open_conn比较短,贴出来瞅瞅:
void bta_hh_le_open_conn(tBTA_HH_DEV_CB *p_cb, BD_ADDR remote_bda)
{
APPL_TRACE_DEBUG1("%s", __FUNCTION__);
/* update cb_index[] map */
p_cb->hid_handle = BTA_HH_GET_LE_DEV_HDL(p_cb->index);
memcpy(p_cb->addr, remote_bda, BD_ADDR_LEN);
bta_hh_cb.le_cb_index[BTA_HH_GET_LE_CB_IDX(p_cb->hid_handle)] = p_cb->index;
p_cb->in_use = TRUE;
BTA_GATTC_Open(bta_hh_cb.gatt_if, remote_bda, TRUE);
}
你看,最后它是调用了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也有一个状态机数组:
const tBTA_GATTC_ST_TBL bta_gattc_st_tbl[] =
{
bta_gattc_st_idle,
bta_gattc_st_w4_conn,
bta_gattc_st_connected,
bta_gattc_st_discover
};
分别处理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,具体是一个数组:
static const UINT16 bta_hh_le_disc_char_uuid[BTA_HH_LE_DISC_CHAR_NUM] =
{
GATT_UUID_HID_INFORMATION,
GATT_UUID_HID_REPORT_MAP,
GATT_UUID_HID_CONTROL_POINT,
GATT_UUID_HID_REPORT,
GATT_UUID_HID_BT_KB_INPUT,
GATT_UUID_HID_BT_KB_OUTPUT,
GATT_UUID_HID_BT_MOUSE_INPUT,
GATT_UUID_HID_PROTO_MODE /* always make sure this is the last attribute to discover */
};
以上均需要在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的文件描述符中。