Linux
systemd
udev
uinput

Linux の入力デバイスをカスタマイズ

デバイスから来るイベントを調べる。

デモのために『リングマウス』 http://amzn.asia/4XB6JeF というのを入手した。Linux からは普通のマウス入力として見える。ただデモではマウスの代わりではなく、ある機能を呼び出すボタンとして使いたいので、左クリック相当のイベントをキーボードの『V』に変換したい。そんな状況になった時のためのメモ。

まず /proc/bus/input/devices でデバイスファイル名を調べる。

$ cat /proc/bus/input/devices
...
I: Bus=0003 Vendor=0e8f Product=00a8 Version=0110
N: Name="DaKai 2.4G RX"
P: Phys=usb-0000:00:0c.0-1/input1
S: Sysfs=/devices/pci0000:00/0000:00:0c.0/usb1/1-1/1-1:1.1/0003:0E8F:00A8.0004/input/input10
U: Uniq=
H: Handlers=kbd mouse2 event8 
B: PROP=0
B: EV=1f
B: KEY=3007f 0 0 483ffff17aff32d bf54444600000000 1f0001 130c130b17c000 267bfad941dfed 9e168000004400 10000002
B: REL=1c3
B: ABS=7f0100000000
B: MSC=10

2つ出てきた。Handlers によると、event8 に割り当てられている事がわかる。hexdump で内容を読んでみる。

$ sudo hexdump /dev/input/event8
...
例えばボタンを押し込んだときのデータ
00035a0 d5ac 5a0b 0000 0000 0d48 0007 0000 0000
00035b0 0004 0004 0001 0009 d5ac 5a0b 0000 0000
00035c0 0d48 0007 0000 0000 0001 0110 0001 0000
00035d0 d5ac 5a0b 0000 0000 0d48 0007 0000 0000
...

/usr/include/linux/input.h にこのデータの定義がある。

struct input_event {
        struct timeval time;
        __u16 type;
        __u16 code;
        __s32 value;
};

これを使って、次のようなプログラムでデータの内容を読む。

/* input.c */

#include <assert.h>
#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

int main(void)
{
  int fd = open("/dev/input/event8", O_RDWR);
  assert(fd != -1);

  for (;;) {
    struct input_event event;
    assert(read(fd, &event, sizeof(event)) == sizeof(event));
    printf("tv_sec: %lu, type: %04x, code: %04x, value: %08x\n", event.time.tv_sec, event.type, event.code, event.value);
    fflush(stdout);
  }
  close(fd);
}

私の環境の struct timeval は 16 bytes なので、一つの input_event は 24 bytes となる。

$ gcc -o input input.c
$ sudo ./input
tv_sec: 1510729620, type: 0004, code: 0004, value: 00090001
tv_sec: 1510729620, type: 0001, code: 0110, value: 00000001
tv_sec: 1510729620, type: 0000, code: 0000, value: 00000000

コードの意味は /usr/include/linux/input-event-codes.h に書いてある。例えば上記のイベントは次のように翻訳される。

  • type: EV_MSC, code: 0x0004, value: 0x00090001
    • EV_MSC は他のイベントに当てはまらない入力をあらわす。
  • type: EV_KEY, code: BTN_LEFT, value: 1
    • EV_KEY はキーボードやボタンをあらわす。1 で押された事をあらわす。
  • type: EV_SYN, code: 0, value: 0
    • イベントを分割する印。マルチタッチに使われるらしい。

イベントを変換する

次に、やってきたマウスイベントをキーボードイベントに変換する。イベントは /dev/input から読むだけでなく書き込む事も出来る。イベントを書き込むとあたかもユーザが操作したかのようにアプリを制御出来る。ここで2つのトリッキーな点を解決すればデバイスのイベント変換を行える。

  • もともとのイベントの動作を無効化したい。
    • ioctl(fd, EVIOCGRAB, 1) を使ってもともとのイベントを隠蔽出来る。
  • 出力したいデバイスが繋がっていなくても生成したデバイスを書き込みたい。
    • /dev/uinput というのを使うと、仮想的にデバイスを作成出来る。
#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define INPUT_DEVICE "/dev/input/ringmouse"
#define OUT_EVENT KEY_V

#define die(str)                                                               \
  do {                                                                         \
    perror(str);                                                               \
    exit(EXIT_FAILURE);                                                        \
  } while (0)

void make_output(int fd) {
  ioctl(fd, UI_SET_EVBIT, EV_KEY);
  ioctl(fd, UI_SET_KEYBIT, OUT_EVENT);

  struct uinput_user_dev uidev = {0};

  snprintf(uidev.name, UINPUT_MAX_NAME_SIZE, "RingMouse");
  uidev.id.bustype = BUS_USB;
  uidev.id.vendor = 0x7777;
  uidev.id.product = 0x7777;
  uidev.id.version = 1;

  if (write(fd, &uidev, sizeof(uidev)) < 0)
    die("Fail to write uinput_user_dev");

  if (ioctl(fd, UI_DEV_CREATE) < 0)
    die("Fail to set UI_DEV_CREATE");
}

void emit(int output, int type, int code, int value) {
  struct input_event event = {.type = type, .code = code, .value = value};
  event.time.tv_sec = 0;
  event.time.tv_usec = 0;
  write(output, &event, sizeof(event));
}

int main(void) {
  int input = open(INPUT_DEVICE, O_RDWR);
  if (input == -1)
    die("Cannot open the input device");
  int output = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
  if (output == -1)
    die("Cannot open the output device");

  make_output(output);

  // Disable the input device
  ioctl(input, EVIOCGRAB, 1);

  while (1) {
    struct input_event event;

    ssize_t result = read(input, &event, sizeof(event));
    if (result == -1) {
      perror("Stopping");
      return 0;
    } else if (result != sizeof(event)) {
      die("Inconsistent event size");
    }
    switch (event.type) {
    case EV_KEY:
      if (event.code == BTN_LEFT && event.value == 1) {

        // Press left button -> Press v
        emit(output, event.type, OUT_EVENT, 1);
        emit(output, EV_SYN, SYN_REPORT, 0);

        // Release the key To prevent key repeat
        emit(output, event.type, OUT_EVENT, 0);
        emit(output, EV_SYN, SYN_REPORT, 0);
      }
    }
  }

  // Never reach here...
  // Enable the mouse
  ioctl(input, EVIOCGRAB, 0);
  close(input);
  close(output);
  return (EXIT_SUCCESS);
}

ということで、イベント変換 (ringmouse コマンド) は上手くいきました。あとは udev を使って device 名のハードコードを無くし、systemd で自動起動し、レシピを書けば完成。

イベントファイルを作成する

/dev/input/event8 などのデバイスファイル名は可変なのでプログラムに書けない。そこで、デバイス接続時に udev で /dev/input/ringmouse のように固定のシンボリックリンクを作成する。udev では、ついでに ringmouse コマンドを自動起動する unit も定義する。

まず、udevadm info -a sysfsパス を使って udev が拾えるイベントを調べる。

$ udevadm info -a /sys/class/input/event8
...
  looking at device '/devices/pci0000:00/0000:00:0c.0/usb1/1-1/1-1:1.1/0003:0E8F:00A8.0011/input/input47/event8':
    KERNEL=="event8"
    SUBSYSTEM=="input"
    DRIVER==""

  looking at parent device '/devices/pci0000:00/0000:00:0c.0/usb1/1-1/1-1:1.1/0003:0E8F:00A8.0011/input/input47':
    KERNELS=="input47"
    SUBSYSTEMS=="input"
    DRIVERS==""
    ATTRS{name}=="DaKai 2.4G RX"
    ATTRS{phys}=="usb-0000:00:0c.0-1/input1"
    ATTRS{properties}=="0"
    ATTRS{uniq}==""
...
    ATTRS{idProduct}=="00a8"
    ATTRS{idVendor}=="0e8f"
    ATTRS{ltm_capable}=="no"
    ATTRS{manufacturer}=="DaKai"
    ATTRS{maxchild}=="0"
    ATTRS{product}=="2.4G RX"
...
    ATTRS{bInterfaceProtocol}=="02"
...

デバイス情報はツリー状になっているが、udevadm info は指定されたデバイスから親に向かって走査してゆく。USB デバイスは idVendor と idProduct で特定するらしいので、このデバイスを特定するルールはこんな感じでしょうか。

KERNEL=="event*", ATTRS{idVendor}=="0e8f", ATTRS{idProduct}=="00a8", ENV{ID_INPUT_MOUSE}=="1", TAG+="systemd", SYMLINK+="input/ringmouse", ENV{SYSTEMD_ALIAS}="/sys/class/input/ringmouse"

デバッグ

sudo udevadm test /sys/class/input/event8

TAG+="systemd" によってリングマウス接続時に sys-class-input-ringmouse.device という unit が出来るので、その条件で ringmouse コマンドを開始する。

[Unit]
Description=Ring Mouse Event Translator
After=network.target
BindsTo=sys-class-input-ringmouse.device
After=sys-class-input-ringmouse.device

[Service]
ExecStart=/usr/bin/ringmouse
Restart=always
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=sys-class-input-ringmouse.device
WantedBy=multi-user.target

参考