提示:本文基于android 11.0讲解如何适配TV android系统常用的2.4G和蓝牙HID遥控设备
文章目录
前言
HID(人体学接口设备 )是一个设备类定义,使用通用 USB 驱动程序。 在 HID 之前,设备只能对鼠标和键盘使用严格定义的协议(PS/2)。 硬件创新要求使用现有协议重载数据,或使用其自己的专用驱动程序创建非标准硬件。 HID 为这些“启动模式”设备提供了支持,同时通过可扩展、标准化且易于编程的接口添加对硬件创新的支持。
HID一开始为USB,但设计为与总线无关。 它为低延迟、低带宽设备而设计,但可以灵活地指定基础传输中的速率。 1996 年,基于 USB 的 HID 的规范被 USB-IF 批准。不久之后,对其他传输的支持又获得批准。如2.4G,蓝牙,I2C,GPIO,SPI等 。 此外,还允许通过自定义传输驱动程序进行特定于供应商的第三方传输。
目前,HID设备包括各种设备,例如字母数字显示器、条码读取器、扬声器/耳机上的音量控制、辅助显示器、传感器、2.4G遥控、蓝牙遥控等。 许多硬件供应商还对其专用设备使用 HID。
USB-IF HID 规范: link
1. HID遥控设备是什么?
它们通常长这样:
TV常用的HID遥控设备分两种:2.4G和蓝牙。
1.1 2.4G遥控
我们通常拿到这种HID设备,都会有一个说明书。里面会详细的定义每个按键的信息,这里截取了一部分。可以看到里面的每个按键都会定义一个hid page,一个hid code,和按键描述。
2.4G遥控都需要搭配usb接收端(俗称usb dongle)使用,即插即用。
1.2. 蓝牙遥控
蓝牙跟2.4G遥控,按键的HID这部分相似,每个按键都有一个hid page,一个hid id,一个按键描述。另外这个说明书还多了一个IR CODE,这个是红外协议的码值。说明书上面也有描述红外协议NEC,头码0x8890。
蓝牙遥控有的带usb dongle,有的不带。
为什么会有这种区别呢?
这里简单说下,蓝牙usb dongle有HCI模式和HID模式两种工作模式。在HCI工作模式中,dongle只是相当一个标准的HCI蓝牙usb dongle。在HID工作模式下,dongle是属于一个标准usb的HID设备,具有HID键盘、按键、鼠标等功能。
所以要看遥控厂商提供的usb dongle是在哪种工作模式,如果是HCI,那它就相当于一个usb蓝牙模块,需要配对后才能使用按键。如果是HID,那么它就相当于一个标准的HID设备,即插即用,无需适配。
2. HID遥控设备响应流程
3. linux HID协议
主要实现在kernel部分里的drivers/hid/hid-input.c
ps :这里插个题外话。如果我们拿到的android code,搜索这个hid-input.c文件,发现存在好几个kernel版本里。那么我们可以通过在对应平台系统里执行cat /proc/version命令查看当前平台使用的kernel版本,从而找到对应使用的文件。比如下面这个搜索到两个,我们在跑起来的平台下执行命令发现当前使用的是4.9版本,那么我们使用的文件就是第一个
回归正题,在HID协议里,hidinput_configure_usage()会先匹配HID_USAGE_PAGE(高8位),再匹配HID_USAGE(低8位)。
遥控规格书里的Usage Page或者Page ID就是HID_USAGE_PAGE,Usage ID或者HID code就是HID_USAGE。
TV目前常用的HID遥控设备HID_USAGE_PAGE基本都是HID_UP_KEYBOARD(0x07),HID_UP_CONSUMER(0x0c)这两种。
#define HID_USAGE_PAGE 0xffff0000
#define HID_UP_KEYBOARD 0x00070000
#define HID_UP_CONSUMER 0x000c0000
#define HID_USAGE 0x0000ffff
static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_field *field,
struct hid_usage *usage)
{
******
switch (usage->hid & HID_USAGE_PAGE) {
******
case HID_UP_KEYBOARD:
set_bit(EV_REP, input->evbit);
if ((usage->hid & HID_USAGE) < 256) {
if (!hid_keyboard[usage->hid & HID_USAGE]) goto ignore;
map_key_clear(hid_keyboard[usage->hid & HID_USAGE]);
} else
map_key(KEY_UNKNOWN);
break;
******
case HID_UP_CONSUMER: /* USB HUT v1.12, pages 75-84 */
set_bit(EV_REP, input->evbit);
switch (usage->hid & HID_USAGE) {
case 0x000: goto ignore;
******
case 0x041: map_key_clear(KEY_SELECT); break; /* Menu Pick */
case 0x042: map_key_clear(KEY_UP); break; /* Menu Up */
case 0x043: map_key_clear(KEY_DOWN); break; /* Menu Down */
case 0x044: map_key_clear(KEY_LEFT); break; /* Menu Left */
case 0x045: map_key_clear(KEY_RIGHT); break; /* Menu Right */
******
case 0x076: map_key_clear(KEY_OPEN); break;
******
default: map_key_clear(KEY_UNKNOWN);
}
break;
******
}
******
}
#define KEY_UNKNOWN 240
这里补充一下,后面会用到
3.1. HID USAGE PAGE之KEYBOARD(0x07)
从第3节的switch中,我们可以看到HID_UP_KEYBOARD判断HID_USAGE最终流向hid_keyboard[256]数组
#define unk KEY_UNKNOWN
static const unsigned char hid_keyboard[256] = {
0, 0, 0, 0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 28, 1, 14, 15, 57, 12, 13, 26,
27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
65, 66, 67, 68, 87, 88, 99, 70,119,110,102,104,111,107,109,106,
105,108,103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
72, 73, 82, 83, 86,127,116,117,183,184,185,186,187,188,189,190,
191,192,193,194,134,138,130,132,128,129,131,137,133,135,136,113,
115,114,unk,unk,unk,121,unk, 89, 93,124, 92, 94, 95,unk,unk,unk,
122,123, 90, 91, 85,unk,unk,unk,unk,unk,unk,unk,111,unk,unk,unk,
unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,
unk,unk,unk,unk,unk,unk,179,180,unk,unk,unk,unk,unk,unk,unk,unk,
unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,unk,
unk,unk,unk,unk,unk,unk,unk,unk,111,unk,unk,unk,unk,unk,unk,unk,
29, 42, 56,125, 97, 54,100,126,164,166,165,163,161,115,114,113,
150,158,159,128,136,177,178,176,142,152,173,140,unk,unk,unk,unk
};
这个表的规则是,序号从0开始,HID_USAGE高位代表行,低位代表列。
比如规格书里的一个按键对应Usage ID 0x76,即是表中第8行第7列。或者把0x76换算成十进制118,即是hid_keyboard[118]的值,只是这样不太方便查。
(注意,在这里查的前提是这个按键的Usage Page是0x07,07才是调用HID_UP_KEYBOARD)
这个数组里的数字我们一般叫scancode,它的定义通常都在linux kernel里的input.h文件里。如果这个按键的HID_USAGE查到是对应数组里的unk位置,也就是KEY_UNKNOWN,即240,则代表这个HID_USAGE未做过适配。我们需要把数组里对应位置的unk修改为我们想要的scancode即可。如何修改scancode,第4节将进行讲解。
3.2. HID USAGE PAGE之CONSUMER(0x0c)
从第3节的switch中,我们可以看到HID_UP_CONSUMER里还嵌套了一个switch,用于判断HID_USAGE。这个就比较简单了,我们添加的时候按顺序从上到下递增,后面检查也方便。如果这个按键HID_USAGE没有匹配到,那么则抛出KEY_UNKNOWN,即240,则代表这个HID_USAGE未做过适配。我们需要按顺序在合理的位置添加上对应的case HID_USAGE: map_key_clear(scancode);即可。
4. scancode
scancode都定义在linux kernel里的include/uapi/linux/input.h文件
HID为什么会使用到这个文件呢?这里简单画个kernel流程图
input.h这里,我们也可以做一些扩展
比如在input.h里
#include "input-event-codes.h"
或者
#include "key_define.h"
就可以把scancode定义在这些扩展文件里了。
如果是一些新加入的芯片厂商,大家都知道该怎么找他们使用的客制化scancode了吧。
这里也附上一些scancode定义,定义格式如下:
******
#define KEY_STOP 128 /* AC Stop */
#define KEY_AGAIN 129
#define KEY_PROPS 130 /* AC Properties */
#define KEY_UNDO 131 /* AC Undo */
#define KEY_FRONT 132
#define KEY_COPY 133 /* AC Copy */
#define KEY_OPEN 134 /* AC Open */
#define KEY_PASTE 135 /* AC Paste */
#define KEY_FIND 136 /* AC Search */
#define KEY_CUT 137 /* AC Cut */
#define KEY_HELP 138 /* AL Integrated Help Center */
#define KEY_MENU 139 /* Menu (show menu) */
#define KEY_CALC 140 /* AL Calculator */
******
所以,我们在第3节的两个小节中,如果查到该按键匹配的HID_USAGE是KEY_UNKNOWN,那么我们在这里找一个我们想要的KEY定义或者新增一个define,然后07 page是在hid_keyboard[256]数组里填上10进制数字,0c page是在switch里加上对应case HID_USAGE: map_key_clear(KEY_XXX);
5. android input子系统之kl文件
scancode是通过input子系统映射到对应设备节点使用的KeyLayoutFile文件,即kl文件
input子系统详细原理这里就不做介绍了,有兴趣的同学可以在网上搜索下,一大把资料
这里也推荐一篇
AndroidR Input子系统(5)解析“.kl“文件
Android Framework 输入子系统(04)InputReader解读
5.1 getevent命令获取当前设备节点,HID page,HID code及scancode
先在平台上执行getevent命令,然后按下任意遥控按键会刷出几行打印,最前面就是当前遥控设备使用的节点,比如下面这个案例使用的是/dev/input/event4,该按键HID page是0x0c,HID code是0x76,scancode是0xf0
这里注意核对读到按键的HID page和HID code是否与规格书写的一致,不一致则要联系遥控厂商
注意,scancode 0xf0=240,代表unknown,说明这个按键的HID code没有在hid-input.c里有对应的scancode关系。按照第4节的思路进行添加或者修改,在hid-input.c的0c page也就是CONSUMER使用的switch位置处,按顺序增加定义:
case 0x076: map_key_clear(KEY_OPEN); break;
再比如下面这两个,就已经有对应的scancode了,无需修改hid-input.c文件
5.2 dumpsys input查看对应设备节点使用的kl文件
然后在平台上执行dumpsys input命令,有一大堆的打印出来。我们需要找到对应节点使用的KeyLayoutFile地方
可以看到,读到了当前遥控设备的vendor product,使用的KeyLayoutFile和KeyCharacterMapFile。
KeyCharacterMapFile,即kcm文件,本篇就不做介绍了,tv的话用的相对较少。有兴趣的同学可以读读这篇文章,介绍的比较详细。弄懂规则后也比较简单。
AndroidR Input子系统(6)解析“.kcm“文件
这里说下,如果我们是新的HID设备,读到的KeyLayoutFile都是调用android默认的/vendor/user/keylayout/Generic.kl,因为没有对新设备的vendor product做过适配。
那怎么适配kl呢?
其实也比较简单,在对应的路径下放置对应vendor product的kl文件,命名格式是Vendor_xxxx_Product_xxxx.kl,所有的kl文件内容格式都是长这样的:
key 8 7
key 9 8
key 10 9
key 11 0
key 12 MINUS
key 13 EQUALS
key 14 DEL
key 15 TAB
格式:key arg1 arg2
arg1:取值scancode对应的10进制值
arg2:取值KeyCodeLabel或者KeyCodeLabel对应的10进制值
举例:第5.1小节中的按键HID page是0x0c,HID code是0x76,那么我们先在hid-input.c里找到0c page也就是CONSUMER使用的switch位置,0x76对应的是KEY_OPEN,KEY_OPEN在input.h里找到定义的数值是134,那么我们就可以写key 134 OPEN,这个OPEN字符串的来源在第6节会讲解
添加的kl可以放在/vendor/user/keylayout/里,也可以放在/system/user/keylayout/里,如果这两个路径下存在同名的kl文件,会优先调用vendor路径下的。
上面读到的vendor=0x005d product=0x0002,我们可以放在/vendor/user/keylayout/Vendor_005d_Product_0002.kl,修改重启系统后就会调用我们这个对应的kl文件
如果要加到android源码code里,kl一般会放在frameworks/base/data/keyboards/路径下,这个路径下的kl是跑在系统的/system/usr/keylayout/路径里。另外芯片厂商会有自己的客制化kl文件,一般是跑在系统的/vendor/usr/keylayout/路径里以保证高优先级,我们也可以把新kl加芯片厂商的客制化路径下。
6. keycode
这里也简单画下kl中的KeyCodeLabel是怎么转成apk可识别的keycode的
涉及到的3个文件,格式都是统一的,比如
如果要新增一个,记得这3个文件都要加上,value要对应上。
7. apk实现keycode功能
apk在监听按键事件处理中,就可以对该KEYCODE_XXX或者value数值进行相关功能实现了。
总结
通过上面的分析,我们知道了整个流程。那么我们拿到一个未添加过的新HID设备,整个思路应该是这样的
遇到的问题----KeyLayoutFile调用为空
什么原因呢?
有两种情况会造成这种现象
- 错误的未知关键词
比如这样
未知的ket关键词,kl文件内容格式只有1个关键词,那就是key
- 错误的未知KEYCODE
比如上面的MZHE没有在第6节中的3个文件中进行定义