USB-RH で遊ぶ

USB-RH って何?

ストロベリー・リナックスさんが開発・販売している USB 温度・湿度測定モジュールです。

スイス製半導体温湿度センサを利用した高精度温湿度計で,ディスプレイの類はまったくついていない代わりに,USB を通じて温度・湿度を読み取ることができます。夢が広がりますね。

しかし,残念なことに公式に配布されているソフト(ドライバ)は Windows の DLL 形式のものだけであり,Linux 等には対応していません。さいわい?なぜか?HID デバイスクラスとして実装されているので解析すればなんとかなりそうですが……また USB HID の入出力を snoop する仕事がはじまるお……

と,思っていたんですが,すばらしいことに,とっくに Linux / BSD で使えるソフトウェアを書いてくださった方々がいらっしゃいました。

これらを使ってハイオシマイ!でいいんですが,若干の tips を加えてお送りします。

ちなみに http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.htmlCentOS 5.3 i386 で使用しました。将来的にはカーネルドライバのほうが負荷が低そうなんですが,ユーザランドで動くほうがお手軽ですし,カーネルの usbfs ドライバを利用しているだけなので,別に効率が悪いわけではないですからね。

一般ユーザ権限で動くようにする

CentOS 5.3 だと,root 権限がないと http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.html を動かすことができませんでした。HID デバイスとしてカーネルがデバイスを登録するとき,デバイスノードが 0644 になってるんで一般ユーザではうまくアクセスできないからっぽい。

なので,まずはこのデバイスノードを一般ユーザ読み書き可能なようにシステムを構成します。

ぶらさがっているデバイスノードを調べる

まず,(最終的には必要ないんですが,調査のため)USB-RH がぶらさがっている USB バスのノード位置を調べてみます。

# lsusb

Bus 002 Device 001: ID 0000:0000  
Bus 004 Device 001: ID 0000:0000  
Bus 001 Device 001: ID 0000:0000  
Bus 003 Device 009: ID 1774:1001  
Bus 003 Device 001: ID 0000:0000  
Bus 005 Device 001: ID 0000:0000  

USB-RH はなぜか文字列でベンダ ID が出力されないんですが,ベンダ ID == 1774, プロダクト ID == 1001 なので,バス 003 の 9 つめのデバイスとして認識しているようです。

というだけだと不親切ですね。lsusb-v オプションを付けると,きちんと manufacturer がでてくるので,こいつだってわかります。

# lsusb -v -d 1774:

Bus 003 Device 009: ID 1774:1001
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0x1774
  idProduct          0x1001
  bcdDevice            1.00
  iManufacturer           1 Strawberry Linux Co.,Ltd.
  iProduct                2 Hygrometer/Thermometer
  iSerial                 0
  bNumConfigurations      1

...... snip snip snip ......
udev のルールを設定する

いまどきの Linux では udevd という daemon がデバイスノードの生成・削除を引き受けてます。んで,その「ルール」は /etc/udev/ 以下の設定ファイルに書いてあります。

RHEL / CentOS だと,/etc/udev/rules.d/50-udev.rules にデバイスノード生成時などのルールが書いてあります。USB デバイスに限定すると,下記の部分になります。

# /etc/udev/rules.d/50-udev.rules

ACTION=="add", SUBSYSTEM=="usb_device", \
        PROGRAM="/bin/sh -c 'K=%k; K=$${K#usbdev}; printf bus/usb/%%03i/%%03i $${K%%%%.*} $${K#*.}'", \
        NAME="%c", MODE="0644"

なんだか難しいですが,シェルスクリプトを介在させてデバイスノードパスを生成しているだけです。重要なのは,MODE="0644" の部分です。こいつを MODE="0666" にしたい。

んで,この上の部分の MODE= を書き換えてしまうと,すべての USB デバイスのデバイスノードの permission が 0666 になってしまいよろしくありません。

USB-RH 用にマッチするルールを一から書くというのも手なのですが,面倒なので上の設定をいかしつつ USB-RH の場合だけ MODE="0666" になるようなルールを追加してみます*1


結論だけ具体的にいうと,50-udev.rules より後で読み込まれるよう /etc/udev/rules.d/99-local-after.rules などのファイルを作成し,

# /etc/udev/rules.d/99-local-after.rules

BUS=="usb", SYSFS{idVendor}=="1774", SYSFS{idProduct}=="1001", MODE="0666"

のように書くだけです(3行目だけでよい)。


この書き方(文法)は http://www.gentoo.gr.jp/transdocs/udevrules/udevrules.html がとても参考になりました(ところどころ CentOS だと違ってましたけど)。

まず,USB-RH が接続されたときに udev にどのように認識されるのかを udevinfo というコマンドで調べました((udevmonitor --env でモニタリングしてもいいです。udevinfo のほうがアクティブに実行できる分楽でしたけど。))。

# udevinfo -a -p /sys/class/usb_device/usbdev3.9

Udevinfo starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/class/usb_device/usbdev3.9':
    KERNEL=="usbdev3.9"
    SUBSYSTEM=="usb_device"
    SYSFS{dev}=="189:264"

  looking at parent device '/devices/pci0000:00/0000:00:1d.1/usb3/3-1':
    ID=="3-1"
    BUS=="usb"
    DRIVER=="usb"
    SYSFS{configuration}==""
    SYSFS{product}=="Hygrometer/Thermometer"
    SYSFS{manufacturer}=="Strawberry Linux Co.,Ltd."
    SYSFS{maxchild}=="0"
    SYSFS{version}==" 1.10"
    SYSFS{devnum}=="9"
    SYSFS{speed}=="1.5"
    SYSFS{bMaxPacketSize0}=="8"
    SYSFS{bNumConfigurations}=="1"
    SYSFS{bDeviceProtocol}=="00"
    SYSFS{bDeviceSubClass}=="00"
    SYSFS{bDeviceClass}=="00"
    SYSFS{bcdDevice}=="0100"
    SYSFS{idProduct}=="1001"
    SYSFS{idVendor}=="1774"
    SYSFS{bMaxPower}==" 50mA"
    SYSFS{bmAttributes}=="80"
    SYSFS{bConfigurationValue}=="1"
    SYSFS{bNumInterfaces}==" 1"

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

/sys/class/usb_device/usbdev3.9 という引数は,最初に調査した USB-RH がどこにぶら下がっているかという状況によりけりです。

で,この ID=="3.1" とかそういう羅列を,udev のルールのマッチング式で使えばよい。んで,確実に USB-RH にマッチさせるなら,ベンダ ID とプロダクト ID だろうということで((もちろん SYSFS{product}SYSFS{manufacturer} でマッチしてもよかったんですが。)),上記のような式になったわけです。


さて,さきほどのルールで,うまくいくか,udevtest というコマンドで調べることができます。

# udevtest /class/usb_device/usbdev3.9

main: looking at device '/class/usb_device/usbdev3.9' from subsystem 'usb_device'
run_program: '/bin/sh -c 'K=usbdev3.9; K=${K#usbdev}; printf bus/usb/%03i/%03i ${K%%.*} ${K#*.}''
run_program: '/bin/sh' (stdout) 'bus/usb/003/009'
run_program: '/bin/sh' returned with status 0
udev_rules_get_name: rule applied, 'usbdev3.9' becomes 'bus/usb/003/009'
udev_device_event: device '/class/usb_device/usbdev3.9' already in database, validate currently present symlinks
udev_node_add: creating device node '/dev/bus/usb/003/009', major = '189', minor = '264', mode = '0666', uid = '0', gid = '0'
main: run: 'socket:/org/kernel/udev/monitor'
main: run: '/lib/udev/udev_run_devd'
main: run: 'socket:/org/freedesktop/hal/udev_event'
main: run: '/sbin/pam_console_apply /dev/bus/usb/003/009 '

udevtest コマンドの場合,パスの先頭の /sys があるとうまくいかないので注意!

ともあれ,udev_node_add のとこで mode = '0666' のようになっているので,うまくいきそうだということがわかります。

さて,これでテストしただけなので,udev の設定として反映はされていません。CentOS の場合 udevd は service として登録されていないみたいなので,udevcontrol というコマンド(apachectl みたいなものですね)で,設定ファイルを再読み込みするよう指示を出します。

# udevcontrol reload_rules      

では,USB-RH を USB から抜き差しします。

んで,デバイスノードの permission を調べると……

# ls -l /dev/bus/usb/003

total 0
crw-r--r-- 1 root root 189, 256 Jul 21 13:41 001
crw-rw-rw- 1 root root 189, 264 Jul 21 14:51 010

抜き差ししたためデバイス ID が 1 増えてしまいましたが,無事 0666 になっていることがわかります。

なお,似たようなパスとして /proc/bus/usb/* というパスもあるのですが,

# ls -l /proc/bus/usb/003 

total 0
-rw-r--r-- 1 root root 43 Jul 15 21:27 001
-rw-r--r-- 1 root root 52 Jul 21 14:47 010

こちらはこのように 0644 のままでも構わないようです。

USBRH on Linux をうまく動くようにちょっと改造する

あとは http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.html を一般ユーザ権限で実行するとうまくいくはず……なのですが,usb_set_configuration() という関数を実行する際に,Device or resource busy で怒られます。

libusb のドキュメントを読むと,すでに interface を claim してある場合にそうなるみたいなのですが,まだ claim_interface する前だし……

と思っていろいろ調べてみると,libusbについて - Linux工作室 のサンプルコードでは,usb_set_configuration() が失敗した場合にも usb_detach_kernel_driver_np() を呼んでいます。usb_claim_interface() が失敗した場合には usb_detach_kernel_driver_np() しろって libusb のドキュメントにも書いてありましたけど,こっちでもやったほうがいいのかな?

と疑いつつコードを修正すると,うまく動作しました。kernel usb driver のバージョンにもよるのかな?

--- a/usbrh_main.c      Tue Jul 21 13:28:30 2009 +0900
+++ b/usbrh_main.c      Tue Jul 21 14:51:54 2009 +0900
@@ -132,14 +132,15 @@

 void usage()
 {
-    puts("USBRH on Linux 0.05 by Briareos\nusage: usbrh [-vthm1fl]\n"
+    puts("USBRH on Linux 0.05 by Briareos\nusage: usbrh [-vthm1fls]\n"
          "       -v : verbose\n"
          "       -t : temperature (for MRTG2)\n"
          "       -h : humidity (for MRTG2)\n"
          "       -m : temperature/humidity output(for MRTG2)\n"
          "       -1 : 1-line output\n"
          "       -fn: set device number(n>0)\n"
-         "       -l : Device list\n" );
+         "       -l : Device list\n"
+         "       -sn: set sleep duration in n msec (default: 100ms)\n" );
 }

 int main(int argc, char *argv[])
@@ -154,6 +155,7 @@
 char flag_v, flag_t, flag_h, flag_d, flag_f, flag_1line, flag_mrtg, flag_l;
 char tmpDevice[8];
 int  DeviceNum;
+unsigned long sleep_usec;

     dev = NULL;
     dh = NULL;
@@ -161,11 +163,12 @@
     DeviceNum = 1;
     flag_f = flag_v = flag_t = flag_h = flag_d = flag_1line = flag_mrtg = flag_l = 0;
     temperature = humidity = 0;
+    sleep_usec = 100 * 1000;
     memset(buff, 0, sizeof(buff));
     memset(data, 0, sizeof(data));
     memset(tmpDevice, 0, sizeof(tmpDevice));

-    while((opt = getopt(argc, argv,"lvth1dmf:?")) != -1){
+    while((opt = getopt(argc, argv,"lvth1dmf:s:?")) != -1){
         switch(opt){
             case 'v':
                 flag_v = 1;
@@ -196,6 +199,9 @@
             case 'l':
                 flag_l = 1;
                 break;
+            case 's':
+                sleep_usec = atoi(optarg) * 1000;
+                break;
             default:
                 usage();
                 exit(0);
@@ -230,15 +236,23 @@
     }

     if((rc = usb_set_configuration(dh, dev->config->bConfigurationValue))<0){
-        puts("usb_set_configuration error");
-        usb_close(dh);
-        exit(3);
+        if((rc = usb_detach_kernel_driver_np(dh, dev->config->interface->altsetting->bInterfaceNumber))<0){
+            printf("usb_detach_kernel_driver_np error: %s\n", usb_strerror());
+            usb_close(dh);
+            exit(3);
+        }else{
+            if((rc =usb_set_configuration(dh, dev->config->bConfigurationValue))<0){
+                printf("usb_set_configuration error: %s\n", usb_strerror());
+                usb_close(dh);
+                exit(3);
+            }
+        }
     }

     if((rc =usb_claim_interface(dh, dev->config->interface->altsetting->bInterfaceNumber))<0){
         //puts("usb_claim_interface error");
         if((rc = usb_detach_kernel_driver_np(dh, dev->config->interface->altsetting->bInterfaceNumber))<0){
-            puts("usb_detach_kernel_driver_np error");
+            printf("usb_detach_kernel_driver_np error: %s\n", usb_strerror());
             usb_close(dh);
             exit(4);
         }else{
@@ -266,7 +280,7 @@

     // usb_control_msg() is successed
     if(rc>=0){
-        sleep(1);
+        usleep(sleep_usec);

         // Read data from device
         rc = usb_bulk_read(dh, 1, buff, 7, 5000);

上記コードでは,その他にも sleep(1) してるところが待ち時間がちと長くて嫌だったので,usleep() にして,コマンドラインから与えるように改造してあります。

これで,一般ユーザでも,うまく動くようになりました。

#と,ここまで書き上げてきて http://d.hatena.ne.jp/Briareos/20080325/1206397982 のコメント欄に同じ内容が書いてあることに気がつきました……orz

*1:というか,それでうまくいくかなと思ってやったらうまくいっただけの話ですが。