Socket.IOでセンサー&MongoDB〜AngularJSアプリ間の通信を行うArduinoで始めるWeb技術者のためのIoT入門(6)(2/3 ページ)

» 2014年11月19日 15時00分 公開
[岩永義弘株式会社インターネットイニシアティブ]

サーバー側の実装

 全体像がつかめたところで、コードの解説に入っていきます。まずは依存モジュールから説明していきます。ジェネレーターが自動的に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);
server/app.js(抜粋)

 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	};
server/arduino.js(抜粋)

 引数に渡された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	}
server/arduino.js(抜粋)

 「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);
server/api/thermoHygroHistory.model.js(抜粋)

 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	}
server/api/thermoHygroHistory.socket.js(抜粋)

 これを実現しているのが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	  });
server/config/socketio.js(抜粋)

 新しい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.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。