GWの最初の日、ということと誕生日がもうちょいなので、早めの誕生日プレゼントということでRaspberryPi 3(以降RasPi)の一式を購入しちゃいました。
セットとしてはこんな感じ。
本体とケースのセット。ケースはGPIOの部分が外れたり、GPIOのチートシートが入っていたりと、初心者にも優しげだった+ケース付きということで決定。
http://www.amazon.co.jp/Raspberry-ボード&ケースセット-(Element14版-Computing-Lab/dp/B01CSFZ4JG?ie=UTF8&psc=1&redirect=true&ref_=oh_aui_detailpage_o00_s00
どうあがいても必須なSDカード。Class10だとインストールが早かったです(後述)
http://www.amazon.co.jp/【Amazon-co-jp限定】Transcend-microSDHCカード-Class10-Newニンテンドー3DS-TS32GUSDHC10E/dp/B008UR8TS0?ie=UTF8&psc=1&redirect=true&ref_=oh_aui_detailpage_o00_s00
やりたいことが一通り出来そうだったスターターパック。
http://www.amazon.co.jp/Raspberry-Piで学ぶ電子工作-基本部品セット-スターターパック-電子部品関連/dp/B01AUN1JYW?ie=UTF8&psc=1&redirect=true&ref_=oh_aui_detailpage_o00_s00
今回やりたいこと
さて、これを買って何をしたかったのかというと、 温度計 です。普通に買えって?ごもっともです・・・。まぁでもやりたかったからいいんです。それと、過去一ヶ月くらいとかそういった期間でグラフとか出せるようにしたいので、水銀温度計とかじゃこれはできません(たぶん)。
↑で買ったスターターパックには、LCDも含まれているため、現在の温度をこれに出す、とかも頑張れば出来そうです。頑張れば。
RasPiのセットアップ〜最初のLチカ
まずはセットアップですが、これは公式の https://www.raspberrypi.org/documentation/installation/noobs.md を使いました。リンク先のページの方法そのまんまです。この辺、フォームファクターが決まりきっている場合、色々と楽ですね。自作PCでドライバとかに悩んでうーんとかならないのが非常に素晴らしい。
Avahiと関係ないハマりポイント
さて、私は当然ながらRasPi以外にもPC持ってますので、そっちをメインに使いたい。つまりはSSH接続したいです。しかし、再起動とかするたびにIPが変わると禿げそうになるので、知識の範囲&調べたところ
− DHCP固定割当にする(ルーター側)
− Avahiを使う
のどちらかだろう、ということになりました。最初は楽だからDHCP固定割当にしよーかなーとか思ってましたが、私の使っているルーターだとDHCPのリース更新がどうも怪しく、割り当てたいアドレスに割当たらなくてイラッと来たので、下記のサイトを参考にAvahiを使うことにしました。
https://wiki.archlinux.org/index.php/avahi
http://d.hatena.ne.jp/pasela/20131023/mdns
こういうとき、ミニマリストなArch Linuxのドキュメントは、必要なことしか書いてないので非常に役立ちます。が、これをやっても設定ができない・・・。で、私はそもそもGentoo Linuxなので
こっちを見ました。そしたらAvahiは入ってたけど、nss-mdnsが入っていなくてMulticast DNSが解決できなかったというシンプルなオチでした。
NOOBS v1.9.0 からインストールできるRaspbianには、すでにAvahiのインストールとかはされているので、
$ ssh pi@raspberrypi.local
とかでログインできればOKです。
公開鍵認証アクセスに変更
さすがにデフォルトのパスワードのままで行くってことはないので、公開鍵認証でやります。秘密鍵については、Githubやらなんやらですでに持っているのでそれを使います。
$ ssh-copy-id ~i ~/.ssh/id_rsa.pub pi@raspberrypi.local
今回初めて使いましたが、これ便利ですね・・・。まぁそもそもSSHが一回はつながらないとダメなんですが、RasPiのような、最初はLANの中でだけしかいないようなものに対してはうってつけです。
後は、RasPiの方の /etc/ssh/sshd_config を編集して、パスワード認証を出来ないように、公開鍵認証だけ有効にしたらOKです
Node.jsのセットアップ
ちょっと前から、 http://nodered.org/ というのが標準でバンドルされるようになったそうです。Node RED自体は、IBMのBluemix上で動作していたものをオープンソース化したもの?なんでしょうかね。IBMの方がAuthorに入ってるので。
ただし、Raspbianに入っているNodeのバージョンはなんと 0.10 系列。さすがにそれはないわーと、現状の最新LTSを落としてきて導入しました。
$ wget https://nodejs.org/dist/v4.4.3/node-v4.4.3-linux-armv7l.tar.xz
$ tar xf node-v4.4.3-linux-armv7l.tar.xz
$ cd node-v4.4.3-linux-armv7l
$ sudo cp -R * /usr/local/
$ node --version
v4.4.3
これでOKです。後はNode-REDをインストールします。RasPiの場合、CPUがよくなったとはいえ、やっぱりそれなりの時間がかかります。
$ sudo npm install -g node-red
はじめてのLチカ
電子工作のHello,Worldと言われるLチカにチャレンジ・・・ですが、ここでちょっと落ち着きます。簡単にやる分には、Node-REDを使ってささーっとやってしまうのが一番手っ取り早いし、JavaScriptを書けばそれで済みます。
しかし、私は趣味の時間にまでJavaScriptを書きたくないんです・・・(もはや宗教)。しかし、継続的に実行する、とか繰り返し実行する、とかのフローを書くということについては、Node-REDを使う方がそもそも視覚的にわかりやすいし、管理もしやすいでしょう。
ということで、動作させるフローについては、Node-REDで管理して、実際に動かすコマンド自体は、せっかくなので前から興味があったRustで書くことにしました。幸いにもRustにもGPIOをゴニョゴニョしたりするライブラリなどは一通り揃ってるっぽいので、チャレンジがてらやってみます。
・・・さて、ここまで来たところでふと気づきました。 どこで実装する? と。RasPiはお世辞にも高性能とは言えず(当たり前なんですけど)、またSDカードしかストレージがない(し、追加する気もない)ことで、ファイルが出来たり消えたりを繰り返す開発作業は色々と難がある・・・。
そうなると、Rustがクロスコンパイル可能であるということをフルに活かして、ホスト側でRustを書いて、それをRasPi側に持って行って動かす、ということをやることにしました。どうしてそうなった。
実際にこの作業をSCPとかでやると、100%確実に禿げ上がるので、sshが使えるってことを前提にして、sshfsで実行ファイル置き場を繋いで、そこに置く、ということにしました。これならcpだけで心が折れづらいはず。
$ sudo emerge sshfs
$ sshfs raspi:/home/pi <dir>
.ssh/configに記載して、 ssh raspi
で接続できるようにしてあるので、これでOKです。これやる前に色々掃除してたら、カーネルのソースが最新版以外無くなってて、泣く泣くカーネルを最新バージョンに更新したっていうのは秘密です。
rustについては、とりあえずGentooのOverlay経由で rustc
と cargo
をインストールしましたが、クロスコンパイルの環境を整えるのが大分つらそうだったので、ちょうどいいところに以下のパッケージがあったため、Docker経由でビルドすることにしました。
GPIOの制御は、ものすごいそのまんま使えるものがありましたんで、これを使います。
さて、初Rustはこんな感じになりました。ぶっちゃけ try!
が何なのかよくわからず使ってますが・・・。
extern crate sysfs_gpio;
use sysfs_gpio::{Direction, Pin};
use std::thread::sleep;
use std::time::Duration;
fn main() {
let my_led = Pin::new(2);
my_led.with_exported(|| {
try!(my_led.set_direction(Direction::Out));
let mut counter = 4;
while counter > 0 {
try!(my_led.set_value(0));
sleep(Duration::from_millis(200));
try!(my_led.set_value(1));
sleep(Duration::from_millis(200));
counter = counter - 1;
}
try!(my_led.set_value(0));
Ok(())
});
}
200msの感覚で4回チカチカしてとまります。やばいなにこれ簡単・・・。このくらいの規模だと、Rustのよさとかよくわかりませんが・・・。
温度センサーを利用する
さて、いよいよ本番となる温度センサーですが、今回注文したセンサーの中には、DS18B20というセンサーが入っているのでこれを使います・・・。
が、ここで罠が。色々なサイトを参考にしてみたところ、どうも接続がうまく行かないです(センサーが超熱くなる)。
なんでかなーと思って調べてみたら
どうも納品されているモジュールがこれだったようで、内部で配線が切り替わっていて、電源とかGNDの線が変わっていること、抵抗とかがすでに入っているからプルアップ抵抗とかもいらない、ということがすでに先人が書かれていました。
丁度同じ(多分)モジュールを使っていた方。マジ助かりました。。。
http://wordpress.ideacompo.com/?p=5431
ちゃんと接続できて取り方さえわかれば怖いものはありません。1-Wire接続形式に対するRustのモジュールはどうも無いようでしたので、とりあえずファイルを開いて云々、ということをすることにします。
ところで、大抵はこのセンサーに対するデバイスファイルを直接開いている方ばかりでしたが、 /sys/devices/w1_bus_master1/w1_master_slaves
というファイルに、センサーのデバイスIDが記載されてるっぽいので、これを読み取って云々することにしました。
extern crate chrono;
use std::i32;
use chrono::*;
use std::io;
use std::process::exit;
use std::io::prelude::*;
use std::fs::File;
use std::borrow::Cow;
use std::ops::Index;
fn load_slave_devices<'a>() -> Result<Cow<'a, Vec<String>>, io::Error> {
let mut f : File = try!(File::open("/sys/devices/w1_bus_master1/w1_master_slaves"));
let mut s = String::new();
try!(f.read_to_string(&mut s));
fn is_not_empty(s: &&str) -> bool { (*s).ne("")}
let lines = s.lines().filter(is_not_empty).collect::<Vec<&str>>();
let mut ret = Vec::with_capacity(lines.len());
for s in lines {
ret.push(s.to_owned());
}
return Ok(Cow::Owned(ret));
}
fn read_temperature<'a>(val : Cow<'a, Vec<String>>) -> Result<(), io::Error>{
if val.len() < 1 {
writeln!(std::io::stderr(), "No any devices founded");
}
let val: Vec<String> = val.into_owned();
let sensor = val.index(0);
let sensor_device = format!("/sys/devices/w1_bus_master1/{}/w1_slave", sensor);
let mut f : File = try!(File::open(sensor_device));
let mut s = String::new();
try!(f.read_to_string(&mut s));
let lines = s.lines().collect::<Vec<&str>>();
let temperature = parse_temperature(lines.index(1));
if let Some(temp) = temperature {
let now: DateTime<Local> = Local::now();
writeln!(std::io::stdout(), "{:?},{:.3}", now, temp);
};
return Ok(());
}
fn parse_temperature(s: &str) -> Option<f64> {
let prefix = "t=";
s.find(prefix).map(|index| {
let (_, temps) = s.split_at(index);
let (_, temps) = temps.split_at(prefix.len());
match i32::from_str_radix(temps, 10) {
Ok(v) => Some((v / 1000) as f64),
Err(_) => None
}
}).map(|v| v.unwrap())
}
fn main() {
let f = load_slave_devices();
match f {
Ok(_) => (),
Err(e) => {
writeln!(std::io::stderr(), "Has error : {}", e); exit(1)
}
};
match f.map(read_temperature) {
Ok(_) => (),
Err(e) => {
writeln!(std::io::stderr(), "Has error : {}", e); exit(1)
}
}
}
これだけのことをやるのにめちゃめちゃ長い・・・。長い理由的には、LifecycleとかReferenceとかMutabilityとか、そういったRustの概念にめっちゃ苦戦したためです。
手軽にやりたいんならGolangの方がよほど手軽かと思います。ここまで厳密じゃないし、何よりエディタサポートについてはGolangの方が圧倒的に良いです。
とりあえずこれで、実行すると温度とその時刻を次の様な形で取得することが出来るようになりました。
2016-04-30T19:07:31.596256045+09:00,25.000
前が日付+時刻、後ろが温度です。温度自体は小数点以下3桁までは取得できる(っぽい)ので、そこまで取得するようにしてます。
Node-REDでフローづくり
Node-REDについては、調べると非常に色々見つかるのでそっちを見てもらうとして、今回はこのサイトを参考にさせていただきました。
どっちかというと、Node-REDをブート時に実行させる方法とかを参考にしました。
そして、これで作った結果がこちら。
なんかLost Connectionとか出てますが気にしないでください。スクリーンショット取ろうとしたらミスってSSH切ってしまったので・・・。
Node-REDもこれが初めての利用でしたが、簡単なものであればもうこれでいいんじゃね?って確かに思えちゃいます。
このフローでJavaScriptを書いたのは、日付をファイル名に動的に設定するって箇所だけで、それ以外は全て標準のNode+コマンド実行だけで終わってます。
さて、これで日付毎の気温が、1分ごとに測れるようになりました。
・・・しかし、RasPiには一つ決定的な弱点があります。ストレージがSDカードっていうことです。こういったログ類は頻繁に書き込みが行ってしまうので、正直あまりストレージに直接書き込ませたくないです。
そこで、このログデータ、AWSに送っちゃいましょう。なんでAWSかというと仕事で使ってるから知ってるってのが一番・・・Azureとかでも良かったんですが。
このデータ自体は、別段個人情報でも何でも無いので、ついでにS3の静的ウェブサイトとかにして、部屋の外でも見られるようにする、ってのもありです。他の人に見られたところで痛くないし。
というわけでその辺りの仕組みも作ります。これもついでにNode-REDでやってしまいましょう。
S3へ温度データのアップロード
S3を利用するプログラムをRustを使って書くのは、もはや苦行に近いので、普通にAWS CLIを使いましょう。
$ sudo pip install awscli
PIP一発で入るので非常に楽です。次はS3でのバケット作成〜パーミッション設定ですが・・・全部割愛で。IAMユーザー作ってインラインポリシー割り当てるってだけです。
さて、後はセンサーから取得したデータを定期的にS3にsyncします。
$ aws s3 sync /dev/shm/ s3://<bucket-name>/
/dev/shmは、Raspiにおいてはデフォルトでtmpfsになっており、メモリしか使わないのでSDカードに優しいため、センサーデータはここに出力するようにしています。ただし、溜まり過ぎるとメモリが死ぬので、別途別フローで一定時間を超えたファイルは削除するような処理を入れてあります。
実際には、毎回S3にSyncするってのはそれはそれで色々と問題な気がしなくもないので、AWS IoTとか経由で置くほうがいいのかも知れません。サイズ的に変わんないけど。
S3に静的サイトを置いてグラフ化する
こっからは嫌でもJavaScriptを書かないと始まりません。なので仕方ないですが書きます。要はグラフで見られれば何でもいいので、下手に凝らずにシンプルに・・・と行きたいところですが、最近はもうHTMLに直接書くとか耐えられないので、Browserifyの力を借ります。
- Highcharts
- 仕事で使ってるのと、商用利用じゃなければ無料なので遠慮無く。
- ClojureScript + Om
- 使ってみたかった。
- fetch polyfill
- これまた仕事で試しに使ってみてるので。jQuery.ajaxのほうが使いやすい(デフォルトが決まってる的な意味で)ですがとりあえず。
AltJS的なものもついでに触ろうかと思い、Elmも検討しましたが・・・Elmは独自世界過ぎて、Highchartsを使えるようにするまでが大分つらそう(とりあえずやりたいのでお手軽さ大事)だったので、なんとかなりそうなClojureScriptにしてみた次第。
とりあえず環境揃えるのが先ですが、JavaとかはScalaとかもやる都合上問題なく入っているので、 Leiningen のインストールと、Emacsの設定を行います。
次に、 ClojureScriptをビルドするための諸々を楽にしてくれるプラグインである、 https://github.com/emezeske/lein-cljsbuild を追加して、以下のライブラリをついでに追加します。
- Om
- cljs-ajax
jQueryのラッパーもあるようでしたが、そもそもOm使うので、じゃあ最小限の機能ということで、Ajaxライブラリとしては、cljs-ajax になりました。
(GET "/url" {:handler ...})
みたいに書くことができます。It's simple.
さて、実際にどんな画面になったかというと。
白いな・・・。CSSはInkというCSSフレームワークを使いました。一応バレると嫌なので、ロケーションバーのURLは適当にしてあります。
さて、肝心のClojureScriptのソースですが、想定以上にでかくなったため載せません。ただ、一箇所ちょっとええなーと思った部分は
(defn chart-change-view [data owner]
(let [handler (fn [c date day]
(fn [_]
(go (loop []
(>! c (tm/plus date (tm/days day)))))))]
(reify
om/IInitState
(init-state [_]
{:change-chart (:change-chart data) :date (:date data)})
om/IRender
(render [_]
(dom/div #js {:className "button-group"}
[(dom/button #js {:className "ink-button"
:onClick (handler (om/get-state owner :change-chart)
(om/get-state owner :date)
-1)}
"前日")
(dom/button #js {:className "ink-button"
:onClick (handler (om/get-state owner :change-chart)
(tm/now)
0)}
"今日")
(dom/button #js {:className "ink-button"
:onClick (handler (om/get-state owner :change-chart)
(om/get-state owner :date)
1)}
"翌日")])))))
onClickでクリックされたら翌日とか前日とかのデータを取得する、というありがちな処理ですが、Reactだけとかだと、どこでそれをやるか・・・とか若干迷いがちですが、core.asyncというパッケージを利用することで、channelに対して値を受け取って、他の場所で単純にそれを待ち受けて〜ってやることができるので、自然と処理とデータの更新が分離できていい感じです。
まとめと今後の展望
本来やろうとしていたことまで考えると、若干時間切れになってしまいましたが、とりあえず目標は達成できました。
一番時間がかかったのは、電子工作自身というよりは、周辺の環境作成とかClojure調べるのとかRust調べるとかしてた時間のほうがかかってたという笑えない事実が・・・。GWでやろうと決めてて正解でした。
これ以降は、もうちょっと改善しないとならない点として、以下の点があるかなと。
- 今の温度 が見られないので、LCDかなんかで見られるように
- LCD自体はあるので出来そうですが、LCD自体は角度があると見られないので、もうちょっとアナログにLEDの色を変えて〜とかでもいいかも知れません。
- RasPiを再起動したら、途中までのデータが全部飛ぶ
- 飛んだ場合、その時間帯のデータが全部吹っ飛ぶので、今のシンプルな表示の仕組みだと中々めんどくさい感じになります。
- ただ、S3から持ってくる、というのも色々とめんどくさいのと、まぁ自分の部屋の気温なんざ欠落してもどうでもいいデータなので、ここは問題ないってことにします。Fluentdとかで送るってなると、今度はデータのまとめかたを考えないとならないんで・・・。
最後に、 RasPi楽しいです 。あ、後ClojureScriptも楽しいです。電子工作とか学生以来ですが、手軽さも相まって非常に手が出しやすくなってます。時間があるときにやってみちゃどうでしょうか。