IoTビジネスの現場で学んだNode-RED活用術

[Node-RED アドベントカレンダー2018](https://qiita.com/advent-calendar/2018/nodered)17日目の記事です。

## はじめに

**吉野 祥之**（よしの あきゆき）と申します。NTTコミュニケーションズ（株）でIoTをやってます。
先日の [Node-RED UG勉強会 2018 年末LTパーティ](https://node-red.connpass.com/event/104928) で登壇させていただいたのですが、かなり駆け足の説明になってしまったので改めて文章にまとめてみたいと思います。

あくまで本記事は筆者の経験と主観によるものですので、事実誤認やベストプラクティスなどあればコメントいただけると幸いです。

- 対象読者
  - Node-RED初学者（サンプルを見ながら簡単なフローを個人で作ったことあるくらい）

- 元ネタ
  - [Node-RED UG勉強会発表資料（Slideshare）](https://www.slideshare.net/AkiyukiYoshino/iotnodered)

## データモデル

Node-REDを使っていく上で最初に頭に入れておくべき点だと思います。
この辺の話は基本すぎるためか、まとめて書いてある記事があまり無い気がするので整理してみます。

### msgオブジェクトの利用

Node-REDフローを作成する上で真っ先に意識すべきなのは、**線で繋がった2ノード間ではmsgオブジェクトが受け渡される**ということです。Functionノードであっても入力される引数は `msg` であり、出力される際にも `msg` として扱われることを理解しましょう。
また、上記の仕様に基づきDebugノードを利用することでNode-REDエディタ上のデバッグタブでフローの処理状況を簡単に確認することができます。

一方、`msg` には**予約語的に扱われるプロパティが幾つか存在します**。最も一般的な例は `msg.payload` です。injectノードも指定した値を `msg.payload` の値として出力します。
その他、http-requestノードにおける `msg.url` 、 `msg.method` など、幾つかデフォルトの入出力値として利用されるプロパティが存在します。意図しない値の上書きなどを避けるためfunctionノードで `msg` に与えるプロパティの命名には注意してください。
エディタ上のノードを選択することでエディタの「情報」タブに入出力の記載がでてきますので、よく確認する癖をつけましょう。

<img width="640" alt="http-info.png" src="https://qiita-image-store.s3.amazonaws.com/0/315383/1326372d-aadf-874a-9fd5-7daeece9153a.png">

また、単一のfunctionノードで利用する一時的な変数（=次のノードに受け渡す必要がない変数）は `msg` のプロパティとして扱う**べきではありません**。

### contextの利用

msgオブジェクトは線で繋がった2ノード間でやり取りされるオブジェクトですが、直接線で繋がっていないノードや別のタブに存在するノードと情報を共有させたい時があると思います。こうした場合にはcontextを利用します。

contextは情報共有スコープが異なる3種類が存在します。いずれもgetter, setterのメソッド（`global.get("hoge")`, `flow.set("hoge","fuga")` など）で値を読み書きすることになります。

1. **Node context**：同一のノード内でのみ共有されるコンテクスト
2. **Flow context**：同一タブ内でのみ共有されるコンテクスト
3. **Global context**：同一Node-REDプロセス内で共有されるコンテクスト

上記の中でよく使うのは**Global context**だと思います。共通的な関数をGlobal contextにして使いまわしたり、複数タブにまたがるフラグ管理をGlobal contextとして管理するなどの利用が想定されます。
Node contextやFlow contextはあまり利用するケースが少ない印象です。

一方、Global contextの乱用は値の汚染などのリスクが大きくなるため、複数フロー/タブを跨るオブジェクトのやり取りを行う[^1]のであれば、以下に記載するデザインパターンの **UI & APIパターン** の利用を先に検討するのが良いかと思います[^2]。

[^1]: linkノードと同じ用途だと思います。linkノードは便利なのでNode-REDを始めた頃によく使うことになりがちですが、link先の設定が大変面倒なため得てして代替手段を模索するようになります。
[^2]: ただし、localhostへの通信を利用するとAWSやAzureなどのパブリッククラウド上で動的IPアドレスのインスタンスを利用した際に通信できない事象が発生する可能性があるので注意が必要です。

基本的にcontextはメモリ上の変数として扱われるためNode-REDプロセスを再起動すると値がクリアされます[^3]。ただし**フロー編集時に「デプロイ」を行っただけでは値がクリアされないことがある**ことに注意してください。

[^3]: v0.19以降ではcontextをファイルシステムに出力可能となったようです。（参考：[Context stores](https://nodered.org/docs/user-guide/context#context-stores)）

### 合わせて読みたい

- [Functionノードの書き方](https://nodered.jp/docs/writing-functions.html)
- [Working with messages](https://nodered.org/docs/user-guide/messages)
- [Working with context](https://nodered.org/docs/user-guide/context)

## デザインパターン

Node-REDはGUIベースのプログラムエディタ[^4]であり、一般的なテキストベースのプログラム作成とは異なるノウハウが多くあります。Node-REDを利用する上でこれらのデザインパターンを理解し、使いこなすことで圧倒的に生産性、メンテナンス性が変わってくると思います。

[^4]: 完全に余談ですが、筆者は昔Max/MSPという画像/音声編集のGUIプログラミングツールを利用していたことがあります。Max/MSPはGUIプログラミング環境として先駆者的存在だったのではないでしょうか。

### フロー作成の原則

Node-REDにおいてはプログラムがノードと線でビジュアル表現されるため、プログラムの内容を把握するのが容易です。この利点を生かすためには、まず原則として以下を意識してフローを記載するのが良いと考えています（あくまで筆者の主観です）。

- **極力基本のNodeを利用し、functionノードは必要最低限の利用とする**
- **1つのfunctionノードは1関数レベルの記述とし、行数を増やしすぎない**
- **フローの見た目にも出来る限りこだわり、ノード間のつながりをわかりやすくする**

### 基本：デザインパターン10選

上記の原則を踏まえた上で、特に不要なfunctionノードの増大を抑えるために [目からウロコ！Node-REDのデザインパターン10選](https://qiita.com/zuhito/items/e9abfd6f1ba188f908ed) は必ず目を通し、利用可能な場合はまずこのパターンを利用することを考えましょう。習うより慣れろな部分がかなり大きいと思います。

筆者は主にIoTのエッジ側でNode-REDを活用していますが、以下のデザインパターンをよく利用しています。

- **2. UI & APIパターン**
  - フロー間やタブ間でオブジェクトの受け渡しを行うためにAPIパターンを利用
- **4. Whileパターン**
  - 見た目にループがわかりやすくなることもあり、出来る限り利用
- **5. Sequenceパターン**
  - サブフローの処理中にオリジナルの値を退避させたい時などに利用
  - 一時的なテストフローやスタブ/ドライバなど書くときに利用すると便利
- **6. Aggregatorパターン**
  - 複数ファイルや複数APIの並列読み出しなどに利用

## TIPS

その他、細かいネタを列挙していきます。

### injectノードのCron処理

定期的にメッセージを飛ばして死活監視をしてみたり、負荷試験のために同時リクエストを発生させたりなど何かと重宝します。
injectノードのデフォルト機能で設定可能です。

<img width="860" alt="cron.png" src="https://qiita-image-store.s3.amazonaws.com/0/315383/f1226c28-ec69-fe72-4805-104d8790b43b.png">


### 初期化待ち

whileパターンの応用です。Global contextに初期化完了フラグを管理させることで、複数のタブの実行タイミングを制御します。
特にIoTでは 端末初期化 -> サイクル処理（センサデータ送信など）という実装パターンが多いため重宝しています。

<img width="860" alt="initialize.png" src="https://qiita-image-store.s3.amazonaws.com/0/315383/824b5cec-2041-db3f-1ae0-04f000fbf08b.png">


```json
[{"id":"fbe0c477.11aa08","type":"inject","z":"118622e7.d17d2d","name":"起動時に開始","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":140,"y":80,"wires":[["f624eb19.86f868"]]},{"id":"f624eb19.86f868","type":"change","z":"118622e7.d17d2d","name":"初期化フラグをfalse","rules":[{"t":"set","p":"initialize","pt":"global","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":80,"wires":[["f0751445.7208e8"]]},{"id":"9464c9d7.a9f8e8","type":"change","z":"118622e7.d17d2d","name":"初期化フラグをtrue","rules":[{"t":"set","p":"initialize","pt":"global","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":80,"wires":[[]]},{"id":"6a516d5c.a58714","type":"comment","z":"118622e7.d17d2d","name":"初期化処理","info":"","x":100,"y":40,"wires":[]},{"id":"e3224924.ee1fc8","type":"comment","z":"118622e7.d17d2d","name":"初期化後の処理","info":"","x":120,"y":160,"wires":[]},{"id":"1d4ecc18.87b6e4","type":"inject","z":"118622e7.d17d2d","name":"起動時に開始","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":140,"y":240,"wires":[["240129a3.f9bb56"]]},{"id":"240129a3.f9bb56","type":"switch","z":"118622e7.d17d2d","name":"初期化フラグがtrue？","property":"initalize","propertyType":"global","rules":[{"t":"false"},{"t":"true"}],"checkall":"true","repair":false,"outputs":2,"x":340,"y":240,"wires":[["2b6a264e.4ce2fa"],["8d5dc929.4e82b8"]]},{"id":"2b6a264e.4ce2fa","type":"delay","z":"118622e7.d17d2d","name":"10秒待ち","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":340,"y":180,"wires":[["240129a3.f9bb56"]]},{"id":"8d5dc929.4e82b8","type":"function","z":"118622e7.d17d2d","name":"初期化後の処理","func":"\nreturn msg;","outputs":1,"noerr":0,"x":560,"y":240,"wires":[[]]},{"id":"f0751445.7208e8","type":"function","z":"118622e7.d17d2d","name":"初期化処理","func":"\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":80,"wires":[["9464c9d7.a9f8e8"]]}]
```

### ロギング

作成中のフローのデバッグはdebugノードを利用するのが基本中の基本ですが、実運用においてはログレベルの設定やファイル出力を行いたくなってきます。こうした際にはfunctionノードでログ出力機能を記載するのが現実的かと思います。

これまでのNode-REDでは `node.log()` 、`node.warn()`、`node.error()` の3種類が利用可能でしたが、Node-RED v0.18.5 以降ではfunctionノード内で `node.debug()`、`node.trace()` を加えて利用可能になり、利便性が向上しました。
settings.jsでファイル出力するログレベルの制御なども可能なため、詳細ログは `node.trace()` などに寄せて必要なタイミングで取得することとし、必要な情報を `node.log()` や `node.error()` などで出力する、などが良いかと思います。

また、ログ出力を行う際にはノードの接続順が重要になるケースがあります。
以下の例では、 `[DEBUG] HTTP Request` のノードを先に接続した上で `POST実行`のノードを接続する形でノードを接続します。ここで接続順を逆にしてしまうと、HTTP RequestのログとHTTP Responseのログが逆転して出力されてしまう可能性が高くなります（Node-REDの仕様によります）。得てしてログ出力用のノードは最後に接続することが多いと思いますので事前に意識しておくと良いと思います。

<img width="860" alt="logging.png" src="https://qiita-image-store.s3.amazonaws.com/0/315383/ad07b3ab-c053-47a1-d9af-0455919e9810.png">


```json
[{"id":"3f1b1e93.8795c2","type":"inject","z":"e6262998.32b9e8","name":"起動時に開始","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":880,"wires":[["2371425.307b1be"]]},{"id":"b2ed2125.bb5dd","type":"http request","z":"e6262998.32b9e8","name":"POST実行","method":"GET","ret":"obj","url":"https://hogehoge.com/api","tls":"","x":490,"y":960,"wires":[["c32c552d.10eaf8"]]},{"id":"2371425.307b1be","type":"change","z":"e6262998.32b9e8","name":"APIパラメータ付与","rules":[{"t":"set","p":"headers","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"headers.content-type","pt":"msg","to":"application/json","tot":"str"},{"t":"set","p":"headers.Accept","pt":"msg","to":"application/json","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":880,"wires":[["c445800b.da7c4","b2ed2125.bb5dd"]]},{"id":"c32c552d.10eaf8","type":"function","z":"e6262998.32b9e8","name":"[DEBUG] HTTP Response","func":"node.log(JSON.stringify(msg));\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":960,"wires":[[]]},{"id":"c445800b.da7c4","type":"function","z":"e6262998.32b9e8","name":"[DEBUG] HTTP Request","func":"node.log(JSON.stringify(msg));\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":880,"wires":[[]]},{"id":"beb3b77c.da8e28","type":"comment","z":"e6262998.32b9e8","name":"ロギング","info":"","x":100,"y":840,"wires":[]},{"id":"a9c3e49b.98dd98","type":"comment","z":"e6262998.32b9e8","name":"↓ここを先に繋ぐ","info":"","x":500,"y":860,"wires":[]},{"id":"1a256f55.66e501","type":"comment","z":"e6262998.32b9e8","name":"←ここを後に繋ぐ","info":"","x":510,"y":920,"wires":[]}]
```

### file出力時の日本語文字化け対策

環境依存か確認していないですが、fileノードに日本語を含む文字列を渡すとファイル保存時に文字化けすることがあります。
文字列を`Buffer.from`でbufferクラスに変換した上でfileノードに渡してやるとこの問題を回避できます。

<img width="860" alt="mojibake-flow.png" src="https://qiita-image-store.s3.amazonaws.com/0/315383/7a0c2ca7-37d9-18d4-cbd7-80814b00f2c5.png">


```json
[{"id":"d5e2f01a.bc1e4","type":"file","z":"e6262998.32b9e8","name":"日本語ファイル出力","filename":"hoge.txt","appendNewline":true,"createDir":false,"overwriteFile":"true","x":480,"y":740,"wires":[["c27962b.45337a"]]},{"id":"94723481.a32548","type":"function","z":"e6262998.32b9e8","name":"文字化け対策","func":"msg.payload = Buffer.from( msg.payload, 'utf8' );\n\nreturn msg;","outputs":1,"noerr":0,"x":280,"y":740,"wires":[["d5e2f01a.bc1e4"]]},{"id":"402e9ce9.10cf54","type":"inject","z":"e6262998.32b9e8","name":"ほげほげ","topic":"","payload":"ほげほげ","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":740,"wires":[["94723481.a32548"]]},{"id":"fa51ea63.3823a8","type":"file","z":"e6262998.32b9e8","name":"日本語ファイル出力","filename":"hoge.txt","appendNewline":true,"createDir":false,"overwriteFile":"true","x":480,"y":660,"wires":[["3afe62e.b0b049e"]]},{"id":"11ffe757.5e1ad9","type":"inject","z":"e6262998.32b9e8","name":"ほげほげ","topic":"","payload":"ほげほげ","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":660,"wires":[["fa51ea63.3823a8"]]},{"id":"c27962b.45337a","type":"debug","z":"e6262998.32b9e8","name":"文字化けしない","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":680,"y":740,"wires":[]},{"id":"3afe62e.b0b049e","type":"debug","z":"e6262998.32b9e8","name":"文字化け","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":660,"y":660,"wires":[]},{"id":"6a0d9321.33de5c","type":"comment","z":"e6262998.32b9e8","name":"文字化けする","info":"","x":110,"y":620,"wires":[]},{"id":"75486517.be521c","type":"comment","z":"e6262998.32b9e8","name":"文字化けしない","info":"","x":120,"y":700,"wires":[]}]
```

## 参考資料

- [Node-RED User Group Japan ドキュメント](https://nodered.jp/docs/)
  - Node-REDの日本UGの方々による公式ドキュメントの翻訳。大変頼りになります。
- [Node-RED Document](https://nodered.org/docs/)
  - 本家のドキュメント（英語）。日本版にない記事などもそれなりにあるので、日本語ドキュメントでわからないことがあればこちらを確認すると良いかと思います。
- [Node-REDリリースノート](https://github.com/node-red/node-red/blob/master/CHANGELOG.md)
  - 頻繁に変更があるので折に触れて確認すると良いです。
- [目からウロコ！Node-REDのデザインパターン10選](https://qiita.com/zuhito/items/e9abfd6f1ba188f908ed)
  - 珠玉の名作。絶対に読みましょう。