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

MVPenLinux から使う試みシリーズ。Linux の hiddev デバイスドライバの使い方。

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

今回は hiddev デバイスドライバのうち,Report の送受信にからむ部分のみ説明します。

HIDIOCGREPORT, HIDIOCSREPORT

バイスと Report をやりとりするには,HIDIOCGREPORT, HIDIOCSREPORTioctl() で使います。

struct hiddev_report_info repo;

int result = ioctl(handle, HIDIOCGREPORT, &repo);

こんな感じで。もちろん repo にはあらかじめそれなりの内容を設定しておく必要がありますけど。handle/dev/usb/hiddev*open (2) したファイルハンドルです。

3番目の引数で指定している struct hiddev_report_info の定義は以下のような感じです。

struct hiddev_report_info {
        __u32 report_type;
        __u32 report_id;
        __u32 num_fields;
};

report_type には Report の種別が Input Report か Output Report か Feature Report かを指定します。linux/hiddev.h では次のような定義があります。

#define HID_REPORT_TYPE_INPUT   1
#define HID_REPORT_TYPE_OUTPUT  2
#define HID_REPORT_TYPE_FEATURE 3

report_id は Report の ID*1を指定します。num_fields には前回説明したフィールド数を指定します(MVPen の場合,INPUT でも OUTPUT でも 1 になります)。

あれ?実際に送受信する Report の内容はどこで指定するのでしょうか?これは hiddev がちょっとかわったインタフェースになっていることを説明しなくてはなりません。

hiddev ドライバのマナー

hiddev インタフェースでは,各 Report Type, Report ID ごとに Field の値をドライバ内部に保持しています。これを事後・事前に HIDIOCGUSAGE, HIDIOCSUSAGE で取得・設定する必要があります。スクラッチパッド RAM のようなイメージです。

Report を送信するためには,

  1. HIDIOCSUSAGE で各 Field, 各 Usage ごとに値を設定する
  2. HIDIOCSREPORT でデバイスに Report を送信する

という手順をふむことになります。

逆に Report を受信するためには,

  1. HIDIOCGREPORT でデバイスから Report を受信する
  2. HIDIOGSUSAGE で各 Field, 各 Usage ごとの値を取得する

という手順をふむことになります。

HIDIOCGUSAGE, HIDIOCSUSAGE

今説明した Report の各 Field, 各 Usage ごとの値を取得・設定するのが,HIDIOCGUSAGE, HIDIOCSUSAGE という ioctl() コマンドです。

ioctl() の3番目のパラメータとして 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_type, report_id は先ほどの struct hiddev_report_info と同じです。

field_index は Field のインデックスになります。usage_index はその Field の中での Usage のインデックスを示します。usage_code は上位16 bitが Usage Page,下位16 bitが Usage (Type) になります。たとえば Generic Desktop Usage Page (= 0x01) の X (= 0x30) の usage_code0x00010030 になります。

value というのが指定した Field Index, Usage Index の値になります。注意しなくてはいけないのは,signed long になっているところです。

MVPen の場合,Report Descriptor の指示にしたがうと,各 Usage の値は Logical には -128〜127 の範囲の値を指定することになります。ですので,

uref.value = 0xff;

というのは期待どおりの結果とはなりません。Logical には -1 となるので,

uref.value = -1;    /* or 0xffffffff */

となります。実際にはバイト列として char 型を利用することが多いので,

char code = 0xff;
uref.value = code;

のように実質書くことになってこの違いが見えづらいのですが,unsigned char とか使っていた場合にはハマりますのでご注意を。以後の例では char を使っているのでこの符号付き値拡張変換が暗黙的になされることを利用したコードになっています。

Output Report を送信する code snipet

MVPen に Report を送信するコードを書いてみます。

MVPen の場合,Output Report は Report ID = 0 しか定義されていません。

  • Field の数は 1 つ
  • この Field の唯一の Usage は
    • Usage Page = 0xff0a
    • Usage (Type) = 0x0003
  • この Usage の Report Size = 8 bit,Report Count = 8
    • つまり 1 byte × 8 の Usage

実直に書くと以下のような感じになります。

char code[8] = { ... };

struct hiddev_usage_ref uref;

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

uref.usage_index = 0;
uref.value = code[0];
ioctl(fd, HIDIOCSUSAGE, &uref);

uref.usage_index = 1;
uref.value = code[1];
ioctl(fd, HIDIOCSUSAGE, &uref);

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

struct hiddev_report_info repo;

memset(&repo, 0, sizeof(repo));
repo.report_type = HID_REPORT_TYPE_OUTPUT;
repo.report_id   = 0;
repo.num_fields  = 1;

ioctl(fd, HIDIOCSREPORT, &repo);

HIDIOCSUSAGE で各 Usage のスクラッチパッドに値を設定しておき,最後に HIDIOCSREPORT で作り上げた Report を送信します。

本当はエラーチェックをしたりする必要があるのですが,それは以降の実例を参照,ということで。

MVPen のステータスを取得してみる

以前書いたように,MVPen のステータスを取得するための Output Report のバイト列は下記のようになります。

02 02 95 95 00 00 00 00

これを実際に送信して,ステータスを取得してみましょう。

コードは下記のようになります。

#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 msg_status[8] = { 0x02, 0x02, 0x95, 0x95 };

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

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

    /*
        send a report
     */

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

    /* set each of usages */

    for (i = 0; i < 8; i ++) {
        uref.usage_index = i;
        uref.value       = msg_status[i];
        if (ioctl(fd, HIDIOCSUSAGE, &uref) < 0) {
            perror("HIDIOCSUSAGE");
            exit(1);
        }
    }

    /* send */

    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);
    }

    /*
        wait, wait, wait ...
     */

    sleep(1);

    /*
        receive a report
     */

    /* receive */

    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);
    }

    memset(&uref, 0, sizeof(uref));
    uref.report_type = HID_REPORT_TYPE_INPUT;
    uref.report_id   = 0;
    uref.field_index = 0;
    uref.usage_code  = 0xff0a0003;

    /* get each of usages */

    for (i = 0; i < 64; i ++) {
        uref.usage_index = i;
        if (ioctl(fd, HIDIOCGUSAGE, &uref) < 0) {
            perror("HIDIOCGUSAGE");
            exit(1);
        }
        fprintf(stdout, "%02x ", (unsigned char) uref.value);

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

    close(fd);

    return 0;
}

ほんとうは,select (2), read (2) インタフェースを使うと,Input Report を receive したことを検出できるのですが,そのへんは後日説明するとして,便宜上 sleep() で Report を receive するまで待っています。


通常 hiddev デバイスを操作するには root 権限が必要なので((/etc/udev/ にそれなりの設定をすると一般ユーザでも操作できるようになる,と思います。)),sudo を使って実行すると,

% sudo ./hidtest /dev/usb/hiddev1
[sudo] password for dayflower: ********

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 

無事取得できました。

HIDIOCGUSAGES, HIDIOCSUSAGES をつかって Usage をバルク取得,バルク設定

このように1つ1つの Usage を HIDIOCSUSAGES でちまちま設定するのは面倒だよ,ということで,まとめて設定するための API もあります。それが HIDIOCGUSAGES, HIDIOCSUSAGES です。

カーネルドキュメントには説明がないのですが,ヘッダファイルに含まれていました。使い方は下記の実例を参照するほうが早いでしょう。

2008-08-19 追記: 下記では values を任意の量で用意してますけど,これだとときどきうまく動かなくなりました。素直に struct hiddev_usage_ref_multi(これだと 1024 個ぶん!)をそのまま使う必要があるようです。

#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 msg_status[8] = { 0x02, 0x02, 0x95, 0x95 };

struct hiddev_usage_ref_multi8 {
    struct hiddev_usage_ref uref;
    __u32 num_values;
    __s32 values[8];
};

struct hiddev_usage_ref_multi64 {
    struct hiddev_usage_ref uref;
    __u32 num_values;
    __s32 values[64];
};

int
main(int argc, char *argv[])
{
    int fd;
    struct hiddev_report_info           repo;
    struct hiddev_usage_ref_multi8      uref8;
    struct hiddev_usage_ref_multi64     uref64;
    int i;

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

    /*
        send a report
     */

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

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

    /* set bulk usages */

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

    /* send */

    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);
    }

    /*
        wait, wait, wait ...
     */

    sleep(1);

    /*
        receive a report
     */

    /* receive */

    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);
    }

    memset(&uref64, 0, sizeof(uref64));
    uref64.uref.report_type = HID_REPORT_TYPE_INPUT;
    uref64.uref.report_id   = 0;
    uref64.uref.field_index = 0;
    uref64.uref.usage_code  = 0xff0a0003;
    uref64.num_values       = 64;

    /* get bulk usages */

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

    for (i = 0; i < 64; i ++) {
        fprintf(stdout, "%02x ", (unsigned char) uref64.values[i]);

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

    close(fd);

    return 0;
}

usage_indexusage_code が異なる場合*2にどうすればいいのかはわからないのですが,MVPen の場合は単一の usage_code で済みますのでこれでうまくいきました。

*1:といってもシリアル番号ではなく,複数 Report が定義されている場合のタイプのようなものです。

*2:具体的には Mouse Pointer の X, Y や Usage Minimum, Usage Maximum で複数 Usage にまたがる INPUT, OUTPUT が定義されている場合です。