hsuetsugu’s diary

ITの技術的なことに関して主に書きます。Rとpythonとd3.jsとAWSとRaspberryPiあたりを不自由なく使いこなせるようになりたいです。

(35連休)6日目:Raspberry Pi,fluentd,TreasureData,AWS,d3.jsを使った自宅の温度・湿度データモニタリングと可視化①

今回は、これまで準備したいろいろな要素技術を組みあわせて、自宅の温度、湿度を定期的に自動でTwitterに投稿し、経時での変化をグラフとしてみれるようなWEB画面を構築しようと思います。

背景

私が好きな、NHK Eテレの最終土曜日にやっている「NHK 新世代が解く!ニッポンのジレンマ」という番組で、前回(7月末の回)のテーマだった「僕らのアグリビジネス論」にて、農地で温度・湿度をはかるセンサーをおいて30分おきとかに計測して、Twitterにその場で自動的に投稿して、自宅や外出さきでも自分の農地の状態がみれるようなものが紹介されていました。
また、最近のカンブリア宮殿でも、北海道の浜中町という町で、農協が各酪農家から牛乳を2日に1回集めて、成分分析をして酪農家に結果をフォードバックし、酪農家はその結果に基づいて飼料の配合を変えるなど手をうっていく、というような事例も紹介されていました。
2014年8月14日放送 浜中町農協組合長 石橋 榮紀(いしばし しげのり)氏|カンブリア宮殿:テレビ東京

やっぱこういう時代なんだな〜とか思いつつ、1つめのようなものはRaspberryPi使って簡単に作れそうだったので、やってみることにしました。

今想定しているシステム構成

なんとなく技術的にははやりものをふんだんに使う感じでやってみます。

  • 温度湿度センサ(USB):これで温度と湿度を計測
  • Raspberry Pi:センサはここにつないで、ここでデータを収集。Twitterに投稿しつつ、fluentdでTreasureDataに送る。
  • TreasureData:無料の範囲で平気そうなので、一旦ここにデータを格納しておく。
  • AWS:TreasureDataからデータを取得*1して、AWS上のApacheでd3.jsを使ってこれまでの温度、湿度のデータをグラフに可視化する。

前提

先週やってきた下記のことは、今回やる上での前提となります。

温度湿度センサーの購入

ここで、完成品を購入しました。
http://strawberry-linux.com/catalog/items?code=52002

Raspberry Piに接続

USBで接続して、lsusbやdmsegコマンドで認識されていることを確認する。

計測データの取得

sudo apt-get install gcc libusb-dev
sudo wget http://www.dd.iij4u.or.jp/~briareos/soft/usbrh-0.05.tar.gz
sudo tar xvfz usbrh-0.05.tar.gz

この状態で、/home/pi/usbhrにてmakeして、sudo ./usbhrしても、"usb_set_configuration error"となるので、"usbhr_main.c"を下記のように修正する。
引用:USB温湿度計 - PukiWiki

USBRH on Linuxの実行時に発生する usb_set_configuration error のエラーの対処としていくつか書き込みがありましたが、以下の方法でもうまくいきました。
232行〜236行に2行追加

if((rc = usb_set_configuration(dh, dev->config->bConfigurationValue))<0){
if( rc = usb_detach_kernel_driver_np(dh, dev->config->interface->altsetting->bInterfaceNumber)<0 ){
puts("usb_set_configuration error");
usb_close(dh);
exit(3);
}
}

再度makeすると、下記のように温度と湿度が取得できました。暑い・・・。
f:id:hsuetsugu:20140815150320p:plain

下記コマンドにより、簡単に取得できるようになりました。

$ sudo chown root:root usbrh
$ sudo chmod u+s usbrh
$ sudo mv -i usbrh /usr/local/bin/
$ usbrh

Twitterに投稿(pythonで)

pythonからtwitterに投稿するのは先週やったので、少しだけ手をいれて、やってみました。
※先週の記事(http://hsuetsugu.hatenablog.com/entry/2014/08/11/112032)では、単にpython tweet.py "XXX"とすればXXXとtweetされるものでsys.argv[1]で取得していたので、この部分に取得したデータをいれるようにだけ修正しました。

pythonのコードはこちら。なにげにシェルの実行結果で返り値をどう取得するかというのを調べていたら時間がかかってしまいました。subprocessという標準ライブラリがでたおかげで簡単になったようです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# センサーからデータ取得
from subprocess import Popen,PIPE
p=Popen('usbrh',shell=True,stdin=PIPE,stdout=PIPE,close_fds=True)

line = p.stdout.readline()
temphum=line.rstrip().split(' ')

print temphum

# 取得データをTweet
import time, random, urllib, urllib2, cgi, hmac, hashlib, commands
import sys
# text = sys.argv[1] #この部分を変えただけ。
text = '-- Automatic Tweet from Raspberry Pi  --  Temp:' + temphum[0] + '  Hum:' + temphum[1]

conf = {}
for line in open('conf.txt'):
    name,value = line.rstrip().split(' ')
    conf.update({name:value})
for line in open('access_token.txt'):
    name,value = line.rstrip().split(' ')
    conf.update({name:value})

update_url = conf["update_url"]

params = {
    "oauth_consumer_key": conf["consumer_key"],
    "oauth_signature_method": "HMAC-SHA1",
    "oauth_timestamp": str(int(time.time())),
    "oauth_nonce": str(random.getrandbits(64)),
    "oauth_version": "1.0"
    }
params['oauth_token'] = conf["oauth_token"]
params['status'] = text
for k,v in params.items():
    if isinstance(v, unicode):
        params[k] = v.encode('utf8')
params_str = '&'.join(['%s=%s' % (urllib.quote(key, ''),urllib.quote(params[key], '~'))
                           for key in sorted(params)])
message = '%s&%s&%s' % ('POST',urllib.quote(update_url,''), urllib.quote(params_str,''))
key = "%s&%s" % (conf["consumer_secret"], conf["oauth_token_secret"])
signature = hmac.new(key, message, hashlib.sha1)
digest_base64 = signature.digest().encode("base64").strip()
params['oauth_signature'] = digest_base64
del params['status']
header_params_str = ",".join(["%s=%s" % (urllib.quote(k,''), urllib.quote(params[k],'~'))
                       for k in sorted(params)])
opener = urllib2.build_opener()
opener.addheaders = [('Authorization','OAuth %s' % header_params_str)]

result = opener.open(update_url,urllib.urlencode(
    {'status':text.encode('utf-8')})).read()

fluentdでTreasureDataに収集データを送る。(pythonで)

参考URL:Streaming Import from Python Apps | Treasure Data
fluent-loggerというライブラリを活用します。

$ sudo apt-get install python-setuptools
$ sudo easy_install pip
$ sudo pip install fluent-logger

決まりがあるらしく(理解できていないのですが・・・)、設定ファイルのsourceのところを書き換えます。

# Treasure Data Input and Output
<source>
  type forward
  port 24224
</source>

<match td.*.*>
  type tdlog
  apikey YOUR_API_KEY
  auto_create_table
  buffer_type file
  buffer_path /var/log/td-agent/buffer/td
</match>

pythonのコードは、下記の通りです。これで、TreasureDataのpidbというDBのusbrhというテーブルに測定データが格納されます。

# センサーからデータ取得
from subprocess import Popen,PIPE
p=Popen('usbrh',shell=True,stdin=PIPE,stdout=PIPE,close_fds=True)

line = p.stdout.readline()
temphum=line.rstrip().split(' ')

print temphum

temp=float(temphum[0])
hum=float(temphum[1])

# fluentdに送る
from fluent import sender
from fluent import event
sender.setup('td.pidb', host='localhost', port=24224)
event.Event('usbrh', {
  'temp': temp,
  'hum':  hum
})

定期的に実行

crontab(crontab - Wikipedia)という便利なツールがあったので、これを使いました。
crontab -eで設定、crontab -lで参照でき、実行したいコマンドを「分、時、日、月、曜日」の順番に設定していきます。下記の場合は、上記pythonのコードを10おきに実行するようになるはずです。

0,10,20,30,40,50 * * * * python /home/pi/SendViaFluentd.py

・・・なるはずだったのですが、実行されません。
/var/log/syslogをみると、"(CRON) info (No MTA installed, discarding output)"というメッセージが出力されていました。これを調べて、postfixをいれるとよさそうだったので、下記コマンドでインストール。

sudo apt-get install postfix

そして次の実行タイミング(10分後)がすぎてから、再び/var/log/syslogをみると、"メールが /var/mail/pi にあります"と書かれていたので、/var/mail/piをみてみると、"usbrh: not found"というメッセージがありました。
ということで、上記pythonのコードでセンサーから温度と湿度のデータを取得しているところを、
'usbrh' → 'sudo /usr/local/bin/usbrh'
と変更したら、無事10分おきに実行されてTreasureDataに格納されるようになりました。

f:id:hsuetsugu:20140818094706p:plain

ただずっと30度超えていて、なんか暑すぎるような・・・。それより体調を崩してしまい、自分の熱が38度くらいあるので、自分の熱をモニタリングしてみれるほうが今はずっと助かる・・・。

少し手を加えようとするだけでも何かしらエラーが出るので、意外に時間がかかってしまいました。明日はTreasureDataにためたこのデータをAWS上に、d3.jsで可視化する画面を作ろうと思います。

*1:ここが今回やるなかで技術的に見えていないところ。