LUA言語拡張ハンドラ

LUA言語は、C言語ベースのバイナリー実行モジュールに、高級言語並みのスクリプト実行環境をアドオンできるシステムです。
OpenBlocksのPD Handlerではシステムソフトウェアの決まった処理系の全てをC言語で用意しておき、あとちょっとしたプログラムをLUAスクリプトで追加可能にしてあります。
例えばEnOceanデバイスの様にプロファイルがあれば、そのプロファイルによってバイナリーデータ列が規則化されているので、EnOceanコミュニティからプロファイルの仕様を手に入れれば、容易にデータを抽出しjsonテキスト化ができます。
本章ではBLE(ビーコン型センサー)とEnOcean(EEP仕様)およびRS-232CやRS-485などのシリアル通信の機能拡張ハンドラの作成について解説しています。

BLE Lua

BLEハンドラは過去のOpenBlocksシステムの流れから、コネクションタイプのnode.js型のものから、現時点ではビーコン型を対象としたC言語+LUA言語版へと移行しています。
ビーコン型を選んでいる理由としているのは、IoTシステムとして何年もの間に安定してデータを受信し続けられる点が第一の理由です。
BT/BLEなどでのコネクションは非常に安定性が悪く、また、センサーメーカー毎のプロトコルの作りに互換性が無く、コネクション切断毎の処理系がマチマチでコネクション復旧方法の処理系が曖昧でした。
どちらかと言うとスマートフォンでデータを見たい時、一時的なコネクションで一気にデータを取り込むような使い方に、多機能なコネクション型は向いています。
このためぷらっとホームとして、BT/BLEのコネクションモードでのサポートをIoT機器向けには続けることが非常に難しい所があり、現時点ではコネクションモードの新規開発を行っていません。

BLEのビーコンモードと呼んでいるのは、BLEデバイスが一定期間毎に発生させるアドバタイジングで、ビーコンセンサーはこのアドバタイジングのデータにセンサーデータを乗せています。
通常このアドバタイジングによって、社員証にBLEデバイスを仕込んで出退勤を管理したり、社員の位置情報システムに利用しています。
このビーコンとして発せられるアドバタイジングデーターからセンサーデータを拾うだけの仕組みのため、通信プロトコル無しで非常に単純な仕組みであり、何年も安定したIoTデータ受信に非常に向いています。

BLEデバイスのLua対応

BLEから受信したアドバタイズのデータ部分であるペイロードには、デバイスメーカーが自由に使っていい領域があり、BLEハンドラではこのペイロード部分をLua言語に渡しています。
それぞれのBLEデバイスに対応したLua関数は、WEB-UIのIoTデータBLE Luaタブの操作画面の以下のディレクトリに保存されています。

Lua関数の保存ディレクトリは以下の様に分けられています。

ディレクトリ名説明
devices標準でサポートしているBLEデバイス毎のLuaハンドラの保存ディレクトリ
devices_customユーザーが独自に追加するLuaハンドラの保存ディレクトリ
func標準のLua関数の保存ディレクトリ
func_customユーザーが独自に追加するLua関数の保存ディレクトリ

この4つのディレクトリに保存されたLuaファイルと1つのLuaファイル(pd-handler-ble-init.lua)がPD Handler起動時に全てが読み込まれます。
起動後、PD HandlerがBLEビーコンを受信するとBLE登録で登録されたデバイスのみ選別されLuaに送り込まれます。
Lua言語内ではBLEデバイス毎のLua関数(デバイス毎のソースコード)を、BLEの種類がマッチングするまで全ての関数を順次呼び出します。
BLEデバイスの種類がマッチングで特定されると、必要なデータをバイナリデータ列から取り出して、jsonに変換される連想配列の規定単位(温度とか)に変換されたのち保存されます。

以下にナカヨ製ボタン付きBLEビーコンのハンドラソースコードをサンプルに説明します。
※このビーコンはiBeaconに互換です。
※仕様変更もありえる詳細はナカヨ様の最新仕様書で確認ください。

nakayo = {} -- 配列の初期化
-- iBeacon規格互換でナカヨビーコンのIDとなるマッチング変数の宣言
nakayo_uuid = string.char(0xa9, 0x03, 0x01, 0x00, 0x14, 0x78, 0x48, 0x24, 0xb2, 0x98, 0x8e, 0x68, 0x23, 0xcf, 0xde, 0xfa)
nakayo_minor_normal = string.char(0xff, 0xe0)
nakayo_minor_1 = string.char(0xff, 0xe8)
nakayo_minor_2 = string.char(0xff, 0xf0)
nakayo_minor_3 = string.char(0xff, 0xf8)
-- ハンドラのメインから順次呼び出される関数
-- 引数 jo = jsonテキストに変換される連想配列、manufacturerData = バイナリデータ列
-- localname = BLE規格で設定される名前(付けられるかはメーカーによってまちまち)
function nakayo.nakayo_beacon(jo, manufacturerData, localname)
if is_ibeacon(manufacturerData) then -- 先ずiBeaconであるかチェック
local uuid = string.sub(manufacturerData, 5, 20) -- バイナリー列からUUID部取り込み
local major = string.sub(manufacturerData, 21, 22)-- バイナリー列からmajor部取り込み
local minor = string.sub(manufacturerData, 23, 24)-- バイナリー列からminor部取り込み
if string.match(uuid, nakayo_uuid) ~= nil then -- UUIDがマッチングするかチェック
pdHandlerUtil.log_debug("nakayo") -- デバッグログへUUIDなどを出力
pdHandlerUtil.log_debug("uuid: "..pdHandlerUtil.bin2str(uuid, #uuid))
pdHandlerUtil.log_debug("major: "..pdHandlerUtil.bin2str(major, #major))
pdHandlerUtil.log_debug("minor: "..pdHandlerUtil.bin2str(minor, #minor))
jo.uuid = pdHandlerUtil.bin2str(uuid, #uuid) -- jsonへ変換されるUUIDの保存
jo.major = pdHandlerUtil.bin2str(major, #major) -- jsonへ変換されるmajorの保存
jo.minor = pdHandlerUtil.bin2str(minor, #minor) -- jsonへ変換されるminorの保存
if string.match(minor, nakayo_minor_normal) ~= nil then
jo.push = 0 -- ボタンが押されていないステータスをjson変換される配列に保存
elseif string.match(minor, nakayo_minor_1) ~= nil then
jo.push = 1 -- ボタンが押されたステータスをjson変換される配列に保存
elseif string.match(minor, nakayo_minor_2) ~= nil then
jo.push = 2 -- ボタンが押されたステータスをjson変換される配列に保存
elseif string.match(minor, nakayo_minor_3) ~= nil then
jo.push = 3 -- ボタンが押されたステータスをjson変換される配列に保存
else
jo.push = -1 -- ボタンのステータスがおかしい時
end
end
end
end
return nakayo

ナカヨ製ボタン付きBLEビーコンは、ボタンを長押しするとjo.pushの値が1 2 3 1 2とこんな感じに変化します。
BLEビーコンセンサにはiBeacon規格以外のもあるので、デバイスマッチングについては他のソースコードも参考にしてください。
以上のコードでIoTクラウドへ送信されるjsonテキストは以下の通りになります。

{
"time": "2017-12-08T12:34:56.789+09:00", ← ここから3つがOpenBlocksで設定されるデータ
"deviceId": "xxxxxxxxxxxx",
"rssi": -68,
"uuid": "a903010014784824b2988e6823cfdefa", ← ここから下がハンドラで追加されたデータ
"major": "00c8",
"minor": "ffe0",
"push": 0
}

その他のセンサーのjsonテキストはこちらを参照ください。

編集が終わったら、<デバイス名>.luaと名前を付けてdevices_customディレクトリにUPLOADしてください。

UPLOAD/DOWNLOAD操作

作成したLuaハンドラのアップロードや、参考にするLuaハンドラのダウンロード操作は以下の通りです。

BLE Lua メニューの操作項目
設定項目説明
更新Luaファイルのアップロード先のファイル一覧表示を更新します。
削除ファイルを選択後削除ボタンで削除します。
削除可能なファイルはカスタム用ディレクトリ配下のファイルです。
ダウンロードファイルを選択後ダウンロードボタンでクライアントパソコンのダウンロードフォルダにファイルをダウンロードします。
実行権付与ファイルを選択後実行権付与ボタンで実行権を付与します。通常では不要です。
アップロード先アップロードしたいディレクトリをプルダウンから選択してください。
※対象ディレクトリ配下がプルダウン対象です。
アップロードファイルを選択ボタンを押すと、クライアントパソコンのファイル一覧から指定のファイルを選択してアップロードできます。*1
info
  1. アップロード後にプロセスの再起動が必要なので、ダッシュボードから停止・起動の操作を行ってください。
caution

アップロード先にLuaファイル以外が存在している場合には正常に動作しません。
Luaファイル以外を置かないでください。

EnOcean Lua

EnOceanはEEP(EnOcean Equipment Profile)というプロファイルでデバイスが定義されています。
EEPを使わないモードもありますが、そのモードはサポート外です。(現時点では)

EnOcean登録とLua関数の関係

EnOceanデバイスはBLE機器に比べプロファイルと言う形式をとっているため、データ形式が規格化されているため扱い易いです。
デバイス毎のハンドラは以下の通り、devicesディレクトリにプロファイル名でLuaハンドラが登録されています。
WEB-UIのIoTデータEnOcean Luaタブの操作画面の以下のディレクトリです。

EnOceanを受信登録するする時はEnOcean登録デバイスIDEEPの登録が必須です。

ここで登録されたEEPあわせて、devicesディレクトリに登録されたLuaハンドラーの中から該当するハンドラーが選択され実行します。
※EnOceanハンドラはBLEハンドラとは違ってLuaを呼ばれた時点でデバイスのプロファイルが決定しています。

ユーザースクリプトは以下のdevices_customディレクトリに保存するので、ここに置いてある"skelton.lua"をダウンロードして、テンプレートにすると良いです。

skelton = {}
function skelton.skelton_decode(jo, data)
pdHandlerUtil.log_debug("skelton")
local str
str = pdHandlerUtil.bin2str(data, #data)
pdHandlerUtil.log_info("str: ", str)
if str ~= nil then
jo.data = str
end
end
return skelton

登録したいEEPは次のようなデバイスのものです。

EnOceanアライアンスのEEP仕様書から A50205

最初に"skelton"文字列をデバイスのEEPに置換します。
かつ、クラウド送信用のjo配列のメンバーにtemperatureを追加しtemperaturen値を計算し代入します。
※元のskeltonは受け取ったデータをそのまま16進テキスト文字列でjsonで送るものです。

A50205 = {}
function A50205.A50205_decode(jo, data)
local temperature = string.byte(data, 3)
pdHandlerUtil.log_debug("A50205")
temperature = (255 - temperature) * 40 / 255 -- 仕様書の計算式のまま
pdHandlerUtil.log_info(" temperature: ", temperature)
jo.temperature = temperature
end
return A50205

以上のコードでIoTクラウド送信用のjsonテキストは以下の様になります。

{
"deviceId": "xxxxxx",
"time": "2016-03-14T16:17:02.269+09:00",
"temperature": 25.41176470, ← ここだけハンドラで追加されたデータ
"EEP": "A50701",
"memo": "temperature Sensor",
"rssi": -71
}

その他のセンサーのjsonテキストはこちらを参照ください。

編集が終わったら、<プロファイル名>.luaと名前を付けてdevices_customディレクトリにUPLOADしてください。

UPLOAD/DOWNLOAD操作

作成したLuaハンドラのアップロードや、参考にするLuaハンドラのダウンロード操作は以下の通りです。

EnOcean Lua メニューの操作項目
設定項目説明
更新Luaファイルのアップロード先のファイル一覧表示を更新します。
削除ファイルを選択後削除ボタンで削除します。
削除可能なファイルはカスタム用ディレクトリ配下のファイルです。
ダウンロードファイルを選択後ダウンロードボタンでクライアントパソコンのダウンロードフォルダにファイルをダウンロードします。
実行権付与ファイルを選択後実行権付与ボタンで実行権を付与します。通常では不要です。
アップロード先アップロードしたいディレクトリをプルダウンから選択してください。
※対象ディレクトリ配下がプルダウン対象です。
アップロードファイルを選択ボタンを押すと、クライアントパソコンのファイル一覧から指定のファイルを選択してアップロードできます。*1
info
  1. アップロード後にプロセスの再起動が必要なので、ダッシュボードから停止・起動の操作を行ってください。
caution

アップロード先にLuaファイル以外が存在している場合には正常に動作しません。
そのため、ファイル内容については skelton.lua を参考に作成してください。

SERIAL Lua

PD Handler RS-SERIALでは、対向のシリアルデバイス(RS-232C/RS-485など)向けに処理内容をフルカスタムできるユーザー定義ハンドラです。
WEB-UIのIoTデータSERIAL Luaタブから、カスタマイズしたLuaファイルをアップロードし使用します。

info
  • デフォルトでインストールされているrs-serial.luaはサンプルとなります。このファイルを参考に編集してください。
  • PD Handler RS-SERIALはrs-serial.luaファイルを読み込み、LUA_main関数を実行します。
  • PD Handler RS-SERIALはPD Repeaterへのアクセスするパスのデバイス名部をrs-serial.lua内にて定義しています。そのため、環境に合わせてluaファイルを編集する必要があります。
caution

rs-serial.luaファイルが不正(フォーマットエラーやシンタックスエラー等)の場合エラー終了となるので、ログを確認してください。

SERIAL Lua のカスタマイズ

基本的には一般の高級言語でシリアルインターフェース機器との通信全般を書くのと違いがありません。
※実際にはシリアル通信にこだわらずLuaだけで汎用的なアプリケーションもここで作れてしまいます。

システムの単純な流れとしては

シリアルポートのオープン
速度などの通信パラメータ設定
データ送受信
シリアルポートのクローズ

この作り方になりますが、OpenBlocks上ではデーモンとして起動するので、シリアルポートのクローズはSTOPシグナルが入った時となるので、データ送受信部分で無限ループする作りになります。

このディレクトリにあるrs-serial.luaをテンプレートとしてダウンロードしてから編集して使ってください。
※利用開始前にはアプリ設定でPD Handler RS-Serialの起動を忘れずに!

以下はrs-serial.luaのデフォルトソースコードです。
OpenBlocksとパソコンをRS-232Cクロスケーブルで接続しテストを行ってください。
動作はパソコン側でTeraTermなどを使ってOpenBlocksと接続しキーボード入力するとその文字をエコーバックします。
そして"who?"と入力すると"I am openblocks"と返事し、"exit!"と入力すると"!!!!!! STOPING prcess !!!!!"と返事してからプロセスを停止します。
※本番用デーモンとして起動する時は無限ループを抜ける仕組みがない方が良いです。

function LOG_err(message)
LUA_sys_log (3, message)
end
function LOG_info(message)
LUA_sys_log (6, message)
end
function LOG_debug(message)
LUA_sys_log (7, message)
end
function LUA_main(void)
LOG_info("start rs-serial\n")
-- シリアルポートのオープン(RS485の時は"/dev/ttyMFD1"の部分を"/dev/ttyRS485"に変更)
local fd
fd = LUA_open_serial("/dev/ttyMFD1", 9600, 8, 1, false, false)
if (0 > fd) then
LOG_err("serial port open error\n")
return
end
-- メインループ
local read_buff = ""
local read_size = 0
while (1) do -- フォアグランドモードの場合、無限ループはCTRL-Cでも止まります。
local rbuff, rsize -- ローカル変数宣言
rbuff, rsize = LUA_read_serial(fd)
read_buff = read_buff .. rbuff -- 前にシリアル入力された文字列に今回分を連結
read_size = read_size + rsize -- 同上
-- データを受信した
if rsize > 0 then
LUA_send_serial(fd, rbuff, rsize) -- 入力文字のエコーバック
if nil ~= string.find(read_buff, "exit!") then -- 終了文字列の検索(exit!)
local res_str = "\n\r!!!!!! STOPING prcess !!!!!\n\r" -- レスポンス文字列
local res_len = string.len(res_str) -- レスポンス文字列の長さ
LUA_send_serial(fd, res_str, res_len) -- シリアルポートにレスポンスを送信
break; -- exit!で無限ループを抜ける
-- ※デーモン動作の時は無限ループから抜けないようにする
end
if nil ~= string.find(read_buff, "who%?") then -- 文字列検索(who?)
local res_str = "\n\rI am openblocks\n\r" -- レスポンス文字列
local res_len = string.len(res_str) -- レスポンス文字列の長さ
LUA_send_serial(fd, res_str, res_len) -- シリアルポートにレスポンスを送信
read_buff = ""
end
end
read_buff = string.sub(read_buff, -10) -- 最後に入力された10文字だけ残して捨てる
read_size = string.len(read_buff)
LUA_usleep (50000) -- 50msec程度スリープを入れとくと他の処理が軽くなります。
end
-- シリアルポートのクローズ
LUA_close_serial(fd)
LOG_info("stop rs-serial\n")
end

これは非常に簡単なプログラムで、もう少し色々試したい時はsample.luaを一度ダウンロードでしてrs-serial.luaのファイル名でアップロードしてください。
IoTクラウド(PD Repeater)への送信サンプルなども用意されていますので参考にしてください。

OpenBlocksのLua用関数

以下はOpenBlocksのLua言語用に実装している関数です。
sample.luaではこれら関数を一通り使っているので参考にしてください。

LUA_open_serial (devfile, speed, databit, stopbit, enable_parity, odd_parity)
シリアルポートを指定の通信パラメータでオープンする。
devfile (string)
対象のシリアルポートのデバイスファイル名を設定(例:"/dev/ttyMFD1")
speed (int)
シリアルポートの通信速度を指定(110~921600の範囲で14400はサポート外)
databit (int)
データのビット長(5~8 範囲外の値は8とする)
stopbit (int)
ストップビット長(1 or 2、2以外は全部1とする)
enable_parity (bool)
パリティを有効にする(true or false )
odd_parity (bool)
enable_parityがtrueの時にこの設定が有効になる。
trueの時、奇数パリティになる。
falseの時、偶数パリティになる。
戻り値
ファイルデスクリプタ(int)、オープン失敗時 マイナス値
LUA_close_serial (fd)
シリアルポートをクローズする。
fd (int)
シリアルポートオープン時のファイルデスクリプタ
戻り値 無し
LUA_send_serial (fd, buffer, size)
シリアルポートからバイナリーデータを送信する。
fd
ファイルデスクリプタ(int)
buffer
送信するバイナリーデータ(data ※ポインタではなく実体)
※送信データにNULL(0)などのバイナリーも含む事ができます。
size
バイナリデータの長さ
戻り値
送信できたバイト数
LUA_read_serial (fd, buffer, size)
シリアルポートからバイナリーデータを受信する。
fd
ファイルデスクリプタ(int)
戻り値1
バイナリーデータ列の実体(NULLを含むがLUAではstring型)
※受信データにNULL(0)などのバイナリーも含む事ができます。
戻り値2
バイナリデータの長さ
データが無い時 0 ※C言語内では -1(使いにくいので0に置き換え)
LUA_post (device_number, message)
ユニックスドメインソケット経由で PD Repeater にメッセージを送信する
PD Repeater はそのメッセージをWEB-UIの設定に従いクラウドへ送信する。
device_number
openblocksので確保したソケットの名前(string)
ソケットはWEB-UIにある 基本>Userデバイス登録 タブで登録された
デバイス番号を使う。
message
一般的にはjsonの文字列を指定するが、文字列であれば何でもかまわない。
LUA_dump_byte (buffer, size)
バイナリーのバイト列をダンプする。(デバッグメッセージとして出力)
※デバッグのために用意された関数です。
buffer
ダンプするバイナリーデータ(data ※ポインタではなく実体)
size
ダンプするバイナリデータの長さ
戻り値
無し
LUA_usleep (microsec)
プロセスを指定の時間停止させる。
microsec(int)
マイクロ秒単位で停止させる時間を設定。(互換性のためなるべく1000000未満(1秒)を設定)
戻り値
無し
LUA_sleep (sec)
プロセスを指定の時間停止させる。
microsec(int)
秒単位で停止させる時間を設定。
戻り値
無し
LUA_sys_log (level, message)
シスログにメッセージを送る。フォアグランドでは標準出力も一緒に行われる。
level(int)
ログのレベル。(0~7)
message(string)
出力されるメッセージ。
※printf的に使う場合 string.format関数 を使います。(サンプル参照)
LUA_get_iso_datetime (order_msec)
IoTクラウドに基本的に用いられるISO8601形式の日付時間を得ます。
order_msec(bool)
時間をミリ秒単位まで欲しい時trueにします。それ以外はfalse
戻り値
ISO8601形式の日付時間文字列

UPLOAD/DOWNLOAD操作

SERIAL Luaの操作画面はWEB-UIのIoTデータSERIAL Luaタブの操作画面からアップロードやダウンロードを行います。

設定項目説明
更新Luaファイルのアップロード先のファイル一覧表示を更新します。
削除ファイルを選択後削除ボタンで削除します。
ダウンロードファイルを選択後ダウンロードボタンでクライアントパソコンのダウンロードフォルダにファイルをダウンロードします。
実行権付与ファイルを選択後実行権付与ボタンで実行権を付与します。通常では不要です。
アップロードファイルを選択ボタンを押すと、クライアントパソコンのファイル一覧から指定のファイルを選択してアップロードできます。*1
info
  1. アップロード後に、プロセスの再起動が必要となります。