文章目錄
- 2.1 btd_register_adapter_driver
- 2.2 btd_register_device_driver
本文分析了藍芽bluez協議棧中HID協議的實現。
1. 基本概念
HID協議用於人機輸入裝置。Bluez中關於HID的實現代碼在其根目錄下的input目錄。藍芽規範中包含關於HID的profile,裡面重用了USB中關於HID的一些協議規範。
Bluez協議棧與上層應用之間使用dbus介面。
Bluez與kernel之間使用AF_BLUETOOTH協議族的socket通訊,並使用了gtk+中的glib庫。
2. 初始化
HID的初始化在input目錄的main.c中,input_manager_init函數。該函數會調用input_manager_init。在input_manager_init中,主要是做了三個操作:
btd_register_adapter_driver(&input_server_driver);
btd_register_device_driver(&input_hid_driver);
btd_register_device_driver(&input_headset_driver);
下面分別討論。
2.1 btd_register_adapter_driver
btd_register_adapter_driver(&input_server_driver);
static struct btd_adapter_driver input_server_driver = {
.name = "input-server",
.probe = hid_server_probe,
.remove = hid_server_remove,
};
這個調用的作用是註冊一個adapter driver。系統啟動後對每一個本地藍芽的硬體執行個體,即每一個HCI裝置,都會調用裡面的probe函數hid_server_probe。
static int hid_server_probe(struct btd_adapter *adapter)
// 得到hci裝置的本地藍芽地址
adapter_get_address(adapter, &src);
// 啟動hid服務
server_start(&src);
。。。
int server_start(const bdaddr_t *src)
struct input_server *server = g_new0(struct input_server, 1);
// 在ctrl通道(L2CAP_PSM_HIDP_CTRL)上listen,回呼函數connect_event_cb
server->ctrl = bt_io_listen(BT_IO_L2CAP, connect_event_cb, NULL,
server, NULL, &err,
BT_IO_OPT_SOURCE_BDADDR, src,
BT_IO_OPT_PSM, L2CAP_PSM_HIDP_CTRL,
BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_LOW,
BT_IO_OPT_INVALID);
// 在intr通道(L2CAP_PSM_HIDP_INTR)listen,回呼函數confirm_event_cb
server->intr = bt_io_listen(BT_IO_L2CAP, NULL, confirm_event_cb,
server, NULL, &err,
BT_IO_OPT_SOURCE_BDADDR, src,
BT_IO_OPT_PSM, L2CAP_PSM_HIDP_INTR,
BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_LOW,
BT_IO_OPT_INVALID);
上面的ctrl通道和intr通道都是由藍芽的HID spec規定。
對於control通道,當裝置端有主動串連本機時,會由glib調用回呼函數connect_event_cb:
static void connect_event_cb(GIOChannel *chan, GError *err, gpointer data)
// 得到該裝置的源地址和目的地址,psm等
bt_io_get(chan, BT_IO_L2CAP, &gerr, BT_IO_OPT_SOURCE_BDADDR, &src,
BT_IO_OPT_DEST_BDADDR, &dst, BT_IO_OPT_PSM, &psm,
BT_IO_OPT_INVALID);
// 設定input_device
input_device_set_channel(&src, &dst, psm, chan);
// 如果是非法裝置,並且當前是控制通道,那麼根據HID協議,需要向對方發送“unplug virtual cable”訊息
if (ret == -ENOENT && psm == L2CAP_PSM_HIDP_CTRL) {
unsigned char unplug = 0x15;
int err, sk = g_io_channel_unix_get_fd(chan);
err = write(sk, &unplug, sizeof(unplug));
}
下面繼續研究input_device_set_channel函數。
int input_device_set_channel(const bdaddr_t *src, const bdaddr_t *dst, int psm, GIOChannel *io)
// 根據對方裝置的地址,從HID裝置鏈表中找到對應的input_dev裝置。這裡有一個問題,就是對應的input_dev裝置是什麼時候登記到鏈表中的,這一點稍後再討論
struct input_device *idev = find_device(src, dst);
// 在該裝置中,尋找名為”hid”的串連
struct input_conn * iconn = find_connection(idev->connections, "hid");
switch (psm) {
case L2CAP_PSM_HIDP_CTRL:
if (iconn->ctrl_io)
return -EALREADY;
iconn->ctrl_io = g_io_channel_ref(io);
break;
case L2CAP_PSM_HIDP_INTR:
if (iconn->intr_io)
return -EALREADY;
iconn->intr_io = g_io_channel_ref(io);
break;
}
// 當ctrl通道和intr通道都被設定後,才會進入input_device_connadd。目前我們是沿著L2CAP_PSM_HIDP_CTRL的回呼函數connect_event_cb看到這裡的,所以暫時先不深入研究
if (iconn->intr_io && iconn->ctrl_io)
input_device_connadd(idev, iconn);
。。。
下面再看一下server_start函數中對L2CAP_PSM_HIDP_INTR的情況,此時會調用到confirm_event_cb函數。關於bt_io_listen中關於connect和confirm這兩個函數的區別,可以自行查看glib的文檔或者bluez的原始碼。
static void confirm_event_cb(GIOChannel *chan, gpointer user_data)
bt_io_get(chan, BT_IO_L2CAP, &err, BT_IO_OPT_SOURCE_BDADDR, &src,
BT_IO_OPT_DEST_BDADDR, &dst, BT_IO_OPT_INVALID);
server->confirm = g_io_channel_ref(chan);
// 請求authorization操作,並指定完成後的回呼函數為auth_callback
btd_request_authorization(&src, &dst, HID_UUID, auth_callback, server);
static void auth_callback(DBusError *derr, void *user_data)
bt_io_get(server->confirm, BT_IO_L2CAP, &err,
BT_IO_OPT_SOURCE_BDADDR, &src,
BT_IO_OPT_DEST_BDADDR, &dst,
BT_IO_OPT_INVALID);
bt_io_accept(server->confirm, connect_event_cb, server, NULL, &err)
由此可見,authorization結束後,會調用bt_io_accept,並同樣指定回呼函數為connect_event_cb。此時connect_event_cb會設定intr通道,並最終調用input_device_connadd函數。
static int input_device_connadd(struct input_device *idev, struct input_conn *iconn)
input_device_connected(idev, iconn)
。。。
static int input_device_connected(struct input_device *idev, struct input_conn *iconn)
hidp_add_connection(idev, iconn)
。。。
connected = TRUE;
// 通過dbus發送已串連的訊號
emit_property_changed(idev->conn, idev->path, INPUT_DEVICE_INTERFACE,
"Connected", DBUS_TYPE_BOOLEAN, &connected);
。。。
static int hidp_add_connection(const struct input_device *idev, const struct input_conn *iconn)
struct hidp_connadd_req *req;
sdp_record_t *rec;
req = g_new0(struct hidp_connadd_req, 1);
req->ctrl_sock = g_io_channel_unix_get_fd(iconn->ctrl_io);
req->intr_sock = g_io_channel_unix_get_fd(iconn->intr_io);
req->flags = 0;
req->idle_to = iconn->timeout;
ba2str(&idev->src, src_addr);
ba2str(&idev->dst, dst_addr);
// 尋找該裝置對應的SDP
rec = fetch_record(src_addr, dst_addr, idev->handle);
// 從SDP record中得到一些屬性從而設定req中某些域,具體可看代碼,包括HID的裝置描述符等都在這裡設定
extract_hid_record(rec, req);
sdp_record_free(rec);
// 根據SDP得到裝置的vendor、product等資訊
read_device_id(src_addr, dst_addr, NULL,
&req->vendor, &req->product, &req->version);
// 下面是支援fakehid的代碼,目前僅有PS3的裝置支援,所以這裡不分析
struct fake_hid *fake_hid = get_fake_hid(req->vendor, req->product);
。。。
if (req->subclass & 0x40) // 如果是鍵盤,則啟動加密
bt_acl_encrypt(&idev->src, &idev->dst, encrypt_completed, req);
。。。
// ioctl_connadd中會建立一個BTPROTO_HIDP的socket,並調用HIDPCONNADD建立一個串連。到這裡,與遠端裝置的串連就建立了。建立之後,kernel會建立一個HID裝置,此HID裝置與bluez之間通過ctrl sock和intr sock進行資料互動
ioctl_connadd(req);
2.2 btd_register_device_driver
btd_register_device_driver用於註冊裝置驅動,在bluez中使用這個函數註冊的裝置有兩個,分別是input_headset_driver和input_hid_driver。
其中input-headset與藍芽耳機有關;input-hid則用於普通的HID裝置。
下面先看一下input_hid_driver裝置。
input_hid_driver
static struct btd_device_driver input_hid_driver = {
.name = "input-hid",
.uuids = BTD_UUIDS(HID_UUID),
.probe = hid_device_probe,
.remove = hid_device_remove,
};
當bluez檢測到有一個hid裝置,即uuid中包含HID_UUID的裝置串連上時,就會調用其中的probe函數。
static int hid_device_probe(struct btd_device *device, GSList *uuids)
。。。
input_device_register(connection, device, path, &src, &dst,
HID_UUID, rec->handle, idle_timeout * 60);
int input_device_register(DBusConnection *conn, struct btd_device *device,
const char *path, const bdaddr_t *src,
const bdaddr_t *dst, const char *uuid,
uint32_t handle, int timeout)
。。。
// 分配一個新的input_device結構體,並添加到全域鏈表devices中
// 前文分析input_device_set_channel函數時,提到的添加idev的地方,就在這裡
idev = input_device_new(conn, device, path, src, dst, handle);
devices = g_slist_append(devices, idev);
。。。
// 添加一個名為”hid”的串連
iconn = input_conn_new(idev, uuid, "hid", timeout);
idev->connections = g_slist_append(idev->connections, iconn);
在函數input_device_new中,除了建立裝置之外,還添加了一個dbus介面:
g_dbus_register_interface(conn, idev->path, INPUT_DEVICE_INTERFACE,
device_methods, device_signals, NULL,
idev, device_unregister)
static GDBusMethodTable device_methods[] = {
{ "Connect", "", "", input_device_connect,
G_DBUS_METHOD_FLAG_ASYNC },
{ "Disconnect", "", "", input_device_disconnect },
{ "VirtualUnplug", "", "", input_device_unplug },
{ "GetProperties", "", "a{sv}",input_device_get_properties },
{ }
};
前文分析HID串連的建立時,都是本機作為伺服器,等待遠端裝置串連。有了這個dbus介面之後,本地應用程式就可以主動串連遠端裝置,只要調用”Connect”方法即可,此方法會被連結到input_device_connect函數。
Input_device_connect中的流程與前文中本機作為伺服器的流程基本相同。在此函數中,會先建立ctrl通道的串連,然後再建立intr通道的串連。最終通過調用函數hidp_add_connection通知核心建立一個HID裝置或者input裝置(HID boot protocol裝置)。
input-headset
static struct btd_device_driver input_headset_driver = {
.name = "input-headset",
.uuids = BTD_UUIDS(HSP_HS_UUID),
.probe = headset_probe,
.remove = headset_remove,
};
Input-headset的流程比較特殊,與input-hid的區別至少有以下幾點:
1. HID裝置的串連建立在l2cap上,headset的串連建立在rfcomm上。
2. HID裝置會通知核心建立一個HID裝置或input裝置,headset則只是執行個體化一個uinput裝置。
3. 前文提到的ctrl通道、intr通道都不能適用於headset,因為他們都是在l2cap上的串連。
如果是本地主動串連遠端的headset裝置,同樣是由應用程式調用”connect”方法啟動串連過程,具體實現可查看代碼。