基于Android平台的TV 产品,输入设备中除了上篇提到的遥控器,空鼠。还有一些模拟
出来的uinput 设备“Mstar Smart TV Keypad” ,“libxTouchScreen” 。
1. 虚拟按键板
上篇我们通过getevent -l 可以看到device 2(/dev/input/event3)是属于按键板的keypad
设备,在Mstar 平台作为一个服务在后台运行
在运行时,按键板可以正常操作,kill 之后就无法响应。所以当我们按键无响应时,要看
服务是否正常,按键板ADC 值是否配置正确,按键板连接的sar 通道软件中是否开启。
接着我们看源码部分device\mstar\common\executables\virtualkeypad\main.c
- 处理流程
驱动初始化,主要是一些系统全局模块初识化,gpio,sar 初识化
int main()
{
MDrv_SYS_GlobalInit();
mdrv_gpio_init();
int ret = 0;
MDrv_MIU_SetIOMapBase();
SAR_KpdRegCfg SARCfg;
MDrv_SAR_Kpd_Init();
. . . . . .
}
- keypad 值映射
typedef struct {
MS_BOOL bEnable;
unsigned char u8SARChID;
SARKpdBndCfg tSARChBnd;
unsigned char u8KeyLevelNum; // 0-8 levels
unsigned char u8KeyThreshold[8]; // each threshold match to one keycode
unsigned char u8KeyCode[8];
} SARKpdRegCfg;
static SARKpdRegCfg BOARD_KPD[MAX_KEYPAD_CH] = {
KPD_TABLE
};
int i, j;
// 遍历MAX_KEYPAD_CH 个通道,将每个按键对应的sar值 保存在配置中,
// 根据sar 值,给每个按键定义一个keycode值,注册到sar 模块中,当按键触发时,
// sar模块返回keycode 值,该值也用于kl 文件中的keycode 和 应用层的映射
for (i = 0; i < MAX_KEYPAD_CH; i++) {
if (BOARD_KPD[i].bEnable == false) {
continue;
}
SARCfg.u8SARChID = BOARD_KPD[i].u8SARChID;
SARCfg.tSARChBnd.u8UpBnd = BOARD_KPD[i].tSARChBnd.u8UpBnd;
SARCfg.tSARChBnd.u8LoBnd = BOARD_KPD[i].tSARChBnd.u8LoBnd;
SARCfg.u8KeyLevelNum = BOARD_KPD[i].u8KeyLevelNum;
for (j = 0; j < 8; j++) {
SARCfg.u8KeyThreshold[j] = BOARD_KPD[i].u8KeyThreshold[j]; // sar 物理值
SARCfg.u8KeyCode[j] = BOARD_KPD[i].u8KeyCode[j]; // keycode 值
}
// 将sar 物理值 和 keycode 映射值保存到sar 配置模块中
if (MDrv_SAR_Kpd_SetChInfo(&SARCfg) == E_SAR_KPD_FAIL) {
ALOGV("DFBInfo MDrv_SAR_Config: CH_%d fails\n", i);
return FALSE;
}
}
其中KEYPAD映射如下:
//bEnable, u8SARChID, u8UpBnd, u8LoBnd, u8KeyLevelNum, u8KeyThreshold[8], u8KeyCode[8]
{ 0x01, 0x00, {0xFF , 0x70}, 0x08, {0x10, 0x2F, 0x4D, 0x71, 0x92, 0xAB, 0xC3, 0xE7}, {0x1E, 0x30, 0x23, 0x24, 0x25, 0x26, 0x32, 0x31}}, \
{ 0x00, 0x01, {0xFF , 0x70}, 0x00, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}, \
{ 0x00, 0x02, {0xFF , 0x70}, 0x00, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}, \
{ 0x00, 0x03, {0xFF , 0x70}, 0x00, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
Vendor_3697_Product_0002.kl 映射又如下:
#
# MStar Smart TV Keypad.
#
key 30 POWER
key 35 HOME
key 48 TV_INPUT
key 50 VOLUME_DOWN
key 49 VOLUME_UP
key 37 ENTER
keypad 里面keycode 为十六进制,kl 中keycode 是10进制,右侧枚举是字符串加上
"KEYCODE_"前缀,映射到上层,如POWER 对应的就是KEYCODE_POWER。
- 虚拟uinput设备
主要是打开uinput设备,配置一些参数,包括名称,vendor id,product id,bustype
keycode 的区间
int setup_uinput_device(stKeypadDeviceData* pKpdDev) {
struct uinput_user_dev uinp; // uInput device structure
int i;
// Open the input device
uinp_fd = open("/dev/uinput", O_WRONLY | O_NDELAY);
if (uinp_fd == 0) {
printf("Unable to open /dev/uinput\n");
return -1;
}
// Intialize the uInput device to NULL
memset(&uinp, 0x00, sizeof(uinp));
strncpy(uinp.name, KEYPAD_NAME, sizeof(uinp.name)-1);
uinp.id.vendor = KEYPAD_VENDOR_ID;
uinp.id.product = 0x0002;
uinp.id.bustype = BUS_VIRTUAL;
// Keyboard value of area
ioctl(uinp_fd, UI_SET_EVBIT, EV_KEY);
for (i = pKpdDev->min_keycode; i < pKpdDev->max_keycode; i++) {
ioctl(uinp_fd, UI_SET_KEYBIT, i);
}
// Create input device into input sub-system
if (write(uinp_fd, &uinp, sizeof(uinp)) != sizeof(uinp)) {
printf("First write returned fail.\n");
return -1;
}
// 创建
if (ioctl(uinp_fd, UI_DEV_CREATE)) {
printf("ioctl UI_DEV_CREATE returned fail.\n");
return -1;
}
return 1;
}
- sar 口获取键值
在前面将sar 物理值和keycode 映射值 设置到sar模块以后,连接在sar 口的按键板,有
按键触发时,就可以获取到keycode值
static void* keypad_EventThread(void *driver_data) {
. . . . . .
while (1) {
ret = E_SAR_KPD_FAIL;
u8Keycode = NULL_KEYVALUE;
u8Repeat = 0;
/* sleep an interval time */
usleep(keypad_input_interval);
ret = MDrv_SAR_Kpd_GetKeyCode(&u8Keycode, &u8Repeat);
MDrv_SAR_Adc_Config(1, TRUE);
adc1 = MDrv_SAR_Adc_GetValue(1);
/* check the get keycode successfully */
if (ret != E_SAR_KPD_OK) {
loop_count += 1;
if (loop_count == SLOW_POLLING_BOUNDARY)
keypad_input_interval = KEYPAD_INPUT_SLOW_POLLING;
if (repeatKey != NULL_KEYVALUE) {
write_event_to_device(repeatKey, 0);
repeatKey = NULL_KEYVALUE;
}
continue;
}
/* check the keypad is vaild? */
if (u8Keycode == NULL_KEYVALUE) {
loop_count += 1;
if (loop_count == SLOW_POLLING_BOUNDARY)
keypad_input_interval = KEYPAD_INPUT_SLOW_POLLING;
continue;
}
/* fill event to uinput device. */
if (u8Repeat == 1) {
repeatKey = u8Keycode;
}
write_event_to_device(u8Keycode, u8Repeat);
loop_count = 0;
keypad_input_interval = KEYPAD_INPUT_NORMAL_POLLING;
}
printf ("keypad keypad thread died\n");
pthread_exit(NULL);
return NULL;
}
线程通过MDrv_SAR_Kpd_GetKeyCode 接口不停地遍历SAR,获取keycode值,成功
之后通过write_event_to_device 将键值注入uinput 设备中,也即模拟出来的input设备。
- 模拟设备上抛事件
上一步步骤完成keyevent 事件的获取,接着通过模拟设备上抛到上层了。
void write_event_to_device(MS_U8 u8KeyCode, MS_U8 u8Repeat) {
struct input_event event; // Input device structure
struct timespec s;
s.tv_nsec = 5000000L;
s.tv_sec = 0;
memset(&event, 0x00, sizeof(event));
gettimeofday(&event.time, NULL);
event.type = EV_KEY; // 事件类型
event.code = u8KeyCode; // 键值
event.value = 1; // down: 按下
write(uinp_fd, &event, sizeof(event));
if (u8Repeat == 0) {
memset(&event, 0x00, sizeof(event));
gettimeofday(&event.time, NULL);
event.type = EV_KEY; // 事件类型
event.code = u8KeyCode; // 键值
event.value = 0; // up:按键弹起 . 非长按马上弹起
write(uinp_fd, &event, sizeof(event)); // 写入uinput 设备节点
}
// 发送sync 事件
memset(&event, 0x00, sizeof(event));
gettimeofday(&event.time, NULL);
event.type = EV_SYN;
event.code = SYN_REPORT;
event.value = 0;
write(uinp_fd, &event, sizeof(event));
}
android 虚拟uinput 设备参考链接:
https://www.xuebuyuan.com/1867494.html
https://blog.csdn.net/ZHONGkunjia/article/details/75142699?utm_source=blogxgwz3
uinput 设备驱动参考:
https://blog.csdn.net/mcgrady_tracy/article/details/22961339
2. 虚拟触摸框
主要是介绍触摸框串口数据通过模拟的uinput 输入设备,向上层发送触摸事件的场景。
分为两大部分从串口获取原始数据,向uinput 设备发送event事件,至于协议解析参看
https://blog.csdn.net/kehyuanyu/article/details/95996289 即可。
- 串口获取数据
触摸框的串口发出来的是通过红外框采集的手指触摸的是坐标,宽度等等信息,总共
67 bytes 都是通过/dev/ttyS* 获取,串口初始化
int setupSerial()
{
#if (NON_BLOCK_MODE == 1)
// O_NDELAY/O_NONBLOCK 非阻塞模式也即轮训模式, read 时候没有数据会马上返回
input_fd = open(SERIAL1_IN_DEVICE, O_RDWR | O_NOCTTY | O_NDELAY);
#else
// 阻塞/同步模式,read 在读取数据时,有数据或读取到文件结尾时才会返回
input_fd = open(SERIAL1_IN_DEVICE, O_RDWR | O_NOCTTY);// remmove the O_NDELAY mode
#endif
if (input_fd < 0) {
fprintf(stderr, "inputattach: '%s' - %s\n",SERIAL1_IN_DEVICE, strerror(errno));
return -1;
}
//start
int ret = -1;
struct serial_struct serial;
ret = ioctl(input_fd, TIOCGSERIAL, &serial);
if (ret != 0) {
close(input_fd);
return -1;
}
serial.xmit_fifo_size = 100 * 1024*1024; //1M
ret = ioctl(input_fd, TIOCSSERIAL, &serial);
if (ret != 0) {
close(input_fd);
return -1;
}
//end
#if (NON_BLOCK_MODE == 1)
// 非阻塞模式
fcntl(input_fd, F_SETFL, FNDELAY);
#else
// 阻塞模式
fcntl(input_fd, F_SETFL, 0);// set nonblock mode (FNDELAY to zero)
#endif
tcflush(input_fd,TCIOFLUSH);
if (tcgetattr(input_fd,&old_options) != 0) {
return -1;
}
// 获取串口属性
tcgetattr(input_fd, &new_options);
// 设置新的串口属性
new_options.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ECHOCTL | ECHOPRT | ECHOKE | ISIG);
new_options.c_iflag &= ~(INPCK | INLCR | ICRNL | IUCLC | IXON | IXOFF);
new_options.c_oflag &= ~OPOST; // raw output
// c_CC[VTIME] , c_CC[VMIN] 影响read 的返回值。
new_options.c_cc[VTIME] = 100;
new_options.c_cc[VMIN] = 60;
// 设置波特率
cfsetispeed(&new_options, B115200);
cfsetospeed(&new_options, B115200);
if((tcsetattr(input_fd,TCSANOW,&new_options))!=0)
{
return -1;
}
return 0;
}
初识化串口,注册阻塞/非阻塞模式的区别,另外c_cc[VTIME],c_cc[VMIN]属性的设置影响read的返回值。完成之后线程开始不停读取触摸数据。
#define PACKAGE_SIZE (67)
static MAPI_U8 _cmdLine[CMD_LINE_SIZE];
int serialDataLoop(void* arg)
{
. . . . . .
MAPI_U8 *dataTmp = _cmdLine;
while(!_bExitUartDebug)
{
FD_ZERO(&r_fds);
FD_SET(input_fd, &r_fds);
#if (NON_BLOCK_MODE == 1)
//set Time Out
//sys.boot_completed
memset(&timeout,0x00,sizeof(timeout));
timeout.tv_sec = 0;
timeout.tv_usec = 3*1000; // 7 ms
ret = ::select(input_fd + 1, &r_fds, NULL, NULL, &timeout); // 非阻塞
#else
ret = ::select(input_fd + 1, &r_fds, NULL, NULL, NULL); // 阻塞模式
#endif
if(ret == 0)
{
continue;
}
else if(ret > 0 && FD_ISSET(input_fd, &r_fds))//else if(ret > 0)
{
/*Here is demo code for read/write serial data,please Add Customer
code here for special use case*/
// data recieved
#if (NON_BLOCK_MODE == 1)
readRawDataRet = readRAWData(input_fd, tmpReadBuff);
if(readRawDataRet){
doProcessCommand(tmpReadBuff);
memset(tmpReadBuff,0,sizeof(tmpReadBuff));
ReadBuff = tmpReadBuff;
}else{
continue;
}
#else
readRawDataRet = readFullTouchData(input_fd, tmpReadBuff, DATA_LEN_SIX);
if(readRawDataRet == 1){
doProcessCommand(tmpReadBuff);
memset(tmpReadBuff,0,sizeof(tmpReadBuff));
ReadBuff = tmpReadBuff;
} else {
continue;
}
#endif
}
}
. . . . . .
}
readRAWData(非阻塞)/readFullTouchData(阻塞) 不停从/dev/ttyS* 读取数据,读取完成之后,doProcessCommand 再校验,解析数据。读取数据可以参考另一篇博客https://blog.csdn.net/kehyuanyu/article/details/101756151
更新应用可以参考MSrv_UartDebug.cpp文件。
- 虚拟uinput设备
int setup_uinput_device() {
struct uinput_user_dev uinp; // uInput device structure
// Open the input device
uinp_fd = open("/dev/uinput", O_WRONLY | O_NDELAY);
if (uinp_fd == 0) {
init_uinput = 0;
return -1;
}
// Intialize the uInput device to NULL
// 设置名称,版本,bustype
memset(&uinp, 0, sizeof(uinp));
strncpy(uinp.name, "libxTouchScreen", 16);
uinp.id.version = 4;
uinp.id.bustype = BUS_USB;
uinp.absmin[ABS_MT_SLOT] = 0;
uinp.absmax[ABS_MT_SLOT] = 20; // MT代表multi touch 多指触摸 最大手指的数量我们设置9,//电容屏ID有0x0B,0x11,暂时改成20.
uinp.absmin[ABS_MT_POSITION_X] = 0; // 屏幕最小的X尺寸
uinp.absmax[ABS_MT_POSITION_X] = 32767; // 屏幕最大的X尺寸
uinp.absmin[ABS_MT_POSITION_Y] = 0; // 屏幕最小的Y尺寸
uinp.absmax[ABS_MT_POSITION_Y] = 32767; //屏幕最大的Y尺寸
uinp.absmin[ABS_MT_TRACKING_ID] = 0;
uinp.absmax[ABS_MT_TRACKING_ID] = 65535;//按键码ID累计叠加最大值
// 根据需要设置uinput属性
//uinp.absmin[ABS_MT_PRESSURE] = 0;
//uinp.absmax[ABS_MT_PRESSURE] = 255; //屏幕按下的压力值
//uinp.absmin[ABS_MT_DISTANCE] = 0;
//uinp.absmax[ABS_MT_DISTANCE] = 0; //离表面距离
//uinp.absmin[ABS_MT_TOUCH_MAJOR] = 0;
//uinp.absmax[ABS_MT_TOUCH_MAJOR] = 15;
//ABS_MT_WIDTH_MAJOR 宽度
#if 0//(TOUCH_HAS_WIDTH == 1)
uinp.absmin[ABS_MT_WIDTH_MAJOR] = 0;
uinp.absmax[ABS_MT_WIDTH_MAJOR] = 1500;
//uinp.absmax[ABS_MT_DISTANCE] = 1500; //离表面距离
//uinp.absmin[ABS_MT_DISTANCE] = 0;
#endif
if(!isNoWidth){
uinp.absmin[ABS_MT_WIDTH_MAJOR] = 0;
uinp.absmax[ABS_MT_WIDTH_MAJOR] = 1500;
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_WIDTH_MAJOR);
}
// Setup the uinput device
ioctl(uinp_fd, UI_SET_EVBIT, EV_KEY); //该设备支持按键
ioctl(uinp_fd, UI_SET_EVBIT, EV_REL); //支持鼠标
// Touch
ioctl (uinp_fd, UI_SET_EVBIT, EV_ABS); //支持触摸
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_SLOT);
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_POSITION_X);
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y);
#if 0//(TOUCH_HAS_WIDTH == 1)
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_WIDTH_MAJOR);//weight
//ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_DISTANCE);//height
#endif
ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID);
ioctl (uinp_fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
ioctl (uinp_fd, UI_SET_KEYBIT, BTN_TOUCH);
//ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
//ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
//ioctl (uinp_fd, UI_SET_ABSBIT, ABS_MT_DISTANCE);
// Create input device into input sub-system
if (write(uinp_fd, &uinp, sizeof(uinp)) != sizeof(uinp)) {
return -1;
}
if (ioctl(uinp_fd, UI_DEV_CREATE)) {
return -1;
}
return 1;
}
打开设备,配置好属性,就虚拟成功了。接着就是将解析好的数据封装成event 发送即可。
- 发送模拟事件
void doProcessCommand(unsigned char tmpBuff[])
{
int width = 0;
unsigned char nPosStartIndex = 0;
unsigned int unUsefulDotCnt = tmpBuff[VALID_DOT_NUMBER_IN_EACH_PACKGE];
touchCount = unUsefulDotCnt;
for(int i = 0; i < DOTCNT_OF_PACKAGE; i++){
struct timeval tv;
gettimeofday(&tv,NULL);
nPosStartIndex = i * CNT_OF_DOT + 5;
action = tmpBuff[nPosStartIndex];
id = tmpBuff[nPosStartIndex + 1];
// record the useful dot count
if(unUsefulDotCnt != 0x00){
touchCount = unUsefulDotCnt;
}
if(id > TOUCH_COUNT){ // ID < 12
printf( "\033[1;31;40m Touch data error,id:%d. \n\033[0m", id);
continue;//break;
}
// pos (x,y)
x = tmpBuff[nPosStartIndex+2] + (tmpBuff[nPosStartIndex+3] << 8);
y = tmpBuff[nPosStartIndex+4] + (tmpBuff[nPosStartIndex+5] << 8);
if(!isNoWidth){
// Dot width
width = tmpBuff[nPosStartIndex + 6] + (tmpBuff[nPosStartIndex + 7] << 8);
}
if(action == 0x02){
if(preTouchAct[id] == 0x03 || preTouchAct[id] == 0x02){
touchCount--;
write_event_to_device(EV_ABS, ABS_MT_SLOT, id);
write_event_to_device(EV_ABS, ABS_MT_TRACKING_ID, -1);
if(touchCount == 0){
write_event_to_device(EV_KEY, BTN_TOUCH, 0); // UP to Android
printf("[%s][%d]...... id = [%d] ALL UP .\n",__FUNCTION__,__LINE__,id);
}else{
printf("[%s][%d]..... id = [%d]. ONE UP .\n",__FUNCTION__,__LINE__,id);
}
write_event_to_device(EV_SYN, SYN_REPORT, 0);
preTouchAct[id] = 0x00; // set default when dot up
}else if(preTouchAct[id] == 0x00){
printf("[%s][%d]...... id = [%d] DOWN....\n",__FUNCTION__,__LINE__,id);
count++;
trackingId[id] = count;
write_event_to_device(EV_ABS, ABS_MT_SLOT, id);
write_event_to_device(EV_ABS, ABS_MT_TRACKING_ID, count);
write_event_to_device(EV_ABS, ABS_MT_POSITION_X, x);
write_event_to_device(EV_ABS, ABS_MT_POSITION_Y, y);
if(!isNoWidth){
write_event_to_device(EV_ABS, ABS_MT_WIDTH_MAJOR, width);
}
write_event_to_device(EV_KEY, BTN_TOUCH, 1); // DOWN to Android
write_event_to_device(EV_SYN, SYN_REPORT, 0);
preTouchAct[id] = action;
}
}else if(action == 0x03){
if(preTouchAct[id] == 0x00){
count++;
trackingId[id] = count;
write_event_to_device(EV_ABS, ABS_MT_SLOT, id);
write_event_to_device(EV_ABS, ABS_MT_TRACKING_ID, count);
write_event_to_device(EV_ABS, ABS_MT_POSITION_X, x);
write_event_to_device(EV_ABS, ABS_MT_POSITION_Y, y);
if(!isNoWidth){
write_event_to_device(EV_ABS, ABS_MT_WIDTH_MAJOR, width);
}
write_event_to_device(EV_KEY, BTN_TOUCH, 1);
write_event_to_device(EV_SYN, SYN_REPORT, 0);
preTouchAct[id] = action;
//move
write_event_to_device(EV_ABS, ABS_MT_SLOT, id);
write_event_to_device(EV_ABS, ABS_MT_TRACKING_ID, trackingId[id]);
write_event_to_device(EV_ABS, ABS_MT_POSITION_X, x);
write_event_to_device(EV_ABS, ABS_MT_POSITION_Y, y);
#if (TOUCH_HAS_WIDTH == 1)
write_event_to_device(EV_ABS, ABS_MT_WIDTH_MAJOR, width);
#endif
write_event_to_device(EV_SYN, SYN_REPORT, 0);
tmpX[id] = x;
tmpY[id] = y;
}else if((preTouchAct[id] == 0x02)||(abs(tmpX[id] - x) > 17 || abs(tmpY[id] - y) > 30)){//最大坐标除以分辨率,拿到比例
printf("[%s][%d]...... id = [%d] MOVE....\n",__FUNCTION__,__LINE__,id);
write_event_to_device(EV_ABS, ABS_MT_SLOT, id);
write_event_to_device(EV_ABS, ABS_MT_TRACKING_ID, trackingId[id]);
write_event_to_device(EV_ABS, ABS_MT_POSITION_X, x);
write_event_to_device(EV_ABS, ABS_MT_POSITION_Y, y);
if(!isNoWidth){
write_event_to_device(EV_ABS, ABS_MT_WIDTH_MAJOR, width);
}
write_event_to_device(EV_SYN, SYN_REPORT, 0);
tmpX[id] = x;
tmpY[id] = y;
preTouchAct[id] = action;
}
}else if(action == 0x00){
//
}
printf("\n\n\n");
}
}
将解析后的数据(坐标,宽度,id,tracking_id,sync,report,up,down事件)通过
write_event_to_device 接口发送。
int write_event_to_device(int type, int code, int value) {
if(init_uinput == 0) {
if(setup_uinput_device() < 0) {
return -1;
}
}
struct input_event event; // Input device structure
memset(&event, 0, sizeof(event));
gettimeofday(&event.time, NULL);
event.type = type;
event.code = code;
event.value = value;
write(uinp_fd, &event, sizeof(event));//int ret = write(uinp_fd, &event, sizeof(event));
return 1;
}
至此,完成了触摸框的触摸点数据,从串口到uinput 模拟设备的发送流程。后续android
的eventhub 模块监控 /dev/input/event* 节点,获取event,然后封装给上层使用。