TWE-Lite をWebAPIとWebアプリから操作する

概要

今回の記事では、東京コスモス電機より販売されているワイヤレスモジュール TWE-Lite をリモートから操作する例を紹介したいと思います。TWE-Lite は XBee と同じように簡単にワイヤレス機能を利用することができ、無線モジュールのファームウエアをユーザーが書き換えて外付けマイコン無しでセンサーノードを作成することができます。今回の記事では、ファームウエアはデフォルトでロードされている “超簡単!TWE標準アプリ” を使用しています。

リモートに設置したTWE-Lite デバイスをWebAPI から簡単に操作できるようにします。また、専用のWeb アプリを作成して、スマートフォンや PC のWebブラウザから GUI を使用して簡単に操作できるようにします。

記事の最後に動画を掲載していますので、最初にこれをご覧になると記事の内容が判り易いと思います。

TWE-Lite デバイスの準備

今回使用する TWE-Lite デバイスはブレッドボードなどで簡単に使用できるように、TWE-Lite DIP タイプを3つ使用しています。1つを PC(Windows) に接続する親機として使用して、残りの2つを子機として使用します。

親機用 TWE-Lite DIP デバイスには、PC と仮想COM ポートでアクセスできるように USBシリアルアダプタ(秋月電子 “FT232RL USBシリアル変換モジュールキット”)を接続しています。このシリアルアダプタを使用する代わりに、USBに直接接続できる ToCoStick やTWE-Lite R を利用してももちろん構いません。

親機をPCに接続した様子です。ブレッドボード上にUSBシリアルアダプタと TWE-Lite DIP を配置しています。後の項で説明する TWE-Lite デバイスの初期設定やファームウエアの書き換えにも使用できるように、リセットボタンとプログラムボタンも接続しています。このときブレッドボードを使用しているとTWE-Lite DIP の入れ替えが簡単にできます。

TWE-Lite の電源は USB シリアルアダプタから出力される 3.3V を使用しています。上記の回路は TWE-Lite のホームページに公開されている、 “TWE-Lite のファームウエア書き込み方法” に関するマニュアルを参考にしました。

子機で使用する TWE-Lite も同様にブレッドボード上に配置しました。子機#1(Device ID:1) にはPWM出力確認用の LED が4つ接続されています。子機#2(Device ID:2) にはDO(デジタル出力)確認用の LED が4つと A/D(AD#4)入力用に可変抵抗器を接続しています。また、DI (デジタル入力)ピンを GND に接続するためのワイヤーも引き出しています。

子機#1にはI2C の試験用に 24LC256 EEPROM とTMP102 温度センサを I2C バスに接続しています。I2C バスはブレッドボード上に接続した4.7Kの抵抗でプルアップしています。子機の電源は AC アダプタから 3.3V を供給して動作させています。

TWE-Lite デバイスの初期設定

最初に、TWE-Lite デバイスの初期設定を行います。前述の、ブレッドボード上で作成した親機に搭載する TWE-Lite デバイスを入れ替えて、子機2台と親機のモジュール設定をシリアルコンソールから操作します。

下記は、親機に使用する TWE-Lite をターミナルエミュレータソフト(TeraTermを使用しています) から接続した様子です。

“+” 文字を3回押してコンフィギュレーション画面を表示しています。

ここで必ずファームウエアのバージョンを確認してください。上記の場合には v1.7.1 になっています。最新バージョンの v1.6.6. または v.1.7.1 よりも古いバージョンの場合にはファームエアの更新が必要です。購入した直後のデバイスには古いファームウエアが格納されている場合がありますので必ず確認するようにしてください。

ファームエアを更新する場合には、プログラムボタン(オレンジのタクトスイッチ)を押しながらリセットボタン(白のタクトスイッチ)を押して、その後プログラムボタンを離すことでプログラムモードに入ります。TWE-Lite のホームページから書き込みソフトとファームエアファイルをダウンロードして書き込みを行います。詳しい手順はTWE-Lite のホームページを参考にしてください。

最新バージョンのファームウエアを使用していることを確認したら、設定画面で Application ID(PAN ID)を任意の値に設定します。ここでは 0x00aabbcc に設定しています。Application ID の値は親機と子機全てに同じ値を設定します。次に Option Bits を 0×00000012 に設定します。この設定を行うと親機側のADC変化検出と定期送信を停止することができます。

今回のアプリケーションの様に子機側の I/O を個々にリモート操作する場合には必ずこの設定を行います。この設定を行わないと(デフォルト設定値のままだと)親機側の デジタル入力や A/D 値が子機側のデジタル出力や PWM 出力値に自動的に反映されて、うまくリモート操作することがきませんので注意してください。

親機の設定が終了したら、ブレッドボード上の TWE-Lite DIP を入れ替えて子機#1 の設定を行います。設定値は以下のようになります。

親機と同様に Application IDを 0x00aabbcc に設定しています。子機では Device ID を設定してそれぞれの子機を個別に操作できるようにします。ここでは 0×01 に設定しています。

また、Option Bits は 0×00000002に設定して定期送信を停止していますが、デフォルトの 0×00000000 のままでも構いません。どちらの値に設定しても、デジタル入力が変化した時や A/D 変換値が一定以上変化した場合には、自動的に親機側にイベントが送信されます。このイベント情報によってサーバー側が現在の子機のデジタル入力値とA/D 変換値がどのようになっているかをその都度、子機側に問い合わせなくても把握することができます。

子機#1 の設定が終了したら、TWE-Lite デバイスを子機#2 に入れ替えて同様の設定をしてください。このとき、Device ID の設定値のみを変更して 0×02 にして後の設定値は子機#1 と同じにします。

PC(DeviceServer)に親機を接続

親機を PC に接続した後、子機側の電源も入れて通信可能な状態にします。次に、PC 側にインストールした DeviceServer の “サーバー設定” プログラムを起動して、親機を接続しているシリアルポート(仮想 COMポート)を新規のシリアルデバイスとして登録します。

登録する、シリアルデバイスの設定は上記のようになります。COM ポート名は親機のシリアルアダプタ(FT232RL) で作成された仮想COM ポートを指定します。ボーレートは TWE-Lite DIP のデフォルト値 115200 を指定します。デバイスタイプには “TWE” を指定します。この指定によって DeviceServer ではこのシリアルポートに対して以下の機能が有効になります。

COM ポートから入力されたアスキー形式の TWE-Lite パケットを受信した時に、自動的にイベントハンドラ (SERIAL_TWE.lua) が起動されてパケット内容を解析します。また、DeviceServer に作成したユーザースクリプトやイベントハンドラ中から twe_print() ライブラリ関数を使用して、アスキー形式のパケット送信や、リクエスト・リプライタイプのコマンドの送受信を行うことが可能になります。

デフォルトで動作している ”超簡単!TWE標準アプリ” ファームウエアを独自に拡張して新しいコマンドを作成した場合でも、同様のアスキー形式にしておくことで、新しいコマンドを簡単に DeviceServer から利用することができます。

シリアルデバイス登録後の “サーバー設定” プログラムのデバイス一覧は以下のようになります。

この設定画面では複数の親機を同時に登録することもできます。別々のPAN(Application ID) で管理されている子機グループ間の操作も簡単に行うこともできます。

“次へ” ボタンを押して”サーバー設定” プログラムの操作を完了させると、自動的にDeviceServer が再起動した後 TWE-Lite デバイスを使用することができるようになります。

WebAPI からTWE-Lite 子機を操作する

最初の使用例として リモートの TWE-Lite 子機#1 のデジタル出力ピンを操作してみます。Web API を使用して /command/json/script のURL にアクセスします。

URL パラメータ “name” で指定する Lua スクリプトファイルは “TWE/DO_PIN_SET” でスクリプトの説明は後述します。”com” パラメータには親機が接続されている COM ポート名を指定します。”id” , “pin”, “value” パラメータには操作対象の子機の Device ID, DO ピン番号、出力する値を指定します。この例では子機#1 のDO#3 を Low に設定しています。

Web API で実行する “TWE/DO_PIN_SET”  Lua スクリプトファイルは以下の様に記述されています。

file_id = "TWE/DO_PIN_SET"

--[[

●機能概要

TWEワイヤレスデバイス子機の指定したデジタルピンの値を設定する

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1","120"

pin			DO#番号(1 から 4 までの整数)						"4"

value		pin に指定したI/Oポートに設定する値(0 または 1)		"1"
			TWEワイヤレスデバイスの場合には "1" で Low出力,
			"0" で High出力になります

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"] and g_params["pin"] and g_params["value"]) then
	log_msg("parameter error",file_id)
	error()
end
local stat,com = serial_find_device(g_params["com"])
if not stat then
	log_msg("COM port not found",file_id)
	error()
end
local pin = tonumber(g_params["pin"]) - 1
if (pin < 0) or (pin > 3) then
	log_msg("invalid DO# number",file_id)
	error()
end
----------------------------------------------------------------------
-- 0x80 コマンド送信
----------------------------------------------------------------------
local mask = bit_tohex(bit_lshift(1,pin),2)
local io = "00"
if g_params["value"] == "1" then
	io = mask
end

local command_str = ":" .. bit_tohex(tonumber(g_params["id"]),2) .. "8001" ..
                    io .. mask .. "FFFFFFFFFFFFFFFFX"

for cnt = 1,2,1 do -- 確実に操作するために同一コマンドを2回送信する
	if not twe_print(com,command_str) then error() end
	wait_time(10)
end

URL で指定された複数のパラメータは、Lua スクリプト実行時に g_params[] 連想配列に格納されています。

最初に必要なパラメータが指定されているかを調べた後、command_str 文字変数に TWE-Lite のアスキー形式で送信する文字列データを格納していきます。その後、twe_print() ライブラリ関数を使用して、DeviceServer に登録したシリアルポートからデータパケットが送信されます。データパケットを受信した親機では 0×80 コマンドが実行されてリモート側の子機#1 の DO#3 が変更されます。

次の動作例では、子機のデジタル入力と A/D 変換値を Web API から取得します。

デジタル出力の時と同様に、DeviceServer で公開されている WebAPI コマンドパス /command/json/script にアクセスます。 “name” パラメータには “TWE/AD_DI_GET” を指定して子機のデジタル入力ポートと A/D 変換値を取得します。スクリプトの実行結果は、JSON 形式で返りますのでこの内容がブラウザ画面に表示されています。

Web アプリや、外部のアプリケーションサーバーから同様の URL にアクセスするだけで、リモートに設置している子機の状態を簡単に取得することができることになります。

TWE/AD_DI_GET Lua スクリプトファイルは以下の様に記述されています。

file_id = "TWE/AD_DI_GET"

--[[

●機能概要

TWEワイヤレスデバイス子機のA/D値, DI 値を取得する

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		    									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
AD1				TWEワイヤレスデバイス子機 AD1 の値					"0"
AD2				TWEワイヤレスデバイス子機 AD2 の値					"-1"
AD3				TWEワイヤレスデバイス子機 AD3 の値					"1980"
AD4				TWEワイヤレスデバイス子機 AD4 の値					"500"

DI1				TWEワイヤレスデバイス子機 DI1 の値					"1"
DI2				TWEワイヤレスデバイス子機 DI2 の値					"1"
DI3				TWEワイヤレスデバイス子機 DI3 の値					"1"
DI4				TWEワイヤレスデバイス子機 DI4 の値					"1"

STATUS			スクリプト実行が正常に終了した場合には "OK"、
				エラー発生時には"ERROR" が設定される				"OK"

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"]) then
	log_msg("parameter error",file_id)
	error()
end
local stat,com = serial_find_device(g_params["com"])
if not stat then
	log_msg("COM port not found",file_id)
	error()
end

----------------------------------------------------------------------
-- 子機の最新のデバイス情報をグローバル共有変数から取得
----------------------------------------------------------------------
local key = "TWE_" .. com .. "_" .. g_params["id"]
local val
stat,val = get_shared_data(key)
if (not stat) or (val == "") then
	if not script_result(g_taskid,"STATUS","ERROR") then error() end
	return
end

local tbl= csv_to_tbl(val)
if tbl[3] and tbl[4] and tbl[5] and tbl[6] and tbl[7] and tbl[8] and tbl[9] and tbl[10] then
	if not script_result(g_taskid,"DI1",tbl[3]) then error() end
	if not script_result(g_taskid,"DI2",tbl[4]) then error() end
	if not script_result(g_taskid,"DI3",tbl[5]) then error() end
	if not script_result(g_taskid,"DI4",tbl[6]) then error() end
	if not script_result(g_taskid,"AD1",tbl[7]) then error() end
	if not script_result(g_taskid,"AD2",tbl[8]) then error() end
	if not script_result(g_taskid,"AD3",tbl[9]) then error() end
	if not script_result(g_taskid,"AD4",tbl[10]) then error() end
	if not script_result(g_taskid,"STATUS","OK") then error() end
else
	if not script_result(g_taskid,"STATUS","ERROR") then error() end
end

このスクリプトではTWE-Lite 子機を操作するために、親機に対してコマンドパケットを送信していない点に注意してください。

TWE-Lite 子機はデジタル入力値やA/D 変換値を、定期的にまたは変化したときに親機に対してイベントデータを送信しています。DeviceServer では親機で受信したイベントデータをイベントハンドラで処理して、子機ごとの最新の I/O 値をグローバル共有データに保存しています。

このスクリプトではそのグローバル共有データの値を利用して子機のデジタル入力とA/D 変換値を返しています。親機がイベントパケットデータを受信したときに、DeviceServer 側で実行するイベントハンドラ(SERIAL_TWE) については後の項で説明します。

続いて子機の I2C バスを WebAPI で操作する例を紹介したいと思います。最初に子機で I2C データを書き込み用スクリプト(TWE/I2C_WRITE) をご覧ください。

file_id = "TWE/I2C_WRITE"

--[[

●機能概要

TWEワイヤレスデバイス子機の I2C バスに接続したスレーブデバイスにデータを書き込む

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1"

slave_addr		I2C スレーブデバイスアドレス(16進数2桁)					"50"

data			スレーブデバイスに書き込むデータ(16進数)列				"0A0B0C"
				最大31バイトまで

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"] and g_params["slave_addr"] and g_params["data"]) then
	log_msg("parameter error",file_id)
	error()
end
local stat,com = serial_find_device(g_params["com"])
if not stat then
	log_msg("COM port not found",file_id)
	error()
end

local data_len =  math.floor(string.len(g_params["data"]) / 2)
if data_len < 1 then
	log_msg("data parameter error",file_id)
	error()
end

----------------------------------------------------------------------
-- 排他制御開始
-- 同じ TWE親デバイスに対して、QUERY コマンドが複数同時実行される場合に備えています。
----------------------------------------------------------------------
local cstat,handle = critical_section_enter("TWEQuery" .. com,5000);
if not cstat then
	log_msg("critical_section_enter() failed",file_id)
	error()
end

------------------------------------------------------------------------------------------------
-- リクエストフレーム送信とリプライ受信
------------------------------------------------------------------------------------------------
local command_str = ":" .. bit_tohex(tonumber(g_params["id"]),2) .. "880101" ..
                    bit_tohex(tonumber("0x" .. g_params["slave_addr"]),2)
if data_len == 1 then
	command_str = command_str .. g_params["data"] .. "00X"
elseif data_len > 1 then
	command_str = command_str .. string.sub(g_params["data"],1,2) .. bit_tohex(data_len - 1,2) .. string.sub(g_params["data"],3,-1) .. "X"
end

stat,rdata = twe_print(com,command_str,"89")

----------------------------------------------------------------------
-- 排他制御終了
----------------------------------------------------------------------
cstat = critical_section_leave(handle)
if not cstat then
	log_msg("critical_section_leave() failed",file_id)
	error()
end
if not stat then
	log_msg("twe_print() failed",file_id)
	error()
end
if string.sub(rdata,10,11) ~= "01" then -- TWE レスポンスフレームで SUCCESS 以外を受信した
	log_msg("twe remote operation failed",file_id)
	error()
end

このスクリプトでは TWE-Lite のファームウエアで用意されている、子機の I2C バスを操作するためのコマンドパケット(0×88) を送信しています。twe_print() ライブラリでリクエストパケットを送信するときに、リプライパケット(0×89) を受信するまで内部で待機するようにパラメータを指定しています。リプライパケットには I2C 操作が正常に完了したかどうかを示すステータスが格納されていますので、これを確認しています。

またI2Cバス操作のコマンドの様に、リクエストパケット送信とその結果をリプライパケットで受信するようなタイプのコマンドを処理する場合には、クライアント(Web APIを利用しているプログラム)側から同時にリクエストが発生した場合に備える必要があります。Lua スクリプト中に排他処理を記述して、Web API を使用する側からは同時使用しても問題が発生しないようになっています。

次は子機のI2C データを読み込むためのスクリプト(TWE/I2C_READ)で、以下の様になっています。

file_id = "TWE/I2C_READ"

--[[

●機能概要

TWEワイヤレスデバイス子機の I2C バスに接続したスレーブデバイスから
指定バイト数分のデータ読み込みを行う

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1"

slave_addr		I2C スレーブデバイスアドレス(16進数2桁)					"50"

count			データ読み込みバイト数(32 以下の10進整数)				"32"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
I2C_DATA		スレーブデバイスから読み込んだデータ(16進数)列
				最大32 バイト(64文字)まで								"0A0B0C

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"] and g_params["slave_addr"] and g_params["count"]) then
	log_msg("parameter error",file_id)
	error()
end
local stat,com = serial_find_device(g_params["com"])
if not stat then
	log_msg("COM port not found",file_id)
	error()
end

----------------------------------------------------------------------
-- 排他制御開始
-- 同じ TWE親デバイスに対して、QUERY コマンドが複数同時実行される場合に備えています。
----------------------------------------------------------------------
local cstat,handle = critical_section_enter("TWEQuery" .. com,5000);
if not cstat then
	log_msg("critical_section_enter() failed",file_id)
	error()
end

------------------------------------------------------------------------------------------------
-- リクエストフレーム送信とリプライ受信
------------------------------------------------------------------------------------------------
local command_str = ":" .. bit_tohex(tonumber(g_params["id"]),2) .. "880102" ..
                    bit_tohex(tonumber("0x" .. g_params["slave_addr"]),2) .. "00" ..
					bit_tohex(tonumber(g_params["count"]),2) .. "X"

stat,rdata = twe_print(com,command_str,"89")

----------------------------------------------------------------------
-- 排他制御終了
----------------------------------------------------------------------
cstat = critical_section_leave(handle)
if not cstat then
	log_msg("critical_section_leave() failed",file_id)
	error()
end
if not stat then
	log_msg("twe_print() failed",file_id)
	error()
end
if string.sub(rdata,10,11) ~= "01" then -- TWE レスポンスフレームで SUCCESS 以外を受信した
	log_msg("twe remote operation failed",file_id)
	error()
end

---------------------------------------------------------------
-- リターンパラメータ設定
---------------------------------------------------------------
if not script_result(g_taskid,"I2C_DATA",string.sub(rdata,14,-3)) then error() end

殆ど、I2C_WRITE スクリプトの内容と同じですが、リプライパケットで受信したバイトデータをスクリプトリターンパラメータで返す部分が追加されています。

前述の I2C_WRITE と I2C_READ スクリプトを組み合わせて、I2C データの書き込みと読み込みをする I2C_WRITE_READ スクリプトも作成してあります。内容は以下になります。

file_id = "TWE/I2C_WRITE_READ"

--[[

●機能概要

TWEワイヤレスデバイス子機の I2C バスに接続したスレーブデバイスにデータを書き込んだ後、
指定バイト数分のデータ読み込みを行う

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1"

slave_addr		I2C スレーブデバイスアドレス(16進数2桁)					"50"

count			データ読み込みバイト数(32 以下の10進整数)				"32"

data			スレーブデバイスに書き込むデータ(16進数)列				"0A0B0C"
				最大31バイトまで

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

I2C_DATA		スレーブで倍しから読み込んだデータ(16進数)列
				最大32 バイトまで										"0A0B0C

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"] and g_params["slave_addr"] and g_params["data"] and g_params["count"]) then
	log_msg("parameter error",file_id)
	error()
end

-------------------------
-- I2C write操作
-------------------------
local param = {}
param["com"] = g_params["com"]
param["id"] = g_params["id"]
param["slave_addr"] = g_params["slave_addr"]
param["data"] = g_params["data"]
local stat,result = script_exec2("TWE/I2C_WRITE",param)
if not stat then error() end

-------------------------
-- I2C read操作
-------------------------
param["count"] = g_params["count"]
local stat,result = script_exec2("TWE/I2C_READ",param)
if not stat then error() end

script_result(g_taskid,"I2C_DATA",result["I2C_DATA"])

このスクリプトは、最初に I2C 書き込みトランザクションを実行した後、読み込みトランザクションを実行します。読み込んだデータ列をスクリプトパラメータで返します。内容は、単に前述の2つのスクリプトを順番にコールしているだけです。

この I2C_WRITE_READ スクリプトを使用して、TWE-Lite 子機#1 に接続した 24LC256 EEPROM のデータをリードしてみます。接続の様子は前の項で紹介した子機の写真を参考にしてください。

“slave_addr” パラメータには 24LC256 のI2Cスレーブアドレス0×50 を指定しています。”data” パラメータに “0000″ を指定してEEPROM デバイス内のアドレスポインタを示す 0×00, 0×00 の2バイトを書き込みます。その後、”count” パラメータで指定した32バイト分のデータを JSON フォーマットで取得しています。

次の動作例は、同じ子機のI2C バスに搭載されている TMP102 温度センサの値を取得してみます。I2Cバス操作と温度計算を以下のスクリプト(TWE/DEVICE/TMP102_READ)で実行します。

file_id = "TWE/DEVICE/TMP102_READ"

--[[

●機能概要

I2C バスに接続した温度センサー(TMP102) の値を取得する

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
com			TWEワイヤレスデバイス親機が接続された COM ポート名
			またはタイトル名									"COM4"

id			取得対象のTWEワイヤレスデバイス子機の論理IDを10進数の文字列で
			指定する											"1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

temperature			センサーから取得した摂氏温度						"12.5"
 					                                                    "-25.0"
]]

local slave_addr = "48"

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["com"] and g_params["id"]) then
	log_msg("parameter error",file_id)
	error()
end

----------------------------------------
-- 12 bit 幅の2の補数を符号付整数に変換
----------------------------------------
function calc_2comp(val)
	if(bit_and(val,0x800) ~= 0) then
		return -1 * (bit_and(bit_not(val),0xfff) + 1)
	else
		return val
	end
end

-----------------------------------------------------------------------
-- TMP102温度レジスタの値を取得する
-- pointer register 0x00 をセットした後、2 バイトのレジスタ値を取得する
-----------------------------------------------------------------------
local param = {}
param["com"] = g_params["com"]
param["id"] = g_params["id"]
param["slave_addr"] = slave_addr
param["data"] = "00"
param["count"] = "2"
local stat,result = script_exec2("TWE/I2C_WRITE_READ",param)
if not stat then error() end

---------------------------------------
-- 温度レジスタ値から摂氏温度を計算する
---------------------------------------
local reg = {}
reg = hex_to_tbl(result["I2C_DATA"])
local temp_int = bit_lshift(reg[1],4) + bit_rshift(reg[2],4)
local temperature = 0.0625 * calc_2comp(temp_int)
script_result(g_taskid,"temperature",string.format("%3.1f",temperature))

このスクリプト中からは、先ほど紹介した I2C_WRITE_READ スクリプトをコールして子機#1 のI2C バスを操作して温度レジスタ値をリードしています。その後摂氏温度を計算してリターンパラメータに返しています。このスクリプトを Web API から実行したときの様子が以下になります。

JSON で返されたデータ内の “temperature” 部分が子機#1で計測された現在の温度になります。

ここで紹介した以外にも子機の PWM を操作するためのスクリプトやデジタル出力と PWM の全チャンネルを同時に更新するためのスクリプト、親機に接続されている全子機のID リスト取得などのスクリプトが予め用意されています。これらのスクリプトファイルは DeviceServer インストール時に “C:\Program Files (x86)\AllBlueSystem\Scripts\TWE” フォルダに格納されていますので Web API から直ぐに使用することができます。

Lua スクリプトはテキストファイル(UTF-8) ですので、簡単にコピーしたり独自のコマンド処理用に改造することができます。

TWE-Lite 親機からパケットを受信したときの動作

TWE-Lite 子機からは、定期的に現在のステータスは送信されていたり、子機のA/D 変換値やデジタル入力の変化を検出した時にはイベントデータが親機に対して送信されます。

イベントデータを受信すると親機からはアスキー形式のパケットデータが送信されます。DeviceServer では親機から送信されるこのようなパケットデータを処理するためのスクリプト(SERIAL_TWE)が予め設定されています。スクリプトの内容は以下の様になっています。

file_id = "SERIAL_TWE"

--[[

SERIAL_TWE スクリプト起動時に渡される追加パラメータ
---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
COMPort			イベントを送信したシリアルデバイスの COMポート名		"COM10"

Title			イベントを送信したシリアルデバイスのタイトル名			"TWE-Lite PAN#1"
				タイトルが設定されていない場合にはこのパラメータは
				設定されません

TWE_DATA		COM ポートから入力されたアスキー形式のデータパケット全体を格納しています。

	値の例

	タイプ(1)	":01890902010A010203040405060708092F"
	タイプ(2)	"::rc=80000000:lq=84:ct=0001:ed=81002A1E:id=1:ba=3320:a1=0009:a2=0009:p0=001:p1=000"
	タイプ(3)	";116;00000000;054;001;1002ecd;3330;0007;0042;0007;0007;S;"
	タイプ(4)	"!INF TOCOS TWELITE DIP APP V1-00-2, SID=0x81000038, LID=0x78"
	タイプ(5)	"*** Samp_Monitor (Parent) 1.03-3 *** Title = TWE-Zero"

	文字列は、下記の何れかのデータで終端されたものです。
	ヌル文字(0x00),CR(0x0D),LF(0x0A),CR-LF(0x0D,0x0A)
	TWE_DATA パラメータには、終端文字を含まない文字列部分が格納されています

TWE_DATAを解析してこのスクリプト中で作成されるテーブルと文字列変数
---------------------------------------------------------------------------------
テーブルまたは文字列変数名		説明									値の例
---------------------------------------------------------------------------------
byte_arr	TWE_DATA が ":" 1文字から始まっている場合に、16進数文字列部分を
			1バイト毎に数値に変換して配列に格納したものが入る
			上記 TWE_DATA データ例タイプ(1)を参照

			byte_arr[1] = 0x01
			byte_arr[2] = 0x89
			byte_arr[3] = 0x09
			..

key_val		TWE_DATA が "::" 2文字から始まっている場合に、続く ":" 文字毎にカラムを
			分けて "<key>=<val>" で記述された部分を連想配列に格納したものが入る。
			上記 TWE_DATA データ例タイプ(2)を参照

			key_val["rc"] = "80000000"
			key_val["lq"] = "84"
			key_val["ct"] = "0001"
			key_val["ed"] = "81002A1E"
			..

tag_arr		TWE_DATA が ";" 1文字から始まっている場合に、続く ";" 文字毎にカラムを
			分けたものを文字列形式で配列に格納したものが入る。
			上記 TWE_DATA データ例タイプ(3)を参照

			tag_arr[1] = "116"
			tag_arr[2] = "00000000"
			tag_arr[3] = "054"
			tag_arr[4] = "001"
			..

comment		TWE_DATA が ":"または ";" 文字以外から始まっている場合に、
			データパケット全体の文字列を格納したものが入る
			上記 TWE_DATA データ例タイプ(4),(5)を参照

			COMMENT = "!INF TOCOS TWELITE DIP APP V1-00-2, SID=0x81000038, LID=0x78"

TWE_DATAを解析してこのスクリプト中で作成されるグローバル共有変数と共有文字列リスト
---------------------------------------------------------------------------------
グローバル共有変数名						説明
または、共有文字列リストChannel名
---------------------------------------------------------------------------------

TWE_<COMPort>_<ChildID>

			TWE_DATA が ":" 1文字から始まっていて、かつコマンド種別を示すバイト値が
			0x81(状態通知)の場合に、メッセージ内容を解析した値がカンマ区切りで
			グローバル共有変数に格納される。この変数の値は常に最後のイベント発生時
			の内容で更新されます。

			<COMPort>は、イベントを送信したシリアルデバイスの COMポート名になります
			<ChildID>は、メッセージ中の送信元論理デバイスIDを10進数にしたものになります

			変数の内容に設定されるカンマ区切りの文字列は下記のフォーマットになります。

			<LQI>,<Batt>,<DI1>,<DI2>,<DI3>,<DI4>,<AD1>,<AD2>,<AD3>,<AD4>

			<LQI> にはLQI値フィールドの値を10進数に変換したものが入ります
			<Batt> には電源電圧[mV]フィールドの値を10進数に変換したものが入ります
			<DI1>..<DI4> にはDI の状態ビットが Lowの場合に1, High の場合に 0 が入ります
			<AD1>..<AD4> にはAD変換値の値を10進数に変換したものが入ります

TWE_<COMPort>_CHILD_LIST (共有文字列リストChannel名)
			TWE_DATA が ":" 1文字から始まっているパケットデータを受信したときの
			<ChildID> 部分を文字列リストに保存します。文字列リストにはメッセージ中の
			送信元論理デバイスIDを10進数表現にしたものを重複なく保存しています。

]]

local str = ""
for key,val in pairs(g_params) do
	str = str .. key .. " = " .. val .. " "
end
log_msg(str,file_id)

------------------------------------------------------------
-- g_params["TWE_DATA"] データパケット文字列をデコードする
------------------------------------------------------------
local data = g_params["TWE_DATA"]
local byte_arr,key_val,tag_arr,comment
if string.match(data,"^::") then		-- タイプ(2)
	key_val = key_val_to_tbl(string.sub(data,3,-1))
elseif string.match(data,"^:%x") then	-- タイプ(1)
	byte_arr = hex_to_tbl(string.sub(data,2,-1))
	local id = tostring(byte_arr[1])
	if not add_shared_strlist("TWE_" .. g_params["COMPort"] .. "_CHILD_LIST",id,true) then error() end -- 子機のIDリストを更新
	if byte_arr[2] == 0x81 then -- 状態通知の場合には共有変数に現在のセンサ値を保存
		local di1,di2,di3,di4,ad1,ad2,ad3,ad4
		if bit_and(byte_arr[17],0x01) ~= 0 then di1 = "1" else di1 = "0" end
		if bit_and(byte_arr[17],0x02) ~= 0 then di2 = "1" else di2 = "0" end
		if bit_and(byte_arr[17],0x04) ~= 0 then di3 = "1" else di3 = "0" end
		if bit_and(byte_arr[17],0x08) ~= 0 then di4 = "1" else di4 = "0" end
		if byte_arr[19] == 0xFF then ad1 = "-1" else ad1 = tostring((byte_arr[19]*4 + bit_and(byte_arr[23],0x03))*4) end
		if byte_arr[20] == 0xFF then ad2 = "-1" else ad2 = tostring((byte_arr[20]*4 + bit_and(bit_rshift(byte_arr[23],2),0x03))*4) end
		if byte_arr[21] == 0xFF then ad3 = "-1" else ad3 = tostring((byte_arr[21]*4 + bit_and(bit_rshift(byte_arr[23],4),0x03))*4) end
		if byte_arr[22] == 0xFF then ad4 = "-1" else ad4 = tostring((byte_arr[22]*4 + bit_and(bit_rshift(byte_arr[23],6),0x03))*4) end
		local val = tostring(byte_arr[5]) .. "," .. tostring(byte_arr[14]*256 + byte_arr[15])
				.. "," .. di1 .. "," .. di2 .. "," .. di3 .. "," .. di4
				.. "," .. ad1 .. "," .. ad2 .. "," .. ad3 .. "," .. ad4
		if not set_shared_data("TWE_" .. g_params["COMPort"] .. "_" .. id,val) then error() end
		-- リレーサーバーに最新データの配信を依頼する場合には下記のコメントを取り除く
		script_exec("RELAY_SERVER_UPLINK","COM,TYPE,NodeID,LQI,Batt,DI1,DI2,DI3,DI4,AD1,AD2,AD3,AD4",g_params["COMPort"] .. ",TWE_UPDATE," .. id .. "," .. val)
	end
elseif string.match(data,"^;") then		-- タイプ(3)
	tag_arr = ssv_to_tbl(string.sub(data,2,-2))
else									-- タイプ(4),(5)
	comment = data
end

-----------------------------------
-- デコード後のデータをログに出力
-----------------------------------
--[[
local line = ""
if key_val then
	for key,val in pairs(key_val) do
		line = line .. "[" .. key .."]" .. val .. " "
	end
end
if byte_arr then
	for key,val in ipairs(byte_arr) do
		line = line .. "0x" .. bit_tohex(val,2) .. " "
	end
end
if tag_arr then
	for key,val in ipairs(tag_arr) do
		line = line .. "[" .. tostring(key) .. "]" .. val .. " "
	end
end
if comment then
	line = comment
end
log_msg("decoded: " .. line,file_id)
]]

このスクリプトでは TWE-Lite のファームエア (TWE-Zeroアプリ)で使用されている全てのアスキー形式のパケットを解析して、DeviceServer で処理しやすい形に変換しています。

他のイベントハンドラやユーザースクリプトから TWE-Lite のイベントデータを利用し易い様に、グローバル共有変数に子機毎の最新のI/O 値などをカンマ区切りのデータで保持しています。そのほかにも親機のCOM ポート毎に、全ての子機のID リストを作成しています。

これらの詳しいデータ構造は上記スクリプトのコメントを参照してください。ユーザーが独自のファームエアを TWE-Lite に搭載した場合でも、TWE-Zero アプリで使用しているアスキー形式のフォーマットに合わせておくと、このイベントハンドラで自動的に内容が解析されるようになります。

もちろん独自のフォーマットでデータを送信することもできます。この場合にはこのイベントハンドラにフォーマットを解析するためのスクリプトを追加して対応してください。

Webアプリを作成する

ここからはWebアプリの作成例を紹介します。Webブラウザ上に表示したチェックボックスやスライダー を使用して子機をリモートコントロールできます。HTML5 アプリで作成していますのでPC の Webブラウザはもちろんのこと、iOS や Android デバイスからも実行することができます。

この Webアプリではフレームワークとして jQuery と jQuery-mobile を使用しています。もちろん、これら以外のフレームワークを利用して独自のWeb アプリを自由に作成することができます。Web アプリ中からは、前の項で紹介したWeb API をコールして TWE-Lite デバイスを操作しています。

ここで紹介するWeb アプリは、DeviceServer インストール時に”C:\Program Files (x86)\AllBlueSystem\WebRoot\web_api_sample\twe_control” フォルダに格納されていますので直ぐに使用することができます。Webアプリを起動させるときは、Webブラウザで “http://<DeviceServer PC ホスト>/web_api_sample/twe_control/index.html” にアクセスしてください。

最初にログイン画面が表示されます。DeviceServer に登録したユーザーアカウントでログインします。このとき、ユーザーアカウントに設定したアプリケーション許可フラグに “WebLogin” を付与して Web API 経由でログイン可能にしておいてください。

ログインに成功すると、COM ポートの選択画面が表示されます。ここで、操作したいTWE-Lite の子機が所属している親機の COM ポートを選択します。ここで表示しているCOM デバイスリストにはシリアルデバイスタイプとして “TWE” を選択しているものだけが表示されます。

上記の画面で表示されている親機のポート(TWE#1) は1つだけですが、複数の親機を DeviceServer で同時に管理してすることもできます。このようにすると、それぞれの親機に属している TWE-Lite デバイスの Application ID を分けて、別々の PAN に属している TWE-Lite デバイスを Web アプリから操作することができます。

次にノード(子機の Device ID)選択画面が表示されます。子機から送信されてきたイベントデータパケットの情報から作成した最新のノードリスト(子機の Device ID リスト)を表示しています。

子機のノードを選択すると I/O 画面が表示されます。

デジタル入力と A/D 変換値は最後に親機で受信したイベントデータを元に表示されます。この部分は表示のみで操作することはできません。スクロールダウンすると、デジタル出力と PWM 出力を操作する画面が現れます。

チェックボックスを操作すると、子機の指定したデジタル出力ポートの値を High または Low に設定できます。”全てのDO#を更新” ボタンはデジタル出力ポートの全ピンの値を現在のチェックボックスの状態に合わせて全て更新することができます。

このボタンは TWE-Lite のファームウエアの制限から現在の出力ポートの値が取得できないため、最初にこのボタンを押してチェックボックスの状態と子機のデジタル出力の値を一致させるときに使用します。この部分については後の”考察”の項でも説明しています。

子機のPWM 出力は、スライダーを操作することで簡単に設定することができます。

別の子機を操作したくなった場合にはブラウザの “戻る” ボタンを使用してノード選択画面やポート選択画面に移動して選択するデバイスを変更できます。

下記にこの Webアプリで使用している HTML ファイルを載せます。HTMLファイルではWebアプリの GUI やダイアログの定義のみを行っています。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>TWE Control</title>
		<link rel="stylesheet" href="libs/css/themes/default/jquery.mobile-1.4.5.min.css" />
		<script src="libs/js/jquery.js"></script>
		<script src="libs/js/jquery.mobile-1.4.5.js"></script>
		<script src="libs/js/socket.io.js"></script>
		<script src="libs/device_server/webapi.js"></script>
	</head>
	<body>

		<div data-role="page" id="login">
			<div data-role="header" data-position="inline">
				<h3>ユーザー認証</h3>
			</div>
			<div role="main" class="ui-content">
				<label for="login_name">Name</label>
				<input id="login_name" value="" type="text" data-clear-btn="true"/>
				<label for="login_password">Password</label>
				<input id="login_password" value="" type="password" data-clear-btn="true"/>
				<div><h3>&nbsp</h3></div>
				<div class="ui-grid-b">
					<div class="ui-block-b">
							<a class="ui-btn ui-btn-inline ui-icon-check ui-btn-icon-left " id="login_btn" >Login</a>
					</div>
				</div>
			</div>
			<div data-role="footer">
				<h3>ABS-9000 DeviceServer</h3>
			</div>
		</div>

		<div data-role="page" id="select_device">
			<div data-role="header" data-position="inline">
				<h3>ポート選択</h3>
				<a data-icon="home" id="logout_btn" href="#logout_caution" data-rel="dialog" data-transition="pop">Logout</a>
			</div>
			<ul data-role="listview" id="device_list" data-inset="true"></ul>
			<div data-role="footer">
				<h3>ABS-9000 DeviceServer</h3>
			</div>
		</div>

		<div data-role="page" id="select_node"  data-theme="a">
			<div data-role="header" data-position="inline">
				<a data-icon="back" id="select_node_prev_btn">戻る</a>
				<h3>ノード選択</h3>
				<a data-icon="home" id="logout_btn" href="#logout_caution" data-rel="dialog" data-transition="pop">Logout</a>
			</div>

			<ul data-role="listview" id="node_list" data-inset="true"></ul>
			<div data-role="footer">
				<h3>ABS-9000 DeviceServer</h3>
			</div>
		</div>

		<div data-role="page" id="twe_control">
			<div data-role="header" data-position="inline">
				<a data-icon="refresh" id="reloadBtn">Reload</a>
				<h3 id="main_title">TWE Control</h3>
				<a data-icon="home" id="logout_btn" href="#logout_caution" data-rel="dialog" data-transition="pop">Logout</a>
			</div>
            <div role="main" class="ui-content">
                <div data-role="fieldcontain">
                    <fieldset data-role="controlgroup" data-type="vertical">
                        <input name="checkbox1" id="checkbox1" type="checkbox" class="di_pin" pin="1" disabled="disabled"/>
                        <label for="checkbox1">DI#1</label>
                        <input name="checkbox2" id="checkbox2" type="checkbox" class="di_pin" pin="2" disabled="disabled"/>
                        <label for="checkbox2">DI#2</label>
                        <input name="checkbox3" id="checkbox3" type="checkbox" class="di_pin" pin="3" disabled="disabled"/>
                        <label for="checkbox3">DI#3</label>
                        <input name="checkbox4" id="checkbox4" type="checkbox" class="di_pin" pin="4" disabled="disabled"/>
                        <label for="checkbox4">DI#4</label>
                    </fieldset>
                </div>
            </div>
            <div role="main" class="ui-content">
				<fieldset data-role="controlgroup" data-type="vertical">
					<div data-role="fieldcontain">
						<label for="slider-1">AD#1</label>
						<input type="range" name="slider-1" id="slider-1" style="width : 58px;" value="-1" min="-1" max="1024" data-highlight="true" disabled="disabled"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-2">AD#2</label>
						<input type="range" name="slider-2" id="slider-2" style="width : 58px;" value="-1" min="-1" max="1024" data-highlight="true" disabled="disabled"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-3">AD#3</label>
						<input type="range" name="slider-3" id="slider-3" style="width : 58px;" value="-1" min="-1" max="1024" data-highlight="true" disabled="disabled"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-4">AD#4</label>
						<input type="range" name="slider-4" id="slider-4" style="width : 58px;" value="-1" min="-1" max="1024" data-highlight="true" disabled="disabled"/>
					</div>
				</fieldset>
			</div>
            <div role="main" class="ui-content">
                <fieldset data-role="controlgroup" data-type="vertical">
						<a class="ui-btn ui-btn-inline ui-icon-gear ui-btn-icon-left ui-mini" id="do_all_btn" >全ての DO# 更新</a>
                </fieldset>
                <fieldset data-role="controlgroup" data-type="vertical">
	                <div data-role="fieldcontain">
                        <input name="checkbox5" id="checkbox5" type="checkbox" class="do_pin" pin="1"/>
                        <label for="checkbox5">DO#1</label>
                        <input name="checkbox6" id="checkbox6" type="checkbox" class="do_pin" pin="2"/>
                        <label for="checkbox6">DO#2</label>
                        <input name="checkbox7" id="checkbox7" type="checkbox" class="do_pin" pin="3"/>
                        <label for="checkbox7">DO#3</label>
                        <input name="checkbox8" id="checkbox8" type="checkbox" class="do_pin" pin="4"/>
                        <label for="checkbox8">DO#4</label>
                	</div>
                </fieldset>
            </div>
            <div role="main" class="ui-content">
				<fieldset data-role="controlgroup" data-type="vertical">
					<a class="ui-btn ui-btn-inline ui-icon-gear ui-btn-icon-left ui-mini" id="pwm_all_btn" >全ての PWM# 更新</a>
					<div class="ui-field-contain">
						<label for="slider-5">PWM#1</label>
						<input type="range" name="slider-5" id="slider-5" style="width : 58px;" value="0" min="0" max="1024" class="pwm_pin" pin="1" data-highlight="true"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-6">PWM#2</label>
						<input type="range" name="slider-6" id="slider-6" style="width : 58px;" value="0" min="0" max="1024" class="pwm_pin" pin="2" data-highlight="true"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-7">PWM#3</label>
						<input type="range" name="slider-7" id="slider-7" style="width : 58px;" value="0" min="0" max="1024" class="pwm_pin" pin="3" data-highlight="true"/>
					</div>
					<div data-role="fieldcontain">
						<label for="slider-8">PWM#4</label>
						<input type="range" name="slider-8" id="slider-8" style="width : 58px;" value="0" min="0" max="1024" class="pwm_pin" pin="4" data-highlight="true"/>
					</div>
				</fieldset>
			</div>
			<div data-role="footer">
				<h3>ABS-9000 DeviceServer</h3>
			</div>
		</div>

		<div data-role="page" id="login_error_dialog">
			<div data-role="header" data-theme="b">
				<h1>*LOGIN ERR*</h1>
			</div>
			<div role="main" class="ui-content">
				<h2>ログインに失敗しました</h2>
				<p>ユーザー名またはパスワードが間違っています。システムのログイン制限により失敗している場合があります</p>
				<p>
					<a href="#login" data-rel="back" class="ui-btn ui-shadow ui-btn-a ui-icon-check ui-btn-icon-left">戻る</a>
				</p>
			</div>
		</div>

		<div data-role="page" id="logout_caution">
			<div data-role="header" data-theme="b">
				<h1>*WARNING*</h1>
			</div>
			<div role="main" class="ui-content">
				<h2>ログアウトしますか?</h2>
				<p>ログアウト操作を行う場合には "OK" を押してください。"キャンセル" で元の画面に戻ります</p>
				<p><a data-rel="back" class="ui-btn ui-btn-inline ui-shadow ui-btn-a ui-icon-back ui-btn-icon-left">キャンセル</a>
				   <a id="logout_ok_btn" class="ui-btn ui-btn-inline ui-shadow ui-btn-a ui-icon-check ui-btn-icon-left">OK</a></p>
			</div>
		</div>

		<div data-role="page" id="error_quit_dialog">
			<div data-role="header" data-theme="b">
				<h1>*USER ERR*</h1>
			</div>
			<div role="main" class="ui-content">
				<h2>セッションが無効です</h2>
				<p>サーバー処理中にエラーが発生しました。現在のセッションが無効になっている場合があります。再ログイン操作を行ってください</p>
				<p><a data-role="button" id="server_error_ok_btn" class="ui-btn ui-shadow ui-btn-a ui-icon-check ui-btn-icon-left">OK</a></p>
			</div>
		</div>

		<div data-role="page" id="error_back_dialog">
			<div data-role="header" data-theme="b">
				<h1>*SERVER ERR*</h1>
			</div>
			<div role="main" class="ui-content">
				<h3>サーバー側でエラーが発生しました</h3>
				<p>サーバー処理中にエラーが発生しました。スクリプト実行中にエラーが発生した可能性がありますのでサーバー側のログを確認して下さい</p>
				<p><a data-role="button" data-rel="back" class="ui-btn ui-shadow ui-btn-a ui-icon-check ui-btn-icon-left">OK</a></p>
			</div>
		</div>

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

    </body>
</html>

Web アプリケーションのロジック部分は、下記のmain.js(JavaScript) で作成しています。このスクリプト中から、先に説明した Web API をコールすることで、TWE-Lite 子機を操作しています。

//////////////////////////////////////
// common function
//////////////////////////////////////

// スクリプト実行結果ステータスのみをチェック
function script_exec_callback(data){
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$("#login_password").val("");
			$("body").pagecontainer("change","#error_quit_dialog", { transition: "pop",role:"dialog" });
		} else {
			$("body").pagecontainer("change","#error_back_dialog", { transition: "pop",role:"dialog" });
		}
	}
}

// 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;
}

//////////////////////////////////////
// twe_control page
//////////////////////////////////////

// IO 値の配列を受け取って GUI表示に反映させる
// data パラメータ例
// {"DI1":"1","DI2":"0","DI3":"0","DI4":"0","AD1":"-1","AD2":"-1","AD3":"4","AD4":"248"}
function apply_ui(data){
	var	flag;
	flag = ( data.DI1 == "0"); // TWE DIは負論理だが、チェック状態の時に High出力とみなす
	$("#checkbox1").prop("checked",flag).checkboxradio("refresh");
	flag = ( data.DI2 == "0");
	$("#checkbox2").prop("checked",flag).checkboxradio("refresh");
	flag = ( data.DI3 == "0");
	$("#checkbox3").prop("checked",flag).checkboxradio("refresh");
	flag = ( data.DI4 == "0");
	$("#checkbox4").prop("checked",flag).checkboxradio("refresh");

	if ($("#slider-1").val() != data.AD1) $("#slider-1").val(data.AD1).slider("refresh");
	if ($("#slider-2").val() != data.AD2) $("#slider-2").val(data.AD2).slider("refresh");
	if ($("#slider-3").val() != data.AD3) $("#slider-3").val(data.AD3).slider("refresh");
	if ($("#slider-4").val() != data.AD4) $("#slider-4").val(data.AD4).slider("refresh");
}

// TWE/AD_DI_GETスクリプト実行結果のイベントハンドラ。
function io_get_handler(data){
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$("#login_password").val("");
			$.mobile.changePage( "#error_quit_dialog", {transition: "pop",role:"dialog"});
		} else {
			$.mobile.changePage( "#error_back_dialog", {transition: "pop",role:"dialog"});
		}
		return;
	}
	apply_ui(data.ResultParams);
}

// デバイスから最新の I/O 値を取得する
function load_device_io(){
	var params = {};
	params["com"] = serial_comport;
	params["id"] = node_id;
	script_exec("TWE/AD_DI_GET",params,"io_get_handler");
}

// スライダーを操作して PWM#<n> ピンの値が更新された
$("body").on("slidestop",".pwm_pin",function(event, ui){
	var params = {};
	var attrVal = getAttrVal(this,"pin");
	if (attrVal != ""){
		params["com"] = serial_comport;
		params["id"] = node_id;
		params["pin"] = attrVal;
		params["value"] = event.target.value;
		script_exec("TWE/PWM_SET",params,"script_exec_callback");
	}
});

// チェックボックスを操作して DO#<n> ピンの値が更新された
$("body").on("change",".do_pin",function(event, ui){
	var params = {};
	var attrVal = getAttrVal(this,"pin");
	if (attrVal != ""){
		params["com"] = serial_comport;
		params["id"] = node_id;
		params["pin"] = attrVal;
		// TWE DOは負論理だが、チェック状態の時に High出力にする
		if (this.checked){
			params["value"] = "0";
		} else {
			params["value"] = "1";
		}
		script_exec("TWE/DO_PIN_SET",params,"script_exec_callback");
	}
});

// 全ての PWM# 更新ボタンを押した
$("#pwm_all_btn").on( "click",function(event, ui){
	var params = {};
	var pwm_val = $("#slider-5").val();
	pwm_val = pwm_val + "," + $("#slider-6").val();
	pwm_val = pwm_val + "," + $("#slider-7").val();
	pwm_val = pwm_val + "," + $("#slider-8").val();
	params["com"] = serial_comport;
	params["id"] = node_id;
	params["value"] = pwm_val;
	script_exec("TWE/PWM_ALL_SET",params,"script_exec_callback");
});

// 全ての DO# 更新ボタンを押した
$("#do_all_btn").on( "click",function(event, ui){
	var params = {};
	var flag = 0;
	if (! $("#checkbox5").is(':checked')){ flag = flag | 0x01; }
	if (! $("#checkbox6").is(':checked')){ flag = flag | 0x02; }
	if (! $("#checkbox7").is(':checked')){ flag = flag | 0x04; }
	if (! $("#checkbox8").is(':checked')){ flag = flag | 0x08; }
	params["com"] = serial_comport;
	params["id"] = node_id;
	params["value"] = flag.toString(16);
	script_exec("TWE/DO_PUT",params,"script_exec_callback");
});

// ページが表示された
$(document).on("pageshow","#twe_control",function(event){
	load_device_io();
});

// リロードボタンが操作された
$("#reloadBtn").on( "click",function(event, ui){
	load_device_io();
});

//////////////////////////////////////
// select_node page
//////////////////////////////////////

// アプリで選択した TWEデバイス NodeID
var node_id = ""

// TWE(子)ノードリストを取得する
function list_node_device(){
	var params = {};
	params["noquote"] = "1";
	params["com"] = serial_comport;
	script_exec("TWE/LIST_JSON",params,"list_node_handler");
}

// ノードデバイスが選択されたらデバイス操作画面に移動する
$(document).on( "click",".node_select", function(event, ui){
	var params = {};
	var attrVal = getAttrVal(this,"device");
	if (attrVal != ""){
		node_id = attrVal;
		// TWEデバイス操作画面に移動する
		$( "#main_title" ).text(serial_comport + " Node#" + node_id);
		$("body").pagecontainer("change","#twe_control", { transition: "none" });
	}
});

// TWE/LIST_JSON スクリプト実行結果のイベントハンドラ。
function list_node_handler(data){
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$("body").pagecontainer("change","#error_quit_dialog", { transition: "pop",role:"dialog" });
		} else {
			$("body").pagecontainer("change","#error_back_dialog", { transition: "pop",role:"dialog" });
		}
		return;
	}
	// リストビューに ノード番号を表示する
	$("#node_list li").remove();
	for(i in data.ResultParams.DeviceList){
		$('<li data-theme="a"><a data-transition="none" device="' + data.ResultParams.DeviceList[i].ID + '" class="node_select">' +
            "TWE Node#" + data.ResultParams.DeviceList[i].ID + '</a></li>').appendTo($('#node_list'));
	}
	$("#node_list").listview("refresh");
}

// ページが表示された
$(document).on("pageshow","#select_node",function(event){
	list_node_device();
});

// ノード選択画面の Prevボタンが操作された
$("#select_node_prev_btn").on( "click",function(event, ui){
	$("body").pagecontainer("change","#select_device", { transition: "none" });
});

//////////////////////////////////////
// select_device page
//////////////////////////////////////

// アプリで選択したシリアルデバイスの COM ポート名またはタイトル名
var serial_comport = ""

// シリアルデバイスリストを取得する
function list_serial_device(){
	var params = {};
	params["type"] = "TWE";
	params["noquote"] = "1";
	script_exec("SERIAL_DEVICE_LIST",params,"list_serial_handler");
}

// SERIAL_DEVICE_LIST スクリプト実行結果のイベントハンドラ。
function list_serial_handler(data){
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$("body").pagecontainer("change","#error_quit_dialog", { transition: "pop",role:"dialog" });
		} else {
			$("body").pagecontainer("change","#error_back_dialog", { transition: "pop",role:"dialog" });
		}
		return;
	}
	// リストビューに シリアルデバイス名を表示する
	$("#device_list li").remove();
	for(i in data.ResultParams.DeviceList){
		$('<li data-theme="a"><a data-transition="none" device="' + data.ResultParams.DeviceList[i].COM + '" class="dev_select">' +
            data.ResultParams.DeviceList[i].Title + '</a></li>').appendTo($('#device_list'));
	}
	$("#device_list").listview("refresh");
}

// シリアルデバイスが選択されたらノード選択画面に移動する
$(document).on('click', '.dev_select', function () {
	var params = {};
	var attrVal = getAttrVal(this,"device");
	if (attrVal != ""){
		serial_comport = attrVal;
		$("body").pagecontainer("change","#select_node", { transition: "none" });
	}
});

// ページが表示された
$(document).on("pageshow","#select_device",function(event){
	list_serial_device();
});

//////////////////////////////////////
// login page
//////////////////////////////////////

// サーバー側のログイン処理が成功したときにメイン画面に移動する
function login_callback(data){
	if (data.Result == "Success"){
		session_token = data.SessionToken;
		$("body").pagecontainer("change","#select_device", { transition: "none" });
	} else {
		$("body").pagecontainer("change","#login_error_dialog", { transition: "pop",role:"dialog" });
	}
}

// サーバー側のログアウト処理が完了したらログイン画面に戻る
function logout_callback(data){
	session_token = "";
	$("#login_password").val("");

	$("body").pagecontainer("change","#login", { transition: "pop" });
}

// ログインボタンを押した
$( "#login_btn" ).on( "click", function(event, ui){
	var user = $("#login_name").val();
	var pass = $("#login_password").val();
	login(user,pass,"login_callback");
});

// ログアウトボタンを押した
$( "#logout_ok_btn" ).on( "click", function(event, ui){
	logout("logout_callback");
});

// サーバーエラーのダイアログから復帰する場合はログイン画面に戻る
$( "#server_error_ok_btn" ).on( "click", function(event, ui){
	session_token = "";
	$("body").pagecontainer("change","#login", { transition: "pop" });
});

// ログインページが表示された
$(document).on("pageshow","#login",function(event){
	// セッショントークンが指定されている場合にはユーザー認証を省略する
	if (session_token != ""){
		$("body").pagecontainer("change","#select_device", { transition: "pop" });
	}
});

////////////////////////////////////////////////////////////////
// (オプション機能)
//
// デバイスの I/O 状態が変更された場合に最新のI/O データが配信されるように、
// WebSocket(socket.io) でリレーサーバーに接続する
// この機能を使用する場合には、サーバー側でデバイスからのイベントデータを
// ブラウザに配信する node.js + socket.io で作成されたリレーサーバーを
// 実行して下さい。(node relay_server.jsで実行できます)
//
//
//var socket = io.connect('/',{ port:9090});
var socket = io.connect(':9090');

// リレーサーバーから 'broadcast' タグの付いたメッセージを受信した
socket.on('broadcast', function (data) {
	// 配信されてきたJSON データを取得
	var obj = $.parseJSON(data["message"]);

	// Webアプリで自動更新の対象となるイベントデータを含んでいた場合には、
	// 最新のイベントデータで GUI の状態を更新する
	if((session_token != "") && (obj.COM == serial_comport) && (obj.NodeID == node_id) && (obj.TYPE == "TWE_UPDATE")){
		apply_ui(obj);
	}
});

リアルタイム更新用のリレーサーバーを node.js で作成する

Web アプリを使用するとリモート側の デジタル出力 や PWM をリアルタイムに更新することができますが、デジタル入力と A/D 変換値は “Reload” ボタンを押さないと最新の情報に更新できないのは困ります。

ここでは、WebSocket を利用して子機側でデジタル入力や A/D 変換値が変化したときに、自動的に最新の値に Web アプリの GUI を更新させたいと思います。

この項で紹介する方法は以前の記事で紹介した内容と同じ node.js を使用してリレーサーバーを外部に作成する方法を採っています。リレーサーバーの仕組みや node.js のセットアップ方法については此方の(記事1,記事2) を参照してください。

親機からは、子機側で発生した I/O 変化時のイベントデータがシリアルに出力されていますので、このタイミングでイベントデータを WebSocket に配信します。先に紹介したイベントハンドラ (SERIAL_TWE) 中の

-- リレーサーバーに最新データの配信を依頼する場合には下記のコメントを取り除く
script_exec("RELAY_SERVER_UPLINK","COM,TYPE,NodeID,LQI,Batt,DI1,DI2,DI3,DI4,AD1,AD2,AD3,AD4",g_params["COMPort"] .. ",TWE_UPDATE," .. id .. "," .. val)

の部分で RELAY_SERVER_UPLINK Lua スクリプトに最新のイベントデータを渡しています。このスクリプトは以下の様になっています。

file_id = "RELAY_SERVER_UPLINK"

--[[

●機能概要

リレーサーバーに配信用のデータをアップロードする

●スクリプト中の変数に初期設定が必要な項目

---------------------------------------------------------------------------------
変数名			値		            									値の例
---------------------------------------------------------------------------------
host			リレーサーバーが動作している PC のホスト名				"localhost"
				リレーサーバーは node.js プログラムで起動された
				relay_server.js ファイルで実行されます。

port			リレーサーバーのポート番号								9080

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
<RelayServerParamKey#1>	<RelayServerParamValue#1>				"AD0" "-1"
..
..
<RelayServerParamKey#n>	<RelayServerParamValue#n>

リレーサーバーで配信するメッセージ中に格納するデータ値を任意の数だけ指定できる。
このスクリプトを起動する時に指定した全てのパラメータ(キー値と値)はそのまま
リレーサーバーに渡されて、その後 WebSocketで接続している全てのクライアントに配信される。

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

スクリプト中で実行している tcp_send_recv_data() 関数は、相手側のサーバーがダウン
しているときにネットワークエラー検出までに時間がかかることが想定されます。
このとき、イベントハンドラなど別スレッドで同時にこのスクリプトを続けて起動すると、
スクリプトプールの未使用エントリ数がなくなり、システム全体の動作に影響する可能性
があります。これを防ぐために、スクリプトプールの未使用エントリ数が一定以下になっ
た場合には、このスクリプトの実行をすぐに中止させるようにしています

中止させるためのフラグは abort_key のキー名で設定されたグローバル共有変数を使用
しています。一度このフラグが設定されるとこのスクリプトは直ぐに終了して、ログに
*ABORT* が記録されるようになります。再びこのスクリプトを実行可能にするためには、
DeviceServerを再起動させるか abort_key に設定されたグローバル共有変数を削除して
ください。このとき PERIODIC_TIMER.lua 中に dec_shared_data() 関数を使用すると
簡単に一定期間後にグローバル共有変数を削除することができます。このスクリプトでは
中止フラグの値に "60" を設定していますので、下記のスクリプトをPERIODIC_TIMER.lua 中
に記述すると、60分後に中止フラグが自動的に削除されます。

dec_shared_data("$ABORT_RELAY_SERVER_UPLINK")

●変更履歴

2014/11/8	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

local host = "localhost"
local port = 9080

-----------------------------------------------------------------------------------------------------
-- リレーサーバー接続が失敗する場合にスクリプトプールの逼迫を防ぐため、スクリプト実行中止の判断をする
-----------------------------------------------------------------------------------------------------
local abort_key = "$ABORT_" .. file_id
local stat, val = get_shared_data(abort_key)
if val ~= "" then
	log_msg("*ABORT*",file_id)
	return
end
if script_free_count() < 5 then
	if not set_shared_data(abort_key,"60") then error() end
	log_msg("*ABORT SET*",file_id)
	return
end

-----------------------------------------------------------------------------------
-- スクリプトパラメータで渡されたキーと値を JSON 文字列に変換する
-----------------------------------------------------------------------------------
local json_str = '{'
local first = true
for key,val in pairs(g_params) do
	if first then
		first = false
	else
		json_str = json_str .. ","
	end
	json_str = json_str .. string.format('"%s":"%s"',key,val)
end
json_str = json_str .. '}'

------------------------------------------
-- リレーサーバーにメッセージを送信する
------------------------------------------
log_msg(json_str,file_id)
local nstat,rdata = tcp_send_recv_data(host,port,json_str,2,2)

スクリプトパラメータで渡されてきた子機の最新の デジタル入力値、A/D 変換値などを node.js で作成するリレーサーバーにTCP/IP ソケット通信で送信します。送信データは相手側で利用しやすいように予め JSON フォーマットに加工しています。

node.js で作成するリレーサーバー本体(relay_server.js)は以下になります。

//////////////////////////////////////////////////////////////////////////////////////
//
// RelayServer                                             All Blue System
//
// このプログラムではソケット通信(TCP)で受信した文字列データを
// WebSocket(socket.io)に接続している全てのクライアントにブロードキャスト配信します。
//
// プログラムを起動するときは、予め node.js プログラムをインストールしておきます。
// その後、node.js で提供されているパッケージ管理プログラム npm を使用して
// socket.io モジュールをインストールします。
//
// >npm install socket.io
//
// 環境変数 NODE_PATH を node.js をインストールしたフォルダにある node_modules フォルダ名に設定します。
// デフォルトでは NODE_PATH = C:\Program Files\nodejs\node_modules になります。
//
// コマンドプロンプトから node コマンドにスクリプトファイル名をパラメータに指定して RelayServerを起動します。
//
// >node relay_server.js
//
// uplink ポートにメッセージを送信するための DeviceServer側スクリプトは
// C:\Program Files (x86)\AllBlueSystem\Scripts\RELAY_SERVER_UPLINK.lua に
// 格納されています。
//
// downlinkポートからメッセージを受信してWebアプリケーションからブラウザに配信するための
// サンプルは、C:\Program Files (x86)\AllBlueSystem\WebRoot\web_api_sample\twe_control
// フォルダ中の main.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 an ack
	    	uplink_stream.end();

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

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

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

子機からのイベントデータを受信受信するための uplink ポートをTCPソケットサーバーで作成しています。また、Web アプリ側に WebSocket で配信するための downlink ポートを socket.io ライブラリを使用して作成しています。

ここで使用している node.js のインストール方法と、socket.io モジュールの設置方法に関しては此方(記事1,記事2) を参照して下さい。

早速 node.js を使用してリレーサーバーを起動します。

リレーサーバーを記述した JavaScript ファイル(relay_server.js) を node コマンドで起動しています。

リレーサーバーが起動している状態で Web アプリを動作させると、自動的に WebSocket で配信されるメッセージをリレーサーバーから受信するようになります。

子機側のデジタル入力やA/D 値を変化させると、リレーサーバー側には以下の様にログメッセージが表示され、最新のI/O 値がブロードキャストされると同時に、Web アプリ側のデジタル入力のチェックボックスと A/D スライダーの値がリアルタイムに変化するようになります。

Web ソケットで配信された JSON データは Web アプリ(main.js) 内の後半部分に記述したイベントハンドラ部分で受信されて、GUI の更新を行っています。

また、リレーサーバーに子機のイベントデータを送信するためのスクリプト(RELAY_SERVER_UPLINK) の最初の部分で、ソケット通信エラーが発生する場合に備えて、DeviceServer のスクリプトプールの残りを常にチェックするようにしています。

これによって node.js で作成されたリレーサーバーがダウンした場合などに、DeviceServer 側のリソースが一時的に不足するのを防止しています。詳しくは前述の(RELAY_SERVER_UPLINK)スクリプト中のコメントをご覧ください。下記のログ画面は子機からのイベントを受信中にリレーサーバーを強制的に停止させたときの様子です。

ソケット通信を行っている tcp_send_recv_data()関数がエラーを検出するまでの間に、イベントデータが次々に到着している状態です。このとき、マルチスレッドで起動しているイベントハンドラがスクリプトプールの残りが少なくなってきているのを検出して、abort フラグを共有変数に設定しています(*ABORT SET* のログ部分)。

abort フラグが設定されると、それ以降はイベントハンドラは直ぐに終了するようになり、スクリプトプールの逼迫を防いでシステム全体の動作に影響が及ばないようにしています。

考察

TWE-Lite でデフォルトで動作しているファームウエアでは、現在の子機のデジタル出力と PWM 設定値を取得するための方法がありません。このため、Web アプリを操作するときに”望まない出力操作” を行う必要がでてきます。

また、デジタル出力や PWM 設定値を変更した場合にもイベントは送信されませんので、リレーサーバーを使用しても、複数の Web アプリの画面のデジタル出力や PWM スライダーの設定を同期させて最新の状態にすることができません。

サーバー側で擬似的に子機を操作したときのタイミングでイベントデータを出力することもできますが、DeviceServer に接続した親機を経由しないで操作された場合にはやはり検出することができません。

これらを解決するためには、既存のファームエアを一部改造して以下の機能を追加するのが望ましいと思います。

(1) デジタル出力とPWM 設定値を取得するためのコマンドとレスポンスパケットを追加

(2) デジタル出力とPWM 設定変更時に新規のイベントデータを作成するか、または既存のイベントデータ中にこれらの最新の状態を含めるようにする

幸いファームウエアのソースと開発環境は TWE-Lite のホームページで公開されていますので、追加するのも出来そうな気がします?

動作例

Web アプリを操作した時の動画を載せましたのでご覧ください。(音量注意)

ここで紹介した DeviceServer の機能は、ABS-9000 DeviceServer インストールキットをダウンロードして、直ぐに使用することができます。Web API で使用したスクリプトファイルや Webアプリのソースファイルも全て格納されていますので是非お試しください。デモライセンスが添付されていますので直ちに使用可能です。

それではまた。

リモート(XBee + CPUボード)から取得したセンサデータをWebアプリでグラフ表示

●概要

リモートセンサーノードからセンサーデータを収集して集計するアプリケーション例を前回までの2回の記事(クラウドサービスにセンサーデータを保管する例ローカルにデータを保管してバッチジョブで作成したCSV ファイルをエクセルで利用する例)に分けて紹介してきました。今回は サーバーPC に設置したWeb アプリケーションでセンサーデータをグラフ表示する例を紹介したいと思います。今回のシステム構成も前回までの記事と全く同様に、複数のリモートセンサーノードからセンサーデータを収集するシステムで動作させます。

リモートに設置した2つのセンサーノードから送信されてくる、温度・明るさ・赤外線の各センサデータをサーバーPC に保存して、任意のタイミングでWebブラウザからグラフを表示することができます。全体のシステム構成については、こちらの記事を参照してください。

クラウドサービスを利用したアプリケーションでも、センサデータのグラフ表示をすることが出来ましたが、今回は全てローカルPC(DeviceServer を設置したサーバーPC) のみで実現しています。クラウド等の外部のサービスを一切利用しませんので、セキュリティなどの要因で LAN 内のみでシステムを構築したい病院や工場などで利用する場合にも、Webアプリ版で作成する今回のアプリケーション例が参考になると思います。

●動作例

以下が今回作成する Webアプリケーションでグラフ表示した様子です。DeviceServer を設置したPC にアクセスできる端末であれば、OS の種類を問わず Webブラウザさえあれば何時でも直近のセンサーデータの傾向をグラフを表示したり、月次や週次のグラフを集計して表示することができます。

上の画面はWindows7(64bit)で動作している Firefox Web ブラウザでWebアプリを起動している様子です。

今回のアプリで紹介しているリモートセンサーからのセンサーデータ以外のあらゆるデータをグラフ化できるようにWebアプリを設計しています。DeviceServer の統計データベースに集計したいデータを登録しておくだけで、後から簡単にグラフを表示できるようになります。

●WebAPI で実行するサーバー側スクリプト(Lua)を作成する

今回作成する Webアプリは DeviceServer の Webサーバー機能を利用して配信する HTML ファイルと JavaScriptファイル、CSS ファイル等で作成されています。これらのファイルは Webブラウザ側でダウンロードされて実行されますが、これとは別に、JavaScript 中から WebAPI 経由で実行する DeviceServer 側(サーバーPC) のスクリプト(Lua)もいくつか準備しておきます。

ログイン認証やログアウト、スクリプト(Lua)実行などの基本的な WebAPI 機能はDeviceServerに予め用意されています。このスクリプト実行 WebAPI で指定する DeviceServer 側の Lua スクリプトを幾つか用意することで、サーバー側で実行する機能とブラウザ側で実行する機能を分離させることができます。

今回の Web アプリでは画面に表示する GUI コンポーネントや画面遷移、グラフ化の処理などは JavaScript で記述して Webブラウザ側で実行しています。統計データベースのキー名列挙、統計データベースの集計処理などは DeviceServer 側に用意する Lua スクリプトで実行します。Lua スクリプトでは Lua のライブラリ関数をコールして、処理の殆どが DeviceServer 内に記述しているネイティブコードで実行します。このような形式をとることで、処理スピードが向上して、エラー発生時のリカバリ処理なども確実に行えるようになります。

今回の Web アプリ用に新規に作成DeviceServer 側に用意する Lua スクリプトは以下の2種類あります。

(1) 統計データベースに格納しているキー名リストを取得するときの WebAPI で実行する Lua スクリプト

(2) 任意の期間のセンサーデータを集計して、結果を配列で取得するときの WebAPI で実行する Lua スクリプト

最初に(1) の WebAPI を実現するための Lua スクリプト(LIST_JSON.lua)を作成します。ファイルの内容は以下になります。このファイルは最新のDeviceServer インストールキットを使用すると、C:\Program Files (x86)\AllBlueSystem\Scripts\SUMMARY\LIST_JSON.lua に配置されます。

file_id = "SUMMARY/LIST_JSON"

--[[

●機能概要

統計データベースで使用中のキー名リストを取得する。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
prefix			キー名の部分文字列を指定する。前方一致でマッチしたキー名
				のみがリストに格納される。パラメータを省略した場合には全て
				のキー名が対象になる									"SENSOR_"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
KeyList			統計データベースで使用中のキー名リストがJSON フォーマット
				文字列で格納される

[
  {"Key":"<統計データベース登録時のキー#1>"},
  {"Key":"<統計データベース登録時のキー#2>"},
  ..
  ..
  {"Key":"<統計データベース登録時のキー#n>"}
]

値の例

[
 {"Key":"SENSOR_IR_Device4"},
 {"Key":"SENSOR_IR_Node1"},
 {"Key":"SENSOR_LUMI_Node1"},
 {"Key":"SENSOR_TP_Device4"},
 {"Key":"SENSOR_TP_Node1"}
]

●備考

Web API 経由でこのスクリプトを実行するときに、URL パラメータに noquote=1 を指定
して、リターンパラメータの値を直接 JSON オブジェクトとしてアクセスします。

JavaScriptから以下のURL をコールしてJSON リプライデータを受信します。

http://<hostname>:<port>/command/json/script?session=<session_token>&name=SUMMARY%2FLIST_JSON&noquote=1

取得した JSON データからデバイスリスト中の各データ項目にアクセスするときには下記の様な
JavaScript を記述します。このとき、data 変数には WebAPI で取得した JSON オブジェクトが格納されている
ものとします。

for(i in data.ResultParams.KeyList){
	..
	..  data.ResultParams.KeyList[i].Key ..
	..
}

●変更履歴

2014/07/13	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

-----------------------------------------------------
-- 統計データベースで使用中のキー名リストを取得
-----------------------------------------------------
local stat,keys
if g_params["prefix"] then
	stat,keys = key_list_stat_data(g_params["prefix"])
else
	stat,keys = key_list_stat_data()
end
if not stat then error() end

-----------------------------------------------------------------
-- キー名リストを JSON 文字列に変換
-----------------------------------------------------------------
local key_list = "["
local cnt = 0
for key,val in ipairs(keys) do
	if cnt ~= 0 then
		key_list = key_list .. ","
	end
	key_list = key_list .. '{"Key":"' .. val .. '"}'
	cnt = cnt + 1
end
key_list = key_list .. "]"

---------------------------------------------
-- デバイスリストをリターンパラメータに格納
---------------------------------------------
script_result(g_taskid,"KeyList",key_list)

このスクリプトでは、センサーデータを格納している統計データベースに存在する全てのキー名リストを JSON 文字列で返します。リクエストパラメータにキー名の一部分(前部)を指定すると一致するキー名のみを選択することもできます。

Web アプリで、グラフ表示の対象とするキー名をチェックボックスで選択するときに、このスクリプトを WebAPI で実行して選択肢のチェックボックスリストを表示します。

次に (2) の集計用のスクリプト(SUMMARY_DATA_JSON.lua) を作成します。ファイルの内容は以下になります。このファイルは最新のDeviceServer インストールキットを使用すると、C:\Program Files (x86)\AllBlueSystem\Scripts\SUMMARY\SUMMARY_DATA_JSON.lua に配置されます。

file_id = "SUMMARY_DATA_JSON"

--[[

●機能概要

統計データベースに保存されているデータを集計して JSON配列で取得する。
集計パラメータには日、週、月の範囲を指定できる。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

KeyList			集計対象とする統計データベース中のキー名リスト
				複数指定するときにはカンマで区切る
											"SENSOR_IR_Device4,SENSOR_IR_Node1"

TargetDate		集計対象開始日付(YYYY/MM/DD)							"2010/01/31"
				パラメータ省略時には Range パラメータの指定によってデフォルト
				日付が設定される。
				Range パラメータを省略または "day" または "month" 指定時には、
				現在日が集計対象開始日になる。
				Rangeパラメターが "week","2d","month" の場合には現在日から
				その日数期間前の日付が集計対象開始日になる。

TargetTime		集計対象開始時刻(HH:MM:SS)								"13:25:0"
				TargetDate, TargetTime 両方のパラメータ省略時には、
				現在の日付・時刻(秒部分は切り捨てて xx:xx:00 になる)から集計期間分前の
				日付時刻が設定される。
				TargetDate を指定して、TargetTime パラメータのみを省略した場合には
				"0:0:0" が設定される
				Range パラメータに 1日以上の期間を指定した場合には、このパラメータ
				の指定は無視されて常に "0:0:0" からの集計期間になる

Range			集計期間を指定する。下記の値が指定可能でパラメータ省略時には
				"day" が選択される

				"hour"
					TargetDate, TargetTime に指定した日付時刻から 1時間を 30秒単位
					に集計する

				"6h"
					TargetDate, TargetTime に指定した日付時刻から 6時間を 1分単位
					に集計する

				"day"
					TagetDate に指定した日付の 0:0:0 から 24:00:00 までの期間を
					5 分単位に集計する。

				"2d"
					TagetDate に指定した日付の 0:0:0 から次の日の 24:00:00 までの期間を
					10 分単位に集計する

				"week"
					TagetDate に指定した日付の 0:0:0 から 7日間を 1時間単位に集計する

				"month"
					TagetDate に指定した日付の同月 1日の 0:0:0 から月の最終日の
					24:00:00 までの期間を 4時間単位に集計する

Summary			集計単位ごとの期間で集計計算したときに使用する値を指定する
				パラメター省略時には "mean" が選択される				"mean"
				"mean"	平均値
				"total"	合計値

Interval		Range パラメータの指定によって予め決められた集計単位時間を変更して
				このパラメータで指定された秒を使用する。
				秒数で指定する。										"3600"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
SeriesList		集計結果を jqplot の データ配列パラメータで指定できるような
				フォーマットに変換した JSON 文字列。
				KeyList に指定したキーの数だけ集計結果が格納される。

				集計単位期間の開始時刻と集計値のペアからなる集計データ <item> が作成される
				複数の集計データを合わせて集計配列 <series> を構成する。
				KeyList に指定したキーごとに 集計配列 <series> が作成されて、それらを配列で
				まとめたものが SeriesList になる。

LabelList:		KeyList で指定された文字列を jqplot の series ラベル・オプションパラメータで
				指定できるようなフォーマットに変換した JSON 文字列。

●リターンパラメータフォーマット

SeriesList := [
	[<series#1>],
	[<series#2>],
	..
	[<series#n>]
]

series := [
	[<item#1],
	[<item#2],
	..
	[<item#n]
]

item := ["<集計単位の開始時刻", <集計値>]

SeriesList例:(2つのキー文字列を KeyList に指定した場合)

[
	[
		["2014/07/02 00:00:00",10],
		["2014/07/02 00:10:00",13],
		["2014/07/02 00:20:00",13.5],
		..
		["2014/07/02 23:50:00",20]
	],
	[
		["2014/07/02 00:00:00",12],
		["2014/07/02 00:10:00",12],
		["2014/07/02 00:20:00",12],
		..
		["2014/07/02 23:50:00",12.4]
	]
]

LabelList := [
	{label:"<key#1>"},
	{label:"<key#2>"},
	..
	{label:"<key#n>"}
]

●備考

集計単位ごとの期間内に統計データレコードが見つからなかった場合には、
その単位期間の集計結果レコードは SeriesList 中に入りません。

Web API 経由でこのスクリプトを実行するときに、URL パラメータに noquote=1 を指定
して、リターンパラメータの値を直接 JSON オブジェクトとしてアクセスします。

JavaScriptから以下のURL をコールしてJSON リプライデータを受信します。

http://<hostname>:<port>/command/json/script?session=<session_token>&name=SUMMARY%2FSUMMARY_DATA_JSON&noquote=1

●変更履歴

2014/08/01	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

----------------------------------------------------------------------
-- KeyList パラメータからキー文字列配列 keys 作成
----------------------------------------------------------------------
local keys
if g_params["KeyList"] then
    keys = csv_to_tbl(g_params["KeyList"])
else
	log_msg("parameter error",file_id)
	error()
end

----------------------------------------------------------------------
-- TargetDate パラメータが指定されていない場合にはスクリプトが起動
-- された日を集計対象開始日にして timestamp 変数に設定する。
-- Range パラメータが "day","2d","week","month" の場合に有効で、それ以外
-- の場合には timestamp は後で上書きされる
----------------------------------------------------------------------
local timestamp -- 一日以上の集計期間を指定する場合の開始日付時刻
local now = os.date "*t"
if g_params["TargetDate"] then
    timestamp = g_params["TargetDate"] .. " 0:0:0"
else
	timestamp = string.format("%4.4d/%2.2d/%2.2d 0:0:0",now["year"],now["month"],now["day"])
end
local interval,count,days
if not g_params["Range"] then g_params["Range"] = "day" end -- デフォルトは1日間の集計を行う

----------------------------------------------------------------------
-- Range, Interval パラメータから集計間隔と集計データ数を決定
----------------------------------------------------------------------

---------------
-- 1日間集計
---------------
if g_params["Range"] == "day" then
	days = 1
	if not g_params["Interval"] then
		interval = 300
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor((days * 24 * 3600)/interval)
end
---------------
-- 2日間集計
---------------
if g_params["Range"] == "2d" then
	if not g_params["TargetDate"] then -- 集計対象日が未指定の場合には現在日から 1日前に設定する
		local stat,y,m,d,h,min,s = str_to_datetime(timestamp)
		if not stat then error() end
		local stat,y,m,d = inc_day(-1,y,m,d)
		if not stat then error() end
		timestamp = string.format("%4.4d/%2.2d/%2.2d 0:0:0",y,m,d)
	end
	days = 2
	if not g_params["Interval"] then
		interval = 600
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor((days * 24 * 3600)/interval)
end
---------------
-- 1週間集計
---------------
if g_params["Range"] == "week" then
	if not g_params["TargetDate"] then -- 集計対象日が未指定の場合には現在日から 6日前に設定する
		local stat,y,m,d,h,min,s = str_to_datetime(timestamp)
		if not stat then error() end
		local stat,y,m,d = inc_day(-6,y,m,d)
		if not stat then error() end
		timestamp = string.format("%4.4d/%2.2d/%2.2d 0:0:0",y,m,d)
	end
	days = 7
	if not g_params["Interval"] then
		interval = 3600
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor((days * 24 * 3600)/interval)
end
---------------
-- 1月間集計
---------------
if g_params["Range"] == "month" then
	local stat,y,m,d,h,min,s = str_to_datetime(timestamp)
	if not stat then error() end
	timestamp = string.format("%4.4d/%2.2d/%2.2d 0:0:0",y,m,1) -- 検索対象日を月初め1日に変更
	stat,days = days_in_month(y,m)	-- 集計対象日数を対象月に含まれる日数に設定
	if not stat then error() end
	if not g_params["Interval"] then
		interval = 14400
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor((days * 24 * 3600)/interval)
end
---------------
-- 1時間集計
---------------
if g_params["Range"] == "hour" then
	if (not g_params["TargetDate"]) and (not g_params["TargetTime"]) then -- 集計対象日と時間の両方が未指定の場合は1時間前の時刻に設定
		local stat,y,m,d,h,min,s = inc_second(-3600,now["year"],now["month"],now["day"],now["hour"],now["min"],0)
		if not stat then error() end
		timestamp = string.format("%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%2.2d",y,m,d,h,min,s)
	end
	if (not g_params["TargetDate"]) and g_params["TargetTime"] then -- 集計対象時間のみ指定の場合は現在日に設定
		timestamp = string.format("%4.4d/%2.2d/%2.2d ",now["year"],now["month"],now["day"]) .. g_params["TargetTime"]
	end
	if g_params["TargetDate"] and (not g_params["TargetTime"]) then -- 集計対象日のみ指定の場合は 0:0:0 に設定
	    timestamp = g_params["TargetDate"] .. " 0:0:0"
	end
	if g_params["TargetDate"] and g_params["TargetTime"] then -- 集計対象日と時間の両方を指定の場合
	    timestamp = g_params["TargetDate"] .. " " .. g_params["TargetTime"]
	end
	if not g_params["Interval"] then
		interval = 30
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor(3600/interval)
end
---------------
-- 6時間集計
---------------
if g_params["Range"] == "6h" then
	if (not g_params["TargetDate"]) and (not g_params["TargetTime"]) then -- 集計対象日と時間の両方が未指定の場合は6時間前の時刻に設定
		local stat,y,m,d,h,min,s = inc_second(-21600,now["year"],now["month"],now["day"],now["hour"],now["min"],0)
		if not stat then error() end
		timestamp = string.format("%4.4d/%2.2d/%2.2d %2.2d:%2.2d:%2.2d",y,m,d,h,min,s)
	end
	if (not g_params["TargetDate"]) and g_params["TargetTime"] then -- 集計対象時間のみ指定の場合は現在日に設定
		timestamp = string.format("%4.4d/%2.2d/%2.2d ",now["year"],now["month"],now["day"]) .. g_params["TargetTime"]
	end
	if g_params["TargetDate"] and (not g_params["TargetTime"]) then -- 集計対象日のみ指定の場合は 0:0:0 に設定
	    timestamp = g_params["TargetDate"] .. " 0:0:0"
	end
	if g_params["TargetDate"] and g_params["TargetTime"] then -- 集計対象日と時間の両方を指定の場合
	    timestamp = g_params["TargetDate"] .. " " .. g_params["TargetTime"]
	end
	if not g_params["Interval"] then
		interval = 60
	else
		interval = tonumber(g_params["Interval"])
	end
	count = math.floor(21600/interval)
end

if count > 10000 then -- 集計計算に時間がかかりすぎるため、エラーにする
	log_msg("too many records",file_id)
	error()
end

----------------------------------------------------------------------
-- 集計に使用する計算方法のデフォルト設定
----------------------------------------------------------------------
if not g_params["Summary"] then g_params["Summary"] = "mean" end

----------------------------------------------------------------------
-- データ集計
----------------------------------------------------------------------
log_msg(string.format("start_datetime: %s interval: %d count: %d summary: %s",
							timestamp,interval,count,g_params["Summary"]),file_id)
local sum
local series_json = '['
local label_json = '['
local first_series = true
for k,v in ipairs(keys) do
	log_msg("calculating: " .. v,file_id)
	if first_series then
		first_series = false
	else
		series_json = series_json .. ","
		label_json = label_json .. ","
	end
	---------------------------
	-- キー(シリーズ)毎の集計
	---------------------------
	local stat,datetime,sample,total,mean,max,min = summary_stat_data(v,timestamp,interval,count)
	if not stat then error() end
	series_json = series_json .. '['
	label_json = label_json .. '{label:"' .. v .. '"}'
	local first_item = true
	for key,val in ipairs(datetime) do
		if g_params["Summary"] == "mean" then
			sum = mean[key]		-- 集計単位期間内の平均データを使用
		else
			sum = total[key]	-- 集計単位期間内の合計データを使用
		end
		if sample[key] > 0 then -- 集計単位期間内にデータが存在しない場合にはレコードを出力しない
			if first_item then
				first_item = false
			else
				series_json = series_json .. ","
			end
			series_json = series_json .. string.format('["%s",%g]',val,sum)
		end
	end
	series_json = series_json .. ']'
end
series_json = series_json .. ']'
label_json = label_json .. ']'

--------------------------------------------
-- リターンパラメータに JSON 文字列を設定
--------------------------------------------
script_result(g_taskid,"SeriesList",series_json)
script_result(g_taskid,"LabelList",label_json)

このスクリプトでは、Web API 実行時に指定された統計データベースのキー名(複数指定可)のデータを集計して JSON 配列を返します。Web API の URL パラメータには集計期間の長さや日付、時刻、集計間隔、集計に使用する値(合計値または平均値)を指定することができます。

Web アプリの集計パラメータ設定画面で、ユーザーからラジオボタンやチェックボックスなどのGUI で指定された条件を、このスクリプトに渡すことで様々な条件で集計することができます。

集計結果はシリーズ配列として JSON 文字列で返します。1シリーズはキー名1つに対応する時系列の集計データを表します。このシリーズ配列のフォーマットはそのまま、グラフ表示を行うための JavaScript ライブラリ( jqplot )のプロットデータとして使用します。詳しいフォーマットは上記スクリプト中のコメントをご覧ください。

●WebAPIの動作確認

Lua スクリプトの準備ができたら Web API を実行して動作確認を行います。Web アプリの雛形がある程度できている場合には、Webアプリを動作させている Webブラウザのデバッグ機能を使用してネットワーク中にやり取りされるデータを直接モニタしてデバッグすることができます。また URL パラメータの数やリターンデータが少ない場合には Web ブラウザの URI 欄に直接 Web API のパスを入力して確かめる方法も使えます。

今回は Web API 動作を確認するために linux コマンド(curl) を使用しています。curl コマンドは HTTP プロトコルでデータの送受信を簡単にコンソールから実行できます。linux マシンは手元にあった Raspberry Pi マシン(Raspbian OS) から実行しています。

Raspberry Pi にログインして、”curl -X GET xxxxxx” コマンドを実行して HTTP GET コマンドを DeviceServer に送信しています。URL のIP アドレス部分で指定している 192.168.100.45 は DeviceServer が動作している サーバーPC のアドレスです。

最初のコマンド実行で SUMMARY フォルダに格納したキー名リスト取得(LIST_JSON) を実行しています。スクリプト名は URL パラメータの name= 部分で指定します。noquote=1 パラメータは実行結果の JSON 文字列中でリターンパラメータに指定した値をダブルコートで囲まない指定です。これを指定すると JavaScript 側でJSON.parse() を使用しなくても直接リターン値を JSON オブジェクトとして利用できるようになります。session=1234 は WebAPI の動作試験をするときにログイン認証を省略して、予めサーバー側に作成したセッショントークン文字列 “1234″ を指定しています。

(Web API で指定可能な URL パラメータの種類については DeviceServer ユーザーマニュアル中の “36.2 /command/json/script” WebAPI コマンドの項を参照してください。また試験用のセッショントークン作成方法については “36.11 セッショントークン作成方法” の項をご覧ください)

WebAPI コマンドの実行結果は コンソールに出力されます。JSON 配列形式でキー名リストが返っているのが確認できます。また、日本語などのマルチバイト文字列は Unicode (UCS-2) の “\uxxxx” エンコード形式で出力しています。これによってWebブラウザやOSの種類に影響されずに日本語を正しく表示できます。

次に、集計スクリプトも同様に動作確認します。URL パラメータ name= 部分をSUMMARY/SUMMARY_DATA_JSON に変更して curl コマンドを実行します。KeyList パラメータには2つのリモートセンサーデバイスから送信された温度センサデータが格納されている SENSOR_TP_Device4 と SENSOR_TP_Node1 をカンマ区切りで指定しています。

集計パラメータの指定によってはかなりのデータ量が JSON 配列で返されますので、curl コマンドの出力は一旦リダイレクトして out.txt ファイルに保存してから more コマンドで内容を確認しています。

これでWeb API 経由でキー名リスト取得と集計機能の動作確認ができました。

●Webアプリ作成

次にWebアプリ本体を作成します。アプリの基本的な動作は jqueryjquery mobile のフレームワークを使用して作成しますので、主に作成するファイルは HTML ファイルと JavaScript ファイルのみで非常にシンプルです。もちろん、これ以外の Web アプリフレームワークを使用して Web アプリを作成しても構いません。

最初に、アプリの各ページで表示する画面とダイアログなどを記述した HTML ファイルを作成します。ファイルの内容は以下になります。このファイルは最新のDeviceServer インストールキットを使用すると、C:\Program Files (x86)\AllBlueSystem\WebRoot\web_api_sample\summary\chart\index.html に配置されます。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>集計グラフ</title>
        <link rel="stylesheet" href="libs/css/themes/default/jquery.mobile-1.2.1.css" />
        <link rel="stylesheet" href="my.css" />
        <link rel="stylesheet" href="libs/css/jquery.jqplot.css" />
		<link rel="stylesheet" href="libs/css/jquery.mobile.datepicker.css" />
		<link rel="stylesheet" href="libs/css/jquery.mobile.datepicker.theme.css" />
        <script src="libs/js/jquery.js"></script>
        <script src="libs/js/jquery.mobile-1.2.1.js"></script>
		<script src="libs/js/socket.io.min.js"></script>
		<script src="libs/js/jquery.jqplot.js"></script>
		<script src="libs/js/jqplot.cursor.min.js"></script>
		<script src="libs/js/jqplot.dateAxisRenderer.js"></script>
		<script src="libs/js/jqplot.barRenderer.js"></script>
		<script src="libs/js/jqplot.highlighter.js"></script>
		<script src="libs/js/datepicker.js"></script>
		<script src="libs/js/jquery.mobile.datepicker.js"></script>
        <script src="libs/device_server/webapi.js"></script>
    </head>
    <body>
        <div data-role="page" id="login" data-theme="a">
            <div data-theme="a" data-role="header" data-position="inline">
                <h3>ユーザー認証</h3>
            </div>
			<ul data-role="listview">
				<li data-role="fieldcontain">
                    <fieldset data-role="controlgroup">
  						<div><h1>&nbsp</h1></div>
                    </fieldset>
                    <fieldset data-role="controlgroup">
                        <label for="login_name">Name:</label>
                        <input id="login_name" placeholder="" value="" type="text" />
                    </fieldset>
                    <fieldset data-role="controlgroup">
                        <label for="login_password">Password:</label>
                        <input id="login_password" placeholder="" value="" type="password" />
                    </fieldset>
                    <fieldset data-role="controlgroup">
  						<div><h1>&nbsp</h1></div>
                    </fieldset>
                    <fieldset data-role="controlgroup">
						<div class="ui-grid-b">
							<div class="ui-block-a"></div>
							<div class="ui-block-b">
		                		<a data-role="button" data-inline="true" data-theme="a" data-icon="check" data-iconpos="left" id="login_btn" >Login</a>
						    </div>
							<div class="ui-block-c"></div>
						</div>
                    </fieldset>
				</li>
			</ul>

            <div data-theme="a" data-role="footer">
                <h3>ABS-9000 DeviceServer</h3>
            </div>
        </div>

        <div data-role="page"  id="select_keys_page" data-theme="a">
            <div data-theme="a" data-role="header" data-position="inline">
				<a data-icon="home" id="logout_btn" href="#logout_caution" data-rel="dialog" data-transition="pop">Logout</a>
                <h3>集計対象とするキー選択</h3>
				<a data-icon="arrow-r" id="key_list_next_btn">(次に進む) 集計パラメータ</a>
            </div>

			<a id="key_list_reload_btn" data-role="button" data-icon="refresh" data-iconpos="left">最新の状態に更新</a> 

			<fieldset id="key_list" data-role="controlgroup">
			</fieldset>

            <div data-theme="a" data-role="footer">
                <h3>ABS-9000 DeviceServer</h3>
            </div>
        </div>

        <div data-role="page"  id="summary_params_page" data-theme="a">
            <div data-theme="a" data-role="header" data-position="inline">
				<a data-icon="arrow-l" id="summary_params_prev_btn">(戻る) キー選択</a>
                <h3>集計パラメータ</h3>
				<a data-icon="arrow-r" id="summary_params_next_btn">(次に進む) グラフ作成</a>
            </div>

			<div data-role="fieldcontain">
    			<label for="target_keys">集計対象キー(カンマ区切り):</label>
				<input type="text" id="target_keys">
			</div>
			<div data-role="fieldcontain">
    			<label for="target_date">集計対象とする最初の日付:</label>
				<input type="text" id="target_date" placeholder="YYYY/MM/DD または NULL(自動設定)" class="date_input">
			</div>
			<div data-role="fieldcontain">
    			<label for="target_time">集計対象とする最初の時刻:</label>
				<input type="text" id="target_time" placeholder="HH:MM:SS または NULL(自動設定)">
			</div>
			<fieldset data-role="controlgroup">
				<legend>期間 (集計単位):</legend>
				<input type="radio" name="summary_range" id="range-5"  value="hour"/>
				<label for="range-5">1時間 (30秒)</label>
				<input type="radio" name="summary_range" id="range-6"  value="6h"/>
				<label for="range-6">6時間 (1分)</label>
				<input type="radio" name="summary_range" id="range-1" value="day" checked="checked"  />
				<label for="range-1">1日 (5分)</label>
				<input type="radio" name="summary_range" id="range-2" value="2d"/>
				<label for="range-2">2日 (10分)</label>
				<input type="radio" name="summary_range" id="range-3"  value="week"/>
				<label for="range-3">1週 (1時間)</label>
				<input type="radio" name="summary_range" id="range-4"  value="month"/>
				<label for="range-4">1月 (4時間)</label>
			</fieldset>
			<fieldset data-role="controlgroup">
				<legend>グラフのタイプ:</legend>
				<input type="radio" name="plot_type" id="plot-type-1" value="bar" checked="checked"  />
				<label for="plot-type-1">棒グラフ (bar)</label>
				<input type="radio" name="plot_type" id="plot-type-2"  value="line"/>
				<label for="plot-type-2">折れ線グラフ (line)</label>
			</fieldset>
			<fieldset data-role="controlgroup">
				<legend>プロットする集計値:</legend>
				<input type="radio" name="summary_method" id="method-1" value="mean" checked="checked"  />
				<label for="method-1">平均値 (mean)</label>
				<input type="radio" name="summary_method" id="method-2"  value="total"/>
				<label for="method-2">合計値 (total)</label>
			</fieldset>
			<div data-role="fieldcontain">
	         <label for="summary_interval">集計単位を変更:</label>
	         <input type="number" id="summary_interval" placeholder="秒数 または NULL(自動設定)" value="" />
			</div>

            <div data-theme="a" data-role="footer">
                <h3>ABS-9000 DeviceServer</h3>
            </div>
        </div>

        <div data-role="page"  id="chart_disp_page" data-theme="a">
            <div data-theme="a" data-role="header" data-position="inline">
				<a data-icon="arrow-l" id="chart_disp_prev_btn">(戻る) 集計パラメータ</a>
                <h3>集計グラフ</h3>
				<a data-icon="gear" id="chart_disp_redraw_btn">再描画</a>
            </div>

			<div id="chartdiv" style="height:400px;width:95%; "></div>

            <div data-theme="a" data-role="footer">
                <h3>ABS-9000 DeviceServer</h3>
            </div>
        </div>

		<div data-role="page" id="login_error_dialog">
			<div data-role="header" data-theme="e">
				<h3>*ERROR*</h3>
			</div>
			<div data-role="content" data-theme="e">
				<h2>ログインに失敗しました</h2>
				<p>ユーザー名またはパスワードが間違っています。システムのログイン制限により失敗している場合があります</p>
				<p><a href="#login" data-role="button" data-inline="true" data-icon="check" data-theme="c">戻る</a></p>
			</div>
		</div>

		<div data-role="page" id="logout_caution">
			<div data-role="header" data-theme="e">
				<h3>*WARNING*</h3>
			</div>
			<div data-role="content" data-theme="e">
				<h2>ログアウトしますか?</h2>
				<p>ログアウト操作を行う場合には "OK" を押してください。"キャンセル" で元の画面に戻ります</p>
				<p><a data-role="button" data-inline="true" data-icon="back" data-rel="back" data-theme="c">キャンセル</a>
				   <a data-role="button" data-inline="true" data-icon="check" id="logout_ok_btn" data-theme="c">OK</a></p>
			</div>
		</div>

		<div data-role="page" id="error_quit_dialog">
			<div data-role="header" data-theme="e">
				<h3>*ERROR*</h3>
			</div>
			<div data-role="content" data-theme="e">
				<h2>エラーが発生しました</h2>
				<p>サーバー処理中にエラーが発生しました。現在のセッションが無効になっている場合があります。再ログイン操作を行ってください</p>
				<p><a data-role="button" data-inline="true" data-icon="check" id="server_error_ok_btn" data-theme="c">OK</a></p>
			</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>

		<div data-role="page" id="error_prev_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" id="error_prev_ok_btn" data-theme="e">OK</a></p>
			</div><!-- /content -->
		</div>

		<div data-role="page" id="key_select_error_dialog">
			<div data-role="header" data-theme="e">
				<h3>script error</h3>
			</div>
			<div data-role="content" data-theme="e">
				<h3>エラーが発生しました</h3>
				<p>1つ以上の統計デーベースキーを選択してください</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>

今回作成したWeb アプリには、ログイン認証画面、グラフ作成対象のキー名選択画面、集計パラメータ設定画面とグラフ表示画面のページがあります。これらの画面レイアウトを index.html ファイル中に記述しています。また、エラー発生時やログアウト動作時に表示されるダイアログメッセージも同様に index.html ファイル中に記述しています。

index.html ファイル中には画面レイアウトのみが記述されていて、チェックボックス操作時やボタンを押したときの動作、WebAPI のコール、画面遷移などのアプリケーションロジックは除いた形になっています。これらのアプリケーションロジック部分は 別途用意する JavaScript ファイル(main.js) 中に全て記述しています。

次に JavaScript ファイルを作成します。ファイルの内容は以下になります。このファイルは最新のDeviceServer インストールキットを使用すると、C:\Program Files (x86)\AllBlueSystem\WebRoot\web_api_sample\summary\chart\main.js に配置されます。

//
// 	ABS-9000 DeviceServer 統計データ集計アプリケーション
//
//  2014/8/7	ver1.00 初版作成
//
//  copyright(c) all rights reserved 2014 All Blue System
//

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

// 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;
}

// Line チャート表示
function plot_chart_line(data){
	$.mobile.loading( 'hide');
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$.mobile.changePage( "#error_quit_dialog", {transition: "pop",role:"dialog"});
		} else {
			$.mobile.changePage( "#error_prev_dialog", {transition: "pop",role:"dialog"});
		}
		return;
	}
	// jqplot ライブラリを使用してチャート表示
	// プロットデータとラベルはスクリプトリターンパラメータから取得する
	var plot1 = $.jqplot('chartdiv',data.ResultParams.SeriesList,{
		series: data.ResultParams.LabelList,
		legend:{
			show: true
		},
		highlighter: {
			show: true,
			sizeAdjust: 7.5
      	},
		axes: {
            xaxis: {
                renderer: $.jqplot.DateAxisRenderer,
                tickOptions: {
                    formatString: "%m/%d %H:%M",
                    angle: -30,
                    textColor: '#dddddd'
                },
                drawMajorGridlines: true
            },
            yaxis: {
                rendererOptions: {
                    minorTicks: 1
                },
				label:" ", // 左マージンの為にダミーを配置
                min: 0,
                tickOptions: {
                    formatString: "%'d",
                    showMark: false,
                    textColor: '#dddddd'
                }
            }
        },
		cursor:{show: true,zoom:true}
	});
}

// Bar チャート表示
function plot_chart_bar(data){
	$.mobile.loading( 'hide');
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$.mobile.changePage( "#error_quit_dialog", {transition: "pop",role:"dialog"});
		} else {
			$.mobile.changePage( "#error_prev_dialog", {transition: "pop",role:"dialog"});
		}
		return;
	}
	// jqplot ライブラリを使用してチャート表示
	// プロットデータとラベルはスクリプトリターンパラメータから取得する
	var plot1 = $.jqplot('chartdiv',data.ResultParams.SeriesList,{
		seriesDefaults: {
			renderer:$.jqplot.BarRenderer,
			rendererOptions: {
				barWidth: 4
			},
			pointLabels: { show: true }
		},
		series: data.ResultParams.LabelList,
		legend:{
			show: true
		},
		highlighter: {
			show: true// ,
      	},
		axes: {
            xaxis: {
                renderer: $.jqplot.DateAxisRenderer,
                tickOptions: {
                    formatString: "%m/%d %H:%M",
                    angle: -30,
                    textColor: '#dddddd'
                },
                drawMajorGridlines: true
            },
            yaxis: {
                rendererOptions: {
                    minorTicks: 1
                },
                min: 0,
				label:" ", // 左マージンの為にダミーを配置
                tickOptions: {
                    formatString: "%'d",
                    showMark: false,
                    textColor: '#dddddd'
                }
            }
        },
		cursor:{show: true,zoom:true}
	});
}

// DeviceServer の集計スクリプトを起動して、集計パラメータで指定された
// シリーズデータを取得する。スクリプト完了時のイベントハンドラでグラフを描画する
function plot_chart(){
	$('#chartdiv').empty();
	$.mobile.loading( 'show');

	// DeviceServer の集計スクリプトを起動する
	var params = {};
	params["noquote"] = "1";	// スクリプトリターンパラメータを JSON オブジェクトとして受信する
	params["KeyList"] = selected_keys;
	var target_date = $("#target_date").val();
	if (target_date != ""){
		params["TargetDate"] = target_date;
	}
	var target_time = $("#target_time").val();
	if (target_time != ""){
		params["TargetTime"] = target_time;
	}
	params["Range"] = $('input[name=summary_range]:checked').val();
	params["Summary"] = $('input[name=summary_method]:checked').val();
	var summary_interval = $("#summary_interval").val();
	if (summary_interval != ""){
		params["Interval"] = summary_interval;
	}
	var plot_type =  $('input[name=plot_type]:checked').val();
	switch(plot_type){
		case "bar":
					script_exec("SUMMARY/SUMMARY_DATA_JSON",params,"plot_chart_bar");
					break;
		case "line":
					script_exec("SUMMARY/SUMMARY_DATA_JSON",params,"plot_chart_line");
					break;
	}
}

// グラフ画面が表示された
$( "#chart_disp_page" ).live( "pageshow",function(event){
	plot_chart();
});

// グラフ画面の Redrawボタンが操作された
$( "#chart_disp_redraw_btn" ).bind( "click", function(event, ui){
	plot_chart();
});

// グラフ画面の Prevボタンが操作された
$( "#chart_disp_prev_btn" ).bind( "click", function(event, ui){
	$.mobile.changePage( "#summary_params_page", { transition: "slide"});
});

// 集計パラメータ設定画面の Nextボタンが操作された
$( "#summary_params_next_btn" ).bind( "click", function(event, ui){
	$.mobile.changePage( "#chart_disp_page", { transition: "slide"});
});

// 集計パラメータ設定画面の Prevボタンが操作された
$( "#summary_params_prev_btn" ).bind( "click", function(event, ui){
	$.mobile.changePage( "#select_keys_page", { transition: "slide"});
});

// 集計パラメータ設定画面が表示された
$( "#summary_params_page" ).live( "pageshow",function(event){
	$("#target_date" ).datepicker({ dateFormat: "yy/mm/dd" });
	$("#target_keys").val(selected_keys);
});

// 集計対象キーのテキスト入力コンポーネントの内容が変更された
$( "#target_keys" ).bind( "change", function(event, ui){
	selected_keys = $("#target_keys").val();
});

// サーバー側でログイン操作が成功したらデバイス選択画面に移動する
function login_callback(data){
	if (data.Result == "Success"){
		session_token = data.SessionToken;
		// dump_login_result(data);
		$.mobile.changePage( "#select_keys_page", { transition: "none"});
	} else {
		$.mobile.changePage( "#login_error_dialog", {transition: "pop",role:"dialog"});
	}
}

// サーバー側でログアウト操作が完了したらログイン画面に戻る
function logout_callback(data){
	session_token = "";
	$("#login_password").val("");
	$.mobile.changePage( "#login", { transition: "pop"});
}

// ログインボタンを押した
$( "#login_btn" ).bind( "click", function(event, ui){
	var user = $("#login_name").val();
	var pass = $("#login_password").val();
	login(user,pass,"login_callback");
});

// ログアウトボタンを押した
$( "#logout_ok_btn" ).bind( "click", function(event, ui){
	logout("logout_callback");
});

// サーバーエラーのダイアログから復帰する場合はログイン画面に戻る
$( "#server_error_ok_btn" ).bind( "click", function(event, ui){
	session_token = "";
	$.mobile.changePage( "#login", { transition: "pop"});
});

// ログインページが表示された
$( "#login" ).live( "pageshow",function(event){
	// セッショントークンが指定されている場合にはユーザー認証を省略して
	// デバイス選択画面に移動する
	if (session_token != ""){
		$.mobile.changePage( "#select_keys_page", { transition: "slide"});
	}
});

// 集計対象の統計データベースキー名リスト(カンマ区切り)
// チェックボックスを操作する時に、update_selected_keys() 関数によって
// 常に最新のチェック状態を反映している
var selected_keys = "";

// チェックボックスで選択されているキーのリストを selected_keys に反映させる
function update_selected_keys(){
	var first = true;
	selected_keys = ""; // キーが少なくとも1つ以上選択されているかを調べる
	$(".key_select:checked").each(function(index, checkbox){
		var key_name = checkbox.name;
		if(first) {
			first = false;
		} else {
			selected_keys = selected_keys + ",";
		}
  		selected_keys = selected_keys + key_name;
	});
};

// 集計対象キーのチェックボックスが操作された
$( ".key_select" ).live( "change", function(event, ui){
	update_selected_keys();
});

// 集計対象キー選択画面の Nextボタンが操作された
$( "#key_list_next_btn" ).bind( "click", function(event, ui){
	if (selected_keys == ""){ // キーが未指定の場合にはエラー
		$.mobile.changePage( "#key_select_error_dialog", {transition: "pop",role:"dialog"});
		return;
	}
	$.mobile.changePage( "#summary_params_page", { transition: "slide"});
});

// 統計データベースで使用中のキー名リストを取得する
function get_key_list(){
	var params = {};
	params["noquote"] = "1";		// スクリプトリターンパラメータを JSON オブジェクトとして受信する
	//params["prefix"] = "SENSOR_";	// SENSOR_ で始まるキー名のみを対象にする
	script_exec("SUMMARY/LIST_JSON",params,"get_key_list_handler");
}

// SUMMARY/LIST_JSONスクリプト実行結果のイベントハンドラ。
function get_key_list_handler(data){
	if (data.Result != "Success"){
		if(data.ErrorText.match(/CertifyUpdateSession failed/i)) {
			$.mobile.changePage( "#error_quit_dialog", {transition: "pop",role:"dialog"});
		} else {
			$.mobile.changePage( "#error_back_dialog", {transition: "pop",role:"dialog"});
		}
		return;
	}
	// 選択済みのキーがある場合にはチェックボックスをチェック済みにする
	var selected_arr = selected_keys.split(",");
	// 統計データベースで使用中のキー名をチェックボックスリストに表示する
	$('#key_list').empty();
	for(i in data.ResultParams.KeyList){
		var item =  data.ResultParams.KeyList[i].Key;
		if ($.inArray(item,selected_arr) >= 0) { // 既に選択済み?
			$('#key_list').append('<input type="checkbox" name="' + item + '" id="' + item +
						'"checked="checked" class="key_select" /><label for="' + item + '">' + item + '</label>');
		} else {
			$('#key_list').append('<input type="checkbox" name="' + item + '" id="' + item +
						'" class="key_select" /><label for="' + item + '">' + item + '</label>');
		}
	}
	$("#key_list").trigger('create');
}

// 集計対象キー選択画面が表示された
$( "#select_keys_page" ).live( "pageshow",function(event){
	get_key_list();
});

// 集計対象キー選択画面の Reloadボタンが操作された
$( "#key_list_reload_btn" ).bind( "click", function(event, ui){
	get_key_list();
});

// 集計スクリプト実行中エラーのダイアログから復帰する場合は集計パラメータ設定画面に戻る
$( "#error_prev_ok_btn" ).bind( "click", function(event, ui){
	$.mobile.changePage( "#summary_params_page", { transition: "slide"});
});

main.js ファイルは、Webアプリケーションの以下の機能を実現しています。

(1) ログイン認証

(2) ログアウト

(3) 統計データベースキー名リストをチェックボックスリストに表示

(4) 集計パラメータの操作

(5) グラフ表示

これらの機能はWeb アプリ画面に表示されている GUI コンポーネントを操作したときのイベントハンドラとして記述しています。jquery API 関数の $(xxxx).live(xxxx) や $(xxxx).bind(xxxx) を使用して index.html ファイル中の id や class 属性に指定したコンポーネントとその GUI を操作したときに動作する内容を結び付けています。

ログイン直後に表示されるキー名選択画面では、get_key_list() 関数がコールされて、その関数内から Web API 用に作成したLIST_JSON.lua スクリプトをコールしています。script_exec() 関数は予めオールブルーシステム側で用意している JavaScript 関数で C:\Program Files (x86)\AllBlueSystem\WebRoot\web_api_sample\summary\chart\libs\device_server\webapi.js 内で定義されていて、下記のようになっています。

/////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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
	});
}

Web API をコールするための URL パラメータを作成した後、jquery の $.ajax() 関数をコールしています。

集計パラメータ設定画面では、集計対象期間やグラフプロット値に使用する計算方法(平均または合計)の選択が行えます。これらの集計パラメータ設定値は、グラフ表示画面へ移動するボタンを押したときにplot_chart() 関数がコールされて、SUMMARY_DATA_JSON スクリプトを実行する WebAPI コール時の URL パラメータに指定されます。

グラフの表示には jqplot ライブラリを使用しています。WebAPI の実行完了時にコールされるハンドラ( plot_chart_bar() または plot_chart_line() )関数で JSON 配列で返された集計データ(シリーズ配列)をグラフに表示します。

jqplot ライブラリを使用すると、軸ラベルの表示やスケーリングを全て自動計算しますので、集計データのみをパラメータで渡すだけで最適なグラフを表示してくれます。また、マウスクリックによるズーム機能がありますので、集計パラメータで集計単位間隔を短い時間に変更した場合でも拡大して見易く表示することができます。

●Webアプリケーション動作例

早速Webアプリを起動してみます。その前に、センサーデータが統計データベースに定期的に登録されていないとなにも表示できませんので、この記事この記事を参考にしてデータを格納してください。また、簡単に試験するだけでしたら、DeviceServer のライブラリ関数 add_stat_data() にタイムスタンプパラメータ(第3パラメータを省略すると登録時の時刻に設定されます)も指定して、

add_stat_data(“試験用キー”, 10,”2014/1/1 10:10:0″)

add_stat_data(“試験用キー”, 20,”2014/1/1 10:20:0″)

add_stat_data(“試験用キー”,30,”2014/1/1 10:30:0″)

の様な感じでテストデータを登録するだけでもグラフ表示できます。

Web ブラウザを起動して、http://localhost/web_api_sample/summary/chart/index.htmlにアクセスします。WebブラウザをサーバーPC とは別の PC で起動する場合には、localhost 部分を DeviceServer の動作する PC のIP アドレスに変更してブラウザの URI 欄に指定します。

最初にログイン画面が表示されます。DeviceServer に作成したユーザー名とパスワードを指定してログインします。DeviceServer のアカウントはインストール時に作成した一般ユーザーアカウントや管理者アカウントを指定できます。このとき、ユーザーアカウントのアプリケーションフラグ”WebLogin” にチェックが付いていないと一切のWebAPI 経由での操作(ログイン, スクリプト実行等)ができませんので注意してください。

ログインに成功すると、統計データベース中に登録済みの全てのキー名がチェックボックスリストで表示されます。グラフ表示したいキーをここで複数選択できます。

グラフ表示するときには、ここで指定したデータが合わせて1つのグラフに表示されます。このため、例えば温度センサーのグラフならば同じデータ単位(℃) をもったものだけを複数選択するようにします。赤外線センサと明るさ(CDS)のデータを同時に表示するような指定をすると、グラフの Y 軸の単位(意味)が不明になりますので避けましょう。

集計パラメータボタンを押すと下記の画面が表示されます。

ここではグラフ表示したい期間を指定します。集計対象とする最初の日付や時刻を空のままにしておくと、現在の日付と時刻を基に最新のデータを表示するように自動計算されます。

グラフのタイプは棒グラフまたは折れ線グラフを選択できます。リモート側からのデータが届いていない欠損部分を見やすくするには棒グラフの方が見やすいです。折れ線グラフでは欠損部分を飛ばして線を描画しますので変化傾向を見やすくなります。

グラフ作成ボタンを押すとグラフを表示します。

グラフのX軸には登録データのタイムスタンプを元に自動スケーリングされた目盛りが表示されます。Y 軸も同様にデータの値を元に自動スケーリングされて表示されます。

ヘッダ部分の集計パラメータボタンを押すと集計パラメータ設定画面に戻って、集計期間や集計間隔の指定をやり直すことができます。

ここでは、集計対象日付を2014/7/1 に設定したのち、月次グラフを表示するように変更しています。月次集計はデフォルトでは 4 時間毎に集計値をプロットしますが、今回はもっと細かく表示するために 1 時間(3600秒)単位で計算するように集計単位パラメータを変更しています。

グラフ作成ボタンを押して再び集計計算を行ってグラフを表示してみます。

かなりグラフが込み入っていますが日々のデータの動きがよくわかるようになりました。細かいデータを確認するために、マウスで拡大表示したい部分を囲んでみます(上記の明るくなっている部分) 。

グラフが拡大表示されると同時に、X, Y 軸のラベルも更新されて、細かいデータまで確認できるようになります。

このように jqplot ライブラリを利用すると簡単にグラフを作成したりズーム機能を利用することができます。また、Y 軸に別の単位のデータを重ねて表示する機能も jqplot に用意されていますので、温度と赤外線 、明るさの複合グラフを表示するように変更するのも簡単にできると思います。

ここで紹介した Web アプリのソースファイルは全てインストールキットに含まれています。商用・個人利用を問わずに自由に改造や流用をしていただいて構いません。此方から最新のキットをダウンロードして、直ぐに使用することができます。(デモライセンスが添付されていますので直ちに使用可能です)。

それではまた。

 

リモート(XBee + CPUボード)から取得したセンサデータを集計

前回の記事ではリモートに設置したリモートCPU ボードからXBee 経由で、温度センサ、赤外線センサ、照度センサの各サンプリングデータを取得して、クラウドサービスにデータを保存してグラフ表示する例について紹介しました。

今回は、前回と同じリモートCPU ボードとセンサを使用して、センサーデータの集計を行う例を紹介したいと思います。センサーデータを手元の表計算ソフト(エクセル)にロードしてグラフ作成などで活用できるようになります。クラウドサービスに送信したデータもネットワーク経由でダウンロードすることも可能ですが、今回の記事で使用するセンサデータは DeviceServer を設置したPC にインストールされた統計用データベース(Firebird RDMS )を利用します。

前回の記事でクラウド側にセンサデータを送信するときに使用したスクリプトでは、Xively にHTTP プロトコルでデータ送信する部分で、同時にサーバーPC 内の統計用データベースにもセンサーデータを保存するように記述していました。 たとえば XBee-ZB を利用したリモートCPU ボードから定期的に送信されたセンサーデータを処理するスクリプト ZB_SENSOR_DATA_STORE では以下の様に記述されています。

file_id = "ZB_SENSOR_DATA_STORE"

--[[

●機能概要

TDCP デバイスから送信されたサンプリングデータを DeviceServerに登録
するスクリプトの例です。このスクリプトはユーザー環境に合わせて
カストマイズして使用してください。

パラメータで指定した XBeeデバイスに接続している TDCPボード上の
センサーデータを取得してデータベースに登録します。

このスクリプトは、リモートデバイスから定期的に送信されてくる
SAMPLINGイベントのイベントハンドラ中からコールされて使用できるように
しています。

リモートデバイスに接続した各種センサーから、I2C、SPI、A/D変換入力、
デジタルポート、カウンタ入力ポート経由で現在の測定値を取得します。
測定した値は、センサーデータとして扱いやすい値に変換した後
データベースに登録します。

リモートデバイスごとに、接続しているセンサーが異なる場合には、
スクリプト内でデバイスの NodeIdentifier を元に処理を分けることができます。

データベースに登録するときには、キー名に NodeIdentifier と センサー種別
を示す文字列を含めると、後のデータ処理が行い易くなります。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

TDCP_1			TDCPイベント文字列第1カラム(Prefix文字列)				"$$$"

TDCP_2			TDCPイベント文字列第2カラム(イベント種別)				"SAMPLING

TDCP_3			TDCPイベント文字列第3カラム(app_mode)					"32"

TDCP_4			TDCPイベント文字列第4カラム(イベントデータ#1)			"0F"

TDCP_5			TDCPイベント文字列第5カラム(イベントデータ#2)			"0"
..
..

TDCP_N			TDCPイベント文字列第4カラム(イベントデータ#n)			"123"

( ZB_TDCP_DATA イベントハンドラに渡された全てのパラメータを、このスクリプトを
  コールする時にも指定する)

NodeIdentifier		XBee-ZB デバイスのNodeIdentifier					"Node1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

●変更履歴

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["NodeIdentifier"]) then
	log_msg("parameter error",file_id)
	error()
end

local dev = g_params["NodeIdentifier"]
local param = {}

---------------------------------------------------------------------------------
-- NodeIdentifierが Node1 の名前をもつ XBee-ZBデバイスに接続された TDCPZB リモート
-- CPUボードに、以下のセンサーデバイスが接続されている場合の例です
--   * TMP102 温度センサ	(I2Cバスに接続)
--   * CDS照度センサ		(A/D#0に接続)
--   * IR(人感)センサ		(DIO#3/COUNTER入力接続)
---------------------------------------------------------------------------------

--------------------------------------------------------------------------------
-- NodeIdentifier, TDCP イベント種別、TDCP app_modeが一致するものだけを選択して
-- サンプリングデータをデータベースに格納する。
--------------------------------------------------------------------------------
if (dev == "Node1") and (g_params["TDCP_2"] == "SAMPLING") and (g_params["TDCP_3"] == "32") then 

	---------------------------------------------
	-- Xively サービスに登録するためのパラメータ
	---------------------------------------------
	local xively_ch_data = {}

	-----------------------------------------------------------
	-- I2Cバスに接続された TMP102 センサーから温度データを取得
	-----------------------------------------------------------
	param["device"] = dev
	stat,result = script_exec2("ZB/DEVICE/TMP102_READ",param)
	if not stat then error() end

	-----------------------------------------------------------------
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_TP_<NodeIdentifier>"
	-----------------------------------------------------------------
	if not add_stat_data("SENSOR_TP_" .. dev,result["temperature"]) then error() end
	if not set_shared_data("SENSOR_TP_" .. dev,result["temperature"]) then error() end
	xively_ch_data["SENSOR_TP_" .. dev] = result["temperature"] -- for Xively

	-----------------------------------------------------------------------------
	-- SAMPLING イベント発生時の A/D#0 (TDCP_7) は光センサーの値が格納されている
	--
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_LUMI_<NodeIdentifier>"
	-----------------------------------------------------------------------------
	if not add_stat_data("SENSOR_LUMI_" .. dev,g_params["TDCP_7"]) then error() end
	if not set_shared_data("SENSOR_LUMI_" .. dev,g_params["TDCP_7"]) then error() end
	xively_ch_data["SENSOR_LUMI_" .. dev] = g_params["TDCP_7"] -- for Xively

	-----------------------------------------------------------------------------
	-- SAMPLING イベント発生時の COUNTER (TDCP_6) はIRセンサーのカウンタ値が格納されている
	--
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_IR_<NodeIdentifier>"
	-----------------------------------------------------------------------------
	if not add_stat_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	if not set_shared_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	xively_ch_data["SENSOR_IR_" .. dev] = g_params["TDCP_6"] -- for Xively

	-----------------------------------------------------------------------------
	-- Xively サービスにセンサーデータを登録
	-----------------------------------------------------------------------------
	if not script_exec("XIVELY_DATA_STORE",xively_ch_data) then error() end

end

xively_ch_data[] 連想配列はクラウドにデータ送信するためのスクリプトに渡すセンサーデータが格納されたパラメータになります。この配列にセンサデータを代入すると同時に、 add_stat_data() ライブラリ関数を使用してセンサーデータを統計用データベースにも登録しています。(このスクリプトの詳しい説明は前回の記事を参照していください)

統計データベースには、キー名とデータ登録時のタイムスタンプ、センサデータの値を含んだレコードが保存されていきます。これらのレコードは、いつでも集計したい日付時刻とデータ登録時のキー名を指定して集計することができます。集計計算は DeviceServer のスクリプトやイベントハンドラ中からコールできる専用のライブラリ関数 summary_stat_data() を使用します。このライブラリ関数は DeviceServer ユーザーマニュアル中で以下の様に定義されています。

このライブラリ関数では、datetime_start パラメータに集計対象となる期間の開始日付時刻を指定します。interval パラメータには集計する間隔を指定します。count パラメータには集計間隔をどれだけ繰り返すかの回数を指定します。ここで指定した回数だけ、 datetime_start パラメータに指定した時刻を interval 分だけ進めて集計計算を繰り返します。これらの結果得られた複数の集計データ(対象データ個数、合計値、平均値、最大値、最小値)を配列の形で取り出せます。

このライブラリ関数を使用して、センサーデータを集計するスクリプトファイル(SENSOR_DATA_CSV.lua ) を作成します。スクリプトファイルは DeviceServer をインストールしたフォルダにある “Script” フォルダの中に “SUMMARY” フォルダを作成してその中に格納しています。(もちろん別のフォルダに設置しても構いません)

file_id = "SENSOR_DATA_CSV"

--[[

●機能概要

統計データベースに保存されているセンサーデータを集計して、
結果を CSV ファイルに出力する。

統計データベースに登録したときのキー名毎に1日分の集計結果をファイルに出力する。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
TargetDate			集計対象日付(YYYY/MM/DD)							"2010/01/31"
					パラメータ省略時はスクリプトが起動された日付が
					集計対象日になる

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

出力ファイルは、C:\WORK フォルダの中に <Key名>.csv
のファイル名で格納される。
出力先フォルダを変更したい場合には、outfolder 変数の初期値を変更する。

●変更履歴

2014/07/2	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

local outfolder = "c:/work"

log_msg("start..",file_id)

----------------------------------------------------------------------
-- TargetDate パラメータが指定されていない場合にはスクリプトが起動
-- された日を集計対象にする
----------------------------------------------------------------------
local timestamp
if g_params["TargetDate"] then
    timestamp = g_params["TargetDate"] .. " 0:0:0"
else
	local now = os.date "*t"
	timestamp = string.format("%4.4d/%2.2d/%2.2d 0:0:0",now["year"],now["month"],now["day"])
end

----------------------------------------------------------------------
-- 統計用データベースに保管された全てのセンサーデータを集計対象とする
----------------------------------------------------------------------
local file;
local stat,dev_list = key_list_stat_data("SENSOR_") -- キー名リストを取得
for k,v in ipairs(dev_list) do

	log_msg("calculating: " .. v,file_id)
	-------------------------------------------------------------------------
	-- 集計対象日の 00:00 から 24:00 までを集計する
	-------------------------------------------------------------------------
	local stat,datetime,sample,total,mean,max,min = summary_stat_data(v,timestamp,600,144) -- 10分単位の集計
--	local stat,datetime,sample,total,mean,max,min = summary_stat_data(v,timestamp,1800,48) -- 30分単位の集計
--	local stat,datetime,sample,total,mean,max,min = summary_stat_data(v,timestamp,3600,24) -- 1時間単位の集計
	if not stat then error() end

	----------------------------------------------------------------------
	-- ファイルオープン
	----------------------------------------------------------------------
	file = io.open(outfolder .. "/" .. v .. ".csv","w+");
	if (file == nil) then error() end;

	----------------------------------------------------------------------
	-- CSV形式でタイトルと集計データ出力
	----------------------------------------------------------------------
	file:write("DataKey,DateTime,SampleCount,MeanValue,MaxValue,MinValue,Total\n")
	for key,val in ipairs(datetime) do
		file:write(string.format("%s,%s,%d,%2.3g,%2.3g,%2.3g,%2.3g\n",v,datetime[key],sample[key],mean[key],max[key],min[key],total[key]))
	end

	----------------------------------------------------------------------
	-- ファイルクローズ
	----------------------------------------------------------------------
	file:close();

end

このスクリプトでは統計データベースに登録された1日分のセンサデータを10分単位で集計して CSV ファイルの形で出力します。センサーデータ登録時に指定したキー名ごとに別々のファイルを “C:\WORK” フォルダ内に書き出しています。ファイル出力には Lua 標準ライブラリ io を使用しています。ちなみに、DeviceServer で実行するスクリプトでは標準io 出力や標準 io 入力を使用することはできません。これは DeviceServer がサービスプログラムとして Windows のバックグランドで動作しているためです。ただし、ファイルに出力することは今回の様に可能です。

スクリプトパラメータ “TargetDate” に日付を指定すると、指定した日付のセンサーデータを集計します。パラメータ省略時にはスクリプトを起動した日付が集計対象日になります。

DeviceServer のクライアントプログラムにログインして、”スクリプト” ツールボタンを押してスクリプト実行画面を出します。プルダウンメニューから上記のスクリプトを選択して実行します。

ここでは、TargetDate スクリプトパラメータを指定しています。集計対象日付をパラメータの値に設定して、”実行” ボタンを押します。

今回の集計では合計5個のセンサーに対するファイルを計算していますが、集計計算は2秒程度で直ぐに完了します。もし、大量のセンサーデータを集計する場合に時間が掛かることが予想される場合には、”別スレッド実行” ボタンを使用してバックグランドで実行させることもできます。

集計が完了すると “C:\WORK” フォルダの中にセンサーデータ登録時に指定したキー名ごとに CSV ファイルが作成されます。

この CSV ファイルはエクセル等の表計算ソフトで簡単に扱うことができます。エクセルから集計ファイルを読み込んだ様子は以下になります。

CSV ファイル中の各カラムがセルに変換されてロードされます。集計データ中にある SampleCount は集計期間(10分毎)内に見つかったデータ個数を表します。もしこの値が 0 の場合にはリモートからセンサーデータが届いていないことを意味します。

温度センサから取得したデータの場合には集計期間中に見つかったデータの平均値を使用できますので “MeanValue” カラムの値を使用します。赤外線センサの場合には合計値を意味する “Total” カラムの値を使用します。今回はリモートCPU ボードから定期的に送信するサンプリング間隔と集計計算時に指定した時間間隔が等しく 10 分なのでどのカラムの値も同じになります。

集計間隔(Interval) を 1時間単位など変更した場合には、集計結果を利用するときに上記のセンサーデータ種別に応じて使用する値を選択してください。

全ての CSV ファイルをエクセルで同時に開いて、セルのコピー&ペーストを行って温度と赤外線センサの値をまとめてグラフを作成した様子が以下になります。

CSV ファイルの形でセンサーデータを集計しておくと、表計算ソフトやWindowsアプリケーションからセンサデータを簡単に利用できます。また、エクセルのマクロを併用すると毎日の集計作業を自動化することもできると思います。

自動化を行うときには、DeviceServer 側では集計用のスクリプトを毎日定時に起動するように設定しておきます。このときには Windows のタスクスケジューラ(schtasks) や DeviceServer のスクリプト実行プログラム・コマンドライン版 (ScriptExecCmd.exe) が使用できます。詳しくは DeviceServer ユーザーマニュアル中の “39. Windowsタスクスケジューラを使用してスクリプトを実行” の章をご覧ください。

それではまた。

 

リモート(XBee + CPUボード)から取得したセンサデータをクラウドサービス(Xively)で表示

今回は、リモートに設置した複数のセンサーノードから取得したデータを、クラウドサービス(Xively) に定期的に送信してグラフ表示する例を紹介します。

実は、DeviceServer には組み込みのデータベース(Firebird)と統計機能がありますので、今回紹介する Xively を使用しなくてもセンサーデータの集計やグラフ作成を行うことができまます。ただ、DeviceServer 側にグラフ作成用のWebアプリやスクリプトを用意する必要があります。

クラウドサービスを利用すると、これらのアプリを作成しなくてもセンサーデータを送信するだけで簡単にグラフ表示することができます。送信したセンサデータのメンテナンス機能もクラウド側で提供されていますので本格的な運用にも応用できます。

Xivey ( https://xively.com/ ) は、データの保存とグラフ表示を行ってくれるクラウドサービスで、無料で開発者アカウントを作ってセンサーシステムの集計機能として利用することができます。アカウント登録や利用条件など詳しくは上記サイトを確認してください。いろいろなサイトでも Xively の利用方法を紹介されていますので、情報も豊富に入手できます。

今回紹介するセンサーネットワークではリモートセンサーノードを2つ用意しています。センサーノード1は、XBee Series1(Device4) と CPU ボード(ATmega1284P) を使用したセンサーノードで、アナログ温度センサ(LM35) と人感センサ(IR)が接続されています。

センサーノードのボックスを開けると中は、汎用のCPU ボードとセンサ部分に分かれています。

2つ目のセンサーノード2は、XBee-ZB Series2(Node1)と CPUボード(ATmega328P) を使用して作成しています。センサーノード2にはI2C 接続の温度センサ(TMP102)と人感センサ(IR)、明るさを測るためのアナログ照度センサ(CDS)を接続しています。

これらのリモートセンサーノードから10分に一回、定期的に DeviceServer が動作するサーバーPC にサンプリングデータを送信しています。サーバーPC では受信したサンプリングデータから摂氏温度など扱いやすい値に変換した後、Xively に送信します。

Xively サービスに送信したセンサーデータはいつでもWebブラウザから表示できるようになります。下記は最新のセンサーデータを表示しているところです。 “Graphs” をクリックすると任意の期間のデータをグラフで表示することもできます(記事の最後で紹介します)

それではこれらの仕組みを詳しく説明していきます。まずは、XBee Series1 と ATmega1284 を組み合わせたセンサーノード1について説明します。

センサーノード1の回路図は以下になります。

ATmega1284 では サンプリングデータをXBee データパケットに格納して定期的に送信するためのモニタプログラムが動作しています。モニタプログラム(TDCP プログラム)のファームウエアとマニュアルはこちらからダウンロードすることができます。

このセンサーノードでは ATmega1284 プロセッサのデジタル入力ポートに人感(IR)センサを接続して、センサが反応した回数を数えています。また、A/D ポートに LM35 アナログ温度センサを接続して現在の温度を測定します。TDCP プログラムは定期的に上記のセンサから取得したデータをサンプリングイベントデータとしてサーバーに XBee 経由で送信します。

TDCP プログラムのサンプリング機能を設定するために、下記のリモートコマンドを実行します。

sampling_rate,600  // 600秒(10分)ごとにサーバーPC に SAMPLING イベントデータを送信

TDCP モニタプログラムではこの他にも、DIOとA/Dのコンフィギュレーションやプルアップ抵抗の有無などを設定するために、以下のリモートコマンドを実行しておきます。コマンドの詳細についてはマニュアルをご覧ください。

app_mode,8     //DIO, A/D機能等をポートに割り当てるモードを設定

server_addr,0C03  // イベント送信先 XBee アドレス設定、サーバーPC 側に設置したXBee のアドレスになります。

pullup,FF // デジタル入力ポートのプルアップ設定、IR センサはオープンコレクタ出力のためプルアップを有効にします。

adc_vref,3 // A/D リファレンス電圧の設定

リモートデバイスの設定を行うと、下記のサンプリングイベントデータ文字列が格納された XBee フレームデータが10分に1回サーバーに送信されます。

センサーノード1(XBee Device4) から送信される SAMPLING イベントデータ

$$$,SAMPLING,0D04,8,FF,2,0,0,0,76

カンマ区切りで表現されたイベントデータの第6カラム(値は2) に IR センサが10分間に何回アクティブになったかを示すカウント値が入っています。また、第10カラム(値は76) には A/D #3 に接続された LM35 のアナログ出力値が格納されています。

上記の SAMPLING イベントデータを格納した XBee フレームデータを受信したときに、DeviceServer は XBEE_TDCP_DATA イベントハンドラ(Luaスクリプト) を実行します。

file_id = "XBEE_TDCP_DATA"

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
APIType			フレームデータ中のAPI Type(16進数2桁)					81

SourceAddress	フレームデータ中のSourceAddress
				16bit アドレスの場合(16進数4桁)							0A01
				64bit アドレスの場合(16進数16桁)						0013A200404AC397

SerialNumber	XBee デバイスの SerialNumber
				DeviceServer に保持されたマスターファイルを使用して、
				SourceAddress から変換した値が設定される。				0013A200404AC397

NodeIdentifier	XBee デバイスの NodeIdentifier。
				DeviceServer に保持されたマスターファイルを使用して、
				SourceAddress から変換した値が設定される。				Device1

RSSI			フレームデータ中のRSSI(16進数2桁)						45

Options			フレームデータ中Options									00

TDCP_WHOLE		カンマ区切りのTDCP データ全体							"$$$,CHANGE_DETECT,0A01,8,01,FE"

TDCP_COUNT		TDCP データカラム数										2

TDCP_<Column#>	TDCP データ値(ASCII 文字列)
				TDCP_1 は常にコマンドプリフィックス文字列を表す			"$$$1234"
				"$$$" で始まり、0文字以上の任意の文字列が後に続く。

				TDCP_2 はコマンド実行ステータスを表す					"1"
				"1" はコマンド実行成功、"0" は失敗を示す
				イベントデータの場合にはイベント名が入る

				TDCP_3以降のデータはTDCPコマンド毎に決められた、
				オプション文字列が入る	

				<Column#> には 最大、TDCP_COUNT まで 1から順番に
				インクリメントされた値が入る。

]]

log_msg(g_params["NodeIdentifier"] .. "[" .. g_params["SourceAddress"] .. "," .. g_params["SerialNumber"] .. "] TDCPData = " .. g_params["TDCP_WHOLE"],file_id)

-------------------------------------------------------------------
-- サンプリングデータをデータベースに登録する
-- 時間がかかる処理を行うため、別スレッドでスクリプトを実行します
-------------------------------------------------------------------
if not script_fork_exec("XBEE_SENSOR_DATA_STORE",g_params) then error() end

イベントハンドラスクリプトの最後の部分に処理が記述されています。

サンプリングデータを解析してセンサーデータのデータベース登録や、Xively クラウドサービスへ送信するときに時間がかかる場合がありますので、処理部分を別スクリプト XBEE_SENSOR_DATA_STORE に作成して、このスクリプトを別スレッドで実行するようにしています。このように別スレッドに処理に分けることで、XBEE_TDCP_DATA イベントハンドラに別の処理を行うためにスクリプトを追加した場合でも、それらの処理開始時間がクラウド側へのデータ送信時間などに影響されないようになります。

XBEE_TDCP_DATA イベントハンドラに渡されているスクリプトパラメータ(g_params[] )は、連想配列のまま全て XBEE_SENSOR_DATA_STORE スクリプトのスクリプトパラメータとして渡します。XBEE_SENSOR_DATA_STORE は下記の様になっています。

file_id = "XBEE_SENSOR_DATA_STORE"

--[[

●機能概要

TDCP デバイスから送信されたサンプリングデータを DeviceServerに登録
するスクリプトの例です。このスクリプトはユーザー環境に合わせて
カストマイズして使用してください。

パラメータで指定した XBeeデバイスに接続している TDCPボード上の
センサーデータを取得してデータベースに登録します。

このスクリプトは、リモートデバイスから定期的に送信されてくる
SAMPLINGイベントのイベントハンドラ中からコールされて使用できるように
しています。

リモートデバイスに接続した各種センサーから、I2C、SPI、A/D変換入力、
デジタルポート、カウンタ入力ポート経由で現在の測定値を取得します。
測定した値は、センサーデータとして扱いやすい値に変換した後
データベースに登録します。

リモートデバイスごとに、接続しているセンサーが異なる場合には、
スクリプト内でデバイスの NodeIdentifier を元に処理を分けることができます。

データベースに登録するときには、キー名に NodeIdentifier と センサー種別
を示す文字列を含めると、後のデータ処理が行い易くなります。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

TDCP_1			TDCPイベント文字列第1カラム(Prefix文字列)				"$$$"

TDCP_2			TDCPイベント文字列第2カラム(イベント種別)				"SAMPLING

TDCP_3			送信元XBee 16ビットアドレス								"0D04"

TDCP_4			TDCPイベント文字列第3カラム(app_mode)					"8"

TDCP_5			TDCPイベント文字列第4カラム(イベントデータ#1)			"0F"

TDCP_6			TDCPイベント文字列第5カラム(イベントデータ#2)			"0"
..
..

TDCP_N			TDCPイベント文字列第4カラム(イベントデータ#n)			"123"

( XBEE_TDCP_DATA イベントハンドラに渡された全てのパラメータを、このスクリプトを
  コールする時にも指定する)

NodeIdentifier		XBee デバイスのNodeIdentifier						"Device1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

●変更履歴

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["NodeIdentifier"]) then
	log_msg("parameter error",file_id)
	error()
end

local dev = g_params["NodeIdentifier"]
local param = {}

---------------------------------------------------------------------------------
-- XBeeデバイスに接続された TDCPリモートCPUボードに、以下のセンサーデバイスが
-- 接続されている場合の例です, app_mode は 8 に設定されているものとします
--   * アナログ温度センサ	(A/D#3に接続)
--   * IR(人感)センサ		(DIO#4/COUNTER入力接続)
---------------------------------------------------------------------------------

--------------------------------------------------------------------------------
-- TDCP イベント種別、TDCP app_modeが一致するものだけを選択して
-- サンプリングデータをデータベースに格納する。
--------------------------------------------------------------------------------
if (g_params["TDCP_2"] == "SAMPLING") and (g_params["TDCP_4"] == "8") then 

	---------------------------------------------
	-- Xively サービスに登録するためのパラメータ
	---------------------------------------------
	local xively_ch_data = {}

	-----------------------------------------------------------
	-- A/D#3 に接続された LM35センサから温度データを取得
	-----------------------------------------------------------
	local temperature = (100 * 2.56 * tonumber(g_params["TDCP_10"])) / 1024;

	-----------------------------------------------------------------
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_TP_<NodeIdentifier>"
	-----------------------------------------------------------------
	if not add_stat_data("SENSOR_TP_" .. dev,tostring(temperature)) then error() end
	if not set_shared_data("SENSOR_TP_" .. dev,tostring(temperature)) then error() end
	xively_ch_data["SENSOR_TP_" .. dev] = tostring(temperature) -- for Xively

	-----------------------------------------------------------------------------
	-- SAMPLING イベント発生時の COUNTER値 はIRセンサーのカウンタ値が格納されている
	--
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_IR_<NodeIdentifier>"
	-----------------------------------------------------------------------------
	if not add_stat_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	if not set_shared_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	xively_ch_data["SENSOR_IR_" .. dev] = g_params["TDCP_6"] -- for Xively

	-----------------------------------------------------------------------------
	-- Xively サービスにセンサーデータを登録
	-----------------------------------------------------------------------------
	if not script_exec("XIVELY_DATA_STORE",xively_ch_data) then error() end

end

スクリプトの最初の部分で、イベントハンドラからコールされたこのスクリプトが処理対象とするかどうかを判断しています。イベント名が格納されたスクリプトパラメータの内容が “SAMPLING” になっているかと、イベントデータ中に格納された TDCP モニタプログラムの動作モードが設定した内容と一致しているかをチェックしています。これによって将来別のセンサを搭載したセンサーノードを追加したときに、処理をわけることができます。

このスクリプトでは Xively にセンサデータを送信するだけではなく、DeviceServer 内蔵の統計データベースにも同時にセンサデータを格納しています。また、最新のセンサデータを DeviceServer の他のモジュールから利用しやすいようにグローバル共有変数にも格納しています。

XBEE_SENSOR_DATA_STOREスクリプト中に作成した xively_ch_data[] 連想配列変数に、温度センサと IR センサのデータを一時的に格納しています。配列のキー名にはそれぞれ以下の名前を割り当てています。(”Device4″ はセンサーノード1に接続された XBee Series1 の NodeIdentifier 文字列です)

SENSOR_TP_Device4       センサーノード1の温度データ

SENSOR_IR_Device4      センサーノード1の人感センサ(IR) データ

Xively サービスにセンサデータを登録するために作成した XIVELY_DATA_STORE スクリプトのスクリプトパラメータに上記の xively_ch_data[] 連想配列変数 を指定してコールします。XIVELY_DATA_STORE スクリプトの内容は以下になります。

file_id = "XIVELY_DATA_STORE"

--[[

●機能概要

Xivelyの feed にデータを格納する

●スクリプト中の変数に初期設定が必要な項目

---------------------------------------------------------------------------------
変数名			値		            									値の例
---------------------------------------------------------------------------------
feed_id			Xivery サービスに登録したデバイスの Feed ID				"12345678"

apikey			Xivery サービスに登録したアカウントに割り当てられた
				ApiKey 文字列	 	"sadfj3225n25thisisasampleapikeyj2kn2kjk3j53"

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

<channel_id#1>	Xiveryサービスに登録したデバイス中のチャンネル文字列をスクリプト
				パラメータのキーとして指定する。パラメータ値には、
				現在の測定値を文字列で格納する							"1.234"
..
..
<channel_id#n>

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

Xively API にアクセスするときに TCP/80(http) ポートを使用しています。

スクリプトパラメータに指定した複数のチャンネルに対するデータ値を同時に
登録することができます。パラメータで指定した各チャンネルのデータ値は
current_value に登録されます。

●変更履歴

2014/05/07	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

local feed_id = "<your_Xively_device_feedid>"	-- 自分の環境に合わせて変更してください
local apikey = "<your_Xively_API_key>"			-- 自分の環境に合わせて変更してください

local host = "api.xively.com"
local port = 80
local path = "/v2/feeds/" .. feed_id ..  ".json"

-----------------------------------------------------------------------------------
-- スクリプトパラメータで渡されたチャンネル毎のセンサデータを JSON 文字列に変換する
-----------------------------------------------------------------------------------
local json_str = '{ "version": "1.0.0", "datastreams": ['
local first = true
for key,val in pairs(g_params) do
	if first then
		first = false
	else
		json_str = json_str .. ","
	end
	json_str = json_str .. string.format('{"id":"%s","current_value": "%s"}',key,val)
end
json_str = json_str .. ']}'
log_msg(json_str,file_id) -- 登録データ確認用にログ出力

---------------------
-- HTTP PUT データ
---------------------
local data = {}
table.insert(data,json_str)

-----------------------
-- HTTP カスタムヘッダ
-----------------------
local header = {}
header["X-ApiKey"] = apikey

--------------------------------------------
-- Xively API をコールしてデータ登録
--------------------------------------------
local stat,rpl = http_put(host,port,path,data,header)
if not stat then error() end

スクリプトパラメータで渡された複数のセンサーデータ値のキーを Xively 側の Channel名としてXively にデータを登録します。センサーデータ値と Channel 名を Xively の登録用 API のリクエストフォーマット文字列(JSON) に変換した後、HTTP PUT コマンドでデータを転送しています。

http_put() ライブラリ関数は DeviceServer のスクリプトから HTTP PUT コマンドで任意のデータを送信することができます。またオプションで HTTP ヘッダに任意のキーと値のペアを指定することができます。Xively ではこの HTTP ヘッダ部分にユーザーアカウントに割り当てられた ApiKey 文字列を設定しています。

Xively 側では予めセンサーデータの格納用の Channel を作成しておきます。また、Xively のアカウントを作成したときにアサインされる ApiKey と feed ID をスクリプト中に設定しておきます。

これで、センサーノード1から定期的に送信されるセンサデータがクラウド側に保存されるようになりました。

次に、センサーノード2側の説明をします。センサーノード1と比べると、使用する XBee デバイスが XBee Series1 と XBee-ZB Series2の違いだけで、ほぼ処理の流れは同じです。

センサーノード2に使用しているモニタプログラムと回路については、こちらの記事この記事で詳しく紹介していますので参照してください。センサーノード1と同様に TDCP モニタプログラムが動作していて、デジタル入力ポートに IR センサが、アナログ入力ポートには照度(CDS)センサが接続されています。また、温度センサは I2C バスに接続するタイプのためアナログ入力ではなく SDA, SCL ラインに接続しています。

TDCP モニタプログラムでは以下の設定を行います。

sampling_rate,600  // 定期的にサンプリングデータをサーバーに送信する間隔(10分)

dio_config,00  // DIO ポートを全てデジタル入力として使用する

dio_pullup,0F  // デジタル入力ポートのプルアップ設定、IR センサはオープンコレクタ出力のためプルアップを有効にします。

adc_vref,1 // A/D リファレンス電圧の設定

i2c_use,1  // I2C 機能を有効にする

リモートデバイスの設定を行うと、下記のサンプリングイベントデータ文字列が格納された XBee-ZB フレームデータが10分に1回サーバーに送信されます。

センサーノード1(XBee-ZB Node1) から送信される SAMPLING イベントデータ

$$$,SAMPLING,32,0F,0,16,804,797,791,787

カ ンマ区切りのイベントデータの第6カラム(値は16) に IR センサが10分間に何回アクティブになったかのカウント値が入っています。また、第7カラム(値は804) には A/D #0 に接続された CDS のアナログ出力値が格納されています。

ここでは、I2C バスに接続された温度センサ(TMP102)の値は、サンプリングイベントデータには含まれていない点に注意してください。I2Cバス接続のデバイスはスレーブデバイスの指定と、レジスタへの書き込みや読み込みトランザクションを実行して、初めてデータを取得できます。これらのデバイス毎に異なる I2Cバス操作は SAMPLING イベントでは行えません。このため、今回は SAMPLING イベント受信時に実行されるイベントハンドラを実行するタイミングで、サーバー側からリモートコマンドを実行して I2Cバスを操作して温度センサからデータを取得します。(詳しくは後述)

SAMPLING イベントデータを含んだ XBee-ZB フレームデータを受信したときには、DeviceServer では ZB_TDCP_DATA イベントハンドラが実行されます。

file_id = "ZB_TDCP_DATA"

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
FrameType		フレームデータ中のFrame Type
				(16進数2桁)												90

SourceAddress	フレームデータ中のSourceAddress
				64bit アドレス(16進数16桁)								0013A200404AC397

NetworkAddress	フレームデータ中の SourceNetworkAddress
				16bit アドレス(16進数4桁)								D565

NodeIdentifier	XBee デバイスの NodeIdentifier。
				DeviceServerのマスターファイルを検索して設定される。	Node1
				マスターにNodeIdentifier未登録の場合は"" が設定される

DeviceType		XBee デバイスの Device Type
				DeviceServerのマスターファイルを検索して設定される。	01
				マスターにDeviceType未登録の場合は"" が設定される
				8bit値(16進数2桁)
				00: coordinator
                01: router
                02: end device

DeviceTypeID	XBee デバイスの Device Type Identifier
				DeviceServerのマスターファイルを検索して設定される。
				マスターにDeviceTypeID未登録の場合は"" が設定される
				32bit値(16進数8桁)										00030000

ReceiveOptions	フレームデータ中 ReceiveOptions							01
				8bit値(16進数2桁)

TDCP_WHOLE		カンマ区切りのTDCP データ全体							"$$$,CHANGE_DETECT,8,01,FE"

TDCP_COUNT		TDCP データカラム数										2

TDCP_<Column#>	TDCP データ値(ASCII 文字列)
				TDCP_1 は常にコマンドプリフィックス文字列を表す			"$$$1234"
				"$$$" で始まり、0文字以上の任意の文字列が後に続く。

				TDCP_2 はコマンド実行ステータスを表す					"1"
				"1" はコマンド実行成功、"0" は失敗を示す
				イベントデータの場合にはイベント名が入る

				TDCP_3以降のデータはTDCPコマンド毎に決められた、
				オプション文字列が入る	

				<Column#> には 最大、TDCP_COUNT まで 1から順番に
				インクリメントされた値が入る。

]]

log_msg(g_params["NodeIdentifier"] .. "[" .. g_params["NetworkAddress"] .. "," .. g_params["SourceAddress"] .. "," .. g_params["DeviceType"] .. "," .. g_params["DeviceTypeID"] .. "] TDCPData = " .. g_params["TDCP_WHOLE"],file_id)

-------------------------------------------------------------------
-- サンプリングデータをデータベースに登録する
-- 時間がかかる処理を行うため、別スレッドでスクリプトを実行します
-------------------------------------------------------------------
if not script_fork_exec("ZB_SENSOR_DATA_STORE",g_params) then error() end

XBee Series1 を使用したセンサーノード1のイベントハンドラ XBEE_TDCP_DATA と同様に、最後の部分にデータベース登録や Xively サービスへの送信処理を行うための別スクリプト ZB_SENSOR_DATA_STORE を別スレッドでコールしています。

ZB_SENSOR_DATA_STORE スクリプトには ZB_TDCP_DATA イベントハンドラに渡された全てのスクリプトパラメータが連想配列のまま全て渡しています。ZB_SENSOR_DATA_STORE は下記の様になっています。

file_id = "ZB_SENSOR_DATA_STORE"

--[[

●機能概要

TDCP デバイスから送信されたサンプリングデータを DeviceServerに登録
するスクリプトの例です。このスクリプトはユーザー環境に合わせて
カストマイズして使用してください。

パラメータで指定した XBeeデバイスに接続している TDCPボード上の
センサーデータを取得してデータベースに登録します。

このスクリプトは、リモートデバイスから定期的に送信されてくる
SAMPLINGイベントのイベントハンドラ中からコールされて使用できるように
しています。

リモートデバイスに接続した各種センサーから、I2C、SPI、A/D変換入力、
デジタルポート、カウンタ入力ポート経由で現在の測定値を取得します。
測定した値は、センサーデータとして扱いやすい値に変換した後
データベースに登録します。

リモートデバイスごとに、接続しているセンサーが異なる場合には、
スクリプト内でデバイスの NodeIdentifier を元に処理を分けることができます。

データベースに登録するときには、キー名に NodeIdentifier と センサー種別
を示す文字列を含めると、後のデータ処理が行い易くなります。

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

TDCP_1			TDCPイベント文字列第1カラム(Prefix文字列)				"$$$"

TDCP_2			TDCPイベント文字列第2カラム(イベント種別)				"SAMPLING

TDCP_3			TDCPイベント文字列第3カラム(app_mode)					"32"

TDCP_4			TDCPイベント文字列第4カラム(イベントデータ#1)			"0F"

TDCP_5			TDCPイベント文字列第5カラム(イベントデータ#2)			"0"
..
..

TDCP_N			TDCPイベント文字列第4カラム(イベントデータ#n)			"123"

( ZB_TDCP_DATA イベントハンドラに渡された全てのパラメータを、このスクリプトを
  コールする時にも指定する)

NodeIdentifier		XBee-ZB デバイスのNodeIdentifier					"Node1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

●備考

●変更履歴

]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["NodeIdentifier"]) then
	log_msg("parameter error",file_id)
	error()
end

local dev = g_params["NodeIdentifier"]
local param = {}

---------------------------------------------------------------------------------
-- NodeIdentifierが Node1 の名前をもつ XBee-ZBデバイスに接続された TDCPZB リモート
-- CPUボードに、以下のセンサーデバイスが接続されている場合の例です
--   * TMP102 温度センサ	(I2Cバスに接続)
--   * CDS照度センサ		(A/D#0に接続)
--   * IR(人感)センサ		(DIO#3/COUNTER入力接続)
---------------------------------------------------------------------------------

--------------------------------------------------------------------------------
-- NodeIdentifier, TDCP イベント種別、TDCP app_modeが一致するものだけを選択して
-- サンプリングデータをデータベースに格納する。
--------------------------------------------------------------------------------
if (dev == "Node1") and (g_params["TDCP_2"] == "SAMPLING") and (g_params["TDCP_3"] == "32") then 

	---------------------------------------------
	-- Xively サービスに登録するためのパラメータ
	---------------------------------------------
	local xively_ch_data = {}

	-----------------------------------------------------------
	-- I2Cバスに接続された TMP102 センサーから温度データを取得
	-----------------------------------------------------------
	param["device"] = dev
	stat,result = script_exec2("ZB/DEVICE/TMP102_READ",param)
	if not stat then error() end

	-----------------------------------------------------------------
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_TP_<NodeIdentifier>"
	-----------------------------------------------------------------
	if not add_stat_data("SENSOR_TP_" .. dev,result["temperature"]) then error() end
	if not set_shared_data("SENSOR_TP_" .. dev,result["temperature"]) then error() end
	xively_ch_data["SENSOR_TP_" .. dev] = result["temperature"] -- for Xively

	-----------------------------------------------------------------------------
	-- SAMPLING イベント発生時の A/D#0 (TDCP_7) は光センサーの値が格納されている
	--
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_LUMI_<NodeIdentifier>"
	-----------------------------------------------------------------------------
	if not add_stat_data("SENSOR_LUMI_" .. dev,g_params["TDCP_7"]) then error() end
	if not set_shared_data("SENSOR_LUMI_" .. dev,g_params["TDCP_7"]) then error() end
	xively_ch_data["SENSOR_LUMI_" .. dev] = g_params["TDCP_7"] -- for Xively

	-----------------------------------------------------------------------------
	-- SAMPLING イベント発生時の COUNTER (TDCP_6) はIRセンサーのカウンタ値が格納されている
	--
	-- 統計データベースに測定値を保存、共有データには最新の値を保存
	-- キー名  "SENSOR_IR_<NodeIdentifier>"
	-----------------------------------------------------------------------------
	if not add_stat_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	if not set_shared_data("SENSOR_IR_" .. dev,g_params["TDCP_6"]) then error() end
	xively_ch_data["SENSOR_IR_" .. dev] = g_params["TDCP_6"] -- for Xively

	-----------------------------------------------------------------------------
	-- Xively サービスにセンサーデータを登録
	-----------------------------------------------------------------------------
	if not script_exec("XIVELY_DATA_STORE",xively_ch_data) then error() end

end

処理の内容はほぼセンサーノード1の XBEE_SENSOR_DATA_STORE と同じですが、SAMPLING イベント中に含まれる、IR センサのカウント値と CDS のA/D 変換値データのカラム位置が XBee の場合と少し違っています。詳しくはスクリプト中のコメントをご覧ください。

温度データは別途リモート CPU ボードに接続した I2Cバスを操作する必要があります。これは別に作成したスクリプト TMP102_READ をコールすることで実現します。

local dev = g_params["NodeIdentifier"]

local param = {}

param["device"] = dev

stat,result = script_exec2(“ZB/DEVICE/TMP102_READ”,param)

スクリプトパラメータに XBee-ZB の NodeIdentifier を格納した param 連想配列を指定してコールします。script_exec2() 関数は同じスレッド中で別の lua スクリプトを実行してリターンパラメータを取得するライブラリ関数です。

スクリプト名に “/” で区切ったフォルダ名を指定しています。今回の場合には DeviceServer のデフォルトスクリプトフォルダ “C:\Program Files (x86)\AllBlueSystem\Scripts” (Windows7 64bit の場合) フォルダの中の “C:\Program Files (x86)\AllBlueSystem\Scripts\ZB\DEVICE\TMP102_READ.lua” ファイルを実行します。スクリプトの内容は以下になります。

file_id = "TMP102_READ"

--[[

●機能概要

I2C バスに接続した温度センサー(TMP102) の値を取得する

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

device		XBee-ZB デバイス64ビットアドレスまたは NodeIdentifier	"Node1"

●リターンパラメータ

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

temperature			センサーから取得した摂氏温度						"12.5"
					                                                    "-25.0"

●備考

●変更履歴

2014/04/23	初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

local slave_addr = "48"

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["device"]) then
	log_msg("parameter error",file_id)
	error()
end

----------------------------------------
-- 12 bit 幅の2の補数を符号付整数に変換
----------------------------------------
function calc_2comp(val)
	if(bit_and(val,0x800) ~= 0) then
		return -1 * (bit_and(bit_not(val),0xfff) + 1)
	else
		return val
	end
end

-----------------------------------------------------------------------
-- TMP102温度レジスタの値を取得する
-- pointer register 0x00 をセットした後、2 バイトのレジスタ値を取得する
-----------------------------------------------------------------------
stat,result = zb_tdcp_safe_retry(g_params["device"],"i2c_write," .. slave_addr .. ",00,2")
if (not stat) or (result[2] == "0") then error() end

---------------------------------------
-- 温度レジスタ値から摂氏温度を計算する
---------------------------------------
local reg = {}
reg = hex_to_tbl(result[3])

local temp_int = bit_lshift(reg[1],4) + bit_rshift(reg[2],4)
local temperature = 0.0625 * calc_2comp(temp_int)
script_result(g_taskid,"temperature",string.format("%3.1f",temperature))

リモートCPU ボードで動作している TDCP モニタプログラムの i2c_write コマンドを実行して、I2C バスを操作しています。

i2c_write,48,00,2

を実行すると、I2C スレーブアドレス 0×48 を持つ TMP102 温度センサに、レジスタアドレス 0×00 を書き込んだ後、2バイト分のデータを続けて読み込みます。この2バイトのセンサデータから摂氏温度を計算して、結果をスクリプトリターンパラメータ ”temperature” に設定してスクリプトを終了します。その後コールを行った ZB_SENSOR_DATA_STORE に制御が戻ります。

ZB_SENSOR_DATA_STORE中の xively_ch_data[] 連想配列変数に、温度値、IR センサのデータ、CDS の値を格納します。xively_ch_data[] 連想配列のキー名にはそれぞれ以下の名前を割り当てています。(”Node1″ はセンサーノード2に接続された XBee-ZB Series2 の NodeIdentifier 文字列です)

SENSOR_TP_Node1       センサーノード2の温度データ

SENSOR_IR_Node1     センサーノード2の人感センサ(IR) データ

SENSOR_LUMI_Node1 センサーノード2の CDSセンサデータ

このxively_ch_data[] 連想配列変数をスクリプトパラメータに指定して、センサーノード1の時と同じXIVELY_DATA_STORE スクリプトをコールして、Xively サービスにデータを登録します。

これで、センサーノード2から定期的に送信されるデータもクラウド側に保存されるようになりました。

センサーノード1とセンサーノード2からセンサーデータが送信されているときのログは下記のようになります。

定期的に SAMPLING イベントデータが送信されている様子がわかります。センサーノード2からイベントを受信したときには同時に、リモート側の I2Cバスを操作している様子も記録されています。Xively サービスには HTTP PUT コマンドで JSON 文字列を送信しています。このJSON 文字列の内容もログに出力していますので Xively を利用するときの参考にしてください。

一日分のデータを参照するために、Xively のアカウントにログインしてセンサーデータをグラフ表示してみました。

クラウドサービスを使用すると、すぐにセンサーデータの傾向がつかめるので、センサーネットワークを構築する開発段階に非常に強力なツールとなると思います。また、複数の拠点にサーバーPCに分けて設置した場合でも、全体のセンサーデータをまとめてクラウドサービスで表示できますので、分散センサーネットワークのデータ参照システムを簡単に作成できると思います。

それではまた。

XBee デバイスが通信可能かどうかを常に監視する(WATCHDOG)

XBee デバイスや XBee-ZB デバイスを利用したリモートシステムを構築するときに、リモート側の機器が通信可能かどうかを監視する必要があると思います。このときに利用できるワッチドッグ方式の実現方法について説明します。

ここでは、リモート機器として XBee-ZB デバイスとマイコンを組み合わせた下記のような簡単なセンサーノードを利用していることを想定しています。XBee-ZB や XBee 単体だけで運用している場合でも、簡単な変更でこの記事で紹介するワッチドッグ方式の監視を実現できます。

このリモートノードは XBee-ZB (series2) とマイコン(ATmega328P)を組み合わせた CPUボードで、マイコンには簡単なモニタプログラムが組み込んであります。このリモート側CPU ボードの詳しい内容については、こちらの記事でも紹介していますのでご覧ください。

リモート機器側で動作しているモニタプログラム(TDCPZB for ATmega328) には定期的に XBee の RF データパケットを送信する “LIVE” イベント送信機能があり、この機能を利用して 20分に一回 Coordinator XBee-ZB が接続されているサーバーPC にデータを送信するように設定します。サーバーPC 側では定期的に “LIVE” イベントの受信状態を監視して、もしリモート側からのイベントが到着していない場合には、何らかの通信障害が発生しているとして警報メールを送信します。

今回の記事で紹介するリモート側の XBee は、XBee 802.15.4 Series1 デバイス が2つで、NodeIdentifier はそれぞれ “Device2″, “Device4″ になっています。また XBee-ZB Series2 デバイス2つも同時に監視していて、それぞれの NodeIdentifier は “Node1″, “Node3″ になっています。全てのリモート側の機器には XBee にはマイコンが接続されていて、上記のモニタプログラムから定期的にイベントを送信するように設定します。

XBee デバイス一覧です。Device2, Device4 がリモート側に設置されていて Device3 はサーバーPC に接続されています。

XBee-ZB デバイス一覧です。Node1, Node3 がリモート側に設置されていて Node2 はサーバーPC に接続されています。

モニタプログラムにはリモートから “heartbeat_rate” コマンドで送信間隔を設定します。今回は20分(1200秒)ごとに送信するようにします。

このコマンドの後に “config_save” コマンドを実行して、リモート機器側マイコンに搭載された不揮発メモリ(EEPROM) に設定値を保存します。このコマンドを全てのリモート機器に対して実行します。上記は XBee-ZB デバイスを管理しているプログラムの実行例です。XBee Series1に対しては “XBee” ツールボタンで表示される “XBee デバイス管理” プログラムから同様の設定を行います。

この設定によってサーバーPCはリモート機器からは 20分に一回、以下の文字列が含まれた RF データパケットを受信します。以下はこのときのログメッセージです。

(XBee Series1 の場合)

$$$,LIVE,0D04,8

(XBee Series2 の場合)

$$$,LIVE,32

それぞれの第2カラムには “heartbeat_rate” コマンドで設定した LIVE イベントを示す “LIVE”という文字列が格納されています。この RF データパケットをサーバー側で受信すると、XBee Series1 の場合には Lua で記述された以下のイベントハンドラスクリプト XBEE_TDCP_DATA が実行されます。

file_id = "XBEE_TDCP_DATA"

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
APIType			フレームデータ中のAPI Type(16進数2桁)					81

SourceAddress	フレームデータ中のSourceAddress
				16bit アドレスの場合(16進数4桁)							0A01
				64bit アドレスの場合(16進数16桁)						0013A200404AC397

SerialNumber	XBee デバイスの SerialNumber
				DeviceServer に保持されたマスターファイルを使用して、
				SourceAddress から変換した値が設定される。				0013A200404AC397

NodeIdentifier	XBee デバイスの NodeIdentifier。
				DeviceServer に保持されたマスターファイルを使用して、
				SourceAddress から変換した値が設定される。				Device1

RSSI			フレームデータ中のRSSI(16進数2桁)						45

Options			フレームデータ中Options									00

TDCP_WHOLE		カンマ区切りのTDCP データ全体							"$$$,CHANGE_DETECT,0A01,8,01,FE"

TDCP_COUNT		TDCP データカラム数										2

TDCP_<Column#>	TDCP データ値(ASCII 文字列)
				TDCP_1 は常にコマンドプリフィックス文字列を表す			"$$$1234"
				"$$$" で始まり、0文字以上の任意の文字列が後に続く。

				TDCP_2 はコマンド実行ステータスを表す					"1"
				"1" はコマンド実行成功、"0" は失敗を示す
				イベントデータの場合にはイベント名が入る

				TDCP_3以降のデータはTDCPコマンド毎に決められた、
				オプション文字列が入る	

				<Column#> には 最大、TDCP_COUNT まで 1から順番に
				インクリメントされた値が入る。

]]

------------------
-- BEGIN SCRIPT --
------------------

log_msg(g_params["NodeIdentifier"] .. "[" .. g_params["SourceAddress"] .. "," .. g_params["SerialNumber"] .. "] TDCPData = " .. g_params["TDCP_WHOLE"],file_id)

-------------------------------------------------------------------
-- LIVE イベントを使用してデバイスワッチドッグを行う
-------------------------------------------------------------------
if g_params["TDCP_2"] == "LIVE" then
	if not set_shared_data("WATCHDOG_" .. g_params["NodeIdentifier"],"") then error() end
end

------------------
-- END SCRIPT   --
------------------

イベントハンドラスクリプト中ではイベント送信元の XBee デバイス NodeIdentifier 文字列に “WATCHDOG_” を付けた名前の共有変数をクリアしています。たとえばXBee NodeIdentifier が “Device4″ から送信されていた場合には “WATCHDOG_Device4″ という名前の共有変数をクリア(削除) します。この共有変数はワッチドッグカウンタになっていてサーバー側で定期的にインクリメントされています(後述)。

XBee-ZB Series2 の場合にも同様に Lua で記述された以下のイベントハンドラスクリプト ZB_TDCP_DATA が実行されます。

file_id = "ZB_TDCP_DATA"

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
FrameType		フレームデータ中のFrame Type
				(16進数2桁)												90

SourceAddress	フレームデータ中のSourceAddress
				64bit アドレス(16進数16桁)								0013A200404AC397

NetworkAddress	フレームデータ中の SourceNetworkAddress
				16bit アドレス(16進数4桁)								D565

NodeIdentifier	XBee デバイスの NodeIdentifier。
				DeviceServerのマスターファイルを検索して設定される。	Node1
				マスターにNodeIdentifier未登録の場合は"" が設定される

DeviceType		XBee デバイスの Device Type
				DeviceServerのマスターファイルを検索して設定される。	01
				マスターにDeviceType未登録の場合は"" が設定される
				8bit値(16進数2桁)
				00: coordinator
                01: router
                02: end device

DeviceTypeID	XBee デバイスの Device Type Identifier
				DeviceServerのマスターファイルを検索して設定される。
				マスターにDeviceTypeID未登録の場合は"" が設定される
				32bit値(16進数8桁)										00030000

ReceiveOptions	フレームデータ中 ReceiveOptions							01
				8bit値(16進数2桁)

TDCP_WHOLE		カンマ区切りのTDCP データ全体							"$$$,CHANGE_DETECT,8,01,FE"

TDCP_COUNT		TDCP データカラム数										2

TDCP_<Column#>	TDCP データ値(ASCII 文字列)
				TDCP_1 は常にコマンドプリフィックス文字列を表す			"$$$1234"
				"$$$" で始まり、0文字以上の任意の文字列が後に続く。

				TDCP_2 はコマンド実行ステータスを表す					"1"
				"1" はコマンド実行成功、"0" は失敗を示す
				イベントデータの場合にはイベント名が入る

				TDCP_3以降のデータはTDCPコマンド毎に決められた、
				オプション文字列が入る	

				<Column#> には 最大、TDCP_COUNT まで 1から順番に
				インクリメントされた値が入る。

]]

------------------
-- BEGIN SCRIPT --
------------------

log_msg(g_params["NodeIdentifier"] .. "[" .. g_params["NetworkAddress"] .. "," .. g_params["SourceAddress"] .. "," .. g_params["DeviceType"] .. "," .. g_params["DeviceTypeID"] .. "] TDCPData = " .. g_params["TDCP_WHOLE"],file_id)

-------------------------------------------------------------------
-- LIVE イベントを使用してデバイスワッチドッグを行う
-------------------------------------------------------------------
if g_params["TDCP_2"] == "LIVE" then
	if not set_shared_data("WATCHDOG_" .. g_params["NodeIdentifier"],"") then error() end
end

------------------
-- END SCRIPT   --
------------------

動作内容は XBee Series1 とまったく同じで、ワッチドッグカウンタをクリアするだけです。

次にサーバー側で定期的(1分に一回)に実行される PERIODIC_TIMER スクリプトに以下の様な記述を追加します。

file_id = "PERIODIC_TIMER"

--[[

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

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

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

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

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

]]

------------------
-- BEGIN SCRIPT --
------------------

-----------------------------------------------------------------------------
-- TDCP,TDCPZBモニタを搭載したCPUボードのワッチドッグ監視をする
-----------------------------------------------------------------------------
local error_msg = ""
local dev_list = { "Device2", "Device4", "Node1", "Node3"}
for k,v in ipairs(dev_list) do
	-------------------------------------------------------------
	-- デバイス毎の WATCHDOG 監視用カウンタをインクリメントする
	-- このカウンタは LIVE イベント受信時にクリアされる。
	-------------------------------------------------------------
	local flag = "WATCHDOG_" .. v
	stat,val = inc_shared_data(flag)
	if not stat then error() end

	-------------------------------------------------------------
	-- 規定以上カウンタ値が増加した場合にはエラーとする
	-------------------------------------------------------------
	if tonumber(val) >= 25 then
		error_msg = error_msg .. " " .. v
		if not set_shared_data(flag,"") then error() end
	end
end

--------------------------------------------------------------------------
-- WATCHDOG エラーが発生した場合には警報メッセージを電子メールで送信する
--------------------------------------------------------------------------
if error_msg ~= "" then
	local mail_addr = "監視警報メール宛先 <your_mail@your_mail_server.jp>";
	local body = {};
	table.insert(body,"*************************************")
	table.insert(body,"** WATCHDOG ERROR, device =" .. error_msg)
	table.insert(body,"*************************************")
	for key,val in ipairs(body) do
		log_msg(val,file_id)	-- ログにメッセージを出力
	end
	if not mail_send(mail_addr,"","** WATCHDOG アラームメール **",unpack(body)) then error() end
end

PERIODIC_TIMER スクリプトは DeviceServer では1分に一回実行されています。この中で 監視対象の XBee デバイスの NodeIdentifier に “WATCHDOG_” を先頭につけた共有変数をカウンタに見立てて、リモート側からのイベント到着を監視しています。XBee デバイス毎に作成したカウンタは1分ごとにインクリメントされて、25(25分)を超えた場合にはリモート側で異常が発生したものとして、警報メールを送信するようにしています。

警報メールを受信したときの様子は以下になります。

リモート側が正常に動作している場合には、先に設定した LIVE イベントの受信時に実行されるスクリプトでカウンタがクリアされるのでカウンタ値は 20 以上には増加しません。

ワッチドッグ方式はこのように簡単に実現可能で、大量のリモートデバイスを監視する必要があるときに大変便利です。また、リモート側の機器故障やバッテリ消耗、電波状況の一時的な悪化などあらゆる障害を簡単に監視することができます。リモート側の監視デバイス数が少ない場合にはポーリング方式を採用して、定期的にサーバー側から全てのリモート側機器にリモートコマンドを実行して正常終了するかどうかを調べる方法でも有効だと思います。

今回は XBee とマイコンを組み合わせましたが、XBee, XBee-ZB 単体でも簡単に同様の機能を実現できます。XBee, XBee-ZB には IO Sampling イベントを送信する “IR” AT コマンドがありますので、これを利用して定期的に IO Sampling データをサーバー側で受信するように設定します。このとき、同時にXBee デバイス自身の DIO, A/D を最低限1つを有効にするのを忘れないようにします。また “DH”, “DL” ATコマンドで IO Sampling イベント送信先をサーバーPC に設定しておくと、サーバー側では XBEE_IO_DATA (Series1の場合)または ZB_IO_DATA(Series2の場合) イベントハンドラが実行されます。このイベントハンドラ中に、先に説明した WATCHDOG カウンタをクリアする記述を入れておくだけで実現できます。

XBee, XBee-ZB 単体の IO Samplingの間隔は 長くても1分毎なので、サーバー側の PERIODIC_TIMER イベントハンドラ中の監視カウンタ値の上限を 25 からもっと少ない数に設定するようにします。

それではまた。