使用BlueZ连接蓝牙手柄
一、HOGP協(xié)議
常見的藍牙鼠標、藍牙鍵盤、藍牙手柄,它們都屬于HID設(shè)備,但與有線設(shè)備不同的是,有線鼠標等設(shè)備屬于USB HID設(shè)備,而藍牙鼠標等設(shè)備屬于Bluetooth HID設(shè)備,即協(xié)議是一樣的,只是通信方式不同。HOGP是HID Over GATT Profile的縮寫,即藍牙HID設(shè)備是通過BLE的GATT來實現(xiàn)HID協(xié)議的。下圖是手機BLE調(diào)試APP掃描獲取到的手柄廣播信息,點擊"RAW"后可以看到原始的廣播數(shù)據(jù),解析結(jié)果如下:
- tpye 0x01:藍牙的FLAG信息,0x06表示設(shè)備僅支持BLE,不支持經(jīng)典藍牙,廣播類型為通用廣播。
- type 0x03:UUID16_ALL,0x1218是16位的HID服務(wù)的UUID,這里已經(jīng)初步表明設(shè)備是一個藍牙HID設(shè)備。
- type 0x19:GAP的apperance,即設(shè)備的外觀,0xC303表示設(shè)備是一個藍牙手柄(Joystick)。
- type 0x09:藍牙設(shè)備的全名,該手柄的設(shè)備名叫"269"。
連接藍牙手柄后,可以發(fā)現(xiàn)設(shè)備支持的服務(wù),其中一個服務(wù)是Human Interface Device,該服務(wù)也進一步表明了該設(shè)備是一個藍牙HID設(shè)備。Bluez在連接藍牙HID設(shè)備后,在發(fā)現(xiàn)服務(wù)時如果發(fā)現(xiàn)了HID服務(wù),就會讀取Report Map,這個是HID的報告描述符,通過解析這張表就可以知道設(shè)備支持哪些功能了,解析功能內(nèi)核會幫我們完成。
二、內(nèi)核配置
內(nèi)核對藍牙HID的支持分為2部分,一部分是藍牙部分,另一部分就是uhid。
在藍牙協(xié)議層使能HID協(xié)議:
內(nèi)核驅(qū)動中使能uhid:
三、HOGP原理
3.1 Bluez創(chuàng)建HID設(shè)備
當(dāng)主機連上藍牙手柄時,Bluez會發(fā)現(xiàn)PnP ID服務(wù),讀取PnP ID服務(wù)可以獲取設(shè)備的制造商信息,例如VIP和PID,串口會有相應(yīng)的打印。在向內(nèi)核注冊HID設(shè)備時,VIP和PID是非常重要的參數(shù)。
bluetoothd[536]: profiles/deviceinfo/dis.c:read_pnpid_cb() source: 0x01 vendor: 0x1949 product: 0x0402 version: 0x0000當(dāng)Bluez繼續(xù)發(fā)現(xiàn)服務(wù)時,會發(fā)現(xiàn)HID服務(wù),于是hog-lib.c中的char_discovered_cb函數(shù)會被調(diào)用,該函數(shù)會解析HID服務(wù)下所有特征值,其中有一部是比對report_map_uuid,report_map_uuid是0x2A4B,即在手機BLE調(diào)試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);} /* ...... */ }讀到該特征值后會回調(diào)report_map_read_cb函數(shù),該函數(shù)會打印設(shè)備的報表描述符,并向內(nèi)核申請創(chuàng)建HID設(shè)備。核心代碼如下:
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"); }相應(yīng)串口打印如下:
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 device3.2 內(nèi)核創(chuàng)建HID設(shè)備
正常來說,到現(xiàn)在為止,內(nèi)核中應(yīng)該已經(jīng)創(chuàng)建了藍牙手柄的input設(shè)備節(jié)點,但實際調(diào)試過程發(fā)現(xiàn)卻沒有,猜想應(yīng)該哪里失敗了,因此有必要深入了解下內(nèi)核對Bluez創(chuàng)建HID設(shè)備請求的處理流程。
在內(nèi)核配置中開啟uhid的支持后,會生成一個/dev/uhid設(shè)備節(jié)點,用戶層可以通過該文件操作hid操作,Bluez正是通過該文件向內(nèi)核注冊HID設(shè)備。具體來說,report_map_read_cb函數(shù)中的bt_uhid_send函數(shù)會/dev/uhid寫入一個UHID_CREATE消息,內(nèi)核驅(qū)動中的uhid.c中的uhid_char_write函數(shù)將會被調(diào)用,對于UHID_CREATE,uhid_char_write函數(shù)將會調(diào)用uhid_dev_create函數(shù)完成hid設(shè)備的創(chuàng)建。大致流程如下圖所示。
具體地,uhid_dev_create會喚醒專門添加uhid設(shè)備的工作隊列uhid_device_add_worker,該工作隊列會調(diào)用hid_add_device嘗試添加HID設(shè)備,hid_add_device函數(shù)會比對要注冊的設(shè)備的VIP和PID是否在已支持的列表中,比對失敗就不會創(chuàng)建,具體函數(shù)如下:
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是一個很大的數(shù)組,里面記錄了當(dāng)前已支持設(shè)備的HID類型(USB還是BLE)、VID、PID。調(diào)試過程中之所以創(chuàng)建HID設(shè)備失敗就是因為藍牙手柄的VIP和PID不在該設(shè)備列表中。修改方法有兩種:一是可以修改hid_have_special_driver數(shù)組,添加藍牙手柄的VID和PID;二是修改hid_match_one_id函數(shù),增加HID_GROUP_GENERIC的支持。修改完畢后,內(nèi)核成功創(chuàng)建手柄HID設(shè)備,內(nèi)核打印如下:
[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目錄,下面多了兩個輸入設(shè)備:event0和js0。解析event0即可獲取手柄的數(shù)據(jù)。
/ # 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=1f3.3 input子系統(tǒng)
Linux的input子系統(tǒng)框架如下圖所示,圖中沒有包含Bluetooth HID設(shè)備,但實際Bluetooth HID設(shè)備也適用于該框架。
當(dāng)向內(nèi)核注冊HID設(shè)備時,會觸發(fā)經(jīng)典的device和driver匹配機制,probe函數(shù)將被調(diào)用,具體調(diào)用關(guān)系如下:
hid_device_probehid_hw_starthid_connecthidinput_connecthidinput_allocatehid_device_probe函數(shù)在注冊HID設(shè)備時會被回調(diào),hidinput_allocate函數(shù)則申請了input_dev,注冊到input子系統(tǒng)。
整條數(shù)據(jù)鏈路如下:當(dāng)手柄的按鍵或搖桿被操作時,bluetoothd進程將收到手柄的notify數(shù)據(jù),bluetoothd通過uhid向HID系統(tǒng)發(fā)送UHID_INPUT消息,HID驅(qū)動會根據(jù)Report Map將數(shù)據(jù)轉(zhuǎn)換成對應(yīng)的input_event事件并上報,用戶層解析/dev/input目錄下對應(yīng)的文件即可獲取手柄的狀態(tài)。
四、手柄數(shù)據(jù)解析
手柄有多種模式:自定義模式和標準模式。在自定義模式下,用戶可以通過專用的APP來設(shè)置每個按鍵對應(yīng)的坐標,以此來靈活適配各種使用場景(例如適配王者榮耀的鍵位或英雄聯(lián)盟的鍵位)。在標準模式下,搖桿返回的是坐標值,而按鍵返回的則是按鍵值。
讀取手柄input_event消息并解析即可獲得手柄按鍵的坐標。手柄一共有三種不同的輸入:
測試代碼如下:
#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; }執(zhí)行測試程序后,隨意撥動手柄的搖桿或按下手柄的按鍵,串口輸出如下:
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總結(jié)
以上是生活随笔為你收集整理的使用BlueZ连接蓝牙手柄的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何培养自己的爱好兴趣
- 下一篇: word题注格式(从每章开始,如:图1-