【RaspberryPi】Node-REDでFileを使用する[ 2021.08.10 再追記あり]

Node-REDのデータのStore関係はSQLiteで行っていたのですが、その他にもFileノードが存在しています。そちらがどんな感じの機能を持っているかを調べてみました。

【追記】
Node-REDがバージョンアップするにあたってStorageカテゴリーのノードであるtailノードがなくなりました。そのため、その部分に関しては最新版では使用できない状況です。 そのため、他のノードを使用してtailのような機能を実装する場合などを検討し追記いたします。

【再追記】
エントリにコメントを頂き訂正の内容を以下に書かせていただきました。 tailノードの件に関してはこちらを御覧ください。

uepon.hatenadiary.com

最近、やったことを軽くまとめてみようかなと思ったたら、トラブルを引き当てることが多く、リカバリや調査やらなんやらで時間的にも重たい感じのボリュームになってしまったので、今回は軽めの内容になります。

f:id:ueponx:20200505234411p:plain

File関連のノードを調べてみる

File関連のノードのカテゴリーはパレットの【ストレージ】のカテゴリーになります。

対象となるノードは以下です。

  • fileノード … ファイルの書き込み(fwrite()的な)削除もできるらしい
  • file inノード … ファイルの読み込み(fread()的な)
  • watchノード … ファイルの変化を監視(トリガーにできる)
  • tailノード … ファイルの末尾を監視(トリガーにできる)(Node-REDのバージョンアップに伴い削除)

ファイルのREAD/WRITEに関してはもちろんですが、ファイルの変化を検知するトリガーがあるのはかなりいいかも。例えば、ログファイル変化を検知してアクションを行うようなことがすんなりできます。あんまり見ていませんでしがが割といいノードがある感じです。

ファイルへの書き込み

fileノードを使用します。

処理はmsg.payloadの内容をファイルに書き込み(アウトプット)するというものです。

f:id:ueponx:20200506002532p:plain

プロパティは以下のようになっています。

f:id:ueponx:20200506143424p:plain

msg.payloadに入れた内容をファイルに書き込む機能を持っていますが、以下の機能も持っています。

  • 追記(指定したファイルの末尾に追記する)
  • 上書き(指定したファイルに上書きする)
  • 削除(指定したファイルを削除する)

f:id:ueponx:20200506143434p:plain

その他に処理時に書き込み後に改行を入れる指定、フォルダがなかった場合に作成をすることも可能です。また、文字コード指定もできる点では割と高性能です。 指定するファイル名の指定はファイル名だけだとNode-REDの相対パスになるので、絶対パスにしておいたほうが無難かと思います。

今回のテストでは、処理のログファイルのような使用を考えていたので以下の様に設定してみました。

f:id:ueponx:20200506143440p:plain

あとは、トリガーとなるinjectノード、結果表示用のdebugノードを追加しています。テスト用のフローは以下のようにしています。

f:id:ueponx:20200506143912p:plain

ノード間の端子を接続したら【デプロイ】ボタンをクリックし、injectノードをクリックすることでフローの実行ができます。指定ファイルは事前には作成していなかったので、追記した場合にはファイルを作成してから書き込みを行います。ファイルがあればちゃんと追記してくれます。

Fileノードで指定した/home/pi/time.logファイルにタイムスタンプを書き込み、デバックのタブにも出力されています。

f:id:ueponx:20200506145453p:plain

fileノードは入力時のmsgをそのまま出力するので、debugノードで書き込み内容と同様のpayloadが出力されます。

f:id:ueponx:20200506150528p:plain

これだけでログ出力の対応ができるのか。本格的に行うのであればfunctionノードtemplateノードで書き込み内容を整形したほうが良さそうです。

ファイルの読み込み

file inノードを使用します。

f:id:ueponx:20200506002535p:plain

文字列とバイナリの切り替えができます。初期のプロパティの画面は以下のようになっています。

f:id:ueponx:20200506153145p:plain

【ファイル名】や【出力形式】を設定していくことになりますが、出力形式は以下の4つが選択可能です。

  • 文字列(ファイルの内容を文字列として一括で出力)
  • 行毎のメッセージ(ファイルを行ごとに出力、一行毎にデータが出力されます。ループのような動作)
  • バイナリバッファ(ファイルの内容をbyte配列として一括で出力)
  • バッファのストリーム(ストリーム形式での出力ですが、単体処理後のdebugノードの出力ではバイナリバッファとの違いは見えない感じです)

また、このノードの処理を行うと出力にmsg.filenameが追加されます。

f:id:ueponx:20200506154617p:plain

では、テストを行ってみます。先程のfileノードのテストフローにfile inノードを追加していきます。

  1. injectノードをクリック
  2. fileノードで/home/pi/time.logへ追記
  3. 書き込んだデータをdebugノードで表示
  4. fie inノードで/home/pi/time.log内容の読み込み
  5. 読み込んだファイル名の表示 1.読み込んだデータの表示

こんな感じの処理にします。では作成したフローを表示します。

f:id:ueponx:20200506161135p:plain

では、出力形式毎にデバック表示結果をみてみます。(実行は2回以上おこなった場合のものにしてあります)

文字列  ファイル内のすべてのデータが文字列として出力されています。改行コードも含まれています。

f:id:ueponx:20200506161437p:plain

行毎のメッセージ  ファイル内のデータが毎行ごとに文字列として出力されています。ファイルが2行ある場合には出力も2回行われます。また、改行毎に処理が行われるので、データの末尾改行コードで終わっている場合には空のデータが出力されます。

f:id:ueponx:20200506162148p:plain

バイナリバッファ byte配列が出力されています。

f:id:ueponx:20200506162300p:plain

バッファのストリーム こちらもbyte配列が出力されています。日本語文字列などであれば少し変化があるのでしょうか。

f:id:ueponx:20200506162655p:plain

ファイルの変化を監視する

watchノードを使用します。

f:id:ueponx:20200506002538p:plain

こちらノードはinjectノードなどと同様にトリガーになるノードになります。ファイルの変化を検知するとフローの次のノードに出力が行われます。 初期のプロパティは以下のようになっています。

f:id:ueponx:20200506165120p:plain

このノードではディレクトリも監視できます。実際に変化したファイル名をmsg.payload(フルパス)として、検知したファイル名をmsg.topic(フルパス)として出力します。また、msg.fileに変化したファイル名(フルパスではない)を出力します。msg.typeは変化のあったファイル・ディレクトリを、msg.sizeは変更時に増加したファイルサイズ(バイト数)が入ります。

こちらは特にファイルの変更を検知すれば動作するものなので先程のfile inノードのときに作成したテストフローを活用します。あとは変更されたことを別の手段で検知したいと思います。先程のノードに以下の3つのノードを追加して

  • watchノード(ファイル変更の検知トリガー)
  • templateノード(後続のplay audioノードは話すメッセージの作成)
  • play audioノード(メッセージの音声出力)

フローを作成します。フローを作成すると以下のようになります。

f:id:ueponx:20200506173402p:plain

各ノードのプロパティは以下のようになります。

watchノード 変更を監視するファイル/home/pi/time/logを設定

f:id:ueponx:20200506173603p:plain

templateノード 後続のplay audioノードは話すメッセージをmsg.payloadへ格納

f:id:ueponx:20200506173618p:plain

play audioノード メッセージの音声出力するときのTTSのエンジンの指定(WindowsではMicorosoft Haruka desktopがデフォルトになっています)

f:id:ueponx:20200506173637p:plain

設定が終わったらデプロイを行い、injectノードをクリックすると、/home/pi/time.logが更新され、「ファイルが変更されました」と音声出力されます。(TTSのエンジン設定がないと音声出力はうまくいかないかもしれません)

ファイルの末尾の更新検知

この部分に関してはNode-REDのバージョンアップに伴いtailノードが削除となりましたのでご注意ください

tailノードを使用します。

f:id:ueponx:20200506002541p:plain

このノードは設定したファイルの末尾を出力(追加されたデータを監視)します。先程のwatchノードは変化を検知しますが、こちらは更新されたファイルの末尾データを後続のフローに出力します。

では、こちらも先程まで触ったフローに追加します。今回は読み上げるメッセージを「対象ファイルの末尾が変更されました。」とします。

以下のようなフローにします。

  • tailノード(ファイル末尾の検知トリガー)
  • templateノード(後続のplay audioノードは話すメッセージの作成)
  • play audioノード(メッセージの音声出力)

f:id:ueponx:20200506175427p:plain

各ノードのプロパティは以下のようになります。

tailノード 末尾の変更を監視するファイル/home/pi/time/logを設定

f:id:ueponx:20200506175438p:plain

templateノード 後続のplay audioノードは話すメッセージ「対象ファイルの末尾が変更されました。」をmsg.payloadへ格納

f:id:ueponx:20200506175442p:plain

play audioノード メッセージの音声出力するときのTTSのエンジンの指定

f:id:ueponx:20200506175447p:plain

ここまで設定が終わったら【デプロイ】ボタンをクリックして、これまでと同様に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ノードの処理側に入れているので、変更すれば最後の数行という形の処理も可能になると思います。

f:id:ueponx:20210804154922p:plain

watchノードのプロパティ

f:id:ueponx:20210804163053p:plain

(注)watchノードは生成されたファイルが存在せず、新規に作成された場合にはトリガーに反応しないようです。ファイルがあれば処理に問題ありません。

file inノードのプロパティ

f:id:ueponx:20210804163108p:plain

functionノードの処理

f:id:ueponx:20210804163122p:plain

最後の行を取得する場合

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に配列データとして格納されて後続にデータが送られていきます。

f:id:ueponx:20210804163859p:plain

ポイント

途中に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"
            ]
        ]
    }
]
/* -----codeの行番号----- */