admin 管理员组文章数量: 1184232
一、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协议:
三、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特征值。
staticvoidchar_discovered_cb(uint8_t status, GSList *chars,void*user_data){/* ...... */elseif(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设备。核心代码如下:
staticvoidreport_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是否在已支持的列表中 ,比对失败就不会创建,具体函数如下:
inthid_add_device(structhid_device*hdev)/* ...... */if(hid_ignore_special_drivers){
hdev->group = HID_GROUP_GENERIC;}elseif(!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(structhid_device*hdev,conststructhid_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);}conststructhid_device_id*hid_match_id(structhid_device*hdev,conststructhid_device_id*id){for(; id->bus; id++)if(hid_match_one_id(hdev, id))return id;returnNULL;}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=1949Product=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 0000 483ffff 17aff32d bf544446 0 ffff0000 1 130f93 8b17c000 677bfa d9415fed e09effdf 1cfffff ffffffff fffffffe
B: REL=40
B: ABS=130627
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"/* 按键编码 */#defineBUTTON_CODE_LB0x0136#defineBUTTON_CODE_RB0x0137#defineBUTTON_CODE_LT0x0138#defineBUTTON_CODE_RT0x0139#defineBUTTON_CODE_SELECT0x013A#defineBUTTON_CODE_START0x013B#defineBUTTON_CODE_A0x0130#defineBUTTON_CODE_B0x0131#defineBUTTON_CODE_X0x0133#defineBUTTON_CODE_Y0x0134/* 左摇杆或右摇杆 */typedefenum{
ROCKER_LEFT,
ROCKER_RIGHT,
ROCKER_MAX,}RockerType;typedefstruct{uint8_t x;uint8_t y;}JsRocker;typedefstruct{uint16_t button_code;char*button_name;}Button;intmain(int argc,char**argv){structinput_event event_joystick ;structpollfd 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"}};constchar*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);elseif(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;elseif(event_joystick.code == ABS_Y)
rocker[ROCKER_LEFT].y = event_joystick.value;/* 右摇杆事件,需要等同步事件同时获取x和y坐标 */elseif(event_joystick.code == ABS_Z)
rocker[ROCKER_RIGHT].x = event_joystick.value;elseif(event_joystick.code == ABS_RZ)
rocker[ROCKER_RIGHT].y = event_joystick.value;/* 方向键 X方向有键被按下 */elseif(event_joystick.code == ABS_HAT0X){if(event_joystick.value ==-1)printf("dir button: left\n");elseif(event_joystick.value ==1)printf("dir button: right\n");elseprintf("dir button: none\n");}/* 方向键 Y方向有键被按下 */elseif(event_joystick.code == ABS_HAT0Y){if(event_joystick.value ==-1)printf("dir button: up\n");elseif(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;}elseif(ret ==0){printf("timeout\n");}else{printf("err\n");close(fd);return-1;}}close(fd);return0;}执行测试程序后,随意拨动手柄的摇杆或按下手柄的按键,串口输出如下:
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入门课:让蓝牙手柄连接不再难 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/p/1772596161a3557139.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论