opniz拡張実装方法解説#1 Node.jsからデバイスへのリクエスト拡張編

# はじめに
[**opniz**](https://github.com/miso-develop/opniz)というM5ATOMといったESP32系デバイスをNode.jsから制御するIoTフレームを作っています。  
M5ATOMであれば専用の[Arduino Library](https://github.com/miso-develop/opniz-arduino-m5atom)を用意しているのでexampleの[Basicスケッチ](https://github.com/miso-develop/opniz-arduino-m5atom/blob/main/examples/Basic/Basic.ino)をそのまま書き込めば各種メソッドを[Node.js SDK](https://github.com/miso-develop/opniz-sdk-nodejs)から利用可能です。  

しかしある電子パーツと対応するArduinoライブラリを使ってNode.jsから制御したい、といった場合にはopnizを拡張実装する必要があります。  
本記事ではopnizの拡張実装方法について解説します。  



# [opniz](https://github.com/miso-develop/opniz)のしくみ

まずopnizのしくみを簡単に説明します。  
opnizはデバイス（Arduino Library）とPC（Node.js SDK）とでWebSocketまたはTCPで接続し、JSON-RPCでやりとりします。  

![.](https://raw.githubusercontent.com/miso-develop/opniz/main/extras/images/overview.png)

JSON-RPCは以下の形式となっており`method`にRPCで実行したいメソッド名を指定し、必要に応じて`params`にパラメーターを埋め込みます。  

```json: JSON-RPC
{
	"method": "method-name",
	"params": ["param1", "param2", "param3"]
}
```

opnizのNode.js SDKおよびArduino LibraryはこのJSON-RPCを受け取ったら`method`と一致する処理を実行する、という単純なしくみです。  



# 拡張実装の概要
JSON-RPCを受け取ったときに「どんな`method`がきたら」「どんな処理をするか」を定義することがopnizの拡張実装となります。  

またopnizは双方向通信を行っているのでJSON-RPCは**PC（Node.js SDK）からデバイス（Arduino Library）に送られる**こともあれば、その逆で**デバイス（Arduino Library）からPC（Node.js SDK）に送られる**こともあります。  
たとえばそれぞれ以下のようなケースが考えられます。  

* PC（Node.js SDK）からデバイス（Arduino Library）へのリクエスト
	* 例：PCからデバイスのLEDを制御する、デバイスのセンサー値を取得する
* デバイス（Arduino Library）からPC（Node.js SDK）へのリクエスト
	* 例：デバイスのボタンが押されたとき、赤外線信号を受信したときにPCへイベントを送る



# 具体的な拡張実装方法
ここからPC（Node.js SDK）→デバイス（Arduino Library）へリクエストするケースについて拡張実装方法をハンズオン形式で解説していきます。  
（解説が長くなってしまうためデバイス（Arduino Library）→PC（Node.js SDK）へリクエストするケースは別記事にします）  

## Step1 下準備
実装例としてM5ATOMにて拡張実装を行っていきます。  
以下を参考にNode.js SDK環境とopnizデバイスを準備してください。  

### 開発環境
バージョンはメジャーバージョンがそろっていればいけると思います。  

* デバイス：M5ATOM
	* M5ATOM Matrix、M5ATOM LiteどちらでもOK
* Arduino IDE
	* バージョン：v2.0.3
	* ボード：esp32:M5Stack-ATOM
	* ライブラリ
		* M5ATOM@0.1.0
		* FastLED@3.5.0
		* ArduinoJson@6.19.4
		* WebSockets@2.3.6
* Node.js：v16.17.1



### Node.js SDKのコード
以下のコマンドを実行し、index.jsに下記のコードをコピペしてください。  

```sh
$ npm install opniz
$ touch index.js
```

```js: index.js
"use strict"
const { Opniz } = require("opniz")

const port = 3000

const opniz = new Opniz.Esp32({ port }) // opnizインスタンス生成（M5ATOMクラスではなくESP32クラスをインスタンス生成）

const main = async () => {
	while (!(await opniz.connectWait())) console.log("connect...") // opnizデバイスへ接続
	console.log("[connected]")
	
	for (;;) { // ループ処理
		console.log("[loop]")
		console.log(await opniz.getFreeHeap()) // デバイスのヒープメモリーサイズを表示
		await opniz.sleep(1000)
	}
}
main()
```

### Arduino Libraryのコード
[opniz Arduino Library for M5ATOM](https://github.com/miso-develop/opniz-arduino-m5atom)をインストールし、以下のコードでスケッチを作成してください。  
（opniz Arduino Library for M5ATOMのインストール方法[はこちら](https://github.com/miso-develop/opniz-arduino-m5atom#arduino%E3%81%A7%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)）  

スケッチの`<SSID>`、`<PASSWORD>`、`<IP Address>`はそれぞれお使いの環境の情報に変更してください。  

```cpp: main.ino
#include <OpnizM5Atom.h>
#include <lib/WiFiConnector.h>

const char* ssid = "<SSID>";         // WiFiのSSIDに書き換え
const char* password = "<PASSWORD>"; // WiFiのパスワードに書き換え

const char* address = "<IP Address>"; // Node.js SDKを実行する端末のIPアドレスを指定
const uint16_t port = 3000;          // Node.js SDKを実行する端末のポート番号を指定

WiFiConnector wifiConnector(ssid, password); // WiFi接続ヘルパーインスタンス生成
Opniz::Esp32* opniz = new Opniz::Esp32(address, port); // opnizインスタンス生成（M5ATOMクラスではなくESP32クラスをインスタンス生成）

void setup() {
    initM5(); // M5ATOM初期化
    wifiConnector.connect(); // WiFi接続
    opniz->connect();        // Node.js SDKへ接続
}

void loop() {
    opniz->loop();         // opnizメインループ
    wifiConnector.watch(); // WiFi接続監視
}
```

### テスト実行
以下のコマンドでNode.jsプログラムを実行します。  
1秒おきにデバイスのヒープメモリサイズが表示されればOKです。  

```sh
$ node index.js
```

以上で下準備は完了です。  
ここから準備したコードへ変更を加えていき拡張実装します。  



## Step2 最小限の拡張実装
PCからデバイスのLEDを制御する、デバイスのセンサー値を取得するといったケースではNode.js SDKからJSON-RPCを送り、Arduino Libraryにて受け取ったJSON-RPCに一致する処理を実行します。  

ここでは例としてM5ATOMの内蔵LEDを制御できるようにしてみましょう。  

### Node.js SDKの拡張
M5AtomのArduinoライブラリでは`M5.dis.drawpix`関数で内蔵LEDを制御できます。  
Node.js SDKを拡張し、`M5.dis.drawpix`を呼び出すクラスを作ってみましょう。  

まずはOpnizクラスを継承実装します。  
Opnizは2022/12現在`M5Atom`クラスと`Esp32`クラスを提供しています。  
`M5Atom`クラスではすでに`M5.dis.drawpix`を呼び出す処理が実装されていますので、ここでは`Esp32`クラスを継承実装します。  

```js: Opniz.Esp32拡張クラス
class ExtendOpniz extends Opniz.Esp32 {
	async drawpix() {
		await this.exec("drawpix")
	}
}
```

`drawpix`メソッドでは`exec`メソッドをawait実行しています。  
`exec`メソッドは前述したJSON-RPCのJSONを生成しデバイス側へ送信するメソッドです。  
第一引数の値がJSON-RPCの`method`に割り当てられ、第ニ引数以降が`params`配列に割り当てられます。  

ここでは第一引数に`drawpix`のみ指定しているので、以下のようなJSON-RPCが生成されデバイスへ送信されます。  

```json: JSON-RPC
{
	"method": "drawpix",
	"params": []
}
```

上記の拡張クラスを用いたコードは以下のようになります。  

```js: index.js
"use strict"
const { Opniz } = require("opniz")

const port = 3000

class ExtendOpniz extends Opniz.Esp32 {
	async drawpix() {
		await this.exec("drawpix")
	}
}

// const opniz = new Opniz.Esp32({ port }) // opnizインスタンス生成（M5ATOMクラスではなくESP32クラスをインスタンス生成）
const opniz = new ExtendOpniz({ port }) // opnizインスタンス生成（ESP32クラスを継承し拡張したExtendOpnizクラスをインスタンス生成）

const main = async () => {
	while (!(await opniz.connectWait())) console.log("connect...") // opnizデバイスへ接続
	console.log("[connected]")
	
	for (;;) { // ループ処理
		console.log("[loop]")
		console.log(await opniz.getFreeHeap()) // デバイスのヒープメモリーサイズを表示
		await opniz.drawpix() // 拡張したLED制御メソッドを実行
		await opniz.sleep(1000)
	}
}
main()
```

opnizインスタンスの生成箇所が`new Opniz.Esp32`から`new ExtendOpniz`となっています。  
またループ処理内にて今回実装した`opniz.drawpix`をawait実行しています。  

### Arduino Libraryの拡張
続いてopniz Arduino Libraryを拡張実装します。  
先ほど拡張実装したNode.js SDKにて`ExtendOpniz.drawpix`を実行すると`method`に`"drawpix"`を指定されたJSON-RPCがデバイス側へ送信されます。  
このJSON-RPCを受け取り、実際に`M5.dis.drawpix`関数を実行する処理を実装します。  

opniz Arduino LibraryではNode.js SDKから送られたJSON-RPCに対する処理をハンドラークラスとして定義します。  
ハンドラークラスはストラテジーパターンとして実装しています。  
`BaseHandler`という継承元となるハンドラークラスを用意していますので、これを継承し専用のハンドラークラスを定義します。  

Node.jsの`ExtendOpniz.drawpix`から送られるJSON-RPCを受けて`M5.dis.drawpix`関数を実行するハンドラークラスは以下のようになります。  

```cpp: 拡張ハンドラークラス
class DrawpixHandler : public BaseHandler {
public:
    String name() override {
        return "drawpix";
    }
    String procedure(JsonArray params) override {
        M5.dis.drawpix(0, 0x00ff00);
        return "true";
    }
};
```

`BaseHandler`には`name`と`procedure`の2つのメソッドが用意されています。  
`BaseHandler.name`はNode.js SDKから送られてきたJSON-RPCの`method`と一致する文字列を返すようオーバーライドします。  
`BaseHandler.procedure`ではJSON-RPCの`method`と一致した場合に実行する処理を実装します。  

ここでは`BaseHandler.name`にて`drawpix`を返すよう実装することで、JSON-RPCの`method`が`"drawpix"`だった場合に`BaseHandler.procedure`内の`M5.dis.drawpix(0, 0x00ff00);`が実行されます。  

戻り値はセンサーの値を取得したりする場合はセンサー値を返すよう記述すべきですが、今回のように戻り値が特にない場合は文字列で`"true"`を返すようにします。  

上記の拡張ハンドラークラスを用いたコードは以下のようになります。  

```cpp: main.ino
#include "OpnizM5Atom.h"
#include "lib/WiFiConnector.h"

const char* ssid = "<SSID>";         // WiFiのSSIDに書き換え
const char* password = "<PASSWORD>"; // WiFiのパスワードに書き換え

const char* address = "<IP Address>"; // Node.js SDKを実行する端末のIPアドレスを指定
const uint16_t port = 3000;          // Node.js SDKを実行する端末のポート番号を指定

WiFiConnector wifiConnector(ssid, password); // WiFi接続ヘルパーインスタンス生成
Opniz::Esp32* opniz = new Opniz::Esp32(address, port); // opnizインスタンス生成（M5ATOMクラスではなくESP32クラスをインスタンス生成）

// drawpix用ハンドラークラスを継承実装
class DrawpixHandler : public BaseHandler {
public:
    String name() override {
        return "drawpix";
    }
    String procedure(JsonArray params) override {
        M5.dis.drawpix(0, 0x00ff00);
        return "true";
    }
};

void setup() {
    initM5(); // M5ATOM初期化
    wifiConnector.connect(); // WiFi接続
    
    opniz->addHandler({ new DrawpixHandler }); // drawpix用ハンドラークラスを登録
    opniz->connect();        // Node.js SDKへ接続
}

void loop() {
    opniz->loop();         // opnizメインループ
    wifiConnector.watch(); // WiFi接続監視
}
```

drawpix用ハンドラークラスの実装に加え、`setup`関数にて`opniz->connect`の前に`opniz->addHandler({ new DrawpixHandler });`を追記しています。  
新たに実装したハンドラークラスは`opniz->addHandler`にて登録を行います。  

ここまでのコードをデバイスへ書き込み、Node.jsを実行するとM5ATOMのLEDが緑色に点灯し続けます。  



## Step3 JSON-RPCにパラメーターを指定する
上記の実装では`M5.dis.drawpix`のピン番号やカラーコードといった引数が固定で実行されている状態です。  
これらの引数もNode.js SDKから指定できるように変更してみます。  

### Node.js SDKへの変更
ここは大した変更ではなく`Opniz.Esp32`継承クラスにて実行していた`exec`メソッドに対し、第二、第三引数を追加します。  
`exec`を実行する`drawpix`メソッドにも同様にパラメーターを追加します。  

```js: Opniz.Esp32拡張クラス
class ExtendOpniz extends Opniz.Esp32 {
	async drawpix(ledNumber, colorCode) { // ledNumber, colorCodeを追加
		await this.exec("drawpix", ledNumber, colorCode) // ledNumber, colorCodeを追加
	}
}
```

この変更により`exec`メソッドで生成、送信されるJSON-RPCが以下のようになります。  

```json: JSON-RPC
{
	"method": "drawpix",
	"params": ["<ledNumberの値>", "<colorCodeの値>"]
}
```

あとは`ExtendOpniz.drawpix`を呼び出す側で引数を指定します。  
以下のコードではOn/Off状態のフラグ変数を用意し、ループの度にOn/Offを切り替え`ExtendOpniz.drawpix`のカラーコードを変化させています。  

```js: index.js
"use strict"
const { Opniz } = require("opniz")

const port = 3000

class ExtendOpniz extends Opniz.Esp32 {
	async drawpix(ledNumber, colorCode) { // ledNumber, colorCodeを追加
		await this.exec("drawpix", ledNumber, colorCode) // ledNumber, colorCodeを追加
	}
}

const opniz = new ExtendOpniz({ port }) // opnizインスタンス生成（ESP32クラスを継承し拡張したExtendOpnizクラスをインスタンス生成）

const main = async () => {
	while (!(await opniz.connectWait())) console.log("connect...") // opnizデバイスへ接続
	console.log("[connected]")
	
	let onOffFlag = true // On/Offフラグ
	
	for (;;) { // ループ処理
		console.log("[loop]")
		console.log(await opniz.getFreeHeap()) // デバイスのヒープメモリーサイズを表示
		
		onOffFlag = !onOffFlag // On/Offフラグ反転
		await opniz.drawpix(0, onOffFlag ? "00ff00" : "000000") // On/Offフラグによりカラーコードを変化
		
		await opniz.sleep(1000)
	}
}
main()
```

### Arduino Libraryへの変更
拡張ハンドラークラスの`BaseHandler.procedure`に対し変更を行います。  

`BaseHandler.procedure`には`JsonArray params`というパラメーターがあります。  
JSON-RPCの`params`のデータがここに文字列配列として格納されます。  

先ほどNode.js SDK側で行った変更によりJSON-RPCの`params`は`["<ledNumberの値>", "<colorCodeの値>"]`となりました。  
たとえばNode.js側で`opniz.drawpix(0, "00ff00")`を実行した場合に生成、送信されるJSON-RPCの`params`は`["0", "00ff00"]`となります。  

`JsonArray params`より値を取り出し、適切な型にキャストしなおしたうえで`M5.dis.drawpix`の引数に指定します。  

```cpp: 拡張ハンドラークラス
class DrawpixHandler : public BaseHandler {
public:
    String name() override {
        return "drawpix";
    }
    String procedure(JsonArray params) override {
        uint8_t ledNumber = (uint8_t)params[0];         // paramsの1つ目の値を取得しuint8_t型にキャスト
        String colorCode = params[1];                   // paramsの2つ目の値を文字列のまま取得
        M5.dis.drawpix(ledNumber, str2crgb(colorCode)); // 取得したparamsの値をM5.dis.drawpixの引数に指定し実行
        return "true";
    }
};
```

この変更を加えたデバイス側のコードは以下のようになります。  

```cpp: main.ino
#include "OpnizM5Atom.h"
#include "lib/WiFiConnector.h"

const char* ssid = "<SSID>";         // WiFiのSSIDに書き換え
const char* password = "<PASSWORD>"; // WiFiのパスワードに書き換え

const char* address = "<IP Address>"; // Node.js SDKを実行する端末のIPアドレスを指定
const uint16_t port = 3000;          // Node.js SDKを実行する端末のポート番号を指定

WiFiConnector wifiConnector(ssid, password); // WiFi接続ヘルパーインスタンス生成
Opniz::Esp32* opniz = new Opniz::Esp32(address, port); // opnizインスタンス生成（M5ATOMクラスではなくESP32クラスをインスタンス生成）

// drawpix用ハンドラークラスを継承実装
class DrawpixHandler : public BaseHandler {
public:
    String name() override {
        return "drawpix";
    }
    String procedure(JsonArray params) override {
        uint8_t ledNumber = (uint8_t)params[0];         // paramsの1つ目の値を取得しuint8_t型にキャスト
        String colorCode = params[1];                   // paramsの2つ目の値を文字列のまま取得
        M5.dis.drawpix(ledNumber, str2crgb(colorCode)); // 取得したparamsの値をM5.dis.drawpixの引数に指定し実行
        return "true";
    }
};

void setup() {
    initM5(); // M5ATOM初期化
    wifiConnector.connect(); // WiFi接続
    
    opniz->addHandler({ new DrawpixHandler }); // drawpix用ハンドラークラスを登録
    opniz->connect();        // Node.js SDKへ接続
}

void loop() {
    opniz->loop();         // opnizメインループ
    wifiConnector.watch(); // WiFi接続監視
}
```

ここまでのコードをデバイスへ書き込み、Node.jsを実行するとM5ATOMのLEDが1秒おきに緑色に点滅します。  



# おわりに
簡単にですが以上がPC（Node.js SDK）→デバイス（Arduino Library）へリクエストするケースでの拡張実装方法となります。  
拡張実装を行えばあらゆるArduinoライブラリを活かした開発が可能となります。  

わかりやすく説明できているかなんともいえないところですので、ご不明点あればお気軽にコメント欄やDM等でご連絡ください。