USB-RH で遊ぶ
USB-RH って何?
ストロベリー・リナックスさんが開発・販売している USB 温度・湿度測定モジュールです。
スイス製半導体温湿度センサを利用した高精度温湿度計で,ディスプレイの類はまったくついていない代わりに,USB を通じて温度・湿度を読み取ることができます。夢が広がりますね。
しかし,残念なことに公式に配布されているソフト(ドライバ)は Windows の DLL 形式のものだけであり,Linux 等には対応していません。さいわい?なぜか?HID デバイスクラスとして実装されているので解析すればなんとかなりそうですが……また USB HID の入出力を snoop する仕事がはじまるお……
と,思っていたんですが,すばらしいことに,とっくに Linux / BSD で使えるソフトウェアを書いてくださった方々がいらっしゃいました。
- http://www.nk-home.net/~aoyama/usbrh/ *BSD 用ユーザランドコマンド
- http://acapulco.dyndns.org/usbrh/ Linux 用カーネルドライバ!
- http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.html Linux 用ユーザランドコマンド(id:Briareos さん作)
これらを使ってハイオシマイ!でいいんですが,若干の tips を加えてお送りします。
ちなみに http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.html を CentOS 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:というか,それでうまくいくかなと思ってやったらうまくいっただけの話ですが。