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) 。

それではまた。

 

コメントは受け付けていません。