一、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_probehid_hw_starthid_connecthidinput_connecthidinput_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");elseprintf("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");elseprintf("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

使用BlueZ连接蓝牙手柄相关推荐

  1. M401 Emuelec 连接蓝牙手柄

    本文参考自: https://www.znds.com/tv-1154224-1-1.html 大家去顶帖 1,ssh到emuelec主机(这里使用putty),emuelec的默认凭据是用户名roo ...

  2. Android 蓝牙手柄连接流程解析和自动化方案

    为了提高蓝牙手柄的连接成功率,实现自动连接蓝牙手柄,替代用户手动连接蓝牙手柄的整个流程. 首先,我们将"连接蓝牙手柄"这个步骤拆分开来,可以细分为搜索.识别.配对.连接四个步骤.为 ...

  3. 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(一)各种尝试(1)

    这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给这一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 前几天从网上买了个TPMini大眼睛,配置比我的 ...

  4. BlueZ双模蓝牙音频卡顿问题优化

    一.问题现象 由于设备支持双模蓝牙,设备的BLE需求中,既需要支持作为从机被手机等设备连接,也支持作为主机连接蓝牙手柄等外设,即在播放音频时,允许同时进行低功耗蓝牙相关的功能.实际开发过程中发现在播放 ...

  5. 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(三)开发环境

    这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 上文说到,发现了另一条出路:linmctool.看 ...

  6. 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄 文章索引以及其它

    这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 这一套文章是我自娱自乐的一个小项目"Si ...

  7. 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(四)围绕linmctool挖掘SixAxis通讯协议

    这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 上文说到,linmctool编译后连接手柄成功,各 ...

  8. 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(二)各种尝试(2)

    这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给这一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. (接上文) 既然要回到hidd的正途,那就得先具备 ...

  9. 树莓派3 蓝牙连接 PS3手柄

    网上没有直接用3蓝牙连接树莓派的教程,看到一篇用2b 和 蓝牙适配器一起用 连接PS3 手柄的,所以先安装一下试试. http://tieba.baidu.com/p/3237051512 下面就是安 ...

最新文章

  1. R语言使用treemap包中的treemap函数可视化treemap图:treemap将分层数据显示为一组嵌套矩形、自定义设置treemap图的调色板、自定义设置treemap标题字体的大小
  2. centos7安装tomcat_手把手教你,使用 Nginx 搭配 Tomcat 实现负载均衡!
  3. 安卓使用 HTTP 协议访问网络
  4. java7 xp版下载64位_JRE7 64位下载|JRE7 64位(java运行环境) V1.7.0.65官方版
  5. react usecontext_Vue3原理实战运用,我用40行代码把他装进了React做状态管理
  6. 企业微信H5_网页jssdk调用 config和agentconfig的区别
  7. python queue windows_python Queue模块
  8. 整型和浮点型之间的转化
  9. Spring学习总结(11)——Spring JMS MessageConverter介绍
  10. servlet request参数只能取一次解决方法
  11. 配置IIS Express 7.5以允许外部访问
  12. 用 JavaScript 实现内存位翻转漏洞
  13. 浅谈单片机工程师职业规划
  14. 计算机教室的英文音标,小学四年级英语单词(带音标).doc
  15. 简单 Quartz定时器使用 入门
  16. python实现xlsx批量转xls(或者xls批量转xlsx)
  17. IFR202型红外雨量传感器非接触式检测降雨量的传感器
  18. java多边形合并_geotools实现多边形的合并缓冲区
  19. 哪些场景N1 mode是disable状态
  20. oracle 报12560,UNIX系统中Oracle报TNS-12560错误的解决思路

热门文章

  1. 关于报错ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging
  2. python应用seo_SEO快排技术和应用技术编程大全
  3. 程序员又背锅了!虾米音乐代码注释惊现“穷逼vip”
  4. 比子弹速度快十倍的导弹是怎么被拦截的?
  5. Python爬虫之爬取图片
  6. JPEG系列二 JPEG文件中的EXIF(上)
  7. 谷歌为AI再上“紧箍咒”:道德团队审查产品,确保AI不作恶
  8. Linux下查看隐藏文件夹
  9. 0基础跟班学习前端的第三天(因为上完一次课需要上一天自习啦~)内容整理归纳还有附带的小练习~希望大家多动手练习(二)
  10. [C++]Inside C++对象模型:第三、四、五章笔记