使用BlueZ连接蓝牙手柄

  • Post author:
  • Post category:其他




一、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消息并解析即可获得手柄按键的坐标。手柄一共有三种不同的输入:

  1. 摇杆:摇杆一共有左右两个。摇杆的事件类型为EV_ABS,左摇杆X轴返回ABS_X类型坐标值,左摇杆Y轴返回ABS_Y类型坐标值;右摇杆X轴返回ABS_Z类型坐标值,右摇杆Y轴返回ABS_RZ类型坐标值。摇杆中心的坐标为(128,128),摇杆的左上角为坐标原点(0,0)。
  2. 方向键:方向键共有上下左右4个按键。方向键事件类型也为EV_ABS,其中左键和右键返回ABS_HAT0X类型数据,当值为-1时表示左键按下,当值为1时表示右键按下;当值为0时表示左右键没有被按下;同理,上键和下键返回ABS_HAT0Y类型数据,当值为-1时表示上键按下,当值为1时表示下键按下;当值为0时表示上下键没有被按下。
  3. 普通按键:普通按键包含了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



版权声明:本文为qq_27575841原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。