前回の記事に引き続き、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 インストールキットをダウンロードして、直ぐに使用することができます。(デモライセンスが添付されていますので直ちに使用可能です)
それではまた。