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

MVPenLinux から使う試みシリーズ。libusb でモバイルノートデータを取得してみました。

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

MVPen のローレベルインタフェース仕様

MVPen のローレベルなインタフェースを見てみます。

device: vendor = 0x0e20, product = 0x0101
  config#0
    interface#0
      (altsetting#0)
        endpoint#0
          address    = 0x81 (EPin 1)
          attributes = 0x03 (interrupt transfer)
    interface#1
      (altsetting#0)
        endpoint#0
          address    = 0x82 (EPin 2)
          attributes = 0x03 (interrupt transfer)

interface#1 はタブレットインタフェースである(と思われる)ため,今回は扱いません。

上記より,

  • PC→デバイスには,コントロール転送が用いられる(MVPen の場合,8 バイト)
  • バイス→PCには,エンドポイント 0x81*1からのインタラプト転送が用いられる(MVPen の場合,64 バイト)

ことになります。なぁんだ,HID レイヤーをかますよりより単純でしたね。

コントロール転送やインタラプト転送,エンドポイントといった専門用語については,USB通信プログラミングテクニック - 電子工作の実験室USBの基本アーキテクチャが参考になります。

libusb の使い方

マルチプラットフォームな USB の抽象 API として libusb があるので,今回は Linux hiddev インタフェースではなくこちらを使ってみました。MacOS X でもうまく動くかも?

libusb の使い方としては http://libusb.sourceforge.net/doc/ というドキュメントがあるのですが,ほとんど内容がありません。

ヘッダ usb.h や他のソース(libhid*2 など)を参考に,こういうことじゃないかなぁという解釈を書いておきます。

Output Report の送出

[http://libusb.sourceforge.net/doc/function.usbcontrolmsg.html:title] を使います。

MVPen(というか HID デバイス)の場合,コントロール転送を使って,

int length = usb_control_msg(
    usb_dev_handle,
    USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
    HID_REPORT_SET,
    (HID_RT_OUTPUT << 8) + report_id,
    interface_index,
    (char *) buffer, size, timeout);

のように書きます。と libhid のソースを読んで知りました。

本来3番目の引数は usb.h で定義されている USB_REQ_* という形式の定数を使うと思っていたのですが,それは2番目の引数が USB_TYPE_STANDARD (=0) の場合のようです。上記のように USB_TYPE_CLASS とするとデバイスクラス(今回の場合 HID クラス)に沿ったリクエストタイプ定数(今回の場合 HID_REPORT_SET)を指定することになります。

interface_index は MVPen の場合 0 となります(上記参照)。

Input Report の取得

MVPen の場合 Input Report はインタラプト転送で取得するので,より簡単です。

int length = usb_interrupt_read(
    usb_dev_handle,
    endpoint_address,
    (char *) buffer, size, timeout);

のようにします。endpoint_address は MVPen の場合 0x81 となります(上記参照)。

ソースコード

以前の Linux hiddev インタフェースを使用したバージョンからかなり流用しています。が,細かい部分が違います。もっときちんと抽象化しておくべきでしたね。

/* mvpen.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <usb.h>

extern int opt_verbose;

#define HID_REPORT_GET 0x01
#define HID_REPORT_SET 0x09

#define HID_GET_IDLE   0x02
#define HID_SET_IDLE   0x0A
  
#define HID_RT_INPUT   0x01
#define HID_RT_OUTPUT  0x02
#define HID_RT_FEATURE 0x03

static struct usb_device *
_get_mvpen_device(struct usb_bus *busses,
                  unsigned short vendor, unsigned short product)
{
    struct usb_bus *bus;

    for (bus = busses; bus; bus = bus->next) {
        struct usb_device *dev;

        for (dev = bus->devices; dev; dev = dev->next) {
            if (dev->descriptor.idVendor  == vendor
             && dev->descriptor.idProduct == product)
                return dev;
        }
    }

    return NULL;
}

static int
_claim_mvpen_interface(usb_dev_handle *handle, int interface)
{
#ifdef LINUX
    if (! usb_claim_interface(handle, interface)) {
        if (opt_verbose)
            fputs("successfully claimed.\n", stderr);
        return 0;
    }
    if (opt_verbose)
        fprintf(stderr, "claim_interface(first try): %s\n", usb_strerror());

    if (opt_verbose)
        fputs("detaching kernel driver\n", stderr);
    if (usb_detach_kernel_driver_np(handle, interface)) {
        fprintf(stderr, "detach_kernel_driver_np: %s\n", usb_strerror());
        return -1;
    }
#endif

    if (! usb_claim_interface(handle, interface)) {
        if (opt_verbose)
            fputs("successfully claimed.\n", stderr);
        return 0;
    }

    fprintf(stderr, "claim_interface: %s\n", usb_strerror());
    return -1;
}

usb_dev_handle *
mvpen_init(unsigned short vendor, unsigned short product)
{
    usb_dev_handle *handle;
    struct usb_device *dev;

    usb_init();
    usb_set_debug(1);
    usb_find_busses();
    usb_find_devices();

    dev = _get_mvpen_device(usb_get_busses(), vendor, product);
    if (! dev)
        return NULL;

    handle = usb_open(dev);
    if (! handle) {
        fprintf(stderr, "usb_open: %s\n", usb_strerror());
        return NULL;
    }

    if (_claim_mvpen_interface(handle, 0 /* as interface */)) {
        return NULL;
    }

    return handle;
}

void
mvpen_free(usb_dev_handle *handle)
{
    usb_release_interface(handle, 0 /* as interface */);
    usb_reset(handle);
    usb_close(handle);
}

static char buffer[0x40];

int
mvpen_recv_report(usb_dev_handle *handle, char *buf, int len, int timeout)
{
    int got;

    got = usb_interrupt_read(handle,
                             0x81 /* as endpoint */,
                             buffer, 64,
                             timeout);

    if (got != 64) {
        if (opt_verbose)
            fprintf(stderr, "interrupt_read(%d): %s\n", got, usb_strerror());

        return 0;
    }

    memcpy(buf, buffer, len);

    return len;
}

void
mvpen_send_report(usb_dev_handle *handle, const char *buf)
{
    int len;

    len = usb_control_msg(
        handle,
        USB_ENDPOINT_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
        HID_REPORT_SET,
        (HID_RT_OUTPUT << 8) + 0 /* as report id */,
        0 /* as interface */,
        (char *) buf, 8, 1000);

    if (len != 8) {
        fprintf(stderr, "control_msg: %s\n", usb_strerror());
        exit(1);
    }
}

int
mvpen_wait_ready(usb_dev_handle *handle)
{
    static char cmd_status[8] = { 0x02, 0x02, 0x95, 0x95 };
    static char sig[2] = { 0x80, 0xa9 };

    if (opt_verbose)
        fputs("waiting ready ..", stderr);

    for (;;) {
        mvpen_send_report(handle, cmd_status);

        if (mvpen_recv_report(handle, buffer, 0x10, 500) > 0) {
            if (! memcmp(buffer, sig, 2)) {
                if (opt_verbose)
                    fputs(". done\n", stderr);

                return (unsigned char) buffer[10];
            }
        }

        if (opt_verbose)
            fputs(".", stderr);
    }

    /* NOTREACHED */
}

int
mvpen_get_notes_size(usb_dev_handle *handle)
{
    static char cmd_notes_size[8] = { 0x02, 0x01, 0xb5, 0x00 };
    static char sig1[5] = { 0xaa, 0xaa, 0xaa, 0xaa, 0xaa };
    static char sig2[2] = { 0x55, 0x55 };

    if (opt_verbose)
        fputs("waiting report_size ..", stderr);

    for (;;) {
        mvpen_send_report(handle, cmd_notes_size);

        if (mvpen_recv_report(handle, buffer, 0x10, 500) > 0) {
            if (! memcmp(buffer + 0, sig1, 5)
             && ! memcmp(buffer + 7, sig2, 2)) {
                if (opt_verbose)
                    fputs(". done\n", stderr);

                return (unsigned char) buffer[5] * 0x100
                      + (unsigned char) buffer[6];
            }
        }

        if (opt_verbose)        
            fputs(".", stderr);
    }
}

int
mvpen_copy_notes_to_stream(usb_dev_handle *handle, FILE *fp)
{
    static char cmd_get_notes[8] = { 0x02, 0x01, 0xb6, 0x00 };
    char buf[0x40];
    size_t notes_size;
    unsigned index = 0;

    mvpen_wait_ready(handle);
    notes_size = mvpen_get_notes_size(handle);
    mvpen_send_report(handle, cmd_get_notes);

    for (;;) {
        int rd;

        rd = mvpen_recv_report(handle, buf, 0x40, 500);
        if (rd <= 0) {
            if (index >= notes_size)
                break;

            if (index > 0) {
                if (fseek(fp, SEEK_SET, 0)) {
                    fputs("output stream is not seekable\n", stderr);
                    exit(1);
                }
            }

            mvpen_wait_ready(handle);
            notes_size = mvpen_get_notes_size(handle);
            mvpen_send_report(handle, cmd_get_notes);

            continue;
        }

        index = ((unsigned char) buf[0]) * 0x100
              +  (unsigned char) buf[1];

        fprintf(stderr, "\rreading %u / %u", index, notes_size);

        if (0x40 - 2 != fwrite(buf + 2, 1, 0x40 - 2, fp)) {
            perror("fwrite");
            return 1;
        }

        if (index >= notes_size)
            break;
    }

    fputs("\n", stderr);

    return index;
}

void
mvpen_clear_notes(usb_dev_handle *handle)
{
    static char cmd_clear_notes[8] = { 0x02, 0x01, 0xb0, 0x00 };

    mvpen_send_report(handle, cmd_clear_notes);
}

関数インタフェース仕様が異なるのでメインルーチンもやや書き直してあります。

/* main.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>     /* for getopt() */
#include <usb.h>

int opt_size     = 0;
int opt_retrieve = 0;
int opt_clear    = 0;
int opt_verbose  = 0;

/* from mvpen.c */
usb_dev_handle * mvpen_init(unsigned short vendor, unsigned short product);
void mvpen_free(usb_dev_handle *handle);
int mvpen_wait_ready(usb_dev_handle *handle);
int mvpen_get_notes_size(usb_dev_handle *handle);
int mvpen_copy_notes_to_stream(usb_dev_handle *handle, FILE *fp);
void mvpen_clear_notes(usb_dev_handle *handle);

void
usage()
{
    fputs(
        "usage: mvpen [-s] [-r] [-c] [-v]\n"
        "\n"
        "\toptions:\n"
        "\t\t-s\treport size of notes\n"
        "\t\t-r\tretrieve notes and write to stdout\n"
        "\t\t-c\tclear all notes data\n"
        "\t\t\n"
        "\t\t-v\toutput messages verbosely\n"
        , stderr
    );
}

int
main(int argc, char *argv[])
{
    usb_dev_handle *handle;
    int notes_size = 0;
    int opt;

    while ((opt = getopt(argc, argv, "vsrc")) >= 0) {
        switch (opt) {
        case 'v':
            opt_verbose = 1;
            break;
        case 's':
            opt_size = 1;
            break;
        case 'r':
            opt_retrieve = 1;
            break;
        case 'c':
            opt_clear = 1;
            break;
        default:
            usage();
            exit(1);
        }
    }

    handle = mvpen_init(0x0e20, 0x0101);
    if (! handle)
        return 1;

    fprintf(stderr, "mode: %d\n", mvpen_wait_ready(handle));

    if (opt_size) {
        notes_size = mvpen_get_notes_size(handle);

        fprintf(stderr, "size of notes: %d bytes\n", (0x40 - 2) * notes_size);
    }

    if (opt_retrieve) {
        if (! notes_size)
            notes_size = mvpen_get_notes_size(handle);

        if (notes_size <= 0) {
            fputs("There are no notes\n", stderr);
        }
        else {
            mvpen_copy_notes_to_stream(handle, stdout);

            mvpen_wait_ready(handle);
        }
    }

    if (opt_clear) {
        mvpen_wait_ready(handle);

        mvpen_clear_notes(handle);

        mvpen_wait_ready(handle);
    }

    mvpen_free(handle);

    return 0;
}

*1:0x80 は OUTPUT Endpoint を示すので実質 1 です。

*2:素直に libhid を使うべきなのですが,うまく動かなかったので libusb で直接書きました。レポート構造が既にわかっていますしね。