はじめに
マイコンを遠隔操作したくないですか?
したいに決まってますよね!(4ヶ月ぶり2回目)
ということで今回お勧めするのはSORACOM Inventoryを利用したマイコン遠隔操作プログラムeasi(イージー)です。
1分でWio LTEを遠隔再起動するよ
注意
すでにSORACOM Inventoryを使用中の場合、以下を実行すると費用が発生します。(初期費用100円、利用料金50円)
未使用の場合は150円の無料枠に入ります。
事前準備として、
- Wio LTEを入手していること
- SORACOM AirのSIMをWio LTEに入れていること
- Arduino IDEをインストールすること
- Wio LTEのボード定義がインストールされていること
- USBドライバがインストールされていること
が必要です。ここは1分じゃ無くてすみません。。
これはソラコムさんが詳しく説明してくれているので、こちらの資料をご覧ください。
準備ができたら、GitHubのリポジトリからeasiをダウンロードし、ファイルをArduino IDEで開きましょう。macOSの場合は以下のようになります。
git clone https://github.com/1stship/easi.git
open easi/easi.ino
Wio LTEをDFU(Device Firmware Upgrade)モードで立ち上げ、Arduino IDEにてマイコンボードにプログラムを書き込みます。
書き込み終わったらリセットボタンを押して、通常モードで再起動します。立ち上がってしばらくすると、シリアルコンソールに以下のように表示されればOKです。
次にSORACOMコンソールにログインして、SORACOM Inventoryのデバイス管理メニューに入ります。
デバイスを選択して、詳細をクリックします。
Reboot /3/0/4を探して、実行ボタンをクリックします。
「コマンド実行」をクリックします。
はい、Wio LTEが再起動しましたね!これは簡単!
SORACOM Inventoryによる遠隔操作について
SORACOM Inventoryについての説明は、過去の同種の記事をご覧ください。
簡単に言うとSORACOMのユーザーコンソールやWeb APIから接続されたデバイスの情報を読み取ったり、情報を書き込んだり、処理を実行させたりできるのがSORACOM Inventoryです。
面倒なデバイス認証もSIMを用いたブートストラップという仕組みでデバイス認証用の秘密鍵を入手できるため簡単。
ある程度形の決まったLwM2Mという仕組みを使っているため、処理を一つ一つ用意しなくても良いので、自由度は若干下がるものの、扱いも簡単でコンソールやWebAPIもすでに用意されている。
簡単にデバイスを遠隔操作するにはうってつけのサービスです。
ただ、LwM2Mのエージェントを作らなければならない、というハードルがあり、便利そうだけどなかなか手が出せない、という人も多いのでは?と思って、主にラズパイで使えるエージェントとしてinventorydというプログラムを去年作りました。これはhttpdのように読み書きできるファイルや処理する実行ファイルを置いておくだけでSORACOM Inventoryから遠隔操作できる便利ツールです。
ところがオンデマンドリモートアクセス SORACOM Napterというサービスが発表され、ラズパイとかの遠隔操作これでいいんじゃない?という感じに個人的になってます。ラズパイでSSHサーバとか動かしておけば十分遠隔操作できちゃう。Napter超便利。
なんですが、Wio LTEとかM5Stackといったマイコンボードでは、サーバープログラム作るの大変だよね、ということで、このような環境でも動作するSORACOM Inventory エージェントを作りました。
easi - 簡単マイコン遠隔操作プログラム
easi
Easy Agent for SORACOM Inventory
https://github.com/1stship/easi
読みは「イージー」です。とても簡単に使えるマイコン遠隔操作プログラムを目指しました。
簡単、というのがどんな感じかと言えば、
メインファイルだけで完結する
というようにしました。正直Arduino IDEはIDEとしては相当使いにくく、ファイルエクスプローラ的なものもないし、コード補完とかも無く、デバッガもない、というかこれIDEなのか?ビルド、書き込みツール付きメモ帳じゃ無いのか?という感じです。一応シンタックスハイライトはあるみたいですが。。
この状態で色んなファイルを編集するのも大変ですし、Qiitaとかに記事書く時にも1ファイルにまとまった方がコード貼りやすいですよね。
ということで、DTLS、CoAP、LwM2Mといったプロトコルは裏で対応して、メインファイル(easi.ino)は以下の2点だけ記述すればいいようにしています。
- どのLwM2Mオブジェクトを使うか
- LwM2Mリソースのオペレーションとマイコン内の関数の関連づけ
具体的にはデフォルトでこんな感じのコードになっています。(2020/5/17時点)
#include "easi.h"
Lwm2m lwm2m;
WioLTE wio;
void setup() {
// 初期化
delay(200);
wio.Init();
wio.PowerSupplyLTE(true);
logText("POWER ON LTE");
delay(500);
if (!wio.TurnOnOrReset()) {
logText("Turn on error");
return;
}
// 接続
if (!wio.Activate("soracom.io", "sora", "sora")) {
logText("Connect error");
return;
}
delay(1000);
// LWM2Mエンドポイントとブートストラップサーバの設定
lwm2mInit(&lwm2m, "wiolte");
udpInit(&lwm2m.bootstrapUdp, "bootstrap.soracom.io", 5683);
// ブートストラップをせず払い出したデバイスIDとキーを使用する場合はこちら
// char identity[] = "d-01234567890123456789";
// uint8 psk[16] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
// lwm2mSetSecurityParam(&lwm2m, identity, &psk[0]);
// オブジェクトの設定のサンプル
// 以下のオブジェクトリストのうちID:0 〜 ID:9、ID:3200 〜 3203、ID:3300 〜 3350を登録済み
// http://www.openmobilealliance.org/wp/omna/lwm2m/lwm2mregistry.html
// デバイスオブジェクトをインスタンス0番として登録
addInstance(3, 0);
// Light Controlオブジェクトをインスタンス0番として登録
addInstance(3311, 0);
// オペレーションとメソッドを対応づけのサンプル
setReadResourceOperation ( 3, 0, 2, &getSerial); // READ /3/0/2 でgetSerialが呼ばれるよう設定
setWriteResourceOperation (3311, 0, 5850, &turnOnOffLED); // WRITE /3311/0/5850 turnOnOffLEDが呼ばれるよう設定
setExecuteResourceOperation( 3, 0, 4, &reboot); // EXECUTE /3/0/3 でrebootが呼ばれるよう設定
// ブートストラップ(接続情報取得)実行
// 成功するまで繰り返す
while (!lwm2mBootstrap(&lwm2m)){ }
}
void loop() {
// LWm2mのイベントが無いかチェックし、イベントを処理したらtrue、イベントが無ければfalseを返す
if (!lwm2mCheckEvent(&lwm2m)){
delay(100);
}
}
// READの場合は値をtlvの各要素に代入する
// Integer / Boolean / Timeはtlv->intValue
// Floatはtlv->floatValue
// Stringはtlv->bytesValue
// Opaqueはバイナリをtlv->bytesValue、長さをtlv->bytesLen
// ObjlnkはオブジェクトIDをtlv->ObjectLinkValue、インスタンスIDをtlv->InstanceLinkValue
// にそれぞれ代入する
void getSerial(Lwm2mTLV *tlv){
strcpy((char *)&tlv->bytesValue[0], "123456789");
};
// WRITEの場合は値がtlvの各要素から渡される
// 対応する要素はREADと同じ
void turnOnOffLED(Lwm2mTLV *tlv){
if (tlv->intValue){
wio.LedSetRGB(255, 0, 0);
} else {
wio.LedSetRGB(0, 0, 0);
}
};
// EXECUTEの場合、
// パラメータはOpaqueと同じ形式で渡す(使わなくてもよい)
void reboot(Lwm2mTLV *tlv){
NVIC_SystemReset();
};
メインファイル(easi.ino)がそのままチュートリアルになるようにコメントを入れています。
編集が必要な箇所はaddInstance
を使っている箇所と、set(Read/Write/Execute)ResourceOperation
を使っている箇所です。
addInstanceについて
addInstanceはLwM2Mのオブジェクトをこのプログラムで使用する、という処理です。
前提として、LwM2Mでは、デバイスへの問い合わせや書き込み、実行などをオブジェクトモデルというもので定義し、そのモデルをパスに割り当てています。
例えば最初に実行した「Reboot /3/0/4」というのは、Deviceというモデルの仕様にObjectIDが3と定義されており、次の0は最初のリソースを表し、(同じ定義のモデルが複数ある場合は/3/1/4、3/2/4と2個目の数字が上がっていく)、その中のItem ID="4"としてRebootが以下のように記載されています。
<Item ID="4">
<Name>Reboot</Name>
<Operations>E</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type/>
<RangeEnumeration/>
<Units/>
<Description>
<![CDATA[ Reboot the LwM2M Device to restore the Device from unexpected firmware failure. ]]>
</Description>
</Item>
従って、/3/0/4はDeviceというモデルの最初のリソースに対してRebootというExecuteを実行するもの、ということになります。
addInstance(3, 0);
とすると、オブジェクトID=3のオブジェクト、つまりDeviceオブジェクトをインスタンスID=0のインスタンスとして登録することができます。登録すると接続した時にこのオブジェクトは使えますよ、という情報がSORACOM Inventoryに届き、ユーザーコンソールやWebAPIでそのオブジェクトに対する操作ができるようになります。オブジェクトは同じIDでもインスタンスIDを変えれば複数使えますので、たとえばデジタルIOのオブジェクトをピンの数だけ用意する、ということもできます。
使えるオブジェクトはOMA LightweightM2M (LwM2M) Object and Resource Registryに登録されております。このプログラムで使えるように用意しているのは、以下のオブジェクトです。
- ID: 0 〜 9 (基本的なオブジェクト)
- ID: 3200 〜 3203 (汎用的なデジタル/アナログIO)
- ID: 3300 〜 3350 (センサーや表示、制御)
このくらいあれば、WioLTEで扱う分には十分と思います。使いたいセンサーや実行したい制御があれば、この中から見つけて、addInstance
で追加しましょう。
setResourceOperationについて
LwM2Mではリソースに対する操作を、
- Read(読み出し)
- Write(書き込み)
- Execute(実行)
の3つのOperationで定義しています。これらが呼び出されたら実行する処理を割り当てるのがset(Read/Write/Execute)ResourceOperation
です。デフォルト状態では、何もしない関数が割り当てられています。
たとえば先ほどのRebootだと、
setExecuteResourceOperation(3, 0, 4, &reboot);
と記載されており、これは/3/0/4に対するExecuteにrebootという関数を割り当てています。つまりSORACOM InventoryからEXECUTE /3/0/4が呼び出されると、マイコン内でreboot関数が実行されます。
reboot関数は以下のように定義しています。
void reboot(Lwm2mTLV *tlv){
NVIC_SystemReset();
};
これはCPUのリセットを実行する処理なので、rebootが実行されると再起動されます。
Read/Writeも基本的には同じで、
void 関数名(Lwm2mTLV *tlv)
で関数を定義し、その中に必要な処理を書いて、set(Read/Write/Execute)ResourceOperationにて割り当てれば、マイコン内の任意の処理とSORACOM Inventoryからの呼び出しを対応づけることができます。
Readの場合は、何らかの値を返す必要があり、Writeの場合は書き込まれる値を受け取る必要がありますが、このために以下のLwm2mTLV構造体を定義しています。
typedef struct {
enum Lwm2mTypeOfID typeOfId;
enum Lwm2mResourceType resourceType;
uint16 id;
int64 intValue; // Integer, Time, Boolean
float64 floatValue; // Float
uint16 ObjectLinkValue; // objlnk
uint16 InstanceLinkValue; // objlnk
uint8 bytesValue[4096]; // String, Opaque, None
int bytesLen; // Opaque, None
} Lwm2mTLV;
TLVとはType - Length - Valueの略で、様々な種類のデータ型を扱うフォーマットです。LwM2MのリソースにはString、Integer、Float、Boolean、Opaque、Time、Objlnk、noneという8つのデータ型があり、それぞれTLVにするフォーマットが決まっています。(Lwm2m仕様 Appendex C. Datatypes参照)
このシリアライズ、デシリアライズはまあまあ面倒なので、裏でやっています。関数内でやることは、構造体の適切なメンバーに値を代入する(Read)、値を参照する(Write、Operation)だけです。
例えばIntegerのリソースに対するReadの場合は、以下のようにtlv->intValueに値を代入すれば良いです。センサー値やステータスを返す場合などはこれを使います。
tlv->intValue = 100;
Stringのリソースに対するWriteの場合は、以下のようにtlv->bytesValueから値を取得すれば良いです。LCDに値を表示したりする時にはこれを使います。
SerialUSB.println((char *)tlv->bytesValue);
これで任意の値を返すことも、受け取って使用することも簡単にできます。
データ型の注意点としては以下のものになります。
- TimeはUNIX時間(秒)で扱われます
- Booleanは1がtrue、0がfalseとして扱います。(true / falseは定義しているのでそのまま使えます)
- OpaqueはbytesValueにデータが、bytesLenにデータ長さが格納されています。
- ObjlnkはObjectLinkValue、InstanceLinkValueにそれぞれ関連するオブジェクト、インスタンスのIDを格納します。(多分使わないと思いますが)
- Resourceのパラメータは厳密に言えばTLVフォーマットで渡されていないのですが、Opaqueと同じようにbytesValue、bytesLenで渡すようにしています。
これで好きなタイミングでセンサーから値を取得したり、渡されたパラメータをもとに制御したりできますね。
ブートストラップについて
プログラムの以下の部分は「ブートストラップ」という処理を実行しています。(この部分は変更する必要はありません)
// LWM2Mエンドポイントとブートストラップサーバの設定
lwm2mInit(&lwm2m, "wiolte");
udpInit(&lwm2m.bootstrapUdp, "bootstrap.soracom.io", 5683);
// 中略
// ブートストラップ(接続情報取得)実行
// 成功するまで繰り返す
while (!lwm2mBootstrap(&lwm2m)){ }
デバイスをクラウド上のサービスに接続するという場合、困るのはどのようにデバイスの認証情報を取得するか、ということです。たとえばAWS IoTではサービスから発行したデバイス証明書をデバイスに書き込む必要がありますが、そのためにマイコンに情報を書き込むのは大変な作業になります。
LwM2Mではブートストラップという処理があり、これを実行すると認証情報を提供するサーバーから認証情報を取得することができます。ただ、ブートストラップは平文通信(暗号化のDTLSの接続を確立するには認証情報が必要であるため、その前段階のブートストラップでは暗号化出来ない)であるため、普通に実行すると盗聴の危険性があります。SORACOM Inventoryではブートストラップは閉域内での通信とすることでこの問題を解消しています。これにより、デバイスによる個別の対応をしなくともデバイスの認証情報を取得できるため、特に生産段階においてとても楽になります。
ブートストラップは「エンドポイント」という単位で実行され、同じエンドポイントには同じSORACOM Inventoryのデバイスが対応づけられます。このエンドポイントをlwm2mInit(&lwm2m, "wiolte");
で設定しています。デフォルトでは「wiolte」という固定値を割り当てていますが、複数のデバイスの際にはIMEIなどのデバイスごとに固有の値を使った方がよいでしょう。
イベント処理について
プログラムの以下の部分はSORACOM Inventoryからの通信をチェックし、要求が届いていれば対処する、という動作をします。(この部分は変更する必要はありません)
// LWm2mのイベントが無いかチェックし、イベントを処理したらtrue、イベントが無ければfalseを返す
if (!lwm2mCheckEvent(&lwm2m)){
delay(100);
}
マイコンではスレッドを使うのが大変なので、イベントをチェックし、イベントが発生していれば処理する、という形を取っています。この中で各種Operationの処理や、DTLS接続をチェックして切れていればつなげ直す、などの処理を実行しています。Operation呼び出しにはタイムアウトがあるので、イベントチェック間隔は短めにしておいた方がよいでしょう。
プログラムの説明は以上です。
実装の例
では実際に試してみましょう。例として、「指定した色にLEDを光らせる」処理を追加してみます。
まずオブジェクトモデルの中からLEDを扱えそうなモデルを探します。
http://www.openmobilealliance.org/wp/omna/lwm2m/lwm2mregistry.html
を確認すると、オブジェクトID: 3311の「Light Control」というオブジェクトが使えそうです。
以下の定義
http://www.openmobilealliance.org/tech/profiles/lwm2m/3311.xml
を確認すると、リソースID: 5706の「Colour」というリソースがStringの読み書き可能なリソースとして定義されているので、これに色を書き込むとLEDがその色に光る、とすれば良さそうです。
まず関数を定義します。以下のような感じですかね。
void setLEDColor(Lwm2mTLV *tlv){
if (strcmp((char *)tlv->bytesValue, "red") == 0){
wio.LedSetRGB(255, 0, 0);
} else if (strcmp((char *)tlv->bytesValue, "green") == 0){
wio.LedSetRGB(0, 255, 0);
} else if (strcmp((char *)tlv->bytesValue, "blue") == 0){
wio.LedSetRGB(0, 0, 255);
} else {
wio.LedSetRGB(0, 0, 0);
}
}
次にオブジェクトID: 3311を追加します。(実はすでに入っていますが)
addInstance(3311, 0);
最後にオブジェクトID: 3311、リソースID: 5706に対するWriteを先ほどの関数に結びつけます。
setWriteResourceOperation(3311, 0, 5706, &setLEDColor);
これだけです。とても簡単ですね。全体としてはこうなりました。(必要ないリソースやコメントなどもそのままにしています)
#include "easi.h"
Lwm2m lwm2m;
WioLTE wio;
void setup() {
// 初期化
delay(200);
wio.Init();
wio.PowerSupplyLTE(true);
logText("POWER ON LTE");
delay(500);
if (!wio.TurnOnOrReset()) {
logText("Turn on error");
return;
}
// 接続
if (!wio.Activate("soracom.io", "sora", "sora")) {
logText("Connect error");
return;
}
delay(1000);
// LWM2Mエンドポイントとブートストラップサーバの設定
lwm2mInit(&lwm2m, "wiolte");
udpInit(&lwm2m.bootstrapUdp, "bootstrap.soracom.io", 5683);
// ブートストラップをせず払い出したデバイスIDとキーを使用する場合はこちら
// char identity[] = "d-01234567890123456789";
// uint8 psk[16] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
// lwm2mSetSecurityParam(&lwm2m, identity, &psk[0]);
// オブジェクトの設定のサンプル
// 以下のオブジェクトリストのうちID:0 〜 ID:9、ID:3200 〜 3203、ID:3300 〜 3350を登録済み
// http://www.openmobilealliance.org/wp/omna/lwm2m/lwm2mregistry.html
// デバイスオブジェクトをインスタンス0番として登録
addInstance(3, 0);
// Light Controlオブジェクトをインスタンス0番として登録
addInstance(3311, 0);
// オペレーションとメソッドを対応づけのサンプル
setReadResourceOperation ( 3, 0, 2, &getSerial); // READ /3/0/2 でgetSerialが呼ばれるよう設定
setWriteResourceOperation (3311, 0, 5850, &turnOnOffLED); // WRITE /3311/0/5850 turnOnOffLEDが呼ばれるよう設定
setExecuteResourceOperation( 3, 0, 4, &reboot); // EXECUTE /3/0/3 でrebootが呼ばれるよう設定
setWriteResourceOperation(3311, 0, 5706, &setLEDColor);
// ブートストラップ(接続情報取得)実行
// 成功するまで繰り返す
while (!lwm2mBootstrap(&lwm2m)){ }
}
void loop() {
// LWm2mのイベントが無いかチェックし、イベントを処理したらtrue、イベントが無ければfalseを返す
if (!lwm2mCheckEvent(&lwm2m)){
delay(100);
}
}
// READの場合は値をtlvの各要素に代入する
// Integer / Boolean / Timeはtlv->intValue
// Floatはtlv->floatValue
// Stringはtlv->bytesValue
// Opaqueはバイナリをtlv->bytesValue、長さをtlv->bytesLen
// ObjlnkはオブジェクトIDをtlv->ObjectLinkValue、インスタンスIDをtlv->InstanceLinkValue
// にそれぞれ代入する
void getSerial(Lwm2mTLV *tlv){
strcpy((char *)&tlv->bytesValue[0], "123456789");
};
// WRITEの場合は値がtlvの各要素から渡される
// 対応する要素はREADと同じ
void turnOnOffLED(Lwm2mTLV *tlv){
if (tlv->intValue){
wio.LedSetRGB(255, 0, 0);
} else {
wio.LedSetRGB(0, 0, 0);
}
};
// EXECUTEの場合、
// パラメータはOpaqueと同じ形式で渡す(使わなくてもよい)
void reboot(Lwm2mTLV *tlv){
NVIC_SystemReset();
};
void setLEDColor(Lwm2mTLV *tlv){
if (strcmp((char *)tlv->bytesValue, "red") == 0){
wio.LedSetRGB(255, 0, 0);
} else if (strcmp((char *)tlv->bytesValue, "green") == 0){
wio.LedSetRGB(0, 255, 0);
} else if (strcmp((char *)tlv->bytesValue, "blue") == 0){
wio.LedSetRGB(0, 0, 255);
} else {
wio.LedSetRGB(0, 0, 0);
}
}
これを書き込んで実行してみましょう。
うまくいきましたね!(blueが緑にしか見えないのはカメラのせい?肉眼では明らかに青いのですが、写真に撮るとどう見ても緑。。)
これはユーザーコンソールでもできますが、当然WebAPIでもできますので、外部プログラムから色を変える、というのも簡単です。CLIでやる場合はこんな感じになります。(デバイスIDは自分のデバイスIDを入れてください)
soracom devices put-resource --device-id 'd-XXXXXXXXXXXXXXXXXXXX' --object 3311 --instance 0 --resource 5706 --body '{"value":"red"}'
これは便利!WebAPIが用意されていると色々なシステム連携、またデバイスからの情報でデバイスを制御、とかもできそうですね。
おわりに
SORACOMでIoTを始めて、
- LTE-Mボタンでデバイスからクラウドの処理呼び出し
- GPSマルチユニットでデバイスからのセンサ情報をHarvest、Lagoonで可視化
をやって次に何をやろうかな、という人は多いのではないでしょうか?そんな方は次はWio LTEを使ったクラウドからデバイスへの遠隔制御を試してみれば良いと思います。IoTの基本的な要素である、デバイスからのデータ収集、可視化、クラウドでの処理、そしてデバイスへのフィードバックの一連の流れが習得できます。
デバイスからクラウドはやりやすいですが、クラウドからデバイスはそこそこ難しいです。それを簡単にしてくれるのがSORACOM Inventoryであり、これを使ったことないのはもったいないな〜、ということでInventoryを簡単に使えるプログラムを作ってみました。
ぜひeasiを試していただき、IoTの用途を広げていただければと思います。