全体像がつかめたところで、コードの解説に入っていきます。まずは依存モジュールから説明していきます。ジェネレーターが自動的にpackage.jsonを生成しますが、今回のアプリケーションでは次の三つを追加して利用します。
連載第2回の「電子回路を作る基礎を学びNode.jsでボードを操ろう」からずっとお世話になっているモジュールです。Arduinoの制御に使います。
体感温度を計算するモジュールです。
mongooseのプラグインモジュールです。モデル定義にcreatedAt、updatedAtを追加し、適切な値を自動的に挿入できます。
サーバー側の依存モジュールは「npm install」コマンドを実行することで自動的にインストールされます。
それでは、コードの解説に進みます。前回のシーケンス図を見ながら読み進めると、理解しやすいと思いますので、再掲しておきます。
「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” |
先ほど、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年間分のデータを保持するように値を指定しています。
体感温度をクライアントへ送信する処理は、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して、保存した体感温度データを送信しています。
先ほど、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.