MVPen (Pegasus Mobile NoteTaker) の解析 (7)

MVPenLinux から使う試みシリーズ。Linux の hiddev デバイスドライバでイベントを取得する方法。

※注意※ MVPen が故障する可能性があります

read (2) によるイベントの捕捉

いままで /dev/usb/hiddev* のデバイスデスクリプタハンドルにたいして ioctl (2) を使ってやりとりをしてきました。このやり方だけではデバイスから PC への情報の送出をリアルタイムでとらえることができません(=とりこぼす可能性もある)。このとき,ハンドルを read (2) すると,デバイスのステートが変わったときにイベントとして取得することができます。

read (2) で取得するデータは,デフォルトでは下記の構造体になります。

struct hiddev_event {
    unsigned hid;
    signed int value;
};

hid フィールドは Usage Code,つまり Usage Page と Usage の合成値になります。MVPen の場合 INPUT Report として 0xffa00002 に固定されています。value はその Usage で変化した値です。

イベントを捕まえるサンプルを書いてみます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <asm/types.h>
#include <linux/hiddev.h>

static char send_status[8] = { 0x02, 0x02, 0x95, 0x95 };

int
main(int argc, char *argv[])
{
    int fd;
    struct hiddev_report_info           repo;
    struct hiddev_usage_ref_multi       urefs;
    struct hiddev_event                 hidev;
    int flag;
    int i;

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    memset(&urefs, 0, sizeof(urefs));
    urefs.uref.report_type = HID_REPORT_TYPE_OUTPUT;
    urefs.uref.report_id   = 0;
    urefs.uref.field_index = 0;
    urefs.uref.usage_code  = 0xff0a0003;
    urefs.num_values       = 8;

    for (i = 0; i < 8; i ++) {
        urefs.values[i] = send_status[i];
    }

    /* OUTPUT Report の Usage Values の設定 */
    if (ioctl(fd, HIDIOCSUSAGES, &urefs) < 0) {
        perror("HIDIOCSUSAGES");
        exit(1);
    }

    /* OUTPUT Report の送信 */
    memset(&repo, 0, sizeof(repo));
    repo.report_type = HID_REPORT_TYPE_OUTPUT;
    repo.report_id   = 0;
    repo.num_fields  = 1;

    if (ioctl(fd, HIDIOCSREPORT, &repo) < 0) {
        perror("HIDIOCSREPORT");
        exit(1);
    }

    /* イベントを待つ */
    for (;;) {
        if (read(fd, &hidev, sizeof(hidev)) < sizeof(hidev)) {
            perror("read");
            exit(1);
        }
        fprintf(stderr,
                "#%u: %d\n",
                hidev.hid,
                hidev.value);
    }

    close(fd);

    return 0;
}

ほんとうは read (2)struct hiddev_event をジャストサイズで取得するとは限りません。厳密には自力でバッファリングする必要があるかとも思うのですが,経験上イベントの取得に関しては構造体のサイズで読み待ちをしても問題がないようです。

上記プログラムでは,MVPen のステータスを取得するための OUTPUT Report をあらかじめ送信しています。ですから,これを実行すると(ステータスの戻り)イベントが発生するはず,です。ところが,発生しません。

HIDDEV_FLAG_UREFHIDDEV_FLAG_REPORT

HIDDEV_FLAG_REPORTHIDIOCSFLAG でデバイスデスクリプタハンドルに設定すると,デバイスが Report を送信したときに,read (2) で捕まえることができます*1。なお,HIDDEV_FLAG_REPORT を設定するためには,あわせて HIDDEV_FLAG_UREF というフラグも設定する必要があります。これは read (2) で取得するイベント構造体が struct hiddev_event ではなく,struct hiddev_usage_ref になります。より詳細にわたる情報を得ることができるので一挙両得ですね。

struct hiddev_usage_ref の構造を再掲します。

struct hiddev_usage_ref {
    __u32 report_type;
    __u32 report_id;
    __u32 field_index;
    __u32 usage_index;
    __u32 usage_code;
    __s32 value;
};

report_id やら field_index やらも戻ってきますので便利です。

コード例は以下のようになります(ヘッダ等と OUTPUT Report の送信は省略しています)。

/* ...... snip snip snip ...... */

int
main(int argc, char *argv[])
{
    int fd;
    struct hiddev_report_info           repo;
    struct hiddev_usage_ref_multi       urefs;
    struct hiddev_usage_ref             uref;
    int flag;
    int i;

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    flag = HIDDEV_FLAG_UREF | HIDDEV_FLAG_REPORT;
    if (ioctl(fd, HIDIOCSFLAG, &flag) < 0) {
        perror("HIDIOCSFLAG");
        exit(1);
    }

    /* ...... snip snip snip ...... */

    for (;;) {
        if (read(fd, &uref, sizeof(uref)) < sizeof(uref)) {
            perror("read");
            exit(1);
        }
        fprintf(stderr,
                "[#%u] field#%u, usage#%u (0x%08x): %d\n",
                uref.report_id,
                uref.field_index,
                uref.usage_index,
                uref.usage_code,
                uref.value);
    }

    close(fd);

    return 0;
}

事前に HIDIOCSFLAG でフラグをセットしているだけです。なお,デバイスopen (2) するたびにフラグをセットする必要があるようです(open (2) するたびにフラグは 0 にリセットされる)。

これを実行すると,

% sudo ./hidtest /dev/usb/hiddev1

# GET STATUS (0x02 0x02 0x95 0x95 ...) への返答
[#0] field#0, usage#63 (0xffa00002): 0

# 以下ボタンを押すごとに発生する Report
[#0] field#0, usage#63 (0xffa00002): 0
[#0] field#0, usage#63 (0xffa00002): 0

無事イベントを捕まえることができるようになりました。


ところで,Usage Index が 63 になっているところが気になりますね。

わたしが実験したところ,タイミングにより,

[#0] field#0, usage#0 (0xffa00002): -128
[#0] field#0, usage#1 (0xffa00002): -75
[#0] field#0, usage#2 (0xffa00002): 0

...... snip snip snip ......

[#0] field#0, usage#62 (0xffa00002): 0
[#0] field#0, usage#63 (0xffa00002): 0

のように Usage Index が 0〜63*2 まで舐める場合と,

[#0] field#0, usage#63 (0xffa00002): 0

のように最終インデックスのみ帰ってくる場合がありました。さらにいうと後者の最終インデックスが帰ってくる状況が圧倒的に多かったです。

もし前者のようにすべての Usage Index に対してイベントが発生することが保証されていれば,HIDIOCGREPORT で改めてレポートを取得する必要はなくなるのですが……残念です。

よって,INPUT Report を取得するためには

  1. HIDDEV_FLAG_UREF | HIDDEV_FLAG_REPORT なフラグを HIDIOCSFLAG で立てておく
  2. read (2) で INPUT Report イベント待ち
  3. Usage Index が最終インデックスであるイベントだったら下記に進む
  4. HIDIOCGREPORT で INPUT Report を取得する
  5. HIDIOCGUSAGESHIDIOCGUSAGE で Report の内容を取得する

という流れで書くことになります。

イベントを待ってから INPUT Report を取得する

前回のサンプルでは,OUTPUT Report を送出してから sleep() して,INPUT Report を取得していました。今回の話をもとに,sleep() のかわりにイベントを待ってから INPUT Report を取得してみましょう。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <asm/types.h>
#include <linux/hiddev.h>

static char send_status[8] = { 0x02, 0x02, 0x95, 0x95 };

int
main(int argc, char *argv[])
{
    int fd;
    struct hiddev_report_info           repo;
    struct hiddev_usage_ref_multi       urefs;
    struct hiddev_usage_ref             uref;
    int flag;
    int i;

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    flag = HIDDEV_FLAG_UREF | HIDDEV_FLAG_REPORT;
    if (ioctl(fd, HIDIOCSFLAG, &flag) < 0) {
        perror("HIDIOCSFLAG");
        exit(1);
    }

    /* OUTPUT Report の構成 */
    memset(&urefs, 0, sizeof(urefs));
    urefs.uref.report_type = HID_REPORT_TYPE_OUTPUT;
    urefs.uref.report_id   = 0;
    urefs.uref.field_index = 0;
    urefs.uref.usage_code  = 0xff0a0003;
    urefs.num_values       = 8;

    for (i = 0; i < 8; i ++) {
        urefs.values[i] = send_status[i];
    }

    if (ioctl(fd, HIDIOCSUSAGES, &urefs) < 0) {
        perror("HIDIOCSUSAGES");
        exit(1);
    }

    /* OUTPUT Report の出力 */
    memset(&repo, 0, sizeof(repo));
    repo.report_type = HID_REPORT_TYPE_OUTPUT;
    repo.report_id   = 0;
    repo.num_fields  = 1;

    if (ioctl(fd, HIDIOCSREPORT, &repo) < 0) {
        perror("HIDIOCSREPORT");
        exit(1);
    }

    for (;;) {

        /* イベント待ち */
        for (;;) {
            if (read(fd, &uref, sizeof(uref)) < sizeof(uref)) {
                perror("read");
                exit(1);
            }
            fprintf(stderr,
                    "[#%u] field#%u, usage#%u (0x%08x): %d\n",
                    uref.report_id,
                    uref.field_index,
                    uref.usage_index,
                    uref.usage_code,
                    uref.value);

            if (uref.usage_index == 63)
                break;
        }

        /* INPUT Report の取得 */
        memset(&repo, 0, sizeof(repo));
        repo.report_type = HID_REPORT_TYPE_INPUT;
        repo.report_id   = 0;
        /* repo.num_fields  = 1; */

        if (ioctl(fd, HIDIOCGREPORT, &repo) < 0) {
            perror("HIDIOCGREPORT");
            exit(1);
        }

        /* INPUT Report の Usage Values の取得 */
        memset(&urefs, 0, sizeof(urefs));
        urefs.uref.report_type = HID_REPORT_TYPE_INPUT;
        urefs.uref.report_id   = 0;
        urefs.uref.field_index = 0;
        urefs.uref.usage_code  = 0xff0a0003;
        urefs.num_values       = 64;

        if (ioctl(fd, HIDIOCGUSAGES, &urefs) < 0) {
            perror("HIDIOCGUSAGES");
            exit(1);
        }

        /* INPUT Report のダンプ */
        for (i = 0; i < 64; i ++) {
            fprintf(stdout, "%02x ", (unsigned char) urefs.values[i]);

            if (i % 16 == 15)
                fputs("\n", stdout);
        }
    }

    close(fd);

    return 0;
}

いままでのプログラムでもそうですが,close (2) に到達しないので書いてある必要はないのですが,気持ち悪いので書いてあります。

これを実行すると,

% sudo ./hidtest /dev/usb/hiddev1

# GET STATUS (0x02 0x02 0x95 0x95 ...) への返答
[#0] field#0, usage#63 (0xffa00002): 0
80 a9 28 01 3e 01 09 7d 1c 0e 02 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

# 以下ボタンを押すごとに発生する Report
[#0] field#0, usage#63 (0xffa00002): 0
80 b5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

[#0] field#0, usage#63 (0xffa00002): 0
80 b5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

無駄なく INPUT Report を取得できるようになったことがわかります。

今回触れなかったこと

read (2) というのはデータが来るまでブロッキングしてしまいます。もし柔軟なプログラムを書くのであれば,select (2)タイムアウトを指定して待つようにするべきでしょう。

*1:じゃあいままでのイベントというのは何に呼応するはずのものなのか,ということになるのですが,よくわかりません。

*2:あくまで MVPen の INPUT Report の場合です