Socket.IOでセンサー&MongoDB〜AngularJSアプリ間の通信を行う:Arduinoで始めるWeb技術者のためのIoT入門(6)(2/3 ページ)
家電〜Webアプリ間の双方向通信をSocket.IOで行うアプリについて、サーバー側のArduino連携やMongoDBへのデータ保存などと、クライアント側のAngularJSに分けて動作を解説します。
サーバー側の実装
全体像がつかめたところで、コードの解説に入っていきます。まずは依存モジュールから説明していきます。ジェネレーターが自動的にpackage.jsonを生成しますが、今回のアプリケーションでは次の三つを追加して利用します。
- "johnny-five": "~0.8.11"
連載第2回の「電子回路を作る基礎を学びNode.jsでボードを操ろう」からずっとお世話になっているモジュールです。Arduinoの制御に使います。
- "heat-index": "~0.0.3"
体感温度を計算するモジュールです。
- "mongoose-timestamp": "~0.3.0"
mongooseのプラグインモジュールです。モデル定義にcreatedAt、updatedAtを追加し、適切な値を自動的に挿入できます。
サーバー側の依存モジュールは「npm install」コマンドを実行することで自動的にインストールされます。
それでは、コードの解説に進みます。前回のシーケンス図を見ながら読み進めると、理解しやすいと思いますので、再掲しておきます。
Arduinoとの連携処理
「grunt serve」または「grunt serve:dist」コマンドでサーバーを起動したとき、最初に評価されるのがserver/app.jsです。
21 var app = express();
22 var server = require('http').createServer(app);
23 var arduino = new (require('johnny-five').Board)();
24 var socketio = require('socket.io')(server, {
25 serveClient: (config.env === 'production' || config.env === 'development'),
26 path: '/socket.io-client'
27 });
28
29 require('./arduino')(arduino);
30 require('./config/socketio')(socketio, arduino);
31 require('./config/express')(app);
32 require('./routes')(app);
23行目は、本連載では毎度おなじみの処理です。ボードのインスタンスを変数「arduino」に格納しています。これ以降は、モジュールをrequireする際にこの変数を渡すことで、そのモジュールでもArduinoのメソッドやイベントを扱えるようになります。ファイルを細かく分割するためのテクニックですね。
注目すべきは29行目で、早速このテクニックを使っています。「require(‘./arduino’)」は無名関数を返しますが、その引数にarduinoボードのインスタンスを渡して実行しています。このとき何が起こっているのかをたどるため、requireしているファイルを見てみましょう。
55 module.exports = function (arduino) {
56 arduino.on('ready', function () {
57 arduino.on('string', function (data) {
58 onString(arduino, data);
59 });
60
61 setInterval(requestTemperatureHumidity, conf.arduino.interval, arduino);
62 });
63 };
引数に渡されたArduinoインスタンスが至る所で使われていますね。このモジュールが依存するArduinoインスタンスを引数で受け取るスタイルであると解釈できます。
56行目の「ready」イベントは、Arduinoに制御命令を送信できるようになったときにemitするものでした。
まずは、SysExを使って温度湿度センサーの読み取り命令をArduinoへ送信する部分を解説します。起点となっているのが、61行目です。requestTemperatureHumidityという関数を定期的に実行しています。この関数は同じファイルのすぐ上側で定義しており、52行目が一番重要な箇所です。
45 // send Temperature and Humidity request to Arduino
46 function requestTemperatureHumidity(arduino) {
47 if (! arduino.io) {
48 console.error('not connected to arduino.');
49 return;
50 }
51
52 arduino.io.sendString(COMMAND.TH.GET);
53 console.log('to arduino: sent TH request');
54 }
「SysEx STRING_DATA」コマンドを送信するために「arduino.io.sendString」というメソッドを使います。データ部に格納する文字列は引数で指定します。
次は、Arduinoから「SysEx STRING_DATA」を受信したときの処理を説明します。STRING_DATAを受信すると、Arduinoインスタンスで「string」イベントが発生します。そのイベントハンドラーを登録しているのが58〜60行目です。データ部の文字列はハンドラーの引数に渡されます。
57 arduino.on('ready', function () {
58 arduino.on('string', function (data) {
59 onString(arduino, data);
60 });
そのままonString関数にデータを渡しています。この関数の本質部分は21〜26行目です。
13 // SysEx STRING_DATA handler
14 function onString(arduino, data) {
15 var response = parseStringMessage(data);
16 if (response.type === 'XX') {
17 console.log(data);
18 return;
19 }
20
21 if (response.type === 'TH') {
22 var th = new ThermoHygroHistory(response);
23 th.save();
24 } else if (response.type === 'AC') {
25 arduino.emit('AC:toggled', response);
26 }
27 }
温度湿度データを受け取ったとき、MongoDBへ保存します。保存後の処理は後述の「クライアントへのpush通知」で解説します。
エアコンの電源切り替え操作完了の通知を受け取ったときは、Arduinoインスタンスで ”AC:toggled” イベントをemitしています。こうすることで、Arduinoのイベントを他のモジュールでも拾うことができるようになりました。
参考までに、STRING_DATAで受け取る文字列データは、前回解説したCustomFirmataで定義しており、下記の4パターンになります。
| 条件 | 文字列 |
|---|---|
| エアコン電源操作が完了 | “AC:togged” |
| 温度と湿度取得に成功 | “TH:27,65” |
| 温度と湿度取得に失敗 | “TH:er,er” |
| 無効な命令を受信 | “XX:unknown” |
ModelとCollectionの定義
先ほど、MongoDBに保存する部分まで到達しました。モデルの定義がどうなっているのかを見てみましょう。
1 'use strict';
2
3 var mongoose = require('mongoose'),
4 timestamp = require('mongoose-timestamp'),
5 Schema = mongoose.Schema;
6
7 var ThermoHygroHistorySchema = new Schema({
8 temperature: Number,
9 humidity: Number,
10 heatIndex: Number
11 }, {
12 capped: {
13 size: 1073741824,
14 max: 1051200,
15 autoIndexId: true
16 }
17 });
18 ThermoHygroHistorySchema.plugin(timestamp);
19 ThermoHygroHistorySchema.index({createdAt: -1});
20
21 module.exports = mongoose.model('ThermoHygroHistory', ThermoHygroHistorySchema);
8〜10行目の通り、センサーから取得した温度と湿度、計算した体感温度を保存します。18行目でtimestampプラグインを利用しており、createdAtとupdatedAtが自動的に挿入されます。このため、作成/変更時刻を管理する処理を自前で書く必要がありません。
19行目でインデックスを指定しています。今回想定しているreadパターンは、最新の測定データを1件取得するだけで、具体的には次のようなクエリになります。
Thermohygrohistory.findOne({}, null, { sort:{ createdAt: -1 } }, callback);
クエリした際にフルスキャンにならないよう、createdAtに降順のインデックスを作成しています。
12〜16行目でMongoDBの「Capped Collection」を有効化しています。Capped Collectionとは、Collectionのデータサイズまたは件数が指定値を超えた際に古いdocumentを自動的に削除してくれる機能です。これにより、ディスクあふれを心配する必要がなくなります。
実は、MongoDBのオペレーション履歴「oplog」も、このCapped Collectionになっており、ログを格納するときに便利です。今回は、2年間分のデータを保持するように値を指定しています。
クライアントへのpush通知
体感温度をクライアントへ送信する処理は、MongoDBへの保存が成功したときのイベントにフックさせます。
5 'use strict';
6
7 var ThermoHygroHistory = require('./thermoHygroHistory.model');
8
9 exports.register = function(socket) {
10 ThermoHygroHistory.schema.post('save', function (doc) {
11 onSave(socket, doc);
12 });
13 };
14
15 function onSave(socket, doc, cb) {
16 socket.emit('ThermoHygroHistory:save', doc.toObject());
17 }
これを実現しているのが10〜12行目です。この「save」イベントは、保存に成功したときに発生します。具体的には、MongoDBのJavaScriptドライバー「mongodb-native」の「save」イベントがそのまま利用されています。ここでSocket.IOでイベントをemitして、保存した体感温度データを送信しています。
Socket.IO接続時の処理
先ほど、MongoDBへの保存が成功したときの処理を説明しましたが、このファイルはどこでrequireされているのでしょうか。それは、thermoHygroHistory.controller.jsではなく、実はserver/config/socketio.jsです。
41 socketio.on('connection', function (socket) {
.. ...
54 // Call onConnect.
55 onConnect(socket, arduino);
56 console.info('[%s] CONNECTED', socket.address);
57 });
新しいSocket.IOクライントが接続すると、「connection」イベントが発生します。そのハンドラーの55行目でSocket.IOイベントへのひも付け処理を呼んでいます。実際にひも付けを行っている箇所は、onConnect関数の21〜22行目です。
14 function onConnect(socket, arduino) {
15 // When the client emits 'info', this listens and executes
16 socket.on('info', function (data) {
17 console.info('[%s] %s', socket.address, JSON.stringify(data, null, 2));
18 });
19
20 // Insert sockets below
21 require('../api/airConditioner/airConditioner.socket').register(socket, arduino);
22 require('../api/thermoHygroHistory/thermoHygroHistory.socket').register(socket);
23 }
ここでrequireしているファイルは、先ほどMongoDBの「post save」フックを登録した部分です。新たに接続してきたクライアントのsocketを渡して、体感温度データの送信先を追加登録しています。この処理は全ての新規コネクションに対して行われるので、体感温度データは全てのクライアントに送信されることになります。
エアコン制御のコードは、体感温度のコードとほとんど同じ構成ですので、説明を割愛します。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
忘れずに手を洗おう……ビッグデータとIoTの活用例
張り紙に頼る代わりに、センサーとInternet of Things(IoT)、ビッグデータといった技術を活用して手洗い忘れ防止を実現したケースを紹介する。
確定不能なデータたち――IoT時代を受け止めるOracle NoSQL、JSON Anywhere
エンタープライズを指向するOracle NoSQL、マルチデバイスでのデータハンドリングのための環境を整備するCouchbase、適用用途は異なるものの、IoT時代を見据えた開発を進める2つの非RDBをウォッチした。
冷蔵庫は「無実」――シマンテックがスパム発信元を調査
「インターネットにつながったスマート家電から75万通以上のスパムメールが発信された」という報道に関して、シマンテックは2014年1月27日、実際に感染していたのは同じホームネットワーク内のWindows搭載PCであり、冷蔵庫は「無実だ」とするブログを公開した。
「俺たちのJavaは、まだまだこれからだ」未来の鍵はInternet of Thingsにあり?〜JavaOne 2013まとめレポート(前編)
Java開発者の年次カンファレンス、JavaOne 2013がサンフランシスコで9月22〜26日に開催された。3つの基調講演、そして400を超えるセッションが行われるなど、圧巻のボリュームは、まさに「Java開発者の祭典」といえる。基調講演と主なセッション、全体の雰囲気を前中後編に分けてレポートする。
IoTをクラウドで管理、「Microsoft Azure Intelligent Systems Service」プレビュー公開
米マイクロソフトは2014年4月15日、デバイスやセンサーといった「モノのインターネット」(IoT)からのデータを収集し、ビジネスインテリジェンスツールと結び付けて分析する「Microsoft Azure Intelligent Systems Service」の限定パブリックプレビューを公開した。
