Node-RED の右にあるサイドバー領域に表示される黒板と、それを操作するノードを作成してみましたので、ご紹介します。
コードは yamachan/node-red-contrib-rtk-board リポジトリにあります。少しずつ、地味に機能追加していたりします。2019年内に npm に登録するのが目標だったり。
作成した理由
Node-RED Advent Calendar 2019 の投稿ネタとして作成しました。ちょっと趣味に走って、想定を超えて拡張しつつありますが…
昨年の Node-RED Advent Calendar 2018 では「Node-RED パズルで遊んでフローに親しんでもらいたい」 という記事を投稿しました。Node-RED のフロー編集画面を生かし、パズル的なものを用意することで楽しく学んでもらいたい、という感じで。
ただその際、フロー中の正解/不正解をステータスのアイコンだけで表現しているのが地味で、もう少し派手な表現方法が欲しいな、何か作ってみよう、とネタを練っていました。まさか1年も寝かすことになるとは思いませんでしたが…
node-red-dashboard の利用を考えましたが、Node-RED お馴染みのフロー画面とは別ページ(別タブ)になります。そうではなくて、デバッグメッセージのように、フロー画面の右にあるサイドバー領域に何かを表示したいと思っていました。
将来的には Scratchキャット のように、見た目にわかりやすい反応で、プログラミング初心者が学習時に最初に遊んでみるノードのひとつになればいいな、などと妄想しております。
Virtual TJBot から学ぶ
サイドバー領域に何かを表示したい!と資料を探していたところ、発見したのが「How to Train Virtual TJBot in Node-RED」記事を訳して試してみた」の記事で紹介した、node-red-contrib-virtual-tjbot です。これはフロー画面の右にあるサイドバー領域に、TJBot の画像を表示して動かしています。まずはこのリポジトリのコードを読んで、サイドバーの仕組みを理解しました。
パネルを表示する
パネル表示のキーとなるのは /tjbot/config.html ファイル 55 行目あたりにある以下のコードです。
onpaletteadd: function () {
RED.sidebar.addTab({ // ココ!
id: "vtjbot",
label: "Virtual TJBot", // the label is displayed on the tab
name: "Virtual TJBot", // displayed in the view menu
content: '<div id="vtjbot" style="width: 100%; height: 100%"><iframe src="/tjbot" style="min-width: 260px; height: 500px; border: 0px" border="0"></iframe></div>', // content of the tab
//toolbar: "toolbar", // content for the footer of the tab
closeable: true, // can be closed
enableOnEdit: true,
iconClass: 'fa fa-android'
});
}
RED.sidebar.addTab()
関数を用いてパネルを追加しているのがわかります。ググったら API Reference に載っていましたが、詳細な説明はありませんでした。
パネルの中身を生成する
実際に表示されるコンテンツは <iframe src="/tjbot"
で指定されている以下のページです。
そして、このページを表示するためのhttpサーバー機能を提供しているのが /tjbot/ui.js ファイル 52行目あたりにある以下のコードです。
function init(server, app, log, redSettings) {
tjbotPath = join(redSettings.httpNodeRoot, "tjbot");
const socketIoPath = join(tjbotPath, "socket.io");
const bodyParser = require("body-parser");
app.use(bodyParser.json({ limit: "50mb" }));
app.use(tjbotPath, serveStatic(path.join(__dirname, "dist"))); // ココ!
io = socketio(server, { path: socketIoPath });
io.on("connection", socket => {
socket.emit("config", state);
});
}
なおこの /tjbot/ui.js は package.json ファイルで定義されていないので、自動的には読み込まれません。各ノードの定義部分の先頭で呼び出すことで、初期化が実施されています。
module.exports = function (RED) {
const ui = require("./ui.js")(RED);
// 以下略
}
操作ノードとの連携
頭の LED を光らせる shine ノードのコード 41行目あたりを見てみます。
switch (mode.toLowerCase()) {
case "shine":
if (color == "random") {
const randIdx = Math.floor(Math.random() * colors.length);
ui.emit("shine", { color: colors[randIdx] });
} else {
ui.emit("shine", { color: color });
}
break;
// 省略
}
emit 関数は /tjbot/ui.js ファイル 67行目あたりで定義されています。
function emit(command, params) {
io.emit(command, params);
switch (command) {
case "shine":
state.led.color = params.color;
break;
case "pulse":
state.led.color = "off";
break;
case "armBack":
case "lowerArm":
case "raiseArm":
case "wave":
state.arm.position = command;
break;
}
}
io
はさきほどの init()
関数で定義されていました。 WebSopcket 技術を使った socket.io でノードと表示パネルを連携させていることがわかります。
開発用の Node-RED 環境を準備
さて、ローカルで開発用の Node-RED 環境を用意しましょう。私の場合、適当な作業用のフォルダで、以下のようにコマンドを実行します。(Windowsコマンドプロンプトの場合)
md node-red-dev
cd node-red-dev
npm init -y
npm install --unsafe-perm --save node-red
md .node-red
echo 2> .node-red/settings.js
node_modules\.bin\node-red -u .node-red -s .node-red/settings.js
まあ普通のやり方だと思いますが、-u
-s
オプションで設定ファイルも同じフォルダ内に指定することで、既にインストール済みの Node-RED 環境に影響しないようにしています。これなら利用後はフォルダごと削除すればokです。
手抜きで setting.js を空にしているので、既存の設定を引き継ぎたい場合にはホームディレクトリにある .node-red フォルダからちゃんとコピーしてください。
そしてノード開発用のリポジトリ (今回は c:\work\GitHub\node-red-contrib-rtk-board) を作成し、管理者権限のコマンドプロンプトからシンボリックリンクを作成すれば開発準備は完了です。
mklink /d node_modules\node-red-contrib-rtk-board c:\work\GitHub\node-red-contrib-rtk-board
今回の機能を実際に試してみたい方は、まだ npm に登録されていないので、node_modules フォルダに必要ファイルを用意するため、ローカルで準備したNode-RED のフォルダで以下を実施してください。
cd node_modules
git clone https://github.com/yamachan/node-red-contrib-rtk-board.git
なお開発中に動作がおかしくなったら、.node-red 設定フォルダを settings.js ファイルだけにして、Node-RED を再起動すればok。
開発を始めよう
さて、準備ができたところでノード開発を始めましょう。基本的な構成は Virtual TJBot を参考に進めていきます。
設定ノード
まずは基本となる設定ノードから進めましょう。開発ガイド がとても参考になります。
後でいくらでも拡張できるので、最初は最低限の項目だけ設定できるようにします。
- ボードの種類 (いわゆる黒板、ホワイトボード、ブラックボードなど)
- ボードの大きさ (横幅、高さ)
以下が実際に開発した設定画面です。
わりと基本に忠実で、特別なことはやっていません。短いですし、詳細は実際のコードを見てください。
Output ノード
これが今回の主役ノードで、黒板への出力を全て担っています。
実はループや条件判断もある、簡易言語として実装されていて、テキストとしてプログラムを与える(payloadに指定する)といろんな操作ができたりします。詳しくは README を参照してください。時間があったら日本語の解説記事を書きますね!
入力を payload で受けるだけなので、設定画面は最低限です。「RTK Board Config」は前の章で説明した黒板全体の設定ノードです。
実装コードは以下で、dist フォルダに配置した黒板用の html と、そこで利用している描画用の js コードも含みます。
- rtk-board/output.html
- rtk-board/output.js
- rtk-board/ui.js
- rtk-board/dist/index.html
- rtk-board/dist/rtk-face.js
- rtk-board/dist/rtk-board.js
Output ノードの動作例 (基本的な使い方)
Output ノードの使い方は簡単で、Inject ノードを用意し、ペイロードを文字列に設定して、描画用のコマンドを記載すればokです。
以下はコマンドを記載した Inject ノードを幾つか接続して、上から順にクリックしていった結果です。
cls
rect 80 60 50 50 red
fillRect 100 80 70 70 blue
bp;arc 30 200 40 0 2 yellow;stroke
bp;arc 60 200 160 0 2 pink;fill
コマンドは ;
文字で繋げることで、複数指定することができます。今回の例ではわかりやすいように Inject ノードを幾つも並べていますが、1つの Inject ノードのテキストに、全部指定してしまってもokです。
Output ノードの動作例 (ステートの利用)
Output ノードで処理する際、処理系はステートフルに実装されていて、過去の描画パラメータを覚えています。
各コマンドのパラメータは省略されるか、省略を意味する _
文字が指定されると、最後に実施した値を再利用します。また color のような命令は、実際に描画を実施せず、この保存された値を更新します。
以下の例を参照してください。これも上から順に Inject をクリックした結果です。
cls
rect 100 100 50 50 red
rect 80 80
color blue
rect 40 40
color white; go 10 10
rect 40 40
rect
は四角形を描画する命令で、最初の2つの引数は縦横のサイズを、次の2つの引数は表示位置を、そして次の引数は描画色を指定します。
例えば Inject (3) にある rect
はサイズしか指定されていないため、表示位置と表示色は (2) の rect
の描画と同じになっています。Inject (5) にある rect
も同様にサイズしか指定されていませんが、その前の (4) で color
命令で描画色が青に変更されているため、青い四角形を描画しています。
Inject (7) にある最後の rect
もやはりサイズしか指定されていませんが、その直前 (6) で color
で描画色が白に変更されており、また go
で表示位置も移動されているので、左上に白い四角形として描画されています。
Output 処理系のステートを利用して指定を省略することで、指定するコマンドの総量を減らすことができます。また修正箇所が減り、変更も容易になります。
Output ノードの動作例 (ランダム表示)
ステートはランダム系のコマンドとも相性が良いです。
例えば以下はランダムに位置を指定する goRand
コマンドと、colorRand
コマンドを利用した例になります。
cls
goRand; colorRand; rect 10 10
Inject (2) をクリックすると、ランダムな位置に、ランダムな色で、小さな四角形が表示されます。何度もクリックすると、上記のようにカラフルな画面になりました。
後で説明のある loop
コマンドと組み合わせると、いろいろな表示が楽しめそうです。
Output ノードの動作例 (ステートの演算)
ステートを利用した、もう少し複雑なサンプルを見てみましょう。
まず (2) のコマンドに注目してください。円の半径に 45%
を指定されていて、これは表示する黒板のサイズの 45% の長さを指定したことになります。縦横の長さが違う場合には小さいほうの値をもとに計算されます。
そして表示位置として 50% 50%
が指定されていて、これはそれぞれ、黒板の横幅の50%、縦幅の 50% を意味しており、結果として黒板の中心を示しています。
今回のサンプルでは Inject を以下の順でクリックしています。
(1) → (2) → (3) → (4) → (3) → (4) → (3) → (4) → (3) → (4)
つまりは以下を実行したことになります。
cls
bp;arc 45% 50% 50%;stroke
let _r $($._r * 0.8)
bp;arc;stroke
let _r $($._r * 0.8)
bp;arc;stroke
let _r $($._r * 0.8)
bp;arc;stroke
let _r $($._r * 0.8)
bp;arc;stroke
そして鍵となるのが (3) の let
コマンドです。
let _r $($._r * 0.8)
let
コマンドは、ステートの内部値(描画を実施する際の変数の値)をセットするためのコマンドです。そしてパラメータを処理する際に $(
と )
で囲まれた範囲は、式として評価され演算が実行されます。
そして式のなかで、内部値は $
オブジェクトの値として参照できます。今回の let
コマンドを実施すると、円の半径を意味する _r
という内部値の値に対して 0.8
の数を積算していますから、つまりは半径が少し小さくなります。
(3) と (4) を繰り返し Inject してコマンドを実行するたび、より半径の小さい円が描画される、というわけです。
Output ノードの動作例 (ループの例)
さきほどは手動で Inject (3) (4) を何回かクリックしました。これをループを使って自動化してみましょう。
cls
bp;arc 45% 50% 50%;stroke
let l 4
let _r $($._r * 0.8);bp;arc;stroke;loop l -5
ここでは let
コマンドを使って、ループ回数をカウントする内部値(変数)である l
を定義しています。4回実施したいので、値は 4
をセットしています。
その後の loop
コマンドが繰返しを実施する命令で、これは指定された内部値(変数)、今回は l
を使って繰り返しを実施します。具体的には l
から1を引き、0より大きければその後にある値 -5
を実行カウンタに加える、つまり5つ命令を戻しています。
昔のアセンブリ言語にあったような、原始的な繰り返し命令です。現時点ではまだエラーチェックが甘く、下手するとブラウザがフリーズしかけるので、注意して扱ってください。
ステートの仕組みや、相対的な移動である move
コマンドと組み合わせてうまく使えば、記述を大きく減らせる可能性をもっています。まぁ、趣味の機能なのでゆるーく楽しむ程度にしてください。
Face ノード
今回の黒板機能は、Output ノードだけで完結しています。この Face ノードは、その機能を簡単に使うための、サポート用のノードになります。
次のサンプルをみてください。
(3) の Face ノードの設定は以下のようになっています。
また (4) の Inject のペイロードは以下の文字列になっていて、face コマンドを記述しています。
face smile rf 80% 80% 10% 10%
このサンプルにおいて、(2) の smile を設定した Inject をクリックしても、(4) の face コマンド設定した Inject をクリックしても、描画される画像は同じです。
つまり Face ノードは、Output に実装された face コマンドを簡単に記述するためのノードです。基本的な情報を設定すれば、後は smile(笑顔), ugly(憂鬱顔), sad(悲しい顔), safe(安心した顔), angly(怒り顔), usual(素の顔) などの使いたい表情(face_mode)をペイロードで指定するだけで描画できます。
実際のコードはこちら。
テキストのコマンドを並べるのも楽しいですが、やはりこの Face のようなサポート用のノードを使ったほうが簡単に楽しめますし、Node-RED っぽいですよね。今後も、サポート用のノードを増やしていく予定です。
縦長の顔はいやだ
今回の Face ノードのサンプルを見て「縦長の顔は可愛くない」との指摘をいただきました。表示サイズを % で指定しているので、表示する黒板のサイズにあわせて顔が縦長に表示されています。
とりあえずの解決策なのですが、縦のサイズを 80%
という黒板からの相対値ではなく、$_w
という内部値を参照としたものに変更してください。``_w'' は横のサイズを示す内部値で、それを指定することで、表示する際に縦横同じ、つまり正方形の領域に顔が表示されるようになります。
Inject に指定したコマンドのほうも同様に変更しておきます。
というわけで
とりあえずはサイドバー領域に黒板的な表示パネルを追加し、そこに描画するためのノードを定義できました。ノード開発楽しいです。
まだ実装できそうな描画コマンドはありますし、それらを簡単に扱えるようにサポートノードの数も増やしたいところです。しばらくはコードを修正して、機能を拡張し、年内の npm への公開まで頑張ります!
とりあえずはステータスちゃんと表示して、国際化して日本語追加して、あとは画像表示コマンドの実装とラベル機能の完成、はクリスマスまでには終わらせる予定。などと宣言して自分を追いつめてみる。
それではまた!