有时候在家里用手机或者Pad打字时觉得太慢,很希望有个蓝牙键盘用用。可惜家里只有一个USB蓝牙适配器,没有蓝牙键盘。本想在Windows上实现一个虚拟蓝牙键鼠的软件,把我这个BenQ海湾键盘变成蓝牙键盘,但后来发现微软的Bluetooth协议栈实在太烂,难以实现这个功能。于是退而求其次,看看在Ubuntu上用Bluez协议栈怎么来实现。
首先用简短的文字来聊聊Bluetooth的HID协议。Bth HID协议里面有2个角色:Host主机和Device设备。主机通常是PC、笔记本或者平板电脑甚至手机,而设备就是蓝牙的鼠标、键盘或者游戏手柄之类的输入设备。
蓝牙设备通过SDP协议,增加一个SDP Record用以对外宣告自己提供HID服务,并在对应的SDP Record中附带HID Report Descriptor数据块。这个所谓的“HID报告描述符”说明了该设备所发出的数据格式。与此同时,蓝牙设备必须在0x11和0x13这两个PSM上监听L2CAP信道,并接受从蓝牙主机端发来的L2CAP连接请求。
蓝牙主机通过SDP协议发现周围的蓝牙设备,蓝牙配对成功后,如果主机发现蓝牙设备支持HID服务,则主机会主动向蓝牙设备的0x11和0x13这两个PSM发起L2CAP连接。一旦建立连接,则PSM 0x11将作为HID Control端口,而PSM 0x13则作为HID Interrupt端口。此后,蓝牙设备即可通过HID Interrupt端口向蓝牙主机发送HID数据包,而这些数据包的格式必须符合HID Report Descriptor的约定。
因为我不清楚的某些原因,Bluez的HID Host服务也占用了0x11和0x13这两个PSM,这导致我在Bluez HID Host Service存在时,不能再次打开并监听它们。所以首先我得先停掉Bluez的HID Host服务:打开/etc/bluetooth/main.conf,解除第2行代码的注释,或者在第一行代码之前插入:
DisablePlugins = input
然后重启Bluez(推荐直接重启系统):
sudo service bluetooth restart
这样,我就可以随意使用0x11和0x13这两个PSM了。
其实Python有个叫pybluez的Packet,封装了Bluetooth的接口,理论上对我有很大帮助。但当我试图打开、绑定并监听0x11和0x13这两个PSM时,listen函数报错,告诉我不能监听0x11这个PSM。这就奇怪了,因为此时Bluez的HID Host服务已经关闭,我直接用C语言编程是可以在0x11和0x13上建立socket并绑定、监听、接受连接且连接成功的。所以果断放弃这个Packet,决定用C语言写个简单的.so文件给Python来调用。(以下代码部分参考了hidclient.c这个开源代码)
首先我们来写一个SDP HID Helper函数库,把构造SDP HID Record的繁琐步骤封装一下,只暴露必要的接口。
SDP Record由一系列Attribute组成,每个Attribute有自己的ID和数据结构,不同的AttributeID代表着不同的含义,比如0x0001代表ServiceClassIDList,而0x0004代表ProtocolDescriptorList等等。Attribute数据结构的基础元素是“类型-值”的对,多个基础元素可以组合成一个列表(Sequence),列表中的元素可以是基础元素也可以是列表。所以,特别是对于某些数据结构比较复杂的Attribute来说,组成一个合法的Attribute数据块也挺费事。
组成一个表示HID服务的SDP Record,大约需要二十几个Attribute数据。我先把这些Attribute数据的构造函数实现。
#include <errno.h> #include <stdlib.h> #include <bluetooth/bluetooth.h> #include <bluetooth/hci.h> #include <bluetooth/sdp.h> #include <bluetooth/sdp_lib.h> #define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) #define PSMHIDCTL 0x11 #define PSMHIDINT 0x13 //ATTR 0x0001 static void my_sdp_set_ServiceClassIDList(sdp_record_t *record) { uuid_t hidsvcls_uuid; sdp_list_t *svclass_id; sdp_uuid16_create(&hidsvcls_uuid, HID_SVCLASS_ID); svclass_id = sdp_list_append(0, &hidsvcls_uuid); sdp_set_service_classes(record, svclass_id); } //ATTR 0x0004 static void my_sdp_set_ProtocolDescriptorList(sdp_record_t *record) { uuid_t hidp_uuid, l2cap_uuid; sdp_list_t *proto_l2cap, *proto_hidp, *apseq, *aproto; sdp_data_t *psm; unsigned short ctrl = PSMHIDCTL; sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); proto_l2cap = sdp_list_append(0, &l2cap_uuid); psm = sdp_data_alloc(SDP_UINT16, &ctrl); proto_l2cap = sdp_list_append(proto_l2cap, psm); sdp_uuid16_create(&hidp_uuid, HIDP_UUID); proto_hidp = sdp_list_append(0, &hidp_uuid); apseq = sdp_list_append(0, proto_l2cap); apseq = sdp_list_append(apseq, proto_hidp); aproto = sdp_list_append(0, apseq); sdp_set_access_protos(record, aproto); } //ATTR 0x0005 static void my_sdp_set_BrowseGroupList(sdp_record_t *record) { uuid_t browsegroup_uuid; sdp_list_t *browsegroup_seq; sdp_uuid16_create(&browsegroup_uuid, PUBLIC_BROWSE_GROUP); browsegroup_seq = sdp_list_append(0, &browsegroup_uuid); sdp_set_browse_groups(record, browsegroup_seq); } //ATTR 0x0006 static void my_sdp_set_LanguageBaseAttributeIDList(sdp_record_t *record) { sdp_lang_attr_t base_lang; sdp_list_t *langs = 0; base_lang.code_ISO639 = (0x65 << 8) | 0x6e; base_lang.encoding = 106; base_lang.base_offset = SDP_PRIMARY_LANG_BASE; langs = sdp_list_append(0, &base_lang); sdp_set_lang_attr(record, langs); sdp_list_free(langs, 0); } //ATTR 0x0009 static void my_sdp_set_BluetoothProfileDescriptorList(sdp_record_t *record) { sdp_profile_desc_t profile; sdp_list_t *pfseq; sdp_uuid16_create(&profile.uuid, HID_PROFILE_ID); profile.version = 0x0100; pfseq = sdp_list_append(0, &profile); sdp_set_profile_descs(record, pfseq); } //ATTR 0x000d static void my_sdp_set_AdditionalProtocolDescriptorLists(sdp_record_t *record) { uuid_t hidp_uuid, l2cap_uuid; sdp_list_t *proto_l2cap, *proto_hidp, *apseq, *aproto; sdp_data_t *psm; unsigned short intr = PSMHIDINT; sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID); proto_l2cap = sdp_list_append(0, &l2cap_uuid); psm = sdp_data_alloc(SDP_UINT16, &intr); proto_l2cap = sdp_list_append(proto_l2cap, psm); sdp_uuid16_create(&hidp_uuid, HIDP_UUID); proto_hidp = sdp_list_append(0, &hidp_uuid); apseq = sdp_list_append(0, proto_l2cap); apseq = sdp_list_append(apseq, proto_hidp); aproto = sdp_list_append(0, apseq); sdp_set_add_access_protos(record, aproto); } //ATTR 0x0100 static void my_sdp_set_ServiceName(sdp_record_t *record, const char *name) { sdp_attr_add_new(record, SDP_ATTR_SVCNAME_PRIMARY, SDP_TEXT_STR8, name); } //ATTR 0x0101 static void my_sdp_set_ServiceDescription(sdp_record_t *record, const char *desc) { sdp_attr_add_new(record, SDP_ATTR_SVCDESC_PRIMARY, SDP_TEXT_STR8, desc); } //ATTR 0x0102 static void my_sdp_set_ProviderName(sdp_record_t *record, const char *prov) { sdp_attr_add_new(record, SDP_ATTR_PROVNAME_PRIMARY, SDP_TEXT_STR8, prov); } //ATTR 0x0200 static void my_sdp_set_HID_DeviceReleaseNumber(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_DEVICE_RELEASE_NUMBER, SDP_UINT16, &val); } //ATTR 0x0201 static void my_sdp_set_HID_ParserVersion(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_PARSER_VERSION, SDP_UINT16, &val); } //ATTR 0x0202 static void my_sdp_set_HID_DeviceSubClass(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_DEVICE_SUBCLASS, SDP_UINT8, &val); } //ATTR 0x0203 static void my_sdp_set_HID_CountryCode(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_COUNTRY_CODE, SDP_UINT8, &val); } //ATTR 0x0204 static void my_sdp_set_HID_VirtualCable(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_VIRTUAL_CABLE, SDP_BOOL, &val); } //ATTR 0x0205 static void my_sdp_set_HID_ReconnectInitiate(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_RECONNECT_INITIATE, SDP_BOOL, &val); } /* data - hid report descriptor data string. len - data length in byte. */ //ATTR 0x0206 static void my_sdp_set_HID_DescriptorList(sdp_record_t *record, void *data, int len) { void *dtds[2]; void *values[2]; unsigned char dtd2=SDP_UINT8; unsigned char hid_spec_type=0x22; unsigned char dtd_data=SDP_TEXT_STR8; int leng[2]; sdp_data_t *hid_spec_lst; sdp_data_t *hid_spec_lst2; dtds[0] = &dtd2; values[0] = &hid_spec_type; dtds[1] = &dtd_data; values[1] = data; leng[0] = 0; leng[1] = len; hid_spec_lst = sdp_seq_alloc_with_length(dtds, values, leng, 2); hid_spec_lst2 = sdp_data_alloc(SDP_SEQ8, hid_spec_lst); sdp_attr_add(record, SDP_ATTR_HID_DESCRIPTOR_LIST, hid_spec_lst2); } /* hid_attr_langs - array of uint16. size - how many elements in the array. */ //ATTR 0x0207 static void my_sdp_set_HID_LangIdBaseList(sdp_record_t *record, unsigned short *hid_attr_langs, int size) { void *dtds2[2]; unsigned char dtd = SDP_UINT16; void *values2[2]; sdp_data_t *lang_lst; sdp_data_t *lang_lst2; int i; for (i = 0; i < size; i++) { dtds2[i] = &dtd; values2[i] = &hid_attr_langs[i]; } lang_lst = sdp_seq_alloc(dtds2, values2, size); lang_lst2 = sdp_data_alloc(SDP_SEQ8, lang_lst); sdp_attr_add(record, SDP_ATTR_HID_LANG_ID_BASE_LIST, lang_lst2); } //ATTR 0x0208 static void my_sdp_set_HID_SdpDisable(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_SDP_DISABLE, SDP_BOOL, &val); } //ATTR 0x0209 static void my_sdp_set_HID_BatteryPower(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_BATTERY_POWER, SDP_BOOL, &val); } //ATTR 0x020a static void my_sdp_set_HID_RemoteWakeUp(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_REMOTE_WAKEUP, SDP_BOOL, &val); } //ATTR 0x020b static void my_sdp_set_HID_ProfileVersion(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_PROFILE_VERSION, SDP_UINT16, &val); } //ATTR 0x020c static void my_sdp_set_HID_SuperVersionTimeout(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_SUPERVISION_TIMEOUT, SDP_UINT16, &val); } //ATTR 0x020d static void my_sdp_set_HID_NormallyConnectable(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_NORMALLY_CONNECTABLE, SDP_BOOL, &val); } //ATTR 0x020e static void my_sdp_set_HID_BootDevice(sdp_record_t *record, unsigned int val) { sdp_attr_add_new(record, SDP_ATTR_HID_BOOT_DEVICE, SDP_BOOL, &val); }
第一个参数 sdp_record_t *record 是Bluez声明的SDP Record数据结构。sdp_开头的函数也都是Bluez提供的sdp相关接口。
这些函数都是static,没错,我就没打算让上层逻辑看到它们,因为它们太底层,太细节了。现在,我就用这些工具函数来实现一层更友好的SDP HID Record接口。
struct hidc_info_t { char * service_name; char * description; char * provider; char * desc_data; //USB HID Report Descriptor data unsigned int desc_len; unsigned int sub_class; }; static unsigned short g_hid_attr_lang[] = {0x409, 0x100}; // With 0xffffffff, we get assigned the first free record >= 0x10000 // Make HID service visible (add to PUBLIC BROWSE GROUP) sdp_record_t *SdpCreateRecord(unsigned int handle) { sdp_record_t *record = calloc(1, sizeof(sdp_record_t)); record->handle = handle; return record; } void SdpDestroyRecord(sdp_record_t *record) { if (record) free(record); } sdp_record_t *SdpMakeUpHidRecord(sdp_record_t *record, struct hidc_info_t *hidc_info) { my_sdp_set_ServiceClassIDList(record); my_sdp_set_ProtocolDescriptorList(record); my_sdp_set_BrowseGroupList(record); my_sdp_set_LanguageBaseAttributeIDList(record); my_sdp_set_BluetoothProfileDescriptorList(record); my_sdp_set_AdditionalProtocolDescriptorLists(record); my_sdp_set_HID_DeviceReleaseNumber(record, 0x0100); my_sdp_set_HID_ParserVersion(record, 0x0111); my_sdp_set_HID_CountryCode(record, 0x21); my_sdp_set_HID_VirtualCable(record, 1); my_sdp_set_HID_ReconnectInitiate(record, 1); my_sdp_set_HID_LangIdBaseList(record, g_hid_attr_lang, ARRAY_SIZE(g_hid_attr_lang)); my_sdp_set_HID_SdpDisable(record, 0); my_sdp_set_HID_BatteryPower(record, 1); my_sdp_set_HID_RemoteWakeUp(record, 1); my_sdp_set_HID_ProfileVersion(record, 0x0100); my_sdp_set_HID_SuperVersionTimeout(record, 0x0c80); my_sdp_set_HID_NormallyConnectable(record, 0); my_sdp_set_HID_BootDevice(record, 1); if (hidc_info->service_name) my_sdp_set_ServiceName(record, hidc_info->service_name); if (hidc_info->description) my_sdp_set_ServiceDescription(record, hidc_info->description); if (hidc_info->provider) my_sdp_set_ProviderName(record, hidc_info->provider); my_sdp_set_HID_DeviceSubClass(record, hidc_info->sub_class); my_sdp_set_HID_DescriptorList(record, hidc_info->desc_data, hidc_info->desc_len); return record; } int SdpRegisterRecord(sdp_record_t *record) { sdp_session_t *session; // Connect to SDP server on localhost, to publish service information session = sdp_connect(BDADDR_ANY, BDADDR_LOCAL, 0); if (!session) { fprintf(stderr, "Failed to connect to SDP server: %s\n", strerror(errno)); return 1; } // Submit our IDEA of a SDP record to the "sdpd" if (sdp_record_register(session, record, SDP_RECORD_PERSIST) < 0) { fprintf (stderr, "Service Record registration failed\n"); return -1; } fprintf (stdout, "HID keyboard/mouse service registered. handle=0x%x\n", record->handle); return 0; } void SdpUnregisterRecord(unsigned int handle) { unsigned int range=0x0000ffff; sdp_list_t * attr; sdp_session_t * session; sdp_record_t * record; // Connect to the local SDP server session = sdp_connect(BDADDR_ANY, BDADDR_LOCAL, 0); if (!session) return; attr = sdp_list_append(0, &range); record = sdp_service_attr_req(session, handle, SDP_ATTR_REQ_RANGE, attr); sdp_list_free(attr, 0); if (!record) { sdp_close(session); return; } sdp_device_record_unregister(session, BDADDR_ANY, record); sdp_close(session); return; }
亲爱的观众朋友们,现在我只需要构造一个struct hidc_info_t结构体,然后顺次调用SdpCreateRecord()、SdpMakeUpHidRecord()、SdpRegisterRecord()以及SdpDestroyRecord()就可以向Bluez的SDP服务中添加一条表示HID服务的SDP Record了。这其中,最关键的是准备一个HID Report Descriptor数组。
//============================================================================== // Sample code //============================================================================== #define HIDINFO_NAME "Bluetooth HID Test" #define HIDINFO_PROV "Huipeng Zhao" #define HIDINFO_DESC "Test Bluetooth HID" // HID-Record for virtual keyboard and mouse device static unsigned char g_hid_report_desc_moukbd[] = { 0x05, 0x01, // UsagePage GenericDesktop 0x09, 0x02, // Usage Mouse 0xA1, 0x01, // Collection Application 0x85, 0x01, // REPORT ID: 1 0x09, 0x01, // Usage Pointer 0xA1, 0x00, // Collection Physical 0x05, 0x09, // UsagePage Buttons 0x19, 0x01, // UsageMinimum 1 0x29, 0x03, // UsageMaximum 3 0x15, 0x00, // LogicalMinimum 0 0x25, 0x01, // LogicalMaximum 1 0x75, 0x01, // ReportSize 1 0x95, 0x03, // ReportCount 3 0x81, 0x02, // Input data variable absolute 0x75, 0x05, // ReportSize 5 0x95, 0x01, // ReportCount 1 0x81, 0x01, // InputConstant (padding) 0x05, 0x01, // UsagePage GenericDesktop 0x09, 0x30, // Usage X 0x09, 0x31, // Usage Y 0x09, 0x38, // Usage ScrollWheel 0x15, 0x81, // LogicalMinimum -127 0x25, 0x7F, // LogicalMaximum +127 0x75, 0x08, // ReportSize 8 0x95, 0x02, // ReportCount 3 0x81, 0x06, // Input data variable relative 0xC0, 0xC0, // EndCollection EndCollection 0x05, 0x01, // UsagePage GenericDesktop 0x09, 0x06, // Usage Keyboard 0xA1, 0x01, // Collection Application 0x85, 0x02, // REPORT ID: 2 0xA1, 0x00, // Collection Physical 0x05, 0x07, // UsagePage Keyboard 0x19, 0xE0, // UsageMinimum 224 0x29, 0xE7, // UsageMaximum 231 0x15, 0x00, // LogicalMinimum 0 0x25, 0x01, // LogicalMaximum 1 0x75, 0x01, // ReportSize 1 0x95, 0x08, // ReportCount 8 0x81, 0x02, // **Input data variable absolute 0x95, 0x08, // ReportCount 8 0x75, 0x08, // ReportSize 8 0x15, 0x00, // LogicalMinimum 0 0x25, 0x65, // LogicalMaximum 101 0x05, 0x07, // UsagePage Keycodes 0x19, 0x00, // UsageMinimum 0 0x29, 0x65, // UsageMaximum 101 0x81, 0x00, // **Input DataArray 0xC0, 0xC0, // EndCollection }; struct hidc_info_t g_hidc_info = { .service_name = HIDINFO_NAME, .description = HIDINFO_DESC, .provider = HIDINFO_PROV, .desc_data = (char*)&g_hid_report_desc_moukbd, .desc_len = sizeof(g_hid_report_desc_moukbd), .sub_class = 0x80, }; static int sample_register_sdp_record(void) { sdp_record_t *record; record = (sdp_record_t *)SdpCreateRecord(0xffffffff); SdpMakeUpHidRecord(record, &g_hidc_info); SdpRegisterRecord(record); SdpDestroyRecord(record); return 0; } int main(void) { sample_register_sdp_record(); return 0; }
好,以上就是我的sdp_helper.c的全部内容,现在我来编译它。
gcc -o sdphelper sdp_helper.c -lbluetooth -Wall
当然,编译通过的前提是Ubuntu系统已经安装了Bluez的开发环境,安装方法:
sudo apt-get install libbluetooth-dev
在运行它之前,我打算先运用Bluez提供的sdptool工具,清除所有SDP Record,这样在运行sdphelper之后,Bluez就只有我新加入的一个SDP Record了,不会显得太乱。
sdptool del 0x10000 sdptool del 0x10001 ......
可以先用 sdptool browse local 命令查看当前有多少条SDP Record。
好了,现在我终于要执行sdphelper了。
./sdphelper
它确实产生效果了吗?用 sdptool browse 命令和 sdptool records 命令看一下:
ubuntu$ sdptool browse local Browsing FF:FF:FF:00:00:00 ... Service Name: Bluetooth HID Test Service Description: Test Bluetooth HID Service Provider: Huipeng Zhao Service RecHandle: 0x10000 Service Class ID List: "Human Interface Device" (0x1124) Protocol Descriptor List: "L2CAP" (0x0100) PSM: 17 "HIDP" (0x0011) Language Base Attr List: code_ISO639: 0x656e encoding: 0x6a base_offset: 0x100 Profile Descriptor List: "Human Interface Device" (0x1124) Version: 0x0100 ubuntu$ sdptool records --raw local Sequence Attribute 0x0000 - ServiceRecordHandle UINT32 0x00010000 Attribute 0x0001 - ServiceClassIDList Sequence UUID16 0x1124 - HumanInterfaceDeviceService (HID) Attribute 0x0004 - ProtocolDescriptorList Sequence Sequence UUID16 0x0100 - L2CAP UINT16 0x0011 Sequence UUID16 0x0011 - HIDP Attribute 0x0005 - BrowseGroupList Sequence UUID16 0x1002 - PublicBrowseGroup Attribute 0x0006 - LanguageBaseAttributeIDList Sequence UINT16 0x656e UINT16 0x006a UINT16 0x0100 Attribute 0x0009 - BluetoothProfileDescriptorList Sequence Sequence UUID16 0x1124 - HumanInterfaceDeviceService (HID) UINT16 0x0100 Attribute 0x000d - AdditionalProtocolDescriptorLists Sequence Sequence Sequence UUID16 0x0100 - L2CAP UINT16 0x0013 Sequence UUID16 0x0011 - HIDP Attribute 0x0100 String Bluetooth HID Test Attribute 0x0101 String Test Bluetooth HID Attribute 0x0102 String Huipeng Zhao Attribute 0x0200 UINT16 0x0100 Attribute 0x0201 UINT16 0x0111 Attribute 0x0202 UINT8 0x80 Attribute 0x0203 UINT8 0x21 Attribute 0x0204 Bool True Attribute 0x0205 Bool True Attribute 0x0206 Sequence Sequence UINT8 0x22 Data 05 01 09 02 a1 01 85 01 09 01 a1 00 05 09 19 01 29 03 15 00 25 01 75 01 95 03 81 02 75 05 95 01 81 01 05 01 09 30 09 31 09 38 15 81 25 7f 75 08 95 02 81 06 c0 c0 05 01 09 06 a1 01 85 02 a1 00 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 95 08 75 08 15 00 25 65 05 07 19 00 29 65 81 00 c0 c0 00 Attribute 0x0207 Sequence Sequence UINT16 0x0409 UINT16 0x0100 Attribute 0x0208 Bool False Attribute 0x0209 Bool True Attribute 0x020a Bool True Attribute 0x020b UINT16 0x0100 Attribute 0x020c UINT16 0x0c80 Attribute 0x020d Bool False Attribute 0x020e Bool True
当当当——党!OK,跟我们代码里写的一样!
如果拿另一台带蓝牙模块的PC发现我的Ubuntu,蓝牙配对之后,这台PC就会主动向Ubuntu的PSM 0x11和0x13发起L2CAP连接,可以用hcidump工具来验证,此处就略过了先。所以接下来我就要实现绑定、监听和接受L2CAP连接请求的代码了。注意,hcidump工具并不是Bluez软件包自带的,需要自己apt-get install一下。