一、HOGP协议
常见的蓝牙鼠标、蓝牙键盘、蓝牙手柄,它们都属于HID设备,但与有线设备不同的是,有线鼠标等设备属于USB HID设备,而蓝牙鼠标等设备属于Bluetooth HID设备,即协议是一样的,只是通信方式不同。HOGP是HID Over GATT Profile的缩写,即蓝牙HID设备是通过BLE的GATT来实现HID协议的。下图是手机BLE调试APP扫描获取到的手柄广播信息,点击”RAW”后可以看到原始的广播数据,解析结果如下:
- tpye 0x01:蓝牙的FLAG信息,0x06表示设备仅支持BLE,不支持经典蓝牙,广播类型为通用广播。
- type 0x03:UUID16_ALL,0x1218是16位的HID服务的UUID,这里已经初步表明设备是一个蓝牙HID设备。
- type 0x19:GAP的apperance,即设备的外观,0xC303表示设备是一个蓝牙手柄(Joystick)。
- type 0x09:蓝牙设备的全名,该手柄的设备名叫”269″。
连接蓝牙手柄后,可以发现设备支持的服务,其中一个服务是Human Interface Device,该服务也进一步表明了该设备是一个蓝牙HID设备。Bluez在连接蓝牙HID设备后,在发现服务时如果发现了HID服务,就会读取Report Map,这个是HID的报告描述符,通过解析这张表就可以知道设备支持哪些功能了,解析功能内核会帮我们完成。
二、内核配置
内核对蓝牙HID的支持分为2部分,一部分是蓝牙部分,另一部分就是uhid。
在蓝牙协议层使能HID协议:
内核驱动中使能uhid:
三、HOGP原理
3.1 Bluez创建HID设备
当主机连上蓝牙手柄时,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
3.2 内核创建HID设备
正常来说,到现在为止,内核中应该已经创建了蓝牙手柄的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
3.3 input子系统
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消息并解析即可获得手柄按键的坐标。手柄一共有三种不同的输入:
- 摇杆:摇杆一共有左右两个。摇杆的事件类型为EV_ABS,左摇杆X轴返回ABS_X类型坐标值,左摇杆Y轴返回ABS_Y类型坐标值;右摇杆X轴返回ABS_Z类型坐标值,右摇杆Y轴返回ABS_RZ类型坐标值。摇杆中心的坐标为(128,128),摇杆的左上角为坐标原点(0,0)。
- 方向键:方向键共有上下左右4个按键。方向键事件类型也为EV_ABS,其中左键和右键返回ABS_HAT0X类型数据,当值为-1时表示左键按下,当值为1时表示右键按下;当值为0时表示左右键没有被按下;同理,上键和下键返回ABS_HAT0Y类型数据,当值为-1时表示上键按下,当值为1时表示下键按下;当值为0时表示上下键没有被按下。
- 普通按键:普通按键包含了X、Y、A、B、LB、RB、LT、RT、Select、Start这10个键。事件类型为EV_KEY,数据类型即为键值,例如0x0130表示A键,当值为0时表示该按键处于弹起状态,当值为1时表示该按键正在被按下(触发),当值为2时表示该按键处于被长按的状态。
测试代码如下:
#include <stdio.h>
#include "string.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <poll.h>
#include <unistd.h>
#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