4、事件注入与模拟:触摸事件模拟(tslib/uinput)、按键事件模拟、坐标映射与校准

各位做嵌入式GUI的同行,大家好。今天我们来聊聊事件注入与模拟。说实话,这玩意儿在调试阶段特别重要。你想想看,硬件还没完全做好,或者触摸屏驱动还没调通,但UI代码已经写好了——这时候怎么测试?

嗯,事件模拟就是干这个的。我个人习惯在项目早期就把这套机制搭好,省得后面手忙脚乱。

4.1 触摸事件模拟:tslib 与 uinput

触摸事件模拟,说白了就是让系统以为有人在点屏幕。常用的工具有两个:tslibuinput

4.1.1 用 tslib 模拟触摸

tslib 本身是个触摸屏校准库,但它也提供了测试工具。我最常用的是 ts_testts_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));
}
注意: 使用 uinput 需要 root 权限。另外,记得在程序退出时调用 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);
关键点: 按键事件必须成对出现——按下和释放。只发按下不发释放,系统会认为按键一直卡住。我见过有人调试时忘了发释放事件,结果UI界面像中了邪一样不停触发。

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 在读取触摸事件时,会自动用这个矩阵做坐标转换。

我的经验: 如果发现校准后还是不准,别急着怀疑算法。先检查触摸屏的安装角度——我遇到过屏幕装歪了2度,校准参数怎么调都不对。后来把屏幕扶正,一次校准就OK了。

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调试的必备技能。tslib 适合快速验证,uinput 适合精细控制。坐标映射这块,千万别想当然——一定要做校准,而且要考虑屏幕旋转。我见过太多项目,UI逻辑没问题,最后发现是坐标映射搞反了,白白浪费几天调试时间。

嗯,这一章就到这里。下一章我们聊聊性能分析——怎么找出GUI卡顿的根源。到时候我会分享一个我踩过的坑,保证让你印象深刻。