Node-REDのABC

この章では、R-CPSシステムを構築することに特化注目し、必要な機能について解説します。実事例は各ページでも紹介しているので合わせてお読みください。


データの受信

センサ等の機器からのデータのほとんどは、シリアル通信ポートを介して取り込みます。つまり、ハードウエアの状況に応じてシリアル通信用のノードを置けばその後は、ハードウエアを気にすることなく、データフローを構築することができます。

例えば、OSがLinux系(Ubuntu)ならば、通信ノードの中で/dev/ttyUSB0 の様に割り当てれば良く、OSがWindowsならば、COM6の様に割り当てれば、その後のフローは全く同じように構築することが可能です。

データは連続した文字列として送られてくることを基本として想定しています。また、データの最後(文字列の最後には)、一般的にCR(Carriage Return)復帰、LF(Line Feed) 改行、が追加されてきます。データの区切りで読み込みエラーになる場合は、念のためこの点を確認してください。

 

Bluetoothや有線(USB Serial)のように、異なる接続手段でデータを受信する場合でも、Serial inノードの設定でデータリンクが確立すれば後段の処理は同様に行うことができます。


データの表示と入力

Node-REDには、ダッシュボードと言うデータを画面表示する機能があります。また、画面に配置されたボタンやスライダーを使って値を取り込むことができます。


データの送信

データの受信と同様、データを送信する方法を説明します。基本的には、データ受信と同様、通信ポートの割り当て、通信速度等の設定を行い、フローの最後に繋げれば完了です。

・Serial Out ノード

シリアル通信でデータを送出します。シリアル通信で接続された機器に合わせて、入力の分割方法や区切り文字を設定してください。R-MSMでは「\n」を区切り文字として使用しています。機器によっては、文字列の最後が見つけられずに、受信待ちのままで止まってしまったり、送信コマンドが正しく区切られず誤動作する等のトラブルにつながります。

・Web Socket Out, in ノード

Web Socketを使用してデータを送出します。

websocketを使ったフローの例を上に示します。説明のために、同じシート上に記載していますが、それぞれ、サーバー側のフローはサーバー側のNode-REDのフローとして記載、クライアント側のフローはクライアント側のNode-RED上に記載してください。

①②は、サーバー側に準備されたデータを、クライアント側から接続し、データーを受け取りに行く場合です。従って、サーバー側①は下の図左のように、「待ち受け」にしてパスを指定します。クライアント側②は下の図右のように、「接続」として、サーバーのURL(WS://で始める)、ポート(1880)、パス(①に合わせます)を指定します。

   

③④は、クライアント側③から接続し、サーバー側④へデータを送る場合です。なので、③は接続、④は待ち受け、と設定します。

 

・MQTT Out, inノード

MQTTを使用してデータを送受信します。MQTTの通信ではbrokerを経由してpublish/subscribe で1:nの通信が可能です。
例えば、ある場所のセンサデータをpublishしておいて、複数の場所でそのデータをsubscribeするといった使い方ができます。

MQTTの通信では、brokerへの接続にユーザIDとパスワードを設定することができます。broker接続にユーザIDとパスワードが必要な場合には、サーバの設定画面(上記図の鉛筆マークをクリック)のセキュリティタブで設定してください。

MQTTについてはこちらでも詳しく解説しています。

他にも様々なデータ送出手段がありますので、用途に応じて利用してください。


データの書式設定とデータの操作

1. JSON(JavaScript Object Notation)形式

R-CPSで扱うデータの書式はJSON(JavaScript Object Notation)を基本として、オブジェクト(一つの塊)として取り扱います。データを数値としてだけで扱うのではなく、名前、数値、単位の3要素をひとくくりにして扱います。

2. オブジェクト(Object)データの操作(取出し/追加/削除)

上記のように、JSON形式のデータは、Node-RED内部ではObjectとして扱われます。そしてノード間をメッセージとしてオブジェクトデータが伝えられます。そのため、どのようなデータを次段のノードに送るかは,とても重要です。

ここでは、オブジェクトデータの取出し/追加/削除に関して簡単に説明します。以下のようなR-MSMに搭載している9軸モーションセンサBMX160の出力を例にして説明します。この表記は、JSON形式です。”device”, “id”, “sensor”, “timestamp”, “acc”, “gyro”, “mag”は、同じ階層のデータです。”acc”, “gyro”, “mag”は、もう一つ下の階層”val”を持っています。そして、”val”は配列(array)データを持っています。

{
    "device": "R-MSM",
    "id": "0000",
    "sensor": "BMX160",
    "timestamp": 478497.65,
    "acc": {
        "val": [
            0.411,
            -4.178,
            -8.85
        ]
    },
    "gyro": {
        "val": [
            2.744,
            -5.091,
            -4.39
        ]
    },
    "mag": {
        "val": [
            -48,
            -69,
            -177
        ]
    }
}
2-1. オブジェクトデータの確認

上記のJSONデータをinjectノードとdebugノードでobject形式に直してみます。Node-REDのフローエディタに以下の様に配置して接続します。

injectノードのプロパティ画面に、上記のJSON形式データを貼り付けてください。
「デプロイ」して、injectノードのボタンを押すと、デバッグ・サイドバーにobujetに変換されたデータが表示されます。

デバッグ・サイドバーで折りたたまれたデータを伸ばすと次のようになります。
  ①:msg.payloadは、object形式である。objectは、”device”, “id”, “sensor”, “timestamp”, “acc”, “gyro”, “mag”で構成されている。
  ②:acc も、object形式である。
  ③:accのobjectの構成要素は、“val”である。”val”は3つの要素を持つ配列 array[3]である。
  ④:配列の要素は、val[0]=0.411, val[1]=-4.178, val[2]=-8.85である。

それでは、以下で、このデータを使って、データの操作(取出し/追加/削除)をしてみます。

2-2. オブジェクトデータの取出し

オブジェクトデータの取出しは、このページの一番最初の章「データの受信」でも説明しています。

ここでは、配列データの取出しをしてみたいと思います。magとgyaro[2]を取り出してみます。
取出しには、最初の章の説明と同じくchangeノードを使います。
上のNode-REDのフローに2つchangeノードを追加して、「デプロイ」後、injectノードのボタンを押します。

デバッグ・サイドバーに、magの値とgyroのval[2]の値が表示されています。
それぞれのchageノードの記述は以下の様になります。changeノードの4つの機能のうち「値の代入」と「値の移動」を使って記述しました。
説明のために、別の機能を使いましたが、どちらか一方だけ使ってもOKです。「値の代入」と「値の移動」の違いは、元のデータが残っているか消えるかの違いです。もう一つは、データの移動の向きが異なります。「値の代入」は、「下から上」です。「値の移動」は、「上から下」です。
ここでは、msg.payloadに入れて、debagノードで表示しています。

もう一つの注目ポイントは、階層の表現方法です。magは、“msg.payload.mag”、gyroのval[2]は、“msg.payload.gyro.val[2]”のように、階層を“.”で記述します。

2-3. オブジェクトデータへの追加

2-1で説明したオブジェクトデータに単位を追加してみます。
具体的には、accには、(“unit”:”g”}を、gyroには、{“unit”:“deg/s”}を、magには、{“unit”:”uT”}を追加します。
追加もchangeノードの代入の機能を使います。
以下のフローのように、changeノードを追加して、「デプロイ」後、injectノードのボタン押します。
結果、デバッグ・サイドバーに、単位が追加されたデータが表示されます。

changeノードのプロパティ画面を見てみます。
acc, gyro, magのobjectとして追加しますので、それぞれの階層に下の”unit”に単位を代入しています。
chageノードのルールは複数持つことができます。左下の「+追加」ボタンを押すことでルールが追加されます。
それぞれのルールは独立していますので、異なる処理を入れることもできます。また、上から順番に処理されますので、上で処理した結果を下で利用することもできます。

2-4. オブジェクトデータの削除

2-1で説明したオブジェクトデータの一部を削除してみます。
具体的には、”id”と”timestamp”を削除してみます。

削除もchangeノードを使って行います。
下のようなフローを作成します。changeノードのプロパティ画面には、”msg.payload.id”と”msg.payload.timestamp”を削除するルールを記述します。
記述後、「デプロイ」を実行して、injectノードのボタンを押します。
結果、”id”と”timestamp”が無いデータが表示されます。

objectを指定するとその下のデータすべてが消えます。
例えば、「値の削除」で”msg.payload.acc”を指定すると、
下位の“val”のarrayすべてが消えます。

 


数値計算

取り込んだデータの計算です。たとえば、取り込んだ観測データにオフセットを加えたり、単位変換を行う場合に使います。

計算には様々な方法がありますが、例として下記のようなノードで計算処理を行うことができます。
こういったノードを単独、または組み合わせて目的の処理を実装します。

1.Changeノードを使用した温度データの変更

changeノード内でJSONataを選択し、計算式を入れます。この事例では、温度センサの値を示す「payload.temp.val」に10を加算しています。フローを実行すると、センサの元データに対して10加算されたデータが得られています。

 

2.function ノードを使用して温度センサの値を補正する

Functionノードを使用して、Javascriptコードで温度センサの値を補正しています。データフォーマットは維持されている例です。
Javascriptで記述できるので融通は利くのですが、補正値を変更する時にコードを修正するのに抵抗のある場合が考えられますので、このケースではfunctionノードの手前にchangeノードを配置し、「補正値」のみを設定し、functionノード内でその情報を使って温度補正しています。

3.calculatorノードを使用して様々な計算をする

calculatorノードは標準のインストールでは含まれていませんので、「パレットの設定/ノードの追加」で「node-red-contrib-calc」を検索して追加してください。
calculatorノードでは様々な関数が利用できますが、前段に配列処理を置く必要があります。functionノードで配列の操作を行い、calcノードの入力に接続するようにします。

・移動平均の計算

この事例では、移動平均を計算するフローを組んでいます。
加速度センサのX軸データの値を抽出して5つの配列にシフトしながら順に格納し、calcノードで「Avarage」の関数をセットして平均値を求めています。
計算後に改めて加速度センサのX軸データが送られてくると、配列をシフトして新しいデータを格納し、その配列の内容で改めて平均値を求めて出力します。

・こういった計算はFunctionノードの内部だけで処理することも可能ですが、配列操作とcalcノードの関数を別に配置することで違う関数を適用する場合に変更しやすいので、このようにしています。他の目的で計算が必要な時に、ターゲットシステムに合わせて変更してお試しください。

/* Node-RED flow --moving average */[{"id":"70619e0.050a864","type":"function","z":"2fc4596c.812606","name":"配列操作","func":"if ( !context.array ) {\n context.array = new Array (5);}\ncontext.array.shift();\ncontext.array.push(msg.payload);\nmsg.payload.length = context.array.length;\nmsg.payload = context.array;\nreturn msg;","outputs":1,"noerr":0,"x":460,"y":140,"wires":[["a6dbd477.00d388"]]},{"id":"9a8e54fb.793b48","type":"debug","z":"2fc4596c.812606","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":790,"y":140,"wires":[]},{"id":"a6dbd477.00d388","type":"calculator","z":"2fc4596c.812606","name":"Average","inputMsgField":"payload","outputMsgField":"payload","operation":"avg","constant":"","round":false,"decimals":0,"x":620,"y":140,"wires":[["9a8e54fb.793b48"]]},{"id":"b69ea88c.9ac218","type":"change","z":"2fc4596c.812606","name":"データ抽出","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.acc.val[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":290,"y":140,"wires":[["70619e0.050a864"]]}]
/* 配列操作ノード */if ( !context.array ) { context.array = new Array (5);} context.array.shift(); context.array.push(msg.payload); msg.payload = context.array; return msg;
・メディアンフィルタの計算

上記の移動平均の事例のcalculationノードにソートの関数を設定してchangeノードを組み合わせると、メディアンフィルタの計算もできます。メディアンフィルタは直近のn個のデータを昇順または降順にソートし、その中央の値を出力します。連続して計測している中でたまたま異常値が計測されたような場合でも、このメディアンフィルタを使用すれば異常値を取り除くことができます。一つの出力を得るのにn個のセンサデータを計測する必要がありますが、安定したデータが必要な場合には有効な手段です。

センサからのデータを移動平均の例と同様に配列に格納しますが、次のcalculationノードで「Sort ascending (昇順ソート)」を設定します。ここまでで入力された5個のセンサデータは昇順に並び替えられますので、次に配置したchangeノードで並び替えられた配列の中央にあたるmsg.payload[2]を抽出して出力とします。

/* Node-RED flow -- median filter */
[{"id":"73cabe4d.88cf4","type":"function","z":"2fc4596c.812606","name":"配列操作","func":"if ( !context.array ) {\n context.array = new Array (5);}\ncontext.array.shift();\ncontext.array.push(msg.payload);\nmsg.payload.length = context.array.length;\nmsg.payload = context.array;\nreturn msg;","outputs":1,"noerr":0,"x":440,"y":440,"wires":[["8c6eac0.cce4b58","1fee9cde.a60ce3"]]},{"id":"52ae13ee.55281c","type":"debug","z":"2fc4596c.812606","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":790,"y":400,"wires":[]},{"id":"8c6eac0.cce4b58","type":"calculator","z":"2fc4596c.812606","name":"","inputMsgField":"payload","outputMsgField":"payload","operation":"sorta","constant":"","round":false,"decimals":0,"x":600,"y":440,"wires":[["52ae13ee.55281c","684cb333.ea936c"]]},{"id":"62dc0f47.01a79","type":"change","z":"2fc4596c.812606","name":"データ抽出","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.acc.val[0]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":290,"y":440,"wires":[["73cabe4d.88cf4"]]},{"id":"684cb333.ea936c","type":"change","z":"2fc4596c.812606","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload[2]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":440,"wires":[["74a9e1ae.33632"]]},{"id":"74a9e1ae.33632","type":"debug","z":"2fc4596c.812606","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":810,"y":500,"wires":[]},{"id":"1fee9cde.a60ce3","type":"debug","z":"2fc4596c.812606","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":590,"y":400,"wires":[]}]

 

統計処理

・標準偏差を求める

この手法の応用で、配列にデータを格納することで統計処理を適用しやすくなります。
例えば、分散や標準偏差を求めたい場合は同様に配列にデータを格納して次のような処理を行います。
各ステップで計算内容をmsg.xxxに追加していますが、最終的にデバッグノードでmsg.全体を表示すると、計算対象のデータを格納した配列と、合計・平均・分散・標準偏差の値を格納しているのを見ることができます。後段での処理や表示に使う場合はchangeノードなどを使って適切な処理を加えてください。

/* Node-RED flow -- standard deviation */
[{"id":"12c31cce.d3da33","type":"function","z":"b38b7342.e8ca4","name":"平均","func":"let sum = 0, average = 0;\nfor (i=0; i<msg.payload.length; i++) {\n sum += msg.payload[i];\n}\nmsg.sum = sum;\nmsg.average = msg.sum / msg.payload.length;\nreturn msg;\n","outputs":1,"noerr":0,"x":530,"y":80,"wires":[["59e4e6c2.15c2c8","7740fa42.ffa324"]]},{"id":"59e4e6c2.15c2c8","type":"function","z":"b38b7342.e8ca4","name":"分散","func":"let variance = 0;\nfor (i=0; i<msg.payload.length; i++) {\n variance += Math.pow(msg.payload[i] - msg.average, 2);\n}\nmsg.variance = variance / msg.payload.length;\nreturn msg;\n","outputs":1,"noerr":0,"x":670,"y":80,"wires":[["d419a5a2.34baf8","84bafbd6.8926d8"]]},{"id":"d419a5a2.34baf8","type":"function","z":"b38b7342.e8ca4","name":"標準偏差","func":"msg.std_dev = Math.sqrt(msg.variance);\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":80,"wires":[["52bea1.297ee16"]]},{"id":"449cff9a.813f6","type":"function","z":"b38b7342.e8ca4","name":"配列操作","func":"if ( !context.array ) {\n context.array = new Array (5);\n}\ncontext.array.shift();\ncontext.array.push(msg.payload);\nmsg.payload = context.array;\nreturn msg;\n","outputs":1,"noerr":0,"x":400,"y":80,"wires":[["12c31cce.d3da33"]]},{"id":"52bea1.297ee16","type":"debug","z":"b38b7342.e8ca4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":970,"y":80,"wires":[]}]
/* 配列操作ノード */
if ( !context.array ) {
context.array = new Array (5);
}
context.array.shift();
context.array.push(msg.payload);
msg.payload = context.array;
return msg;
/* 平均ノード */
let sum = 0, average = 0;
for (i=0; i<msg.payload.length; i++) {
sum += msg.payload[i];
}
msg.sum = sum;
msg.average = msg.sum / msg.payload.length;
return msg;
/* 分散ノード */
let variance = 0;
for (i=0; i<msg.payload.length; i++) {
variance += Math.pow(msg.payload[i] - msg.average, 2);
}
msg.variance = variance / msg.payload.length;
return msg;
/* 標準偏差ノード */
msg.std_dev = Math.sqrt(msg.variance);
return msg;

データの結合と分離

R-CPSでは、複数のデータ源を扱うことが必要になります。例えば、下図左のように、ポートAからは1秒間隔、ポートBからは2秒間隔で、データがPDHに送られてくるとします。

この場合、結合するパターンは大別して、①データの更新頻度の高いAのデータを基準に結合する場合、②データの更新頻度の低いBのデータを基準に結合する場合、③AともBとも異なるタイミングで結合する場合、の3通りが考えられます。

手順を説明します。タイミング調整に先立ち、データ保持のためにバッファを設けて、データ源から送られてきた都度、バッファに書き込みその内容を更新しします。その後、①では、①のタイミングを利用して、データ結合し、次節に送ります。①では、①のタイミングを利用します。②では、②のタイミングを利用します。③では、別途設けたタイミング(トリガ)に合わせて結合します。

将来的にデータ源を増やすことを想定すると、③の方法をお勧めします。

それでは実際にフローを動かして確認していきましょう。この事例では、2つのセンサデータを結合する時の書式としてmsg.payload内のObject内に、sensor_Aとsensor_BのObjectをすっぽり格納してしまいます。この方法では、それぞれのセンサでデータを取得したときの情報は失われません。


まず、「センサAのタイミング」でデータを結合してセンサA,センサBのデータを後段に送るフローです。
ここでは、センサAのタイミングに合わせるためにセンサBのデータをflow.sensor_Bに一時格納します。flow.sensor_Bの値は、センサBが動作するごとに更新されます。
そして、センサAの後段にfunctionノードを設けて、ここで先ほどの書式に合わせてデータをマージします。
デバッグノードで出力タイミングを確認すると、この事例でセットしたセンサAの駆動タイミングである1秒ごとにデータが出力されています。
(デバッグノードの表示を開くと、上の図のようなsensor_Aのデータ、sensor_Bのデータが確認できます。)

次に、「センサBのタイミング」での駆動フローです。上記同様に、センサBのタイミングに合わせるためにセンサAのデータはflow.sensor_Aに一時保存され、センサAの動作するタイミングで更新されていきます。センサBの後段に設けたfunctionノードで一時保存されたセンサAのデータを読み込んでマージします。
デバッグノードで出力タイミングを確認すると、この事例でセットしたセンサBの駆動タイミングである3秒ごとにデータが出力されています。

最後に、センサAでもセンサBでもなく、別のタイミングで出力する場合です。この場合はセンサAのデータもセンサBのデータも一時保存し、それぞれのセンサのタイミングで更新されています。事例では、timebaseという5秒間隔で駆動するノードを使用して、センサAとセンサBのデータをマージして出力しています。
デバッグノードで出力タイミングを確認すると、この事例でセットしたtime baseの駆動タイミングである5秒ごとにデータが出力されています。

*注意点*
このように2つのセンサデータをマージした場合は、R-CPSの標準で定めたデータフォーマットから外れることになりますので、この後段の処理で格納されたデータを使用する場合は各センサオブジェクト内のキーや値を正しく取得できるように各ノードの設定を変更し、処理フロー上でR-CPS標準のデータフォーマットで格納されているデータと混在しないように注意してください。もし混在させる必要がある場合は、改めて必要とするセンサデータのオブジェクトを抜き出してmsg.payloadに格納しなおしてから合流させてください。そうでないとswitchノードでの分岐やchangeノード、functionノードなどのデータ処理でkeyが見つからないためエラーとなる場合があります。

 

これ以外に、普通の利用では非同期でデータを処理するため、シンプルに、「あるノード」にワイヤで結合します。
合流した後、通信手段を利用して別の場所に送信するような用途の時に利用します。

他から送られたデータを受信した場合は、パーサを使用してObjectに変換します。そのあと、Switchノードを使用してセンサデータに含まれるキー値(デバイスid、センサ名、データ名など)で分離し、利用するフローを組み立てます。


データ流量の制限

受信したデータの頻度に関係なく、PDHからデータを間引いて後段(例えばエッジサーバー)に送りたい場合や、処理負荷の重い表示処理にデータの流量を制限したいことがあります。ダッシュボード表示する際のデータの間引き方法を例にとって説明します。

以下のフローは実際に評価キットの温度湿度気圧などの表示に使われているものです。各種のダッシュボードノードの手前、センサデータが送られてくるところにdelayノードを配置しています。このdelayノードの動作をプルダウンメニューから設定することでデータの流量制限を行う事ができます。流量設定で必要なレートにして、このケースでは中間メッセージは不要となるので削除する設定とします。このノードを通すことで流量をコントロールすることができます。

 


 

打刻とハードウエアIDの挿入

取り込むデータは測定値だけを持ち、時刻を持たないものが多くあります。そこで、PDHに到達した時刻を測定した時刻と定義し、受信したデータにPDHの時計の時刻を追加します。事前にPDHは上位のサーバーにと時刻同期させておくことが必須です。

また、複数の同じ仕様のセンサを扱う場合など、データ源を明確にすることが必要です。データ源を明確にするために、ハードウエアのIDも時刻と同様にデータに入れ込みます。ハードウェアのIDはセンサモジュールからデータが送られてきたときに含まれていますので、複数のセンサモジュールを接続している場合はこのIDを元に処理を振り分けてください。

 

Ou

“Output to”の欄は、”msg.payload.datetime”のように、msg.payloadの下位の階層に割り当てて、前段から送られてきたmsg.payloadのobjectに追加するようにしてください。msg.payloadとすると前段のデータを置き換えられてしまいます。


データの保存と読み込み

CSV形式、JSON形式でファイルとしてデータを保存する方法・ファイルを読み込む方法について解説します。

初めに、注意点が1つあります。
それは、ファイルはNode-REDが実行されているマシン(サーバ, PDH etc)に保存される、もしくは読み込まれるということです。ですので、fileノードに指定するパスとファイル名は、Node-REDが実行されているマシンのパスとファイル名を記載ください。
具体的には、ブラウザでフローエディタを起動する際にブラウザに入力するURL”http://(Node-RED起動マシンのIPアドレス):1880/”の(Node-RED起動マシンのIPアドレス)のマシンに保存されるか、マシンから読み込まれます。
”http://localhost:1880/”の場合には、Node-REDを実行しているマシンとフローエディタを起動しているマシンが同じですので、フローエディタを起動しているマシンに保存され/読み込まれます。

1. 送られてきたデータをファイルに逐次書き出す場合(JSON形式)

Node-REDでデータをファイルに保存する場合、fileノードを使用します。
fileノード自体にパスと保存ファイル名を設定して送られてきたデータを保存する方法と、fileノードにはファイル名を指定せず送られてきたデータのmsg.filenameにパスと保存ファイル名を追加してデータと一緒にfileノードに送る方法があります。


R-CPSの評価キットでは、後者の方法を使用して「記録を開始した際の日付時刻」をファイル名の一部に含めてデータを保存しています。
・この処理のために、データの記録ボタンが押された時に日付時刻を含むファイル名を作成する方法を見ていきましょう。

injectノード「rec_start」を押すと、ファイル名に付加する日付時刻を取得します。次のchangeノードではその日付時刻を使用してjsonで記録する際のファイル名を作成し、flow.json_filenameに一時保存します。更に、次のchangeノードで記録状態である事をflow.rec_enableに一時保存します。
このフローを動かしたときに、デバッグノードを配置している場所では「記録が開始されたこと」を示すメッセージと、ファイル名が得られます。

次に、ファイル記録停止のフローを作成します。injectノード「rec_stop」を押すと、startの時にtrueにセットしたflow.rec_enableをfalseにします。

これで日付時刻を含むファイル名と、記録開始・停止の操作が準備できましたので、実際にファイル記録を行うフローを作成します。
これまでに作ったstartとstopのフローも合わせると、以下のような関係で動作します。

/* Node-RED flow -- file recording */
[{"id":"a1a47242.ae78e","type":"inject","z":"9f9cfeee.b231","name":"","topic":"","payload":"rec_start","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":280,"wires":[["c5ea6dab.2e04c"]]},{"id":"eb87bab8.7aa628","type":"change","z":"9f9cfeee.b231","name":"ファイル情報作成","rules":[{"t":"set","p":"filepath","pt":"msg","to":"C:\\Users\\USER\\Desktop\\data\\","tot":"str"},{"t":"set","p":"fileheader","pt":"msg","to":"R-MSM_","tot":"str"},{"t":"set","p":"json_ext","pt":"msg","to":".json","tot":"str"},{"t":"set","p":"json_filename","pt":"flow","to":"msg.filepath&msg.fileheader&msg.datetime&msg.json_ext","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":280,"wires":[["2294eba0.e81f94"]]},{"id":"2294eba0.e81f94","type":"change","z":"9f9cfeee.b231","name":"記録開始処理","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"R-MSM\":\"rec_start\"}","tot":"json"},{"t":"set","p":"rec_enable","pt":"flow","to":"true","tot":"bool"},{"t":"set","p":"filename","pt":"msg","to":"json_filename","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":280,"wires":[["aa246098.baf17","71d55a80.860d64"]]},{"id":"aa246098.baf17","type":"debug","z":"9f9cfeee.b231","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":830,"y":280,"wires":[]},{"id":"c5ea6dab.2e04c","type":"moment","z":"9f9cfeee.b231","name":"時刻取得","topic":"","input":"","inputType":"date","inTz":"Asia/Tokyo","adjAmount":0,"adjType":"days","adjDir":"add","format":"YYYY-MM-DDTHH-mm-ss","locale":"ja-JP","output":"datetime","outputType":"msg","outTz":"Asia/Tokyo","x":300,"y":280,"wires":[["eb87bab8.7aa628"]]},{"id":"71d55a80.860d64","type":"debug","z":"9f9cfeee.b231","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"filename","targetType":"msg","x":830,"y":320,"wires":[]},{"id":"f012bfe7.7402d","type":"change","z":"9f9cfeee.b231","name":"","rules":[{"t":"set","p":"filename","pt":"msg","to":"json_filename","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":460,"wires":[["302da884.962428"]]},{"id":"e48d6145.09a4a","type":"inject","z":"9f9cfeee.b231","name":"","topic":"","payload":"rec_stop","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":160,"y":340,"wires":[["c09fe12c.01a13"]]},{"id":"c09fe12c.01a13","type":"change","z":"9f9cfeee.b231","name":"記録停止処理","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"R-MSM\":\"rec_stop\"}","tot":"json"},{"t":"set","p":"rec_enable","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":340,"wires":[["8d2e0996.96a318"]]},{"id":"8d2e0996.96a318","type":"debug","z":"9f9cfeee.b231","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":490,"y":340,"wires":[]},{"id":"72ba05e6.93db5c","type":"debug","z":"9f9cfeee.b231","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":850,"y":460,"wires":[]},{"id":"6ee9ea46.1209f4","type":"switch","z":"9f9cfeee.b231","name":"rec_gate","property":"rec_enable","propertyType":"flow","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":400,"y":460,"wires":[["f012bfe7.7402d"]]},{"id":"73bf6273.44315c","type":"inject","z":"9f9cfeee.b231","name":"センサデータ","topic":"","payload":"{\"device\":\"R-MSM\",\"id\":\"179E\",\"sensor\":\"BME688\",\"timestamp\":5636.84,\"temp\":{\"val\":31.13},\"humid\":{\"val\":63.44},\"press\":{\"val\":1017.54},\"gas\":{\"val\":123.98}}","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":170,"y":460,"wires":[["6ee9ea46.1209f4"]]},{"id":"302da884.962428","type":"file","z":"9f9cfeee.b231","name":"","filename":"","appendNewline":true,"createDir":false,"overwriteFile":"false","encoding":"none","x":730,"y":460,"wires":[["72ba05e6.93db5c"]]}

ここまで説明した中では、rec_start と rec_stop のインジェクトノードを使用してデータの記録開始/停止の操作をしていましたが、これをダッシュボードのスライドスイッチを使って操作するように変更していきます。

まずスライドスイッチを配置するダッシュボードの「タブ」と「グループ」を追加します。
ダッシュボードのスライドスイッチを配置して、先ほど作成したタブとグループをセットします。
スライドスイッチを操作したときのメッセージは、ONの時true, OFFの時falseにしておきます。

スライドスイッチのノードの後ろにswitchノードを接続し、下記のように配線します。
switchノードはスライドスイッチのメッセージがtrueなら1,falseなら2に出力する設定にして、それぞれをrec_startとrec_stopが接続されていたノードの入力に接続します。

これでデプロイすると、ダッシュボードのtab2でスライドスイッチを操作することができるようになります。

フローエディタのデバッグウインドウで実際の動作を確認しておきます。

2.送られてきたデータをファイルに逐次書き出す場合(csv形式)

ここまで、json形式でファイルを逐次書き出す方法を説明してきました。ここでは、csv形式のファイルで保存する方法を説明します。
json形式のデータをcsv形式のデータの変換するためにcsvノードを使用します。 そのため、json形式と異なる点は、2点あります。
 1) 保存するデータを列名として記載する必要があります。
 2) csvとして保存できるのは、msg.payloadの直下のobjectのみです。従って、その下のobjectに値がある場合には、直下に移動させる必要があります。

以下、この点を説明します。
R-MSMのBME688データをcsvファイルとして保存するフローを以下に示します。
・ 一番上の列(①)で、R-MSMのデータのうち、BME688を抽出しています。詳細は、「データの受信」を参照してください。
・ 2番目の列(②)で、gateノードを使って、ファイル保存のON/OFFを切り替えています。また、時刻のデータを追加しています。詳細は、「打刻とハードウエアIDの挿入」と「gateノードの追加」を参照してください。
・ 3番目の列(③)で、csvデータに変換してファイルに保存しています。以下、この列に関して説明します。

まず、changeノードから説明します。
csvノードを使ってcsvファイルに保存する際には、msg.payloadの直下のobjectが保存されます。
そのため、msg.payloadの直下にないデータを、直下に移動させています。
具体的には、BME688の温度データは、“msg.payload.temp.val”に格納されてR-MSMから送られてきます。これを、”msg.payload.temp”に移動させます。これをしないと、csvのtempの列に、 {“val”:23.45}というようなjson形式で保存されてしまいます。
湿度データ、気圧データ、空気清浄度データも同様に移動させます。

次に、csvノードを説明します。
一番上の、列名に記録したいデータ名を記載します。ここでは、“sensor”, ”datetime”, “temp”, “humid”, “press”, “gas” を記載しています。
今回は、オブジェクトからcsvへ変換ですので、一番下の「オブジェクトからCSVへの変換」の出力の設定を変更します。
「ヘッダーを一度だけ送信する」を選択します。
これによって、1行目に、上記の列名が出力されます。

最後のwrite fileノードは、「1.送られてきたデータをファイルに逐次書き出す場合(json形式)」で説明されていますので、ここでは割愛します。

最後に、書き込んだcsvファイルを載せておきます。

sensor,datetime,temp,humid,press,gas
BME688,2023-04-13T19:24:32.832+09:00,31.12,26,1006.24,56.28
BME688,2023-04-13T19:24:35.843+09:00,31.12,26.14,1006.24,55.58
BME688,2023-04-13T19:24:38.832+09:00,31.12,26.22,1006.24,55.98
BME688,2023-04-13T19:24:41.833+09:00,31.12,26.15,1006.23,55.94
BME688,2023-04-13T19:24:44.843+09:00,31.12,26.07,1006.23,56.84
BME688,2023-04-13T19:24:47.832+09:00,31.13,25.94,1006.24,57.14
BME688,2023-04-13T19:24:50.820+09:00,31.13,25.8,1006.24,58
BME688,2023-04-13T19:24:53.832+09:00,31.13,25.48,1006.24,55.04
BME688,2023-04-13T19:24:56.821+09:00,31.13,25.34,1006.24,52.12

3. フォルダに置かれたファイルの読み込み、書き込み

   画像ファイル(*.png)などの読み込み もfile in ノードを使用すれば可能です。読み込んだ画像をフローエディタに表示するために追加のノード「node-red-contrib-image-output」を「パレットの管理」/「ノードの追加」で検索して追加してください。

設定は特に変更しなくても大丈夫です。このノードとfile inノード、fileノードを使って次のようなフローを組みます。
ここではテスト用に単純に読み込んだ画像をフローエディタに表示し、ファイル名を変更してコピーを作成するだけです。
file in ノードとfileノードの設定は以下の通りです。

これでデプロイしてトリガをクリックすると… 画像が表示されて、読み込んだ画像のコピーが作成されました。

 


コマンドの実行や別プログラムの起動

OSでサポートするコマンドの実行にはexecノードを使用します。このexecノードを使用すれば、コマンドプロンプトやターミナルで実行するコマンドの処理を始めることができますので、Linux系のバッチファイル、pythonで記述したプログラム等をNode-REDから呼び出し、起動することもできます。


ここでは、コマンドの実行が可能なことを確認するために簡単なechoコマンドを設定しています。
以下のようなフローを組んで、インジェクトノードにはhello worldの文字列を設定しておきます。execでは受け取ったメッセージを引数とすることもできますので、このhello world の文字列をecho して標準出力に出力します。

次に、コマンド処理がエラーになった場合の挙動を見るためにexecにコマンド以外を設定してみます。
execノードに赤いエラー表示が出るとともに、返却コードの出力からCommand failedが返されているのを見ることができます。