Weston下控制台读取键盘输入的正确方法 (evdev/libinput)
2025-05-02 04:56:22
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()
系统调用就能把这些事件读出来。
步骤:
-
找到键盘对应的
eventX
设备文件:- 键盘插上后,查看
/dev/input/
目录下的变化。 - 一个更稳妥的方式是查看
/dev/input/by-path/
目录,这里的文件名通常包含了设备的物理连接路径(比如platform-...-usb-....-event-kbd
),方便识别。 - 可以使用
evtest
工具 (需要安装:sudo apt install evtest
)。运行sudo evtest
,它会列出所有输入设备,让你选择一个进行事件测试,能帮你确认哪个eventX
是你的目标键盘。
- 键盘插上后,查看
-
获取读取权限:
这是关键一步!默认情况下,/dev/input/eventX
文件通常只有root
用户和input
用户组有读取权限。- 方法A (不推荐,测试用): 用
root
权限运行你的控制台程序。 - 方法B (常用): 把运行你的控制台程序的用户添加到
input
组。sudo usermod -a -G input your_username
。注意: 用户需要重新登录才能使组变更生效。 - 方法C (更精细,推荐): 使用
udev
规则(后面会详述),专门为你这个程序访问特定键盘设备授权,避免赋予过宽泛的input
组权限。
- 方法A (不推荐,测试用): 用
-
编写代码读取事件:
你需要用 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
: 表示按键重复(长按)。
- 使用
-
解析事件:
程序需要判断读到的事件type
是不是EV_KEY
。如果是,就根据code
和value
来判断是哪个键发生了什么动作。读到type
为EV_SYN
,code
为SYN_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
打交道。
步骤:
- 安装
libinput
开发库: 在 Debian/Ubuntu 系统上通常是sudo apt install libinput-dev
。 - 权限问题: 和直接用
evdev
一样,运行程序的进程仍然需要有读取/dev/input/eventX
文件的权限。解决方法同上(input
组或udev
规则)。 - 编写代码:
- 包含
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
组。 - 实现权限的精细化控制,只授权给特定的设备。
- 即插即用,键盘插上就能被程序正确访问。
操作步骤:
-
找到键盘的唯一标识:
- 插上你的 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 通常能唯一标识你的键盘型号。记下xxxx
和yyyy
的值。也关注一下SUBSYSTEMS=="input"
和KERNELS=="event*"
这样的匹配条件。
-
创建
udev
规则文件:
在/etc/udev/rules.d/
目录下创建一个新文件,名字以.rules
结尾,比如99-my-keyboard.rules
(数字表示执行优先级,越大越靠后执行)。 -
编写规则内容:
在文件里写入类似下面的一行(根据你上一步找到的信息修改):# 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"
: 精确匹配你的键盘型号 。把xxxx
和yyyy
替换成你查到的值。MODE="0660"
: 设置设备文件的权限为rw-rw----
(所有者可读写,同组用户可读写,其他人无权限)。这比0666
(所有人可读写) 安全。GROUP="your_program_group"
: 推荐! 指定一个特定的用户组。让你的控制台程序以这个组的成员身份运行。你需要先创建这个组 (sudo groupadd your_program_group
),然后把运行程序的用户加进去 (sudo usermod -a -G your_program_group your_username
)。如果图省事,也可以用input
组,但安全性稍低。
-
重新加载
udev
规则:sudo udevadm control --reload-rules sudo udevadm trigger # 触发规则应用到现有设备
或者直接拔插一下键盘,让规则生效。之后检查一下对应
/dev/input/eventX
文件的权限和所属组是不是变了。
安全提醒:
udev
规则匹配要尽可能精确,避免误伤其他设备。Vendor ID 和 Product ID 是比较好的选择。- 权限设置要遵循最小权限原则。
MODE="0660"
+ 指定一个专用GROUP
是比较推荐的安全配置。
总结一下推荐的方案:
使用 evdev
或 libinput
来读取键盘事件,并通过 udev
规则来动态管理设备文件的访问权限。这种方式绕开了 Weston 的限制,直接与内核输入子系统交互,同时通过 udev
实现了较好的权限控制和热插拔支持。具体选用 evdev
还是 libinput
取决于你是否愿意处理原始事件数据,以及对项目依赖库的接受程度。libinput
通常更方便一些。