はじめに
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
にパラメーターを埋め込みます。
{
"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
"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>
はそれぞれお使いの環境の情報に変更してください。
#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
クラスを継承実装します。
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が生成されデバイスへ送信されます。
{
"method": "drawpix",
"params": []
}
上記の拡張クラスを用いたコードは以下のようになります。
"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
には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"
を返すようにします。
上記の拡張ハンドラークラスを用いたコードは以下のようになります。
#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
メソッドにも同様にパラメーターを追加します。
class ExtendOpniz extends Opniz.Esp32 {
async drawpix(ledNumber, colorCode) { // ledNumber, colorCodeを追加
await this.exec("drawpix", ledNumber, colorCode) // ledNumber, colorCodeを追加
}
}
この変更によりexec
メソッドで生成、送信されるJSON-RPCが以下のようになります。
{
"method": "drawpix",
"params": ["<ledNumberの値>", "<colorCodeの値>"]
}
あとはExtendOpniz.drawpix
を呼び出す側で引数を指定します。
以下のコードではOn/Off状態のフラグ変数を用意し、ループの度にOn/Offを切り替えExtendOpniz.drawpix
のカラーコードを変化させています。
"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";
}
};
この変更を加えたデバイス側のコードは以下のようになります。
#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等でご連絡ください。