使用BlueZ连接蓝牙手柄
一、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消息并解析即可获得手柄按键的坐标。手柄一共有三种不同的输入:
- 摇杆:摇杆一共有左右两个。摇杆的事件类型为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");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连接蓝牙手柄相关推荐
- M401 Emuelec 连接蓝牙手柄
本文参考自: https://www.znds.com/tv-1154224-1-1.html 大家去顶帖 1,ssh到emuelec主机(这里使用putty),emuelec的默认凭据是用户名roo ...
- Android 蓝牙手柄连接流程解析和自动化方案
为了提高蓝牙手柄的连接成功率,实现自动连接蓝牙手柄,替代用户手动连接蓝牙手柄的整个流程. 首先,我们将"连接蓝牙手柄"这个步骤拆分开来,可以细分为搜索.识别.配对.连接四个步骤.为 ...
- 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(一)各种尝试(1)
这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给这一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 前几天从网上买了个TPMini大眼睛,配置比我的 ...
- BlueZ双模蓝牙音频卡顿问题优化
一.问题现象 由于设备支持双模蓝牙,设备的BLE需求中,既需要支持作为从机被手机等设备连接,也支持作为主机连接蓝牙手柄等外设,即在播放音频时,允许同时进行低功耗蓝牙相关的功能.实际开发过程中发现在播放 ...
- 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(三)开发环境
这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 上文说到,发现了另一条出路:linmctool.看 ...
- 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄 文章索引以及其它
这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 这一套文章是我自娱自乐的一个小项目"Si ...
- 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(四)围绕linmctool挖掘SixAxis通讯协议
这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给那一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. 上文说到,linmctool编译后连接手柄成功,各 ...
- 一不小心踏进Android开发: TPMini大眼睛使用PS3蓝牙手柄(二)各种尝试(2)
这是一套连载文章,用以记录大眼睛连接蓝牙手柄的全过程.谨以此文献给这一周我缺失的睡眠. 此文的性质相当于(公开的)个人日记,未经本人允许,请勿转载. (接上文) 既然要回到hidd的正途,那就得先具备 ...
- 树莓派3 蓝牙连接 PS3手柄
网上没有直接用3蓝牙连接树莓派的教程,看到一篇用2b 和 蓝牙适配器一起用 连接PS3 手柄的,所以先安装一下试试. http://tieba.baidu.com/p/3237051512 下面就是安 ...
最新文章
- R语言使用treemap包中的treemap函数可视化treemap图:treemap将分层数据显示为一组嵌套矩形、自定义设置treemap图的调色板、自定义设置treemap标题字体的大小
- centos7安装tomcat_手把手教你,使用 Nginx 搭配 Tomcat 实现负载均衡!
- 安卓使用 HTTP 协议访问网络
- java7 xp版下载64位_JRE7 64位下载|JRE7 64位(java运行环境) V1.7.0.65官方版
- react usecontext_Vue3原理实战运用,我用40行代码把他装进了React做状态管理
- 企业微信H5_网页jssdk调用 config和agentconfig的区别
- python queue windows_python Queue模块
- 整型和浮点型之间的转化
- Spring学习总结(11)——Spring JMS MessageConverter介绍
- servlet request参数只能取一次解决方法
- 配置IIS Express 7.5以允许外部访问
- 用 JavaScript 实现内存位翻转漏洞
- 浅谈单片机工程师职业规划
- 计算机教室的英文音标,小学四年级英语单词(带音标).doc
- 简单 Quartz定时器使用 入门
- python实现xlsx批量转xls(或者xls批量转xlsx)
- IFR202型红外雨量传感器非接触式检测降雨量的传感器
- java多边形合并_geotools实现多边形的合并缓冲区
- 哪些场景N1 mode是disable状态
- oracle 报12560,UNIX系统中Oracle报TNS-12560错误的解决思路
热门文章
- 关于报错ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging
- python应用seo_SEO快排技术和应用技术编程大全
- 程序员又背锅了!虾米音乐代码注释惊现“穷逼vip”
- 比子弹速度快十倍的导弹是怎么被拦截的?
- Python爬虫之爬取图片
- JPEG系列二 JPEG文件中的EXIF(上)
- 谷歌为AI再上“紧箍咒”:道德团队审查产品,确保AI不作恶
- Linux下查看隐藏文件夹
- 0基础跟班学习前端的第三天(因为上完一次课需要上一天自习啦~)内容整理归纳还有附带的小练习~希望大家多动手练习(二)
- [C++]Inside C++对象模型:第三、四、五章笔记