●概要
今回の記事は、センサーデバイスで取得したデータをクラウドサービスに送信するゲートウエイやエッジデバイス側で、ローカルに保存したデータベースを参照してグラフを作成する Web アプリを紹介します。
センサーデータ取得で使用するデバイスは前回の記事で紹介した Raspberry Pi に接続する環境センサ基板を使用しています。前回の記事ではローカルの液晶ディスプレイに最新のセンサデータを表示していました。
今回はこの機能に、クラウド側 MQTT ブローカにセンサデータを送信(Publish)する機能と同時に、ローカル側に一定期間データを保持するデータベースを構築します。このローカルに保存したデータベースを元に集計グラフ表示を行う Web アプリを作成します。
Web アプリで表示するデータはローカル側に保存したデータですが、クラウド(MQTT ブローカ側) から配信された JSON 形式のデータも同様にローカルデータベースに保存することでグラフ表示することができます。Web アプリは PC やタブレット等の Web ブラウザで動作しますので、手軽にデータの傾向を確認することができます。
グラフタイプは折れ線グラフとバーグラフを選択できます。任意の集計期間を設定できますので細かくデータの変化を調べたり、長期間にわたるデータ値の傾向を簡単にチェックできます。複数のデータ列を1つのグラフに同時に表示できますので、センサデータの比較なども簡単に行えます。
●データの流れ・全体図
今回のシステムのデータの流れは次の図のようになります。矢印の部分が主なデータと処理の流れを示しています。
Raspberry Pi に接続した環境センサ基板や外付けしたセンサデータを I2C バスで取り込んだ後(1)、このデータをRaspberry Pi で動作している abs_agent サーバー内のインメモリデータベースに登録します(2)。
(1) で取得したセンサデータをクラウド側の MQTT ブローカにも JSON フォーマットで送信できます(3)。
もし、クラウド側の MQTT ブローカで購読しているセンサデータ等があれば、これを受信してインメモリデータベースに格納することができます(4)。
インメモリデータベースに格納するセンサデータは、センサの種別毎に任意のキー名を指定して時系列で保存します。
任意のタイミングで、PC やタブレット等の Web ブラウザから abs_agent にアクセスして Web アプリケーションを起動します。Web アプリ内でインメモリデータベースのデータを集計してグラフ表示を行います(5)。
今回のアプリでは MQTT への送・受信等の機能も説明していますが、センサデータをグラフ表示するだけであれば、上記の (1),(2),(5) の機能だけでも動作します。
●ハードウエア構成
ハードウエアは Raspberry Pi と保存対象とする何らかのセンサデバイスがあれば動作確認できます。Raspberry Pi は ver1,2,3 のどれでも構いませんが、ネットワークデバイス(Ethernet or Wi-Fi)は必須です。
Raspberry Pi に接続するセンサデバイスが無い場合でも、MQTT ブローカから何らかのセンサデータの配信を JSON フォーマットで受けることができれば、そのデータをグラフ化することができます。今回紹介する主な機能のインメモリデータベースと Web アプリ(Web API) 機能は インテル x86 版の abs_agent でも同様に動作しますので、MQTT ブローカから購読したセンサデータをグラフ化するのであれば通常の PC でセットアップしても構いません。この場合にはPC に Debian GNU/Linux 8 をインストールしてください。
abs_agent が出力するログメッセージを参照・保存する場合には、Raspberry Pi からネットーワーク経由でアクセス可能なWindows PC が1台必要になります。OS は Windows XP SP3 以降であればどれでも動作します。
センサデバイスは、前回の記事の時に使用した Raspberry Pi の拡張ピンに直接接続できるスイッチサイエンス社で販売されている環境センサ基板を使用します。また、2つのセンサデータを温度グラフで比較できるように、TMP102 温度センサを追加で接続してみました。このTMP102 温度センサのデータもインメモリデータベースに同時に取り込みます。
●ソフトウエア構成
Raspberry Pi では raspbian OS と オールブルーシステムが提供する abs_agent プログラムを動作させます。abs_agent を動作させるために、OS のセットアップ後に必要なライブラリやカーネルモジュールは一切ありません。インストールキットはここからダウンロードしてください。abs_agent のインストール方法については、前回の記事 と abs_agent ユーザーマニュアルをご覧ください。
Web API アクセス時の詳細ログや、スクリプト中から出力されるログメッセージを確認したい場合にはログサーバーを設置してください。上記のダウンロードページから” ABS-9000 LogServer”インストールキットを Windows PC にダウンロードして、インストーラを起動してください。Windows PC 側ではポート開放等の操作を行う必要がありますので、詳しいインストール手順は abs_agent ユーザーマニュアルをご覧ください。
abs_agent インストールキットをダウンロードした後、tar コマンドでファイルを展開するだけでインストールは完了します。最新のインストールキットには、今回の記事で紹介している全てのスクリプトとWeb アプリのソースが含まれていますので、簡単にセットアップは完了します。
(2018/11/06 追記:abs_agent インストールキットのバージョンによって グラフWeb アプリが格納されているディレクトリが <abs_agentインストールディレクトリ>/webroot/app/summary に変更されています。その場合は記事中の webroot/app/chart 部分を適宜読み替えて下さい)
その後、abs_agent を起動します。起動コマンドは “sudo abs_agent -l <log_server>” です。ログサーバーを設置していない場合には “sudo abs_agent” になります。下記のコマンド実行例はabs_agent をリスタートさせています。動作中の abs_agent を停止させる agent_shutdown コマンドを実行させた後、agent_stat がエラーを返すことで完全にサーバーが停止しているのを確認してから abs_agent の起動コマンドを実行しています。
●I2C バス経由でセンサデバイスを操作してデータ取得 全体図中の(1)
ここからは、Raspberry Pi に接続されたセンサデバイスを操作して、データを取得するスクリプトをセットアップします。
Raspberry Pi と拡張バスで接続した環境センサ基板には、BME280 センサと明るさセンサが搭載されています。環境センサ基板の拡張バスにピンを付けてI2C バスと電源ラインを引き出しています。このピンをブレッドボードと接続して TMP102 温度センサを接続しています。(ハードウエア構成項の写真をご覧ください)
センサデバイスを Raspberry Pi から操作するのは、abs_agent に設置した Lua スクリプトを使用します。BME280 と明るさセンサをI2C バスでアクセスしてデータを取得するスクリプトについては前回の記事で紹介しています。今回追加したTMP102 温度センサにアクセスしてデータを取得するスクリプト(RASPI/DEVICE/TMP102_READ)は下記のようになっています。
--[[
●機能概要
I2C バスに接続した温度センサー(TMP102) の値を取得する
●リクエストパラメータ
---------------------------------------------------------------------------------
キー値 値 値の例
---------------------------------------------------------------------------------
bus I2C バス番号 "1"
"0" または "1"を指定、省略時は "1" を使用する
●リターンパラメータ
---------------------------------------------------------------------------------
キー値 値 値の例
---------------------------------------------------------------------------------
temperature センサーから取得した摂氏温度 "12.5"
"-25.0"
●備考
●使用条件・免責事項
このスクリプトファイルは自由にお客様が複製、改変を行うことができます。またお客様のアプリケーションと
共にこのファイルを配布することができます。
このスクリプトは公開されているデバイス仕様書を元に、オールブルーシステムがサンプル実装したものです。
お客様がこのファイルを利用される場合には、お客様または第三者に損害が生じた場合でも、
オールブルーシステムは損害賠償その他一切の責任を負担しません。
●変更履歴
2016/05/10 abs_agent RASPI H/W モジュール用に移植
2014/04/23 初版作成
ABS-9000 DeviceServer copyright(c) All Blue System
]]
local slave_addr = "48"
local bus = 1
-------------------------
-- パラメータチェック
-------------------------
if g_params["bus"] then
bus = tonumber(g_params["bus"])
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 stat,result = raspi_i2c_write(bus,slave_addr,"00",2)
if not stat then error() end
---------------------------------------
-- 温度レジスタ値から摂氏温度を計算する
---------------------------------------
local reg = {}
reg = hex_to_tbl(result)
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))
Raspberry Pi のI2C バス#1のスレーブアドレス 0×48 にアクセスして、データレジスタを Read するだけで温度を取得できます。レジスタデータは 2 の補数で表現されているので、これを符号付整数に戻した後、係数を掛けて摂氏温度に変換しています。
ここで、コンソールから BME280, 明るさセンサ、TMP102 温度センサを操作してデータを取得してみます。それぞれのセンサを取得するスクリプトを agent_script コマンドで実行して、リターンパラメータにセンサデータが返ってくるのを確認します。
スクリプト実行時に I2C バス経由でデバイスを操作しますが、同時に他のプロセスで同じデバイスを操作していても ライブラリ関数内部で適切に排他制御されます。このため、前回の記事で作成した表示アプリと今回のデータ取得動作をバックグランドで同時に動作させることができます。
●センサデータをインメモリデータベースに保存 全体図中の(2)
前項でセットアップしたスクリプトを使用して、abs_agent 内のインメモリデータベース(ユーザーマニュアルでは “FASTDB” と略しています)にセンサデータを格納する部分を作成します。インメモリデータベースには、任意のキーとデータ値(Double値)を時系列に格納します。
インメモリデータベースはファイル I/O を使用しないので、Raspberry Pi 等のフラッシュメモリで作成されたファイルシステム上で動作させている場合でも長期間安定して運用することができます。ただし、abs_agent を終了した場合やコンピュータの電源を落とした場合には全てのデータは消えてしまいます。
下記のスクリプト(RASPI/ENVSENSOR_DATA_STORE)で環境センサ基板のセンサーデータをインメモリデータベースに格納します。
--[[
●機能概要
環境センサーボード(スイッチサイエンス社製)に搭載している BME280 センサと光センサの
測定値を MQTT ブローカに送信する。
同時にローカルコンピュータのインメモリデータベース FASTDBにもデータを保存する。
●備考
abs_agent 設定ファイルに MQTT ブローカ接続用のエンドポイントを下記例を参考に作成してください。
ブローカのホスト名や接続ユーザー名、パスワード、ClientID等は環境に合わせて変更します。
エンドポイントタイトル名を変更した場合には、このスクリプト中のタイトル名部分も変更して
ください。
abs_agent.xml ファイル中の MQTTサービスモジュール設定例:
<MQTT>
<AutoOnline type="boolean">True</AutoOnline>
<KeepAliveTimer type="integer">60</KeepAliveTimer>
<EndPointList>
<Item>
<Title>センサーデータ送受信</Title>
<ClientID>abs9k:93501-raspi3</ClientID>
<BrokerHostName>192.168.100.14</BrokerHostName>
<PortNumber>1883</PortNumber>
<AutoSubscribeTopicList>/EnvSensor/+</AutoSubscribeTopicList>
<AutoSubscribeQoSList>0</AutoSubscribeQoSList>
<UserName/>
<Password/>
<WillTopic/>
<WillMessage/>
<WillQoS>0</WillQoS>
<WillRetain>False</WillRetain>
<RecvBuffInit>2048</RecvBuffInit>
<DetailLog>False</DetailLog>
</Item>
</EndPointList>
</MQTT>
●変更履歴
2017/5/1 初版作成
copyright(c) 2017 All Blue System
]]
---------------------------------------------------------------------------------
-- BME280 センサー
---------------------------------------------------------------------------------
local stat,BME280 = script_exec2("RASPI/DEVICE/BME280_READ","","")
if not stat then error() end
---------------------------------------------------------------------
-- 光センサー
---------------------------------------------------------------------
local stat,ENVSENSOR = script_exec2("RASPI/DEVICE/ENVSENSOR_LIGHT_READ","","")
if not stat then error() end
---------------------------------------------------
-- インメモリデータベースに測定データを保存する
---------------------------------------------------
if not fastdb_add("温度(2階)",tonumber(BME280["temperature"])) then error() end
if not fastdb_add("気圧(2階)",tonumber(BME280["pressure"])) then error() end
if not fastdb_add("湿度(2階)",tonumber(BME280["humidity"])) then error() end
if not fastdb_add("明るさ(2階)",tonumber(ENVSENSOR["light"])) then error() end
---------------------------------------------
-- MQTT ブローカに測定データを送信する
---------------------------------------------
local end_point = "センサーデータ送受信"
local topic = "/EnvSensor/" .. g_hostname
local qos = 0
local json_str = '{'
json_str = json_str .. '"temperature":' .. BME280["temperature"]
json_str = json_str .. "," .. '"pressure":' .. BME280["pressure"]
json_str = json_str .. "," .. '"humidity":' .. BME280["humidity"]
json_str = json_str .. "," .. '"light":' .. ENVSENSOR["light"]
json_str = json_str .. '}'
if not mqtt_publish(end_point,topic,json_str,qos) then error() end
前項でコンソールから手動で実行した “RASPI/DEVICE/BME280_READ” と “RASPI/DEVICE/ENVSENSOR_LIGHT_READ” スクリプトをこのスクリプト中から呼び出して結果を取得した後、fastdb_add() ライブラリ関数をコールしてインメモリデータベースに格納しています。
このとき、センサーデータ毎に任意のキー名を指定することで、後の集計操作時にデータ利用し易くできます。fastdb_add() ライブラリ関数の詳しい仕様については abs_agent ユーザーマニュアルをご覧ください。
“RASPI/ENVSENSOR_DATA_STORE” スクリプトには MQTT ブローカへの送信部分も記述されていますが、これは後の項で説明します。
同様に TMP102 温度センサのデータをインメモリデータベースに格納するスクリプト”RASPI/TMP102_DATA_STORE” は以下の様になります。
--[[
●機能概要
TMP102 温度センサの測定値をローカルコンピュータのインメモリデータベース FASTDBに保存する。
●変更履歴
2017/5/6 初版作成
copyright(c) 2017 All Blue System
]]
---------------------------------------------------------------------------------
-- TMP102 センサー
---------------------------------------------------------------------------------
local stat,TMP102 = script_exec2("RASPI/DEVICE/TMP102_READ","","")
if not stat then error() end
---------------------------------------------------
-- インメモリデータベースに測定データを保存する
---------------------------------------------------
if not fastdb_add("温度(TMP102)",tonumber(TMP102["temperature"])) then error() end
これも、先ほどと同様に fastdb_add() ライブラリ関数を使用して、TMP102 温度センサから取得したデータをインメモリデータベースに格納します。
次に定期的にこれらのセンサデータをインメモリデータベースに格納する部分を作成します。上記の “ENVSENSOR_DATA_STORE” と “TMP102_DATA_STORE” スクリプトを定期的にコールするためには、abs_agent で 1分毎に自動起動されている”PERIODIC_TIMER” イベントハンドラスクリプト中に下記の様に記述するだけで完了します。インストールキットに含まれる “PERIODIC_TIMER” イベントハンドラスクリプトには下記の内容は記述されていませんので、ここだけは手動で修正を行って下さい。
今回のシステムでは1 分毎にセンサデータを取得していますが、センサデータ取得間隔を大きくするためにはグローバル共有変数を使用したカウンタを作成することで簡単に10 分や 1 時間単位に変更できます。また、秒単位の間隔でコールしたい場合には “TICK_TIMER” イベントハンドラスクリプト中に同様の記述を行って下さい。
file_id = "PERIODIC_TIMER"
--[[
******************************************************************************
PERIODIC_TIMER イベントハンドラスクリプトは約 1 分に1回自動的に実行されます。
一つのスクリプトの実行は長くても数秒以内で必ず終了するようにしてください。
また、同時実行可能なスクリプトの数に制限があるため、他のスクリプトの実行開始が
待たされる原因にもなります。
頻繁には発生しない条件で、処理時間がかかるスクリプトを実行したい場合は
スクリプトを別に作成して、このイベントハンドラ中から script_fork_exec() を使用して
別スレッドで実行することを検討してください。
******************************************************************************
]]
-- log_msg("start..",file_id)
---------------------------------------------------------------------------------------
-- 環境センサーボード(スイッチサイエンス社製)に搭載している BME280 センサと光センサの
-- 測定値を定期的に MQTT ブローカに送信する
---------------------------------------------------------------------------------------
if not script_fork_exec("RASPI/ENVSENSOR_DATA_STORE","","") then error() end
---------------------------------------------------------------------------------------
-- Raspberry Pi に外付けした TMP102 温度センサのデータを定期的に取得して
-- abs_agent 内部の FASTDB データベースに格納する
---------------------------------------------------------------------------------------
if not script_fork_exec("RASPI/TMP102_DATA_STORE","","") then error() end
------------------------------------------------------------
-- 8時間に一回実行する
------------------------------------------------------------
stat,val = inc_shared_data("TIME_8H")
if not stat then error() end
if (tonumber(val) >= 480) then
if not set_shared_data("TIME_8H","") then error() end -- counter clear
-------------------------------------------------------------
-- FASTDB データベースから 10 日以上過去のデータを削除する
-------------------------------------------------------------
if not script_fork_exec("FASTDB_PURGE","days_before","10") then error() end
end
abs_agent のインメモリデータベースはコンピュータの物理メモリサイズ以上のデータは保持できないので、 定期的に古いデータを削除するスクリプトが上記の “PERIODIC_TIMER” イベントハンドラ中に組み込まれています。
デフォルトでは 10 日以上経過した古いデータは削除されますので、もしこれ以上の期間のデータを保持したい場合には、上記の “FASTDB_PURGE” スクリプトをコールしている script_fork_exec() ライブラリ関数で指定しているパラメータを変更してください。
ここまでのセットアップで、Raspberry Pi に接続した環境センサと TMP102温度センサのデータが1分毎にローカル側のインメモリデータベースに格納されるようになりました。
●MQTT ブローカ接続設定、センサデータをMQTT ブローカに送信 全体図中の(3)
先ほど説明した環境センサ基板のセンサーデータをインメモリデータベースに格納するスクリプト(RASPI/ENVSENSOR_DATA_STORE)中で、MQTT ブローカへセンサーデータを送信する部分のセットアップを行います。
abs_agent ではサーバー設定ファイル(abs_agent.xml) にMQTT ブローカへのエンドポイント(ホスト名やポート番号、デフォルト購読トピック等の設定)を記述することで、スクリプトやイベントハンドラ中から何時でもデータを送信(publish) することが出来るようになります。
abs_agent 起動時には設定ファイルに記述した全てのエンドポイントの MQTT ブローカに自動的に接続して、デフォルトのトピック購読などを開始します。また、接続中には KeepAliveTimer 間隔でPING パケットの処理や購読したトピックメッセージの受信処理、接続エラー発生時の再接続などが全て自動で行われます。
今回、センサーデータを受信するために作成する MQTT エンドポイントの記述例は、下記のようになります。
<MQTT>
<AutoOnline type="boolean">True</AutoOnline>
<KeepAliveTimer type="integer">60</KeepAliveTimer>
<EndPointList>
<Item>
<Title>センサーデータ送受信</Title>
<ClientID>abs9k:93501-raspi3</ClientID>
<BrokerHostName>192.168.100.14</BrokerHostName>
<PortNumber>1883</PortNumber>
<AutoSubscribeTopicList>/EnvSensor/+</AutoSubscribeTopicList>
<AutoSubscribeQoSList>0</AutoSubscribeQoSList>
<UserName/>
<Password/>
<WillTopic/>
<WillMessage/>
<WillQoS>0</WillQoS>
<WillRetain>False</WillRetain>
<RecvBuffInit>2048</RecvBuffInit>
<DetailLog>False</DetailLog>
</Item>
</EndPointList>
</MQTT>
サーバー設定ファイルに MQTT ブローカへの接続を追加した場合には、必ず abs_agent を再起動してください。abs_agent でMQTT ブローカへ接続する場合の詳しい説明はこの記事中に記載していますので是非ご覧ください。
もし、MQTT ブローカへのセンサデータ送信を使用しない場合には、”RASPI/ENVSENSOR_DATA_STORE” スクリプト中で mqtt_publish() ライブラリ関数をコールしている部分をコメントアウトしてください。
●MQTT ブローカで配信されたセンサデータをインメモリデータベースに保存 全体図中の(4)
MQTT ブローカで配信しているセンサデータをインメモリデータベースに登録する設定を行います。abs_agent の設定ファイル中に記述した MQTT ブローカへの接続設定(エンドポイント設定)では下記のトピックを自動で購読するように指定されています。
自動購読トピック: “/EnvSensor/+” , QoS = 0
このトピックは前述の “RASPI/ENVSENSOR_DATA_STORE” スクリプト中で mqtt_publish() ライブラリ関数を使用してデータを送信するときに指定しているトピックと同じにしています。このため、Raspberry Pi からデータを MQTT ブローカに送信すると同時に、同じデータが MQTT ブローカから配信されています。
ここには任意のトピックを複数指定できますので、たとえば追加で “/+/+/io” と “/+/+/tdcp” の2つのトピック名に一致するデータを購読する場合には、設定ファイル中の自動購読を指定するタグに下記の様に記述します。
<AutoSubscribeTopicList>/+/+/io,/+/+/tdcp,/EnvSensor/+</AutoSubscribeTopicList>
<AutoSubscribeQoSList>0,0,0</AutoSubscribeQoSList>
abs_agent が MQTT ブローカで購読中のトピックに一致するデータを受信すると MQTT_PUBLISH イベントハンドラが自動的に実行されます。このイベントハンドラ中にセンサデータをインメモリデータベースに格納する記述を行います。イベントハンドラスクリプトの内容は下記の様になります。
file_id = "MQTT_PUBLISH"
--[[
******************************************************************************
一つのスクリプトの実行は長くても数秒以内で必ず終了するようにしてください。
処理に時間がかかると、イベント処理の終了を待つサーバー側でタイムアウトが発生します。
また、同時実行可能なスクリプトの数に制限があるため、他のスクリプトの実行開始が
待たされる原因にもなります。
頻繁には発生しないイベントで、処理時間がかかるスクリプトを実行したい場合は
スクリプトを別に作成して、このイベントハンドラ中から script_fork_exec() を使用して
別スレッドで実行することを検討してください。
******************************************************************************
MQTT_PUBLISH スクリプト起動時に渡される追加パラメータ
---------------------------------------------------------------------------------
キー値 値 値の例
---------------------------------------------------------------------------------
ClientID エンドポイントの ClientID 文字列 "abs9k:2222-eagle"
Title エンドポイントに設定されたタイトル文字列。
タイトル文字列が設定されていない場合には、"" 空文字列
が入ります "センサーデバイス#1"
MessageType MQTT プロトコルで定義されたメッセージタイプが入ります。 "3"
PUBSLIH メッセージの場合には常に "3"が設定されます
MessageID Brokerから送信するときに使用された MQTT メッセージID が
入ります。(QoS = 1 または QoS = 2 の場合) 値は "1" から
"65535" の整数値をとります。
QoS = 0 の場合には常に "0" が設定されます。 "1234"
Dup MQTT 固定ヘッダ中の Dup フラグの値が設定されます。
"0" または "1" の値をとります。 "0"
QoS MQTT 固定ヘッダ中の QoS フラグの値が設定されます。
"0", "1", "2" の何れかの値をとります。 "0"
Retain MQTT 固定ヘッダ中の Retain フラグの値が設定されます。
"0" または "1" の値をとります。 "0"
PublishTopic MQTT ブローカから受信した PUBLISH メッセージ中の Topic
文字列。 "センサー/ノード1"
PublishData MQTT ブローカから受信した PUBLISH メッセージ中のペイロー
ドデータ。
バイナリデータを16進数文字列に変換したものが格納されます
ペイロードデータに格納されたデータが UTF-8 文字列の場合
には文字列コードのバイト列が格納されています。
イベントハンドラ中でこれらの文字列データをデコードする処
理がデフォルトで記述されていますので、UTF-8 文字列を扱う
場合にはデコード後の変数を利用することができます。 "010203414243"
PublishDataで渡されたペイロードデータを解析して作成される文字列変数
PublishString PublishData に格納されたペイロードデータ部分のサイズが
2048 Bytes以内の場合に、データバイト列を UTF-8形式で
文字列にデコードした結果を PublishString に格納します。
変換対象のバイト列のサイズを変更したいときには該当する
スクリプト部分を変更して下さい。
]]
------------------------------------------------------------------------------------------
-- 受信したペイロードデータのサイズが 2048 bytes 以内の場合には
-- バイナリデータ列を UTF-8 文字列としてデコードしたものを PublishString 変数に格納する
------------------------------------------------------------------------------------------
local PublishString = ""
local pub_len = string.len(g_params["PublishData"]) / 2
if pub_len < 2048 then
PublishString = readUTF_hex(bit_tohex(pub_len,4) .. g_params["PublishData"])
end
log_msg(g_params["Title"] .. "[" .. g_params["ClientID"] .. "] msg:" .. g_params["MessageID"] .. " dup:" .. g_params["Dup"] ..
" retain:" .. g_params["Retain"] .. " qos:" .. g_params["QoS"] .. " topic:" .. g_params["PublishTopic"] .. " " .. PublishString,file_id)
-----------------------------------------------------------------------------------------------
-- センサデータが送信されてきた場合には、FASTDB 集計用データベースにキー値を指定して登録する
-----------------------------------------------------------------------------------------------
-- MQTT PUBLISH パケット例 topic:/EnvSensor/raspi3 {"temperature":21.7,"pressure":1014.8,"humidity":40.2,"light":8}
local sender = string.match(g_params["PublishTopic"],"/EnvSensor/(.+)")
if (sender == "raspi3") then
local msg = g_json.decode(PublishString)
if not fastdb_add("温度(2階)MQTT",msg.temperature) then error() end
if not fastdb_add("気圧(2階)MQTT",msg.pressure) then error() end
if not fastdb_add("湿度(2階)MQTT",msg.humidity) then error() end
if not fastdb_add("明るさ(2階)MQTT",msg.light) then error() end
end
-- MQTT PUBLISH パケット例 topic:/zb/Node1/tdcp {"temperature":14.2,"pressure":1015.7,"humidity":76.6,"ir_count":0,"light":691,"timestamp":"2017/04/...
local rf_type,sender = string.match(g_params["PublishTopic"],"/(.+)/(.+)/tdcp")
if (rf_type == "zb") and (sender == "Node1") then
local msg = g_json.decode(PublishString)
if not fastdb_add("温度(2階リモート)MQTT",msg.temperature) then error() end
if not fastdb_add("気圧(2階リモート)MQTT",msg.pressure) then error() end
if not fastdb_add("湿度(2階リモート)MQTT",msg.humidity) then error() end
if not fastdb_add("赤外(2階リモート)MQTT",msg.ir_count) then error() end
end
-- MQTT PUBLISH パケット例 topic:/xbee/Device4/tdcp {"temperature":13.5,"ir_count":40,"timestamp":"2017/04/08 09:05:55"}
if (rf_type == "xbee") and (sender == "Device4") then
local msg = g_json.decode(PublishString)
if not fastdb_add("温度(1階)MQTT",msg.temperature) then error() end
if not fastdb_add("赤外(1階)MQTT",msg.ir_count) then error() end
end
最初に、MQTT Publish パケットで受信したトピック名を Lua のパターンマッチ関数 string.match() で解析して、大まかな処理を分けています。
その後 abs_agent のライブラリ関数 g_json.decode() を使用して JSON 文字列を Lua言語のテーブル構造に変換します。テーブルのフィールド値は “.” で区切ることで簡単にアクセスできます。このとき、各フィールドの型は、元の JSON で記述されていた型に一致するように変換されています。もし文字列型でデータ値を受信した場合には、tonumber() ライブラリ関数を使用して数値型(浮動小数点型)に変換できます。
JSON データ変換後は、fastdb_add() ライブラリ関数を使用してそれぞれのセンサデータ値をインメモリデータベースに格納します。この時、キー名に任意の文字列を指定できます。
●インメモリデータベースのデータをコンソールから確認
ここまでのセットアップで、Raspberry Pi の I2C バスに接続した環境センサと温度センサのセンサデータが1分に一回取得されてインメモリデータベースに格納されいています。また、MQTT ブローカから購読したトピックのセンサデータを処理している場合には、これらのデータもインメモリデータベースに格納されています。
ここで、データベースに格納されているセンサデータをコンソールから確認してみます。abs_agent インストールキットには agent_fastdb コマンド(クライアントプログラム) を使用してインメモリデータベースの管理を行うことができます。下記はこのプログラムを実行した様子です。
agent_fastdb をパラメータなしで実行すると、現在データベースに登録されている全キー名一覧と、そのキー名を使用して登録されているデータレコード数が表示されます。
2つ目の実行例は、agent_fastdb コマンドの集計パラメータを指定して 2017年5月25 日の1日分の温度データを 10 分刻みで集計しています。
追加で “-f <file_name>” オプションを指定すると、集計結果をCSV形式のファイルに出力することができます。このCSVファイルを表計算ソフト等で読み込むと簡単にグラフ作成等が行えます。この他にも、agent_fastdb コマンドではデータを追加・削除したり、登録データをファイルにストアしたりリストアすることが出来ます。詳しくは abs_agent ユーザーマニュアルをご覧ください。
●グラフ作成用の Web アプリを作成 全体図中の(5)
ここからは、PC やタブレット、スマートフォン等の Web ブラウザから Raspberry Pi にアクセスして、センサーデータをグラフ表示するアプリケーションを作成します。abs_agent には HTTP サーバーと Web API 機能が内蔵されていますので、簡単に Web アプリを作成して公開することができます。
最初に Web アプリのページとダイアログを定義した HTML ファイル(webroot/app/chart/index.html) を設置します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FASTDB グラフ</title>
<link rel="stylesheet" href="libs/css/themes/default/jquery.mobile-1.4.5.min.css" />
<link rel="stylesheet" href="libs/css/jquery.jqplot.css" />
<script src="libs/js/jquery.js"></script>
<script src="libs/js/jquery.mobile-1.4.5.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>
<!-- abs_agent Web API アクセス用 -->
<script src="libs/abs_agent/webapi.js"></script>
</head>
<body>
<!-- -->
<!-- アプリケーションの各ページ定義 -->
<!-- -->
<div data-role="page" id="login">
<div data-role="header" data-position="inline">
<h3>FASTDB グラフ ユーザー認証</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> </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_agent [ALL BLUE SYSTEM]</h3>
</div>
</div>
<div data-role="page" id="select_keys_page">
<div 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" class="ui-btn ui-btn-inline ui-shadow ui-btn-a ui-icon-refresh ui-btn-icon-left">最新の状態に更新</a></p>
<fieldset data-role="controlgroup" id="key_list"></fieldset>
<div data-role="footer">
<h3 >abs_agent [ALL BLUE SYSTEM]</h3>
</div>
</div>
<div data-role="page" id="summary_params_page">
<div 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" data-clear-btn="true">
</div>
<div data-role="fieldcontain">
<label for="target_time">集計対象とする最初の時刻:</label>
<input type="text" id="target_time" placeholder="HH:MM:SS または NULL(自動設定)" data-clear-btn="true">
</div>
<div data-role="fieldcontain">
<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"/>
<label for="plot-type-1">棒グラフ (bar)</label>
<input type="radio" name="plot_type" id="plot-type-2" value="line" checked="checked"/>
<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="" data-clear-btn="true"/>
</div>
<div data-role="fieldcontain">
<label for="yaxis_min">データ値(縦軸)のスケール最小値:</label>
<input type="number" id="yaxis_min" placeholder="数値 または NULL(自動設定)" value="0" data-clear-btn="true"/>
</div>
</div>
<div data-role="footer">
<h3 >abs_agent [ALL BLUE SYSTEM]</h3>
</div>
</div>
<div data-role="page" id="chart_disp_page">
<div 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:500px;width:95%; "></div>
<div data-role="footer">
<h3 >abs_agent [ALL BLUE SYSTEM]</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>
<div data-role="page" id="error_prev_dialog">
<div data-role="header" data-theme="b">
<h1>*SCRIPT ERR*</h1>
</div>
<div role="main" class="ui-content">
<h3>サーバー側でエラーが発生しました</h3>
<p>サーバー処理中にエラーが発生しました。スクリプト実行中にエラーが発生した可能性がありますのでサーバー側のログを確認して下さい</p>
<p><a data-role="button" id="error_prev_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="key_select_error_dialog">
<div data-role="header" data-theme="b">
<h1>*INPUT ERR*</h1>
</div>
<div role="main" class="ui-content">
<h3>入力エラー</h3>
<p>1つ以上の FASTDB キーを選択してください</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" type="application/javascript"></script>
</body>
</html>
JavaScript の jquery mobile フレームワークを使用してログインページと、キー選択ページ、集計パラメータ設定ページ、グラフ描画ページを作成しています。また、エラー発生時のダイアログメッセージもここで定義しています。
アプリケーションのメインロジックを記述した JavaScript ファイル (webroot/app/chart/main.js) は下記のようになっています。
//
// abs_agent FASTDB データ集計アプリケーション
//
// 2017/5/20 ver1.10 DeviceServer 集計アプリを元に abs_agent 用に移植
//
// 2014/8/7 ver1.00 初版作成
//
// copyright(c) All Rights Reserved 2014-2017 All Blue System
//
// スクリプト実行結果ステータスのみをチェック
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;
}
// chart_disp_page /////////////////////////////////////////////////////
// Line チャート表示
function plot_chart_line(data){
$.mobile.loading( 'hide');
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_prev_dialog", { transition: "pop",role:"dialog" });
}
return;
}
var yaxis_min = $("#yaxis_min").val();
if (yaxis_min != ""){
var yaxis_val = {
rendererOptions: {
minorTicks: 1
},
label:" ", // 左マージンの為にダミーを配置
min: parseInt(yaxis_min),
tickOptions: {
formatString: "%g",
showMark: false,
textColor: '#dddddd'
}
};
} else {
var yaxis_val = {
rendererOptions: {
minorTicks: 1
},
label:" ", // 左マージンの為にダミーを配置
//min: 0,
tickOptions: {
formatString: "%g",
showMark: false,
textColor: '#dddddd'
}
};
}
// jqplot ライブラリを使用してチャート表示
// プロットデータとラベルはスクリプトリターンパラメータから取得する
var plot1 = $.jqplot('chartdiv',data.ResultParams.SeriesList,{
seriesDefaults: {
breakOnNull: true
},
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:yaxis_val
},
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)) {
$("body").pagecontainer("change","#error_quit_dialog", { transition: "pop",role:"dialog" });
} else {
$("body").pagecontainer("change","#error_prev_dialog", { transition: "pop",role:"dialog" });
}
return;
}
var yaxis_min = $("#yaxis_min").val();
if (yaxis_min != ""){
var yaxis_val = {
rendererOptions: {
minorTicks: 1
},
label:" ", // 左マージンの為にダミーを配置
min: parseInt(yaxis_min),
tickOptions: {
formatString: "%g",
showMark: false,
textColor: '#dddddd'
}
};
} else {
var yaxis_val = {
rendererOptions: {
minorTicks: 1
},
label:" ", // 左マージンの為にダミーを配置
//min: 0,
tickOptions: {
formatString: "%g",
showMark: false,
textColor: '#dddddd'
}
};
}
// 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:yaxis_val
},
cursor:{show: true,zoom:true}
});
}
// abs_agent に設置した集計スクリプトを起動して、集計パラメータで指定された
// シリーズデータを取得する。スクリプト完了時のイベントハンドラでグラフを描画する
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("FASTDB/SUMMARY_JSON",params,"plot_chart_bar");
break;
case "line":
script_exec("FASTDB/SUMMARY_JSON",params,"plot_chart_line");
break;
}
}
// グラフ画面が表示された
$(document).on("pageshow","#chart_disp_page",function(event){
plot_chart();
});
// グラフ画面の Redrawボタンが操作された
$("#chart_disp_redraw_btn").on( "click",function(event, ui){
plot_chart();
});
// グラフ画面の Prevボタンが操作された
$("#chart_disp_prev_btn").on( "click",function(event, ui){
$("body").pagecontainer("change","#summary_params_page", { transition: "none" });
});
// 集計スクリプト実行中エラーのダイアログから復帰する場合は集計パラメータ設定画面に戻る
// グラフ画面の Prevボタンが操作された
$("#error_prev_ok_btn").on( "click",function(event, ui){
$("body").pagecontainer("change","#summary_params_page", { transition: "none" });
});
// summary_params_page ///////////////////////////////////////////////////////
// 集計パラメータ設定画面の Nextボタンが操作された
$("#summary_params_next_btn").on( "click",function(event, ui){
$("body").pagecontainer("change","#chart_disp_page", { transition: "none" });
});
// 集計パラメータ設定画面の Prevボタンが操作された
$("#summary_params_prev_btn").on( "click",function(event, ui){
$("body").pagecontainer("change","#select_keys_page", { transition: "none" });
});
// 集計対象キーのテキスト入力コンポーネントの内容が変更された
$(document).on("change", "#target_keys", function () {
selected_keys = $("#target_keys").val();
});
// 集計パラメータ設定画面が表示された
$(document).on("pageshow","#summary_params_page",function(event){
$("#target_keys").val(selected_keys); // 前ページで選択した全てのキーを、テキストエリアに初期値として入力
});
// select_keys_page /////////////////////////////////////////////////////////
// 集計対象の FASTDBキー名リスト(カンマ区切り)
// チェックボックスを操作すると update_selected_keys() イベントハンドラが実行されて
// 常に最新のチェック状態を反映している
var selected_keys = "";
// FASTDB データベースで使用中のキー名リストを取得する
function get_key_list(){
var params = {};
params["noquote"] = "1"; // スクリプトリターンパラメータを JSON オブジェクトとして受信する
script_exec("FASTDB/KEYS_JSON",params,"get_key_list_handler");
}
// FASTDB/KEYS_JSON スクリプト実行結果のイベントハンドラ。
function get_key_list_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;
}
// 選択済みのキーがある場合にはチェックボックスをチェック済みにする
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');
}
// チェックボックスで選択されているキーのリストを 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;
});
};
// 集計対象キーのチェックボックスが操作された
$(document).on("change", ".key_select", function () {
update_selected_keys();
});
// 集計対象キー選択画面が表示された
$(document).on("pageshow","#select_keys_page",function(event){
get_key_list();
});
// 集計対象キー選択画面の Reloadボタンが操作された
$("#key_list_reload_btn").on( "click",function(event, ui){
update_selected_keys(); // チェック済みのキーが FASTDB で削除されているかもしれないので selected_keys を作成し直す
get_key_list();
});
// 集計対象キー選択画面の Nextボタンが操作された
$("#key_list_next_btn").on( "click",function(event, ui){
update_selected_keys(); // チェック済みのキーが FASTDB で削除されているかもしれないので selected_keys を作成し直す
if (selected_keys == ""){ // キーが未指定の場合にはエラー
$("body").pagecontainer("change","#key_select_error_dialog", { transition: "pop",role:"dialog" });
return;
}
$("body").pagecontainer("change","#summary_params_page", { transition: "none" });
});
// login ////////////////////////////////////////////////////////////////
// サーバー側でログイン操作が成功したらデバイス選択画面に移動する
function login_callback(data){
if (data.Result == "Success"){
session_token = data.SessionToken;
$("body").pagecontainer("change","#select_keys_page", { 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_keys_page", { transition: "pop" });
}
});
Web アプリを起動すると最初にログイン画面が表示されます。画面で入力されたユーザー名とパスワードを取得して abs_agent の Web API 経由でログイン認証を行います。ログイン時に使用する login() 関数は、JavaScript ファイル (webroot/app/chart/libs/abs_agent/webapi.js) 中で下記の様に定義されています。
function login(user,pass,callback){
var url = server_host_url + "/command/json/session_login" +
"?user=" + encodeURIComponent(user) +
"&pass=" + encodeURIComponent(pass);
$.ajax({"url" : url,
"dataType" : "jsonp",
"jsonpCallback" : callback
});
}
abs_agent で公開している Web サーバーの URL “/command/json/session_login” にアクセスすると、abs_agent のWeb API 機能が呼び出されてログイン認証を行います。ログインに成功すると、URL パラメータで指定したコールバック関数が呼び出されて、コールバック関数のパラメータにセッショントークン文字列が格納されます。このセッショントークンをその他の Web API を呼び出すときに URL パラメータに指定することでセキュリティを確保できます。
ログインに成功すると、インメモリデータベース中に登録中のキーを選択する画面になります。画面にはグラフ表示の対象とするキー名一覧をチェックボックスで表示します。
この時、abs_agent 側の”FASTDB/KEYS_JSON” スクリプトを実行してキー一覧を取得しています。スクリプト実行時に使用する Web API のラッパー関数 script_exec()は下記の様に定義されています。
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
});
}
ログイン認証時と同様に、”/command/json/script” の URL にアクセスすると abs_agent の Web API 機能を利用してスクリプトを実行することができます。URL パラメータに指定したスクリプト名やリクエストパラメータは abs_agent に設置した Lua スクリプト実行時のリクエストパラメータに変換されます。
この時実行される “FASTDB/KEYS_JSON” スクリプトの内容は以下の様になっています。
--[[
●機能概要
FASTDB データベースで使用中のキー名リストを JSON フォーマットで取得する。
●リクエストパラメータ
なし
●リターンパラメータ
---------------------------------------------------------------------------------
キー値 値 値の例
---------------------------------------------------------------------------------
KeyList FASTDBデータベースで使用中のキー名リストがJSON フォーマット
文字列で格納される
[
{"Key":"<FASTDBデータベース登録時のキー#1>"},
{"Key":"<FASTDBデータベース登録時のキー#2>"},
..
..
{"Key":"<FASTDBデータベース登録時のキー#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 ..
..
}
●変更履歴
2017/4/23 abs_agent FASTDB 用に移植
2014/07/13 初版作成
abs_agent 2014-2017 copyright(c) All Blue System
]]
-----------------------------------------------------
-- 統計データベースで使用中のキー名リストを取得
-----------------------------------------------------
local stat,keys,rec_list = fastdb_key_list()
if not stat then error() end
-----------------------------------------------------------------
-- キー名リストを JSON 文字列に変換
-----------------------------------------------------------------
local key_list = "["
local cnt = 0
for key,val in ipairs(keys) do
if rec_list[key] > 0 then
if cnt ~= 0 then
key_list = key_list .. ","
end
key_list = key_list .. '{"Key":"' .. val .. '"}'
cnt = cnt + 1
end
end
key_list = key_list .. "]"
---------------------------------------------
-- デバイスリストをリターンパラメータに格納
---------------------------------------------
script_result(g_taskid,"KeyList",key_list)
スクリプトのリターンパラメータには、インメモリデータベースのキーリストを取得するライブラリ関数 fastdb_key_list() のリターン値を JSON 形式に変換したものが格納されます。
JavaScript 側ではコールバック関数でこの JSON 値を取得して、キー選択画面でチェックボックスタグをDOM に追加することで、キー名が表示されたチェックボックスリストを画面に表示しています。
ユーザーがチェックボックスで集計したいキーを選択した後、”次に進む”ボタンを押すと集計パラメータ設定画面に変わります。この画面では、集計期間や集計間隔、グラフフォーマットなどの選択ができます。
選択した集計パラメータの内容は集計計算を行うスクリプト “FASTDB/SUMMARY_JSON” に Web API 経由で渡されます。”グラフ作成” ボタンを押すと “FASTDB/SUMMARY_JSON” スクリプトが abs_agent で実行されます。
スクリプト”FASTDB/SUMMARY_JSON” の内容は下記の様になっています。
--[[
●機能概要
FASTDB データベースに保存されているデータを集計して JSON配列で取得する。
集計パラメータには日、週、月の範囲を指定できる。
●リクエストパラメータ
---------------------------------------------------------------------------------
キー値 値 値の例
---------------------------------------------------------------------------------
KeyList 集計対象とする FASTDB データベース中のキー名リスト
複数指定するときにはカンマで区切る
"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>"}
]
●備考
集計単位ごとの期間内にFASTDBデータレコードが見つからなかった場合には、
その単位期間の集計結果レコードは 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
●変更履歴
2017/04/23 ver1.0 DeviceServer用に配布していたスクリプトを abs_agent 用に移植
abs_agent copyright(c) 2014-2017 All Blue System
]]
----------------------------------------------------------------------
-- KeyList パラメータからキー文字列配列 keys 作成
----------------------------------------------------------------------
local keys
if g_params["KeyList"] then
keys = csv_to_tbl(g_params["KeyList"])
else
log_msg("parameter error",g_script)
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",g_script)
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"]),g_script)
local sum
local series_json = '['
local label_json = '['
local first_series = true
for k,v in ipairs(keys) do
log_msg("calculating: " .. v,g_script)
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 = fastdb_summary(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 first_item then
first_item = false
else
series_json = series_json .. ","
end
if sample[key] > 0 then -- 集計単位期間内にデータが存在しない場合にはnullレコードを出力
series_json = series_json .. string.format('["%s",%g]',val,sum)
else
series_json = series_json .. string.format('["%s",null]',val)
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)
集計パラメータで指定されたキー名と集計期間を元に、ライブラリ関数 fastdb_summary() をコールしてインメモリデータベースの集計計算を実行します。集計後のデータは JSON 形式に変換して Web アプリ側の JavaScript に返されます。動作の詳細はスクリプト中のコメントをご覧ください。
最後に、集計結果が格納された JSON データを jqplot ライブラリのシリーズデータに指定してグラフを描画します。
●Web アプリの動作確認
実際に Web アプリを動作させてセンサデータのグラフを表示してみます。abs_agent でWeb API を使用する場合にはログイン認証が必要になりますので、最初にWeb アプリ認証用のユーザーを登録します。
ここで登録するユーザー名とパスワードは abs_agent の Web API アクセス時にのみ使用するもので、Raspberry Pi の Linux ユーザアカウントとは全く関連はありません。また、Web API アクセス用のユーザーは好きなだけ登録することができます。登録には agent_webuser コマンドを使用します。
上記のコマンド例ではユーザー名 “user” で初期パスワードを “pass” で作成しています。これで Web アプリを動作させることができますので、早速起動してみます。
ここでは iPad 上の Firefox ブラウザを使用しています。他Web ブラウザ、例えば IE や Safariブラウザでも問題なく動作します。もちろん PC 上の Web ブラウザからアクセスしても動作します。
Raspberry Pi の IP アドレスが “192.168.100.15″ で、abs_agent インストール時に HTTP サーバーのPort をデフォルトの 8080 のまま使用していた場合には下記の URL にアクセスするとWeb アプリが起動します。
Web アプリ起動用のURL “http://192.168.100.15:8080/app/chart/index.html”
ログイン画面が表示されますので、先ほど作成した Web API アクセス用のユーザー名とパスワードでログインします。
abs_agent 内のインメモリデータベースに保存されている全てのキー一覧が表示されます。ここでグラフを表示したいデータが格納されているキーを選択してください。複数のキーを選択することもできます。ただし、気圧と気温などを同じグラフに描画すると意味のないグラフになりますのでキー選択時はデータ種別を考慮してください。
“集計パラメータボタン”を押すと下記の画面に変わります。
ここでは集計計算をするときの細かいパラメータを指定できます。集計期間を選択することで、集計間隔や集計開始時刻(グラフの左端のタイムスタンプ) などを自動で設定します。必要に応じて開始日付や時刻、集計間隔を自由に変更することもできます。
設定画面の一番下にあるスケール最小値はデフォルトで 0 になっていますが、もしセンサデータ値に負値が含まれる場合には入力欄の “x” ボタンを押して空にするか、適切な負値の最低値を指定します。
“グラフ作成”ボタンを押すと、グラフが表示されます。
ここで iPad を横長に持ち替えた場合には、”再描画” ボタンを押してグラフ表示を最適化できます。また、集計パラメータ選択画面やキー選択画面にボタンで戻ることもできます。
この例では環境センサ基板上のBME280センサと外付けしたTMP102センサの1週間の変化をグラフにしてみました。データの傾向は同じなのですが環境センサ基板上の温度は数度高めになるのがよく判ります。
PC 版の Web ブラウザからアクセスしている場合には、マウスでプロット画面をドラッグで囲むことでズーム表示を行えます。タブレットからアクセスしている場合にはズームは利用できません。この時は集計パラメータ設定画面中のスケール最小値の入力欄を空にすることで、縦軸側のスケールが自動調整されて見やすくなります。
●備考
今回の Web アプリは abs_agent インストールキットに同梱されていますので直ぐに使用することができます。Raspberry Pi に接続したセンサがある場合には簡単にローカルだけでグラフ表示できますので是非試して下さい。
MQTT ブローカにセンサデータを送信してクラウド側でグラフ表示をおこなっている場合でも、この記事で紹介したようにローカル側に MQTT ブローカから Subscribe したデータを保存することで、ゲートウエイやエッジデバイス側で簡単にデータの確認できます。インターネットに接続できない環境でもグラフ表示できます。
今回紹介した Web アプリとインメモリデータベース機能は x86 版の abs_agent でも同様に動作します。x86 版が動作するハードウエアでは物理メモリを多く搭載することができる場合が多いので、インメモリデータベースの保存期間をかなり長くすることが可能です。また、複数のクライアントから Web アプリに同時にアクセスした場合でも CPU パワーに余裕があるので安定して運用できます。
ご意見や質問がありましたら、お気軽にメールをお寄せください(contact@allbluesystem.com)
それではまた。