1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

M5StackAdvent Calendar 2022

Day 4

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

Last updated at Posted at 2022-12-24

はじめに

opnizというM5ATOMといったESP32系デバイスをNode.jsから制御するIoTフレームを作っています。
M5ATOMであれば専用のArduino Libraryを用意しているのでexampleのBasicスケッチをそのまま書き込めば各種メソッドをNode.js SDKから利用可能です。

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

opnizのしくみ

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

.

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

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に下記のコードをコピペしてください。

$ npm install opniz
$ touch index.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をインストールし、以下のコードでスケッチを作成してください。
(opniz Arduino Library for M5ATOMのインストール方法はこちら

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

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です。

$ 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クラスを継承実装します。

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-RPC
{
	"method": "drawpix",
	"params": []
}

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

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関数を実行するハンドラークラスは以下のようになります。

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

BaseHandlerにはnameprocedureの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"を返すようにします。

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

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メソッドにも同様にパラメーターを追加します。

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-RPC
{
	"method": "drawpix",
	"params": ["<ledNumberの値>", "<colorCodeの値>"]
}

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

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の引数に指定します。

拡張ハンドラークラス
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";
    }
};

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

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等でご連絡ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?