このページは、本編のスピンオフページです。
    本編も併せて御覧くださいね。

安価で小型、USB接続で値を取ることができるCO2センサー、CO2Miniの値を、USB経由で読み取って記録するためのスクリプトを書いてみました。

手軽かつ多くの環境で使えるよう、言語はPerlで書いています。
今回はRaspberry Pi等に仕込んで、接続したら自動的にログ取りを開始するようにしてみました。

概要

Co2MiniなるデバイスはUSB接続するとHIDとして認識される。
Windows用にはソフトがあるが、Linux系OSでもHIDからのデータ取得ができれば使用可能。

raspberryPIに接続して、ロギングしよう。

CO2センサーの値を取ってくる

いろんな所からの寄せ集め。
軽く暗号化されていたりするのでそれを紐解く必要あり。
またデバイス名はここでは /dev/co2mini としている(後述)が、環境に合わせること。素の状態では /dev/hidraw0 になるはず。

co2mini.pl

#!/usr/bin/perl -w

use strict;
use warnings;
use POSIX;
use Fcntl;

# 出力をバッファしない
$| = 1;


my $device = "/dev/co2mini";

# Result of printf("0x%08X\n", HIDIOCSFEATURE(9)); in C
my $HIDIOCSFEATURE_9 = 0xC0094806;

# Key retrieved from /dev/random, guaranteed to be random ;-)
my $key =	"\x86\x41\xc9\xa8\x7f\x41\x3c\xac";

sysopen(my $FH, $device, O_RDWR|O_APPEND) or die "Error opening " . $device;
	
# Send a FEATURE Set_Report with our key
ioctl($FH, $HIDIOCSFEATURE_9, "\x00".$key) or die "Error establishing connection to " . $device;

my %result;

#LOOP!
while (1) {

	my $len = sysread($FH, my $buf, 8);
	die "Could not read from device" if $len != 8;
            
	my @data = co2mini_decrypt($key, $buf);
	if($data[4] != 0xd or (($data[0] + $data[1] + $data[2]) & 0xff) != $data[3]) {
     		die "co2mini wrong data format received or checksum error";
	}


	if ( chr($data[0]) eq "B" ) {
		# 気温
		#printf ("  Temp = %s\n", ($data[1] << 8 | $data[2]) / 16.0-273.15);
		#printf("B");
		$result{"B"} = ($data[1] << 8 | $data[2]) / 16.0-273.15;
	} elsif ( chr($data[0]) eq "P" ) {
		# Co2濃度
		#printf ("  Co2 = %s ppm\n", $data[1] << 8 | $data[2]);
		#printf("P");
		$result{"P"} = $data[1] << 8 | $data[2];
	}

	# 気温とCO2濃度が揃ったら表示
	if ( $result{"B"} && $result{"P"} ) {
		my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
		#my $date = sprintf("%04d-%02d-%02d-%02d:%02d:%02d",
		#	$year + 1900, $mon + 1, $mday, $hour, $min, $sec);
		#printf("%s : Co2 = %s ppm, Temp = %.2f degC\n", $date, $result{"P"}, $result{"B"});
		printf("Co2 = %s ppm, Temp = %.2f degC\n", $result{"P"}, $result{"B"});
		%result = ();
		sleep 15;
	}

	#printf (".");
}




# Input: string key, string data
# Output: array of integers result
sub co2mini_decrypt {

  my @key = map { ord } split //, shift;
  my @data = map { ord } split //, shift;
  my @offset = (0x48,  0x74,  0x65,  0x6D,  0x70,  0x39,  0x39,  0x65);
  my @shuffle = (2, 4, 0, 7, 1, 6, 5, 3);
  
  my @phase1 = map { $data[$_] } @shuffle;
  my @phase2 = map { $phase1[$_] ^ $key[$_] } (0 .. 7);
  my @phase3 = map { ( ($phase2[$_] >> 3) | ($phase2[ ($_-1+8)%8 ] << 5) ) & 0xff; } (0 .. 7);
  my @ctmp = map { ( ($offset[$_] >> 4 ) | ($offset[$_] << 4 )) & 0xff; } ( 0 .. 7);
  my @result = map { (0x100 + $phase3[$_] - $ctmp[$_]) & 0xff; } (0 .. 7);
  
  return @result;
}

実行するとフォアグラウンドに居座り、標準出力に対して15秒おきにCO2濃度と気温を出力し続けるという単純な動き。

CO2 miniが接続されたら記録開始

udevルールの記述

udevを使用する。

/etc/udev/rules.d/90-co2mini.rules として以下のように記載。
addとremoveでSUBSYSTEM/SUBSYSTEMSの記載が異なる点と、ベンダID/プロダクトIDの記載方法が異なる点に注意。

ベンダID/プロダクトIDはlsusbコマンドで確認。

raspberrypi# lsusb
Bus 001 Device 007: ID 058f:6366 Alcor Micro Corp. Multi Flash Reader
Bus 001 Device 004: ID 04bb:094c I-O Data Device, Inc.
Bus 001 Device 008: ID 04d9:a052 Holtek Semiconductor, Inc. ←★これ
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

addでSUBSYSTEMS="usb"にすると、USBとして認識された時点、hidデバイスとして認識された時点、hid-rawデバイスとして認識された時点の都合3回にわたって実行されてしまう。
またremoveの際にSUBSYSTEM=hidrawとして記載すると何故かヒットしないのでUSBとして書いておく。

挿すポートが変わったりしても同じデバイス名でアクセスできるように、/dev/co2mini という名前でシンボリックリンクを張り、ソフトウェア類からはそこへアクセスさせるようにしている。

挿入時にはsystemdへ制御を渡し、抜かれたときには単純にプロセスをkillする。

ACTION=="add", \
        SUBSYSTEM=="hidraw", \
        ATTRS{idVendor}=="04d9", \
        ATTRS{idProduct}=="a052", \
        SYMLINK+="co2mini", \
        TAG+="systemd", \
        ENV{SYSTEMD_WANTS}+="rc-co2mini.service"


ACTION=="remove", \
        SUBSYSTEMS=="usb", \
        ENV{ID_VENDOR_ID}=="04d9", \
        ENV{ID_MODEL_ID}=="a052", \
        RUN+="/usr/bin/pkill co2mini.pl"

挿入時に呼ばれるsystemdのスクリプトは /etc/systemd/system/rc-co2mini.service という名前で作ってみた。
やることは、30秒待った後にコマンドを実行するだけ。

[Unit]
Description=CO2-Mini Service
Requires=dev-co2mini.device
After=dev-co2mini.device

[Service]
Type=simple
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/co2mini/co2mini.pl | /usr/bin/logger -p user.err -t co2mini

これでCo2 Miniを接続したら自動的にログ取りが開始される。

ちなみに、udevdの起動コマンドとしてco2mini.pl をバックグラウンドで流すシェルスクリプトを入れてみたところ、一定時間後に「ちゃんと起動していない」と見なされてkillられてしまった。

非PC所有環境でのRaspberry PIのシャットダウン

自宅以外の場所でログ取りをする際に考慮が必要なのが、Raspberry PIの安全なシャットダウン。
起動は電源を入れればよいけど、シャットダウンを電源ブチ切りですますのはあまりにも乱暴。

なので、特定のUSB的な何かを接続したら、電源を落とすように仕込む。これも先ほどと同様udevdを使用。
ぶっ刺したらシャットダウンさせるための「キルスイッチ」は、百円均一ショップで売ってる、MicroSDカードリーダを使用してみた。

デバイスIDの確認

対象デバイスを接続し、ベンダID、プロダクトIDを確認。

raspberrypi# lsusb
Bus 001 Device 013: ID 14cd:121f Super Top ←★これ
Bus 001 Device 004: ID 04bb:094c I-O Data Device, Inc.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

ベンダID:14cd、プロダクトID:121f。

udevスクリプトの作成

/etc/udev/rules.d に適当な名前でファイルを作成。 今回は 90-shutdown.rules として以下のように作成。前述の通りベンダIDやプロダクトIDだけではマッチしないのでSUBSYSTEMやSUBSYSTEMSまで書いてヒット条件を縛っておくとよい。

どのサブシステムとして認識されているかはudevadmコマンドの結果をちょっと加工したものを使用。今回挿すものが /dev/sda として認識される前提なので、そうでない場合はコマンドラインの該当箇所を修正して実行。

raspberrypi# udevadm info -a -p $(udevadm info -q path -n /dev/sda) | more
  looking at device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.0/host0/target0:0:0/0:0:0:0/blo
ck/sda':
    KERNEL=="sda"
    SUBSYSTEM=="block" ←★
    DRIVER==""
    ATTR{range}=="16"
    ATTR{capability}=="51"
(略)

  looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5':
    KERNELS=="1-1.5"
    SUBSYSTEMS=="usb" ←★
    DRIVERS=="usb"
    ATTRS{bDeviceClass}=="00"
    ATTRS{rx_lanes}=="1"
    ATTRS{manufacturer}=="Generic"
    ATTRS{bmAttributes}=="80"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{version}==" 2.00"
    ATTRS{devnum}=="13"
    ATTRS{bMaxPower}=="250mA"
    ATTRS{idProduct}=="121f" ←★
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{urbnum}=="774"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{maxchild}=="0"
    ATTRS{bcdDevice}=="0300"
    ATTRS{bMaxPacketSize0}=="64"
    ATTRS{idVendor}=="14cd" ←★
    ATTRS{product}=="Mass Storage Device"
    ATTRS{speed}=="480"
    ATTRS{removable}=="removable"
    ATTRS{ltm_capable}=="no"
(後略)

このへんから、USB接続のブロックデバイスとして認識されていることが分かるのでそれを特定のキーにしていく。
上から順に、深い→浅い 順番でそのデバイスの情報が表示されるので、最初に下のほうを見てベンダID,プロダクトIDやSUBSYSTEMSを見つつ、上に遡っていって詳細なSUBSYSTEM情報を取ってくるのがいいかな。

ACTION=="add", \
       SUBSYSTEM=="block" , \
       SUBSYSTEMS=="usb" , \
       ATTRS{idVendor}=="14cd",\
       ATTRS{idProduct}=="121f", \
       RUN+="/sbin/shutdown -h now"

ベンダID 14cd 、プロダクトID 121f のUSBデバイスがaddされたら/sbin/shutdown -h now を実行する。

ファイル作成したらudevdを再起動(必要なのかな)

service udev restart

これで該当のUSBデバイスをブッ挿すとshutdownするはず。