#MQTT-SNを試してみよう#
最近耳にする事が増えてきたMQTTは軽量でM2Mにとって使いやすいプロトコルと言われています。しかし、MQTTはTCP/IPを使用するため常時接続が基本です。 クライアントデバイスの電池等の消耗を防ぐには通信の都度CONNECTする必要があり、通信のオーバーヘッドが必要です。 一方、MQTT-SNはUDPなどのセッションレスのプロトコルを使用できるため、通信しない時はCPUをスリープさせて電池の消耗を防ぐ事ができます。
こんな特徴を持つMQTT-SNを簡単にテストできるライブラリーを紹介します。 MQTTのライブラリーとして有名なのはPahoですが、紹介するライブラリー は、Pahoと設計思想が違います。 Pahoはプリミティブで汎用的な機能を提供しますが、このライブラリーはMQTT-SNクライアントの特定用途向けのアプリケーション・フレームワークと汎用的なゲートウェイを提供しています。 そのためPUBLISHだけで、SUBSCRIBE、SERCHGW、GWINFO、CONNECTやREGISTERを知らなくても簡単にアプリが作成できちゃったりします。
Non Block I/Oで非同期通信を実現しています。 PUBLISHでデータ送信しながら、同時にSUBSCRIBEしたトピックがPUBLISHされると指定した処理が実行されます。 Arduino Ethernet (Flash 30KB, RAM 2kB) でも動きます。
##用意するもの##
- LinuxマシンとC++開発環境
- Arduino EthernetボードとArduino開発環境(1.6.3)
- インターネットルーター DHCPサーバー付
##作成するもの##
- UDPを使用したクライアントで、Linux用とArduino用の2種類。
- MQTTプローカーとMQTT-SNクライアントを中継するゲートウェイ。
##テスト内容##
- ArduinoクライアントはLEDランプのオンオフ用トピックxxxx/onoff/arduinoをSUBSCRIBEし、トピックがBool値と一緒にPUBLISHされるとLEDのオンオフを行います。また、xxxx/onoff/linuxトピックをFalse/Tureと一緒に交互にPUBLISHします。
- LinuxクライアントはLEDランプのオンオフ用トピックxxxx/onoff/linuxをSUBSCRIBEし、トピックがBool値と一緒にPUBLISHされると標準出力にON/OFFと出力します。また、xxxx/onoff/arduinoトピックをFalse/Tureと一緒に交互にPUBLISHします。
- ゲートウェイはこれらのトピックをMQTTとMQTT-SNの変換を行って、クライアントとブローカ間で中継します。
- この連携により、ArduinoのLEDランプが点滅し、LinuxクライアントがON/OFFとプリントすることを確認します。
- ゲートウェイは通信ログをリアルタイムで書き出しているので、このログを表示させます。 ログを見ればMQTT-SNプロトコルを理解しやすくなるでしょう。
##Linux用クライアントの作成##
step1. ホームディレクトリーで、Githubからライブラリーをダウンロードします。
$ git clone https://github.com/ty4tw/AsyncMQTT-SN.git
AsyncMQTT-SN/AsyncClient/srcにクライアントアプリのソースコードApplication.cppとAsyncMQTT-SN/AsyncClient/src/libに必要なライブラリーが入っています。
step2. Application.cppをコンパイルします。
$ cd AsyncMQTT-SN/AsyncClient
$ make
コンパイルエラーがなければテスト用クライアントをコピーします。
$ make install
実行モジュール TomyAsyncClient がホームディレクトリーにコピーされます。
step3. クライアントを実行します。
$ ./TomyAsyncClient
ゲートウェイが動いていないので何も起きませんが、wiresharkでモニターすればSEARCHGWメッセージがUDPで送信されていることが確認できます。
また、MqttsnClientApp.h の68,69行の行頭の'//'を削除してから再コンパイルすると、UDPで送信しているメッセージが標準出力に表示されます。
##ゲートウェイは MQTT-SNを簡単にテストする方法 その2 で作成します。##
その前に、アプリケーションの構造について説明します。 アプリケーションの構造はLinuxもArduinoも共通です。
最初のブロックは宣言部分で、すべてのアプリケーションで共通です。
#ifdef ARDUINO
#include <SoftwareSerial.h>
#include <lib/MqttsnClientApp.h>
#include <lib/MqttsnClient.h>
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#else
#include "lib/MqttsnClientApp.h"
#include "lib/MqttsnClient.h"
#endif
#if defined(ARDUINO) && (defined(DEBUG_NW) || defined(DEBUG_MQTTSN) || defined(DEBUG))
SoftwareSerial debug(8, 9);
#endif
using namespace std;
using namespace tomyAsyncClient;
extern MqttsnClient* theClient;
次のブロックはアプリケーションが使用するUDPとXBeeに関わるパラメータとMQTT-SNの設定をしています。
Linux用のクライアントのIPアドレスはDHCPサーバーから取得しているアドレスが暗黙的に使用されますが、Arduinoの場合はMACアドレスとIPアドレスを明示的に設定する必要があります。
また、SERCHGW、GWINFO、ADVERTISEメッセージをブロードキャストするため、マルチキャスト用のIPアドレスとポートNoも指定します。 このアドレスとポートNoは通信するクライアントとゲートウェイで同じ値を設定します。
MQTT-SNの設定では、KeepAliveでPINGREQの送信間隔を秒単位で指定します。
この他、Willメッセージを使用するならばWillTopic と WillMessage を文字列で指定します。 使用しない場合は 0 を指定します。""空文字列は使えません。
MqttsnClientApp.h の43行目 '#define NETWORK_UDP'をコメントアウトするとXBee用の設定が選択されます。
/*============================================
* MQTT-SN Client Application
*===========================================*/
#ifdef NETWORK_XBEE
XBEE_APP_CONFIG = {
{
"client01", //ClientId
57600, //Baudrate
0, //Serial PortNo (for Arduino App)
"/dev/ttyUSB0" //Device (for linux App)
},
{
300, //KeepAlive
true, //Clean session
false, //EndDevice
"willTopic", //WillTopic or 0 DO NOT USE NULL STRING "" !
"willMessage" //WillMessage or 0 DO NOT USE NULL STRING "" !
}
};
#endif
#ifdef NETWORK_UDP
UDP_APP_CONFIG = {
{
"LinuxClient", //ClientId
{225,1,1,1}, // Multicast group IP
1883, // Multicast group Port
{0,0,0,0}, // Local IP (for Arduino App)
12001, // Local PortNo
{0,0,0,0,0,0} // MAC address (for Arduino App)
},
{
300, //KeepAlive
true, //Clean session
false, //EndDevice
"willTopic", //WillTopic or 0 DO NOT USE NULL STRING "" !
"willMessage" //WillMessage or 0 DO NOT USE NULL STRING "" !
}
};
#endif
次のブロックではアプリケーションで使用するトピックを定義します。
XXXの部分は適当に変えてください。 テストで使用するブローカーは test.mosquitto.org です。 トピックは利用者全員の共用となるので、他の人が使用するトピックと重複しないようにしてください。
/*------------------------------------------------------
* Create Topic
*------------------------------------------------------*/
const char* topic1 = "xxxx/onoff/arduino";
const char* topic2 = "xxxx/onoff/linux";
次のブロックはタイマーで定期的に起動されるタスクを関数として定義します。
関数はの様式は
void func(void)
で、複数のタスクを定義できます。
タスクの中でPUBLISH(Topic*,Payload*,QoS)
できます。
PayloadはPUBLISHメッセージで送信するバイナリーデータを表すクラスで、様式はMessagePackのサブセットを使用してデータを圧縮しています。 使い方は次のようになります。
application.cpp
Payload pl = Payload(40); // 40バイトのデータ領域を持つPayloadを生成
pl.set_uint32( 200); // uint32型のデータを書き込む、
pl.set_uint32( 10);
pl.set_uint32( 50000);
pl.set_uint32( 70000);
pl.set_int32( -300); // int32型
pl.set_int32(-70000);
pl.set_float((float)1000.01); //浮動小数点型
pl.set_str("abcdef"); //文字列
//PayloadをQoS1で送信
PUBLISH(tp_request_01,&pl,1);
// PULISHで送られたPayloadはポインターで関数に渡されます。
// Payloadから取り出すデータをインデックスで指定します。
// 詳しくはライブラリーのPayload.hを見てください。
uint32_t val0 = pl->get_uint32(0);
uint32_t val1 = pl->get_uint32(1);
uint32_t val2 = pl->get_uint32(2);
uint32_t val3 = pl->get_uint32(3);
int32_t val4 = pl->get_int32(4);
int32_t val5 = pl->get_int32(5);
int32_t val6 = pl->get_float(6);
uint16_t len;
const char* str = pl->get_str(7,&len);
```cpp:application.cpp
/*------------------------------------------------------
* Tasks invoked by Timer
*------------------------------------------------------*/
static bool onoffFlg = true;
// 関数実行の都度、ON/OFFと交互になるようにブール値を設定してPUBLISH
void task1(void){
printf("TASK1 invoked\n");
Payload* pl = new Payload(10);
onoffFlg = !onoffFlg;
pl->set_bool(onoffFlg);
PUBLISH(topic1,pl,1);
}
// 他にもTask関数を記述すれば、実行されることを確認
void task2(void){
printf("TASK2 invoked\n");
}
上のブロックで定義したタスクを実行する時間間隔を秒単位で設定します。
task1 は 2秒間隔で、task2 は 20秒間隔で実行するように設定しています。
TASK_LIST, TASK, END_OF_TASK はマクロです。
/*--------------- List of task invoked by Timer ------------*/
TASK_LIST = { //TASK( const char* topic, executing duration in second),
TASK(task1,2),
TASK(task2,20),
END_OF_TASK_LIST
};
SUBSCRIBEしたトピックがPUBLISHされたときに実行される関数を定義し、SUBSCRIBE_LISTでトピックと関数をつなぎあわせて、PUBLISHのQoSを指定します。
複数の関数が定義できます。
INDICATOR_ON(bool)、 SUBSCRIBE_LIST、 SUB と END_OF_SUBSCRIBE_LIST もマクロです。
Payloadについている tomyAsyncClient:: の表記はArduinoでは必須となります。
/*------------------------------------------------------
* Tasks invoked by PUBLISH command Packet
*------------------------------------------------------*/
int on_publish(tomyAsyncClient::Payload* payload){
printf("ON_PUBLISH invoked. ");
INDICATOR_ON(payload->get_bool(0)); // Payloadからブール値を取り出すして使用
return 0;
}
/*------------ Link Callback to Topic -------------*/
SUBSCRIBE_LIST = { //SUB(topic, on_publish, QoS),
SUB(topic2, on_publish, 1),
END_OF_SUBSCRIBE_LIST
};
次のブロックでは割り込み処理関数と初期設定関数を定義しますが、この例では何も必要ありません。
/*------------------------------------------------------
* Tasks invoked by INT0 interruption
*------------------------------------------------------*/
void interruptCallback(void){
}
/*------------------------------------------------------
* setup() function
*------------------------------------------------------*/
void setup(void){
}
クライアント・アプリケーションはこれだけです。 あとは全てライブラリーが実行してくれます。
TASK関数 void func(void)
と On_Publish関数 int func(tomyAsyncGateway::Payload* payload)
を記述し、
TASK_LIST と SUBSCRIBE_LIST を宣言するだけで完成です。