最近拿着蓝牙自拍杆出去拍照时,突然想了解下其中的原理,写下了自己的学习过程,本文测试平台为瑞昱8762C。
手机在拍照模式下按音量调节键就可以触发快门按下拍照,蓝牙自拍杆的原理也是通过模拟音量键触发,从而实现远程控制拍照。
首先用nrf connect看下手上的蓝牙自拍杆的广播包和服务是怎么样的,如下:
广播:
服务:
即对于HID设备,广播包数据中需要有HID设备的服务UUID(0x1812)和设备外观(0x03C1(Keyboard))。
服务中需要有Human Interface Device服务和Battery Service服务。蓝牙自拍杆通过Report特性Notify数据到手机。
同时还注意到,连接上蓝牙自拍杆后手机需要能够与蓝牙自拍杆进行连接和绑定,并且在连接后自拍杆的蓝牙广播消失,即功能上只能被一个设备连接。
SDK目录sdk\inc\bluetooth\profile\server下有好几个HID设备的例程可以直接套用,其实像是
广播和服务这些都没什么可讲的,是定好的标准。就是关于 hid report map部分可以讲讲,HID设备被连接上后,HOST会获取hid report map来确定这个设备具有哪些功能(比如每次上报数据有几个字节,有什么按键,每个按键对应上报数据的哪个字节和实现什么功能等等)。所以重点就是了解hid report map,下面附上一张自己用“HID Descriptor tool”工具(保存为.h文件)生成的hid report map:
// 蓝牙自拍杆
static const uint8_t hids_report_descriptor[] =
{
// Report ID 1: Advanced buttons
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report Id (1)
0x15, 0x00, // Logical minimum (0)
0x25, 0x01, // Logical maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x01, // Report Count (1)
0x09, 0x94, // (Quit)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0x95, // (Help)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xEA, // (Volume Down)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xE9, // (Volume Up)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xCB, // (Tracking Decrement)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xCA, // (tracking Increment)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB6, // (Scan Previous Track)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB5, // (Scan Next Track)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB1, // (Pause)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB0, // (Play)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x75, 0x01, // Report Size (1)
0x95, 0x06, // Report Count (6)
0x81, 0x07, // Input (Data,Value,Relative,Bit Field)
0xC0 // End Collection
};
hid report map具体的了解可以参考文章(USB HID报告描述符教程 - 知乎),但是建议再粗略读下手册《Device Class Definition for Human Interface Devices (HID)》会有更好的理解,这个手册里有讲hid report map的解析机制。
根据自定义的hid report map每次上报需要有两个字节,每个bit的含义如下表:
bit位置 | 功能 |
bit 0 | Quit |
bit 1 | Help |
bit 2 | Volume Down |
bit 3 | Volume Up |
bit 4 | Tracking Decrement |
bit 5 | Tracking Increment |
bit 6 | Scan Previous Track |
bit 7 | Scan Next Track |
bit 8 | Pause |
bit 9 | Play |
bit 10~15 | Reserve |
以触发Volume Up为例,上报数据 0x80 0x00(按下)后,再上报数据0x00 0x00(松开),就可以表示一次音量键按下和松开的过程,在手机相机模式下实现了单次拍照。
当然,也可以用电脑蓝牙连接,也可以控制电脑的音量,Pause 和 Play可以在电脑上控制视频播放器的暂停和开始,更多的按键功能大家可以自己探索。
- uint8_t rptData[2] = {0};
- bool ret;
-
- rptData[0] = 0x08;
- rptData[1] = 0x00;
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, rptData, 2);
-
- os_delay(200);
-
- rptData[0] = 0x00;
- rptData[1] = 0x00;
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, rptData, 2);
hid report map如下:
// 键盘
static const uint8_t hids_report_descriptor[] =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xE0, // Usage Minimum (224)
0x29, 0xE7, // Usage Maximum (231)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x81, 0x02, // Input (Data, Variable, Absolute); Modifier byte
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant); Reserved byte
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (LEDs)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x91, 0x02, // Output (Data, Variable, Absolute); LED report
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Constant); LED report padding
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array); Key array (6 bytes)
0xc0 // END_COLLECTION
}
第一个字节表示8个特殊的按键,第二个字节保留,后面6个字节的每个字节都可以表示一个按键的状态,可以同时有多个按键按下。
这里有个问题,hid report map中指示,224到231号键由第一个字节表示,0到101号键由最后6个字节的任意一个字节表示,那么这些键的号码和实际上的按键是怎么样的对应关系呢?可以在手册《HID Usage Tables FOR Universal Serial Bus (USB)》中找到这样的对应关系表,粘贴一段如下:
我们要上报的键号是这份表中的Usage ID,这个Usage ID和USB 键盘通信中上报的那个Keycode值是不一样的东西。
当上报0x00 0x00 0x04 0x00 0x00 0x00 0x00 0x00时,就相当于按下“A”键;当上报0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00时,就相当于按下“win”键。
连接电脑和手机都可以使用,我用的小米手机,蓝牙键盘连上之后会默认关闭屏幕键盘,需要设置同时支持屏幕键盘在输入文字界面才会有屏幕键盘跳出来。
上面是标准的键盘,当然我们也可以自定义键盘,比如只有三个按键Ctrl、C和V。hid report map如下:
// 自定义键盘
static const uint8_t hids_report_descriptor[] =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x01, // REPORT_COUNT (1)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x09, 0xe0, // USAGE (Keyboard LeftControl)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x09, 0x06, // USAGE (Keyboard c and C)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x09, 0x19, // USAGE (Keyboard v and V)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x07, // INPUT (Cnst,Var,Rel)
0xc0, // END_COLLECTION
};
bit 0代表“Ctrl”键,bit 1代表“C”键,bit 2代表“V”键,bit 3~7预留。
当我们上报0x03时代表按下Ctrl C(复制),上报0x05时代表按下Ctrl V(粘贴)。
hid report map如下:
// 鼠标
static const uint8_t hids_report_descriptor[] =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x01, // REPORT_ID (1)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x03, // Usage Maximum (3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input(Data, Variable, Absolute); 3 button bits
0x95, 0x01, // Report Count(1)
0x75, 0x05, // Report Size(5)
0x81, 0x03, // Input(Constant); 5 bit padding
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input(Data, Variable, Relative); 3 position bytes (X,Y,Wheel)
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
};
上报有4个字节,bit 0 鼠标左键,bit 1鼠标右键,bit 2鼠标中键,第二字节鼠标X轴移动,第三字节鼠标Y轴移动,第四字节滚轮移动。
注意在调试的时候,hid report map变了要重新和设备绑定,不然会发现代码改了测试时却不生效。广播包的设备外观0x03C1(keyboard)记得改成0x03C2(mouse),这样就可以看到键盘图标变成了鼠标的图标。
也可以像USB插线的设备一样做成复合设备,把键盘和鼠标两个设备复合在一起,这样连接一个蓝牙设备就相当于同时连接了一个蓝牙键盘和一个蓝牙鼠标。
hid report map如下:
#define HIDS_KB_REPORT_ID 1
#if FEATURE_SUPPORT_MULTIMEDIA_KEYBOARD
#define HIDS_MM_KB_REPORT_ID 2
#endif
// 复合设备
static const uint8_t hids_report_descriptor[] =
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, HIDS_KB_REPORT_ID, // REPORT_ID (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x01, // REPORT_COUNT (1)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x09, 0xe0, // USAGE (Keyboard LeftControl)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x09, 0x06, // USAGE (Keyboard c and C)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x09, 0x19, // USAGE (Keyboard v and V)
0x81, 0x06, // INPUT (Data,Var,Rel)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x07, // INPUT (Cnst,Var,Rel)
0xc0, // END_COLLECTION
#if FEATURE_SUPPORT_MULTIMEDIA_KEYBOARD
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x85, HIDS_MM_KB_REPORT_ID, // REPORT_ID (2)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x03, // Usage Maximum (3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input(Data, Variable, Absolute); 3 button bits
0x95, 0x01, // Report Count(1)
0x75, 0x05, // Report Size(5)
0x81, 0x03, // Input(Constant); 5 bit padding
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x38, // Usage (Wheel)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x03, // Report Count (3)
0x81, 0x06, // Input(Data, Variable, Relative); 3 position bytes (X,Y,Wheel)
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
#endif
};
复合设备就是简单的把前面自定义键盘和鼠标两部分的report map拼在了一起。注意更改广播包中的外观属性为0x03C0(HID),这样连上电脑时可以显示出键盘和鼠标复合在一起的图标,如果不改的话实测对功能也没影响。
对于两个或以上的HID复合设备来说,是要在上报数据前加一个字节的report ID的,在这里键盘的report ID是1,鼠标的report ID是2。那么上报0x01 0x02代表按下字母“C”键,上报0x02 0x02 0x00 0x00 0x00代表按下鼠标右键。
对于瑞昱8762C平台而言,在实际调试过程中,发现这样发送数据不生效,需要打开宏“FEATURE_SUPPORT_MULTIMEDIA_KEYBOARD”即在Human Interface Device服务下再创建一个report特性给report ID为2的鼠标上报数据用,这两个report特性的特征值ID是一样的,用nrf connect看的话只能看到一个report点。由于不同的设备数据通过不同的特征值上报给HOST,所以在瑞昱8762C这里上报的数据前不用加report ID。目前手里没有抓包工具验证是否平台在后续的内部处理过程中加上了这个report ID。
我的调试过程是,在Human Interface Device服务外额外创建一个调试的服务来接收调试命令,实际上就是我要上报给HOST的数据先写入调试命令,然后开发板再将命令转交给HOST。
- void cmd_process(uint8_t *cmdBuf, uint8_t len)
- {
- #if FEATURE_SUPPORT_MULTIMEDIA_KEYBOARD
- bool ret;
-
- // 首先区分发给谁
- if (cmdBuf[0] == HIDS_KB_REPORT_ID)
- {
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, cmdBuf + 1, len - 1);
-
- os_delay(200);
-
- // 按键松开
- memset(cmdBuf, 0, len);
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, cmdBuf + 1, len - 1);
- }
- else
- {
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_MM_KB_INPUT_INDEX, cmdBuf + 1, len - 1);
-
- os_delay(200);
-
- // 按键松开
- memset(cmdBuf, 0, len);
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_MM_KB_INPUT_INDEX, cmdBuf + 1, len - 1);
- }
-
- myprintf("[%s] ret = %d\r\n", __func__, ret);
- #else
- bool ret;
-
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, cmdBuf, len);
-
- os_delay(200);
-
- memset(cmdBuf, 0, len);
- ret = hids_send_report(0, hids_srv_id, GATT_SRV_HID_KB_INPUT_INDEX, cmdBuf, len);
-
- myprintf("[%s] ret = %d\r\n", __func__, ret);
- #endif
- }
在复合设备的调试中,通过调试命令中的report ID区分从哪个report特性点上报数据给HOST。
《Universal Serial Bus (USB)_Device Class Definition .pdf》
《HID Usage Tables FOR Universal Serial Bus (USB).pdf》
《HID Descriptor tool.zip》
《hids_rtl8762c.rar》hid设备调试demo,仅供参考
链接:https://pan.baidu.com/s/1UVz56o377uD3OTKnO5P5kg 提取码:hqus