Raspberry Pi の Hardware 情報取得時の不具合

今回は、Raspberry Pi の abs_agent で発生した不具合対応についての報告です。

先日、ユーザー様より指摘があり Raspberry Pi 用の abs_agent でハードウエア・アクセスモジュール(RASPI)でエラーが発生していました。下記はエラー発生時のログ情報です。

原因は、Raspbian OS の /proc/cpuinfo ファイルにある Hardware タグの内容が、BCM2835 という文字列になっていたためでした。開発時の環境ではここに、BCM2708 または BCM2709 の文字列が格納されていて、abs_agent ではこれをレジスタアクセス時のベースアドレス決定に使用していました。最近のディストリビューションではこの部分に BCM2835 が格納されていて、上記のエラーが発生してしまいました。

これに対応するため、abs_agent の最新のインストールキットとユーザーマニュアルを更新しました。

最新バージョンの abs_agent では、インストール直後に、abs_agent のコンフィギュレーションファイル abs_agent.xml を編集して、直接 Raspberry Pi のタイプを示す文字列を予め設定していただくように変更しました。<RASPI> .. </RASPI> タグ中の <Hardware>..<Hardware> 部分にプロセッサタイプを示す文字列を記入します。

Raspberry Pi で使用している SoC が BCM2835(Raspberry Pi ver1, Zero等) の場合には BCM2708 を記入します。SoC が BCM2836(Raspberry Pi ver2) や BCM2837(Raspberry Pi ver3) の場合には BCM2709 を記入します。

以下は、Raspberry Pi ver3 上でコンフィギュレーションファイルを編集している様子です。

詳しい内容はユーザーマニュアルにも記載してありますのでご覧ください。

ご意見や質問がありましたら、お気軽にメールをお寄せください。また abs_agent を使用する上で、質問や不明点、不具合など何でも結構ですので是非ご連絡ください。(contact@allbluesystem.com)

それではまた。

 

エッジデバイス側でグラフ Webアプリを動作させる

●概要

今回の記事は、センサーデバイスで取得したデータをクラウドサービスに送信するゲートウエイやエッジデバイス側で、ローカルに保存したデータベースを参照してグラフを作成する 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 アプリのソースが含まれていますので、簡単にセットアップは完了します。

その後、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>&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_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)

それではまた。

 

温度、湿度、気圧、明るさを Raspberry Pi に接続したグラフィックLCDに表示

●概要

Raspberry Pi で温度や湿度、気圧などを測定して表示するアプリケーションを紹介したいと思います。ハードウエアは市販のボードを使用しますので簡単に作成することができます。今回はスイッチサイエンス社が販売している、“Raspberry Pi用環境センサ基板” を使用しました。

下記は、環境センサ基板にオプションのグラフィック LCD モジュールを接続したものを Raspberry Pi ver3 で動作させている様子です。

ボードには予め BME280 センサと、I2Cでアクセスできる光センサーが搭載されていますので簡単に Raspberry Pi から測定値を取得することができます。また、グラフィック LCD を接続して最新の測定値をリアルタイムに表示することができます。

今回作成したアプリケーションはグラフィックLCDに温度や湿度、気圧をリアルタイムに表示します。上記の画面の他にも、光センサ表示、デジタル時計表示、IP アドレス表示の各ページを環境センサボード上のスイッチ SW1 で切り替えることができます。

今回のアプリケーションは Raspberry Pi 本体と 環境センサ基板(+ グラフィックLCDモジュール)だけで動作しますので簡単に作成することができます、是非お試しください。

●ハードウエア構成

今回のアプリケーションを動作させるために必要な機材は以下になります。このほかにも、Raspberry Pi 本体用の電源やネットワークケーブル、コンソール端末(インストールやセットアップ作業時に使用)

Raspberry Pi 本体 (B+/2/3) x1

スイッチサイエンス社製 環境センサ基板  x1

環境センサ基板にオプションで接続する AQM1248A小型グラフィック液晶ボード x1

環境センサ基板に LCD モジュールを接続するためのピンヘッダ(両端オス) x1

●ソフトウエア構成

Raspberry Pi に接続する環境センサ(BME280等)は I2C バスで接続されていて、 LCD グラフィックディスプレイは SPI インターフェイスで接続されています。これらのハードウエアインターフェイスを操作したり、表示プログラムの Lua スクリプトの実行環境のためにオールブルーシステムの abs_agent プログラムを使用します。abs_agent のインストールキットと詳しいマニュアルは、こちらから Raspberry Pi 用のバイナリアーカイブをダウンロードできます。個人目的であればフリー版ライセンスが同梱されていますので、期間の制限なく直ぐに使用するこができます。今回紹介するスクリプトもインストールキットに最初から含まれていますので、一部コメントを削除するだけで簡単にセットアップできます。

●インストール

最初に、Raspberry Pi には標準 OS の Raspbian の最新バージョンをインストールしておきます。

公開されている Python や C で作成したプログラムからRaspberry Pi のI2C や SPI を使用する場合には、Raspbian のコンフィギュレーションをデフォルト値から変更する必要がある場合が多いのですが、abs_agent を使用する場合にはこれらの作業は必要ありませんので省略できます。(もちろん、I2C や SPI コンフィギュレーションを変更してカーネルドライバ追加や専用のデバイスファイルを作成した状態でも abs_agent は問題なく動作します)

オールブルーシステムのダウンロードページから最新の abs_agent のインストールキットをダウンロードしてください。このとき、Intel x86 タイプとRaspberry Pi 用の2種類のバイナリがありますので、間違えずに Raspberry Pi 用のものを選択してください。また、インストール手順の詳細や Lua ライブラリ関数の使用方法を確認するために、abs_agent ユーザーマニュアルもダウンロードしておくと便利です。

Raspberry Pi にデフォルトユーザー名 “pi” でログインして、ダウンロードしたインストールキットファイルをホームディレクトリに配置します。その後、”tar zxvf <インストールキットファイル名>” のコマンドを実行して、abs_agent プログラム一式を /home/pi/abs_agent ディレクトリの下にインストールします。(下記実行例を参照してください)

●サーバー起動時に実行される SERVER_START イベントハンドラを修正

一般的なアプリケーション作成時には、先に abs_agent を起動させた状態でアプリケーションを構築してしていくのですが、今回は既にセンサ情報表示プログラムのスクリプトが作成済みですので、それらの自動起動設定を先に行います。

abs_agent 起動時には SERVER_START.lua スクリプトが自動で一度だけ実行されて、このスクリプト中にハードウエアやアプリケーションの初期化を作業を記述することができるようになっています。

今回は環境センサ基板に搭載されている3つのタクトスイッチと 各種LED が接続されている GPIO ポートの初期化を記述します。インストール直後の SERVER_START.lua スクリプト中には既に環境センサ基盤用の初期化がコメントとして記述されています。ここではこのコメントを外して環境センサ基板の設定を有効にするだけです。

abs_agent をインストールしたディレクトリ (/home/pi/abs_agent) の中の scripts ディレクトリに移動して、SERVER_START.lua ファイルを vi エディタで編集します。

スクリプトファイルの最後の部分に記述されている環境センサ基板用の設定部分のコメントを外して有効にします。lua のコメントは “–” ハイフン2つを続けて行全体をコメントにするのと、”–[[" と "]]” で囲まれた複数行をコメントにすることができます。ここでは “–[[" と "]]” を削除して環境センサ基板の設定全体を有効にしてください。

コメントを外して、設定を有効にした部分は以下の様になります。

-------------------------------------------------------------------
-- スイッチサイエンス社製 環境センサボード(RPi_EnvSensor_Rev4) 設定
-------------------------------------------------------------------

raspi_gpio_config(18,"input","off")		-- SW3
raspi_gpio_config(23,"input","off")		-- SW2
raspi_gpio_config(24,"input","off")		-- SW1

raspi_gpio_config(4,"output","off")		-- LED4 Ir
raspi_gpio_config(17,"output","off")	-- LED3 BLUE
raspi_gpio_config(22,"output","off")	-- LED1 RED
raspi_gpio_config(25,"output","off")	-- LCD RS (AQM1284A)
raspi_gpio_config(27,"output","off")	-- LED2 GREEN

-- GPIO 初期値出力、全LED消灯, LCD RS->low
raspi_gpio_write(4,false)
raspi_gpio_write(17,true)
raspi_gpio_write(22,true)
raspi_gpio_write(25,false)
raspi_gpio_write(27,true)

-- アラームタスク起動
script_fork_exec("RASPI/ENVSENSOR_ALARM_TASK","","")

-- センサー値表示タスク起動
script_fork_exec("RASPI/ENVSENSOR_DISPLAY_TASK","","")

最初にタクトスイッチが接続されている GPIO ポートを入力モードにしています。スイッチサイエンス社のホームページではこの基板の回路図が公開されていますので参照します。これによると、タクトスイッチ周りの外付けのプルアップ抵抗が既に接続済みなので、ここでは Raspberry Pi 内部のプルアップ設定は “off” にします。

また各種LED 用に GPIO ポートを出力に設定します。その後 LED を消灯状態にするために初期値を設定します。このとき、赤・青・緑 LED は GPIO Low 出力時に点灯する仕様なのでそのように初期設定します。

GPIO 初期設定後に、今回のアプリケーションのメインタスクを記述している ENVSENSOR_DISPLAY_TASK スクリプトを起動します。

このスクリプトは起動後、無限ループに入ってセンサ値取得とLCD表示を繰り返します。このため、起動時には別スレッドで起動させる script_fork_exec() ライブラリ関数を使用しています。このスクリプトについての詳しい説明は後述します。

この他にも、ENVSENSOR_ALARM_TASK スクリプトを起動していますが、今回の記事ではこのアプリケーションは使用していませんので “–” を挿入してコメントアウトしても構いません。

このENVSENSOR_ALARM_TASK スクリプトは、環境センサ基板上の3つの LED を他のアプリケーションや Web API, MQTT ブローカ経由で操作できるように作成しています。例えば、グローバル共有変数名 “alarm_blue” に “blink” 文字列を設定すると青色 LED が点滅します。興味がありましたら、スクリプト中のコメントに仕様等が記述されていますのでご覧ください。

●タクトスイッチ操作時に実行される RASPI_CHANGE_DETECT イベントハンドラを修正

今回のアプリケーションはグラフィック LCD ディスプレイにデフォルトで気温や湿度、気圧などを表示しますが、その他にも時計表示や IP アドレスを表示する機能があります。これらのページ切り替えには環境センサ基板上の SW1 タクトスイッチを使用します。

タクトスイッチは前述の SERVER_START イベントハンドラ中で入力モードに設定されています。また、メインタスクの ENVSENSOR_DISPLAY_TASK スクリプト起動時に GPIO 入力値が変化したときにイベントを検出するモードに設定します(詳しくは後述します)。

スイッチを操作イベントが発生したときに実行される RASPI_CHANGE_DETECT イベントハンドラの内容を、インストール時のデフォルト設定から変更します。

以下の様に RASPI_CHANGE_DETECT.lua ファイルを vi エディタで編集します。

スクリプトファイルの最後の部分に記述されている環境センサ基板用の設定部分のコメントを外して有効にします。

コメントを外して、設定を有効にした部分は以下の様になります。

---------------------------------------------------------------------------------------
-- スイッチサイエンス社製 環境センサーボード用のセンサー値表示アプリケーション設定
-- アプリケーションの詳細は RASPI/ENVSENSOR_DISPLAY_TASK スクリプトを参照してください
-- アプリ中で指定したページ切り替えを行うスイッチ(SW1)の入力を検出して、
-- アクティブページを示すグローバル共有変数をインクリメントする
---------------------------------------------------------------------------------------
if change_bit[24] then -- 環境センサーボードの SW1 を操作した?
	if change_bit[24] == 0 then
		local stat,val = inc_shared_data("ENVSENSOR_ACTIVE_PAGE")
		if tonumber(val) > 4 then -- SWを押すたびにアクティブなページ番号を 1..4 の順に設定する
			set_shared_data("ENVSENSOR_ACTIVE_PAGE","1")
		end
	end
end

最初に SW1 が接続されている GPIO#24 ピンが変化していたかどうかをチェックします。その後、SW1 スイッチを押し込んだ状態の場合だけを判断して、アプリケーションで表示するページ番号をインクリメントします。

表示中のアクティブなページ番号はグローバル共有変数(キー名: ENVSENSOR_ACTIVE_PAGE)に格納されていて、この値を変更すると起動中のアプリケーションの表示画面をリアルタイムに変更させることができます。また、ページは現在4つまでしか用意していないので、ページ番号が “4″ を超えたらになったら “1″ に戻しています。

●アプリケーション起動

ここまでの設定作業で、abs_agent を起動すると同時にアプリケーションも自動的に実行されるようなりました。早速 abs_agent を起動させます。

上記のコマンド実行例ではログサーバーにメッセージを出力するように、”-l <ログサーバーのIPアドレス>” もオプションで指定していますがこの指定は省略しても構いません。

ログサーバーを設置すると abs_agent 動作の詳細を Windows PC から確認することができます。詳しくは abs_agent ユーザーマニュアルをご覧ください。ログサーバーを設置していると下記の様な起動メッセージが出力されます。

abs_agent が起動すると同時に、アプリケーション・メインタスク用の “RASPI/ENVSENSOR_DISPLAY_TASK” Lua スクリプトが実行されて環境センサ基板のグラフィック LCD に現在の気温、湿度、気圧が表示されます。

表示は約 1秒ごとに更新されて現在の測定値がリアルタイムに表示されます。ここで SW1 ボタンを押すと明るさの測定値画面に切り替わります。

光センサに手をかざすと測定値とバーグラフがリアルタイムに変化するのを確認できると思います。もう一度 SW1 を押すと時計表示になります。

もう一度 SW1 を押すと、Raspberry Pi の IP アドレスと現在時刻を表示します。

以降 SW1 を押すごとに上記のページを繰り返し切り替えて表示します。

●センサ値の取得スクリプト説明と手動実行

ここからはアプリケーション内部で実行しているスクリプトの詳細を説明します。表示アプリからは環境センサ基板に搭載されている BME280 センサから1秒に一回、気温、湿度、気圧データを取得しています。この機能を実現しているスクリプト(RASPI/DEVICE/BME280_READ) の内容は下記の様になっています。

--[[

●機能概要

I2C バスに接続した気圧・温度・湿度センサー(BME280) の値を取得する

●リクエストパラメータ

---------------------------------------------------------------------------------
キー値          値                                               値の例
---------------------------------------------------------------------------------
bus             I2C バス番号                                     "1"
                "0" または "1"を指定、省略時は "1" を使用する

init			このパラメータを指定した場合には、強制的にデバイス
				初期化と補償データレジスタの取得を行う。
				パラメータ値は任意。		 					 "1"

●リターンパラメータ

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

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

humidity		センサーから取得した相対湿度(%)                  "75.00"

pressure	    センサーから取得した気圧(hPa)	                 "1013.10"

]]

local slave_addr = "76"
local bus = 1
local stat,data,result

-------------------------
-- パラメータチェック
-------------------------
if g_params["bus"] then
    bus = tonumber(g_params["bus"])
end

-----------------------------------------------------------------------
-- 初期化と補償データレジスタの取得
-----------------------------------------------------------------------
stat,data = get_shared_data("BME280_REGVAL")

if (data == "") or g_params["init"] then
	log_msg("initialize and setup the device",g_script)

	-----------------------------------------------------------------------
	-- BME280 の測定パラメータ設定
	-----------------------------------------------------------------------
	local osrs_t = 1			-- Temperature oversampling x 1
	local osrs_p = 1			-- Pressure oversampling x 1
	local osrs_h = 1			-- Humidity oversampling x 1
	local mode   = 3			-- Normal mode
	local t_sb   = 5			-- Tstandby 1000ms
	local filter = 0			-- Filter off
	local spi3w_en = 0			-- wire SPI Disable

	local ctrl_meas_reg = bit_or(bit_lshift(osrs_t,5),bit_lshift(osrs_p,2),mode)
	local config_reg    = bit_or(bit_lshift(t_sb,5),bit_lshift(filter,2),spi3w_en)
	local ctrl_hum_reg  = osrs_h

	if not raspi_i2c_write(bus,slave_addr,"F2" .. bit_tohex(ctrl_hum_reg,2)) then error() end
	if not raspi_i2c_write(bus,slave_addr,"F4" .. bit_tohex(ctrl_meas_reg,2)) then error() end
	if not raspi_i2c_write(bus,slave_addr,"F5" .. bit_tohex(config_reg,2)) then error() end

	---------------------------------------------------------------------------
	-- BME280 補償データレジスタの値を取得後共有データに保存
	-- 次回からのスクリプト実行時には共有データに保存されたレジスタ値を使用
	---------------------------------------------------------------------------
	stat = raspi_i2c_write(bus,slave_addr,"88")
	if not stat then error() end
	stat,result = raspi_i2c_read(bus,slave_addr,24)
	if not stat then error() end
	data = result

	stat,result = raspi_i2c_write(bus,slave_addr,"A1",1)
	if not stat then error() end
	data = data .. result

	stat,result = raspi_i2c_write(bus,slave_addr,"E1",7)
	if not stat then error() end
	data = data .. result

	set_shared_data("BME280_REGVAL",data)

end

------------------------------------
-- 補償パラメータ計算
------------------------------------
local reg = hex_to_tbl(data)
local dig_T1 = reg[1] + bit_lshift(reg[2],8)
local dig_T2 = bit_tosigned(reg[3] + bit_lshift(reg[4],8),16)
local dig_T3 = bit_tosigned(reg[5] + bit_lshift(reg[6],8),16)
local dig_P1 = reg[7] + bit_lshift(reg[8],8)
local dig_P2 = bit_tosigned(reg[9] + bit_lshift(reg[10],8),16)
local dig_P3 = bit_tosigned(reg[11] + bit_lshift(reg[12],8),16)
local dig_P4 = bit_tosigned(reg[13] + bit_lshift(reg[14],8),16)
local dig_P5 = bit_tosigned(reg[15] + bit_lshift(reg[16],8),16)
local dig_P6 = bit_tosigned(reg[17] + bit_lshift(reg[18],8),16)
local dig_P7 = bit_tosigned(reg[19] + bit_lshift(reg[20],8),16)
local dig_P8 = bit_tosigned(reg[21] + bit_lshift(reg[22],8),16)
local dig_P9 = bit_tosigned(reg[23] + bit_lshift(reg[24],8),16)
local dig_H1 = reg[25]
local dig_H2 = bit_tosigned(reg[26] + bit_lshift(reg[27],8),16)
local dig_H3 = reg[28]
local dig_H4 = bit_tosigned(bit_and(reg[30],0x0f) + bit_lshift(reg[29],4),16)
local dig_H5 = bit_tosigned(bit_lshift(reg[31],4) + bit_and(bit_rshift(reg[30],4),0x0f),16)
local dig_H6 = bit_tosigned(reg[32],8)

local t_fine -- compensate_T() 関数内で値をセットするので、値を参照する時には compensate_T() を先にコールしておくこと

function compensate_T(adc_T)
	local v1 = (adc_T / 16384.0 - dig_T1 / 1024.0) * dig_T2
	local v2 = (adc_T / 131072.0 - dig_T1 / 8192.0) * (adc_T / 131072.0 - dig_T1 / 8192.0) * dig_T3
	t_fine = v1 + v2
	local temperature = t_fine / 5120.0
	return temperature
end

function compensate_P(adc_P)
	local pressure = 0.0
	local v1 = (t_fine / 2.0) - 64000.0
	local v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * dig_P6
	v2 = v2 + ((v1 * dig_P5) * 2.0)
	v2 = (v2 / 4.0) + (dig_P4 * 65536.0)
	v1 = (((dig_P3 * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((dig_P2 * v1) / 2.0)) / 262144
	v1 = ((32768 + v1) * dig_P1) / 32768

	local pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
	if pressure < 0x80000000 then
		pressure = (pressure * 2.0) / v1
	else
		pressure = (pressure / v1) * 2
	end
	v1 = (dig_P9 * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
	v2 = ((pressure / 4.0) * dig_P8) / 8192.0
	pressure = pressure + ((v1 + v2 + dig_P7) / 16.0)  

	return pressure/100
end

function compensate_H(adc_H)
	local var_h = t_fine - 76800.0
	var_h = (adc_H - (dig_H4 * 64.0 + dig_H5/16384.0 * var_h)) * (dig_H2 / 65536.0 * (1.0 + dig_H6 / 67108864.0 * var_h * (1.0 + dig_H3 / 67108864.0 * var_h)))
	var_h = var_h * (1.0 - dig_H1 * var_h / 524288.0)
	if var_h > 100.0 then
		var_h = 100.0
	end
	if var_h < 0.0 then
		var_h = 0.0
	end
	return var_h
end

------------------------------------
-- 現在の測定値取得
------------------------------------
stat,result = raspi_i2c_write(bus,slave_addr,"F7",8)
if not stat then error() end
local raw = hex_to_tbl(result)

local pres_raw = bit_or(bit_lshift(raw[1],12),bit_lshift(raw[2],4),bit_rshift(raw[3],4))
local temp_raw = bit_or(bit_lshift(raw[4],12),bit_lshift(raw[5],4),bit_rshift(raw[6],4))
local hum_raw  = bit_or(bit_lshift(raw[7],8),raw[8])

local temp = compensate_T(temp_raw)
local press =compensate_P(pres_raw)
local humidity =compensate_H(hum_raw)

script_result(g_taskid,"temperature",string.format("%5.1f",temp))
script_result(g_taskid,"pressure",string.format("%7.1f",press))
script_result(g_taskid,"humidity",string.format("%5.1f",humidity))

最初の部分で BME280 補償データレジスタの内容を取得しています。このレジスタ値は工場で設定後は変化しないので、一度取得したデータをabs_agent のグローバル共用データに保存しておきます。次回からこのスクリプトをコールしたときには BME280 補償データレジスタを読みに行かないで、abs_agent に保存したデータを利用するようにしています。また同時に、初回にコールされたときのみ BME280 デバイスの測定パラメータを設定します。

補償データレジスタから補償パラメータを計算した後、現在の計測値を BME280 センサから取得して温度と湿度、気圧データを計算します。計算結果はスクリプトリターンパラメータに設定してスクリプトを終了します。(浮動小数点演算に伴う誤差が伴いますので使用される前にご自身で計算方法と精度を確認して、必要に応じてスクリプトを修正してからご使用ください)

表示アプリケーションからこのスクリプトを定期的にコールして、リターンパラメータで得られたセンサ値を LCD に表示しています。

次に、明るさを測定するスクリプト(RASPI/DEVICE/ENVSENSOR_LIGHT_READ) の内容は下記の様になっています。

 

--[[

●機能概要

環境センサーボード(スイッチサイエンス社製)光センサの値を取得する

●リクエストパラメータ

無し

●リターンパラメータ

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

light			センサーから取得した値			                  "24"
				明るい時に値が小さく、暗いときに値が大きくなる
				0 .. 255
]]

local slave_addr = "6A"
local bus = 1

------------------------------------
-- 現在の測定値取得
------------------------------------
local stat,result = raspi_i2c_write(bus,slave_addr,"C0",1)
if not stat then error() end

script_result(g_taskid,"light",tonumber("0x" .. result))

このスクリプトは環境センサ基板上に搭載されている、光センサと接続したマイコン(I2Cスレーブ)から1バイトのデータを取得しています。得られた16進数値を10進数に変換した値をリターンパラメータに設定しています。

次に、IP アドレス表示ページで使用される IP アドレス情報は下記のスクリプト(OS/GET_IP)を実行して取得しています。

--[[
******************************************************************************

abs_agent が動作しているコンピュータの IPv4 アドレスを取得する
eth0 以外のインターフェイスを指定する場合にはスクリプトを修正して下さい。

******************************************************************************
]]

local ip_addr = ""
local cmd = [[ip addr show eth0 | grep -o 'inet [0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' | grep -o [0-9].*]]
local f = assert(io.popen (cmd))
for line in f:lines() do -- 通常は1行しか取得しない筈
	ip_addr = line
end
f:close()

script_result(g_taskid,"IPAddress",ip_addr)

このスクリプトは OS で提供されている “ip” コマンドの出力を “grep” コマンドで加工した後 IP アドレス部分だけを取り出してリターンパラメータに設定します。

abs_agent が動作する Unix 系の OS では幾つかのプログラムやスクリプトをパイプで繋いで、このような仕組みを簡単に実現することができます。パイプ(リダイレクト)の機能を利用することでシステムの構築をプログラム単位でブロックを構成するように組み立てていくことができます。

abs_agent のLua スクリプトは上記の様に、これもパイプ経由で標準出力を取り込むことができますので、複数の OS プログラムやスクリプトで提供されている機能を束ねることで新しい機能を提供するスクリプトを簡単に作成できます。

スクリプト中の “cmd” 変数には、文字列中にシングルコート文字がふくまれているため文字列をリテラル表現で代入しています。上記の例では “[[" と "]]” の間に記述されたOS コマンド文字列が cmd に格納されます。

この項で説明したセンサデータや IP アドレス取得スクリプトを手動で実行してみます。abs_agent のライブラリ関数内部で適切に排他制御が行われますので、LCD 表示アプリケーションを動作させている状態でも問題なく実行することができます。

スクリプト実行には abs_agent インストールキット中に含まれる agent_script プログラムを使用します。コンソールから agent_script -s <スクリプト名> で実行できます。

各スクリプトの実行結果で返されるリターンパラメータに、センサから取得したデータが格納されているのを確認できます。

●アプリケーションスクリプトの説明

ここからは LCD 表示アプリケーション本体のスクリプト(RASPI/ENVSENSOR_DISPLAY_TASK) の内容を説明します。スクリプトの内容は下記の様になっています。

--[[

●機能概要

環境センサーボード(スイッチサイエンス社製)に搭載している BME280 センサと光センサの
測定値を AQM1248A グラフィックLCD に表示する。

●参照するグローバル共有変数

---------------------------------------------------------------------------------
キー値                      値
---------------------------------------------------------------------------------
ENVSENSOR_ACTIVE_PAGE    "1" .. "4"

LCD に表示するページを切り替える。
センサーボードのスイッチ入力に対応したイベントハンドラを設定してこの値を変更すると、
LCD に表示するページを切り替えることができる。

●備考

このスクリプトは無限ループに入って終了しないので、必ず別スレッドで起動してください。

●変更履歴

2017/03/11     初版作成

copyright(c) 2017 All Blue System

]]

-- 2重起動防止用チェック
if not exclusive_check(g_script) then
    log_msg("*ERROR* exclusive_check() failed. script = " .. g_script,file_id)
    return
end
log_msg("start..   TaskID = " .. g_taskid,g_script)

-- 表示ページ切り替えスイッチ(SW1) の GPIO モード設定
-- スイッチ入力時のRASPI_CHANGE_DETECT イベントハンドラで、表示ページ切り替え用のグローバル共有変数を更新すること
local SW_pin = 24
if not raspi_gpio_config(SW_pin,"input","off") then error() end
if not raspi_change_detect(SW_pin,true) then error() end

-- デバイスとタスク内変数初期化

raspi_AQM1248A_try_init()							-- LCD AQM1284A 初期化

local active_page_var = "ENVSENSOR_ACTIVE_PAGE"	-- LCD 表示ページ切り替え用のグローバル共有変数
set_shared_data(active_page_var,"1")				-- 初期表示ページ設定

local timer_interval = 100							-- 表示更新間隔の初期値(10msで割った数を指定)、ページ毎に更新間隔は変わる
local run_indicator = false						-- 画面更新 Indicator
local ip_address = ""								-- 取得した自身の IP アドレスを表示するときに使用

---------------------------------------------------------------------
-- BME280 センサー
---------------------------------------------------------------------
function BME280_page()
	local stat,BME280 = script_exec2("RASPI/DEVICE/BME280_READ","","")
	if not stat then error() end

	graphic_clear()
	graphic_print(22,1,BME280["temperature"] .. "[ ]",2)
	graphic_draw_bitmap(80,32,8,8,list_to_hex(0x02,0x05,0x02,0x3c,0x42,0x42,0x24,0x00),true,true,2) -- "℃" 文字
	graphic_print(22,3,BME280["humidity"] .. "[%]",2)
	graphic_print(6,5,BME280["pressure"] .. "[hP]",2)
	graphic_draw_circle(120,38,6,run_indicator,true)
	raspi_AQM1248A_display()
	timer_interval = 100
end

---------------------------------------------------------------------
-- 光センサー
---------------------------------------------------------------------
function LightSensor_page()
	local stat,ENVSENSOR = script_exec2("RASPI/DEVICE/ENVSENSOR_LIGHT_READ","","")
	if not stat then error() end

	graphic_clear()
	graphic_print(10,2,ENVSENSOR["light"],3)
	graphic_print(70,2,"[Lu]",2)
	local bright = math.ceil(110 * tonumber(ENVSENSOR["light"]) / 255)
	graphic_draw_rect(10,5,110,15,true,false)
	graphic_draw_rect(10,5,bright,15,true,true)
	raspi_AQM1248A_display()
	timer_interval = 30
end

---------------------------------------------------------------------
-- デジタル時計
---------------------------------------------------------------------
function clock_page()
	local now = os.date "*t";
	local stat,wday = day_of_week(now["year"],now["month"],now["day"]);
	if not stat then error() end
	local weekstr;
	if wday == 1 then
		weekstr = "Sun"
	elseif wday == 2 then
		weekstr = "Mon"
	elseif wday == 3 then
		weekstr = "Tue"
	elseif wday == 4 then
		weekstr = "Wed"
	elseif wday == 5 then
		weekstr = "Thu"
	elseif wday == 6 then
		weekstr = "Fri"
	elseif wday == 7 then
		weekstr = "Sat"
	end;

	local data1 = string.format("%2.2d/%2.2d %s",now["month"],now["day"],weekstr)
	local data2 = ""
	if run_indicator then
		data2 = string.format("%2.2d:%2.2d",now["hour"],now["min"])
	else
		data2 = string.format("%2.2d %2.2d",now["hour"],now["min"])
	end

	graphic_clear()
	graphic_print(15,1,data1,2)
	graphic_print(20,5,data2,3)
	raspi_AQM1248A_display()
	timer_interval = 50
end

---------------------------------------------------------------------
-- IPアドレスと時計
---------------------------------------------------------------------
function ip_page()
	if ip_address == "" then
		local stat,info = script_exec2("OS/GET_IP","","")
		if not stat then error() end
		ip_address = info["IPAddress"]
	end

	local now = os.date "*t";
	local data1 = ""
	if run_indicator then
		data1 = string.format("%2.2d:%2.2d",now["hour"],now["min"])
	else
		data1 = string.format("%2.2d %2.2d",now["hour"],now["min"])
	end

	graphic_clear()
	graphic_print(35,1,data1,2)
	graphic_print(0,3,"IP=" .. ip_address,2)
	raspi_AQM1248A_display()
	timer_interval = 50
end

-- 指定したグローバル共有変数の値が変更されるか、もしくは指定されたカウント値 x 10ms 経過するまで内部でウェイト
function global_change_wait(global_name,start_val,max_wait_cntr)
	local stat,new_val
	local cntr = 0
	repeat
		wait_time(10)
		cntr = cntr + 1
		stat,new_val = get_shared_data(global_name)
	until (cntr >= max_wait_cntr) or (start_val ~= new_val)
end

-----------------------------------------------------------------------------------------------------------------
-- メインループ。無限ループを停止させる場合には script_kill() または "agent_task -k <taskid>" コマンドを使用する
-----------------------------------------------------------------------------------------------------------------
local event_stat
while true do
	run_indicator = not run_indicator
	local stat,page = get_shared_data(active_page_var)

	if page == "1" then
		BME280_page()
	elseif page == "2" then
		LightSensor_page()
	elseif page == "3" then
		clock_page()
		ip_address = "" -- ページ切り替え時に ip_page() 内で IP アドレスを再取得させる
	elseif page == "4" then
		ip_page()
	end

	global_change_wait(active_page_var,page,timer_interval)
end

スクリプトの先頭部分で2重起動防止のためのチェックをした後、環境センサ基板の SW1 スイッチの設定を行います。全てのスイッチは SERVER_START スクリプト中で入力モードになっていますので、ここではスイッチを押したときと離したときにイベントを検出する設定を行っています。スイッチ入力が行われる毎に、最初に修正作業を行った RASPI_CHANGE_DETECT イベントハンドラがコールされるようになります。

その後、abs_agent 内部のグラフィックライブラリを初期化しています。abs_agent では LCD グラフィックモジュール表示用のライブラリ関数を提供していて、簡単に文字や図形を描画することができます。グラフィックライブラリの詳細については abs_agent ユーザーマニュアルをご覧ください。

スクリプトの後半は、アプリケーションの表示ページ毎に関数が作成しています。このページ毎の関数は、<timer_interval 変数に格納した値>  x 10ms の間隔で繰り返しコールされます。

BME280_page() 関数では、前述の RASPI/DEVICE/BME280_READ スクリプトをコールして温度、湿度、気圧の測定値を取得しています。その後、グラフィックライブラリを使用して現在の測定値を画面に表示します。

グラフィックライブラリには2種類の ASCII フォントが組み込まれていますので簡単に英数字を表示することができます。日本語や “℃” 等の文字を表示する場合には、この例の様にビットマップパターンを指定することで表示できます。

同様に、LightSensor_page() 関数内では光センサの測定値を取得した後画面に表示します。このとき簡単なバーグラフも描画しています。

clock_page() 関数では OS の内部時計のデータを取得してデジタル時計を表示します。

ip_page() 関数では IP アドレスを表示します。このとき、ページ切り替えを行う毎に IP アドレスを新規に取得させることで、DHCP でアサインされたアドレスが変化したときにも対応できるようにします。

スクリプトの最後の部分がメインループになっていて、それぞれのページを繰り返し表示しています。SW1 を押すと表示するページを示すグローバル共有変数ENVSENSOR_ACTIVE_PAGE の内容が変化しますので、表示するページを切り替えています。

●考察・応用

Raspberry Pi の電源を入れたときに自動的に今回のアプリケーションを自動起動させることもできます。この場合には、Raspbian OS の /etc/rc.local 起動スクリプト中に abs_agent プログラムを起動させるための記述するだけで完了します。詳しい方法はabs_agent ユーザーマニュアルに記載されていますのでご覧ください。

今回のアプリケーションは完全にスタンドアロンで動作します。既存のRaspberry Pi に環境データや時計、IP アドレス表示機能をプラスできます。殺風景なファイルサーバーやWebサーバーを素敵なインテリアとしても活用できます。

また応用例としては、Raspberry Pi のCPU 温度やパフォーマンス情報、MQTT publish メッセージのリアルタイム表示機能も簡単に追加できると思います、ぜひチャレンジしてみてください。

ご意見や質問がありましたら、お気軽にメールをお寄せください(contact@allbluesystem.com) 。

それではまた。

 

MQTT接続のアラーム装置作成

●概要

今回はネットーワーク接続されたアラーム装置を Raspberry Pi で作成する例を紹介したいと思います。アラーム装置はネットワーク接続されていて、アプリケーションプログラム等からエラーの状態をランプの点灯やブザーで知らせることができます。

ネットワーク接続のアラーム装置は市販されていますが、今回作成するものは MQTT プロトコルで接続している点が特徴です。MQTT 接続を行うことで、同時に複数のアラーム装置を同期してアプリケーションから簡単に操作できます。また、アラーム装置を利用するアプリケーションの増設も簡単に行えます。

MQTT の Will メッセージ送信対象を今回のアラーム装置に合わせると、運用中のアプリケーションを一切変更することなく、接続エラーやアプリケーションダウンを知らせることができるようになります。

●構成図

下記が、アラーム装置のネットワーク構成図です。

今回のアラーム装置は Raspberry Pi 上に作成しています。Raspberry Pi の GPIO に3つのLED (赤色、黄色、緑色) とブザーを接続して警報を知らせることが出来ます。また、スイッチを一つ GPIO に接続して、警報を止めるためのリセットスイッチとして使用します。

アラーム装置として利用する Raspberry Pi には MQTT クライアント機能をインプリメントしています。また、アラーム利用側のプログラムでも MQTT クライアント機能が必要になります。MQTT ブローカはアラーム装置と利用側アプリケーションの両方からアクセス可能なコンピュータに1つ設置します。今回はアラーム装置の Raspberry Pi に MQTT ブローカソフトのmosquitto をインストールします。

今回紹介するMQTT アラーム装置は複数台設置することも可能です。詳しい設定方法は後述します。

●ハードウエア構成(必要な部品)

MQTT 接続のアラーム装置1台につき下記の部品が必要になります。このほかにも電源やネットワークケーブル、ジャンパ等が必要ですが省略しています。。

* Raspberry Pi (ver1,ver2,ver3 のどれでもOK)   x1

* LED(なるべく高輝度のもの,赤、黄、青など好きな色) x3 (LED1,LED2,LED3)

* 抵抗 (LEDのVfに合わせて 100~2KΩ程度で調整) x3  (R1,R2,R3)

* ブザー(電源接続だけ音がなる自己発振タイプ、圧電スピーカはダメ) x1 (BZ1)

* タクトスイッチ x1 (SW1)

* コンデンサ 0.1μF x1 (C1) 省略しても構いません

* ドライバIC TD62083AP x1 (IC1)

* ブレッドボード x1

結線図は下記になります。図中の Raspberry Pi では version3 を使用していますが、Raspberry Pi version1 等を使用する場合でもピンヘッダの位置は各バージョンで共通な部分のみを使用していますので、接続する GPIO のピン位置は同じです。

今回作成するアラーム装置では警報目的のため、高輝度LED を使用したいのですがRaspberry Pi の GPIO では出力電圧・電流とも不足します。このため、シンクタイプのドライバIC を利用して Raspberry Pi Vcc 5V ピンからLED とブザーの電源を供給しています。

Raspberry Pi ver1 上で配線した様子が下記になります。GND ピンやタクトスイッチ配線で一部上記の配線図と異なっていますが電気的には同一です。

●ソフトウエア構成

MQTT 接続のアラーム装置で動作させるソフトウエアについて説明します。Raspberry Pi には標準 OS の Raspbian の最新バージョンをインストールしておきます。

Raspberry Pi のハードウエア(GPIO,I2C,SPI) を操作したり、MQTT クライアント機能、Lua スクリプトの実行環境のために、オールブルーシステムの abs_agent プログラムを使用します。abs_agent のインストールキットと詳しいマニュアルは、こちらから Raspberry Pi 用のバイナリアーカイブをダウンロードできます。個人目的であればフリー版ライセンスが同梱されていますので、期間の制限なく直ぐに使用するこができます。

今回のアラーム装置では Raspberry Pi のGPIOにアクセスしますが、abs_agent ではメモリマップされたプロセッサのレジスタに直接アクセスしています。このため Raspberry Pi にセットアップした Raspbian OS に、追加のデバイスドライバやカーネルモジュールをインストールする必要はありません。(もちろん、インストールした状態でも動作します)

アラーム装置として動作させるメインロジックや、MQTT ブローカから PUBLISH メッセージを受信した時の動作、タクトスイッチを操作したときに動作するイベントハンドラは全て Lua スクリプトで記述します。詳しいスクリプトの内容は後述します。

●MQTT ブローカのインストール

最初に、MQTT ブローカを用意します。既存の MQTT ブローカが設置されていて Raspberry Pi からネットワークアクセス可能な場合には、その MQTT ブローカを使用できます。MQTT ブローカはLAN 内にあっても、ルーター装置の外のインターネット上にあっても構いません。

既存の MQTT ブローカが無い場合にはアラーム装置として使用する Raspberry Pi に オープンソースの mosquitto をインストールします。最新版の Raspbian OS 上でインストールするのは簡単で、コンソールから下記のコマンドを実行するだけで終了します。

$sudo apt-get install mosquitto

$sudo apt-get install mosquitto-clients

上記のコマンドを実行すると、MQTT ブローカとコマンドラインから使用可能な MQTT クライアントプログラムがインストールされます。 mosquitto MQTT ブローカとクライアントプログラムの詳しい内容については mosquitto のホームページを参照してください。また、過去の記事でも mosquitto について説明していますので是非ご覧ください。

●abs_agent プログラムのインストール

オールブルーシステムのホームページのダウンロードページから最新の abs_agent インストールキットを Raspberry Pi にダウンロードしてください。このときユーザーマニュアルも同時にダウンロードすると、詳しいインストール手順や詳細仕様を事前に確認できます。インストールキットは Raspberry Pi 用のバイナリキットを選択してください。

ダウンロードしたキットファイル(tar + gzip 形式) をRaspberry Pi 上の任意のディレクトリに配置してください。ここでは例としてデフォルトユーザー “pi” のホームディレクトリ /home/pi に配置してインストールを行います。

インストール作業はコンソールから “tar zxvf <キットファイル名>” コマンドでファイルを展開するだけで終了します。上記はコマンドを実行したときの様子です。

MQTT 接続のアラーム装置として使用するためには各種設定が必要なのですが、詳しくは後述しますのでここでは早速 abs_agent サーバーを起動してみます。動作中の詳しい情報を確認するためには、ログサーバーを Windows PC にインストールする必要があるのですが、設置しなくても abs_agent は動作可能です。ログサーバーの設置方法については、インストールキットと同じページからダウンロード可能な abs_agent ユーザーマニュアルのインストールの章をご覧ください。

コンソールから abs_agent をインストールしたトップディレクトリ(今回は /home/pi/abs_agent )に移動して、abs_agent プログラムを root 特権付きで起動します。”sudo ./abs_agent” コマンドで実行します。

上記の例では abs_agent を起動した後、”agent_stat” コマンドで abs_agent の動作ステータス情報を表示しています。

ログサーバーを設置している場合には、”-l <ログサーバーホスト名またはIP アドレス>” オプションを付けて “sudo ./abs_agent -l 192.168.100.45″ の様に指定します。192.168.100.45 はログサーバーとログクライアントプログラムをインストールした Windows PC の IP アドレスです。

ログサーバーを設置した Windows コンピュータでログコンソール画面を表示していると下記の様な abs_agent 起動メッセージが表示されます。

複数のアラーム装置を設置する場合でも、同一のログサーバーを指定することで全てのログを集約して表示・管理できます。

abs_agent プログラムは、起動されると Linux のデーモンプロセスとして OS に常駐します。このため、起動後はコンソールからログアウトしても構いません。”-f” オプション付きで abs_agent を起動するとデーモンではなくフォアグランドで動作します。この場合にはログサーバーに出力するのと同じメッセージをコンソール上でも表示します。詳しくはユーザーマニュアルをご覧ください。

●MQTT エンドポイントの設定

abs_agent プログラムでは、MQTT ブローカへの接続条件や自動で購読を開始するトピック設定などの情報(MQTT エンドポイント)をコンフィギュレーションファイルに記述します。 コンフィギュレーションファイルは abs_agent をインストールしたディレクトリに abs_agent.xml ファイルとして保存されていますので、これを編集して新規の MQTT エンドポイントを追加します。

abs_agent プログラムが実行中の場合でも abs_agent.xml 設定ファイルは何時でも編集することができます。ただし、新しい設定ファイルを有効にするためには abs_agent プログラムを再起動させる必要があります。今回接続する MQTT ブローカ(mosquitto) はアラーム装置の Raspberry Pi で動作していますので、これに接続するための設定を追加します。

以降は、アラーム装置(Raspberry Pi) の IP アドレスが 192.168.100.14 になっているものとして説明しますので、御自分の環境に合わせて適宜設定を変更してください。また、MQTT ブローカを動作させるコンピュータには必ず、LAN 内または Internet 上の固定 IP アドレスまたはアクセス可能なホスト名を割り当てる必要があります。MQTT クライアントとして動作させるだけのアラーム装置には固定 IP を割り当てる必要はなく DHCP で割り振っても問題ありません。これらは、常に MQTT クライアントがMQTT ブローカ側へのソケット接続を行うためです。今回の作成例では、MQTT ブローカとMQTT クライアントのアラーム装置は同じ Raspberry Pi に設置しています。

下記が、コンフィギュレーションファイル例です。

<?xml version="1.0" encoding="utf-8"?>
<Document xmlns="http://www.allbluesystem.com/xasdl">
  <Description>ABSAgent コンフィギュレーション</Description>
  <ServiceMain>
    <PortNumber type="integer">27101</PortNumber>
    <DefaultRemoteHost type="string">127.0.0.1</DefaultRemoteHost>
    <TimeStampMargin type="integer">0</TimeStampMargin>
    <AllowFileUpload type="boolean">True</AllowFileUpload>
    <UseMACProtection type="boolean">False</UseMACProtection>
  </ServiceMain>
  <Class>
    <Basic>
      <ServerKey type="string"></ServerKey>
      <LicenseKey type="string">..... この部分のライセンス文字列は使用中のファイルのものを使用します ....</LicenseKey>
      <AllowFileOperation type="boolean">False</AllowFileOperation>
    </Basic>
    <Convert>
      <AutoOnline type="boolean">True</AutoOnline>
    </Convert>
    <Masters>
      <AutoOnline type="boolean">True</AutoOnline>
      <MasterFile type="string">/home/pi/abs_agent/masters.xml</MasterFile>
      <XMLSessionPool type="integer">4</XMLSessionPool>
    </Masters>
    <Session>
      <AutoOnline type="boolean">True</AutoOnline>
    </Session>
    <Script>
      <AutoOnline type="boolean">True</AutoOnline>
      <ScriptFolder type="string">/home/pi/abs_agent/scripts</ScriptFolder>
      <SessionPool type="integer">16</SessionPool>
      <UsePeriodicTimer type="boolean">True</UsePeriodicTimer>
    </Script>
    <WebProxy>
      <AutoOnline type="boolean">True</AutoOnline>
    </WebProxy>
    <Serial>
      <AutoOnline type="boolean">True</AutoOnline>
      <DeviceList/>
    </Serial>
    <MQTT>
      <AutoOnline type="boolean">True</AutoOnline>
      <KeepAliveTimer type="integer">60</KeepAliveTimer>
      <EndPointList>
        <Item>
          <Title>アラーム装置MQTT接続</Title>
          <ClientID>alarm_123456</ClientID>
          <BrokerHostName>192.168.100.14</BrokerHostName>
          <PortNumber>1883</PortNumber>
          <AutoSubscribeTopicList>/alarm/#</AutoSubscribeTopicList>
          <AutoSubscribeQoSList>0</AutoSubscribeQoSList>
          <UserName/>
          <Password/>
          <WillTopic/>
          <WillMessage/>
          <WillQoS>0</WillQoS>
          <WillRetain>False</WillRetain>
          <RecvBuffInit>0</RecvBuffInit>
          <DetailLog>True</DetailLog>
        </Item>
      </EndPointList>
    </MQTT>
    <RASPI>
      <AutoOnline type="boolean">True</AutoOnline>
    </RASPI>
  </Class>
</Document>

設定ファイル中の <MQTT> タグ内の <EndPointList>..</EndPointList> で囲まれた <Item>..<Item> タグ内にMQTT ブローカへの接続条件(MQTTエンドポイント情報)を記載します。使用するタグの名前とデータ値の詳しい説明は abs_agent ユーザーマニュアルに記載されています。

MQTT エンドポイント設定の <ClientID>alarm_123456</ClientID> タグに書かれた内容は、同一 MQTTブローカに接続する MQTT クライアント毎にユニークな任意の文字列で置き換えてください。また、<BrokerHostName>192.168.100.14</BrokerHostName>部分は、使用する MQTT ブローカのホスト名または IP アドレスを設定します。<Title>アラーム装置MQTT接続</Title> 部分は、abs_agent の Lua スクリプトから MQTT エンドポイントを指定するときの名前になります。このタイトル名をパラメータに指定して、MQTT ブローカにデータを送信するライブラリ関数をコールします。

abs_agent 設定ファイルでは、その他のサーバー設定やシリアルデバイスの設定を行うこともできます。今回はMQTT のエンドポイントを1つ追加するだけで設定は完了します。abs_agent をインストールしたディレクトリの docs ディレクトリには、設定ファイルの記述例がテキストファイルで保存されていますので、これをコピー&ペーストで利用すると簡単に設定ファイルを編集できます。

●MQTT PUBLISH メッセージ受信時のイベントハンドラ設定

次に、MQTT ブローカからPUBLISH メッセージを受信したときの処理を記述します。abs_agent プログラムは前項で設定したエンドポイント設定に従って、起動時に MQTT ブローカへのソケット接続、MQTT-CONNECT 処理、MQTT-SUBSCRIBE リクエストが実行されます。自動購読設定では今回 “/alarm/#” トピックを指定していますので、先頭に “/alarm/” の文字列から始まるトピック名で作成された、下記の様なメッセージが MQTT ブローカに渡されたときにそのメッセージが配信されてきます。

(1) アラーム状態を更新するメッセージ例

トピック名   /alarm/signal/<任意の文字列>     メッセージ {“alarm_red”:”on” }

(2) アラーム装置の警報を停止させるメッセージ例

トピック名 /alarm/switch/<任意の文字列>    メッセージ {“sw1″:”on”}

上記(1) はアラーム装置のLED とブザーの出力ステートを変更するための JSON フォーマットの文字列メッセージです。今回のアラーム装置では幾つかのキー・値ペアのエントリを列挙することでアラーム装置を操作できる様に設計しました。

キー値はアラーム装置の3色の LED (“alarm_red”, “alarm_yellow”, “alarm_green”) とブザー(“alarm_buzz”)に対応します。キーに対応する値は LED の場合には 点灯”on” , 消灯 “off”, 点滅 “blink” の何れかを指定できます。ブザーの場合には停止 “off”, ピピピ音 “beep1″, ピーピー音 “beep2″ の値を指定できます。もし受信した JSON メッセージ中に各ハードウエアに該当するキー・値ペアが無い場合には現在の出力状態のまま変更しません。

MQTT ブローカから PUBLISH パケットを受信すると、MQTT_PUBLISH イベントハンドラスクリプトが自動的に実行されます。このイベントハンドラ中に上記の動作を行う様に処理を記述します。今回作成する MQTT_PUBLISH イベントハンドラの内容は abs_agent インストール時のデフォルトイベントハンドラスクリプト中にコメント文として書き込まれていますので、コメントアウトするだけで編集は完了します。スクリプトファイル名は <abs_agentをインストールしたディレクトリ>/scripts/MQTT_PUBLISH.lua です。

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)

-- MQTTアラーム装置のメッセージ解析
json = require('json')
local flag,sender = string.match(g_params["PublishTopic"],"/alarm/(.+)/(.+)")
if flag == "switch" then
	------------------------------------------------------------------------------------------------------------------
	-- トピックが "/alarm/switch/<送信側ホスト名または任意の文字列>" の場合にメッセージを JSON デコードして、
	-- "sw1" キーの値が "on" の場合にはグローバル共有変数 alarm_red, alarm_yellow, alarm_green, alarm_buzz を削除する
	------------------------------------------------------------------------------------------------------------------
	local msg = json.decode(PublishString)
	if msg.sw1 == "on" then
		set_shared_data("alarm_red","")
		set_shared_data("alarm_yellow","")
		set_shared_data("alarm_green","")
		set_shared_data("alarm_buzz","")
	end
end

if flag == "signal" then
	---------------------------------------------------------------------------------------------------------
	-- トピックが "/alarm/signal/<送信側ホスト名または任意の文字列>" の場合にメッセージを JSON デコードして、
	-- 得られたオブジェクトのキーと値をそのままグローバル共有変数に設定する。
	---------------------------------------------------------------------------------------------------------
	local msg = json.decode(PublishString)
	for k,v in pairs(msg) do
		set_shared_data(k,v)
	end
end

このイベントハンドラは MQTT ブローカから購読中の全てのトピックに対応する PUBLISH パケットを受信したときに共通で実行されます。このため、最初に下記の正規表現を利用したパターンマッチ関数でトピック名の部分文字列取り出して、アラーム装置の処理対象メッセージであるかを判断します。

local flag,sender = string.match(g_params["PublishTopic"],”/alarm/(.+)/(.+)”)

アラーム装置操作用のトピック文字列を指定してメッセージが配信されてきた場合には、flag 変数には “signal” または “switch” の文字列が格納されて、sender 変数には送信元ホスト名や任意の文字列が格納されます。パターンマッチに失敗するような、これ以外のトピック名の場合には以降の処理は行いません。

アラーム装置の状態を更新する “/alarm/signal/<任意の文字列>” のトピック名の場合には、JSON メッセージをデコードした後、列挙されたキーと値のペアをそのまま abs_agent のグローバル共有変数のキーと値にそれぞれセットします。Raspberry Pi の GPIO(LED、ブザー) を操作する部分は別スクリプトとして別途作成することにして、ここではフラグ(グローバル共有変数)の更新のみを行うようにしています。この様な構成にすることで機能を拡張しやすくなり、プログラム(スクリプト)の内容を簡単にすることができます。

アラーム装置をリセットする “/alarm/switch/<任意の文字列>” のトピック名のメッセージを受信した場合には、JSON メッセージをデコードした後、キー値が “sw1″, 値が “on” の時に上記のグローバル共有変数を全て削除します。

●アラーム装置として動作するためのスクリプトファイル作成

次に、上記の MQTT_PUBLISH イベントハンドラで更新されたアラーム状態を示すグローバル共有変数の値を定期的に監視して、Raspberry Pi の GPIO を操作するスクリプトを作成します。

スクリプト名は “RASPI/ALARM_TASK” で、<abs_agentをインストールしたディレクトリ>/scripts/RASPI/ALARM_TASK.lua に格納されていますので作成する必要はありません。スクリプトの内容は以下の様になっています。

--[[

●機能概要

グローバル共有変数にセットされたアラーム状態を示すフラグ値に対応して、
Raspberry Pi の GPIO に接続された LEDの点滅やブザーを鳴らす。

●参照するグローバル共有変数

---------------------------------------------------------------------------------
キー値            値                              操作するGPIO# と設定値
---------------------------------------------------------------------------------
"alarm_red"        "off" or "" (変数未定義)    GPIO#4  Low
                "on"                           GPIO#4  High
                "blink"                        GPIO#4  Low-High 繰り返し

"alarm_yellow"    "off" or "" (変数未定義)     GPIO#17 Low
                "on"                           GPIO#17 High
                "blink"                        GPIO#17 Low-High 繰り返し

"alarm_green"   "off" or "" (変数未定義)       GPIO#18  Low
                "on"                           GPIO#18  High
                "blink"                        GPIO#18  Low-High 繰り返し

"alarm_buzz"    "off" or "" (変数未定義)       GPIO#27  Low
                "beep1"                        GPIO#27  Low-High 早い繰り返し
                "beep2"                        GPIO#27  Low-High 遅い繰り返し

●備考

このスクリプトは無限ループに入って終了しないので、必ず別スレッドで起動してください。

●変更履歴

2016/08/23    初版作成

copyright(c) All Blue System

]]

-- 2重起動防止用チェック
if not exclusive_check(g_script) then
    log_msg("*ERROR* exclusive_check() failed. script = " .. g_script,file_id)
    return
end
log_msg("start..   TaskID = " .. g_taskid,g_script)

-- アラーム状態出力用の Raspberry Pi GPIO ピン番号設定
local RED_pin = 4
local YELLOW_pin = 17
local GREEN_pin = 18
local BUZZ_pin = 27
local SW_pin = 23
local short_timer_interval = 80      -- beep1 ブザー音間隔(ms)
local long_timer_repeat_count = 8    -- beep2 ブザー音間隔、LED 点滅間隔、グローバル共有変数のチェック間隔
                                      -- short_timer_interval の繰り返し回数で指定する

-- グローバル共有変数の最新の値のキャッシュ, beep2 間隔でグローバル共有変数から最新の値をロードして更新される
local alarm_red = ""
local alarm_yellow = ""
local alarm_green = ""
local alarm_buzz = ""

local led_stat = false            -- LEDとブザー(beep2) High - Low 繰り返しの現在のステート値
local buzz_stat = false           -- ブザー(beep1) High - Low 繰り返しの現在のステート値

-- LED と スイッチを接続する GPIO のモードを初期設定する。
if not raspi_gpio_config(RED_pin,"output","off") then error() end
if not raspi_gpio_config(YELLOW_pin,"output","off") then error() end
if not raspi_gpio_config(GREEN_pin,"output","off") then error() end
if not raspi_gpio_config(BUZZ_pin,"output","off") then error() end
if not raspi_gpio_config(SW_pin,"input","pullup") then error() end

-- スイッチ入力を検知するために RASPI_CHANGE_DETECT イベントを有効にする
if not raspi_change_detect(SW_pin,true) then error() end

-- short_timer_interval 間隔で実行
function short_cyc_task()

    -- ブザー(beep1) の状態を反転する
    buzz_stat = not buzz_stat

    -- ブザー(beep1)の GPIO 出力
    if alarm_buzz == "beep1" then
        if not raspi_gpio_write(BUZZ_pin,buzz_stat) then error() end
    elseif (alarm_buzz == "") or (alarm_buzz == "off") then
        if not raspi_gpio_write(BUZZ_pin,false) then error() end
    end

end

-- short_timer_interval * long_timer_repeat_count 間隔で実行
function long_cyc_task()
    local stat

    -- 最新のグローバル共有変数の値をキャッシュにロード
    stat,alarm_red = get_shared_data("alarm_red")
    stat,alarm_yellow = get_shared_data("alarm_yellow")
    stat,alarm_green = get_shared_data("alarm_green")
    stat,alarm_buzz = get_shared_data("alarm_buzz")

    -- LED の状態を反転する
    led_stat = not led_stat

    -- LED とブザー(beep2)の GPIO 出力
    if alarm_red == "on" then
        if not raspi_gpio_write(RED_pin,true) then error() end
    elseif alarm_red == "blink" then
        if not raspi_gpio_write(RED_pin,led_stat) then error() end
    else
        if not raspi_gpio_write(RED_pin,false) then error() end
    end

    if alarm_yellow == "on" then
        if not raspi_gpio_write(YELLOW_pin,true) then error() end
    elseif alarm_yellow == "blink" then
        if not raspi_gpio_write(YELLOW_pin,led_stat) then error() end
    else
        if not raspi_gpio_write(YELLOW_pin,false) then error() end
    end

    if alarm_green == "on" then
        if not raspi_gpio_write(GREEN_pin,true) then error() end
    elseif alarm_green == "blink" then
        if not raspi_gpio_write(GREEN_pin,led_stat) then error() end
    else
        if not raspi_gpio_write(GREEN_pin,false) then error() end
    end

    if alarm_buzz == "beep2" then
        if not raspi_gpio_write(BUZZ_pin,led_stat) then error() end
    elseif (alarm_buzz == "") or (alarm_buzz == "off") then
        if not raspi_gpio_write(BUZZ_pin,false) then error() end
    end

end

-- メインタスク、グローバル変数の値を定期的に監視してアラーム出力を行う
local cntr_long_timer = 0
while true do -- 無限ループを停止させる場合には script_kill() または "agent_task -k <taskid>" コマンドを使用する
    wait_time(short_timer_interval)
    if cntr_long_timer >= (long_timer_repeat_count - 1) then
        cntr_long_timer = 0
        long_cyc_task() -- 約 640ms 間隔で実行
    else
        cntr_long_timer = cntr_long_timer + 1
    end
    short_cyc_task() -- 約 80ms 間隔で実行
end

このスクリプトの先頭では2重起動のチェックを下記の文で行っています。

-- 2重起動防止用チェック
if not exclusive_check(g_script) then
    log_msg("*ERROR* exclusive_check() failed. script = " .. g_script,file_id)
    return
end

このスクリプトは直ぐに無限ループに入って、共有変数の値の監視と GPIO 操作を繰り返す動作になります。起動時には別スレッドで実行させて、スクリプト実行スレッドが常駐する形になります。このため、間違って複数回このスクリプトをバックグランドで実行しないようにチェックしています。

次に、下記の部分で Raspberry Pi の GPIO ポートの初期化を行っています。LED とブザーを接続している GPIO ポートは出力モードでプルアップ抵抗を切り離しています。タクトスイッチを接続している GPIO ポートは入力モードにして、プルアップを有効にします。

-- LED と スイッチを接続する GPIO のモードを初期設定する。
if not raspi_gpio_config(RED_pin,"output","off") then error() end
if not raspi_gpio_config(YELLOW_pin,"output","off") then error() end
if not raspi_gpio_config(GREEN_pin,"output","off") then error() end
if not raspi_gpio_config(BUZZ_pin,"output","off") then error() end
if not raspi_gpio_config(SW_pin,"input","pullup") then error() end

-- スイッチ入力を検知するために RASPI_CHANGE_DETECT イベントを有効にする
if not raspi_change_detect(SW_pin,true) then error() end

タクトスイッチを操作したときに GPIO 値が変化するので、これを検出して abs_agent の RASPI_CHANGE_DETECT イベントハンドラを自動実行するように設定しています。このイベントハンドラの内容も後で編集します。

下記の部分がメインループになります。80ms 間隔で short_cyc_task() 関数を実行して、640ms 間隔で long_cyc_task() 関数を実行します。long_cyc_task() 関数内では現在のグローバル共有変数にセットされたアラーム状態を示すフラグ値をローカル変数にロードして、LED の点滅やブザーのON-OFF のタイミングを判断しています。詳しい処理内容は前述の全体のスクリプト中のコメントをご覧ください。

-- メインタスク、グローバル変数の値を定期的に監視してアラーム出力を行う
local cntr_long_timer = 0
while true do -- 無限ループを停止させる場合には script_kill() または "agent_task -k <taskid>" コマンドを使用する
    wait_time(short_timer_interval)
    if cntr_long_timer >= (long_timer_repeat_count - 1) then
        cntr_long_timer = 0
        long_cyc_task() -- 約 640ms 間隔で実行
    else
        cntr_long_timer = cntr_long_timer + 1
    end
    short_cyc_task() -- 約 80ms 間隔で実行
end

この RASPI/ALARM_TASK スクリプトは、abs_agent 起動時に別スレッドで自動実行するように後で設定します。

●タクトスイッチ操作時のイベントハンドラ設定

アラーム装置のタクトスイッチが押されたときの動作を記述します。 RASPI/ALARM_TASKスクリプト最初の部分で、下記の文が実行されると GPIO#23 ピンが変化する度に、RASPI_CHANGE_DETECT イベントハンドラスクリプトが実行されます。

raspi_change_detect(SW_pin,true)

この RASPI_CHANGE_DETECT スクリプトを編集してタクトスイッチを操作したときに、MQTT ブローカに対してトピック名  /alarm/switch/<ホスト名>  でメッセージ {“sw1″:”on”} または{“sw1″:”off”} を送信するようにします。

このイベントハンドラの内容は abs_agent インストール時のデフォルトイベントハンドラスクリプト中にコメント文として書き込まれていますので、コメントアウトするだけで編集は完了します。スクリ プトファイル名は <abs_agentをインストールしたディレクトリ>/scripts/RASPI_CHANGE_DETECT.lua です。

file_id = "RASPI_CHANGE_DETECT"

--[[

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

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

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

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

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

RASPI_CHANGE_DETECT スクリプト起動時に渡される追加パラメータ
---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------

BitNumList		GPIO の監視対象ビット中で変化したGPIO ピン番号が入る
				複数同時に変化した場合にはカンマ区切りのリストになる。 18,22,23
				0 から 53 までの整数が入る

BitValList		BitNumList に格納されている GPIO ピンの変化後の現在の値。
				カンマ区切りのリストで表される場合にはBitNumList のピン番号
				並びと値の並びが対応しています。						0,1,1
				0 または 1 の値が入る

このスクリプト中で作成される配列
---------------------------------------------------------------------------------
変数名			説明
---------------------------------------------------------------------------------

change_bit[] 	数値配列。キーが GPIO ピン番号の整数で、配列の値は
				GPIO の現在値を示す。 0(low) または 1(high) の整数値
				たとえば上記の BitNumList, BitValList の "値の例" の場合には
				change_bit[] 配列には下記の値が格納されます。

				change_bit[18] = 0
				change_bit[22] = 1
				change_bit[23] = 1

				** メモ **
				Lua では存在しないキーの配列エントリにアクセスすると、
				値は nil が返ります。上記の例では change_bit[0] = nil です。

]]

log_msg("start..",file_id)

--------------------------
-- change_bit[] 配列作成
--------------------------
local arr_pos = csv_to_tbl(g_params['BitNumList'])
local arr_val = csv_to_tbl(g_params['BitValList'])
local change_bit = {}
for i,v in ipairs(arr_pos) do
	change_bit[tonumber(v)] = tonumber(arr_val[i])
end

----------------------------------------------
-- change_bit[] 配列内容確認のためログ出力
----------------------------------------------
for key,val in pairs(change_bit) do
	log_msg(string.format("change_bit[%d] = %d",key,val),file_id)
end

if change_bit[23] then -- SW を操作した?
	local msg
	local clientid = "アラーム装置MQTT接続"
	local topic = "/alarm/switch/" .. g_hostname
	if change_bit[23] == 0 then
		msg = '{"sw1":"on"}'
	else
		msg = '{"sw1":"off"}'
	end
	if not mqtt_publish(clientid,topic,msg,0) then error() end -- MQTTブローカに SW の状態を送信
end

このスクリプトでは GPIO#23 が操作されたかを判断した後、タクトスイッチを押し込んだ状態と離した状態の2つのタイミングで MQTT ブローカにメッセージを送信しています。先に説明した MQTT_PUBLISH イベントハンドラでは、押し込んだ状態のメッセージ {“sw1″:”on”} のみを選択してアラーム状態のフラグをクリアしています。

実は、このスクリプト中で MQTT ブローカにメッセージを送信しなくても、スイッチを操作したアラーム装置の状態のみをクリアするのであれば直接フラグの値を削除しても同じ動作になります。

今回の様に  MQTT ブローカ経由でスイッチ操作の処理を行うことで、複数のアラーム装置のリセットを連動させることが可能になります。送信するトピック名を工夫すると、アラーム操作やリセットで連動するアラーム装置を限定したグループに分けることもできます。

●アラーム装置タスクの自動起動設定

abs_agent 起動時に、自動的に RASPI/ALARM_TASK スクリプトを別スレッドで実行するように設定します。abs_agent が起動されてデバイスやサービスの準備が完了すると、SERVER_START イベントハンドラが1回だけ自動実行されます。

ここではその SERVER_START イベントハンドラを編集して RASPI/ALARM_TASK  スクリプトを起動するようにします。このイベントハンドラの内容は abs_agent インストール時のデフォルトイベントハンドラスクリプト中にコメント文として書き込まれていますので、コメントアウトするだけで編集は完了します。スクリ プトファイル名は <abs_agentをインストールしたディレクトリ>/scripts/SERVER_START.lua です。

file_id = "SERVER_START"

--[[
******************************************************************************
このスクリプトは abs_agent 起動時にコールされます
このスクリプトの実行は長くても数秒以内で必ず終了するようにしてください。
******************************************************************************
]]

log_msg("start..",file_id)

--[[
******************************************************************************

MQTT モジュールが有効な場合にエンドポイントの接続を開始させる

******************************************************************************
]]
local mstat,module_stat = service_module_status()
if not mstat then error() end
if module_stat["MQTT"] then
	script_fork_exec("MQTT_CONNECT_ALL","","")
end

-- Raspberry Pi GPIO アラームタスク起動
script_fork_exec("RASPI/ALARM_TASK","","")

script_fork_exec() ライブラリ関数は、abs_agent に設置された任意のスクリプトを別スレッドで起動するためのライブラリ関数です。スクリプトの前半部分には、MQTT ブローカへの接続処理がデフォルトで記述されていますが、この部分はこのまま変更しないでください。

●abs_agent の再起動

abs_agent の設定と、イベントハンドラ、ユーザースクリプトの準備が完了しましたので、abs_agent プログラムを再起動させます。

コンソールから下記のコマンドを入力して、 abs_agent を再起動させます。

agent_shutdown コマンドを使用して abs_agent プログラムを停止させます。実行中のスクリプトがある場合には全て停止させた後、全てのサービスモジュールが終了します。abs_agent が完全に停止まで数秒かかりますので、agent_shutdown コマンド実行後少し待ってから、agent_stat プログラムを実行してサーバーから応答が無いこと(ソケットエラーが発生)を確認しています。ログコンソールを表示しているとサーバー停止状態をメッセージで確認できます。その後、abs_agent プログラムを手動で起動します。

Raspberry Pi の電源を入れたときに abs_agent プログラムを自動起動させる場合には、OS の /etc/rc.local シェルスクリプトに起動コマンドを記述します。詳しい設定方法は abs_agent ユーザーマニュアルをご覧ください。/etc/rc.local はシステム起動プロセスで実行されますので、abs_agent プログラム起動時に “sodo” コマンドを併用する必要はありません。また、日本語をabs_agent プログラム内で扱うために LANG 環境変数をセットしている点に注意して下さい。

abs_agent が起動すると、ログコンソールには下記の様なメッセージが表示されます。

abs_agent 設定ファイルに記載した MQTT エンドポイント設定に従って、MQTT ブローカに接続する様子がログメッセージに出力されます。また、アラーム装置として動作させる RASPI/ALARM_TASK スクリプトが別スレッドで自動起動されています。別スレッドで実行中のスクリプトは agent_task コマンドで確認できます。agent_task コマンドでは、実行中のスクリプト・タスクを強制終了させることもできます。詳しくは abs_agent ユーザーマニュアルをご覧ください。

これで MQTT 接続のアラーム装置が完成しました。

●アラーム装置で警報ランプやブザーを鳴らす

早速アラーム装置を操作してみましょう。Raspberry Pi にインストールした MQTT ブローカのクライアントプログラム mosquitto_pub コマンドを使用して、アラーム装置を操作するメッセージをMQTT PUBLISH パケットに格納して送信してみます。

このコマンド例では、赤色 LED と 緑色 LED をそれぞれ点滅状態にセットしています。

メッセージ中の “blink” 文字列部分を “on” や “off” に変えると常時点灯や消灯になります。ブザーを鳴らす場合には、メッセージ中に下記の何れかのエントリを指定します。

“alarm_buzz”:”beep1″     短い繰り返し音(ピピピピ…)

“alarm_buzz”:”beep2″  長い繰り返し音(ピーピーピー…)

“alarm_buzz”:”off”     ブザー停止

ブザーはかなり大きな音がしますので、最初はマスキングテープなどで穴を塞いでおくことをお勧めします。

MQTT ブローカに接続可能な様々なクライアントからメッセージを送信してみてください。ちなみに以前の記事で紹介した Windows 版のMQTT クライアントプログラムから送信している様子は下記になります。

●リセットスイッチで警報を停止させる

アラームを停止させる場合には、アラーム装置のタクトスイッチを押します。複数のアラーム装置を設置している場合でも、何れか1つのアラーム装置のタクトスイッチを押すだけで、全てのアラーム装置のLED と ブザーが停止することを確認してください。

アラーム装置のLED とブザーの停止は、MQTT クライアントプログラムからメッセージを送信することでも操作できます。このとき2つのタイプのメッセージで操作できます。

1つ目の方法は、先ほど LED の点滅や点灯時に指定した様に、全ての LED とブザーに対して “off” を指定したメッセージを送信する方法です。

もう一つの方法はタクトスイッチを押し込んだ時に自動送信されるメッセージを、他のMQTT クライアントから同様に送信する方法です。下記は、mosquitto_pub プログラムでリセット操作用のメッセージを送信している様子です。

●複数のアラーム装置を設置する

今回作成した MQTT 接続のアラーム装置は、同一 MQTT ブローカに接続するように複数台設置することができます。この場合には、abs_agent の設定ファイル中に記述する MQTT エンドポイントの設定項目中の下記のタグの内容を変更してください。

<ClientID>alarm_123456</ClientID>

MQTT エンドポイント毎にユニークな文字列であれば何でも構いません。その他のスクリプトやイベントハンドラの内容は一切変更する必要はありません。

複数設置した場合でも、アラーム装置を利用する側のメッセージ送信は全く同じです。これはアラーム装置の追加や削除、入れ替えなどの作業を、クライアントプログラム側の設定を一切変更なく運用できることを示しています。

複数のアラーム装置を同時にリセットする場合でも、他のアラーム装置に影響なくどれか1台のタクトスイッチを操作するだけで自動で全てのアラーム装置が連動してリセットされる点も特徴です。

●Will メッセージの送信対象にアラーム装置を指定する

MQTT ブローカに接続するときには、遺言メッセージ(Will Message)を指定する機能があります。この Will Message を今回設置したアラーム装置操作用のメッセージにすると、MQTT ブローカへの接続が切れたときに自動的にアラームを鳴らすことができます。

下記は、以前の記事で紹介した Windows 版の MQTT プログラムで、CONNECT リクエストを実行するときに Will Message の設定をアラーム装置用に合わせている様子です。

上記は、クライアントプログラムの “Open” ボタンを押して、MQTT ブローカとソケット接続した後、CONNECT リクエストを2回繰り返して強制的にブローカとのソケットを切断させて Will メッセージをテスト送信している様子です。

MQTT ブローカを利用したアプリを作成する場合に、CONNECT コマンド送信部分の Will Topic と Will Message パラメータを、今回設置したアラーム装置のメッセージに合わせるだけで簡単にサーバーダウン時に警報を出す機能を追加することができます。

●応用・拡張

今回作成したアラーム装置は、お手持ちの Raspberry Pi に LEDとブザーを接続するだけで簡単に作成することができます。使用している abs_agent プログラムは、個人目的であればフリーライセンスとして期間の制限なく使用できますので是非ご利用ください。

Raspberry Pi の余っている GPIO にセンサーを接続したり、Wi-Fi 付きの安価なマイコンと組み合わせると、戸締り警報装置や、雨降りお知らせ装置なども簡単に実現できます。また、インターネット上の MQTT ブローカと組み合わせると、遠い場所で暮らしている家族を見守ることもできます。

アラーム装置で使用するLED の種類や発光パターンを増やすのも、スクリプトファイルを少し変更するだけで簡単に対応できます。また、Raspberry Pi に LCD 表示器を取り付けると、メッセージ表示機能付きのアラーム装置に改造することもできます、ぜひチャレンジしてみてください。

ご意見や質問がありましたら、お気軽にメールをお寄せください(contact@allbluesystem.com) 。

それではまた。

 

TOCOS TWE無線タグのセンサデータを MQTT Broker に送信、TWE-MQTT Gateway

●概要

リモートに設置した TOCOS TWE無線タグ・デバイスのセンサデータを MQTT Broker に送信するシステム構築例を紹介します。このシステムは TWE-Lite や TWE-Lite-2525A 等で動作する無線タグアプリ・ファームウエア(Samp_Monitor) を動作させて、これらのリモートデバイスに接続したセンサデータを MQTT Broker に送信します。

●子機の配線

リモートに設置する無線タグデバイス・子機には TWE-Lite DIP を使用しています。東京コスモス電機のホームページで公開されている TWE-Zero アプリの “無線タグアプリ(Samp_Monitor)” を書き込んで使用します。

無線タグファームウエアに設定するセンサ種別は、デフォルト設定のアナログセンサモードで使用しています。アナログ入力AI3(a2タグ) ピンには光センサを接続しています。

A/D の入力電圧が 0-2.4V なので、これを超えないようにフォトダイオードの負荷抵抗を調整してください。上記の回路では2つの5.6K の抵抗で分圧したものを AI3 に接続しています。

子機は複数同時に使用することもできます。また、ファームウエアに設定するセンサー種別を変えて、 I2C 接続の温度センサや気圧センサなどを使用することもできます。この場合には、自動的に MQTT Broker に送信する JSON フォーマットがのタグ名が対応する名前に変更されます。

●親機の配線

子機からのセンサデータを受信する親機を準備します。子機と同様に TWE-Lite DIP デバイスを使用しています。PC の USB ポートに直接接続できる スティックタイプの TWE デバイスや TWE ライタデバイスを親機として使用できます。ここでは、TWE-Lite DIP をブレッドボード上に配置して、USB シリアルアダプタ(秋月電子: AE-UM232R)経由で PC に接続しています。

子機側の TWE-Lite DIP ファームウエアの書き換えや、コンフィギュレーション設定にも上記の回路を使用します。このため、リセットボタンとプログラム書き込み用のボタンも配置しておきます。TWE-Lite DIP デバイスの電源は USB シリアルアダプタから供給される 3.3V を使用しています。

●親機の設定

最初に親機側の TWE-Lite DIP デバイスを設定します。購入直後の TWE-Lite DIP ファームウエアには “超簡単!TWEアプリ” が入っていますが、今回はこれを書き換えて “無線タグ専用の親機ファームウエア” を書き込みます。ファームウエアは東京コスモス電機の TWE-Zero アプリのページで公開されていますのでこれを使用します。詳しい書き込み方法はファームウエアのページを参照して下さい。

ここでは、ファームウエア書き込み後の親機側のコンフィギュレーション設定を説明します。親機の TWE-Lite DIP を USB シリアルアダプタに接続した状態で、シリアル端末ソフト(TeraTerm 等)で仮想 COM ポートに接続します。

“+” キーを3回連続で押してコンフィギュレーション画面に入ります。

最初に “Application ID” を設定します。この例では 0×11223344 を設定しています。この “Application ID” は接続する全ての子機側でも同じ値に設定します。

“Option Bits” に 0×00020000 フラグを既存の設定値と ”OR” した0×00020001 に設定しています。このフラグ設定によって、親機自身から出力されるカウンタ?(::ts=xxxx)データを抑止しています。リモートの無線タグ・デバイスから送信されるセンサデータ以外のデータレコードが親機自身から送信されて、シリアル出力のサイズが増えないようにしています。後で作成するMQTT ブローカに送信する部分の Lua スクリプト中で、これらの不要なデータレコードを取り除く処理を行いますので、このフラグを設定しなくても正常に動作します。

その他のフラグやコンフィギュレーション項目はデフォルトのままで大丈夫です。

●子機の設定

次に子機の TWE-Lite DIP デバイスを設定します。ファームウエアを書き換えますので、親機の設定で使用したブレッドボードから親機用のTWE-Lite DIP デバイスを抜いて、子機の TWE-Lite DIP デバイスを代わりに差し込みます。これらの作業をするときには、PC と接続している USB ケーブルを抜いた状態でデバイスの入れ替えをすると安全に操作できます。

親機と同様にメーカのホームページから”無線タグ専用の子機ファームウエア” を書き込みます。その後、子機のファームウエアを設定します。子機のコンフィギュレーション設定をするときだけは、TWE-Lite DIP の M2 ピンを GND に接続する必要があります。ブレッドボード上でジャンパケーブルを M2(26pin) と GND に接続してください。その後、シリアル端末ソフト(TeraTerm 等)でUSBシリアルアダプタに接続した後、ブレッドボード上のリセットボタンを押して子機ファームウエアのコンフィギュレーション画面に入ります。

“Application ID” には親機と同じ 0×11223344 を設定します。”Option Bits” にはOTA(無線経由での設定)を停止するためのフラグ 0×400 を有効にします。また、ここではセンサデータの送信間隔(Sleep Dur) を 300ms に設定していますが任意の値に設定しても構いません。

子機の設定が終了したら “save Configulation” で設定値を保存します。

全ての子機の設定を同様におこなったら、子機と親機の TWE-Lite DIP デバイスをそれぞれの本来の回路に戻しておきます。このとき、親機のブレッドボード上に配線した M2 <-> GND 間の配線を取り除くのを忘れないようにします。これで、無線タグデバイスのセットアップは完了しました。

●TWE-Lite タグデバイスの動作確認

MQTT broker への送信設定を行う前に、TWE 無線タグ・デバイスが動作しているかどうかを確かめてみます。

親機の USB シリアルアダプタにシリアル端末で再び接続してください。子機の無線タグの電源を入れると、下記の様なデータレコードが親機からシリアル送信されていればリモート側の無線タグは正常に動作しています。

*** Samp_Monitor (Parent) 1.05-5 ***
* App ID:11223344 Long Addr:81004152 Short Addr 0152 LID 00
::rc=80000000:lq=66:ct=0001:ed=81002A1E:id=0:ba=3310:a1=1922:a2=2467:p0=000:p1=000
::rc=80000000:lq=69:ct=0002:ed=81002A1E:id=0:ba=3280:a1=1845:a2=2467:p0=000:p1=000
::rc=80000000:lq=69:ct=0003:ed=81002A1E:id=0:ba=3400:a1=1891:a2=2467:p0=000:p1=000
::rc=80000000:lq=72:ct=0004:ed=81002A1E:id=0:ba=3440:a1=1975:a2=2467:p0=000:p1=000
::rc=80000000:lq=69:ct=0005:ed=81002A1E:id=0:ba=3400:a1=2035:a2=2467:p0=000:p1=000
::rc=80000000:lq=72:ct=0006:ed=81002A1E:id=0:ba=3440:a1=2105:a2=2467:p0=000:p1=000
::rc=80000000:lq=72:ct=0007:ed=81002A1E:id=0:ba=3440:a1=2120:a2=2467:p0=000:p1=000

もし、親機のファームエア設定で 0×00020000 フラグを設定していないときには、下記のようなシリアル出力になります。

::rc=80000000:lq=90:ct=0078:ed=81002A1E:id=0:ba=3440:a1=1435:a2=2467:p0=000:p1=000
::ts=435
::rc=80000000:lq=90:ct=0079:ed=81002A1E:id=0:ba=3460:a1=1442:a2=2467:p0=000:p1=000
::rc=80000000:lq=93:ct=007A:ed=81002A1E:id=0:ba=3470:a1=1435:a2=2467:p0=000:p1=000
::rc=80000000:lq=90:ct=007B:ed=81002A1E:id=0:ba=3440:a1=1418:a2=2467:p0=000:p1=000
::ts=436
::rc=80000000:lq=93:ct=007C:ed=81002A1E:id=0:ba=3450:a1=1418:a2=2467:p0=000:p1=000
::rc=80000000:lq=93:ct=007D:ed=81002A1E:id=0:ba=3460:a1=1440:a2=2467:p0=000:p1=000
::rc=80000000:lq=93:ct=007E:ed=81002A1E:id=0:ba=3460:a1=1430:a2=2467:p0=000:p1=000
::ts=437
::rc=80000000:lq=93:ct=007F:ed=81002A1E:id=0:ba=3440:a1=1420:a2=2467:p0=000:p1=000

これで、親機と子機の無線タグが正常に動作しているのを確認できました。複数の子機を同時に動作させることもできますので、この場合にはそれぞれのデバイスからのデータも表示されます。

動作確認が終了したら、シリアル端末のプログラムを終了させて COM ポートを開放してください。以降の設定で、親機からの出力される上記のシリアルデータを変換して MQTT Broker に JSON 文字列を送信します。

●TWE無線タグ・親機のシリアルデバイスを DeviceServer に登録

TWE無線タグ・親機から出力されるリモートセンサデータを取り込むために、DeviceServer にシリアルデバイスを登録します。”サーバー設定プログラム” を起動して、”SERIAL” タブを選択します。

親機が接続されている仮想 COM ポート(ここでは “COM10″) にデバイスタイプ “TWE”でシリアルデバイスを登録した様子です。デバイスの詳細設定は以下の様になっています。

デバイスタイプは “TWE” を選択します。シリアルデバイスのタイトル文字は適当な名前をつけて下さい。シリアルポート通信条件はファームウエアのデフォルト値 (15200baud, 8bit, 1stop-bit, no parity) に合わせます。

これで、親機からセンサデータを受信する度に、DeviceServer 側ではイベントハンドラスクリプト SERIAL_TWE.lua が実行されるようになります。

●MQTT エンドポイント作成

シリアルデバイスの作成に続いて、サーバー設定プログラムの “MQTT” タブを選択して、MQTT Broker との接続(エンドポイント)を作成します。

ここでは、2つのエンドポイントを作成しています。1つめが、TWE リモートセンサデータを MQTT Brokerに登録するための接続で、後の1つが、MQTT Broker からセンサデータを受信して、ログに出力したり LED 表示器に A/D 変換値を出力するための接続です。

今回は、構成をわかり易くするために MQTT Broker 接続のエンドポイントを2つに分けていますが、センサーデータ登録用と受信用のエンドポイントを1つで共有して運用しても構いません。

“サーバー設定プログラム”の “MQTT” タブを押してエンドポイントリストを表示した状態です。ここでは既にデータ登録用のエンドポイントとして、タイトル名 “センサーデータ登録” と データ取得用のエンドポイント、タイトル名 “センサーデータ取得” の2つのエンドポイントが登録されています。

“センサーデータ登録” のエンドポイントの詳細設定は以下の様になっています。

接続先の MQTT Broker は、IP アドレス192.168.100.14、ポート番号1883 で動作している RaspberryPi 上の mosquitto にしています。mosquitto MQTT ブローカについては、こちらの記事も参照してください。

“センサーデータ取得” のエンドポイントの詳細設定は以下の様になっています。

MQTT Broker の IP アドレスや ポート番号は同じですが、このエンドポイントでは起動時に購読するトピックを複数設定しています。

TWE 無線タグ・デバイスのセンサーデータは、下記のトピック名で送信されています。

”/twe/<TWE 子機のMACアドレスを表す16進数文字列>/Samp_Monitor”

全ての無線タグ子機から送信されるセンサーデータを購読するために、トピック名 “/twe/+/Samp_Monitor” を購読するように設定します。

”/+/+/io” や “/+/+/tdcp” のトピック購読は、リモートに設置した XBee デバイスやマイコンボード等からのセンサーデータ購読のために指定しています。今回はこれらからのトピック・データは使用していません。

これで、DeviceServer 側の親機のシリアル設定と MQTT エンドポイント設定は完了しました。サーバー設定プログラムの “次へ” ボタンを押して設定を反映させると DeviceServer が再起動してTWE 無線タグ親機へのシリアル接続と MQTT Broker へのエンドポイント接続が自動的に開始されます。

●MQTT Broker にTWE無線タグセンサ・データを JSON 形式で送信する

次に、DeviceServer 側で実行されているデフォルトのイベントハンドラスクリプトをエディタで変更して、MQTT Broker にセンサーデータを JSON 形式で送信するようにします。

TWE 無線タグ親機からセンサーデータを受信する毎に、SERIAL_TWE.lua イベントハンドラが実行されますので、この内容を変更して下記のようにします。

(ここで紹介しているイベントハンドラの内容はコメントアウトされた状態でインストールキットに含まれています。そのため、この記事の内容を試す場合にはイベントハンドラ中の該当部分のコメント指定を外して直ぐに使用できます)

file_id = "SERIAL_TWE"

--[[

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

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

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

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

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

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

--[[
********************************************************************

TWE(Samp_Monitor)->MQTT Gateway 機能の実装例

Samp_Monitor ファームウエアで動作中のリモート TWEデバイスから送信されたタグ・データを MQTT に登録します。
Samp_Monitor ファームウエアのオプション設定によって、送信されるタグの項目は変わります。詳しくは
ファームウエアのドキュメントを参照して下さい。このスクリプトでは送信されたタグのキーと値をそのまま
文字列ペアとして JSON に変換しています。

トピック名: "/twe/<Samp_Monitor フレーム中の "ed"(MACアドレス)タグの値>/Samp_Monitor"
メッセージ文字列: JSON キー名部分は下記スクリプト中の "tag_name" テーブルに設定することで変更できます

{"id":"0","ct":"0007","ed":"81002A1E","ba":"2840","rc":"80000000","lq":"57","p0":"000","a2":"2113","a1":"11..(キーと値のペアが続く)

送信先 MQTT ブローカは、サーバー設定プログラムを使用して
エンドポイントを登録して下さい。下記スクリプト中の end_point 変数に
エンドポイントのタイトル文字列または ClientID を設定します。

********************************************************************
]]

-- データが文字列 "::" から始まるデータタイプ(2)の場合で、且つ "ed"タグが含まれているものだけを、MQTTブローカ送信対象にする
if key_val and key_val["ed"] then
	local dev = key_val["ed"]
	local end_point = "センサーデータ登録"
	local topic = "/twe/" .. dev .. "/Samp_Monitor"
	local qos = 1

	-- TWE のタグ名を変更して JSON 形式にする場合は、変更したいタグを下記テーブルに指定する
	local tag_name = {}
	tag_name["ba"] = "battery"
	tag_name["ct"] = "count"
	tag_name["lq"] = "LQI"
	tag_name["te"] = "temperature"
	tag_name["hu"] = "humidity"
	tag_name["at"] = "pressure"
	tag_name["x"] = "acc_x"
	tag_name["y"] = "acc_y"
	tag_name["z"] = "acc_z"

	local json_str = '{'
	local first = true
	for key,val in pairs(key_val) do
		if first then
			first = false
		else
			json_str = json_str .. ","
		end
		if tag_name[key] then
			json_str = json_str .. string.format('"%s":"%s"',tag_name[key],val)
		else
			json_str = json_str .. string.format('"%s":"%s"',key,val)
		end
	end
	json_str = json_str .. '}'
	mqtt_publish(end_point,topic,json_str,qos)
end

イベントハンドラの最初の部分では、TWE-Zero アプリ(ファームウエア) で使用される幾つかの種類のレコードフォーマットを解析するための共通ルーチンが定義されています。

無線タグアプリ(Samp_Monitor) は上記コメント中の タイプ(2) のレコードフォーマットとして解析されて、Lua テーブル key_val[] にタグのキーと値が対応付けられて作成されます。

スクリプトの後半部分で、MQTT Brokerに送信するための JSON 形式の文字列を作成しています。key_val[] テーブルのキーと値は、TWE無線タグ親機から送信されるタグの、キーと値にそのまま対応しています。ここではこのキー名と値を利用して下記のような JSON 形式の文字列に変換します。

{“id”:”0″,”ct”:”0007″,”ed”:”81002A1E”,”ba”:”2840″,”rc”:”80000000″,”lq”:”57″,”p0″:”000″,”a2″:”2113″,”a1″:”11″,”a2″:”334″}

“ba” や ”lq” などのタグ名を、JSON データ利用側で判りやすいように “battery”, “LQI” などの名前に変換するための機能がスクリプト中に記述されています。詳しくはスクリプト中のコメントをご覧ください。

mqtt_publish() ライブラリ関数をコールすることで、JSON 形式に変換したリモート無線タグから受信したセンサデータを MQTT Broker に送信します。

これで、リモートに設置した全てのTWE 無線タグのセンサデータが MQTT Broker に送信されるようになります。TWE無線タグ・子機のオプション設定を変更して、接続するセンサーデバイスを変更した場合でも、出力されるデータのタグ名に対応した JSON 形式に変換されて MQTT Broker に送信されます。

●MQTT Broker からTWE無線タグ・センサ・データを受信する

次に、DeviceServer 側から MQTT Broker のセンサーデータの購読する部分のスクリプトを作成します。MQTT Broker に送信したセンサデータを再び DeviceServer 側に取り込んで利用する形になります。今回のシステム構成では、MQTT Broker に送信する前にセンサデータを処理できるのであくまでも、MQTT Broker からのデータ取得の一例として紹介します。

インストール直後のデフォルトスクリプトは、MQTT Broker から購読したデータ列を文字列に変換してログに出力しているだけですが、この部分を変更して子機無線タグのAI3 (“a2″タグ) に接続した光センサの値をログに出力します。MQTT_PUBLISH.lua イベントハンドラの内容に下記の記述を追加します。

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)

if g_params["Title"] == "センサーデータ取得" then

	---------------------------------------------------------------------------------------
	-- 過去データ確認用に時系列データベースに Publish されてきた文字列(JSON)を保管しておく
	---------------------------------------------------------------------------------------
	if not add_series_str(g_params["PublishTopic"],PublishString) then error() end

	------------------------------------------------
	-- その他のアクション定義
	------------------------------------------------
	json = require('json')
	local data_tbl = json.decode(PublishString) -- JSON デコードして Lua テーブル構造に変換

	if data_tbl.a2 then -- JSON データ中に "a2" キー名に対応するデータが存在するときだけ実行
		local a2_num = tonumber(data_tbl.a2)
		log_msg("A2 = " .. tostring(a2_num),file_id)
		------------------------------------------------
		-- 7セグLED表示器にメッセージ表示
		------------------------------------------------
		local tbl = {}
		tbl["com"] = "Arduino実験ボード#1"
		tbl["data"] = tostring(a2_num)
		tbl["width"] = "8"
		if not script_exec("ARDUINO/DEVICE/SPI_7SEGLED",tbl) then error() end
	end

end

add_series_str() をコールしている部分は、後で過去のデータをグラフ表示する時などに利用できるように、DeviceServer 内蔵のデータベースに取得した JSON 文字列をそのまま格納しています。

次の部分では、JSON 文字列をデコードして Lua のテーブル構造に変換しています。テーブルの形にしておくとで、センサデータの内容を取り出すことが簡単にできるようになります。data_tbl.a2 には JSON 文字列中の { …. “a2″:”xxxx” } の部分が入っていますので、これを数値に変換することで AI3 の A/D 変換値が得られます。ここでは、この値をログに出力しています。

動作中のログを下記に示します。MQTT Broker にセンサーデータを登録して、同時にそのセンサーデータを MQTT から購読した後上記スクリプト中で A/D 変換値を出力するまでがログに出力されています。

●おまけ

MQTT_PUBLISH.lua イベントハンドラスクリプト中からは、Arduino に接続した 7セグLED 表示器(秋月電子 M-06681) に現在の A/D 変換値を表示しています。

7セグLED 表示器は SPI 接続で簡単にスクリプト中から操作できますので、最新のデータを確認するのに便利です。

Arduino と DeviceServer 間はシリアル接続されいて、Firmata プロトコルで SPI 通信コマンドをやり取りしています。(Firmata プロトコルで Arduino をDeviceServerから操作するアプリ例はこちらの記事を参照してください)

動作の詳細はインストールキットに含まれている下記のスクリプトをご覧ください。また、Arduino で動作させるスケッチ(SensorControlModule.ino)もインストールキットに含まれていますのでご利用下さい。

Arduino に SPI 接続した LED表示器を DeviceServer から操作するスクリプト

C:\Program Files (x86)\AllBlueSystem\Scripts\ARDUINO\DEVICE\SPI_7SEGLED.lua

C:\Program Files (x86)\AllBlueSystem\Scripts\ARDUINO\DEVICE\SPI_WRITE.lua

それではまた。