常见的蓝牙鼠标、蓝牙键盘、蓝牙手柄,它们都属于HID设备,但与有线设备不同的是,有线鼠标等设备属于USB HID设备,而蓝牙鼠标等设备属于Bluetooth HID设备,即协议是一样的,只是通信方式不同。HOGP是HID Over GATT Profile的缩写,即蓝牙HID设备是通过BLE的GATT来实现HID协议的。下图是手机BLE调试APP扫描获取到的手柄广播信息,点击"RAW"后可以看到原始的广播数据,解析结果如下:
连接蓝牙手柄后,可以发现设备支持的服务,其中一个服务是Human Interface Device,该服务也进一步表明了该设备是一个蓝牙HID设备。Bluez在连接蓝牙HID设备后,在发现服务时如果发现了HID服务,就会读取Report Map,这个是HID的报告描述符,通过解析这张表就可以知道设备支持哪些功能了,解析功能内核会帮我们完成。
内核对蓝牙HID的支持分为2部分,一部分是蓝牙部分,另一部分就是uhid。
当主机连上蓝牙手柄时,Bluez会发现PnP ID服务,读取PnP ID服务可以获取设备的制造商信息,例如VIP和PID,串口会有相应的打印。在向内核注册HID设备时,VIP和PID是非常重要的参数。
bluetoothd[536]: profiles/deviceinfo/dis.c:read_pnpid_cb() source: 0x01 vendor: 0x1949 product: 0x0402 version: 0x0000
当Bluez继续发现服务时,会发现HID服务,于是hog-lib.c中的char_discovered_cb函数会被调用,该函数会解析HID服务下所有特征值,其中有一部是比对report_map_uuid,report_map_uuid是0x2A4B,即在手机BLE调试APP上看到的Report Map特征值。
static void char_discovered_cb(uint8_t status, GSList *chars, void *user_data)
{
/* ...... */
else if (bt_uuid_cmp(&uuid, &report_map_uuid) == 0) {
DBG("HoG discovering report map");
read_char(hog, hog->attrib, chr->value_handle,
report_map_read_cb, hog);
discover_external(hog, hog->attrib, start, end, hog);
}
/* ...... */
}
读到该特征值后会回调report_map_read_cb函数,该函数会打印设备的报表描述符,并向内核申请创建HID设备。核心代码如下:
static void report_map_read_cb(guint8 status, const guint8 *pdu, guint16 plen,
gpointer user_data)
{
/* ....... */
DBG("Report MAP:");
for (i = 0; i < vlen;) {
ssize_t ilen = 0;
bool long_item = false;
if (get_descriptor_item_info(&value[i], vlen - i, &ilen,
&long_item)) {
/* Report ID is short item with prefix 100001xx */
if (!long_item && (value[i] & 0xfc) == 0x84)
hog->has_report_id = TRUE;
DBG("\t%s", item2string(itemstr, &value[i], ilen));
i += ilen;
} else {
error("Report Map parsing failed at %d", i);
/* Just print remaining items at once and break */
DBG("\t%s", item2string(itemstr, &value[i], vlen - i));
break;
}
}
/* create uHID device */
memset(&ev, 0, sizeof(ev));
ev.type = UHID_CREATE;
bt_io_get(g_attrib_get_channel(hog->attrib), &gerr,
BT_IO_OPT_SOURCE, ev.u.create.phys,
BT_IO_OPT_DEST, ev.u.create.uniq,
BT_IO_OPT_INVALID);
/* Phys + uniq are the same size (hw address type) */
for (i = 0;
i < (int)sizeof(ev.u.create.phys) && ev.u.create.phys[i] != 0;
++i) {
ev.u.create.phys[i] = tolower(ev.u.create.phys[i]);
ev.u.create.uniq[i] = tolower(ev.u.create.uniq[i]);
}
if (gerr) {
error("Failed to connection details: %s", gerr->message);
g_error_free(gerr);
return;
}
strncpy((char *) ev.u.create.name, hog->name,
sizeof(ev.u.create.name) - 1);
ev.u.create.vendor = hog->vendor;
ev.u.create.product = hog->product;
ev.u.create.version = hog->version;
ev.u.create.country = hog->bcountrycode;
ev.u.create.bus = BUS_BLUETOOTH;
ev.u.create.rd_data = value;
ev.u.create.rd_size = vlen;
err = bt_uhid_send(hog->uhid, &ev);
if (err < 0)
return;
bt_uhid_register(hog->uhid, UHID_OUTPUT, forward_report, hog);
bt_uhid_register(hog->uhid, UHID_GET_REPORT, get_report, hog);
err = bt_uhid_register(hog->uhid, UHID_SET_REPORT, set_report, hog);
hog->uhid_created = true;
DBG("HoG created uHID device");
}
相应串口打印如下:
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() HoG inspecting report map
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() Report MAP:
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 05 0d
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 09 04
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() a1 01
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 85 01
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 09 22
/* 太长了,省略大部分 */
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 75 08
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 09 53
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() 95 01
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() b1 02
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() c0
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() c0
bluetoothd[536]: profiles/input/hog-lib.c:report_map_read_cb() HoG created uHID device
正常来说,到现在为止,内核中应该已经创建了蓝牙手柄的input设备节点,但实际调试过程发现却没有,猜想应该哪里失败了,因此有必要深入了解下内核对Bluez创建HID设备请求的处理流程。
在内核配置中开启uhid的支持后,会生成一个/dev/uhid设备节点,用户层可以通过该文件操作hid操作,Bluez正是通过该文件向内核注册HID设备。具体来说,report_map_read_cb函数中的bt_uhid_send函数会/dev/uhid写入一个UHID_CREATE消息,内核驱动中的uhid.c中的uhid_char_write函数将会被调用,对于UHID_CREATE,uhid_char_write函数将会调用uhid_dev_create函数完成hid设备的创建。大致流程如下图所示。
具体地,uhid_dev_create会唤醒专门添加uhid设备的工作队列uhid_device_add_worker,该工作队列会调用hid_add_device尝试添加HID设备,hid_add_device函数会比对要注册的设备的VIP和PID是否在已支持的列表中,比对失败就不会创建,具体函数如下:
int hid_add_device(struct hid_device *hdev)
/* ...... */
if (hid_ignore_special_drivers) {
hdev->group = HID_GROUP_GENERIC;
} else if (!hdev->group &&
!hid_match_id(hdev, hid_have_special_driver)) {
ret = hid_scan_report(hdev);
if (ret)
hid_warn(hdev, "bad device descriptor (%d)\n", ret);
}
/* ...... */
}
static bool hid_match_one_id(struct hid_device *hdev,
const struct hid_device_id *id)
{
return (id->bus == HID_BUS_ANY || id->bus == hdev->bus) &&
(id->group == HID_GROUP_ANY|| id->group == hdev->group) &&
(id->vendor == HID_ANY_ID || id->vendor == hdev->vendor) &&
(id->product == HID_ANY_ID || id->product == hdev->product);
}
const struct hid_device_id *hid_match_id(struct hid_device *hdev,
const struct hid_device_id *id)
{
for (; id->bus; id++)
if (hid_match_one_id(hdev, id))
return id;
return NULL;
}
hid_have_special_driver是一个很大的数组,里面记录了当前已支持设备的HID类型(USB还是BLE)、VID、PID。调试过程中之所以创建HID设备失败就是因为蓝牙手柄的VIP和PID不在该设备列表中。修改方法有两种:一是可以修改hid_have_special_driver数组,添加蓝牙手柄的VID和PID;二是修改hid_match_one_id函数,增加HID_GROUP_GENERIC的支持。修改完毕后,内核成功创建手柄HID设备,内核打印如下:
[260283.344921] input: 269 as /devices/virtual/misc/uhid/0005:1949:0402.0001/input/input0
[260283.345556] hid-generic 0005:1949:0402.0001: input,hidraw0: BLUETOOTH HID v0.00 Device [269] on 78:f2:35:0e:d0:46
查看/dev/input目录,下面多了两个输入设备:event0和js0。解析event0即可获取手柄的数据。
/ # ls /dev/input/
event0 js0 mice
/ # cat /proc/bus/input/devices
I: Bus=0005 Vendor=1949 Product=0402 Version=0000
N: Name="269"
P: Phys=40:24:b2:d1:f2:a8
S: Sysfs=/devices/virtual/misc/uhid/0005:1949:0402.0004/input/input3
U: Uniq=03:21:04:21:29:ad
H: Handlers=kbd leds js0 event0
B: PROP=0
B: EV=12001f
B: KEY=3007f 0 0 0 0 483ffff 17aff32d bf544446 0 ffff0000 1 130f93 8b17c000 677bfa d9415fed e09effdf 1cfffff ffffffff fffffffe
B: REL=40
B: ABS=1 30627
B: MSC=10
B: LED=1f
Linux的input子系统框架如下图所示,图中没有包含Bluetooth HID设备,但实际Bluetooth HID设备也适用于该框架。
当向内核注册HID设备时,会触发经典的device和driver匹配机制,probe函数将被调用,具体调用关系如下:
hid_device_probe
hid_hw_start
hid_connect
hidinput_connect
hidinput_allocate
hid_device_probe函数在注册HID设备时会被回调,hidinput_allocate函数则申请了input_dev,注册到input子系统。
整条数据链路如下:当手柄的按键或摇杆被操作时,bluetoothd进程将收到手柄的notify数据,bluetoothd通过uhid向HID系统发送UHID_INPUT消息,HID驱动会根据Report Map将数据转换成对应的input_event事件并上报,用户层解析/dev/input目录下对应的文件即可获取手柄的状态。
手柄有多种模式:自定义模式和标准模式。在自定义模式下,用户可以通过专用的APP来设置每个按键对应的坐标,以此来灵活适配各种使用场景(例如适配王者荣耀的键位或英雄联盟的键位)。在标准模式下,摇杆返回的是坐标值,而按键返回的则是按键值。
读取手柄input_event消息并解析即可获得手柄按键的坐标。手柄一共有三种不同的输入:
测试代码如下:
#include
#include "string.h"
#include
#include
#include
#include
#include
#include
#include "stdint.h"
/* 按键编码 */
#define BUTTON_CODE_LB 0x0136
#define BUTTON_CODE_RB 0x0137
#define BUTTON_CODE_LT 0x0138
#define BUTTON_CODE_RT 0x0139
#define BUTTON_CODE_SELECT 0x013A
#define BUTTON_CODE_START 0x013B
#define BUTTON_CODE_A 0x0130
#define BUTTON_CODE_B 0x0131
#define BUTTON_CODE_X 0x0133
#define BUTTON_CODE_Y 0x0134
/* 左摇杆或右摇杆 */
typedef enum
{
ROCKER_LEFT,
ROCKER_RIGHT,
ROCKER_MAX,
}RockerType;
typedef struct
{
uint8_t x;
uint8_t y;
}JsRocker;
typedef struct
{
uint16_t button_code;
char *button_name;
}Button;
int main(int argc, char **argv)
{
struct input_event event_joystick ;
struct pollfd pollfds;
int fd = -1 ;
int i,ret;
uint8_t last_code = 0;
JsRocker rocker[ROCKER_MAX];
const Button button_map[] = {
{BUTTON_CODE_LB, "LB"},
{BUTTON_CODE_RB, "RB"},
{BUTTON_CODE_LT, "LT"},
{BUTTON_CODE_RT, "RT"},
{BUTTON_CODE_SELECT,"SELECT"},
{BUTTON_CODE_START, "START"},
{BUTTON_CODE_A, "A"},
{BUTTON_CODE_B, "B"},
{BUTTON_CODE_X, "X"},
{BUTTON_CODE_Y, "Y"}
};
const char *button_state_table[] = {"release", "press", "hold"};
memset(rocker, 0, sizeof(rocker));
fd = open("/dev/input/event0",O_RDONLY);
if(fd == -1)
{
printf("open joystick event failed\n");
return -1;
}
pollfds.fd = fd;
pollfds.events = POLLIN;
while(1)
{
ret = poll(&pollfds, 1, -1);
if(ret > 0)
{
if(read(fd, &event_joystick, sizeof(event_joystick)) <= 0)
{
close (fd);
printf("read err\n");
return -1;
}
switch(event_joystick.type)
{
case EV_SYN:
if(last_code == ABS_X || last_code == ABS_Y)
printf("lelt rocker x=%d, y =%d\n", rocker[ROCKER_LEFT].x, rocker[ROCKER_LEFT].y);
else if(last_code == ABS_Z || last_code == ABS_RZ)
printf("right rocker x=%d, y =%d\n", rocker[ROCKER_RIGHT].x, rocker[ROCKER_RIGHT].y);
break;
case EV_ABS:
/* 左摇杆事件,需要等同步事件同时获取x和y坐标 */
if(event_joystick.code == ABS_X)
rocker[ROCKER_LEFT].x = event_joystick.value;
else if(event_joystick.code == ABS_Y)
rocker[ROCKER_LEFT].y = event_joystick.value;
/* 右摇杆事件,需要等同步事件同时获取x和y坐标 */
else if(event_joystick.code == ABS_Z)
rocker[ROCKER_RIGHT].x = event_joystick.value;
else if(event_joystick.code == ABS_RZ)
rocker[ROCKER_RIGHT].y = event_joystick.value;
/* 方向键 X方向有键被按下 */
else if(event_joystick.code == ABS_HAT0X)
{
if(event_joystick.value == -1)
printf("dir button: left\n");
else if(event_joystick.value == 1)
printf("dir button: right\n");
else
printf("dir button: none\n");
}
/* 方向键 Y方向有键被按下 */
else if(event_joystick.code == ABS_HAT0Y)
{
if(event_joystick.value == -1)
printf("dir button: up\n");
else if(event_joystick.value == 1)
printf("dir button: down\n");
else
printf("dir button: none\n");
}
break;
case EV_KEY:
for(i = 0; i < sizeof(button_map)/ sizeof(button_map[0]); i++)
{
if(event_joystick.code == button_map[i].button_code)
{
printf("button %s %s\n", button_map[i].button_name, button_state_table[event_joystick.value]);
}
}
break;
default:
break;
}
last_code = event_joystick.code;
}
else if(ret == 0)
{
printf("timeout\n");
}
else
{
printf("err\n");
close (fd);
return -1;
}
}
close (fd);
return 0;
}
执行测试程序后,随意拨动手柄的摇杆或按下手柄的按键,串口输出如下:
lelt rocker x=105, y =124
lelt rocker x=75, y =109
lelt rocker x=62, y =103
lelt rocker x=54, y =105
lelt rocker x=51, y =105
lelt rocker x=50, y =106
lelt rocker x=50, y =109
lelt rocker x=50, y =124
lelt rocker x=50, y =128
lelt rocker x=124, y =128
lelt rocker x=128, y =128
right rocker x=132, y =128
right rocker x=166, y =128
right rocker x=200, y =128
right rocker x=226, y =128
right rocker x=251, y =128
right rocker x=255, y =128
right rocker x=239, y =128
right rocker x=184, y =128
right rocker x=128, y =128
dir button: up
dir button: none
dir button: left
dir button: none
dir button: down
dir button: none
dir button: right
dir button: none
button X press
button X relese
button X press
button X hold
button X hold
button X hold
button X relese
button Y press
button Y relese
button LT press
button LT relese
button RT press
button RT relese
button LB press
button LB relese
button RB press
button RB relese