💎

rkremap: キーボードデバイスの自動検出

2022/01/15に公開

https://zenn.dev/tmtms/articles/202201-rkremap の続き。

https://zenn.dev/tmtms/articles/202201-rkremap

Rkremap.new 時に引数でキーボードデバイスファイルを指定しないといけなかったんだけど、USB や Bluetooth キーボードとかデバイスファイル名がわからない場合に調べるのが面倒なので自動検出するようにしてみた。

入力デバイスの種類の取得

/dev/input/event* に対して ioctl(EVIOCGBIT(0)) をすれば入力デバイスの種類が得られる。

こんな感じ:

#include <linux/input.h>
#include <stdio.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
  unsigned char type[(EV_MAX-1)/8+1];
  int fd = open("/dev/input/event3", O_RDONLY);
  ioctl(fd, EVIOCGBIT(0, sizeof(type)), type);
  for (int i=0; i<sizeof(type); i++) {
    printf("%02x ", type[i]);
  }
  puts("");
}

この typeEV_* ビット目が立ってればその種類のデバイスということになるらしい。
EV_* 定数は /usr/include/linux/input-event-codes.h に定義されている。

ThinkPad T460s 本体のキーボード(/dev/input/event3)だと 13 00 12 00 という出力になった。
並び替えて2進数で表すと 0000 0000 0001 0010 0000 0000 0001 0011 となり、対応するビットは、0, 1, 4, 17, 20 なので、EV_SYN, EV_KEY, EV_MSC, EV_LED, EV_REP となる。

EV_KEY が含まれてるので、キーを持つデバイスだということがわかる。

Ruby で同じようなことをするにはこんな感じ:

EV_KEY = 1
EVIOCGBIT_0 = 2147763488     # EVIOCGBIT(0, 4)
f = File.open('/dev/input/event3')
buf = ''
f.ioctl(EVIOCGBIT_0, buf)
p buf[0, 4].unpack('C*').map{'%02X'%_1}.join(' ')

キーデバイスがキーボードかどうかの判定

ただ、この EV_KEY はキーボードだけじゃなくてキー(ボタン?)を持つデバイス全般が該当するらしい。
ThinkPad で evtest コマンドで調べると EV_KEY はほかにもあって、たとえば event1KEY_SLEEP だけを持つ Sleep Button で、event2KEY_POWER だけを持つ Power Button、タッチパッド(event15 Synaptics TM3145-003) やトラックポイント(event16 TPPS/2 IBM TrackPoint)も EV_KEY だった。

rkremap の対象は普通のキーボードデバイスなので、0, 9, A, Z, SPACE キーがあるデバイスを対象にした。

キーデバイスが対応しているキーを調べるには、ioctl(EVIOCGBIT(EV_KEY)) で得られた値に対してデバイスに対応する KEY_* ビットが立っているかどうかで判定できる。

#include <linux/input.h>
#include <stdio.h>
#include <fcntl.h>

int capable(unsigned char key[], int code)
{
  return (key[code/8] >> (code%8)) & 1;
}

int main(int argc, char *argv[])
{
  unsigned char key[(KEY_MAX-1)/8+1];
  int fd = open("/dev/input/event3", O_RDONLY);
  ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key)), key);
  printf("KEY_0=%d\n", capable(key, KEY_0));
  printf("KEY_9=%d\n", capable(key, KEY_9));
  printf("KEY_A=%d\n", capable(key, KEY_A));
  printf("KEY_Z=%d\n", capable(key, KEY_Z));
  printf("KEY_SPACE=%d\n", capable(key, KEY_SPACE));
}

Ruby だとこんな感じ:

EVIOCGBIT_EV_KEY = 2153792801  # EVIOCGBIT(0, 96)
EV_KEY = 1
KEY_0 = 11
KEY_9 = 10
KEY_A = 30
KEY_Z = 44
KEY_SPACE = 57
def capable?(code)= @key[code/8][code%8] != 0
f = File.open('/dev/input/event3')
buf = ''
f.ioctl(EVIOCGBIT_EV_KEY, buf)
@key = buf.unpack('C*')
puts "KEY_0=#{capable? KEY_0}"
puts "KEY_9=#{capable? KEY_9}"
puts "KEY_A=#{capable? KEY_A}"
puts "KEY_Z=#{capable? KEY_Z}"
puts "KEY_SPACE=#{capable? KEY_SPACE}"

/dev/input/event* を一つずつ上記のように調べてキーボードかどうかを調べることができる。

キーボードデバイスが複数の場合もあるので、IO.select で複数の入力を待つようにした。これで、本体のキーボードでCtrlキーを押しながら、USBキーボードでAキーを押すみたいなのにも対応できた。

参考にしたもの等


現状では rkremap 起動後にキーボードデバイスが追加/削除されたのには対応できないもどうにかしたいなぁ…。

Discussion