Arduino I/O をスマートフォン(iPod touch)から操作する、WebSocket(socket.io + node.js) を使用して I/O 変化時にブラウザ画面を自動更新(その3 完成)

前回の記事に引き続き、Arduino I/O を操作する Web アプリに、画面の自動更新機能を追加するための説明をします。今回の記事で一連の作業は完了します。

最初に検討した機能追加項目の中の下記の最後の項目について作業を行います。

(3) Web アプリを実行しているブラウザが、最新の I/O データを受信したときにWeb アプリ上の GUI(チェックボックス) の状態を自動更新させる

最初に、Arduino 側で I/O が更新された時に送信される Firmata パケット DIGITAL_PORTS_MESSAGE をDeviceServer で受信したときのイベント処理を作成します。 Arduino を接続しているシリアルポートのデバイス定義で、デバイスタイプが “FIRMATA” タイプに設定されていたときには、シリアルポートから FIRMATA パケットを受信したときにSERIAL_FIRMATA.lua イベントハンドラが実行されます。ここでは、そのファイル中に DIGITAL_PORTS_MESSAGE パケットの処理を追加記述します。

Arduino I/O と サーバーPC のシリアル接続についてはこの記事を参照して下さい。

SERIAL_FIRMATA.lua ファイルの内容は下記になります。今回のWeb アプリで使用する全てのファイルを含めてここからダウンロードすることができます。

 

file_id = "SERIAL_FIRMATA"

--[[

******************************************************************************
* イベントハンドラスクリプト実行時間について                                 *
******************************************************************************

一つのスクリプトの実行は長くても数秒以内で必ず終了するようにしてください。
処理に時間がかかると、イベント処理の終了を待つサーバー側でタイムアウトが発生します。

また、同時実行可能なスクリプトの数に制限があるため、他のスクリプトの実行開始が
待たされる原因にもなります。

頻繁には発生しないイベントで、処理時間がかかるスクリプトを実行したい場合は
スクリプトを別に作成して、このイベントハンドラ中から script_fork_exec() を使用して
別スレッドで実行することを検討してください。

******************************************************************************

SERIAL_RAW スクリプト起動時に渡される追加パラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
COMPort			イベントを送信したシリアルデバイスの COMポート名		"COM3"

FIRMATA_DATA	COM ポートから読み込んだ FIRMATA プロトコルパケットのバイナリデータ。
				(16進数、可変長)										"0102030A0B"
---------------------------------------------------------------------------------

]]

-------------------------------------
-- FIRMATA イベントデータをログに出力
-------------------------------------
local str = ""
for key,val in pairs(g_params) do
	str = str .. key .. " = " .. val .. " "
end
log_msg(str,file_id);

---------------------------------------------------------------
-- SYSEX コマンド "DIGITAL_PORTS_MESSAGE" を受信した場合の処理
---------------------------------------------------------------
if (string.sub(g_params["FIRMATA_DATA"],1,4) == "F064") then 

	local msg = "{"
	local data = hex72_to_tbl(string.sub(g_params["FIRMATA_DATA"],5,-3),2)
	if data ~= nil then
		for key,val in ipairs(data) do
			if key > 1 then msg = msg .. ',' end
 			msg = msg .. '"PORT_' .. tostring(key - 1) .. '":"' .. bit_tohex(val,2) .. '"'
		end
	end
	msg = msg .. "}"

	log_msg("uplink message = " .. msg,file_id);
	local nstat,rdata = tcp_send_recv_data("localhost",9080,msg,2,2)
	if not nstat then error() end
end

DeviceServer インストール直後は、受信したFirmata イベントデータをログに出力するだけの記述が行われています(ファイルの先頭で定義されている部分です)。これに、この記事で追加してArduino から送信される DIGITAL_PORTS_MESSAGE  パケットの処理を追加します。Arduino I/O から送信されるイベントデータが F064xxxxx (2桁毎の16進数を連結した文字列で表現されています)で始まった SYSEX パケットを受信した場合に DIGITAL_PORTS_MESSAGE であると判断しています。該当したイベントデータを受信した場合には、Firmataプロトコルで定義された7bit のエンコード形式で表現された I/O ポートデータをデコードして、各ポート毎に 8 ビットデータとして取り出します。その後、I/O データを下記の JSON 文字列にフォーマットしています。

フォーマット後の Arduino I/O ポート値を示す文字列データ(Arduino に搭載されているCPU が ATmega328 等の場合にはPORT_0 と PORT_1 の2つになります)

{“PORT_0″:”00″,”PORT_1″:”00″}

次にイベントハンドラでは、上記の文字列データを、この記事で作成した配信用サーバー(node.js + socket.io) にTCPソケット通信で送信します。これによって、Arduino から 取得した最新の I/O データを、Web アプリの動作しているブラウザに配信することになります。

次に、Web アプリケーション(HTML, JavaScript) を作成します。以前の記事で作成した Arduino I/O を操作する Web アプリとほぼ同じですが、配信用サーバーに接続するためのsocket.io(WebSocket) クライアント機能と、配信用サーバーから受信した I/O データを処理して GUI のチェックボックスを更新する機能が追加されています。また、Webアプリの GUI が定義されている HTML ファイルから、不要になった “Reload” ボタンとそのイベントハンドラを削除しています。

最初に、Web アプリのメイン画面が定義されている index.html ファイルの内容を示します。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Arduino I/O コントロール</title>
        <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
        <link rel="stylesheet" href="my.css" />
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
        <script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
		<script src="socket.io.min.js"></script>
    </head>
    <body>
        <div data-role="page" id="page1">
            <div data-theme="a" data-role="header">
                <h3>Arduino I/O v2</h3>
            </div>
            <div data-role="content" data-theme="a">
                <div class="ui-grid-a">
                    <div class="ui-block-a" data-theme="a">
                        <div data-role="fieldcontain">
                            <fieldset data-role="controlgroup" data-type="vertical">
                                <legend>
                                    PORT 0
                                </legend>
                                <input name="checkbox1" id="checkbox1" type="checkbox" class="pin" pin="2"/>
                                <label for="checkbox1">
                                    DIO#2
                                </label>
                                <input name="checkbox2" id="checkbox2" type="checkbox" class="pin" pin="3"/>
                                <label for="checkbox2">
                                    DIO#3
                                </label>
                                <input name="checkbox3" id="checkbox3" type="checkbox" class="pin" pin="4"/>
                                <label for="checkbox3">
                                    DIO#4
                                </label>
                                <input name="checkbox4" id="checkbox4" type="checkbox" class="pin" pin="5"/>
                                <label for="checkbox4">
                                    DIO#5
                                </label>
                                <input name="checkbox5" id="checkbox5" type="checkbox" class="pin" pin="6"/>
                                <label for="checkbox5">
                                    DIO#6
                                </label>
                                <input name="checkbox6" id="checkbox6" type="checkbox" class="pin" pin="7"/>
                                <label for="checkbox6">
                                    DIO#7
                                </label>
                            </fieldset>
                        </div>
                        <a data-role="button" class="port" port="0">
                            CLEAR
                        </a>
                    </div>
                    <div class="ui-block-b" data-theme="a">
                        <div data-role="fieldcontain">
                            <fieldset data-role="controlgroup" data-type="vertical">
                                <legend>
                                    PORT 1
                                </legend>
                                <input name="checkbox7" id="checkbox7" type="checkbox" class="pin" pin="8"/>
                                <label for="checkbox7">
                                    DIO#8
                                </label>
                                <input name="checkbox8" id="checkbox8" type="checkbox" class="pin" pin="9"/>
                                <label for="checkbox8">
                                    DIO#9
                                </label>
                                <input name="checkbox9" id="checkbox9" type="checkbox" class="pin" pin="10"/>
                                <label for="checkbox9">
                                    DIO#10
                                </label>
                                <input name="checkbox10" id="checkbox10" type="checkbox" class="pin" pin="11"/>
                                <label for="checkbox10">
                                    DIO#11
                                </label>
                                <input name="checkbox11" id="checkbox11" type="checkbox" class="pin" pin="12"/>
                                <label for="checkbox11">
                                    DIO#12
                                </label>
                                <input name="checkbox12" id="checkbox12" type="checkbox" class="pin" pin="13"/>
                                <label for="checkbox12">
                                    DIO#13
                                </label>
                            </fieldset>
                        </div>
                        <a data-role="button" class="port" port="1">
                            CLEAR
                        </a>
                    </div>
                </div>
            </div>
        </div>

		<div data-role="page" id="error_back_dialog">
			<div data-role="header" data-theme="e">
				<h3>script error</h3>
			</div>
			<div data-role="content" data-theme="e">
				<h3>エラーが発生しました</h3>
				<p>サーバー処理中にエラーが発生しました。現在のセッションが無効になっている場合があります。サーバー側にログが残されていますので確認して下さい</p>
				<p><a data-role="button" data-inline="true" data-icon="check" data-rel="back" data-theme="e">OK</a></p>
			</div><!-- /content -->
		</div>

		<script src="main.js"></script>

    </body>
</html>

タイトルヘッダからは “Reload” ボタンが取り除かれています。また、socket.io(WebSocket) を使用するために、スクリプトタグで “socket.io.min.js” ファイルの読み込みをファイルの先頭部分で指定しています。このファイルはサーバーPC にnode.js の追加パッケージ socket.io をインストールしたときに下記のディレクトリに格納されています。

“C:\Program Files\nodejs\node_modules\socket.io\node_modules\socket.io-client\dist”

このディレクトリ内にある “socket.io.min.js” ファイルを今回の Web アプリを格納している

“C:\Program Files\AllBlueSystem\WebRoot\arduino_io” フォルダにコピーしておきます。

次に、Web アプリの JavaScript プログラム main.js を修正します。ファイルの内容は以下の様になります。

// 	Webサーバーから、別のPC で実行中の DeviceServerにアクセスする場合の
//	URLホスト名とポート番号部分を設定する
//
//  DeviceServer 自身の HTTP サーバー機能を使用している場合には "" にする
//
//var server_host_url = "http://your_DeviceServer_public_url:80";
var server_host_url = "";

// DeviceServer セッション認証用トークン文字列
var session_token = "";

// DeviceServer の LogService にログメッセージを出力する
function log(msg,module){
	var url = server_host_url + "/command/log?message=";
	url = url + encodeURIComponent(msg);
	if (module != undefined){
		url = url + "&module=" + encodeURIComponent(module);
	}
	$.get(url,"",function(data){},"text");
}

// DeviceServer のスクリプトを実行する。
// 第2パラメータにスクリプトパラメータを指定する
// callback パラメータを省略するとリターンパラメータを受け取らない
function script_exec(name,params,callback){
	if (callback == undefined){
		var callback = "default_callback";
    	var url = 	server_host_url + "/command/json/script" +
					"?session=" + encodeURIComponent(session_token) +
					"&resultrecords=0" +
					"&name=" + encodeURIComponent(name);
	} else {
    	var url = 	server_host_url + "/command/json/script" +
					"?session=" + encodeURIComponent(session_token) +
					"&name=" + encodeURIComponent(name);
	}

	for(key in params){
		url = url + "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
	}

	$.ajax({"url" : url,
			"dataType" : "jsonp",
			"jsonpCallback" : callback
	});
}

// script_exec() でコールバックを指定しない場合のデフォルトコールバック関数
function default_callback(data){
	if (data.Result != "Success"){
		log("*ERROR* script error!");
		return;
	}
}

// スクリプト実行結果ステータスのみをチェック
function script_exec_callback(data){
	if (data.Result != "Success"){
		$.mobile.changePage( "#error_back_dialog", {transition: "pop",role:"dialog"});
	}
}

// スクリプト実行結果ステータスチェックとUI 値リロード
function script_exec_callback_reload(data){
	if (data.Result != "Success"){
		$.mobile.changePage( "#error_back_dialog", {transition: "pop",role:"dialog"});
	}
	load_arduino_io();
}

// UI コンポーネントの xml 属性値を検索取得
function getAttrVal(node,name){
	var val = "";
	var attr =  node.attributes;
	for (var i=0; i<attr.length; i++){
		if (attr[i].nodeName == name){
			val = attr[i].nodeValue;
		}
	}
	return val;
}

// IO ポート値の配列を受け取ってチェックボックスの値に反映させる
// パラメータフォーマット {"PORT_0":"xx","PORT_1":"xx"}
function apply_ui(ports){
	var pval;
	var	flag;

	if (ports.PORT_0 != ""){
		pval = parseInt(ports.PORT_0,16);

		flag = ((pval & (1 << 2)) != 0);
		if ($("#checkbox1").checked != flag) $("#checkbox1").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 3)) != 0);
		if ($("#checkbox2").checked != flag) $("#checkbox2").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 4)) != 0);
		if ($("#checkbox3").checked != flag) $("#checkbox3").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 5)) != 0);
		if ($("#checkbox4").checked != flag) $("#checkbox4").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 6)) != 0);
		if ($("#checkbox5").checked != flag) $("#checkbox5").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 7)) != 0);
		if ($("#checkbox6").checked != flag) $("#checkbox6").attr("checked",flag).checkboxradio("refresh");

	}

	if (ports.PORT_1 != ""){
		pval = parseInt(ports.PORT_1,16);

		flag = ((pval & (1 << 0)) != 0);
		if ($("#checkbox7").checked != flag) $("#checkbox7").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 1)) != 0);
		if ($("#checkbox8").checked != flag) $("#checkbox8").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 2)) != 0);
		if ($("#checkbox9").checked != flag) $("#checkbox9").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 3)) != 0);
		if ($("#checkbox10").checked != flag) $("#checkbox10").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 4)) != 0);
		if ($("#checkbox11").checked != flag) $("#checkbox11").attr("checked",flag).checkboxradio("refresh");
		flag = ((pval & (1 << 5)) != 0);
		if ($("#checkbox12").checked != flag) $("#checkbox12").attr("checked",flag).checkboxradio("refresh");

	}
}

// ARDUINO_IO_GET スクリプト実行結果のイベントハンドラ。
function io_get_handler(data){
	if (data.Result != "Success"){
		$.mobile.changePage( "#error_back_dialog", {transition: "pop",role:"dialog"});
		return;
	}
	apply_ui(data.ResultParams);
}

// UI コンポーネントの初期値ロード
function load_arduino_io(){
	script_exec("ARDUINO_IO_GET",{},"io_get_handler");
}

// メインページが表示された
$( '#page1' ).live( 'pageshow',function(event){
	// 秘密のセッショントークン文字列を強制的に設定
	session_token = "abc123";

	load_arduino_io();
});

// チェックボックスを操作してArduino ピンの値が更新された
$('input[class="pin"]' ).bind( "change", function(event, ui){
	var params = {};
	var attrVal = getAttrVal(this,"pin");
	if (attrVal != ""){
		params["pin"] = attrVal;
		if (this.checked){
			params["value"] = "1";
		} else {
			params["value"] = "0";
		}
		script_exec("ARDUINO_PIN_SET",params,"script_exec_callback");
	}
});

// ボタンを操作してArduino ポートの値がクリアされた
$('[class="port"]' ).bind( "click", function(event, ui){
	var params = {};
	var attrVal = getAttrVal(this,"port");
	if (attrVal != ""){
		params["value"] = "00";
		params["port"] = attrVal;
		script_exec("ARDUINO_IO_PUT",params,"script_exec_callback_reload");
	}
});

// Arduino の I/O 状態が変更された場合に最新のI/O データが配信されてくる
// これを受信するために、WebSocket(socket.io)でリレーサーバーに接続する
var socket = io.connect('/',{ port:9090});

// リレーサーバーから 'broadcast' タグの付いたメッセージを受信した
socket.on('broadcast', function (data) {
	// 配信されてきたJSON データを取得
	// JSON 文字列からオブジェクトにデコード
	// イベントハンドラのパラメータ data 自身もオブジェクトだが、その中の
	// message タグに格納されているのは、ただの JSON 文字列になっている点に注意
	var obj = $.parseJSON(data["message"]);

	// 最新の I/Oデータをチェックボックスに反映させる
	apply_ui(obj);
});

以前に紹介した Web アプリの main.js とほぼ同じですが、最後の部分に socket.io(WebSocket) で配信用サーバーに接続するための記述が追加されています。配信用サーバーからブロードキャストメッセージで “broadcast” タグが付いたデータを受信すると、そのデータから “message” タグに関連付けられた文字列を取り出します。ここには、DeviceServer の SERIAL_FIRMATA イベントハンドラから送信された Arduino の最新のI/Oデータ値が JSON フォーマット文字列で格納されています。 この文字列をオブジェクトにデコートした後、apply_ui() 関数をコールしてWebアプリのチェックボックスの値を更新します。apply_ui() 関数は、Web アプリ起動時に “ARDUINO_IO_GET” スクリプトを実行して最新の Arduino I/O 値を取得したときにも同様にコールされます。

ここまでの作業でWeb アプリが完成しました。Web ブラウザで “http://localhost:8080/arduino_io/index.html” にアクセスすると、Webアプリのメイン画面が表示されて、現在の Arduino I/O の状態がチェックボックスに表示されます。チェックボックスを操作して、Arduino I/O を ON/OFF に設定できます。”localhost” 部分は、スマートフォン等からアクセスする場合には DeviceServer のIP アドレスやホスト名に置き換えて指定して下さい。

(ここで紹介しなかった、チェックボックス操作時やWeb アプリ起動時にWeb API 経由でArduino を操作する方法とWeb アプリのセットアップ方法については以前の記事1記事2も参照して下さい)

iPod touch からWeb アプリを開いた時の画面

同時に、PC から Firefox Web ブラウザ等で Web アプリを起動すると下記の様な画面が表示されます。

どれか一つのWeb アプリの画面からチェックボックスを操作すると、Webアプリを起動している全てのブラウザ画面が同時に更新されるのを確認できると思います。以下に、Web アプリを操作した時の動画を載せましたので参照してください。(音量注意)

今回の一連の記事では、DeviceServer と WebSocket(node.js + socket.io) を利用することで複数の Web アプリ画面を同期して操作する例を紹介しました。またこの仕組みは、DeviceServer 側で取得したイベントを、複数のWeb ブラウザにプッシュ通知する仕組みとしても使用できます。

これらを応用することで、センサーネットワークで取得した測定データをリアルタイムに複数の Webブラウザ画面に表示して、センサー値の変化に応じて画面を自動更新させることができます。また、センサー値の異常や、警報状態、アラームメッセージ等をWebブラウザ画面にリアルタイムに表示することで、ユーザーが必要とするイベント情報をすぐに通知することが可能になります。スマートフォンやタブレットにも同様にリアルタイム表示できますので、それらを監視用専用モニタや表示器として使うことができると思います。

ここで紹介した DeviceServer の機能は、ABS-9000 DeviceServer インストールキットをダウンロードして、直ぐに使用することができます。(デモライセンスが添付されていますので直ちに使用可能です)

それではまた。

 

Arduino I/O をスマートフォン(iPod touch)から操作する、WebSocket(socket.io + node.js) を使用して I/O 変化時にブラウザ画面を自動更新(その2)

前の記事に引き続き今回もArduino I/O を操作する Web アプリに、画面の自動更新機能を追加するための説明をします。

今回は、前の記事で検討した下記の機能追加項目について実現していきます

(2) Web アプリを実行する複数のブラウザ画面のコネクションを管理して、Arduino I/O が変化した時にサーバーPC から最新のI/O データを配信する

最初に今回のアプリで要求される、多数のブラウザのコネクションを継続的に維持して、ブロードキャストメッセージを送信する機能について DeviceServer のみで実現可能かどうかを検討します。

DeviceServer は比較的リアルタイム指向的な制御を行うように設計しています。サーバーに接続されたセンサーや計測機器、リモートデバイスからのイベントは全て別スレッドで処理を開始させて、各々の処理(イベントハンドラやスクリプト)は独立したインスタンスで実行されます。このため、どれか一つの処理に時間がかかったり、エラーが発生した場合でもセンサーネットワークシステム全体には影響を与えないようにしています。

今回検討している複数のブラウザのコネクションをDeviceServer で直接維持・管理すると、スレッドの数が接続中のブラウザ分だけ増加して、すぐにサーバー内部のリソースが不足してしまいます。このため、今回の様なアプリケーションの実現に適した node.js ( http://nodejs.org/ ) と soket.io ( http://socket.io/ )を採用して、DeviceServer と併用してアプリを構築することにします。

node.js と socket.io を利用した配信用サーバーを作成して、DeviceServer とは別に実行します。この配信用サーバーでは、複数ブラウザのコネクション管理とメッセージ配信に特化した機能を実現します。Arduino I/O が変化すると最新の I/O データ値がシリアルポート経由の Firmata パケットで DeviceServer に送信されてきます。その I/O データ値を、配信用サーバー(node.js と socket.io で作成した)にソケット通信で送信します。配信用サーバーはWebアプリ中で接続中の全ブラウザに対して、I/O データ値を WebSocket (socket.io でカプセル化されたコネクション)を使用して配信します。

例えると、これらの2つのサーバー機能は放送局(DeviceServer)と通信衛星(配信用リレーサーバー)の様な関係になります。放送局(DeviceServer)から配信したいデータ(Arduino I/O ポート値)を、一旦通信衛星(配信用サーバー)に送信(uplink) します。その後、通信衛星からは各家庭のテレビ(Webブラウザ)に対してブロードキャスト(downlink) されてきます。また、各家庭のテレビからリモコンの4色ボタンでデータ送信やコマンド要求(Arduino I/O をチェックボックスで操作など) するときには通信衛星(配信用サーバー)ではなく電話回線やインターネット回線(Web API)を使用して直接放送局(DeviceServer の HTTP サーバー)に接続します。

配信用サーバーに使用する WebSocket は双方向通信にも使用できますが、DeviceServer 側を操作するときに各リクエスト毎の認証が必要なため、ブラウザからの通信は Web API (HTTP Server の GET リクエスト) 経由のみに限定しています。

さっそく node.js のインストールから始めます。今回は DeviceServer の動作している Windows PC に node.js をインストールしていますが、別のPC で動作している linux 等にインストールしても構いません。ただし別 PC に配信機能を分けた場合には DeviceServer から配信データを送信(uplink) する部分のホスト名(“localhost”) と、Web アプリ中からアクセスする WebSocket のURLアドレス(“/”) を変更する必要があります。

node.js のホームページ http://nodejs.org/ から最新の Windows 版パッケージをダウンロードしてインストールしてください。node.js のインストール完了後に追加パッケージの socket.io もインストールします。追加パッケージは node.js で提供されている npm プログラムをコマンドプロンプトから実行してインストールします。(下記画面参照。インストール作業を行うディレクトリに注意してください)

sokcet.io インストール後に、Windows のコントロールパネルから “システム” を選択して環境変数 “NODE_PATH” を下記の例の様に設定します。

これで、node.js と socket.io のインストールは完了しました。次に node.js で実行する配信用サーバーのスクリプトファイル(JavaScript)を作成します。配信用サーバーのスクリプトファイルはどこに配置しても構いませんが、今回は “C:\Projects\node\arduino_relay_server.js” に格納しています。

下記に、arduino_relay_server.js ファイルの内容を示します。この後の記事で説明するファイルを含めてここからダウンロードすることができます。

// RelayServer から複数の Webブラウザへデータを配信するためのポート
// 最初にWeb ブラウザ側から WebSocket(socket.io) を使用してコネクションが張られる。
// コネクションが確立した後ブラウザ側は RelayServer から送信される
// ブロードキャストメッセージを受信する。

var downlink_port = 9090;

// 予め socket.io を npm を使用してインストールしておくこと
var downlink = require('socket.io').listen(downlink_port);

downlink.configure(function(){
  downlink.set('log level', 1); // socket.io の debug ログを抑止
});

// DeviceServer からWeb ブラウザに配信するためのデータを受け付けるためのポート
// 通常のソケットサーバー(net)経由で JSON 文字列で記述された配信用データを受信する
// 文字列の終端は \n で判断して、受信後に短い ACK 文字列をDeviceServer 側に送信する。
// この ACK 文字列は、DeviceServer側 API tcd_send_recv_data()が、通信が完了したことを判断する
// ために使用している。

var uplink_port = 9080;

var net = require('net');

var relay_server = net.createServer(function (uplink_stream) {
	var buffer = "";
	var msg = "";

  	uplink_stream.setEncoding("utf8");

	uplink_stream.on("data", function (data) {

		// 配信用データをDeviceServer から受信する
    	if ( data.indexOf('\n') < 0) {
			buffer += data;
    	} else {
			// 配信用データ末尾の空白文字を除去
			msg = buffer + data.substring(0, data.indexOf('\n') );
			msg.replace(/\s+$/g, ""); // trim right

			// DeviceServer に受信完了を知らせる
	    	uplink_stream.write("OK\n"); // sendback a ack
	    	uplink_stream.end();

			// WebSocketで接続中の全ブラウザに対して配信データを送信する
			downlink.sockets.emit('broadcast',{ message: msg });

			console.log('broadcast:' + msg);
		}
	});
});

// 配信用 RelayServer起動
relay_server.listen(uplink_port);

スクリプトの最初の部分で定義された “downlink”  が Webアプリから接続される WebSocket(socket.io) になります。接続後は、配信用サーバーから送信されるブロードキャストメッセージの到着を待ち続けます。

uplink_port と relay_server は、それぞれ DeviceServer から最新の Arduino I/O データを受信するためのTCPソケットのポートとTCP サーバーになります。DeviceServer から改行文字で終端された文字列データを受信すると、その文字列データに ‘message’ タグに対応させた連想配列データを作成して、WebSocket でブラウザが接続している downlink に対してブロードキャスト送信します。送信データは下記の様なJSON フォーマットになっています。

* uplink ポートにDeviceServer から送信されてくる文字列例

{“PORT_0″:”00″,”PORT_1″:”00″}

*WebSocket(downlink) で接続中の全Webブラウザに送信されるデータ。uplink ポートで受信したJSON データを文字列として”message” タグに関連づけて、その全体のJSON データを送信しています。

{“message”: “{“PORT_0″:”00″,”PORT_1″:”00″}” }

配信用サーバーを実行するPC でファイアウォールが設定されている場合には、TCPポート番号(9090、9080)で通信可能に設定して下さい。またブラウザからWebアプリでサーバーPCにアクセスするときに、ルータ等を経由する場合には、適切に設定して通信が可能な状態にしておきます。

配信用サーバーはコマンドプロンプトから node コマンドで起動します。下記の実行例画面中にある “broadcast:xxx” のメッセージは、実際にWebアプリを動作させた時に表示されますので今は表示されません。

これで、配信用サーバーの準備ができました。

これまでの作業で Arduino 側の I/O 変化時のデータ送信機能とブラウザ側へのデータ配信機能の作成が完了しました。続きの作業は、後の記事で説明します….

それではまた。

 

Arduino I/O をスマートフォン(iPod touch)から操作する、WebSocket(socket.io + node.js) を使用して I/O 変化時にブラウザ画面を自動更新(その1)

 

Arduino をスマートフォンやWeb ブラウザから操作するアプリケーションの第2弾を紹介します。以前に紹介したWebアプリの機能に、 Arduino 上のI/O ポート値が変化したときに自動でブラウザ画面を最新の状態にするようにします。

以前に紹介したWeb アプリの説明は、Arduino をシリアルポート経由でサーバーに接続して、スマートフォン等の Web ブラウザから操作する機能については (1)Arduino を Web API 経由で操作 の記事と、(2) Arduino をスマートフォンから操作 の記事を参照してください。

前回紹介した Web アプリでは、複数のブラウザが同時に Arduino I/O を操作した場合に備えて “reload” ボタンをアプリのタイトルヘッダーにつけていましたが、今回のアプリでは Arduino がブラウザから操作されてI/O ポート値が変化すると、すべてのブラウザ画面が自動更新される様になるため “reload”ボタンが不要になります。

今回紹介するアプリケーションの動作画面(前回と比べると Reload ボタンがありません)

このWeb アプリを複数のPC やスマートフォンのブラウザから同時に操作すると、現在のI/O 値を示すチェックボックスの値が、リアルタイムに全てのブラウザ画面で同期させることができます。

これを実現するためには、以前に紹介したアプリに対して下記の機能の追加が必要になります。

(1) I/O チェックボックス操作時または Arduino I/O の値が変化した時に、最新の I/O 値をサーバーPC で検出・取得する

(2) Web アプリを実行する複数のブラウザ画面のコネクションを管理して、Arduino I/O が変化した時にサーバーPC から最新のI/O データを配信する

(3) Web アプリを実行しているブラウザが、最新の I/O データを受信したときにWeb アプリ上の GUI(チェックボックス) の状態を自動更新させる

最初に (1) について実現方法を検討します。

スマートフォンで実行する Web アプリが Arduino I/O を操作する時には、必ずサーバーPC のWeb API 経由で実行しますので、サーバーPC側で I/O の変化を知ることは可能です。ただ、この方法をとると Arduino をリセットした場合など、I/Oポート値がハード(Arduino)側のみの都合で変化した場合に、その状態をブラウザ側に通知する方法がなくなってしまいます。このため、今回のアプリでは Arduino 側で実行するスケッチプログラムで I/O の変化を検出して、最新の I/O データを Firmata プロトコルでシリアルでサーバーに通知する方法をとります。

下記に、Arduino で実行するスケッチプログラムを示します。この後の記事で説明するファイルを含めてここからダウンロードすることができます。

 

/*
 DigitalIOControl2.ino

 DigitalIOControl.ino から下記の機能を追加しています。

 (1) Arduino Digital I/O をPC 側から更新(変更)したときに、
     DIGITAL_PORTS_MESSAGE をPCに送信する

 (2) DIGITAL_PORTS_MESSAGE Firmata プロトコルを新規追加

 Firmata プロトコルで PC 側から Arduino digital I/O を操作するための機能です。
 初期化時に、Arduino pin#2 から pin#13 を digital OUTPUT モードに設定しています。

 ------------------
 サポートしている Firmata プロトコル
 * DIGITAL_MESSAGE
   標準プロトコルで定義されているプロトコル。
   PC から Arduinoボード側へのI/O ポート値設定のみに使用しています。

   -- digital I/O message --
   0  command (0x90) + port#
   1  port value bits 0-6  (LSB)
   2  port value bits 7-13 (MSB)

 * デジタルポートの現在の値を取得するための SYSEX コマンド DIGITAL_PORTS_QUERY と
   リプライDIGITAL_PORTS_REPLY

   -- query digital ports --
   0  START_SYSEX          (0xF0)
   1  query digital ports  (0x61)
   2  END_SYSEX            (0xF7)

   -- reply digital ports --
   0  START_SYSEX          (0xF0)
   1  reply digital ports  (0x62)
   3  port#0 bits 0-6      (LSB)
   4  port#0 bits 7-13     (MSB)
   ..
   5  port#n bits 0-6      (LSB)
   6  port#n bits 7-13     (MSB)
   7  END_SYSEX            (0xF7)

 * ピン番号と値を指定してポートを操作する SYSEX コマンド DIGITAL_WRITE_PIN
   -- write digital port pin --
   0  START_SYSEX          (0xF0)
   1  write digital pin    (0x63)
   2  pin number           (0x02-0x0D)
   1  pin value            (0/1)
   2  END_SYSEX            (0xF7)

 * DIGITAL_PORTS_MESSAGE
   I/O 値が更新された場合に、PC 側に最新ののI/O ポート値を送信する

   -- reply digital ports --
   0  START_SYSEX          (0xF0)
   1  digital ports message(0x64)
   3  port#0 bits 0-6      (LSB)
   4  port#0 bits 7-13     (MSB)
   ..
   5  port#n bits 0-6      (LSB)
   6  port#n bits 7-13     (MSB)
   7  END_SYSEX            (0xF7)

 このファイルは自由に使用することができます。内容を変更するなどをして利用することもできます。
 オールブルーシステムは、このファイルを使用したことによる損害等について一切保障できません。
                   2012 All Blue System     Satoshi Kimura
*/

#include <Firmata.h>

//  デジタルポート数の定義
//  ATmega328 を使用した Arduino ボードの場合は 2 になる
#define TOTAL_DIGITAL_PORTS             ((TOTAL_PINS - TOTAL_ANALOG_PINS + 7) / 8) 

// SYSEX コマンドの定義
#define DIGITAL_PORTS_QUERY      0x61
#define DIGITAL_PORTS_REPLY      0x62
#define DIGITAL_WRITE_PIN        0x63
#define DIGITAL_PORTS_MESSAGE    0x64

// デジタルポートが変更されかどうかを判断するたの値保存用
byte prev_pval[TOTAL_DIGITAL_PORTS];

// PC 側から I/O 値を操作したことを示すフラグ
// 0: I/O 操作なし
// 1: I/O 操作行った
// 2: I/O 値を強制的に PC に送信
volatile byte io_update;

// SYSEX コマンドの処理
void sysexCallback(byte command, byte argc, byte *argv)
{
    byte pval;
    switch(command) {
        case DIGITAL_PORTS_QUERY:
            Serial.write(START_SYSEX);
            Serial.write(DIGITAL_PORTS_REPLY);
            for (byte i=0; i<TOTAL_DIGITAL_PORTS; i++) {
                pval = readPort(i, 0xff);
                Serial.write(pval & B01111111);      // LSB
                Serial.write(pval >> 7 & B01111111); // MSB
            }
            Serial.write(END_SYSEX);
            break;
        case DIGITAL_WRITE_PIN:
            digitalWrite(argv[0],argv[1]);
    	    io_update = 1;
            break;
    }
}

void digitalWriteCallback(byte port, int value)
{
    byte currentPinValue;
    if (port < TOTAL_DIGITAL_PORTS) {
        for(byte i=0; i<8; i++) {
            currentPinValue = (byte) value & (1 << i);
            digitalWrite(i + (port*8), currentPinValue);
        }
    }
    io_update = 1;
}

void checkAndReportDigitalPorts(void){
    byte io_change = 0;
    byte current_pval[TOTAL_DIGITAL_PORTS];

    // 最新のデジタルポート値を読んで前回の値と比較
    for (byte p=0; p<TOTAL_DIGITAL_PORTS; p++) {
        current_pval[p] = readPort(p, 0xff);
        if(prev_pval[p] != current_pval[p]) io_change = 1;
        prev_pval[p] = current_pval[p];
    }

    // デジタルポート値が変化していた場合には最新のデジタルポート値を DIGITAL_PORTS_MESSAGE で送信する
    if((io_change) || (io_update == 2)) {
        Serial.write(START_SYSEX);
        Serial.write(DIGITAL_PORTS_MESSAGE);
        for (byte p=0; p<TOTAL_DIGITAL_PORTS; p++) {
            Serial.write(current_pval[p] & B01111111);      // LSB
            Serial.write(current_pval[p] >> 7 & B01111111); // MSB
        }
        Serial.write(END_SYSEX);
    }

    io_update = 0;
}

void setup()
{
    Firmata.setFirmwareVersion(0, 1);
    Firmata.attach(DIGITAL_MESSAGE, digitalWriteCallback);
    Firmata.attach(START_SYSEX, sysexCallback);
    Firmata.begin(57600);
    // pin#2-#13 のデジタルポートを OUTPUT モードに初期設定する
    for(byte i = 2;i <= 13;i++){
        pinMode(PIN_TO_DIGITAL(i), OUTPUT);
    }

    // ポート変化チェック用の変数を初期化
    for (byte p=0; p<TOTAL_DIGITAL_PORTS; p++) {
        prev_pval[p] = 0x0;
    }

    // 初期化時に最新のデジタルポート値をPCに送信
    io_update = 2;
}

void loop()
{
    // デジタルポート値が変化していた場合には最新のデジタルポート値をPCに送信
    if(io_update){
        checkAndReportDigitalPorts();
    }

    while(Firmata.available()) {
        Firmata.processInput();
    }
}

スケッチプログラムの殆どの部分は以前の記事で紹介したものと同じですが、Firmata プロトコルに下記のメッセージを追加しています。

* DIGITAL_PORTS_MESSAGE

Arduino の I/O 値が変化した場合などに、サーバーPC に最新の I/O ポート値を送信する。詳しいパケットの定義は、コメント部分を参照して下さい。

io_update 変数を使用して、サーバーPCに I/O データを通知するタイミングをコントロールしています。Web API 経由で Arduino 側の I/O を操作するときに実行される Firmata コマンドの DIGITAL_MESSAGE, DIGITAL_WRITE_PIN の処理ルーチン内で、io_update に 1 を代入して最新の I/O データ通知をリクエストします。実際に I/O 値が変化していたかどうかは、loop() 関数内で繰り返しコールされる CheckAndReportDigitalPorts() 関数で調べています。関数内では、前回サーバーPC に I/O 値を送信してから今回チェックした時までに I/O 値が変化していた場合には、Firmata プロトコル DIGITAL_PORTS_MESSAGE を使用して最新の I/O ポート値をサーバーPC に送信します。

また、Arduino リセット時には io_update に 2 を代入して、強制的に最新の I/O 値を DIGITAL_PORTS_MESSAGE パケットを使用してサーバーに送信します。

このスケッチをArduino にロードして実行します。Web API 経由で Arduino I/O を操作したときの DeviceServer で記録されたログは下記の様になります。下記のログは後の記事で紹介する予定のDeviceServer 側のスクリプト等を変更した後に取得しましたので、ログ中にはそれらのメッセージが混じっていますが今回の記事では無視して下さい。

 

Web アプリから I/O 操作を行った直後やリセット時に、DIGITAL_PORTS_MESSAGE パケットがサーバーに送信されているのを確認することが出来ます。

今回の記事では、Arduino 側のファームウエアの変更点についてのみ説明しました。ここまでの作業で Web API や Web アプリから Arduino I/O を操作すると、最新の I/O データ値がサーバーPC に送信されてくるようになりました。続きの作業は、後の記事で説明します….

それではまた。