SORACOM InventoryにESP8266で接続する

# 概要
ESP8266 (Arduino framework) と、[WakaamaNodeライブラリ](https://openhab-nodes.github.io/wakaamaNode/)を用い、SORACOM Inventoryへ接続するための手順です。

[SORACOM Inventory](https://soracom.jp/services/inventory/)は、SORACOMが提供する[OMA LwM2Mプロトコル](https://ja.wikipedia.org/wiki/OMA_LWM2M)で通信を行う、デバイス管理サービスです。

SORACOM上の他のサービス (SORACOM Beam, Funk, Funnel, Harvest Data) と連携させるためのゲートウェイにもなり、一般的なデバイス管理だけでなく、データ収集なども実現できます。

また、事前を登録をしていれば、SIMを接続するが必要なく、例えばWiFiのみを搭載した機器などでSORACOMサービスを利用するときには、特に有用です。

WakaamaNodeは、組み込み向けにLwM2Mプロトコルを実装したライブラリで、デフォルトでESP8266をサポートしていますが、実際の運用のためにはいくつかの注意事項がありますので、ここで手順を追って説明します。

なおESP32でも動けば、M5StickCなどでも使えるのですが、ESP8266とのSDKの差分(mbedtls周り)からフォローできていません。追加情報があれば、まとめたいと思います。

# 手順
## SORACOMでデイバス登録
今回は、実装を簡単にするため、ESP8266からブートストラップは行いません。
事前にSORACOM Console上などでデバイス登録を行います。

![SORACOM Console上でデバイス登録時の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/d34de6cc-1fd8-0494-8fa7-16931ed5fd3b.png)

デバイスIDとシークレットキーは後で利用します。

## PlatformIOのセットアップ
WakaamaNodeが、PlatformIO環境でのライブラリとして提供されているため、これを用います。
Arduino IDEでも必要なファイルをコピーしてくれば利用可能ですが、ソースの複雑さなど、ある程度の規模になりますので、PlatformIO環境での開発をおすすめします。

### ライブラリのインストール
検索すればヒットするので、そのままインストールします。

![Wakaama Nodeライブラリをインストール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/b7f99c20-23f5-7fab-969f-5f1b70ce3155.png)

### ビルドオプションの設定
ライブラリ内で、"wakaama_config.h"を参照しているのですが、PlatformIOのグローバルライブラリとしてインストールすると、プロジェクトソースから参照してくれないのでこれを解決します。

%USERPROFILE%\.platformio\lib\WakaamaNode_ID2964\src\include ディレクトリに*wakaama_config.h*ファイルを作成を作成し、内容に意味のないファイルにしてください。

```cpp:wakaama_config.h
#pragma once
```

空ファイルだと、そもそも include されず、"No such file なんたら"というエラーになるので注意。

本来、*wakaama_config.h*内で設定すべきだったビルドオプションを、プロジェクトのビルドフラッグとして追加します。

```ini:platformio.ini
; (省略)
[env:ほげほげ]
; ビルドフラッグとして下記を追加
build_flags =
	-DLWM2M_BIG_ENDIAN
	-DLWM2M_WITH_DTLS
	-DLWM2M_CLIENT_MODE
	-DLWM2M_DEVICE_WITH_REBOOT
	-DLWIP
	-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH
	-Wno-pointer-arith
```

## SORACOM Inventoryに接続
[SORACOM公開のドキュメント](https://dev.soracom.io/jp/start/inventory_registration_with_keys/)をもとに接続を行います。

SORACOMコンソールで追加したデバイス情報を*include/credentials.h*に、WiFiの接続情報を*include/wifi_config.h*に記載します。

```cpp:wifi_config.h
#pragma once

#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"
```

シークレットキーは、登録時の文字列をBase64デコードしたバイナリ列を用います。
変換時のEndian設定によって、結果が変わるので注意してください。
(WSL上にて `echo <secretKey> | base64 --decode | hexdump` した結果は、さらにEndian変換する必要がありました。)

```cpp:credentials.h
#pragma once

#define DEVICE_ID "d-xxxxxxxxxxxxxxxxxxx"
#define DEVICE_SECRET_KEY_LEN 16
const char DEVICE_SECRET_KEY_BYTE_ARRAY[DEVICE_SECRET_KEY_LEN] = {
	0x00, 0x01, 0x02, 0x03,
	0x04, 0x05, 0x06, 0x07,
	0x08, 0x09, 0x10, 0x11,
	0x12, 0x13, 0x14, 0x15,};

#define DEVICE_SECRET_KEY DEVICE_SECRET_KEY_BYTE_ARRAY
```

接続先ホストは、WakaamaNodeライブラリにはDNS Lookup機能がありませんので、事前にIPアドレスを調べたものを利用します。

次のコードを、ESP8266にアップロードすれば、SORACOM Inventoryへ接続できるはずです。
動作状況の把握が困難ですので、デバッグ情報を追加したソースを、[こちらのリンク](https://github.com/talch218/SORACOM_Inventory_ESP8266/releases/tag/minimal)から取得してください。

```cpp:main.cpp
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <lwm2m/connect.h>

#include "credentials.h"
#include "wifi_config.h"

// 接続先ホストは、IPアドレスを用いる
char coap_uri[] = "coaps://aaa.bbb.ccc.ddd:5684";

#define shortServerId 123
LwM2MConnect context("connect_soracom_inventory_device");

void setup() {
	// デバイス情報の設定
	context.deviceInstance.manufacturer = "some manufacturer";
	context.deviceInstance.model_name = "the model";
	context.deviceInstance.device_type = "led";
	context.deviceInstance.firmware_ver = "1.0";
	context.deviceInstance.serial_number = "140234-645235-12353";

	// WiFi接続
	WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
	int wifi_process_count = 0;
	while (WiFi.status() != WL_CONNECTED) delay(500);

	// サーバー接続設定
	context.add_server(shortServerId, coap_uri, 100, false);
	context.use_dtls_psk(shortServerId, DEVICE_ID, DEVICE_SECRET_KEY, DEVICE_SECRET_KEY_LEN);

	delay(1000);
}

void loop() {
	// LwM2M通信を処理
	context.process();

	delay(100);
}
```

デバッグ情報付きのソースであれば、シリアルコンソールで次のように表示されます。

```sh
State: STATE_INITIAL -> STATE_REGISTER_REQUIRED2
State: STATE_REGISTER_REQUIRED2 -> STATE_REGISTERING
State: STATE_REGISTERING -> STATE_READY
Connection change to: Established
```

また、SORACOMコンソール上でもデバイスがオンライン表示になります。

![SORACOMコンソール上のオンラインデバイス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/bf153e7d-b987-8d24-f33b-5ace107e883e.png)


### 注意
LwM2MはCoAPプロトコルを用い、CoAPプロトコルはUDPでの実装になっています。
そのため、不意の通信切断を検知することができず、接続時のライフタイム（サーバ側の設定も関連するかもしれません）の間、セッションが残っているようにふるまいます。
この状態で、コンフィグの異なるデバイスが登録しようとすると`STATE_BOOTSTRAP_REQUIRED`状態になることがあります。

```sh
State: STATE_INITIAL -> STATE_REGISTER_REQUIRED2
State: STATE_REGISTER_REQUIRED2 -> STATE_REGISTERING
State: STATE_REGISTERING -> STATE_BOOTSTRAP_REQUIRED
```

この時は、しばらく時間を空けて試すようにしてください。

## 予約済みオブジェクトモデルの利用
順番が前後しますが、LwM2Mでは[オブジェクトモデル](http://www.openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html)を用いた通信を行います。

WakaamaNodeライブラリ内で、オブジェクトID 1: **LwM2M Server**とID 3: **Device**が登録されるようになっています。

これだけだと使い勝手が悪いので、予約済みオブジェクトID: 3202 **Analog Input**を追加します。

[コード差分](https://github.com/talch218/SORACOM_Inventory_ESP8266/compare/ffc68fa..5df9c57)

[コード](https://github.com/talch218/SORACOM_Inventory_ESP8266/releases/tag/analog_input)

ID3202オブジェクトを利用するために、ヘッダを読み込みます。
オブジェクトとインスタンスを初期化し、オブジェクトにインスタンスを追加します。
オブジェクトは、どのような内容を通信するかの"規約"に相当し、インスタンスはその実体に相当すると捉えてください。

インスタンスの"R"eadオペレーションで定義された、各メンバの型に応じた初期化を行います。

```diff:main.cpp
（略）
+ #include <lwm2mObjects/3202.h>
+ KnownObjects::id3202::object analogInputObject;
+ KnownObjects::id3202::instance analogInputInstance;
+ 
+ KnownObjects::id3202::ApplicationTypeType applicationType;
+ #define ANALOG_INPUT_INSTANCE_ID 0
（略）
void setup() {
（略）
+ 	analogInputInstance.id = ANALOG_INPUT_INSTANCE_ID;
+ 	strcpy((char*)applicationType.data, "Sensor Values");
+ 	analogInputInstance.ApplicationType = applicationType;
+ 	analogInputInstance.MaxRangeValue = 100.0f;
+ 	analogInputInstance.MinRangeValue = -100.0f;
（略）
}
```

"W"riteオペレーションは、オブジェクトの`verifyWrite`メソッドによって定義します。

```diff:main.cpp
（略）
void setup() {
（略）
+ 	analogInputObject.verifyWrite = [](KnownObjects::id3202::instance* instance, uint16_t resource_id) {
+ 		// SORACOM Inventoryからデータを受信したときの処理を実装する
+ 		if (instance->id == ANALOG_INPUT_INSTANCE_ID) {
+ 			// Writeリクエストを処理したときは true, 拒否するときは false を返す。
+ 			switch ((KnownObjects::id3202::RESID)resource_id) {
+ 				case KnownObjects::id3202::RESID::ApplicationType:
+ 				// 未実装のためfalseを返して終了
+ 				return false;
+ 			}
+ 		}
（略）
}
```

"E"xecuteオペレーションは、インスタンスのメンバに`void(Lwm2mObjectInstance*, lwm2m_context_t*)`関数ポインタを代入します。

```diff:main.cpp
（略）
void setup() {
（略）
+ 	analogInputInstance.ResetMinandMaxMeasuredValues = [](Lwm2mObjectInstance*, lwm2m_context_t*) {
+ 		minmaxResetted = true;
+ 	};
（略）
}
```

コンテキストに`add_server`する前にオブジェクト、インスタンスを登録します。

```diff:main.cpp
（略）
void setup() {
（略）
+ 	analogInputObject.addInstance(CTX(context), &analogInputInstance);
+ 	analogInputObject.registerObject(CTX(context), false);
context.add_server(shortServerId, coap_uri, 100, false);
（略）
}
```

10秒に一回センサー値を更新するなど、接続確立以降に"R"eadオペレーションを行う場合は、インスタンスに新しい値を代入し、オブジェクトの`resChanged`メソッドを呼び出します。

```diff:main.cpp
+ void updateAnalogValue() {
+ 	// アナログ値としてランダムな値を返す
+ 	// 実運用ではセンサーからの出力などを用いる
+ 	float value = (random(20000) - 10000) / 100.0f;
+ 	analogInputInstance.AnalogInputCurrentValue = value;
+ 	analogInputObject.resChanged(CTX(context),
+ 								 analogInputInstance.id,
+ 								 (uint16_t)KnownObjects::id3202::RESID::AnalogInputCurrentValue);
+ }
```

無事に登録されるとSORACOMコンソールでも**Analog Input**インスタンスが追加で見れるようになります。

![Analog Inputインスタンス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/96c53f24-b70e-fd25-c8da-f1c0e687bc97.png)

しかし、このままでは正しい数値がとれていません。SORACOMサーバでは、数値データを64bitで処理していますが、WakaamaNodeライブラリでは `float`(32bit)として処理しているためです。

### 64bit対応
*3202.h*と*main.cpp*内の`float`を`double`に変更することで正常に表示されます。

[コード差分](https://github.com/talch218/SORACOM_Inventory_ESP8266/compare/5df9c57..d7a9292)

[コード](https://github.com/talch218/SORACOM_Inventory_ESP8266/releases/tag/analog_input_x64)

![Analog Inputインスタンス (64bit)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/67bfee03-6d2f-a370-b6ff-3031977ba70c.png)


### DeepSleep対応
先述の注意に記載したように、不意な切断を検知する方法がありません。
DeepSleepを用いるときは、明確に切断してからスリープすることをお勧めします。

```diff:main.cpp
loop () {
	context.process();
	delay(100);
	
+ 	if (context.is_connected()) {
+ 		updateAnalogValue();
+ 		delay(200);
+ 
+ 		lwm2m_network_close(CTX(context));
+ 		ESP.deepSleep(10 * 1000 * 1000);
+ 		delay(100);
+ 	}
}
```

## カスタムオブジェクトの利用
SORACOM Inventoryでは予約済みのオブジェクト以外にも、カスタムオブジェクトを定義、利用することが可能です。

カスタムオブジェクトの構造は、通常XMLで定義されますが、SORACOMではJSON形式での定義にも独自対応していますのでこれを利用します。

SORACOMコンソール上で、SORACOM Inventory -> オブジェクトモデルと開き、*オブジェクトモデルの追加*を行います。

```json:custom_object.json
{
	"id": 40001,
	"name": "CustomObjectSample",
	"description": "Sample object",
	"multiple": false,
	"mandatory": false,
	"resources": {
		"1": {
			"id": 1,
			"name": "Message",
			"operations": "W",
			"multiple": false,
			"mandatory": true,
			"type": "STRING",
			"rangeEnumeration": "",
			"units": "",
			"description": "Printing message for device console"
		},
		"2": {
			"id": 2,
			"name": "LED",
			"operations": "RW",
			"multiple": false,
			"mandatory": true,
			"type": "BOOLEAN",
			"rangeEnumeration": "",
			"units": "",
			"description": "Get or configure connected LED On/Off"
		},
		"3": {
			"id": 3,
			"name": "Analog Output",
			"operations": "W",
			"multiple": false,
			"mandatory": true,
			"type": "INTEGER",
			"rangeEnumeration": "0-255",
			"units": "",
			"description": "Analog output without feedback"
		},
		"4": {
			"id": 4,
			"name": "Sensor Value",
			"operations": "R",
			"multiple": false,
			"mandatory": true,
			"type": "FLOAT",
			"rangeEnumeration": "",
			"units": "",
			"description": "Sensor value"
		},
		"5": {
			"id": 5,
			"name": "Update",
			"operations": "E",
			"multiple": false,
			"mandatory": true,
			"type": "STRING",
			"rangeEnumeration": "",
			"units": "",
			"description": "Update sensor value immediately"
		}
	}
}
```

Data Typeは[OMA-TS-LightweightM2M-V1_0_2-20180209-A](http://www.openmobilealliance.org/release/LightweightM2M/V1_0_2-20180209-A/OMA-TS-LightweightM2M-V1_0_2-20180209-A.pdf)のAppendix Cに記載されています。
IDも用途によってどの範囲を使うか定義されていますので、空いているところを利用しましょう。

コードからも同じオブジェクトを利用できるように[jsonファイルからCヘッダファイルを生成するスクリプト](https://github.com/talch218/SORACOM_Inventory_ESP8266/blob/master/extra/generate_custom_object_header.js)を作成しました。
*mandatory*や*multiple*等の設定には対応していません。

```sh
node extra/generate_custom_object_header.js src/custom_object.json
> Generated 'src/custom_object.h'
```

使い方は**Analog Input**オブジェクトと、同じです。

[コード](https://github.com/talch218/SORACOM_Inventory_ESP8266/releases/tag/custom_object)

SORACOMコンソールから、[鉛筆アイコン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/547bb575-9dae-1add-d003-f9360138a14f.png)や[再生アイコン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640918/71d7ab35-dae1-3859-c1b9-265fddfffbb4.png)をクリックすることで、Write, Executeオペレーションをデバイスに実行させることができます。

最後にデバイスのシリアルコンソールログを載せます。

```
 ets Jan  8 2013,rst cause:2, boot mode:(3,6)

load 0x4010f000, len 3456, room 16 
tail 0
chksum 0x84
csum 0x84
va5432625
~ld
WiFi connecting _____.... connected
Add server, Success
Use DTLS, Success
Server uri: coaps://18.179.186.202:5684
Socket count: 1
Connection: Disconnected
State: STATE_INITIAL
Error: COAP_NO_ERROR

-------- First process -------
TargetP: 0x3fff565c
secObjInstID: 0x	   0
secInst: 0x3fff53dc
Connecting to coaps://18.179.186.202:5684 on 123
decodeUri: 2
Host: 18.179.186.202, Port: 5684
Security Mode: 3
Network Type: 0
AtoN: 1, 3401233170
Connection: 0x3fff65bc

State: STATE_INITIAL -> STATE_REGISTER_REQUIRED2
State: STATE_REGISTER_REQUIRED2 -> STATE_REGISTERING
Update analog value
value is 87.660004
State: STATE_REGISTERING -> STATE_READY
Connection change to: Established
Update sensor value now
value is -28.540001
Analog output: -32
Receive message: hello
```

# まとめ

ESP8266でSORACOM Inventoryに接続するために次のことを行いました。

1. SORACOMコンソールでデバイスの追加
2. カスタムオブジェクトの追加
3. WakaamaNodeライブラリの利用環境構築
4. カスタムオブジェクトから、Cヘッダファイルの生成
5. LwM2Mを用いた通信