为允许访问设备的用户创建一个组,并创建一个 udev 规则以将该输入事件设备的所有权设置为该组。
我使用teensy
(系统)组:
sudo groupadd -r teensy
并使用例如将每个用户添加到其中
sudo usermod -a -g teensy my-user-name
或我可用的任何图形用户界面。
通过管理属于该teensy
组的用户和服务守护程序,您可以轻松管理对设备的访问。
对于我的 Teensy 微控制器(具有原生 USB,我用于 HID 测试),我有以下内容/lib/udev/rules.d/49-teensy.rules
:
ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", GROUP:="teensy", MODE:="0660"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", GROUP:="teensy", MODE:="0660"
SUBSYSTEMS=="usb",
不过,对于 HID 设备,您只需要第三行(一)。确保idVendor
和idProduct
匹配您的 USB HID 设备。您可以使用lsusb
列出当前连接的 USB 设备供应商和产品编号。匹配使用 glob 模式,就像文件名一样。
添加上述内容后,不要忘记运行sudo udevadm control --reload-rules && sudo udevadm trigger
以重新加载规则。下次您插入 USB HID 设备时,您组中的所有成员(teensy
在上面)都可以直接访问它。
请注意,默认情况下,在大多数发行版中,udev 还/dev/input/by-id/
使用 USB 设备类型和串行创建持久符号链接。在我的例子中,我的一个 Teensy LC(序列号 4298820)与组合的键盘-鼠标-游戏设备提供/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-event-kbd
了键盘事件设备、/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if01-event-mouse
鼠标事件设备和/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if03-event-joystick
两个/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if04-event-joystick
操纵杆接口。
(通过“持久”,我并不是说这些符号链接总是存在;我的意思是每当插入该特定设备时,该名称的符号链接就存在,并指向实际的 Linux 输入事件字符设备。)
Linux uinput设备可用于使用简单的特权守护进程实现虚拟输入事件设备。
创建新的虚拟 USB 输入事件设备的过程如下。
开放/dev/uinput
写作(或读写):
fd = open("/dev/uinput", O_RDWR);
if (fd == -1) {
fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
以上需要超级用户权限。但是,在打开设备后,您可以立即放弃所有权限,并让您的守护程序/服务以专用用户身份运行。
对允许的每种事件类型使用UI_SET_EVBIT
ioctl。
您至少要允许EV_SYN
;以及EV_KEY
键盘和鼠标按钮,以及EV_REL
鼠标移动等等。
if (ioctl(fd, UI_SET_EVBIT, EV_SYN) == -1 ||
ioctl(fd, UI_SET_EVBIT, EV_KEY) == -1 ||
ioctl(fd, UI_SET_EVBIT, EV_REL) == -1) {
fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
我个人在代码中使用了一个静态常量数组,以便于管理。
UI_SET_KEYBIT
对设备可能发出的每个键代码使用ioctl,UI_SET_RELBIT
对每个相对移动代码(鼠标代码)使用 ioctl。例如,允许空间、鼠标左键、水平和垂直鼠标移动以及鼠标滚轮:
if (ioctl(fd, UI_SET_KEYBIT, KEY_SPACE) == -1 ||
ioctl(fd, UI_SET_KEYBIT, BTN_LEFT) == -1 ||
ioctl(fd, UI_SET_RELBIT, REL_X) == -1 ||
ioctl(fd, UI_SET_RELBIT, REL_Y) == -1 ||
ioctl(fd, UI_SET_RELBIT, REL_WHEEL) == -1) {
fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
同样,静态 const 数组(一个用于代码UI_SET_KEYBIT
,一个用于UI_SET_RELBIT
代码)更易于维护。
定义 a struct uinput_user_dev
,并将其写入设备。
如果您name
包含设备名称字符串、vendor
USBproduct
供应商和产品 ID 号、version
版本号(0 很好),请使用
struct uinput_user_dev dev;
memset(&dev, 0, sizeof dev);
strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
dev.id.bustype = BUS_USB;
dev.id.vendor = vendor;
dev.id.product = product;
dev.id.version = version;
if (write(fd, &dev, sizeof dev) != sizeof dev) {
fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
后来的内核有一个 ioctl 来做同样的事情(显然参与 systemd 开发会导致这种流失);
struct uinput_setup dev;
memset(&dev, 0, sizeof dev);
strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
dev.id.bustype = BUS_USB;
dev.id.vendor = vendor;
dev.id.product = product;
dev.id.version = version;
if (ioctl(fd, UI_DEV_SETUP, &dev) == -1) {
fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
这个想法似乎是,您可以先尝试后者,而不是使用前者,如果失败,则改用前者。你知道,因为有朝一日单个界面可能还不够。(无论如何,这就是文档和提交所说的。)
在这里,我可能听起来有点胡思乱想,但这只是因为我确实赞同Unix 哲学和KISS 原则(或极简主义方法),并且认为这些疣完全没有必要。而且经常来自同一个松散相关的开发人员组。咳咳。没有人身侮辱的意图;我只是觉得他们做得不好。
通过发出UI_DEV_CREATE
ioctl 创建虚拟设备:
if (ioctl(fd, UI_DEV_CREATE) == -1) {
fprintf(stderr, "Cannot create the virtual uinput device: %s.\n", strerror(errno));
close(fd);
exit(EXIT_FAILURE);
}
此时,内核将构造设备,将相应的事件提供给 udev 守护进程,udev 守护进程将根据其配置构造设备节点和符号链接。所有这一切都需要一点时间——在现实世界中只有几分之一秒,但足以尝试立即发出事件可能会导致其中一些事件丢失。
struct input_event
通过写入 uinput 设备来发出输入事件 ( )。
您可以一次写入一个或多个struct input_event
s,并且永远不会看到短写入(除非您尝试编写部分事件结构)。部分事件结构被完全忽略。(有关内核如何处理此类写入,请参阅drivers/input/misc/uinput.c:uinput_write() uinput_inject_events()。)
许多动作由不止一个组成struct input_event
。例如,您可能会沿对角线移动鼠标(同时发射{ .type == EV_REL, .code == REL_X, .value = xdelta }
和发射{ .type == EV_REL, .code == REL_Y, .value = ydelta }
单个移动)。同步事件 ( { .type == EV_SYN, .code == 0, .value == 0 }
) 用作标记或分隔符,表示相关事件的结束。
因此,您需要{ .type == EV_SYN, .code == 0, .value == 0 }
在每个单独的操作(鼠标移动、按键、按键释放等)之后附加一个输入事件。将其视为等效于换行符,用于行缓冲输入。
例如,以下代码将鼠标沿对角线向右下移动一个像素。
struct input_event event[3];
memset(event, 0, sizeof event);
event[0].type = EV_REL;
event[0].code = REL_X;
event[0].value = +1; /* Right */
event[1].type = EV_REL;
event[1].code = REL_Y;
event[1].value = +1; /* Down */
event[2].type = EV_SYN;
event[2].code = 0;
event[2].value = 0;
if (write(fd, event, sizeof event) != sizeof event)
fprintf(stderr, "Failed to inject mouse movement event.\n");
失败案例不是致命的;这仅意味着事件没有被注入(尽管我不知道在当前内核中如何发生这种情况;最好是防御性的,以防万一)。您可以简单地再次重试,或忽略失败(但让用户知道,以便他们可以调查,如果它发生了)。所以记录它或输出警告,但不需要它导致守护程序/服务退出。
销毁设备:
ioctl(fd, UI_DEV_DESTROY);
close(fd);
当原始打开的描述符的最后一个副本关闭时,设备确实会自动销毁,但我建议像上面那样明确地这样做。
将步骤 1-5 放在一个函数中,你会得到类似的东西
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/uinput.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
static const unsigned int allow_event_type[] = {
EV_KEY,
EV_SYN,
EV_REL,
};
#define ALLOWED_EVENT_TYPES (sizeof allow_event_type / sizeof allow_event_type[0])
static const unsigned int allow_key_code[] = {
KEY_SPACE,
BTN_LEFT,
BTN_MIDDLE,
BTN_RIGHT,
};
#define ALLOWED_KEY_CODES (sizeof allow_key_code / sizeof allow_key_code[0])
static const unsigned int allow_rel_code[] = {
REL_X,
REL_Y,
REL_WHEEL,
};
#define ALLOWED_REL_CODES (sizeof allow_rel_code / sizeof allow_rel_code[0])
static int uinput_open(const char *name, const unsigned int vendor, const unsigned int product, const unsigned int version)
{
struct uinput_user_dev dev;
int fd;
size_t i;
if (!name || strlen(name) < 1 || strlen(name) >= UINPUT_MAX_NAME_SIZE) {
errno = EINVAL;
return -1;
}
fd = open("/dev/uinput", O_RDWR);
if (fd == -1)
return -1;
memset(&dev, 0, sizeof dev);
strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
dev.id.bustype = BUS_USB;
dev.id.vendor = vendor;
dev.id.product = product;
dev.id.version = version;
do {
for (i = 0; i < ALLOWED_EVENT_TYPES; i++)
if (ioctl(fd, UI_SET_EVBIT, allow_event_type[i]) == -1)
break;
if (i < ALLOWED_EVENT_TYPES)
break;
for (i = 0; i < ALLOWED_KEY_CODES; i++)
if (ioctl(fd, UI_SET_KEYBIT, allow_key_code[i]) == -1)
break;
if (i < ALLOWED_KEY_CODES)
break;
for (i = 0; i < ALLOWED_REL_CODES; i++)
if (ioctl(fd, UI_SET_RELBIT, allow_rel_code[i]) == -1)
break;
if (i < ALLOWED_REL_CODES)
break;
if (write(fd, &dev, sizeof dev) != sizeof dev)
break;
if (ioctl(fd, UI_DEV_CREATE) == -1)
break;
/* Success. */
return fd;
} while (0);
/* FAILED: */
{
const int saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
}
static void uinput_close(const int fd)
{
ioctl(fd, UI_DEV_DESTROY);
close(fd);
}
这似乎工作正常,并且不需要库(标准 C 库除外)。
重要的是要认识到 Linux 输入子系统,包括 uinput 和struct input_event
,是Linux 内核的二进制接口,因此将保持向后兼容(除非紧迫的技术原因,如安全问题或与内核其他部分的严重冲突) . (不希望将所有东西都放在 freedesktop.org 或 systemd 的保护伞下。)