16. 输入设备应用编程
- 1. 输入类设备编程介绍
- 1.1 什么是输入设备
- 1.2 input 子系统
- 1.3 读取数据的流程
- 1.4 应用程序如何解析数据
- 2. 读取 struct input_event 数据
- 3. 按键应用编程
- 4. 触摸屏应用编程
- 4.1 解析触摸屏设备上报的数据
- 4.1.1 单点触摸设备——事件上报顺序
- 4.1.2 多点触摸设备——事件上报顺序
- 4.1.2.1 TypeB 协议
- 4.1.2.2 触摸屏上报数据分析
- 4.2 获取触摸屏的信息
- 4.3 单点触摸应用程序——获取一个触摸点的坐标
- 4.4 多点触摸应用程序
1. 输入类设备编程介绍
1.1 什么是输入设备
常见的输入设备有鼠标、键盘、触摸屏、遥控器等,用户通过输入设备与系统进行交互。
1.2 input 子系统
Linux 为了管理这些输入设备,实现了一套能够兼容所有输入设备的框架,这个框架就是 input 子系统,可以屏蔽硬件的差异,向应用层提供一套统一的接口。基于子系统注册成功的输入设备,都会在 /dev/input 目录下生成对应的设备节点,名称通常为 eventX,通过读取这些设备节点可以获取输入设备上报的数据。
1.3 读取数据的流程
假设读取的设备对应的节点为 event0:
- 打开 /dev/input/event0 设备文件
- 发起读操作,如果没有数据可读则会进入休眠(阻塞 IO 情况下)
- 当有数据可读时,应用程序会被唤醒,读操作获取到数据返回
- 应用程序对读取到的数据进行解析
1.4 应用程序如何解析数据
每一次 read 操作获取的都是一个 struct input_event 结构体类型数据
struct input_event
{
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
主要关注以下三个:
- type: 用于描述发生了哪一中类型的事件,事件分类具体可以查看
<linux/input.h>
头文件 - code: 表示该类事件中的哪一个具体事件
- value: 内核每次上报事件都会向应用层发送一个数据 value,对 value 值的解释随 code 变化而变化。
数据同步:
事件中有一个同步事件 EV_SYN,用于实现同步操作,告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次 read 只能读取一个 struct input_event 类型数据,但是如果读取触摸屏数据时,有横纵坐标,需要多次 read。内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整,可以进行同步了。同步事件也包含多种不同的事件:
所有的输入设备都需要上报同步事件,上报的同步事件通常是 SYN_REPORT,而 value 值通常为 0.
2. 读取 struct input_event 数据
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc,char *argv[]) // 执行文件时需要传入参数,参数就是输入设备文件
{
if(argc!=2)
{
fprintf(stderr,"usage:%s input-dev\n",argv[0]);
return -1;
}
struct input_event in_ev={0};
int fd=open(argv[1],O_RDONLY);
for(;;)
{
// 循环读取数据
if(sizeof(struct input_event)!=read(fd,&in_ev,sizeof(struct input_event)))
{
perror("read");
return -1;
}
printf("type:%d code:%d value:%d\n",in_ev.type,in_ev.code,in_ev.value);
}
}
3. 按键应用编程
获取按键状态,以字母 A 为例,上报 KEY_A 事件时,value=1;如果是松开,value=0;如果是长按,value=2
int main(int argc,char *argv[])
{
if(argc!=2)
{
fprintf(stderr,"usage:%s input-dev\n",argv[0]);
return -1;
}
struct input_event in_ev={0};
int fd=open(argv[1],O_RDONLY);
for(;;)
{
if(sizeof(struct input_event)!=read(fd, &in_ev, sizeof(struct input_event)))
{
perror("read error");
exit(-1);
}
if(EV_KEY == in_ev.type)
{
switch (in_ev.value)
{
case 0:
printf("code<%d>: 松开\n", in_ev.code);
break;
case 1:
printf("code<%d>: 按下\n", in_ev.code);
break;
case 2:
printf("code<%d>: 长按\n", in_ev.code);
break;
}
}
}
}
4. 触摸屏应用编程
4.1 解析触摸屏设备上报的数据
触摸屏设备是一个绝对位移设备,可以上报绝对位移事件,绝对位移事件如下:
单点触摸和多点触摸:
单点触摸设备只支持单点触摸, 一轮完整的数据只包含一个触摸点信息;单点触摸设备以 ABS_XXX 事件承载、上报触摸点的信息,譬如 ABS_X(value 值对应的是 X 轴坐标值)、ABS_Y(value 值对应的是 Y 轴坐标值)等绝对位移事件,而有些设备可能还支持 Z 轴坐标(通过 ABS_Z 事件上报、 value 值对应的便是 Z 轴坐标值)、按压力大小(通过 ABS_PRESSURE 事件上报、 value 值对应的便是按压力大小) 以及接触面积等属性。 大部分的单点触摸设备都会上报 ABS_X 和 ABS_Y 事件,而其它绝对位移事件则根据具体的设备以及驱动的实现而定!
多点触摸设备可支持多点触摸,一轮完整的数据可能包含多个触摸点信息。 多点触摸设备则是以 ABS_MT_XXX(MT 是 Multi-touch, 意思为:多点触摸)事件承载、上报触摸点的信息,如 ABS_MT_POSITION_X(X 轴坐标)、ABS_MT_POSITION_Y(Y 轴坐标)等绝对位移事件。
触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件,用于描述按下触摸屏和松开触摸屏;具体的按键事件为 BTN_TOUCH(code=0x14a,也就是 330),当然,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。
Tips:BTN_TOUCH 事件不支持长按状态,故其 value 不会等于 2。 对于多点触摸设备来说,只有第一个点按下时上报 BTN_TOUCH 事件 value=1,当最后一个点离开触摸屏时上报 BTN_TOUCH 事件 value=0。
4.1.1 单点触摸设备——事件上报顺序
流程大致如下:
// 点击触摸屏时
BTN_TOUCH // value=1
ABS_X
ABS_Y
SYN_REPORT
// 滑动
ABS_X
ABS_Y
SYN_REPORT
// 松开
BTN_TOUCH // value=0
SYN_REPORT
4.1.2 多点触摸设备——事件上报顺序
在 Linux 内核中,多点触摸设备使用多点触摸协议(MT)上报各个触摸点的数据,MT 协议分为 TypeA 和 TypeB,前者几乎不用,介绍后一个。
4.1.2.1 TypeB 协议
该协议适用于能够追踪并区分触摸点的设备,是通过 ABS_MT_SLOT 事件上报各个触摸点信息的更新。这类设备通常在硬件上能够区分不同的触摸点,为每一个识别到的触摸点与一个 slot 进行关联,这个 slot 就是一个编号。底层驱动向应用层上报 ABS_MT_SLOT 事件,此事件会告诉接收者当前正在更新的是哪个触摸点的数据,ABS_MT_SLOT 事件中对应的 value 数据存放的便是一个 slot,以告知应用层当前正在更新 slot 关联的触摸点对应的信息。
每个识别出来的触摸点分配一个 slot,与该 slot 关联起来,利用这个 slot 来传递对应触点的变化。除了 ABS_MT_SLOT 事件之外,TypeB 协议还会使用到 ABS_MT_TRACTKING_ID 事件,用于触摸点的创建、替换和销毁工作,携带的 value 表示一个 ID,如果是非负数,表示一个有效的触摸点,如果是 -1,表示该触摸点已经不存在,如果是一个以前不存在的 ID 表示这是一个新的触摸点。
该协议可以减少发送到用户空间的数据,只有发生了变更的数据才会上报。
4.1.2.2 触摸屏上报数据分析
使用命令cat /proc/bus/input/devices
确定触摸屏对应的设备节点。点击后先不要松开
首先第一行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING_ID(code=57)事件,并且 value 值等于 78,也就是 ID,这个 ID 是一个非负数,所以表示这是一个新的触摸点被创建,也就意味着触摸屏上产生了一个新的触摸点(手指按下) 。
第二行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_POSITION_X(code=53)事件,其 value对应的便是触摸点的 X 坐标;
第三行上报了 ABS_MT_POSITION_Y(code=54)事件,其 value 值对应的便是触摸点 Y 坐标,所以由此可知该触摸点的坐标为(372, 381)。
第四行上报了按键类事件 EV_KEY(type=1)中的 BTN_TOUCH(code=330),value 值等于 1,表示这是触摸屏上最先产生的触摸点(slot=0、也就是触摸点 0)
第五行和第六行分别上报了绝对位移事件 EV_ABS(type=3)中的 ABS_X(code=0)和 ABS_Y(code=1),其 value 分别对应的是触摸点的 X 坐标和 Y 坐标。多点触摸设备也会通过 ABS_X、 ABS_Y 事件上报触摸点的 X、 Y 坐标,但通常只有触摸点 0 支持,所以可以把多点触摸设备当成单点触摸设备来使用。
最后一行上报了同步类事件 EV_SYN(type=0)中的 SYN_REPORT(code=0)事件,表示此次触摸点的信息全部上报完毕。
在第一个触摸点的基础上,增加第二个触摸点,打印信息如下所示:
1~7 行不再解释,第八行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_SLOT 事件(code=47),表示目前要更新 slot=1 所关联的触摸点(也就是触摸点 1) 对应的信息。
第九行上报了绝对位移事件 EV_ABS(type=3)中的 ABS_MT_TRACKING_ID 事件(code=57),ID=79,这是之前没有出现过的 ID,表示这是一个新的触摸点。
第十、十一行分别上报了 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 事件。
最后一行上报同步事件(type=0、 code=0) ,告知应用层数据完整。
当手指松开时,触摸点就会被销毁,上报 ABS_MT_TRACKING_ID 事件,并将 value 设置为 -1(ID)
4.2 获取触摸屏的信息
这里需要用到 ioctl() 函数
#include <sys/ioctl.h>
int ioctl(int fd,unsigned long request,...);
第二个参数在input.h
头文件中 EVIOC 相关的宏中有定义,第三个参数则是根据第二个参数来确定。譬如获取设备名称:
char name[100];
ioctl(fd,EVIOCGNAME(sizeof(name)),name);
使用最多的是EVIOCGABS(abs)
,可以获取到触摸屏 slot 的取值范围,参数表示一个 ABS_XXX 绝对位移事件,譬如获取 slot 信息,第三个参数是 struct input_absinfo*
获取触摸屏支持的最大触摸点数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
int main(int argc,char *argv[])
{
if (2 != argc)
{
fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
return -1;
}
struct input_absinfo info;
int fd=open(argv[1],O_RDONLY);
ioctl(fd,EVIOCGABS(ABS_MT_SLOT),&info);
int max_slots=info.maximum+1-info.minimum;
printf("最大触摸点数: %d\n",max_slots);
close(fd);
return 0;
}
4.3 单点触摸应用程序——获取一个触摸点的坐标
int main(int argc,char *argv[])
{
if (2 != argc)
{
fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
return -1;
}
struct input_event in_ev;
int x=0,y=0;
int down=-1; // 记录 BTN_TOUCH 事件的 value,1 表示按下,0 表示松开,-1 表示移动
int valid=0; // 记录数据是否有效,更新表示有效,1 表示有效,0 表示无效
struct input_absinfo info;
int fd=open(argv[1],O_RDONLY);
for(;;)
{
if(sizeof(struct input_event) != read(fd,&in_ev,sizeof(struct input_event)))
{
perror("read");
return -1;
}
switch(in_ev.type)
{
case EV_KEY: // 按键事件
if(BTN_TOUCH == in_ev.code)
{
down = in_ev.value;
valid=1;
}
break;
case EV_ABS: //绝对位移事件
switch (in_ev.code)
{
case ABS_X: //X 坐标
x = in_ev.value;
valid = 1;
break;
case ABS_Y: //Y 坐标
y = in_ev.value;
valid = 1;
break;
}
break;
case EV_SYN: //同步事件
if (SYN_REPORT == in_ev.code)
{
if (valid) //判断是否有效
{
switch (down) //判断状态
{
case 1:
printf("按下(%d, %d)\n", x, y);
break;
case 0:
printf("松开\n");
break;
case -1:
printf("移动(%d, %d)\n", x, y);
break;
}
valid = 0; //重置 valid
down = -1; //重置 down
}
}
break;
}
}
}
4.4 多点触摸应用程序
// 描述 MT 多点触摸每一个触摸点的信息
struct ts_mt
{
int x;
int y;
int id; // 对应 ABS_MT_TRACKING_ID
int valid; // 数据有效标志位
};
// 触摸点的坐标
struct tp_xy
{
int x;
int y;
};
int ts_read(const int fd,const int max_slots,struct ts_mt *mt)
{
struct input_event in_ev;
static int slot = 0;
static struct tp_xy xy[12]={0};
int i;
// 对缓冲区进行初始化
memset(mt,0x0,max_slots *sizeof(struct ts_mt));
for(i=0;i<max_slots;i++)
{
mt[i].id=-2; // 初始化为 -2,-1 表示触摸点删除,id>=0 表示创建
}
for(;;)
{
read(fd,&in_ev,sizeof(struct input_event));
switch(in_ev.type)
{
// 按键事件对单点触摸比较有用
case EV_ABS: //绝对位移事件
switch (in_ev.code)
{
case ABS_MT_SLOT:
slot = in_ev.value;
break;
case ABS_MT_POSITION_X:
xy[slot].x = in_ev.value;
mt[slot].valid = 1;
break;
case ABS_MT_POSITION_Y:
xy[slot].y = in_ev.value;
mt[slot].valid = 1;
break;
case ABS_MT_TRACKING_ID:
mt[slot].id = in_ev.value;
mt[slot].valid = 1;
break;
}
break;
case EV_SYN: //同步事件
if (SYN_REPORT == in_ev.code)
{
for(i=0;i<max_slots;i++)
{
mt[i].x=xy[i].x;
mt[i].y=xy[i].y;
}
}
return 0;
}
}
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
fprintf(stderr,"usage: %s <input_dev>\n", argv[0]);
exit(EXIT_FAILURE);
}
struct input_absinfo slot;
struct ts_mt *mt =NULL;
int max_slots;
int fd;
int i;
fd=open(argv[1],O_RDONLY);
ioctl(fd,EVIOCGABS(ABS_MT_SLOT),&slot)
max_slots=slot.maximum+1-slot.minimum;
printf("max_slots:%d\n",max_slot);
mt=calloc(max_slots,sizeof(struct ts_mt));
// 读数据
for(;;)
{
if(ts_read(fd,max_slots,mt)<0)
{
break;
}
for(i=0;i<max_slots;i++)
{
if(mt[i].valid) // 判断每一个触摸点信息是否发生更新
{
if(mt[i].id>=0)
printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y);
else if (-1 == mt[i].id)
printf("slot<%d>, 松开\n", i);
else
printf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y);
}
}
}
close(fd);
free(mt);
return 0;
}
在单点触摸中,是通过BTN_TOUCH
事件判断手指动作的,而多点触摸中,需要通过 id 来判断多个手指的动作。