Node-REDを使ってみる その5(センサーなど複数の入力を扱うフローについての考察)
はじめに
今回はNode-REDでのセンサー値の扱いについて知り合いの方から質問を頂いたので、私の実装方法について書きまとめてみました。
各ノードの解説記事はあっても、実際にどのようなフローを作成するかはあまり情報が無いような印象です。LLMでも変なフローを作成したりするので、どうやって学習させようか考えていたりします。
▼以前の記事はこちら
挙動の確認
functionノード
計算はfunctionノードを使います。
▼中にJavaScriptのコードを記述できます。

injectノードにNumber型、String型の数字を入れて実行してみました。
▼問題なく実行できました。

[{"id":"81a12d3049d365d4","type":"function","z":"41ad2b666875c180","name":"Divide 2","func":"msg.payload = msg.payload / 2;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1580,"y":320,"wires":[["b2165721a63384e5"]]},{"id":"ccf337e61eda628a","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"14","payloadType":"num","x":1410,"y":320,"wires":[["81a12d3049d365d4"]]},{"id":"f774e275edc23f99","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"15","payloadType":"num","x":1410,"y":360,"wires":[["81a12d3049d365d4"]]},{"id":"6f084383639b8554","type":"inject","z":"41ad2b666875c180","name":"String: 15","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"15","payloadType":"str","x":1400,"y":400,"wires":[["81a12d3049d365d4"]]},{"id":"b2165721a63384e5","type":"debug","z":"41ad2b666875c180","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1740,"y":320,"wires":[]}]
JavaScriptは動的型付けなので、これでも実行できます。もしStringでもNumberで扱われているのか心配であれば、Number(value)の形式で書いておくとよいでしょう。ノードを開発するときはNumberで扱うようにしないとエラーが起きたことがありました。
▼JavaScriptのNumberについてはこちら
injectノードやchangeノードでmsg.payload以外に、msg.payload.a、msg.payload.bに代入しても計算できます。
▼msg.payload.aとmsg.payload.bで割り算をするフローを作成してみました。これでも問題なく実行できます。

[{"id":"55043a16b675521b","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload.a","v":"14","vt":"num"},{"p":"payload.b","v":"2","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1410,"y":500,"wires":[["55be71db93b9db6f"]]},{"id":"55be71db93b9db6f","type":"function","z":"41ad2b666875c180","name":"Divide a/b","func":"msg.payload = msg.payload.a / msg.payload.b;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1560,"y":500,"wires":[["0f7f73fd3546014a"]]},{"id":"0f7f73fd3546014a","type":"debug","z":"41ad2b666875c180","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1720,"y":500,"wires":[]},{"id":"c0dec9c557a75115","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"payload.a","pt":"msg","to":"15","tot":"num"},{"t":"set","p":"payload.b","pt":"msg","to":"2","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1460,"y":560,"wires":[["55be71db93b9db6f"]]},{"id":"de9c343ebe00883f","type":"inject","z":"41ad2b666875c180","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1290,"y":560,"wires":[["c0dec9c557a75115"]]}]



joinノード
これまでは入力が1つのinjectノードでしたが、複数のセンサーの値を扱う場合は入力も複数になるかと思います。しかし、msgオブジェクトにmsg.payload.aとmsg.payload.bの両方が代入されていないと、NaN(Not a Number)になります。
▼例えば以下のフローだと、msg.payload.aは代入されているものの、msg.payload.bには代入されていないのでNaNになります。

[{"id":"c14cca7a6b3692c4","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"14","payloadType":"num","x":2110,"y":340,"wires":[["bceea4bdcd5008d2"]]},{"id":"91c990cdec406220","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"15","payloadType":"num","x":2110,"y":400,"wires":[["bb7dd50e22adef24"]]},{"id":"606146c6d71e76b0","type":"debug","z":"41ad2b666875c180","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2640,"y":340,"wires":[]},{"id":"bceea4bdcd5008d2","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"payload.a","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2290,"y":340,"wires":[["df0850302095ab30"]]},{"id":"df0850302095ab30","type":"function","z":"41ad2b666875c180","name":"Divide a/b","func":"msg.payload = msg.payload.a / msg.payload.b;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2480,"y":340,"wires":[["606146c6d71e76b0"]]},{"id":"bb7dd50e22adef24","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"payload.b","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2290,"y":400,"wires":[["df0850302095ab30"]]}]
joinノードを利用すると、入力が集まってから出力できます。
▼入力が別々でもNaNにならず実行できます。

[{"id":"c14cca7a6b3692c4","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"a","payload":"14","payloadType":"num","x":2330,"y":340,"wires":[["5c9dd685597d2255"]]},{"id":"91c990cdec406220","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"b","payload":"15","payloadType":"num","x":2330,"y":400,"wires":[["5c9dd685597d2255"]]},{"id":"606146c6d71e76b0","type":"debug","z":"41ad2b666875c180","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2780,"y":340,"wires":[]},{"id":"df0850302095ab30","type":"function","z":"41ad2b666875c180","name":"Divide a/b","func":"msg.payload = msg.payload.a / msg.payload.b;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2620,"y":340,"wires":[["606146c6d71e76b0"]]},{"id":"7ce2a1550d7e61a2","type":"debug","z":"41ad2b666875c180","name":"debug 7","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2620,"y":300,"wires":[]},{"id":"5c9dd685597d2255","type":"join","z":"41ad2b666875c180","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","useparts":false,"accumulate":true,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":2470,"y":340,"wires":[["df0850302095ab30","7ce2a1550d7e61a2"]]}]
▼injectノードでmsg.topicをa、bとして設定し、joinノードでmsg.topicをキーとして2つメッセージが届いてから次のメッセージを送信するようにしています。


フローの実装案
joinノードで複数のセンサー値をまとめてもいいのですが、私はセンサーの値をflowオブジェクトに代入しておいて、必要に応じてその値を取得するような実装をよくしています。
線でつながった部分で流れていくmsgオブジェクトとは異なり、flowオブジェクトは同一フロー内で共有されます。globalオブジェクトだとフローを跨いで共有されます。
例として以下のフローを作成してみました。
▼flow.sensorAとflow.sensorBとして代入し、templateノードでその値を取得します。

[{"id":"b17e05313f3ecb58","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"sensorA","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1580,"y":720,"wires":[[]]},{"id":"02a256a04c16dfaf","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"14","payloadType":"num","x":1410,"y":720,"wires":[["b17e05313f3ecb58"]]},{"id":"96719824dd37d600","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"sensorB","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1580,"y":760,"wires":[[]]},{"id":"16552d3e71c356be","type":"inject","z":"41ad2b666875c180","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"2","payloadType":"num","x":1410,"y":760,"wires":[["96719824dd37d600"]]},{"id":"6feb7e52bd055d31","type":"template","z":"41ad2b666875c180","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"Sensor A\": {{flow.sensorA}},\n \"Sensor B\": {{flow.sensorB}}\n}","output":"json","x":1560,"y":820,"wires":[["f35135602422f184","67bfe78ed045f646","5ed2f073c0e84640"]]},{"id":"acc8094a45df4cf8","type":"inject","z":"41ad2b666875c180","name":"","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":1410,"y":820,"wires":[["6feb7e52bd055d31"]]},{"id":"f35135602422f184","type":"debug","z":"41ad2b666875c180","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1720,"y":820,"wires":[]},{"id":"67bfe78ed045f646","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[\"Sensor A\"]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1740,"y":860,"wires":[["25dad2e4c3548d0d"]]},{"id":"5ed2f073c0e84640","type":"change","z":"41ad2b666875c180","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[\"Sensor B\"]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1740,"y":900,"wires":[["479b43c3bac73f40"]]},{"id":"25dad2e4c3548d0d","type":"ui_chart","z":"41ad2b666875c180","name":"","group":"879bd7fc7e4afe72","order":0,"width":0,"height":0,"label":"Sensor A","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":1920,"y":860,"wires":[[]]},{"id":"479b43c3bac73f40","type":"ui_chart","z":"41ad2b666875c180","name":"","group":"879bd7fc7e4afe72","order":0,"width":0,"height":0,"label":"Sensor B","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":1920,"y":900,"wires":[[]]},{"id":"879bd7fc7e4afe72","type":"ui_group","name":"Default","tab":"b1e7631a49932fee","order":1,"disp":true,"width":6,"collapse":false,"className":""},{"id":"b1e7631a49932fee","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]
▼templateノードでJSON形式でパースして出力しています。

このように組んでおくと、後でセンサーの値を監視するのにdashboardノードと繋いだりするのが容易になります。
▼今回は値の変化が無いですが、このように可視化されます。

過去の記事でロボットアームの制御を行っていたときは、flowオブジェクトとdashboardノード、templateノードを多用していました。
▼スライダーで各軸の値を制御していました。

もちろん他にも実装方法はあるかと思いますが、joinノードを使ってまとめるよりも実装しやすいのではないかと思います。
最後に
個人的にflowオブジェクトとtemplateノードを使うようになって、Node-REDで開発するのが楽になったように感じます。一連のノード間での変数のやり取りを意識しがちですが、ノードを跨いで変数をやり取りすることもできるのです。UIでのセンサー値の監視などに簡単につなげていくことができます。
最近作ったNode-RED用のLLM Pluginで、論理的なフローの作成ができるといいなと思っています。
▼Node-REDのサイドバーでLLMとやり取りして、提案されたフローをインポートできるようにはしました。また別の記事にまとめます。