返回

Weston下控制台读取键盘输入的正确方法 (evdev/libinput)

Linux

Weston环境下控制台程序读取键盘输入的正确姿势

咱们碰到的问题是这样的:在一个基于 ARM Cortex-M、跑着 Debian Bullseye 和 Linux 6.2.0 内核的环境里,Weston 9.0.0 作为显示服务器,上面跑了一个控制台程序。这个程序负责启动并管理一个 Chromium Kiosk UI,并且内置了一个 Web 服务器。现在需要让这个后台的控制台程序能够响应 USB 键盘的插入,并读取按键输入。

以前这套东西跑在 X Window 系统下的时候,直接打开 /dev/tty0 设备文件就能读到键盘敲击。但换到 Weston 之后,试了 /dev/tty0/dev/tty/dev/console 甚至 /dev/pts/1,都没法儿捕捉到按键。

这事儿该怎么搞定呢?

问题出在哪?为啥 /dev/tty0 不灵了?

这得从 X Window 和 Weston (Wayland) 的架构差异说起。

在传统的 X Window 系统里,尤其是在没有图形登录管理器、直接从控制台 startx 的场景下,/dev/tty0 往往代表着物理控制台,包括连接到它的键盘。程序如果权限足够,直接去读这个设备文件,确实能拿到原始的键盘输入。这是一种比较直接但不够精细,也存在安全风险的方式。

切换到 Weston(或者说 Wayland 架构)后,情况大变。Wayland 的设计哲学强调安全和隔离。输入设备(键盘、鼠标等)的管理权通常被 Compositor(在这里就是 Weston)独占。Weston 会监听所有硬件输入事件,然后根据当前哪个窗口拥有焦点,把事件精准地派发给对应的图形应用程序。

咱们的控制台程序,它本质上不是一个标准的 Wayland 图形客户端。它没有自己的“窗口”让 Weston 来传递输入焦点和事件。所以,Weston 不会把键盘事件通过 Wayland 协议塞给它。

至于 /dev/tty* 或者 /dev/pts/* 这些设备文件,它们代表的是(虚拟)控制台或者伪终端。当你通过 SSH 登录,或者在图形界面里打开一个终端模拟器(比如 weston-terminal)时,你交互的就是这些 TTY 或 PTS 设备。它们处理的是字符流,而不是底层的原始键盘扫描码或按键事件。Weston 管理下的物理键盘输入,跟这些 TTY/PTS 设备不是一回事儿了,所以读它们自然拿不到你想要的键盘数据。

简单说:Weston 接管了输入硬件,不再允许程序随随便便通过旧的 /dev/tty0 方式去“偷听”键盘了。你的控制台程序需要换一种方式来感知键盘输入。

可行的解决路子

既然不能通过 Weston 的标准事件分发,也不能用老旧的 TTY 方式,那咱就得绕过 Weston 或者说直接跟内核的输入子系统打交道。主要有以下几种靠谱的路子:

路子一:直接对话内核 - 使用 evdev

这是最底层、最直接的方法。Linux 内核通过 /dev/input/eventX (X 是个数字) 这样的设备文件暴露了所有输入设备的原始事件流,这就是所谓的 evdev (event device) 接口。你的程序只要有权限,就可以直接打开对应的键盘设备文件,读取原始的按键按下、弹起等事件。

原理:

内核的输入子系统驱动会把键盘的物理按键动作(按下、弹起、长按重复)转换成标准的 input_event 结构体数据,然后写入到 /dev/input/ 目录下对应的 eventX 文件里。应用程序通过 read() 系统调用就能把这些事件读出来。

步骤:

  1. 找到键盘对应的 eventX 设备文件:

    • 键盘插上后,查看 /dev/input/ 目录下的变化。
    • 一个更稳妥的方式是查看 /dev/input/by-path/ 目录,这里的文件名通常包含了设备的物理连接路径(比如 platform-...-usb-....-event-kbd),方便识别。
    • 可以使用 evtest 工具 (需要安装: sudo apt install evtest)。运行 sudo evtest,它会列出所有输入设备,让你选择一个进行事件测试,能帮你确认哪个 eventX 是你的目标键盘。
  2. 获取读取权限:
    这是关键一步!默认情况下,/dev/input/eventX 文件通常只有 root 用户和 input 用户组有读取权限。

    • 方法A (不推荐,测试用):root 权限运行你的控制台程序。
    • 方法B (常用): 把运行你的控制台程序的用户添加到 input 组。sudo usermod -a -G input your_username注意: 用户需要重新登录才能使组变更生效。
    • 方法C (更精细,推荐): 使用 udev 规则(后面会详述),专门为你这个程序访问特定键盘设备授权,避免赋予过宽泛的 input 组权限。
  3. 编写代码读取事件:
    你需要用 C/C++ 或其他支持底层文件操作的语言来写代码。

    • 使用 open() 打开找到的 /dev/input/eventX 文件(以只读方式)。
    • 循环调用 read() 读取 struct input_event 数据。每次 read() 会返回一个或多个事件结构体。sizeof(struct input_event) 是固定的。
    • struct input_event 结构体通常定义在 <linux/input.h> 里,包含:
      • timeval time: 事件发生的时间戳。
      • __u16 type: 事件类型,比如 EV_KEY (按键事件)、EV_SYN (同步事件)。
      • __u16 code: 事件代码,对于 EV_KEY 就是键码 (比如 KEY_A, KEY_ENTER),定义在 <linux/input-event-codes.h>
      • __s32 value: 事件的值。对于 EV_KEY
        • 1: 表示按键按下。
        • 0: 表示按键弹起。
        • 2: 表示按键重复(长按)。
  4. 解析事件:
    程序需要判断读到的事件 type 是不是 EV_KEY。如果是,就根据 codevalue 来判断是哪个键发生了什么动作。读到 typeEV_SYNcodeSYN_REPORT 的事件通常表示一批相关事件(比如一次按键的按下/弹起)已经发送完毕,可以进行处理了。

代码示例 (C):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <linux/input.h>

// 记得包含 <linux/input-event-codes.h> 来获取 KEY_* 定义
#include <linux/input-event-codes.h>

int main(int argc, char *argv[]) {
    const char *dev_path = "/dev/input/eventX"; // !! 重要:替换 X 为你找到的实际数字

    if (argc > 1) {
        dev_path = argv[1]; // 可以通过命令行参数传入设备路径
    }

    int fd = open(dev_path, O_RDONLY);
    if (fd < 0) {
        perror("Error opening device file");
        fprintf(stderr, "Failed to open %s. Check path and permissions.\n", dev_path);
        return 1;
    }

    struct input_event ev;

    printf("Listening for keyboard events on %s...\n", dev_path);

    while (1) {
        ssize_t n = read(fd, &ev, sizeof(ev));
        if (n == (ssize_t)-1) {
            if (errno == EINTR) // Interrupted by signal, continue
                continue;
            else {
                perror("Error reading from device");
                break; // Other read error
            }
        } else if (n != sizeof(ev)) {
             // Should not happen with event devices, but good practice
            fprintf(stderr, "Error: Partial read (%zd bytes instead of %zu).\n", n, sizeof(ev));
            continue; // Or break, depending on desired robustness
        }

        // 我们只关心按键事件 (按下或弹起)
        if (ev.type == EV_KEY && (ev.value == 1 || ev.value == 0)) {
            printf("Key Event: Type=%d, Code=%d (%s), Value=%d (%s)\n",
                   ev.type, ev.code,
                   "?", // 这里可以加一个函数根据 code 查找键名字符串
                   ev.value,
                   ev.value == 1 ? "Pressed" : "Released");
            
            // 在这里根据 ev.code 和 ev.value 执行你的逻辑
            // 例如: if (ev.code == KEY_F1 && ev.value == 1) { do_something(); }
        }
        // 处理其他类型的事件如果需要...
        // if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
        //     // batch of events finished, maybe process accumulated state here
        // }
    }

    close(fd);
    return 0;
}

编译: gcc your_code.c -o your_program

安全提醒:

  • 权限!权限!权限!直接用 root 跑程序很危险。把用户加入 input 组是常用做法,但这意味着该用户能监听到系统上所有 输入设备的事件(包括鼠标移动、其他键盘等),可能带来安全隐患。最佳实践是配合 udev 规则,仅授予程序访问目标 USB 键盘的权限。
  • 设备文件路径 /dev/input/eventX 中的 X 可能会变。每次系统启动或者 USB 设备重新插拔,数字都可能不一样。所以硬编码 eventX 不可靠,需要动态查找或者使用 /dev/input/by-path/ 下的稳定路径。

进阶技巧:

  • 非阻塞读取: 使用 fcntl(fd, F_SETFL, O_NONBLOCK) 设置文件符为非阻塞模式,这样 read() 不会卡住等待事件,可以配合 select(), poll(), epoll() 等 I/O 多路复用机制,在事件到来时才去读取,让你的程序能同时处理其他任务(比如 Web 服务器的请求)。
  • 热插拔处理: 你的程序可能需要处理键盘被拔掉或插入的情况。可以通过监控 udev 事件来实现,或者简单地在读取出错 (比如 read 返回错误且 errno 不是 EINTR) 时认为设备已移除,然后尝试重新扫描 /dev/input/ 目录或等待 udev 信号来发现新设备。

路子二:更省心一点 - 使用 libinput

如果你觉得直接解析 evdev 的原始事件有点繁琐,特别是处理按键去抖、组合键状态、设备特性差异等问题时,可以考虑使用 libinput 库。libinput 是一个旨在提供统一、稳定接口来处理输入设备的库,Weston 自己可能也在用它。

原理:

libinput 内部封装了对 evdev 的访问和事件解析逻辑。它帮你处理了很多底层细节,提供了一个更高层、更易用的 API。你的应用程序跟 libinput 交互,libinput 再去跟内核的 evdev 打交道。

步骤:

  1. 安装 libinput 开发库: 在 Debian/Ubuntu 系统上通常是 sudo apt install libinput-dev
  2. 权限问题: 和直接用 evdev 一样,运行程序的进程仍然需要有读取 /dev/input/eventX 文件的权限。解决方法同上(input 组或 udev 规则)。
  3. 编写代码:
    • 包含 libinput.h 头文件。
    • 创建一个 libinput 上下文。通常需要提供一个简单的接口,让 libinput 能打开/关闭设备文件(或者直接给它设备路径)。
    • 进入事件循环,调用 libinput_dispatch() 来处理挂起的事件,然后用 libinput_get_event() 获取单个事件。
    • 根据事件类型 (libinput_event_get_type()) 处理,比如键盘事件 (LIBINPUT_EVENT_KEYBOARD_KEY)。
    • 从事件对象中提取详细信息,如键码 (libinput_event_keyboard_get_key()) 和状态 (libinput_event_keyboard_get_key_state())。

代码示例 (C - 简化框架):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <libinput.h>
#include <sys/stat.h>
#include <string.h>

// udev context for finding devices (optional but good)
#include <libudev.h> 

// libinput open/close interface for udev integration (or manual path)
static int open_restricted(const char *path, int flags, void *user_data) {
    // !! Important: Implement proper permission checks if needed !!
    // This example just opens directly. Might need sudo or input group.
    int fd = open(path, flags);
    return fd >= 0 ? fd : -errno;
}

static void close_restricted(int fd, void *user_data) {
    close(fd);
}

const static struct libinput_interface interface = {
    .open_restricted = open_restricted,
    .close_restricted = close_restricted,
};

int main() {
    struct libinput *li;
    struct udev *udev; // Optional: Using udev to manage devices

    // Example using udev to initialize libinput
    udev = udev_new();
    if (!udev) {
        fprintf(stderr, "Failed to create udev context.\n");
        return 1;
    }
    li = libinput_udev_create_context(&interface, NULL, udev);
    if (!li) {
        fprintf(stderr, "Failed to create libinput context from udev.\n");
        udev_unref(udev);
        return 1;
    }
    // Tell libinput to scan existing devices and watch for new ones
    if (libinput_udev_assign_seat(li, "seat0") != 0) {
         fprintf(stderr, "Failed to assign seat.\n");
         libinput_unref(li);
         udev_unref(udev);
         return 1;
    }
    
    printf("libinput listening for events...\n");

    // Get the libinput file descriptor to use with select/poll/epoll
    int li_fd = libinput_get_fd(li); 
    
    struct libinput_event *event;

    while (1) {
        // You'd typically use poll() or select() here on li_fd
        // For simplicity, just call dispatch and then get_event in a loop
        libinput_dispatch(li); 

        while ((event = libinput_get_event(li)) != NULL) {
            enum libinput_event_type type = libinput_event_get_type(event);

            if (type == LIBINPUT_EVENT_KEYBOARD_KEY) {
                struct libinput_event_keyboard *key_event = libinput_event_get_keyboard_event(event);
                uint32_t key_code = libinput_event_keyboard_get_key(key_event);
                enum libinput_key_state key_state = libinput_event_keyboard_get_key_state(key_event);

                printf("Keyboard Event: Code=%u, State=%s\n", 
                       key_code, 
                       key_state == LIBINPUT_KEY_STATE_PRESSED ? "Pressed" : "Released");

                // Your logic here based on key_code and key_state
            }
             // Handle other event types if needed (device added/removed, etc.)
            else if (type == LIBINPUT_EVENT_DEVICE_ADDED) {
                 struct libinput_device *dev = libinput_event_get_device(event);
                 printf("Device added: %s\n", libinput_device_get_name(dev));
                 // Could check if it's a keyboard here: libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD)
            } else if (type == LIBINPUT_EVENT_DEVICE_REMOVED) {
                 struct libinput_device *dev = libinput_event_get_device(event);
                 printf("Device removed: %s\n", libinput_device_get_name(dev));
            }

            libinput_event_destroy(event); // Must destroy event when done
            libinput_dispatch(li); // Check again immediately for more pending events
        }
         // Simple sleep if not using poll/select to avoid busy-looping
         usleep(10000); // 10ms
    }

    libinput_unref(li);
    udev_unref(udev); // If udev was used
    return 0;
}

编译 (需要链接 libinput 和 libudev): gcc your_code.c -o your_program -linput -ludev

安全提醒:

同样,权限是绕不开的问题。使用 libinput 并不能帮你规避掉访问 /dev/input/eventX 的权限需求。

进阶技巧:

  • libinput 提供了更丰富的设备信息和状态管理。可以查询设备能力(比如是不是键盘)、获取设备名称/物理路径等。
  • 结合 libudev (如示例代码框架所示),可以更优雅地处理设备的动态添加和移除。
  • libinput 的文件符 (libinput_get_fd()) 可以整合到你的主事件循环中 (比如和 Web 服务器的 socket 一起 poll/epoll)。

关键辅助:使用 udev 规则动态管理权限

无论是用 evdev 还是 libinput,手动改权限或者把用户加到 input 组都不够理想。udev 是 Linux 用来管理设备事件的系统服务,我们能用它在 USB 键盘插上时,自动设置正确的权限。

原理:

udev 会在内核探测到设备(如 USB 键盘插入)时收到通知。我们可以编写规则文件,告诉 udev:如果匹配到某个特定设备(比如通过 Vendor ID 和 Product ID 识别),就自动执行某些操作,例如修改对应 /dev/input/eventX 文件的权限模式 (MODE) 或所属用户/组 (OWNER, GROUP)。

作用:

  • 无需 root 权限运行程序。
  • 无需把用户加入权限过大的 input 组。
  • 实现权限的精细化控制,只授权给特定的设备。
  • 即插即用,键盘插上就能被程序正确访问。

操作步骤:

  1. 找到键盘的唯一标识:

    • 插上你的 USB 键盘。
    • 确定它对应的 eventX 文件(比如 /dev/input/event2)。
    • 使用 udevadm 命令查看该设备的属性:
      udevadm info -a -p $(udevadm info -q path -n /dev/input/eventX) # 替换 X
      
      仔细看输出,找到包含 ATTRS{idVendor}=="xxxx"ATTRS{idProduct}=="yyyy" 的那几行。这两个 ID 通常能唯一标识你的键盘型号。记下 xxxxyyyy 的值。也关注一下 SUBSYSTEMS=="input"KERNELS=="event*" 这样的匹配条件。
  2. 创建 udev 规则文件:
    /etc/udev/rules.d/ 目录下创建一个新文件,名字以 .rules 结尾,比如 99-my-keyboard.rules (数字表示执行优先级,越大越靠后执行)。

  3. 编写规则内容:
    在文件里写入类似下面的一行(根据你上一步找到的信息修改):

    # Rule for My Specific USB Keyboard
    SUBSYSTEM=="input", KERNEL=="event*", ATTRS{idVendor}=="xxxx", ATTRS{idProduct}=="yyyy", MODE="0660", GROUP="your_program_group"
    
    • SUBSYSTEM=="input": 匹配输入子系统设备。
    • KERNEL=="event*": 匹配 eventX 这种设备名。
    • ATTRS{idVendor}=="xxxx"ATTRS{idProduct}=="yyyy": 精确匹配你的键盘型号 。把 xxxxyyyy 替换成你查到的值。
    • MODE="0660": 设置设备文件的权限为 rw-rw---- (所有者可读写,同组用户可读写,其他人无权限)。这比 0666 (所有人可读写) 安全。
    • GROUP="your_program_group": 推荐! 指定一个特定的用户组。让你的控制台程序以这个组的成员身份运行。你需要先创建这个组 (sudo groupadd your_program_group),然后把运行程序的用户加进去 (sudo usermod -a -G your_program_group your_username)。如果图省事,也可以用 input 组,但安全性稍低。
  4. 重新加载 udev 规则:

    sudo udevadm control --reload-rules
    sudo udevadm trigger # 触发规则应用到现有设备
    

    或者直接拔插一下键盘,让规则生效。之后检查一下对应 /dev/input/eventX 文件的权限和所属组是不是变了。

安全提醒:

  • udev 规则匹配要尽可能精确,避免误伤其他设备。Vendor ID 和 Product ID 是比较好的选择。
  • 权限设置要遵循最小权限原则。MODE="0660" + 指定一个专用 GROUP 是比较推荐的安全配置。

总结一下推荐的方案:

使用 evdevlibinput 来读取键盘事件,并通过 udev 规则来动态管理设备文件的访问权限。这种方式绕开了 Weston 的限制,直接与内核输入子系统交互,同时通过 udev 实现了较好的权限控制和热插拔支持。具体选用 evdev 还是 libinput 取决于你是否愿意处理原始事件数据,以及对项目依赖库的接受程度。libinput 通常更方便一些。