Arduino I/O を Web API から操作する

Arduino を Web から操作する方法について紹介します。この方法を応用することでスマートフォンや PDA はもちろん、クラウドのサーバーやWeb アプリケーションサーバーなどインターネットやイントラネットから自由に Arduino を操作できるようになります。例えば、アプリケーションサーバーでエラーが発生したときに、手元の Arduino に接続したブザーを鳴らしたり、スマートフォンからArduino に接続したリレーを操作してエアコンや警報器を操作できます。

今回は、Arduino の digital I/O ポートのみを使用しています。Arduino では標準ライブラリで提供されている Firmata プロトコルを使用したプログラムを動作させます。Arduino IDE のサンプルで提供されている幾つかのFirmata スケッチプログラムでは機能的に過不足がありますので、今回はdigital I/O リモート操作用のスケッチを新規に作成しました。このプログラムは PC からシリアルポート経由で受信した Firmata コマンドを実行して、I/O ポートの ON/OFF を切り替えたり、現在のI/Oポートの状態を PC 側に知らせることが出来ます。

Arduino で実行するスケッチは下記になります。後で説明する DeviceServer 側のスクリプトを含めて ここから ダウンロードすることができます。

 

/*
 DigitalIOControl.ino

 Firmata プロトコルで PC 側から Arduino digital I/O を操作するための機能です。
 初期化時に、Arduino pin#2 から pin#13 を digital OUTPUT モードに設定しています。

 サポートしている Firmata プロトコル
 * DIGITAL_MESSAGE
   標準プロトコルで定義されているプロトコル。
   PC から Arduinoボード側へのI/O ポート値設定のみに使用しています。

   -- digital I/O message --
   0  command (0x90) + port#
   1  port value bits 0-6 (LSB)
   2  port value bits 7-13(MSB)

 * デジタルポートの現在の値を取得するための SYSEX コマンド DIGITAL_PORTS_QUERY と
   リプライDIGITAL_PORTS_REPLY

   -- query digital ports --
   0  START_SYSEX         (0xF0)
   1  query digital ports (0x61)
   2  END_SYSEX           (0xF7)

   -- reply digital ports --
   0  START_SYSEX         (0xF0)
   1  reply digital ports (0x62)
   3  port#0 bits 0-6     (LSB)
   4  port#0 bits 7-13    (MSB)
   ..
   5  port#n bits 0-6     (LSB)
   6  port#n bits 7-13    (MSB)
   7  END_SYSEX           (0xF7)

 * ピン番号と値を指定してポートを操作する SYSEX コマンド DIGITAL_WRITE_PIN
   -- write digital port pin --
   0  START_SYSEX         (0xF0)
   1  write digital pin   (0x63)
   2  pin number          (0x02-0x0D)
   1  pin value           (0/1)
   2  END_SYSEX           (0xF7)

 このファイルは自由に使用することができます。内容を変更するなどをして利用することもできます。
 オールブルーシステムは、このファイルを使用したことによる損害等について一切保障できません。
                   2012 All Blue System     Satoshi Kimura
*/

#include <Firmata.h>

//  デジタルポート数の定義
//  ATmega328 を使用した Arduino ボードの場合は 2 になる
#define TOTAL_DIGITAL_PORTS             ((TOTAL_PINS - TOTAL_ANALOG_PINS + 7) / 8) 

// SYSEX コマンドの定義
#define DIGITAL_PORTS_QUERY      0x61
#define DIGITAL_PORTS_REPLY      0x62
#define DIGITAL_WRITE_PIN        0x63

// SYSEX コマンドの処理
void sysexCallback(byte command, byte argc, byte *argv)
{
    byte pval;
    switch(command) {
        case DIGITAL_PORTS_QUERY:
            Serial.write(START_SYSEX);
            Serial.write(DIGITAL_PORTS_REPLY);
            for (byte i=0; i<TOTAL_DIGITAL_PORTS; i++) {
                pval = readPort(i, 0xff);
                Serial.write(pval & B01111111);      // LSB
                Serial.write(pval >> 7 & B01111111); // MSB
            }
            Serial.write(END_SYSEX);
            break;
        case DIGITAL_WRITE_PIN:
            digitalWrite(argv[0],argv[1]);
            break;
    }
}

void digitalWriteCallback(byte port, int value)
{
    byte currentPinValue;
    if (port < TOTAL_DIGITAL_PORTS) {
        for(byte i=0; i<8; i++) {
            currentPinValue = (byte) value & (1 << i);
            digitalWrite(i + (port*8), currentPinValue);
        }
    }
}

void setup()
{
    Firmata.setFirmwareVersion(0, 1);
    Firmata.attach(DIGITAL_MESSAGE, digitalWriteCallback);
    Firmata.attach(START_SYSEX, sysexCallback);
    Firmata.begin(57600);
    // pin#2-#13 のデジタルポートを OUTPUT モードに初期設定する
    for(byte i = 2;i <= 13;i++){
        pinMode(PIN_TO_DIGITAL(i), OUTPUT);
    }
}

void loop()
{
    while(Firmata.available()) {
        Firmata.processInput();
    }
}

このスケッチでは Arduino で下記の Firmata コマンドの受付と実行を行います。

[DIGITAL_MESSAGE]

このコマンドはパラメータで指定されたポートにデータを出力します。指定したポート中の全ビットの値を一度に変更したい場合に使用します。

[DIGITAL_WRITE_PIN]

Arduino のピン番号と値を指定して、ポートに出力します。このコマンドは指定したピン番号の I/O ポートのみを更新して、指定したピン以外の I/O ポートに影響を与えません。

[DIGITAL_PORTS_QUERY]

Arduino ボードの現在の I/O ポートの値を全て取得します

それぞれのコマンドの詳細については、スケッチプログラムのコメント欄を参考にしてください。Firmata プロトコルは MIDI を元に作成されているため、データ部分は 7bit で収まる様に設計されています。そのため今回の様に 8 ビットデータを扱う場合には助長なフォーマット(LSB + MSB) になっている点に注意してください。

次に、Arduino に接続するシリアルポートの設定を DeviceServer 側で行います。ボーレートを スケッチプログラムで設定した 57600 に合わせて、タイプに FIRMATA を選択します。

Arduino が接続されている COM ポート番号は、実際に使用する環境に合わせてください。次に、Web サーバー機能を設定して HTTP プロトコルと Web API 機能を有効にします。

この例では、HTTPServer ポートをデフォルトの 80 (HTTP) から 8080 に変更しています。 “WebAPI を有効にする” にチェックを付けて HTTP プロトコル経由で DeviceServer 側のスクリプトを実行できるようにします。次に、DeviceServer 用に3つのスクリプトをスクリプトフォルダ(“C:\Program Files\AllBlueSystem\Scripts”)にコピーします。それぞれのスクリプトについて説明します。

* ARDUINO_IO_PUT.lua スクリプトファイル

file_id = "ARDUINO_IO_PUT"
log_msg("start..",file_id)

--[[
**************************************************************************************
Arduino デバイスの指定したI/O ポートの値を設定する

スクリプト起動時に渡されるパラメータ
---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
port			Arduino port 番号										"1"

value			I/O ポートに設定する値(16進数2桁)						"FC"

ATmega328 を使用した Arduino ボードの場合には、 digital I/O port は 0,1 の2 つで
それぞれ、port0:PORTD, port1:PORTB(bit#0-bit#5) + (PORTC bit#0-#1)に対応しています。
詳しくは、Arduino IDE に同梱されている pins_arduino.h ファイルを参照して下さい。
**************************************************************************************
]]

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["port"] and g_params["value"]) then
	log_msg("parameter error",file_id)
	error()
end

----------------------------------------------------------------------
-- Firmata digital I/O message 送信
----------------------------------------------------------------------
local com = "COM4"
local firmata_command = list_to_hex(bit_or(0x90,tonumber(g_params["port"])))
local send_data = firmata_command .. tbl_to_hex72({tonumber("0x" .. g_params["value"])},2)
local stat,rdata = serial_write(com,send_data)
if not stat then
	log_msg("serial_write() failed",file_id)
	error()
end

このスクリプトはパラメータで指定されたポート番号と値のデータを、Firmata プロトコル(DIGITAL_MESSAGE) で Arudino に送信して I/O ポートに出力します。スクリプトを配置すると直ぐに Web API でこのスクリプトを実行できますので、Web ブラウザから実行してみます。

URL パラメータにスクリプト名とスクリプトパラメータを指定しています。URL の ”/command/json/script” 部分が DeviceServer の Web API コマンドになります。この Web API コマンドは、URL パラメータで指定されたスクリプトを実行して実行結果(スクリプトリターンパラメータと実行結果ステータス)を JSON フォーマットで返します。name=ARDUINO_IO_PUT でサーバー側で実行するスクリプト名を指定しています。session=abc123 は DeviceServer 側で認証を行うためのセッショントークン文字列です。本来はログイン操作を行ってからその都度ユニークなセッションを使用するのですが、今回はサーバー起動時に作成した秘密のセッショントークンを使用してユーザー認証を省略しています。詳しくは ”2012/5/9 メールからFAX送信”の記事を参照して下さい。その他の URL パラメータ(port=0, value=FC)はそのまま、Lua のスクリプトパラメータに変換されてスクリプトに渡されます。Webブラウザでアクセス(HTTP GET)すると Arduino 側の I/O ポートが設定されて下記の様になります。

URLパラメータで指定したポートに I/O 値が出力されているのを確認することができます。

次に、ARDUINO_PIN_SET.lua スクリプトファイルを設定します。

file_id = "ARDUINO_PIN_SET"
log_msg("start..",file_id)

--[[
**************************************************************************************
Arduino デバイスの指定したI/O ピンの値を設定する

スクリプト起動時に渡されるパラメータ
---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
pin			Arduino pin番号(2 から 13 までの整数)						"13"

value		pin に指定したI/Oポートに設定する値(0 または 1)				"1"

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

-------------------------
-- パラメータチェック
-------------------------
if not (g_params["pin"] and g_params["value"]) then
	log_msg("parameter error",file_id)
	error()
end

----------------------------------------------------------------------
-- Arduino pin 番号と値を指定してポート設定
----------------------------------------------------------------------
local com = "COM4"
local sysex_start = "F0"
local sysex_end = "F7"
local firmata_request = "63"
local pin = bit_tohex(tonumber(g_params["pin"]),2)
local val = bit_tohex(tonumber(g_params["value"]),2)
local send_data = sysex_start .. firmata_request .. pin .. val .. sysex_end

----------------------------------------------------------------------
-- リクエストフレーム送信
----------------------------------------------------------------------
local stat = serial_write(com,send_data)
if not stat then
	log_msg("serial_write() failed",file_id)
	error()
end

このスクリプトはURLパラメータで指定されたピン番号と値を、Firmata プロトコル(DIGITAL_WRITE_PIN) で Arduino に送信して I/O を設定します。スクリプト設定後、同様にWeb ブラウザで実行してみます。

URLパラメータ pin=13&value=1を指定することで、Arduino に標準で装備されているパイロットLED(pin#13, PORTB#5) を点灯させています。

ボードに装備されている LED が点灯するのを確認することができます。

次に、ARDUINO_IO_GET.lua スクリプトファイルを設定します。

file_id = "ARDUINO_IO_GET"
log_msg("start..",file_id)

--[[
**************************************************************************************
現在の Arduino デバイスの I/O 値を取得する

リターン時に返されるパラメータ
---------------------------------------------------------------------------------
キー値			値		            									値の例
---------------------------------------------------------------------------------
PORT_0			Arduino port0(8bit)の現在の値(16進数2桁)				"FC"
PORT_1			Arduino port1(8bit)の現在の値(16進数2桁)				"00"
..
PORT_n			Arduino portn(8bit)の現在の値(16進数2桁)				"00"

ATmega328 を使用した Arduino ボードの場合には、 digital I/O port は 0,1 の2 つで
それぞれ、port0:PORTD, port1:PORTB(bit#0-bit#5) + (PORTC bit#0-#1)に対応しています。
詳しくは、Arduino IDE に同梱されている pins_arduino.h ファイルを参照して下さい。
**************************************************************************************
]]

----------------------------------------------------------------------
-- Arduino digital I/O ポート取得 QUERY 送信と REPLY 受信
----------------------------------------------------------------------
local com = "COM4"
local sysex_start = "F0"
local sysex_end = "F7"
local firmata_request = "61"
local firmata_reply = "62"
local send_data = sysex_start .. firmata_request .. sysex_end

----------------------------------------------------------------------
-- 排他制御開始
-- 同じ Arduino デバイスに対して、I/O PORT QUERY コマンドが複数同時実行される場合に備えています。
-- 別のArduino デバイスを操作する場合や、同じ Arduino デバイスに対して異なる FIRMATA REPLY
-- コマンドバイト値を取得する場合には排他制御の必要はありません
----------------------------------------------------------------------
local cstat,handle = critical_section_enter("FirmataQueyOP" .. com,5000);
if not cstat then
	log_msg("critical_section_enter() failed",file_id)
	error()
end

----------------------------------------------------------------------
-- リクエストフレーム送信とリプライ受信
----------------------------------------------------------------------
local stat,rdata = serial_write(com,send_data,firmata_reply)

----------------------------------------------------------------------
-- 排他制御終了
----------------------------------------------------------------------
cstat = critical_section_leave(handle)
if not cstat then
	log_msg("critical_section_leave() failed",file_id)
	error()
end
if not stat then
	log_msg("serial_write() failed",file_id)
	error()
end

----------------------------------------------------------------------
-- 受信した FIRMATA フレームの内容をリターンパラメータに格納
----------------------------------------------------------------------
local data = hex72_to_tbl(string.sub(rdata,5,-3),2)
if data ~= nil then
	for key,val in ipairs(data) do
	 	if not script_result(g_taskid,"PORT_" .. tostring(key - 1),bit_tohex(val,2)) then error() end
	end
end

このスクリプトは Arduino にFirmata プロトコル(DIGITAL_PORTS_QUERY) を送信して現在の I/O 値を取得した後、Firmata プロトコル(DIGITAL_PORTS_REPLY) を使用してPC側に送信されてくるデータを受け取ります。受信したFirmata フレームデータを解析して現在の I/O データを取り出してスクリプトリターンパラメータにセットします。DeviceServer ではスクリプトリターンパラメータにキー名と値のペアを好きなだけ設定することができます。設定したリターンパラメータは、Web API で返される JSON 文字列中に ResultParams 配列として格納されます。また、Firmata プロトコルで Query-Reply タイプのコマンドを同一デバイスに対して複数同時に実行する場合に備えて、ここでは排他制御を行っています。

スクリプトファイル設定後、同様にWeb ブラウザで動作させてみます。

ブラウザでは、Arduino から返信された現在の I/O ポート値が JSON フォーマットで表示されているのを確認することができます。

今回の記事では、Arduino を Web API (URL リクエスト) 経由でアクセスしてポートの設定や値の取得が出来るようになるまでの説明を行いました。今後、この機能を使用してスマートフォン等から Arduino の I/O を操作するアプリを作成していきたいと思います。また応用として、 Arduino の I/O ポート数が足りなくなった場合には、複数の Arduino に今回のスケッチをロードして同時に操作することもできます。この場合には サーバー側のスクリプトのみを変更することで対応できます。今回紹介した Arduino に市販のリレーシールドなどを接続して、ネットワーク対応(Web API 対応) の I/O 装置を非常に安価に作成することができます。

ここで紹介した DeviceServer の機能は、ABS-9000 DeviceServer インストールキットをダウンロードして、直ぐに使用することができます。(デモライセンスが添付されていますので直ちに使用可能です)

それではまた。

メールからFAX送信

XBee イベントデータを FAX で送信するアプリケーション例に続いて、今回はメールで送信したテキストを FAX で出力する利用方法を紹介したいと思います。今回は fax_submit() Lua ライブラリ関数を使用したユーザースクリプトを作成することなく、DeviceServer のデフォルト機能だけを使用して、メールで送信したテキストデータを自動でFAX送信します。メール送信元の端末 はノートパソコンや PDA など、なんでも構いませんが今回は iPod touch を使用した例を紹介します。

メモアプリなどで、FAX送信したい内容を記述したテキストを用意します。もちろんメーラーから直接FAX本文を記述しても構いません。

下の矢印ボタンを押して”メール”を選択します。

メールの送信先には DeviceServer のメール設定で指定した POP メールアカウントのメールアドレスを指定します。メールの件名を $FAX$ にして、DeviceServer にFAX 送信メールコマンドであることを指示します。メール本文1行目に、認証用のセッショントークン文字列を指定します(後述)。2行目には FAX 送信先電話番号を記入します。メール本文3行目以降に書いてある内容が FAXで送信されます。

上記のメール1行目に指定したセッショントークン文字列は、予めサーバー側で作成している秘密のセッションになります。以下の様なスクリプトを (SERVER_START.Lua) に記述することで秘密のセッションを作成できます。

file_id = "SERVER_START"

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

--------------------------------------------------------
-- 認証を省略してアクセスするためのセッションを作成する
--------------------------------------------------------
if not create_session("abc123",true) then error() end

この場合には、メールコマンド実行後でもセッショントークン “abc123″ は常に有効で、いつでも使用することができます。セキュリティを向上させるときには、この様な秘密のセッションは作成しないで、メールコマンド $LOGIN$ を使用してユーザー名とパスワードを指定してその都度セッショントークン文字列をリプライメールで取得するようにします。この場合には、認証用のセッショントークンはデフォルトでは1度だけしか使用できないようになります。

メールを送信するとDeviceServer がメールを自動的に取得して、メールコマンド $FAX$ が実行されます。その後 DeviceServer はメールを送信してきた端末に対して、下記の様なリプライメールを送信してきます。

サーバー側で認証に失敗した場合や、FAX 送信ジョブを Microsoft FAX サービスに登録するのに失敗した場合には、エラー内容を記述したメールが送信されてきます。実際に送信された FAX は下記の様になります。

外出中にノートPC やスマートフォンから印刷したい場合に、ホテルの受付や駅、出張先などに FAX機がある場合には、直ぐに印刷することができますので大変便利です。サーバーPC のFAXモデムからの電話代はかかりますが、FAX用の有料専用サービスなどに比べると非常に安価に運用できます。またセットアップや使い方も簡単ですのでぜひ活用してください。

それではまた。

 

 

XBee イベントデータを FAX で自動送信

DeviceServer にFAX で送信する機能を追加しました。これを利用して、リモートXBee デバイスから送信されたセンサーデータを FAX で自動送信する例を紹介したいと思います。

XBee 自身には I/O や A/D 変換機能がありますので、これを使用すると簡単にセンサーネットワークができます。例えば、XBee に スイッチのみ接続したリモートデバイスで I/O イベントを送信してみます。

この状態でサーバー側 で XBee I/O イベントデータを受信します。イベントハンドラは下記の様に設定しておきます。DeviceServer でイベント受信時に実行されるイベントハンドラ(XBEE_IO_DATA)では、FAX 送信を行うスクリプトを別スレッドで起動するだけの処理にしています。

file_id = "XBEE_IO_DATA"

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

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

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

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

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

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

APIType			フレームデータ中のAPI Type(16進数2桁)					83

SourceAddress	フレームデータ中のSourceAddress
				16bit アドレスの場合(16進数4桁)							0A01
				64bit アドレスの場合(16進数16桁)						0013A200404AC397

NodeIdentifier	XBee デバイスの NodeIdentifier。
				DeviceServer に保持されたマスターファイルを使用して、
				SourceAddress から変換した値が設定される。				Device1
				マスターにNodeIdentifier未登録の場合は"" が設定される

RSSI			フレームデータ中のRSSI(16進数2桁)						45

Options			フレームデータ中Options(16進数2桁)						00

SAMPLE_COUNT	I/O データのサンプル数									1

SAMPLE_DIO		I/O データ中のサンプル対象となったDIOビット番号リスト
				(10進数、カンマ区切り)									0,1,4

SAMPLE_ADC		I/O データ中のサンプル対象となったADCビット番号リスト
				(10進数、カンマ区切り)									2,3

SAMPLE_<Sample#>_<"DIO"|"ADC">_<Bit#>

				I/O サンプルデータ値。
				DIO の場合は、High で "1"、Low で "0"。					1
				ADC の場合は 10進数。									1023

				<Sample#> には 最大、SAMPLE_COUNT まで 1から順番に
				インクリメントされた値が入る。

				<"DIO"|"ADC">は、I/O サンプルデータが ADC
				もしくは、DIO のどちらであるかを示す。

				<Bit#>は、サンプルデータのビット番号。10進数。

		例:"SAMPLE_1_ADC_0" は、第一サンプルデータ中の #0番ポートのADC変換値を示す。

]]

if not script_fork_exec("XBEE_IO_EVENT_FAX",g_params) then error() end

FAX 送信を行うスクリプト (XBEE_IO_EVENT_FAX) は下記の様に作成しておきます。このスクリプトでは、同じ リモートXBee デバイスから連続して受信したイベントデータに対して FAX を送信しないようにする機能も付けています。(同一デバイスのイベントデータを10 分以内に受信した場合には FAX 送信しない)

file_id = "XBEE_IO_EVENT_FAX"

--[[
*********************************************************************
パラメータで渡された XBEE_IO_DATA イベントデータを FAX で送信する
*********************************************************************
]]
log_msg("start..",file_id)

-------------------------------------
-- FAX送信先設定
-------------------------------------
local header = { 	RecipientFax = "0123456789",
					RecipientName = "送信先名前",
           			Subject = "監視システムアラームFAX",
					DocumentName = "ドキュメント名",
					SenderTel = "999-000-222",
					SenderFax = "123-456-789",
					SenderName = "送信者氏名"
				}

-------------------------------------
-- クリティカルセクション開始
-------------------------------------
local cstat,handle = critical_section_enter("FaxSendLapseTimeCheck",10000);
if not cstat then error() end

--------------------------------------------------------
-- 前回 FAX送信したときからの経過時間を計測する
-- XBee デバイス毎に経過時間測定用のフラグを使用するする
--------------------------------------------------------
local t = os.time()
local time_check_key = "PREV_FAX_TIME_" .. g_params["SerialNumber"]
local stat,prev_t = get_shared_data(time_check_key)
if not stat then error() end
if (prev_t ~= "") then
	-----------------------------------------------------------
	-- 前回の FAX送信から 10 分以内の場合には送信しない
	-- ログにのみメッセージを残す
	-----------------------------------------------------------
	local diff_t = os.difftime(t,tonumber(prev_t));
	if diff_t < 600 then
		log_msg("** 連続 FAX送信をキャンセルしました **",file_id)
		critical_section_leave(handle)
		return
	end
end

--------------------------------------------------------
-- 次回 FAX送信時の経過時間計測用に現在時刻を保存する
--------------------------------------------------------
if not set_shared_data(time_check_key,tostring(t)) then error() end

-------------------------------------
-- クリティカルセクション終了
-------------------------------------
if not critical_section_leave(handle) then error() end

-------------------------------------
-- FAX送信
-- キー名でソートしてから送信する
-------------------------------------
local body = {}
table.insert(body,"** XBee I/O event data **")
table.insert(body,"")
for key,val in orderedPairs(g_params) do
	table.insert(body,string.format("xbee_io[%s] = %s",key,val))
end
if not fax_submit(header,body) then error() end

XBee に接続したスイッチを操作するとサーバーからFAX が自動的に送信されます。FAX の内容は XBee I/O イベントデータをフォーマットして見易くした配列形式のテキストです。実際に送信された FAX は下記になります。

FAX 送信は サーバーPC にセットアップされている Microsoft FAX サービスを使用しています。DeviceServer や FAX サービスはサービスプログラムとして動作しますので、サーバーPC でログインしていなくても問題ありません。今回紹介したスクリプトでは、複数の リモートXBeeからのイベントデータをFAX送信する場合にも対応しています。

それではまた。