现在的位置: 首页 > 综合 > 正文

Bluez HID分析(二) .

2013年11月15日 ⁄ 综合 ⁄ 共 16268字 ⁄ 字号 评论关闭

本文分析了蓝牙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”方法启动连接过程,具体实现可查看代码。

本文分析了蓝牙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”方法启动连接过程,具体实现可查看代码。

抱歉!评论已关闭.