Solaris de USB温度計

自宅サーバを運用していると、家の温度を測ってグラフ化したくなるというニーズはそれなりにあるようで、多くのブログ記事等がヒットします。 情報を集めてみると、PCsensor社のTEMPerというシリーズのUSB温度計が、1000円くらいから入手できてよく使われているようです。

USB温度計! USB thermometer-528018

USB温度計! USB thermometer-528018

pcsensor.com

安いのでとりあえずAmazonで購入してみました。私が購入したのは Gold TEMPer という製品のようです。中国語混じりのちょっと怪しげなパッケージです。 ドライバCDが付いていますが、Windows用のソフトウェアしかないので無視することにします。

f:id:shakemid:20170901190536j:plain

検索すると、ラズパイやLinuxサーバでの動作成功例が見つけられましたが、私の家の自宅サーバSolarisなので、当然Solarisでやってみたくなります。

USBポートに挿してみて、cfgadmで見てみたところ、TEMPerV1.4 としてOSには正しく認識されているようです。これはいけそうな気がしてきました。

$ cfgadm -v
Ap_Id                          Receptacle   Occupant     Condition  Information
When         Type         Busy     Phys_Id
...
usb2/1.6                       connected    configured   ok         Mfg: RDing  Product: TEMPerV1.4  NConfigs: 1  Config: 0  <no cfg str descr>
unavailable  usb-device   n        /devices/pci@0,0/pci8086,2036@1d/hub@1:1.6
...

prtconfで見ると、VendorID:0c45, ProductID:7401 のデバイスであることがわかります。

$ prtconf -v
System Configuration:  Oracle Corporation  i86pc
Memory size: 8106 Megabytes
System Peripherals (Software Nodes):

...
                        name='usb-product-name' type=string items=1
                            value='TEMPerV1.4'
                        name='usb-vendor-name' type=string items=1
                            value='RDing'
...
                        name='usb-product-id' type=int items=1
                            value=00007401
                        name='usb-vendor-id' type=int items=1
                            value=00000c45
...

メーカーサイトではWindows用のソフトウェアしか公開されていないようですが、USBプロトコルアナライザで解析して作られたと思われるLinux用のドライバを作っている方がいました。

https://relavak.wordpress.com/2009/10/17/temper-temperature-sensor-linux-driver/

このブログのレスの中にある pcsensor-0.0.1 は、 0c45:7401 のデバイスに対応しているようなので、試してみることにしました。Linux用ですが、カーネルモジュールではなく、ユーザモードで動作するlibusbというライブラリを使って作られているので、Linuxでなくても動作すると期待できます。実際、FreeBSDでの動作成功例も見つけることができました。

ymlab.hatenablog.com

Solarisでとりあえずmakeしてみましたが、だめでした。

$ tar zxvf pcsensor-0.0.1.tar.gz
$ cd pcsensor-0.0.1
$ CC=gcc make
gcc -DUNIT_TEST -o pcsensor pcsensor.c -lusb
pcsensor.c: In function 'interrupt_read':
pcsensor.c:235:5: warning: incompatible implicit declaration of built-in function 'bzero'
pcsensor.c: In function 'interrupt_read_temperatura':
pcsensor.c:254:5: warning: incompatible implicit declaration of built-in function 'bzero'
Undefined                       first referenced
 symbol                             in file
usb_detach_kernel_driver_np         /var/tmp//ccUZxvwd.o
ld: fatal: symbol referencing errors
collect2: ld returned 1 exit status
make: *** [pcsensor] Error 1

libusbのドキュメントを見たところ、usb_detach_kernel_driver_np はLinux専用の関数のようなので、コメントアウトしてみたところ、makeは通るようになりました。

http://www.electric-spoon.com/doc/libusb-dev/html/function.usbdetachkerneldrivernp.html

$ diff pcsensor.c.orig pcsensor.c
77c77
<       ret = usb_detach_kernel_driver_np(lvr_winusb, iInterface);
---
>       ret = 1; //usb_detach_kernel_driver_np(lvr_winusb, iInterface);

実行してみたところ、デバイスを見つけてはいるようなのですが、claimのところでうまくいってないようでした。(後述のmodunloadしてもだめでした)

$ sudo ./pcsensor -v
...
lvr_winusb with Vendor Id: c45 and Product Id: 7401 found.
usb_open: device ptr is 0x8065f68
usb_open: pindex = 0
Detach failed: Error 0[0]
Continuing anyway
Detach failed: Error 0[0]
Continuing anyway
Could not claim interface

libusbのバージョンには0.1系と1.0系があり、このドライバは0.1系を使っていました。libusb-1.0のドキュメントを見ると、Solarisを正式サポートしているようなので、1.0系に対応すれば動作するかもしれないと思いました。

github.com

Linux用のドライバ(pcsensorまたはtemper)は派生版がたくさんあるようでしたが、libusb-1.0に対応しているものは見つけられなかったので、対応する意味はそれなりにありそうです。

libusb-0.1系と1.0系ではAPIが異なっており、互換性がありません。とはいえ、関数名はだいたい先頭を usb から libusb に置き換えたものが同等の機能のようなので、ドキュメントを眺めつつなんとなく似た名前の関数に置き換えていきました。libusbの使い方がちょっとだけわかったような気がします。

丸1日ほど格闘した結果、なんとなく動くようになったのでGithubに上げました。

github.com

Solaris 11 と RHEL 6(CentOS 6)で正常に動作することを確認しています。いくつかの派生版で、複数デバイスに対応する機能追加が行われていたので、それも取り込んでいます。

Solaris 11 だと↓のような感じでビルドできます。事前にgccとlibusb-1を入れておいてください。

$ sudo pkg install gcc libusb-1
...
$ git clone https://github.com/shakemid/pcsensor-temper
$ cd pcsensor-temper
$ make
...
$ sudo ./pcsensor.sh
...

複数デバイスがある場合は、↓のような感じで列挙されます。1つのデバイスに2つのセンサが付いているものにも対応していますが、私のものは2つ目のセンサ(external)はないので異常な値になっています。

$ sudo ./pcsensor
2017/08/31 19:00:35
Temperature (0:internal) 81.28F 27.38C
Temperature (0:external) 214.60F 101.45C
2017/08/31 19:00:35
Temperature (1:internal) 81.16F 27.31C
Temperature (1:external) 214.60F 101.45C
...

また、Muninでグラフ化できるようにプラグインも作ってみました。Githubの同じリポジトリの中に置いてあります。

github.com

MuninのRRDにはキャリブレーションしていない生の値を記録し、CDEF機能で補正するような作りにしてあります。補正した値をRRDに格納してもよいですが、CDEF機能を使うと、後でキャリブレーションをやり直しても以前に取得した値も補正できる利点があります。 CDEF機能はとっつきづらいですが、逆ポーランド記法(RPN)でRRDに格納された値に様々な計算を加えることができるRRDtoolの機能で、いろいろと応用が利きます。

RRDtool - rrdgraph_rpn

上記のプラグインの場合は、たとえば↓のように env.cdef オプションで比率とオフセット値を設定できるようになっています。

/path/to/munin/etc/plugin-conf.d/temper
----
[temper]
    user root
    env.pcsensor /usr/local/bin/pcsensor
    env.cdef  temperature,1.0287,*,0.85,-

TEMPerのキャリブレーションについては↓の記事がわかりやすいと思います。

www.okahiro.info

Muninでグラフ化すると↓のような感じになります。さすがに8月の室内は30度を超えて暑いですね。

f:id:shakemid:20170901191455p:plain

詳細は不明なのですが、どうも16回データを取得すると、17回目にデータを取得できなくなるような事象に遭遇しました。

$ sudo pcsensor
2017/09/01 19:14:57
Temperature (0:internal) 89.60F 32.00C
Temperature (0:external) 214.60F 101.45C

16回繰り返す

$ sudo pcsensor
USB read failed: 0
USB interrupt read: File descriptor in bad state
Fatal error> USB read failed

↓で議論されているのと同じ事象のように見えますが、どうも解決していないようです。 github.com

Linuxだと、もう一度実行すると温度を取得できるようなのですが、Solarisだと何度実行しても取得できないままでした。 この状態のときは、何かがUSBデバイスをロックしているように見えたので、HIDドライバをmodunloadしてみたところ、温度を取得できるようになりました。

$ sudo cat /dev/usb/c45.7401/0/if0in1
cat: /dev/usb/c45.7401/0/if0in1: Device busy

$ modinfo | grep hid
168 fffffffff7e5d000   5858  20   1  hid (USB HID Client Driver)
...

$ sudo modunload -i 168
...

これはちょっとあんまりな気がするのでもっといい方法があるとよいのですが、今のところ他に方法を見つけられていないので、HIDドライバをmodunloadしてpcsensorコマンドを実行するラッパをpcsensor.shとして公開しています。

というわけで、SolarisでもUSB温度計を使用できるようになりました。めでたしめでたし。

2017/9/14追記

↓のPerlモジュールがSolarisでも動作しました。

Device::USB::TEMPer1F - search.cpan.org

cpanに登録されているのでcpanmでインストールできます。

$ sudo cpanm -v Device::USB::TEMPer1F
...

↓のようなサンプルスクリプトで温度を取得できました。

#!/usr/bin/perl

use strict;
use warnings;

use Device::USB::TEMPer1F;

my $temper = Device::USB::TEMPer1F->new or die $@;
print $temper->fetch, "\n";

動作はしたのですが、どうもfetchメソッドでexternalのセンサーの値を読んでいるようなので、internalのセンサーの値を読むように変更しました。これは作者にフィードバックしておこうと思います。

--- TEMPer1F.pm.orig    2017-09-14 10:21:37.907791130 +0900
+++ TEMPer1F.pm 2017-09-14 10:21:48.015163258 +0900
@@ -79,7 +79,7 @@
     ) || die "Cannot read the the temperature!\n";

     my $r = [unpack "C8", $self->{buffer}];
-    return sprintf "%0.2f", $r->[4] + $r->[5]/256;
+    return sprintf "%0.2f", $r->[2] + $r->[3]/256;
 }

 ############################ private methods ##############################
実行例
$ sudo perl temper.pl
33.69

このモジュールだと16回以上取得してもエラーにならないようです。しかし、最初にmodunloadする必要はあるようでした。上記のCプログラムも、このPerlモジュールに倣って、不要そうなini_control_transferのあたりを削除したらエラーが出なくなりました。

ずいぶんシンプルになりました。今までの苦労はいったい。でもだいぶ勉強になった気がしました。