USBRH driver for Linux を CentOS 5 で使う

いままで USB-RH を利用してきました*1が,ユーザランドのコマンド(http://www.dd.iij4u.or.jp/~briareos/soft/usbrh.html)をバックエンドとして使ってきました。

しかし,定期的に情報収集するのならカーネルモジュールである http://acapulco.dyndns.org/usbrh/ を使いたいものです。

メリット
  • カーネルモードで実行されるので効率がよい,可能性がある*2
  • LED やヒータの制御にも対応している(root 権限が必要ですが)
デメリット
  • カーネルをアップデートするたびにビルドする必要がある(と思う)
  • もしコードに問題があるとシステム全体が落ちる


では,ということで CentOS でビルドしてインストールしてみましたが,うまく動きません。

トラブルシューティングの「USBRH ドライバではなく USB HID ドライバがロードされてしまっている」にバッチリあてはまっています。

dmesg の様子を見てみると,

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

usbcore: registered new driver usbfs
usbcore: registered new driver hub

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

usbcore: registered new driver hiddev
usbcore: registered new driver usbhid
drivers/usb/input/hid-core.c: v2.6:USB HID core driver

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

Initializing USB Mass Storage driver...

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

usb 3-1: configuration #1 chosen from 1 choice
hiddev96: USB HID v1.00 Device [Strawberry Linux Co.,Ltd. Hygrometer/Thermometer
] on usb-0000:00:1d.1-1

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

usbcore: registered new driver usb-storage
USB Mass Storage support registered.

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

usbcore: registered new driver usbrh

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

このように,usbrh ドライバがロードされるのは,hiddev や usbhid などのドライバが登録されたあとです。なので USB-RH を usbhid が先に握ってしまい,usbrh ドライバまでまわってこないんですね。

上記トラブルシューティングで usbhid ドライバを一度アンロードしてからロードする,というのは,usb サブクラスドライバのドライバチェーンの順序を変えることで(usbrh のほうが usbhid より先に登録されるようにする)USB-RH が接続されている場合に usbrh ドライバが優先的にアタッチすることを期待している,のでしょう。


しかしながら,CentOS では usbhid ドライバは動的カーネルモジュールとしてではなく,静的にカーネルに組み込まれているため,このようなハックは実行しようがありません。


カーネルをリビルドするか……でもこれだけのためにするのもいやだな……と思いながらカーネルソースを覗いてみる*3と,usb 系のモジュールにはアンバインド・バインド処理を行う関数も登録されています。

これうまく使えるといいなぁ……と思いながらウェブで調べると,その答えとなる手法が載っているページがありました。⇒http://ubuntuforums.org/archive/index.php/t-123061.html

USB のデバイス/sys/bus/usb/devices/ ディレクトリ以下に列挙されています。

$ ls /sys/bus/usb/devices/
1-0:1.0  2-2    2-2.1:1.0  2-2:1.0  3-1      4-0:1.0  usb1  usb3  usb5
2-0:1.0  2-2.1  2-2.1:1.1  3-0:1.0  3-1:1.0  5-0:1.0  usb2  usb4

いっぽう,USB サブクラスドライバのインタフェースは /sys/bus/usb/drivers/ 以下に存在します。

$ ls -F /sys/bus/usb/drivers/
hiddev/  hub/  usb/  usb-storage/  usbfs/  usbhid/  usbrh/

さらに奥底を覗いてみると……

$ ls -F /sys/bus/usb/drivers/usbhid/

2-2.1:1.0@  2-2.1:1.1@  bind  new_id  unbind

まさに,bindunbind などのあやしげな名前のファイルが存在するではないですか。

で,先ほどのページの内容によると,デバイス番号をこれらの疑似ファイルに書き込んでやると,デバイスのドライバへのバインド・アンバインドができる,そうなのです。


じっさいにやってみましょう。とりあえず,USB-RH の USB デバイス番号が 3-1:1.0 だとします(デバイスを抜き差しして調べてみてください)。

root 権限で,まずは hiddev ドライバからアンバインドしてみます。

# echo -n "3-1:1.0" > /sys/bus/usb/drivers/hiddev/unbind
echo: write error: no such device

いきなり怒られてしまいました。CentOS では hiddev ドライバは利用されていないのかな。

では,usbhid ドライバからデバイスをアンバインドしてみます。

# echo -n "3-1:1.0" > /sys/bus/usb/drivers/usbhid/unbind

おお,怒られませんでした。ということは usbhid ドライバから切り離せたのかな。

最後に,いよいよ usbrh ドライバにバインドしてみます。

# echo -n "3-1:1.0" > /sys/bus/usb/drivers/usbrh/bind

怒られなかったので,うまくいったと思いたいです。dmesg みてみます。

/home/dayflower/src/usbrh-0.0.7/src/usbrh.c: USBRH device now attached to /dev/usbrh123

おお,無事 usbrh ドライバが USB-RH デバイスを認識しました。

じゃあ proc インタフェースも存在してるかな……

$ ls -F /proc/usbrh/
0/

$ ls -F /proc/usbrh/0/
heater  humidity  led  status  temperature

でてきてる!

$ cat /proc/usbrh/0/status

t:27.67 h:63.30

温湿度もきちんととれてる!


さて,このステップをいかに自動化するか,ですが。

以前も使った udev のしくみを利用します。udev では,デバイスの追加時などに,コマンドを実行することができます。もともと USBRH Linux Driver も modprobe を udev の rules で(/etc/udev/rules.d/10-usbrh.rule)おこなっていますが,これはまず削除しておきます。

んで,たとえば /etc/udev/rules.d/99-usbrh.rules などのファイルに次のように書きます。

ACTION=="add", BUS=="usb", SYSFS{idVendor}=="1774", SYSFS{idProduct}=="1001", \
    RUN+="usbrh.sh '%b:1.0'"

この RUN という部分が,コマンドを実行する部分です。

引数で渡している %b:1.0 というのは %b の部分は $id と同義で,さきほどの 3-1 のようなもので置換されます。後半の :1.0 を決め打ちにしてしまってますが……ここはどうするのがいいんだろう。複数台 USB-RH を接続するとうまくいかないかもしれません。

そして usbrh.sh というのが,さきほどのアンバインド・バインドをおこなうスクリプトで,以下の内容のものを /lib/udev/usbrh.sh という名前*4で置きます。

#!/bin/sh

DRIVER_PATH=/sys/bus/usb/drivers

/sbin/modprobe -v -s usbrh

echo -n "$1" > $DRIVER_PATH/hiddev/unbind 2>/dev/null
echo -n "$1" > $DRIVER_PATH/usbhid/unbind 2>/dev/null
echo -n "$1" > $DRIVER_PATH/usbrh/bind    2>/dev/null

exit 0

これしきの内容であれば普通はわざわざ別スクリプトにする必要もないんですが,udev rules の RUN で指定されたコマンドは,途中で fail するとその後の処理を断念してしまいます。なので,あえて別スクリプトにして戻り値を 0 (成功)にしているのです。

また,2>/dev/null で標準出力を omit してますが,これをはずすとエラーが発生した場合に udev が syslog にエラーを吐きます。んで,実は,なぜか,これらの echo コマンドはすべて,実際には write error: no such device で怒られているのです。ですが,怒られながらも実行すると,きちんと usbrh ドライバにバインドされます。不思議です。


ともかく,これらの仕掛けをして,udevcontrol reload_rules で udev のルールの再読み込みをさせ,USB-RH を抜き差しすると,以後自動的に usbrh ドライバがバインドされるようになります。接続したまま再起動してもきちんと認識してくれました。

おまけの spec ファイル

カーネル RPM モジュールとしてビルドしてみました。

# kmod-usbrh.spec

Source10: kmodtool
%define   kmodtool sh %{SOURCE10}

%{!?kversion: %define kversion %(uname -r)}
# hint: this can be overridden with "--define kversion foo" on rpmbuild, e.g.
# --define "kversion 2.6.18-128.el5"

%define kmod_name usbrh
%define kverrel %(%{kmodtool} verrel %{?kversion} 2>/dev/null)

%define upvar ""

%ifarch i686 x86_64 ia64
%define xenvar xen
%endif

%ifarch i686
%define paevar PAE
%endif

%{!?kvariants: %define kvariants %{?upvar} %{?xenvar} %{?paevar}}
# hint: this can be overridden with "--define kvariants foo" on rpmbuild, e.g.
# --define 'kvariants "" PAE'

Name:           %{kmod_name}-kmod
Version:        0.0.7
Release:        1%{?dist}
Summary:        kernel module for USB hygrometer / thermometer USB-RH
Group:          System Environment/Kernel
License:        GPL
URL:            http://acapulco.dyndns.org/usbrh/
Source0:        http://acapulco.dyndns.org/usbrh/usbrh-%{version}.tgz
BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
BuildRequires:  sed
ExclusiveOS:    linux
ExclusiveArch:  %{ix86} x86_64

%description
USB-RH is a hygrometer / thermometer connected via USB.

# magic hidden here:
%{expand:%(%{kmodtool} rpmtemplate_kmp %{kmod_name} %{kverrel} %{kvariants} 2>/dev/null)}
# additional files
%{_sysconfdir}/udev/rules.d/99-kmod-usbrh.rules
%attr(0755,root,root) /lib/udev/usbrh.sh

%prep
%setup -q -c -T -a 0

sed -i -e "/etc\/udev/d" %{kmod_name}-%{version}/Makefile

for kvariant in %{kvariants}; do
    cp -a %{kmod_name}-%{version} _kmod_build_$kvariant
done
cd %{kmod_name}-%{version}

%build
[ -n $RPM_BUILD_ROOT -a "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT
mkdir -p %{buildroot}

for kvariant in %{kvariants}; do
    ksrc=%{_usrsrc}/kernels/%{kverrel}${kvariant:+-$kvariant}-%{_target_cpu}
    pushd _kmod_build_$kvariant
    make TOPDIR=${ksrc} all %{?_smp_mflags}
    popd
done

%install
for kvariant in %{kvariants}; do
    ksrc=%{_usrsrc}/kernels/%{kverrel}${kvariant:+-$kvariant}-%{_target_cpu}
    pushd _kmod_build_$kvariant
    make TOPDIR=${ksrc} INSTALL_MOD_PATH=$RPM_BUILD_ROOT INSTALL_MOD_DIR=extra/%{kmod_name} install %{?_smp_mflags}
    popd
done

find $RPM_BUILD_ROOT -type f -name \*.ko -exec strip --strip-debug \{\} \;

mkdir -p $RPM_BUILD_ROOT/etc/udev/rules.d/
rule=$RPM_BUILD_ROOT/etc/udev/rules.d/99-kmod-%{kmod_name}.rules
echo -n 'ACTION=="add", BUS=="usb", '                          >$rule
echo -n 'SYSFS{idVendor}=="1774", SYSFS{idProduct}=="1001", ' >>$rule
echo    "RUN+=\"usbrh.sh '%b:1.0'\""                          >>$rule

mkdir -p $RPM_BUILD_ROOT/lib/udev/
usbrh_sh=$RPM_BUILD_ROOT/lib/udev/usbrh.sh
echo '#!/bin/sh'                                              >$usbrh_sh
echo 'DRIVER_PATH=/sys/bus/usb/drivers'                      >>$usbrh_sh
echo '/sbin/modprobe -v -s usbrh'                            >>$usbrh_sh
echo 'echo -n "$1" > $DRIVER_PATH/hiddev/unbind 2>/dev/null' >>$usbrh_sh
echo 'echo -n "$1" > $DRIVER_PATH/usbhid/unbind 2>/dev/null' >>$usbrh_sh
echo 'echo -n "$1" > $DRIVER_PATH/usbrh/bind    2>/dev/null' >>$usbrh_sh
echo 'exit 0'                                                >>$usbrh_sh
chmod 755 $usbrh_sh

%clean
[ -n $RPM_BUILD_ROOT -a "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT

%changelog
* Fri Jul 24 2009 dayflower - 0.0.7-1
- Initial release

これ使うには /usr/lib/rpm/redhat/kmodtoolSOURCES/ ディレクトリにコピーしておく必要があります(おのおの自分の SRPM に含めておく慣例みたい)。

んで,たとえば i686 なら,

$ rpmbuild -bb --target=i686 --define 'kvariants ""' SPEC/kmod-usbrh.spec

のようにするとカーネルモジュール RPM が生成されます。kvariants の部分を指定しないと,PAE 用カーネルモジュールなどもビルドしようとするので注意。

*1:USB-RH で遊ぶ - daily dayflower, USB-RH で温湿度を収集しグラフ化(collectd & rrdtool) - daily dayflower

*2:じっさいには,USB プロトコルシーケンスのやりとりのほうが時間的に食うのでそんなに差はないかと思います。しかし,ユーザランドのコマンドであれば温湿度を計測するたびに libusb が USB-RH を見つけるなどの手間が必要になります。カーネルモジュールのほうは,一度アクセスするデバイスが probe 時に定まってしまえばその手間はなくなるので,やはり軽い可能性があります。

*3:LXR / The Linux Cross Reference にお世話になりました。すっごい便利!

*4:ここのディレクトリに置くと,絶対パスで書かなくてもアクセスできるのです。もちろん別の場所に置いて絶対パスで指定してもよいですが。