4、事件注入与模拟:触摸事件模拟(tslib/uinput)、按键事件模拟、坐标映射与校准
各位做嵌入式GUI的同行,大家好。今天我们来聊聊事件注入与模拟。说实话,这玩意儿在调试阶段特别重要。你想想看,硬件还没完全做好,或者触摸屏驱动还没调通,但UI代码已经写好了——这时候怎么测试?
嗯,事件模拟就是干这个的。我个人习惯在项目早期就把这套机制搭好,省得后面手忙脚乱。
4.1 触摸事件模拟:tslib 与 uinput
触摸事件模拟,说白了就是让系统以为有人在点屏幕。常用的工具有两个:tslib 和 uinput。
4.1.1 用 tslib 模拟触摸
tslib 本身是个触摸屏校准库,但它也提供了测试工具。我最常用的是 ts_test 和 ts_print。
先看看怎么用 tslib 模拟一个点击事件:
# 设置环境变量
export TSLIB_TSDEVICE=/dev/input/event0
export TSLIB_CALIBFILE=/etc/pointercal
export TSLIB_CONFFILE=/etc/ts.conf
# 用 ts_test 手动测试
ts_test
# 或者用 ts_print 查看原始数据
ts_print
我在项目中遇到过一个问题:tslib 模拟的事件,在 Qt 或 LVGL 里有时不响应。后来发现是环境变量没配全。尤其是 TSLIB_FBDEVICE 这个变量,很多新手会漏掉。
ts_print_raw 看原始坐标数据,能帮你快速判断触摸屏驱动是否正常。
4.1.2 用 uinput 模拟更灵活
tslib 的模拟能力有限。如果你需要更精细的控制——比如模拟多点触控、长按、滑动——那就得上 uinput。
uinput 是 Linux 内核的一个模块,允许用户态程序创建虚拟输入设备。说白了,你可以自己造一个“假触摸屏”。
来看一段代码,创建一个虚拟触摸设备并发送触摸事件:
#include <linux/input.h>
#include <linux/uinput.h>
#include <fcntl.h>
#include <unistd.h>
int create_virtual_touch(void) {
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
if (fd < 0) return -1;
// 设置设备属性
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_EVBIT, EV_ABS);
ioctl(fd, UI_SET_ABSBIT, ABS_X);
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE);
struct uinput_setup usetup = {
.id.bustype = BUS_USB,
.id.vendor = 0x1234,
.id.product = 0x5678,
.name = "Virtual Touchscreen"
};
ioctl(fd, UI_DEV_SETUP, &usetup);
ioctl(fd, UI_DEV_CREATE);
return fd;
}
void send_touch_event(int fd, int x, int y, int pressure) {
struct input_event ev;
// 设置坐标
ev.type = EV_ABS;
ev.code = ABS_X;
ev.value = x;
write(fd, &ev, sizeof(ev));
ev.code = ABS_Y;
ev.value = y;
write(fd, &ev, sizeof(ev));
ev.code = ABS_PRESSURE;
ev.value = pressure;
write(fd, &ev, sizeof(ev));
// 发送同步信号
ev.type = EV_SYN;
ev.code = SYN_REPORT;
ev.value = 0;
write(fd, &ev, sizeof(ev));
}
ioctl(fd, UI_DEV_DESTROY) 销毁虚拟设备,否则系统里会留下“幽灵设备”。
4.2 按键事件模拟
触摸屏模拟搞定了,按键模拟其实更简单。原理是一样的——通过 uinput 创建虚拟键盘设备。
我曾经在一个智能家居项目里,硬件按键还没打样,但UI已经需要响应按键操作。当时就是用 uinput 模拟了12个按键,配合自动化脚本跑了一整晚的回归测试。
按键模拟的核心代码:
// 创建虚拟键盘
int create_virtual_keyboard(void) {
int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
if (fd < 0) return -1;
ioctl(fd, UI_SET_EVBIT, EV_KEY);
// 注册需要的按键
ioctl(fd, UI_SET_KEYBIT, KEY_UP);
ioctl(fd, UI_SET_KEYBIT, KEY_DOWN);
ioctl(fd, UI_SET_KEYBIT, KEY_ENTER);
ioctl(fd, UI_SET_KEYBIT, KEY_BACK);
struct uinput_setup usetup = {
.id.bustype = BUS_USB,
.id.vendor = 0x4321,
.id.product = 0x8765,
.name = "Virtual Keyboard"
};
ioctl(fd, UI_DEV_SETUP, &usetup);
ioctl(fd, UI_DEV_CREATE);
return fd;
}
// 模拟按键按下和释放
void send_key_event(int fd, int keycode, int press) {
struct input_event ev;
ev.type = EV_KEY;
ev.code = keycode;
ev.value = press; // 1=按下, 0=释放
write(fd, &ev, sizeof(ev));
ev.type = EV_SYN;
ev.code = SYN_REPORT;
ev.value = 0;
write(fd, &ev, sizeof(ev));
}
// 使用示例:模拟按下回车键
send_key_event(kbd_fd, KEY_ENTER, 1);
usleep(50000); // 等待50ms
send_key_event(kbd_fd, KEY_ENTER, 0);
4.3 坐标映射与校准
好,事件能模拟了。但还有一个坑——坐标映射。
你想想看,触摸屏的物理坐标和屏幕的显示坐标,往往不是一一对应的。尤其是电阻屏,或者用了不同尺寸的LCD,坐标偏差能让你点东边跑到西边。
4.3.1 坐标映射的原理
坐标映射,说白了就是做一次线性变换。把触摸屏的原始坐标 (tx, ty) 映射到屏幕坐标 (sx, sy)。
公式其实很简单:
sx = (tx - x_min) * (screen_width) / (x_max - x_min)
sy = (ty - y_min) * (screen_height) / (y_max - y_min)
但实际项目中,触摸屏往往有旋转、镜像等问题。这时候就需要更复杂的变换矩阵。
4.3.2 tslib 的校准机制
tslib 自带了校准工具 ts_calibrate。它会让你点屏幕上的几个十字标记,然后生成校准参数,保存在 /etc/pointercal 文件里。
校准文件的内容长这样:
-0.001234 0.999876 12.3456
0.998765 0.001234 -8.7654
0 0 1
这是一个 3x3 的仿射变换矩阵。tslib 在读取触摸事件时,会自动用这个矩阵做坐标转换。
4.3.3 手动实现坐标映射
如果你不用 tslib,或者需要更灵活的映射方式,可以自己写代码。下面是一个带旋转支持的映射函数:
typedef struct {
int x_min, x_max;
int y_min, y_max;
int screen_width, screen_height;
int rotation; // 0, 90, 180, 270
} TouchCalibration;
void map_coordinates(TouchCalibration *cal,
int raw_x, int raw_y,
int *screen_x, int *screen_y) {
// 先归一化到 0-1 范围
float nx = (float)(raw_x - cal->x_min) / (cal->x_max - cal->x_min);
float ny = (float)(raw_y - cal->y_min) / (cal->y_max - cal->y_min);
// 根据旋转角度做变换
switch (cal->rotation) {
case 0:
*screen_x = nx * cal->screen_width;
*screen_y = ny * cal->screen_height;
break;
case 90:
*screen_x = ny * cal->screen_width;
*screen_y = (1 - nx) * cal->screen_height;
break;
case 180:
*screen_x = (1 - nx) * cal->screen_width;
*screen_y = (1 - ny) * cal->screen_height;
break;
case 270:
*screen_x = (1 - ny) * cal->screen_width;
*screen_y = nx * cal->screen_height;
break;
}
}
4.4 实战:自动化测试脚本
最后,分享一个我常用的自动化测试脚本思路。用 Python 调用 uinput,可以写一个完整的触摸测试套件:
#!/usr/bin/env python3
import uinput
import time
def create_touch_device():
device = uinput.Device([
uinput.BUS_USB,
uinput.ABS_X + (0, 1024, 0, 0),
uinput.ABS_Y + (0, 600, 0, 0),
uinput.ABS_PRESSURE + (0, 255, 0, 0),
uinput.BTN_TOUCH,
], name="Test Touch")
return device
def tap(dev, x, y):
"""模拟点击"""
dev.emit(uinput.ABS_X, x)
dev.emit(uinput.ABS_Y, y)
dev.emit(uinput.ABS_PRESSURE, 128)
dev.emit(uinput.BTN_TOUCH, 1)
time.sleep(0.05)
dev.emit(uinput.BTN_TOUCH, 0)
dev.emit(uinput.ABS_PRESSURE, 0)
def swipe(dev, x1, y1, x2, y2, steps=20):
"""模拟滑动"""
for i in range(steps):
x = x1 + (x2 - x1) * i // steps
y = y1 + (y2 - y1) * i // steps
dev.emit(uinput.ABS_X, x)
dev.emit(uinput.ABS_Y, y)
dev.emit(uinput.ABS_PRESSURE, 128)
dev.emit(uinput.BTN_TOUCH, 1)
time.sleep(0.01)
dev.emit(uinput.BTN_TOUCH, 0)
dev.emit(uinput.ABS_PRESSURE, 0)
# 使用示例
dev = create_touch_device()
tap(dev, 100, 200) # 点击 (100, 200)
swipe(dev, 0, 300, 800, 300) # 水平滑动
嗯,这一章就到这里。下一章我们聊聊性能分析——怎么找出GUI卡顿的根源。到时候我会分享一个我踩过的坑,保证让你印象深刻。