WebSocket を使用した双方向通信リアルタイムアプリの作成

●WebSocket 概要

今回は WebSocket を利用した簡単な Webアプリケーション作成例を紹介したいと思います。最初に、WebSocket を利用するとどのようなことが可能になるかを説明します。WebSocket を利用すると主に下記の2つの機能を実現することができます。

(1) ブラウザ等から任意のタイミングでデータをサーバー側に送信できる。

(2) サーバーからも任意のタイミングで現在接続中のクライアント(Webブラウザ等) にデータ送信できる。

(1) に関しては HTTP プロトコルのみでも実現できますが、(2) は色々なテクニックを併用しないとなかなか実現できませんでした。WebSocket を使用すると、Webブラウザ自身の機能(JavaScript) だけで、任意のタイミングでサーバーからクライアント側にデータを送信するアプリを実現することができます。これを利用すると、サーバー側で検出したセンサーデータをリアルタイムにブラウザに表示することができるようになります。

WebSocket の仕様はかなり以前から策定されていて、RFC-6455 として現在のモダンな Webブラウザは殆どサポートしています。PC やスマートフォン・タブレット毎に専用のアプリを開発しなくても、プラットフォーム間で共通して実行可能な Web アプリでリアルタイム操作を実現できます。

●アプリケーション概要

今回作成するアプリは Raspberry Pi に LED とスイッチを接続して、スイッチを押すと LED の点灯と消灯を切り替えることができます。同時に、PCやスマホ・タブレットのWebブラウザから Webアプリを起動して、Raspberry Pi の LED をリモートから同様に操作することができます。

全体図は下記の様になります:

この時、Raspberry Pi に接続しているLED の状態(点灯・消灯) が変化すると、直ぐに Webアプリ上の LED ステータスも更新されます。

Webアプリを複数同時に操作することもできます。各々のWebアプリ上のスイッチを任意のタイミングで操作しても、正常に LED の点灯と消灯を切り替えることができます。もちろん、Raspberry Pi に接続している本物のスイッチも Webアプリと同時に操作できます。

今回の WebSocket を利用したアプリケーションは Raspberry Pi 本体と LED・抵抗器・スイッチをブレッドボード等に配線するだけで直ぐに動作します。是非お試しください。

●Raspberry Pi に LED と スイッチを接続

Raspberry Pi に接続するスイッチと LED、抵抗器の接続は下記のようにします。Raspberry Pi のバージョンによってGPIO ピン数が増減しますが、今回はどのバージョンでも共通に利用可能なポートを選択しています。

手元にあった Raspberry Pi 1 に接続した様子は下記になります。抵抗器はLED に直接ハンダ付けしたものを使用しているのでブレッドボードには配置されていません。また GND は配線図とは違うピンを使用しています。

●サーバー動作環境

LED やスイッチは Raspberry Pi のGPIO ポートに接続されていて、これらの GPIO を操作したり、Webアプリ配信用の Web(HTTP)サーバー機能、WebSocket サーバー機能を利用するためにオールブルーシステムの abs_agent プログラムを使用します。abs_agent のインストールキットと詳しいマニュアルは、こちらから Raspberry Pi 用のバイナリアーカイブをダウンロードできます。

個人目的であればフリー版ライセンスが同梱されていますので、期間の制限なく直ぐに使用するこができます。今回紹介する Webアプリケーションやスクリプトもインストールキットに最初から含まれていますので、一部コメントを削除するだけで簡単にセットアップできます。

●インストール

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

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

下記の操作例では、Raspberry Pi にデフォルトユーザー名 “pi” でログインした後、wget コマンドでインストールキットを直接ダウンロードしています。インストールキットのダウンロードファイルパス名は、ダウンロードページから最新の abs_agent のインストールキットへのリンク(URL)をコピーして使用すると簡単に指定できます。

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

tar コマンドを実行したディレクトリ(/home/pi)に abs_agent ディレクトリが作成されますので、その中のサーバー設定ファイル(abs_agent.xml)を vi エディタ等で編集してRaspberry Pi の CPU タイプを指定します。

この記事では、手持ちの Raspberry Pi 1 を使用していますので下記の XML タグに “BCM2708″ を記述しています。もし、Raspberry Pi 2 や Raspberry Pi 3 を使用する場合には “BCM2709″を記述してください。

abs_agent.xml ファイル中の下記キャプチャの矢印部分にある、<Hardware> タグの内容を、Raspberry Pi ver1用の “BCM2708″ に設定します。

●サーバー起動時のイベントハンドラ設定

ここからは、今回のアプリケーション特有のスクリプトを記述していきます。最初は、サーバー起動時に LED と スイッチを接続した GPIO のモードを設定するスクリプトを作成します。

abs_agent をインストールしたディレクトリ以下にある scripts/SERVER_START.lua をエディタで開いて下記の様に記述します。最新のインストールキットでは既に下記のスクリプト部分が記述されていますので、コメントを外すだけでOKです。

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

-------------------------------------------------------------------------------------------------------------------------------------
-- 下記のサンプルスクリプトの説明は、ホームページの Blog記事 "WebSocket を使用した双方向通信リアルタイムアプリの作成" をご覧ください
-------------------------------------------------------------------------------------------------------------------------------------
if not raspi_gpio_config(18,"output","off") then error() end
if not raspi_gpio_config(23,"input","pullup") then error() end
if not raspi_change_detect(23,true) then error() end -- スイッチ入力変化時にイベント発生
if not raspi_change_detect(18,true) then error() end -- LED出力変化時にイベント発生

raspi_gpio_config() ライブラリ関数を使用して、LED が接続されている GPIO#18 を出力モード・内部プルアップ無しに設定しています。また、スイッチが接続されている GPIO#23 も同様のライブラリ関数で、入力モード・内部プルアップ有効にしています。

また、スイッチと LED の各々の GPIO ポートが変化した場合に、abs_agent がこの変化を検出して RASPI_CHANGE_DETECT.lua (イベントハンドラ)スクリプトを実行する様に raspi_change_detect() ライブラリ関数で設定しています。

●スイッチ入力、LED出力変化時のイベントハンドラを設定

スイッチを操作すると、GPIO#23の値が変化してイベントハンドラスクリプトが自動起動されます。スクリプト名は scripts/RASPI_CHANGE_DETECT.lua になっていますのでこれを編集します。

また、LED の出力が変化したときも同じスクリプトが起動されますので、この処理部分も同じスクリプトファイル内に記述します。スクリプトの内容は以下の様になります。

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

-------------------------------------------------------------------------------------------------------------------------------------
-- 下記のサンプルスクリプトの説明は、ホームページの Blog記事 "WebSocket を使用した双方向通信リアルタイムアプリの作成" をご覧ください
-------------------------------------------------------------------------------------------------------------------------------------
if change_bit[23] then -- LED操作用 タクトSW を操作した?
	if change_bit[23] == 0 then -- SW を押した状態
		local stat,cur_val = raspi_gpio_read(18)
		if not stat then error() end
		if not raspi_gpio_write(18,not cur_val) then error() end
	end
end

if change_bit[18] then -- LED(GPIO#18) が変化した?
	local msg
	if change_bit[18] == 1 then
		msg = '{"led":"on"}'
	else
		msg = '{"led":"off"}'
	end
	log_msg(msg,g_script)
	websocket_emit_text('led_websock',msg)
end

スイッチを操作すると、change_bit[23] の値が 0(スイッチ押し込んだとき)または 1 (スイッチを離したとき)にセットされた状態になります。if 文でスイッチを押したときを判断して、現在の GPIO#18(LED) の値をリードします。次に、この値を反転させた値を再び GPIO#18 に書き込みます。これによって、LED の On/Off を切り替えることができます。

また、GPIO#18 (LED) の出力が変化すると、同じくこのスクリプトがコールされてきますので今度は GPIO#18(LED) の値を、WebSocket サーバーに接続中の WebSocket クライアント(Webブラウザ等) に websocket_emit_text() ライブラリ関数で送信します。この時の送信データはGPIO#18 の値に応じて、JSON 文字列形式で {“led”:”on”} または  {“led”:”off”} のどちらかになります。

WebSocket サーバーは abs_agent 起動時に自動的に作成されていて、常にクライアントからの接続を受け付けています。もし、上記の websocket_emit_text() ライブラリ関数実行時にWebSocketクライアントが接続されていない場合にはなにも行いません。

上記のスクリプトを記述するときには、イベントハンドラは複数同時に実行する点に気をつけてください。実際にはイベントハンドラ・スクリプトの処理は一瞬で終了しますので、平行して動作し続けるわけではありませんが、考え方としては全てのイベント(LED 点灯,LED 消灯,SW 押した,SW 離した)ごとに独立したスレッドでイベントハンドラが平行動作します。それぞれのイベント発生時に処理するスクリプトの内容(RASPI_CHANGE_DETECT.lua) は同じですが、起動パラメータ g_params[] はイベント発生毎に異なってきます。

●WebSocket サーバーがデータを受信したときのイベントハンドラを設定

このイベントハンドラは、Webアプリを起動した時とWebアプリ上の LED 操作ボタンを押したときにWebSocket サーバーにデータ送信されたときに実行されます。Web アプリからは JSON 文字列形式でデータを送信してきます。このイベントハンドラには、そのデータを受信した時の処理を記述します。

イベントハンドラのスクリプトファイルは scripts/WEBSOCKET_DATA.lua に配置されていますのでこれを編集します。

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
Channel			クライアントが WebSocket 接続時に指定した URL パス名部分の
				Channel 文字列が格納される。パス名 := "/<channel>/<SessionToken>"
																		"my_app1"

SessionToken	クライアントが WebSocket 接続時に指定した URL パス名部分の
				SessionToken文字列が格納される。セッショントークン文字列は
				WebSocket 接続時に認証に使用した後は、WebSocket サーバー側
				では一切使用しない。このため、イベント発生時にこの
				セッショントークンが abs_agent 側に存在するかは関知して
				いない点に注意。パス名 := "/<channel>/<SessionToken>"
																		"ST04B20D83368F1C"

WebSocketID		WebSocket クライアント接続毎に付けられたユニークな文字列。
				このパラメータ値を websocket_emit_text(), websocket_emit_binary()
				ライブラリ関数の第一パラメータに指定すると、イベント発生時に
				データを送信してきたWebSocketクライアント接続(1つ)にのみ
				データを送信することができる。
																		"WS04B20D98A65F4F"

PayloadType		WebSocketフレームタイプを示す。"text" または "binary" が格納される。
																		"text"
PayloadSize		受信したフレームデータのサイズ(バイト数)が入る			"128"

PayloadData		受信したフレームのバイナリデータを16進数文字列に変換した
				ものが格納されますPayloadType が "text" の場合は、
				ペイロードデータに格納されたUTF-8 文字列コードのバイト列
				が格納されています。イベントハンドラ中でこれらの文字列
				データをデコードする処理がデフォルトで記述されていますので、
				UTF-8 文字列を扱う場合にはデコード後の変数を利用することができます。
																		"010203414243"

]]

------------------------------------------------------------------------------------------
-- 受信したPayloadType が "text" の場合には
-- バイナリデータ列を UTF-8 文字列としてデコードしたものを PayloadString 変数に格納する
------------------------------------------------------------------------------------------
local PayloadString = ""
if g_params["PayloadType"] == 'text' then
	local pub_len = tonumber(g_params["PayloadSize"])
	PayloadString = readUTF_hex(bit_tohex(pub_len,4) .. g_params["PayloadData"])
end

if PayloadString ~= "" then
	log_msg("/" .. g_params["Channel"] .. "/" .. g_params["SessionToken"] .. " [" .. g_params["WebSocketID"] .. "] " .. PayloadString,g_script)
else
	log_msg("/" .. g_params["Channel"] .. "/" .. g_params["SessionToken"] .. " [" .. g_params["WebSocketID"] .. "] " .. g_params["PayloadData"],g_script)
end

-------------------------------------------------------------------------------------------------------------------------------------
-- 下記のサンプルスクリプトの説明は、ホームページの Blog記事 "WebSocket を使用した双方向通信リアルタイムアプリの作成" をご覧ください
-------------------------------------------------------------------------------------------------------------------------------------
if (g_params["Channel"] == "led_websock") and (PayloadString ~= "") then
	local msg
	local payload_tbl = g_json.decode(PayloadString)

	-- クライアント起動時に最新の LED(GPIO#18) ステータスをイベント送信元にだけ送信する
	if payload_tbl.event == "client_ready" then
		local stat,cur_val = raspi_gpio_read(18)
		if not stat then error() end
		if cur_val then
			msg = '{"led":"on"}'
		else
			msg = '{"led":"off"}'
		end
		websocket_emit_text(g_params["WebSocketID"],msg)
	end

	-- LED 操作ボタン(Webアプリ側のGUI)を押したときは LED(GPIO#18) の状態を反転させる
	-- RASPI_CHANGE_DETECT イベントハンドラ中のタクトスイッチ(GPIO#23)を押したときの処理と同じ内容
	if payload_tbl.event == "toggle_click" then
		local stat,cur_val = raspi_gpio_read(18)
		if not stat then error() end
		if not raspi_gpio_write(18,not cur_val) then error() end
	end

end

最初に、Webアプリ起動と同時に JSON文字列 {“event”:”client_ready”} をWebSocketサーバーに送信してきます。このデータを受信した時には現在の GPIO#18(LED) の状態をリードして、{“led”:”on”} または  {“led”:”off”} のどちらかをクライアントに送り返しします。これによって、GPIO#18(LED) の状態と Webアプリ上の LED GUI のステータスを一致させることができます。Webアプリ側で WebSocket データを受信したときの動作については後述します。

また、Webアプリ上の LED 操作ボタンを押した場合には、JSON 文字列 {“event”:”toggle_click”} が送信されてきます。これを受信した場合には、現在の GPIO#18 の値を反転させることで LEDの点灯・消灯を切り替えます。この処理は前述の RASPI_CHANGE_DETECT イベントハンドラ中のスイッチ入力時の処理と全く同じ内容になっています。

●サーバー起動

ここまでの設定で、スイッチ操作と サーバー側のWebSocket データ受信時の処理は完了していますので abs_agent を起動します。

インストールキットを展開した /home/pi/abs_agent ディレクトリに移動して、sudo コマンドを併用して abs_agent を起動します。

起動時に指定している -l (エル) <IPアドレス> パラメータは、abs_agent 実行時ログを別途 Windowsマシンに設置したログサーバーに送信するオプションです。もし、実行時ログを確認したい場合には、ホームページのダウンロードページから ABS-9000 LogServer をダウンロードして設置してください。詳しいインストール方法は abs_agent のユーザーマニュアルをご覧ください。実行時ログが必要ない場合には -l <IP> パラメータは省略できます。

abs_agent が起動すると、Raspberry Pi に接続したスイッチを操作することで、LED の点灯と消灯を切り替えることができると思います。

●Webアプリの説明

次に、クライアント側のブラウザで実行する Webアプリの説明をします。Webアプリは jQuery Mobile の GUI を利用して作成しています。

インストールキットを展開したディレクトリ以下の webroot/test/led_websocket に Webアプリのファイル一式が格納されています。アプリケーションのメインロジック部分となるのは HTML ファイル(index.html) と JavaScript ファイル(main.js)で、それぞれ下記のようになっています。

index.html の内容

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>WebSocket&LEDアプリケーション</title>
		<link rel="stylesheet" href="libs/css/themes/default/jquery.mobile-1.4.5.min.css" />
		<script src="libs/js/jquery.js"></script>
		<script src="libs/js/jquery.mobile-1.4.5.js"></script>
		<script src="libs/abs_agent/webapi.js"></script>
	</head>
	<body>

		<div data-role="page" id="login">
			<div data-role="header" data-position="inline">
				<h3>WebSocket&LED ユーザー認証</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="main_page">
			<div data-role="header" data-position="inline">
				<h3>LED 画面</h3>
				<a data-icon="home" id="logout_btn" href="#logout_caution" data-rel="dialog" data-transition="pop">Logout</a>
			</div>
			<div data-role="fieldcontain">
    			<label for="LED_status_rdo">LED</label>
				<input id="LED_status_rdo" type="checkbox" disabled="" name="LED_status_rdo">

			</div>
			<a class="ui-btn ui-btn-inline ui-icon-power ui-btn-icon-left " id="led_toggle_btn" >On/Off</a>
			<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="websock_quit_dialog">
			<div data-role="header" data-theme="b">
				<h1>*WEBSOCK ERR*</h1>
			</div>
			<div role="main" class="ui-content">
				<h2>WebSocket 接続エラー</h2>
				<p>WebSocketサーバーとの接続中にエラーが発生しました。サーバー側のログを確認した後、再ログイン操作を行ってください</p>
				<p><a data-role="button" id="websock_error_ok_btn" class="ui-btn ui-shadow ui-btn-a ui-icon-check ui-btn-icon-left">OK</a></p>
			</div>
		</div>

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

    </body>
</html>

index.html では Webアプリケーションのログイン認証画面と、LED 操作画面を定義しています。また、実行中のエラーダイアログもここで定義しています。

main.js ファイルの内容は下記になります。

//////////////////////////////////////
// WebSocketClient
//////////////////////////////////////
var ws_channel = "led_websock";
var ws_port = 9090;
var ws;

// Web(HTTP)サーバーと同じコンピュータで動作している WebSocketサーバーに接続
function connect_websocket_server(){
	ws = new WebSocket("ws://" + location.hostname + ":" + ws_port + "/" + ws_channel + "/" + session_token);

	ws.onopen = function (e) {
		// WebSocketサーバーへ接続したら、新しいクライアントが接続したことを知らせるイベントを送信する。
		// するとサーバーから最新の LED ステータスが送信されて、LEDチェックボックス GUI の状態と一致させることができる。
		ws.send('{"event":"client_ready"}');
	}

	ws.onclose = function (e) {
		log("Disconnected: " + e.reason);
	}

	ws.onerror = function (e) {
		$("body").pagecontainer("change","#websock_quit_dialog", { transition: "pop",role:"dialog"});
	}

	// WebSocketデータフレームを受信した
	ws.onmessage = function (e) {
		if (typeof e.data == "string"){
			// JSON 文字列 {"led":"on"} または {"led":"off"} に従って LEDチェックボックスの状態を更新する。
			var obj = JSON.parse(e.data);
			if (obj.led == "on") {
				$("#LED_status_rdo").prop('checked',true).checkboxradio('refresh');
			}
			if (obj.led == "off") {
				$("#LED_status_rdo").prop('checked',false).checkboxradio('refresh');
			}
		}
	}
}

// WebSocket サーバー切断
function disconnect_websocket_server(){
	ws.close();
}

// WebSocketエラーのダイアログから復帰する場合はログイン画面に戻る
$( "#websock_error_ok_btn" ).on( "click", function(event, ui){
	session_token = ""; // abs_agent 側に残ったログインセッションは後で自動削除される
	$("body").pagecontainer("change","#login", { transition: "pop" });
});

//////////////////////////////////////
// LED 画面
//////////////////////////////////////

// LED操作ボタンが操作された
$("#led_toggle_btn").on( "click",function(event, ui){
	// イベントメッセージを送信することで、サーバー側の WEBSOCKET_DATAイベントハンドラ中で GPIO を操作して
	// LED の状態を反転させる。
	// LED(GPIO) の状態が変化すると、サーバー側でGPIO 値変化を検出して RASPI_CHANGE_DETECTイベントハンドラが起動する。
	// RASPI_CHANGE_DETECTイベントハンドラ中では、最新の LED(GPIO) ステータスを格納したメッセージを WebSocket 経由で
	// 全クライアントに送信する。
	ws.send('{"event":"toggle_click"}');
});

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

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

// サーバー側のログイン処理が成功したら、WebSocketサーバーに接続を試みてメイン画面に移動する
function login_callback(data){
	if (data.Result == "Success"){
		session_token = data.SessionToken;

		connect_websocket_server();

		$("body").pagecontainer("change","#main_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){
	disconnect_websocket_server();
	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 != ""){
		connect_websocket_server();
		$("body").pagecontainer("change","#main_page", { transition: "pop" });
	}
});

main.js では主に、index.html で作成された GUI からのイベントを受けて、そのときの動作を作成しています。

ログイン認証が成功すると、login_callback() 関数がコールされます。この時、abs_agent 側にはセッショントークンが作成されて、main.js では session_token 変数にセッショントークン文字列が格納されます。

続いて login_callback() 関数では、WebSocket サーバーへ接続するために、connect_websocket_server() 関数をコールして、この関数の中で Web(HTTP) サーバーと同じコンピュータで動作している WebSocket サーバーに接続します。これらのサーバー機能は abs_agent 側で自動作成されていて、各々 8080(HTTP), 9090(WebSocket)ポートをデフォルトで使用しています。

WebSocket サーバーに接続するときのパス名は ws:<host>:9090/led_websock/<セッショントークン> にしています。パス名中の “led_websock” は任意の文字列で、WebSocket サーバー側でデータをクライアント側(複数)に送信するときに、今回の Webアプリが動作しているクライアントのみに限定するために使用しています。

WebSocket 接続が完了すると、WebSocket サーバーに JSON 文字列 {“event”:”client_ready”} を送信します。すると、先に説明した WEBSOCKET_DATA.lua イベントハンドラが起動されて、その中で現在の GPIO#18 の状態を格納した JSON 文字列 {“led”:”on”} または  {“led”:”off”} のどちらかをクライアント側に送り返してきます。

この JSON 文字列を受信すると、connect_websocket_server() 関数で定義したイベントハンドラ ws.onmessage() がコールされ、この中で LED ステータスを表すチェックボックス(ReadOnly) のチェック状態を Raspberry Piの GPIO#18(LED)に合わせるように更新します。

また、この ws.onmessage() 関数は、先に説明した RASPI_CHANGE_DETECT.lua イベントハンドラ中で、GPIO#18(LED) が変化したときにもコールされて同様の動作を行います。これによってチェックボックスのチェック状態を Raspberry Pi GPIO#18(LED) に常に追従させることができます。

また、Webアプリ中の LED 操作ボタンを押した場合には

$(“#led_toggle_btn”).on( “click”,function(event, ui) {….} 部分がコールされます。

この関数は、WebSocket サーバーに JSON 文字列 {“event”:”toggle_click”} を単に送信するだけの動作です。このデータを WebSocket サーバーで受信すると、先に説明した WEBSOCKET_DATA.lua イベントハンドラが起動されて、その中で GPIO#18 の値を反転させます。GPIO#18 (LED) の値を反転させると、サーバー側では RASPI_CAHNGE_DETECT イベントハンドラが起動されて、最新の LED ステータスを格納した JSON 文字列を WebSocket でクライアントに配信してきます。このときの ws.onmessage() 関数の動作は先ほど説明した内容と同じです。

このように、サーバー側(LED,スイッチ)やWebアプリ側の GUI イベントを WebSocket 上で双方向にやりとりすることで、リモートからの LEDステータス確認や操作を可能にしています。

●Webアプリ起動

既に Raspberry Pi に接続したスイッチで LED を操作できるようになっていますが、これを先ほど設定したWeb アプリを使用してリモートからも操作してみます。

最初に、Webアプリ内では abs_agent へのログイン認証をおこなっていますのでそのためのユーザーを作成します。Webユーザー作成には abs_agent をインストールしたディレクトリ以下にある agent_webuser プログラムを使用します。

ユーザー名は任意の文字列で構いませんが、今回はユーザー名 “user” 初期パスワード “pass” で作成しています。

Web アプリは abs_agent の Web(HTTP) サーバーに下記のパスにアクセスすると起動します。

http:<Raspberry Pi のIPアドレス>:8080/test/led_websocket/index.html

起動すると下記のログイン認証画面が表示されます。

ここで先ほど登録したユーザー名とパスワードで abs_agent の ログイン認証を行います。ログインに成功すると、Web アプリの LED 操作画面が表示されます。

LED チェックボックスのチェック状態は、Raspberry Pi に接続している LED と一致していると思います。”On/Off” ボタンを押すと本物の LED の点灯と消灯を切り替えることができます。もちろん、Web アプリ上の LED チェックボックスの状態もそれに合わせて切り替わります。

Web アプリは同時に複数起動することもできます。下記は、iPod Touch から同様に Webアプリを操作している様子です。この場合でも、それぞれの Webアプリの操作に合わせて本物の LED の状態と全 Webアプリ上のチェックボックスが連動して切り替わると思います。

●考察・応用

今回は GPIO 出力に LEDを接続していますが、これを市販のリレーHAT 等の出力ポートに変更すると、リモートから家電などをコントロールできるようになります。

また、サーバー側に接続したセンサーデータのサンプリング毎に WebSocket でイベント送信すると、Web ブラウザを利用したダッシュボードを作成することもできます。

●WebSocket パケットの観察

Webアプリを動作させているときの WebSocket データを WireShark でキャプチャした様子を下記に載せておきます。最初に HTTP GET で接続した後 Switching Protocols 応答によって WebSocket 接続が開始されて、その後 WebSocket データフレームをやりとりしている様子が良くわかります。

●おまけ(Webアプリ動作中に他のプログラムで GPIO を操作するとどうなるの?)

下記の Python スクリプトでは GPIO#18 出力の High/Low を繰り返すことができます。

# -*- coding: utf-8 -*-
#
# GPIO#18 On-Off 繰り返し
#
import RPi.GPIO as GPIO
import time
PIN = 18
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN,GPIO.OUT)

for _ in xrange(10):
    GPIO.output(PIN,True)
    time.sleep(0.3)
    GPIO.output(PIN,False)
    time.sleep(0.3)

# 下記のクリーンアップを実行した場合には、H/Wレジスタが変更され abs_agent 側で
# 再度 raspi_gpio_config() をコールしないといけなくなるので実行しない事。

#GPIO.cleanup()

このスクリプトを Web アプリ動作中に Raspberry Pi のコンソールから実行してみます。スクリプト名を led_blink.py で保存しておいた場合には下記のコマンドで実行できます。

Raspberry Pi の LED が点滅すると同時に、Web アプリ側の LED チェックボックスも連動して点滅していると思います。

——

この記事で設定したAPI ライブラリ関数等の使用方法やその他の機能についてはユーザーマニュアルに詳しく記載されています。インストールキット中の docs/user_manual.pdf またはここから最新のユーザーマニュアルをダウンロードして参照してください。

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

それではまた。

 

エッジデバイスのLEDマトリックスに情報表示、Raspberry Pi + ScrollpHATHD

●概要

今回は、エッジデバイスで取得したセンサーデータや、ステータスメッセージなどを LED マトリックスに表示するアプリケーションを紹介します。離れた場所からでも視認性が高いので、センサーデータなどを常時監視が必要なアプリケーションに最適です。

ハードウエアは Raspberry Pi Zero WH に PIMORONI 製の Scroll pHAT HD を接続しています。動作中の様子は下記の様になります。

動作しているときの様子

Raspberry Pi 上では、Raspbian OS と オールブルーシステムの abs_agent が動作しています。Scroll pHAT HD を操作するためのライブラリは、PIMORONI 社で提供されている Python ライブラリは使用せずに、Lua スクリプトで全ての描画処理を行っています。このため、イベントハンドラや Web API 等から簡単に表示内容を更新することができ、またCPU 負荷もかなり抑えることができています。

●セットアップ方法

必要なハードウエアは Raspberry Pi 本体と Scroll pHAT HD だけです。今回は Raspberry Pi Zero WH にセットアップする例で説明します。Scroll pHAT HD のコネクタは半田付けが必要ですが、ピンソケットだけなので難易度は低く簡単です。

Raspberry Pi WH と Scroll pHAT HD を接続したら、Raspbian OS をセットアップします。運用中は LCD ディスプレイやキーボードなどを Raspberry Pi Zero に接続していると邪魔なので、WiFi ネットワーク経由で操作できるように SSH サーバーのセットアップも行って下さい。

今回紹介するアプリでは、Raspbian の I2C ドライバ・ユーティリティや PIMORONI 社で提供されているライブラリは使用しませんのでインストールする必要はありませんが、インストールした状態でも問題ありません。

デフォルトの “pi” ユーザーでログイン後、abs_agent のインストールキットをここからダウンロードしたものを tar コマンドで展開します。

abs_agent のインストール

tar コマンドを実行したディレクトリに abs_agent ディレクトリが作成されますので、その中のサーバー設定ファイル(abs_agent.xml)を vi エディタ等で編集してRaspberry Pi の CPU タイプを指定します。今回は Raspberry Pi Zero を使用しますので下記の XML タグに “BCM2708″ を記述してください。エディタでサーバー設定ファイルを編集します。Raspberry Pi ver2 や ver3 を使用する場合には “BCM2709″にします。

下記キャプチャの矢印部分にある、<Hardware> タグの内容を、Raspberry Pi Zero 用の “BCM2708″ に設定します。

次に、Scroll pHAT HD 操作用の Lua ライブラリを有効にするために、インストールキットを展開した abs_agent/scripts/preload/100_RASPI ディレクトリ内にある “_scrollphathd” ディレクトリ名の先頭にあるアンダースコア “_” を取り除いて “scrollphathd”にリネームします。

最後に、abs_agent 起動時に文字列データをスクロール表示させるタスクを自動起動する設定を行います。abs_agent/scripts/SERVER_START.lua ファイルをエディタで開いて、Scroll pHAT HD 用のスクロール表示タスクの起動部分のコメントを外して有効にします。ここでは2種類のタスクを選択できます。1つ目が1つの文字列をスクロール表示するタスク(RASPI/SCROLLPHATHD_DISPLAY_TASK)で、もう一つが複数の文字列を順に文字列ごとにスクロール表示するタスク(RASPI/SCROLLPHATHD_BULLETIN_BOARD_TASK)です。ここでは、最初の方の1つの文字列を表示するタスクを有効にしています。

これで設定は完了です。もし Windows マシンがある場合にはログサーバーのセットアップを行っておくと動作ログをネットワーク経由で取得することができます。ここでは Windows PC にログサーバー(192.168.100.45) を設置しています。

最初は手動で abs_agent を起動してみます。I2C ハードウエア(実際にはプロセッサのレジスタ)に直接アクセスするために、sudo コマンドで特権を与える必要がある点に注意してください。OS (Raspbian) 起動に abs_agent を自動起動させる場合には /etc/rc.local ファイルに起動コマンドを書き込んでおきます。詳しい設定方法は abs_agent ユーザーマニュアルを参照してください。

正常に起動すると、Scroll pHAT HD に Raspberry Pi の IP アドレスがスクロール表示されます。

この状態で、abs_agent のグローバル共有変数に設定した文字列を Scroll pHAT HD に表示できます。スクリプトやコンソールから文字列を設定する場合には、グローバル共有変数名 “SCROLLPHATHD_MESSAGE” に任意の文字列を設定します。文字列中には日本語も使用できます。

例えば、コンソールから agent_data クライアントプログラムを使用してグローバル共有変数に値を設定する場合には下記の様なコマンドを入力します。

./agent_data -k SCROLLPHATHD_MESSAGE -v 今日は晴れ

すると、Scroll pHAT HD には下記の様に文字列が表示されます。(長い文字列は自動的にスクロール表示されます)

●スクロール表示させるタスク(RASPI/SCROLLPHATHD_DISPLAY_TASK)の説明

abs_agent 起動時に自動起動して、スクロール表示を行っている RASPI/SCROLLPHATHD_DISPLAY_TASKスクリプトの内容は以下のようになっています。

--[[

●機能概要

Scroll pHAT HD LEDマトリックス表示器に文字列を表示する。
表示する文字列データはグローバル共有変数に設定されているものを使用する。

長い文字列はスクロール表示される。表示可能な文字は ASCIIと日本語で、それ以外の
文字は表示できない。

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

---------------------------------------------------------------------------------
キー値                      値
---------------------------------------------------------------------------------
SCROLLPHATHD_MESSAGE	"<message string>"

表示するメッセージ文字列を設定する。
設定可能な文字列の最大長は英数字の場合に約 256文字です。(Max 1536ピクセル)

●セットアップ

このスクリプトは ScrollpHAT HD (17x7 pixel) PIMORONI LEDマトリックス出力用ライブラリ関数を
使用します。インストール直後はライブラリが無効になっていますので、ライブラリの
ディレクトリ名をリネームして有効にしてください。方法は、ライブラリを格納しているディレクトリ名
"preload/100_RASPI/_scrollphathd" の、先頭についたアンダースコアを取り除いて
"preload/100_RASPI/scrollphathd" にリネームした後 abs_agent を再起動させてください。

●備考

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

●変更履歴

2018/05/17     日本語フォント表示機能を追加

2018/05/06     bright パラメータを追加

2018/04/06     初版作成

copyright(c) 2018 All Blue System

]]

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

-----------------------------------------------------------------------------------------------------------------
-- 指定したグローバル共有変数の値が変更されるか、もしくは指定されたカウント値 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)
	return new_val
end

---------------------------------
-- デバイスとタスク内変数初期化
---------------------------------
if not raspi_i2c_clock(1,1500) then error() end	-- I2C bus1 クロック設定 100KHz = 150MHz / 1500
scrollphathd_try_init()								-- ScrollpHATHD 初期化
local message_var = "SCROLLPHATHD_MESSAGE"			-- 表示する文字列データを取得するグローバル共有変数
local font = 2 									-- 5x7smooth フォント
local stat,misaki_font = shmem_copy("MISAKI_FONTX",0,0) -- 日本語フォントデータをグローバル共有メモリから作業用にロードする
if not stat then
    log_msg("*ERROR* MISAKI_FONTX not found.",g_script)
	return
end

-----------------------------------------------------------------------------------------------------------------
-- メインループ。無限ループを停止させる場合には script_kill() または "agent_task -k <taskid>" コマンドを使用する
-----------------------------------------------------------------------------------------------------------------
local pixel_data			-- 描画時のピクセルデータを一時的に保持する
local pixel_idx			-- 描画時のピクセルデータを一時的に保持する
local org_x = 0			-- 文字列の描画開始座標
local org_y = 0			-- 文字列の描画開始座標
local disp_msg = "???"		-- 現在表示中の文字列データ
local new_msg = ""			-- 最後に取得した最新の文字列データ
local sw = 0				-- 文字列のピクセル幅
local sh = 0				-- 文字列のピクセル高
local first_wait = 50		-- スクロール表示時の最初と最後にウェイトする時間 x10ms
local inter_wait = 3		-- スクロール中のウェイトする時間 x10ms
local max_x = 0
local bright = 0.5			-- 描画時の明るさ。0 から 1.0 までの範囲で指定
while true do
	if disp_msg ~= new_msg then -- 新しい文字列データがセットされたので描画する
		disp_msg = new_msg
		pixel_data = {}
		pixel_idx = {}
		scrollphathd_clear()
		-- sw,sh = scrollphathd_draw_string(pixel_idx,pixel_data,org_x,org_y,disp_msg,font,bright) -- 英数字のみ
		sw,sh = scrollphathd_draw_jstring(pixel_idx,pixel_data,org_x,org_y,disp_msg,misaki_font,bright) -- 英数字と日本語
		scrollphathd_draw_commit(pixel_idx,pixel_data)
		scrollphathd_transfer(0,0)
		scrollphathd_display(0)
	end
	max_x = org_x + sw
	if (max_x > 17) then -- スクロール表示が必要か?
		scrollphathd_transfer(0,0) -- 最初の1画面分を表示
		new_msg = global_change_wait(message_var,disp_msg,first_wait)
		if disp_msg ~= new_msg then goto SCROLL_FINISH end
		if max_x > 1535 then max_x = 1535 end
		for x0 = 1,max_x - 17 do -- 画面に表示する部分を1ピクセル右にずらす
			scrollphathd_transfer(0,x0)
			new_msg = global_change_wait(message_var,disp_msg,inter_wait)
			if disp_msg ~= new_msg then goto SCROLL_FINISH end
		end
	end
	new_msg = global_change_wait(message_var,disp_msg,first_wait)

::SCROLL_FINISH::

end

このスクリプトでは、グローバル共有データにセットされた文字列を1文字毎に文字コードに分解した後、フォントテーブルを利用してピクセルデータを作成しています。その後、Scroll pHAT HD に表示する位置を1ピクセルづつずらしてデータを転送します。

上記のスクリプトから Scroll pHAT HD を I2C バスで操作するために、Lua スクリプトで作成したライブラリ関数を使用しています。ここでは、これらのライブラリ関数の説明は省略しますが、もし内容を確認したい方は、abs_agent/scripts/preload/100_RASPI/scrollphathd ディレクトリに格納されているスクリプトファイルをご覧ください。

●Web API から表示する文字列を設定する

abs_agent のグローバル共有変数は Web API から内容を変更できますので、Web アプリケーション等から簡単に Scroll pHAT HD の表示内容を設定できます。

Web API 経由で操作する場合には、通常 abs_agent 内に専用の Web ユーザーアカウントを作成して、ログイン認証した時に取得できるセッション情報を使用します。

LAN 内などで簡単に利用するために、常に有効な固定のセッション情報を予め abs_agent 側で作成しておくと、ログイン認証を省略して Web API を操作することができます。ここでは予め、固定のセッション情報を作成する方法を説明します。

abs_agent 起動時に自動実行されるスクリプト(SERVER_START.lua) ファイルにセッションを作成するスクリプトを記述します。abs_agent/scripts/SERVER_START.lua ファイルをエディタで開いて、下記の様なスクリプトを記入します。

create_session() ライブラリ関数に指定した文字列(セッショントークン) を使用すると、常に Web API 経由でグローバル共有変数の操作やスクリプト実行が可能になります。

最後に、上記の設定を有効にするために abs_agent を再起動させます。単純に Raspberry Pi を Raspbian OS ごとリブートさせても構いませんが、ここではステップ毎にコマンドを入力して再起動させてみます。コマンドの実行例は下記の様になります。

最初に、バックグランドで動作しているScroll pHAT HD のスクロール表示タスクを agent_task コマンドで確認しています。表示された <task_id> 文字列を使用して、agent_task -k <task_id> コマンドを実行すると、実行中のバックグランドタスクを強制終了させることができます。

その後、agent_shutdown コマンドを実行して abs_agent プログラムを終了させます。全てのサービスモジュールをシャットダウンするのに数秒かかりますので、しばらく待ったあと、abs_agent を手動で起動させます。

SERVER_START.lua スクリプトに設定した固定のセッション情報を確認するために、agent_session コマンドを実行しています。セッションが作成されていれば、そのセッショントークン文字列を指定するだけで、Web API 経由で abs_agent にアクセスできます。

早速 PC の Web ブラウザからアクセスして Scroll pHAT HD の表示内容を設定してみます。

上記は Windows で動作している Firefox ウェブブラウザの URI に 、Web API アクセス用のパスを入力した様子です。IP アドレスと HTTP ポート番号は Raspberry Pi の IP アドレスと abs_agent 設定ファイル(abs_agent.xml)に記述されたポート番号に合わせます(デフォルト値は 8080)。/command/json/shared_data が abs_agent のグローバル共有変数を操作するためのコマンドパスに相当します。

URL の session パラメータには、先に設定した固定のセッショントークン文字列を指定します。key パラメータにはスクロール表示タスクで監視しているグローバル共有変数名(SCROLLPHATHD_MESSAGE)を、value パラメータに表示したい文字列を指定します。日本語を URL パラメータに指定する場合には、本来 URL エンコードしたものを指定しないといけないのですが、Firefox の URI 欄では勝手にエンコードしてくれるようで、そのまま記述できるので大変便利です。

HTTP プロトコルでコマンドを実行すると、レスポンスが JSON フォーマットで返ります。Firefox ブラウザでは HTTP で取得したJSON 文字列が自動で整形されて表示されます。このとき同時に、Scroll pHAT HD では設定した文字列がスクロール表示されている筈です。

●エクセルのセル内容を表示する

次は、Windows PC のプログラムから表示する文字列を設定する例を紹介します。abs_agent のインストールキット中にある DLL ファイル、または ABS-9000 LogServer のインストールキットに含まれる DLL を利用すると、Windows プログラムから直接 abs_agent 上のグローバル共有変数を更新することができます。

ここではログサーバーを設置した Windows PC 上でマイクロソフト・エクセルを動作させて、セルの内容を Scroll pHAT HD に表示してみます。

先に紹介した Web API 経由のアクセス時には、認証用にセッション情報を必要としまし。これは、Web 経由でアクセスする場合には不特定のホストからのアクセスになるために、アクセス毎の認証が必要になるためにこのような仕様にしています。

DLL 経由でアクセスする場合や abs_agent 間でアクセスする場合には特定のホストからのアクセスに限定できますので、セッション情報は不要になります。その代わりに予め、アクセスされる側の abs_agent にアクセスする側のホスト名を登録しておきます。ここでは agent_hosts コマンドで Windows PC のホスト名(ここでは eagle)を abs_agent に設定しています。

この状態で Windows PC(eagle) 上で実行するエクセルからアクセスしてみます。エクセルファイルを開くと下記の様な画面が表示されます。このファイルはインストールキット中の contrib/Excel/Agent_ScrollpHATHD.xls ファイルに格納されています。

サーバー設定タブで表示される画面上で、abs_agent のホスト名または IP アドレスをセルに代入してください。

その後、文字列表示タブを開いて項目の値のセル内容を更新すると、自動的に合計値が計算されてセルに表示されます。このとき、Scroll pHAT HD 上には合計セルの値が表示されます。

手動更新ボタンを押すと、その右横のセルに設定している文字列を Scroll pHAT HD に表示します。これらの動作を行っている ワークシート中のマクロは下記の様に記述されています。

マクロからコールしている AG_set_shared_data() ライブラリ関数が DLL ライブラリのラッパー関数になります。これらのラッパー関数定義については、ワークシート中の標準モジュール(XASDLCMDIntf) に記述されています。また DLL 自身の仕様については abs_agent のユーザーマニュアルをご覧ください。

動作したときの様子は記事の最後に添付した動画をご覧ください。

●考察

今回は、Raspberry Pi に簡単に接続可能な LED 表示器を利用して、様々な方法でデータを表示できることを紹介しました。LCD と違って遠くからでも視認しやすいですので、カウンタ値やエラーメッセージの表示に適していると思います。

●動作例の動画

メッセージ表示を行っているときの動画を載せましたのでご覧ください。(音量注意)

この記事で設定した内容の詳しい説明や、ログサーバーのインストール方法、abs_agent の自動起動方法、Web API コマンドや DLL ライブラリ使用方法についてはユーザーマニュアルに詳しく記載されています。インストールキット中の docs/user_manual.pdf またはここから最新のユーザーマニュアルをダウンロードして参照してください。

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

それではまた。

 

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 アプリのソースが含まれていますので、簡単にセットアップは完了します。

(2018/11/06 追記:abs_agent インストールキットのバージョンによって グラフWeb アプリが格納されているディレクトリが <abs_agentインストールディレクトリ>/webroot/app/summary に変更されています。その場合は記事中の webroot/app/chart 部分を適宜読み替えて下さい)

その後、abs_agent を起動します。起動コマンドは “sudo abs_agent -l <log_server>” です。ログサーバーを設置していない場合には “sudo abs_agent” になります。下記のコマンド実行例はabs_agent をリスタートさせています。動作中の abs_agent を停止させる agent_shutdown コマンドを実行させた後、agent_stat がエラーを返すことで完全にサーバーが停止しているのを確認してから abs_agent の起動コマンドを実行しています。

●I2C バス経由でセンサデバイスを操作してデータ取得  全体図中の(1)

ここからは、Raspberry Pi に接続されたセンサデバイスを操作して、データを取得するスクリプトをセットアップします。

Raspberry Pi と拡張バスで接続した環境センサ基板には、BME280 センサと明るさセンサが搭載されています。環境センサ基板の拡張バスにピンを付けてI2C バスと電源ラインを引き出しています。このピンをブレッドボードと接続して TMP102 温度センサを接続しています。(ハードウエア構成項の写真をご覧ください)

センサデバイスを Raspberry Pi から操作するのは、abs_agent に設置した Lua スクリプトを使用します。BME280 と明るさセンサをI2C バスでアクセスしてデータを取得するスクリプトについては前回の記事で紹介しています。今回追加したTMP102 温度センサにアクセスしてデータを取得するスクリプト(RASPI/DEVICE/TMP102_READ)は下記のようになっています。

--[[

●機能概要

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

●リクエストパラメータ
---------------------------------------------------------------------------------
キー値          値                                               値の例
---------------------------------------------------------------------------------
bus             I2C バス番号                                     "1"
                "0" または "1"を指定、省略時は "1" を使用する

●リターンパラメータ
---------------------------------------------------------------------------------
キー値          値                                               値の例
---------------------------------------------------------------------------------
temperature     センサーから取得した摂氏温度                     "12.5"
                                                                 "-25.0"

●備考

●使用条件・免責事項

このスクリプトファイルは自由にお客様が複製、改変を行うことができます。またお客様のアプリケーションと
共にこのファイルを配布することができます。

このスクリプトは公開されているデバイス仕様書を元に、オールブルーシステムがサンプル実装したものです。
お客様がこのファイルを利用される場合には、お客様または第三者に損害が生じた場合でも、
オールブルーシステムは損害賠償その他一切の責任を負担しません。

●変更履歴

2016/05/10  abs_agent RASPI H/W モジュール用に移植

2014/04/23    初版作成

ABS-9000 DeviceServer        copyright(c) All Blue System

]]

local slave_addr = "48"
local bus = 1

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

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

-----------------------------------------------------------------------
-- TMP102温度レジスタの値を取得する
-- pointer register 0x00 をセットした後、2 バイトのレジスタ値を取得する
-----------------------------------------------------------------------
local stat,result = raspi_i2c_write(bus,slave_addr,"00",2)
if not stat then error() end

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

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

Raspberry Pi のI2C バス#1のスレーブアドレス 0×48 にアクセスして、データレジスタを Read するだけで温度を取得できます。レジスタデータは 2 の補数で表現されているので、これを符号付整数に戻した後、係数を掛けて摂氏温度に変換しています。

ここで、コンソールから BME280, 明るさセンサ、TMP102 温度センサを操作してデータを取得してみます。それぞれのセンサを取得するスクリプトを agent_script コマンドで実行して、リターンパラメータにセンサデータが返ってくるのを確認します。

スクリプト実行時に I2C バス経由でデバイスを操作しますが、同時に他のプロセスで同じデバイスを操作していても ライブラリ関数内部で適切に排他制御されます。このため、前回の記事で作成した表示アプリと今回のデータ取得動作をバックグランドで同時に動作させることができます。

●センサデータをインメモリデータベースに保存  全体図中の(2)

前項でセットアップしたスクリプトを使用して、abs_agent 内のインメモリデータベース(ユーザーマニュアルでは “FASTDB” と略しています)にセンサデータを格納する部分を作成します。インメモリデータベースには、任意のキーとデータ値(Double値)を時系列に格納します。

インメモリデータベースはファイル I/O を使用しないので、Raspberry Pi 等のフラッシュメモリで作成されたファイルシステム上で動作させている場合でも長期間安定して運用することができます。ただし、abs_agent を終了した場合やコンピュータの電源を落とした場合には全てのデータは消えてしまいます。

下記のスクリプト(RASPI/ENVSENSOR_DATA_STORE)で環境センサ基板のセンサーデータをインメモリデータベースに格納します。

--[[

●機能概要

環境センサーボード(スイッチサイエンス社製)に搭載している BME280 センサと光センサの
測定値を MQTT ブローカに送信する。

同時にローカルコンピュータのインメモリデータベース FASTDBにもデータを保存する。

●備考

abs_agent 設定ファイルに MQTT ブローカ接続用のエンドポイントを下記例を参考に作成してください。
ブローカのホスト名や接続ユーザー名、パスワード、ClientID等は環境に合わせて変更します。
エンドポイントタイトル名を変更した場合には、このスクリプト中のタイトル名部分も変更して
ください。

abs_agent.xml ファイル中の MQTTサービスモジュール設定例:

    <MQTT>
      <AutoOnline type="boolean">True</AutoOnline>
      <KeepAliveTimer type="integer">60</KeepAliveTimer>
      <EndPointList>
        <Item>
          <Title>センサーデータ送受信</Title>
          <ClientID>abs9k:93501-raspi3</ClientID>
          <BrokerHostName>192.168.100.14</BrokerHostName>
          <PortNumber>1883</PortNumber>
          <AutoSubscribeTopicList>/EnvSensor/+</AutoSubscribeTopicList>
          <AutoSubscribeQoSList>0</AutoSubscribeQoSList>
          <UserName/>
          <Password/>
          <WillTopic/>
          <WillMessage/>
          <WillQoS>0</WillQoS>
          <WillRetain>False</WillRetain>
          <RecvBuffInit>2048</RecvBuffInit>
          <DetailLog>False</DetailLog>
        </Item>
      </EndPointList>
    </MQTT>

●変更履歴

2017/5/1     初版作成

copyright(c) 2017 All Blue System

]]

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

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

---------------------------------------------------
-- インメモリデータベースに測定データを保存する
---------------------------------------------------
if not fastdb_add("温度(2階)",tonumber(BME280["temperature"])) then error() end
if not fastdb_add("気圧(2階)",tonumber(BME280["pressure"])) then error() end
if not fastdb_add("湿度(2階)",tonumber(BME280["humidity"])) then error() end
if not fastdb_add("明るさ(2階)",tonumber(ENVSENSOR["light"])) then error() end

---------------------------------------------
-- MQTT ブローカに測定データを送信する
---------------------------------------------
local end_point = "センサーデータ送受信"
local topic = "/EnvSensor/" .. g_hostname
local qos = 0
local json_str = '{'
json_str = json_str ..        '"temperature":' .. BME280["temperature"]
json_str = json_str .. "," .. '"pressure":' .. BME280["pressure"]
json_str = json_str .. "," .. '"humidity":' .. BME280["humidity"]
json_str = json_str .. "," .. '"light":' .. ENVSENSOR["light"]
json_str = json_str .. '}'

if not mqtt_publish(end_point,topic,json_str,qos) then error() end

前項でコンソールから手動で実行した “RASPI/DEVICE/BME280_READ” と “RASPI/DEVICE/ENVSENSOR_LIGHT_READ” スクリプトをこのスクリプト中から呼び出して結果を取得した後、fastdb_add() ライブラリ関数をコールしてインメモリデータベースに格納しています。

このとき、センサーデータ毎に任意のキー名を指定することで、後の集計操作時にデータ利用し易くできます。fastdb_add() ライブラリ関数の詳しい仕様については abs_agent ユーザーマニュアルをご覧ください。

“RASPI/ENVSENSOR_DATA_STORE” スクリプトには MQTT ブローカへの送信部分も記述されていますが、これは後の項で説明します。

同様に TMP102 温度センサのデータをインメモリデータベースに格納するスクリプト”RASPI/TMP102_DATA_STORE” は以下の様になります。

--[[

●機能概要

TMP102 温度センサの測定値をローカルコンピュータのインメモリデータベース FASTDBに保存する。

●変更履歴

2017/5/6     初版作成

copyright(c) 2017 All Blue System

]]

---------------------------------------------------------------------------------
-- TMP102 センサー
---------------------------------------------------------------------------------
local stat,TMP102 = script_exec2("RASPI/DEVICE/TMP102_READ","","")
if not stat then error() end

---------------------------------------------------
-- インメモリデータベースに測定データを保存する
---------------------------------------------------
if not fastdb_add("温度(TMP102)",tonumber(TMP102["temperature"])) then error() end

これも、先ほどと同様に fastdb_add() ライブラリ関数を使用して、TMP102 温度センサから取得したデータをインメモリデータベースに格納します。

次に定期的にこれらのセンサデータをインメモリデータベースに格納する部分を作成します。上記の “ENVSENSOR_DATA_STORE” と “TMP102_DATA_STORE” スクリプトを定期的にコールするためには、abs_agent で 1分毎に自動起動されている”PERIODIC_TIMER” イベントハンドラスクリプト中に下記の様に記述するだけで完了します。インストールキットに含まれる “PERIODIC_TIMER” イベントハンドラスクリプトには下記の内容は記述されていませんので、ここだけは手動で修正を行って下さい。

今回のシステムでは1 分毎にセンサデータを取得していますが、センサデータ取得間隔を大きくするためにはグローバル共有変数を使用したカウンタを作成することで簡単に10 分や 1 時間単位に変更できます。また、秒単位の間隔でコールしたい場合には “TICK_TIMER” イベントハンドラスクリプト中に同様の記述を行って下さい。

file_id = "PERIODIC_TIMER"

--[[

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

PERIODIC_TIMER イベントハンドラスクリプトは約 1 分に1回自動的に実行されます。

一つのスクリプトの実行は長くても数秒以内で必ず終了するようにしてください。

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

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

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

]]

-- log_msg("start..",file_id)

---------------------------------------------------------------------------------------
-- 環境センサーボード(スイッチサイエンス社製)に搭載している BME280 センサと光センサの
-- 測定値を定期的に MQTT ブローカに送信する
---------------------------------------------------------------------------------------
if not script_fork_exec("RASPI/ENVSENSOR_DATA_STORE","","") then error() end

---------------------------------------------------------------------------------------
-- Raspberry Pi に外付けした TMP102 温度センサのデータを定期的に取得して
-- abs_agent 内部の FASTDB データベースに格納する
---------------------------------------------------------------------------------------
if not script_fork_exec("RASPI/TMP102_DATA_STORE","","") then error() end

------------------------------------------------------------
-- 8時間に一回実行する
------------------------------------------------------------
stat,val = inc_shared_data("TIME_8H")
if not stat then error() end
if (tonumber(val) >= 480) then
	if not set_shared_data("TIME_8H","") then error() end -- counter clear

	-------------------------------------------------------------
	-- FASTDB データベースから 10 日以上過去のデータを削除する
	-------------------------------------------------------------
	if not script_fork_exec("FASTDB_PURGE","days_before","10") then error() end

end

abs_agent のインメモリデータベースはコンピュータの物理メモリサイズ以上のデータは保持できないので、 定期的に古いデータを削除するスクリプトが上記の “PERIODIC_TIMER” イベントハンドラ中に組み込まれています。

デフォルトでは 10 日以上経過した古いデータは削除されますので、もしこれ以上の期間のデータを保持したい場合には、上記の “FASTDB_PURGE” スクリプトをコールしている script_fork_exec() ライブラリ関数で指定しているパラメータを変更してください。

ここまでのセットアップで、Raspberry Pi に接続した環境センサと TMP102温度センサのデータが1分毎にローカル側のインメモリデータベースに格納されるようになりました。

●MQTT ブローカ接続設定、センサデータをMQTT ブローカに送信  全体図中の(3)

先ほど説明した環境センサ基板のセンサーデータをインメモリデータベースに格納するスクリプト(RASPI/ENVSENSOR_DATA_STORE)中で、MQTT ブローカへセンサーデータを送信する部分のセットアップを行います。

abs_agent ではサーバー設定ファイル(abs_agent.xml) にMQTT ブローカへのエンドポイント(ホスト名やポート番号、デフォルト購読トピック等の設定)を記述することで、スクリプトやイベントハンドラ中から何時でもデータを送信(publish) することが出来るようになります。

abs_agent 起動時には設定ファイルに記述した全てのエンドポイントの MQTT ブローカに自動的に接続して、デフォルトのトピック購読などを開始します。また、接続中には KeepAliveTimer 間隔でPING パケットの処理や購読したトピックメッセージの受信処理、接続エラー発生時の再接続などが全て自動で行われます。

今回、センサーデータを受信するために作成する MQTT エンドポイントの記述例は、下記のようになります。

    <MQTT>
      <AutoOnline type="boolean">True</AutoOnline>
      <KeepAliveTimer type="integer">60</KeepAliveTimer>
      <EndPointList>
        <Item>
          <Title>センサーデータ送受信</Title>
          <ClientID>abs9k:93501-raspi3</ClientID>
          <BrokerHostName>192.168.100.14</BrokerHostName>
          <PortNumber>1883</PortNumber>
          <AutoSubscribeTopicList>/EnvSensor/+</AutoSubscribeTopicList>
          <AutoSubscribeQoSList>0</AutoSubscribeQoSList>
          <UserName/>
          <Password/>
          <WillTopic/>
          <WillMessage/>
          <WillQoS>0</WillQoS>
          <WillRetain>False</WillRetain>
          <RecvBuffInit>2048</RecvBuffInit>
          <DetailLog>False</DetailLog>
        </Item>
      </EndPointList>
    </MQTT>

サーバー設定ファイルに MQTT ブローカへの接続を追加した場合には、必ず abs_agent を再起動してください。abs_agent でMQTT ブローカへ接続する場合の詳しい説明はこの記事中に記載していますので是非ご覧ください。

もし、MQTT ブローカへのセンサデータ送信を使用しない場合には、”RASPI/ENVSENSOR_DATA_STORE” スクリプト中で mqtt_publish() ライブラリ関数をコールしている部分をコメントアウトしてください。

●MQTT ブローカで配信されたセンサデータをインメモリデータベースに保存   全体図中の(4)

MQTT ブローカで配信しているセンサデータをインメモリデータベースに登録する設定を行います。abs_agent の設定ファイル中に記述した MQTT ブローカへの接続設定(エンドポイント設定)では下記のトピックを自動で購読するように指定されています。

自動購読トピック: “/EnvSensor/+” , QoS = 0

このトピックは前述の “RASPI/ENVSENSOR_DATA_STORE” スクリプト中で mqtt_publish() ライブラリ関数を使用してデータを送信するときに指定しているトピックと同じにしています。このため、Raspberry Pi からデータを MQTT ブローカに送信すると同時に、同じデータが MQTT ブローカから配信されています。

ここには任意のトピックを複数指定できますので、たとえば追加で “/+/+/io” と “/+/+/tdcp” の2つのトピック名に一致するデータを購読する場合には、設定ファイル中の自動購読を指定するタグに下記の様に記述します。

<AutoSubscribeTopicList>/+/+/io,/+/+/tdcp,/EnvSensor/+</AutoSubscribeTopicList>
<AutoSubscribeQoSList>0,0,0</AutoSubscribeQoSList>

abs_agent が MQTT ブローカで購読中のトピックに一致するデータを受信すると MQTT_PUBLISH イベントハンドラが自動的に実行されます。このイベントハンドラ中にセンサデータをインメモリデータベースに格納する記述を行います。イベントハンドラスクリプトの内容は下記の様になります。

file_id = "MQTT_PUBLISH"

--[[

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

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

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

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

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

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

---------------------------------------------------------------------------------
キー値          値                                                      値の例
---------------------------------------------------------------------------------
ClientID        エンドポイントの ClientID 文字列                        "abs9k:2222-eagle"

Title           エンドポイントに設定されたタイトル文字列。
                タイトル文字列が設定されていない場合には、"" 空文字列
                が入ります                                              "センサーデバイス#1"

MessageType     MQTT プロトコルで定義されたメッセージタイプが入ります。 "3"
                PUBSLIH メッセージの場合には常に "3"が設定されます

MessageID       Brokerから送信するときに使用された MQTT メッセージID が
                入ります。(QoS = 1 または QoS = 2 の場合) 値は "1" から
                "65535" の整数値をとります。
                QoS = 0 の場合には常に "0" が設定されます。             "1234"

Dup             MQTT 固定ヘッダ中の Dup フラグの値が設定されます。
                "0" または "1" の値をとります。                         "0"

QoS             MQTT 固定ヘッダ中の QoS フラグの値が設定されます。
                "0", "1", "2" の何れかの値をとります。                  "0"

Retain          MQTT 固定ヘッダ中の Retain フラグの値が設定されます。
                "0" または "1" の値をとります。                         "0"

PublishTopic    MQTT ブローカから受信した PUBLISH メッセージ中の Topic
                文字列。                                                "センサー/ノード1"

PublishData     MQTT ブローカから受信した PUBLISH メッセージ中のペイロー
                ドデータ。
                バイナリデータを16進数文字列に変換したものが格納されます
                ペイロードデータに格納されたデータが UTF-8 文字列の場合
                には文字列コードのバイト列が格納されています。
                イベントハンドラ中でこれらの文字列データをデコードする処
                理がデフォルトで記述されていますので、UTF-8 文字列を扱う
                場合にはデコード後の変数を利用することができます。      "010203414243"

PublishDataで渡されたペイロードデータを解析して作成される文字列変数

PublishString   PublishData に格納されたペイロードデータ部分のサイズが
                2048 Bytes以内の場合に、データバイト列を UTF-8形式で
                文字列にデコードした結果を PublishString に格納します。
                変換対象のバイト列のサイズを変更したいときには該当する
                スクリプト部分を変更して下さい。

]]

------------------------------------------------------------------------------------------
-- 受信したペイロードデータのサイズが 2048 bytes 以内の場合には
-- バイナリデータ列を UTF-8 文字列としてデコードしたものを PublishString 変数に格納する
------------------------------------------------------------------------------------------
local PublishString = ""
local pub_len = string.len(g_params["PublishData"]) / 2
if pub_len < 2048 then
    PublishString = readUTF_hex(bit_tohex(pub_len,4) .. g_params["PublishData"])
end

log_msg(g_params["Title"] .. "[" .. g_params["ClientID"] .. "] msg:" .. g_params["MessageID"] .. " dup:" .. g_params["Dup"]  ..
" retain:" .. g_params["Retain"]  .. " qos:" .. g_params["QoS"]  .. " topic:" .. g_params["PublishTopic"]  .. " " .. PublishString,file_id)

-----------------------------------------------------------------------------------------------
-- センサデータが送信されてきた場合には、FASTDB 集計用データベースにキー値を指定して登録する
-----------------------------------------------------------------------------------------------

-- MQTT PUBLISH パケット例 topic:/EnvSensor/raspi3 {"temperature":21.7,"pressure":1014.8,"humidity":40.2,"light":8}
local sender = string.match(g_params["PublishTopic"],"/EnvSensor/(.+)")
if (sender == "raspi3") then
	local msg = g_json.decode(PublishString)
	if not fastdb_add("温度(2階)MQTT",msg.temperature) then error() end
	if not fastdb_add("気圧(2階)MQTT",msg.pressure) then error() end
	if not fastdb_add("湿度(2階)MQTT",msg.humidity) then error() end
	if not fastdb_add("明るさ(2階)MQTT",msg.light) then error() end
end

-- MQTT PUBLISH パケット例 topic:/zb/Node1/tdcp {"temperature":14.2,"pressure":1015.7,"humidity":76.6,"ir_count":0,"light":691,"timestamp":"2017/04/...
local rf_type,sender = string.match(g_params["PublishTopic"],"/(.+)/(.+)/tdcp")
if (rf_type == "zb") and (sender == "Node1") then
	local msg = g_json.decode(PublishString)
	if not fastdb_add("温度(2階リモート)MQTT",msg.temperature) then error() end
	if not fastdb_add("気圧(2階リモート)MQTT",msg.pressure) then error() end
	if not fastdb_add("湿度(2階リモート)MQTT",msg.humidity) then error() end
	if not fastdb_add("赤外(2階リモート)MQTT",msg.ir_count) then error() end
end

-- MQTT PUBLISH パケット例 topic:/xbee/Device4/tdcp {"temperature":13.5,"ir_count":40,"timestamp":"2017/04/08 09:05:55"}
if (rf_type == "xbee") and (sender == "Device4") then
	local msg = g_json.decode(PublishString)
	if not fastdb_add("温度(1階)MQTT",msg.temperature) then error() end
	if not fastdb_add("赤外(1階)MQTT",msg.ir_count) then error() end
end

最初に、MQTT Publish パケットで受信したトピック名を Lua のパターンマッチ関数 string.match() で解析して、大まかな処理を分けています。

その後 abs_agent のライブラリ関数 g_json.decode() を使用して JSON 文字列を Lua言語のテーブル構造に変換します。テーブルのフィールド値は “.” で区切ることで簡単にアクセスできます。このとき、各フィールドの型は、元の JSON で記述されていた型に一致するように変換されています。もし文字列型でデータ値を受信した場合には、tonumber() ライブラリ関数を使用して数値型(浮動小数点型)に変換できます。

JSON データ変換後は、fastdb_add() ライブラリ関数を使用してそれぞれのセンサデータ値をインメモリデータベースに格納します。この時、キー名に任意の文字列を指定できます。

●インメモリデータベースのデータをコンソールから確認

ここまでのセットアップで、Raspberry Pi の I2C バスに接続した環境センサと温度センサのセンサデータが1分に一回取得されてインメモリデータベースに格納されいています。また、MQTT ブローカから購読したトピックのセンサデータを処理している場合には、これらのデータもインメモリデータベースに格納されています。

ここで、データベースに格納されているセンサデータをコンソールから確認してみます。abs_agent インストールキットには agent_fastdb コマンド(クライアントプログラム) を使用してインメモリデータベースの管理を行うことができます。下記はこのプログラムを実行した様子です。

agent_fastdb をパラメータなしで実行すると、現在データベースに登録されている全キー名一覧と、そのキー名を使用して登録されているデータレコード数が表示されます。

2つ目の実行例は、agent_fastdb コマンドの集計パラメータを指定して 2017年5月25 日の1日分の温度データを 10 分刻みで集計しています。

追加で “-f <file_name>” オプションを指定すると、集計結果をCSV形式のファイルに出力することができます。このCSVファイルを表計算ソフト等で読み込むと簡単にグラフ作成等が行えます。この他にも、agent_fastdb コマンドではデータを追加・削除したり、登録データをファイルにストアしたりリストアすることが出来ます。詳しくは abs_agent ユーザーマニュアルをご覧ください。

●グラフ作成用の Web アプリを作成 全体図中の(5)

ここからは、PC やタブレット、スマートフォン等の Web ブラウザから Raspberry Pi にアクセスして、センサーデータをグラフ表示するアプリケーションを作成します。abs_agent には HTTP サーバーと Web API 機能が内蔵されていますので、簡単に Web アプリを作成して公開することができます。

最初に Web アプリのページとダイアログを定義した HTML ファイル(webroot/app/chart/index.html) を設置します。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>FASTDB グラフ</title>
		<link rel="stylesheet" href="libs/css/themes/default/jquery.mobile-1.4.5.min.css" />
        <link rel="stylesheet" href="libs/css/jquery.jqplot.css" />
		<script src="libs/js/jquery.js"></script>
		<script src="libs/js/jquery.mobile-1.4.5.js"></script>
		<script src="libs/js/jquery.jqplot.js"></script>
		<script src="libs/js/jqplot.cursor.min.js"></script>
		<script src="libs/js/jqplot.dateAxisRenderer.js"></script>
		<script src="libs/js/jqplot.barRenderer.js"></script>
		<script src="libs/js/jqplot.highlighter.js"></script>
		<!-- abs_agent Web API アクセス用 -->
		<script src="libs/abs_agent/webapi.js"></script>
	</head>
	<body>

		<!--                                -->
		<!-- アプリケーションの各ページ定義 -->
		<!--                                -->

		<div data-role="page" id="login">
			<div data-role="header" data-position="inline">
				<h3>FASTDB グラフ ユーザー認証</h3>
			</div>
			<div role="main" class="ui-content">
				<label for="login_name">Name</label>
				<input id="login_name" value="" type="text" data-clear-btn="true"/>
				<label for="login_password">Password</label>
				<input id="login_password" value="" type="password" data-clear-btn="true"/>
				<div><h3>&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) 。

それではまた。