Node-RED
のデータのStore関係はSQLite
で行っていたのですが、その他にもFileノード
が存在しています。そちらがどんな感じの機能を持っているかを調べてみました。
【追記】
Node-REDがバージョンアップするにあたってStorageカテゴリーのノードであるtailノード
がなくなりました。そのため、その部分に関しては最新版では使用できない状況です。
そのため、他のノードを使用してtailのような機能を実装する場合などを検討し追記いたします。
【再追記】 エントリにコメントを頂き訂正の内容を以下に書かせていただきました。 tailノードの件に関してはこちらを御覧ください。
最近、やったことを軽くまとめてみようかなと思ったたら、トラブルを引き当てることが多く、リカバリや調査やらなんやらで時間的にも重たい感じのボリュームになってしまったので、今回は軽めの内容になります。
File関連のノードを調べてみる
File関連のノードのカテゴリーはパレットの【ストレージ】のカテゴリーになります。
対象となるノードは以下です。
fileノード
… ファイルの書き込み(fwrite()的な)削除もできるらしいfile inノード
… ファイルの読み込み(fread()的な)watchノード
… ファイルの変化を監視(トリガーにできる)(Node-REDのバージョンアップに伴い削除)tailノード
… ファイルの末尾を監視(トリガーにできる)
ファイルのREAD/WRITEに関してはもちろんですが、ファイルの変化を検知するトリガーがあるのはかなりいいかも。例えば、ログファイル変化を検知してアクションを行うようなことがすんなりできます。あんまり見ていませんでしがが割といいノードがある感じです。
ファイルへの書き込み
fileノード
を使用します。
処理はmsg.payloadの内容をファイルに書き込み(アウトプット)するというものです。
プロパティは以下のようになっています。
msg.payloadに入れた内容をファイルに書き込む機能を持っていますが、以下の機能も持っています。
- 追記(指定したファイルの末尾に追記する)
- 上書き(指定したファイルに上書きする)
- 削除(指定したファイルを削除する)
その他に処理時に書き込み後に改行を入れる指定、フォルダがなかった場合に作成をすることも可能です。また、文字コード指定もできる点では割と高性能です。 指定するファイル名の指定はファイル名だけだとNode-REDの相対パスになるので、絶対パスにしておいたほうが無難かと思います。
今回のテストでは、処理のログファイルのような使用を考えていたので以下の様に設定してみました。
あとは、トリガーとなるinjectノード
、結果表示用のdebugノード
を追加しています。テスト用のフローは以下のようにしています。
ノード間の端子を接続したら【デプロイ】ボタンをクリックし、injectノード
をクリックすることでフローの実行ができます。指定ファイルは事前には作成していなかったので、追記した場合にはファイルを作成してから書き込みを行います。ファイルがあればちゃんと追記してくれます。
Fileノード
で指定した/home/pi/time.log
ファイルにタイムスタンプを書き込み、デバックのタブにも出力されています。
fileノード
は入力時のmsgをそのまま出力するので、debugノード
で書き込み内容と同様のpayloadが出力されます。
これだけでログ出力の対応ができるのか。本格的に行うのであればfunctionノード
かtemplateノード
で書き込み内容を整形したほうが良さそうです。
ファイルの読み込み
file inノード
を使用します。
文字列とバイナリの切り替えができます。初期のプロパティの画面は以下のようになっています。
【ファイル名】や【出力形式】を設定していくことになりますが、出力形式は以下の4つが選択可能です。
- 文字列(ファイルの内容を文字列として一括で出力)
- 行毎のメッセージ(ファイルを行ごとに出力、一行毎にデータが出力されます。ループのような動作)
- バイナリバッファ(ファイルの内容をbyte配列として一括で出力)
- バッファのストリーム(ストリーム形式での出力ですが、単体処理後のdebugノードの出力ではバイナリバッファとの違いは見えない感じです)
また、このノードの処理を行うと出力にmsg.filename
が追加されます。
では、テストを行ってみます。先程のfileノード
のテストフローにfile inノード
を追加していきます。
- injectノードをクリック
- fileノードで/home/pi/time.logへ追記
- 書き込んだデータをdebugノードで表示
- fie inノードで/home/pi/time.log内容の読み込み
- 読み込んだファイル名の表示 1.読み込んだデータの表示
こんな感じの処理にします。では作成したフローを表示します。
では、出力形式毎にデバック表示結果をみてみます。(実行は2回以上おこなった場合のものにしてあります)
文字列 ファイル内のすべてのデータが文字列として出力されています。改行コードも含まれています。
行毎のメッセージ ファイル内のデータが毎行ごとに文字列として出力されています。ファイルが2行ある場合には出力も2回行われます。また、改行毎に処理が行われるので、データの末尾改行コードで終わっている場合には空のデータが出力されます。
バイナリバッファ byte配列が出力されています。
バッファのストリーム こちらもbyte配列が出力されています。日本語文字列などであれば少し変化があるのでしょうか。
ファイルの変化を監視する
watchノード
を使用します。
こちらノードはinjectノード
などと同様にトリガーになるノードになります。ファイルの変化を検知するとフローの次のノードに出力が行われます。
初期のプロパティは以下のようになっています。
このノードではディレクトリも監視できます。実際に変化したファイル名をmsg.payload(フルパス)
として、検知したファイル名をmsg.topic
(フルパス)として出力します。また、msg.file
に変化したファイル名(フルパスではない)を出力します。msg.type
は変化のあったファイル・ディレクトリを、msg.size
は変更時に増加したファイルサイズ(バイト数)が入ります。
こちらは特にファイルの変更を検知すれば動作するものなので先程のfile inノード
のときに作成したテストフローを活用します。あとは変更されたことを別の手段で検知したいと思います。先程のノードに以下の3つのノードを追加して
- watchノード(ファイル変更の検知トリガー)
- templateノード(後続のplay audioノードは話すメッセージの作成)
- play audioノード(メッセージの音声出力)
フローを作成します。フローを作成すると以下のようになります。
各ノードのプロパティは以下のようになります。
watchノード 変更を監視するファイル/home/pi/time/log
を設定
templateノード 後続のplay audioノード
は話すメッセージをmsg.payload
へ格納
play audioノード メッセージの音声出力するときのTTSのエンジンの指定(WindowsではMicorosoft Haruka desktop
がデフォルトになっています)
設定が終わったらデプロイを行い、injectノード
をクリックすると、/home/pi/time.log
が更新され、「ファイルが変更されました」と音声出力されます。(TTSのエンジン設定がないと音声出力はうまくいかないかもしれません)
ファイルの末尾の更新検知
この部分に関してはNode-REDのバージョンアップに伴いtailノードが削除となりましたのでご注意ください
tailノード
を使用します。
このノードは設定したファイルの末尾を出力(追加されたデータを監視)します。先程のwatchノード
は変化を検知しますが、こちらは更新されたファイルの末尾データを後続のフローに出力します。
では、こちらも先程まで触ったフローに追加します。今回は読み上げるメッセージを「対象ファイルの末尾が変更されました。」とします。
以下のようなフローにします。
- tailノード(ファイル末尾の検知トリガー)
- templateノード(後続のplay audioノードは話すメッセージの作成)
- play audioノード(メッセージの音声出力)
各ノードのプロパティは以下のようになります。
tailノード 末尾の変更を監視するファイル/home/pi/time/log
を設定
templateノード 後続のplay audioノード
は話すメッセージ「対象ファイルの末尾が変更されました。」をmsg.payload
へ格納
play audioノード メッセージの音声出力するときのTTSのエンジンの指定
ここまで設定が終わったら【デプロイ】ボタンをクリックして、これまでと同様にinjectノード
をクリックするとメッセージが2種類出力されると思います。
今回はNode-RED
上でファイルの変更を行っていますが、全く別の方法でファイルの変更を行っても、同様にファイル変更の検知が行われます。
コマンドで以下のように実行すれば、ファイルの変更(watchノード
)のみが検知されます。
$ touch /hom/pi/time.log
以下のようにすれば、ファイルの変更(watchノード
)と末尾の変更(tailノード
)の両方が検知されます。
$ echo change >> /home/pi/time.log
そのため、全く別でログファイルを出力するシステムがあれば、それをトリガーとして連携を行うことができます。疎結合ができるので便利だと思いました。
【追記 2021.08.04】tailノード
がなくなったので同じような機能を考えてみる
Node-REDがバージョンアップしてしまったので、tailノード
はもう使用できません。そこで、現在使用可能なwatchノード
とfile inノード
を使用して似たような機能を作ってみたいと思います。以下のような形でフローを作ってみました。
処理の流れとしては、以下のようになります。
watchノード
でファイルが更新されたか確認するfile inノード
で内容を確認して最終行を取得する
今回は最終行としていますが、この部分は取得したい数値をfunctionノード
の処理側に入れているので、変更すれば最後の数行という形の処理も可能になると思います。
watchノードのプロパティ
(注)watchノード
は生成されたファイルが存在せず、新規に作成された場合にはトリガーに反応しないようです。ファイルがあれば処理に問題ありません。
file inノードのプロパティ
functionノードの処理
最後の行を取得する場合
data = [] data = msg.payload.split("\n").filter(Boolean); msg.payload = data[data.length-1]; return msg;
複数行を取得する場合
let data = []; let result = []; const lineCount = 3; //この数値が取得する行数 data = msg.payload.split("\n").filter(Boolean); if(data.length-1 < 3){ msg.payload = data; return msg; } else { for (let i=(data.length)-lineCount ; i<data.length ; i++){ result.push(data[i]) } msg.payload = result; return msg; }
このように記述することで、msg.payload
に配列データとして格納されて後続にデータが送られていきます。
ポイント
途中にdata = msg.payload.split("\n").filter(Boolean);
というコードがありますが、これは実行時に末尾に改行が入ってしまう場合の処理で、Split
時の空データを削除するための処理担っています。ログ出力時に末尾に改行を行わない場合には末尾にあるfilter処理は不要です。
念の為、テストに使用したフローをおいておきます。
テストに使用したフロー
[ { "id": "d33484abe0c60907", "type": "file in", "z": "cd0e1fcec1465bf7", "name": "", "filename": "/root/tmp", "format": "utf8", "chunk": false, "sendError": false, "encoding": "none", "allProps": false, "x": 340, "y": 460, "wires": [ [ "80472c5daa51e7dd" ] ] }, { "id": "80472c5daa51e7dd", "type": "function", "z": "cd0e1fcec1465bf7", "name": "", "func": "let data = [];\nlet result = [];\nconst lineCount = 3; //この数値が取得する行数\ndata = msg.payload.split(\"\\n\").filter(Boolean);\nif(data.length-1 < 3){\n msg.payload = data;\n return msg;\n} else {\n for (let i=(data.length)-lineCount ; i<data.length ; i++){\n result.push(data[i])\n }\n msg.payload = result;\n return msg;\n}", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 500, "y": 460, "wires": [ [ "3e1ca8d3ef8681d2" ] ] }, { "id": "3e1ca8d3ef8681d2", "type": "debug", "z": "cd0e1fcec1465bf7", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 690, "y": 460, "wires": [] }, { "id": "d07bb8db40be0641", "type": "watch", "z": "cd0e1fcec1465bf7", "name": "", "files": "/root/tmp", "recursive": false, "x": 120, "y": 480, "wires": [ [ "d33484abe0c60907", "a0ffa85a765b4f3e" ] ] }, { "id": "a0ffa85a765b4f3e", "type": "debug", "z": "cd0e1fcec1465bf7", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 350, "y": 520, "wires": [] }, { "id": "22cc8734d2086148", "type": "comment", "z": "cd0e1fcec1465bf7", "name": "watchノードとfile inノードを使用したファイルの最終行の取出し", "info": "tailノードと同じことが可能", "x": 290, "y": 420, "wires": [] } ]
おわりに
ノード全体を眺めた感じではあんまり使い所がないのかなと思っていたのですが、既存システムとの連携が難しいものでも、ログファイルなどでの出力があるといったパターンでは非常に良くできたノードだと感じました。テストも簡単にできるのはいいですね。
今回のテスト作成したフローを以下においておきます。よかったら動作させてみてください。
[ { "id": "73755b41.2fb194", "type": "tab", "label": "フロー 1", "disabled": false, "info": "" }, { "id": "8efbb7be.484168", "type": "file", "z": "73755b41.2fb194", "name": "", "filename": "/home/pi/time.log", "appendNewline": true, "createDir": false, "overwriteFile": "false", "encoding": "none", "x": 390, "y": 60, "wires": [ [ "bf4784e6.89b858", "af98e3bb.03ed2" ] ] }, { "id": "ea2bd64e.fd74b8", "type": "inject", "z": "73755b41.2fb194", "name": "", "topic": "", "payload": "", "payloadType": "date", "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "x": 180, "y": 60, "wires": [ [ "8efbb7be.484168" ] ] }, { "id": "bf4784e6.89b858", "type": "debug", "z": "73755b41.2fb194", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "x": 590, "y": 60, "wires": [] }, { "id": "4cc468c6.af0f38", "type": "tail", "z": "73755b41.2fb194", "name": "", "filetype": "text", "split": "[\\r]{0,1}\\n", "filename": "/home/pi/time.log", "inputs": 0, "x": 180, "y": 240, "wires": [ [ "9ac418a8.b1ac88" ] ] }, { "id": "4efa81d9.4d03a", "type": "play audio", "z": "73755b41.2fb194", "name": "", "voice": "0", "x": 590, "y": 180, "wires": [] }, { "id": "af98e3bb.03ed2", "type": "file in", "z": "73755b41.2fb194", "name": "", "filename": "/home/pi/time.log", "format": "stream", "chunk": false, "sendError": false, "encoding": "none", "x": 610, "y": 120, "wires": [ [ "1cd01b79.2b76e5", "ecdb5c39.c30f" ] ] }, { "id": "1cd01b79.2b76e5", "type": "debug", "z": "73755b41.2fb194", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "filename", "targetType": "msg", "x": 830, "y": 120, "wires": [] }, { "id": "ecdb5c39.c30f", "type": "debug", "z": "73755b41.2fb194", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "x": 830, "y": 160, "wires": [] }, { "id": "1b98bb88.57ac54", "type": "watch", "z": "73755b41.2fb194", "name": "", "files": "/home/pi/time.log", "recursive": "", "x": 180, "y": 180, "wires": [ [ "65ac1c17.f7afe4" ] ] }, { "id": "65ac1c17.f7afe4", "type": "template", "z": "73755b41.2fb194", "name": "", "field": "payload", "fieldType": "msg", "format": "handlebars", "syntax": "mustache", "template": "ファイルが変更されました。", "output": "str", "x": 380, "y": 180, "wires": [ [ "4efa81d9.4d03a" ] ] }, { "id": "9ac418a8.b1ac88", "type": "template", "z": "73755b41.2fb194", "name": "", "field": "payload", "fieldType": "msg", "format": "handlebars", "syntax": "mustache", "template": "対象ファイルの末尾が変更されました。", "output": "str", "x": 380, "y": 240, "wires": [ [ "4efa81d9.4d03a" ] ] } ]