Bluez HID分析(二)

來源:互聯網
上載者:User
文章目錄
  • 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”方法啟動串連過程,具體實現可查看代碼。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.