Node-REDのノードを開発してみる その3(queueノード)

はじめに

 今回はNode-REDでキューを扱うことができるqueueノードを開発してみました。

 普段から研究で物体検出をするためにYOLOを利用しているのですが、データセットの学習には時間がかかるのでNode-REDで自動化しています。データセットのパスをキューに入れておいて、順次学習するようにしていました。

 そのキューの扱いをさらに再利用しやすいように、ノードとしてまとめてみたというのが今回の内容です。

▼開発したノードは、Node-REDのフローライブラリにも公開しています。パレットの管理からインストールすることが可能です。

https://flows.nodered.org/node/@background404/node-red-contrib-queue

▼GitHubのリポジトリはこちら

https://github.com/404background/node-red-contrib-queue

 なお、今回の内容はバージョン0.0.2の内容です。今後のアップデートで変更がある可能性は十分にありますが、ある程度動作するシンプルな構成になっています。

▼以前の記事はこちら

Pythonで論文を収集する(arXiv、Node-RED)

はじめに  今回はarXiv APIを使って、Pythonで論文を収集してみました。  普段は論文を検索するときにGoogle Scholarを使っていたのですが、プログラムで自動化したかっ…

PythonでgTTSを使ってみる(音声合成、Node-RED)

はじめに  今回はPythonでgTTS(Google Text-to-Speech)を使ってみました。  以前VoiceVoxも使ったことがあるのですが、英語も話すことができて、ローカル環境での音声の…

開発の方針

 以前キューを扱いたかったので、functionノードで実装したことがあります。

▼以下の記事で試しました。

Node-REDを使ってみる その3(ui-tableノード、emailノード)

はじめに  今回はNode-REDのui-tableノードとemailノードを使ってみました。  デスクトップPCの作業を自動化して、遠隔でも状態を確認するためにdashboard画面を確認し…

 キューの操作として、キューに要素を追加するenqueue、キューから要素を取り出すdequeue、キュー全体を取得するget、キュー全体を削除するclearを指定できるようにします。この4つの処理をOperationで指定するようにします。

 msg.payloadにはキューに入れるデータが入るので、別の変数でOperationを指定する必要があります。デフォルトではmsg.topicで指定できるとして、他の変数でも指定できるようにしておきました。

 実際に使っているうちに、キューの状態が変化したときに入っているものを常に出力したかったので、msg._queueとして出力するようにしました。その後の処理はmsg.topicの値をswitchノードで判定することで、分岐させることができます。

ノードを開発する

 基本的な開発方法は、Node-REDのユーザー会のドキュメントを参考にしています。

▼はじめてのノードの開発に関するページはこちら

https://nodered.jp/docs/creating-nodes/first-node

 ノードの開発に関しては、去年Node-REDでPythonを実行できるpython-venvノードの開発を行っていました。

▼Qiitaに開発の変遷を書いています。

https://qiita.com/background/items/d2e05e8d85427761a609

 ある程度仕様がはっきりしていたので、ChatGPTに相談して途中までは書いてもらいました。ノードの登録に関する記述でミスがあって、完全には生成できなさそうな感じではありました。

JavaScriptでの記述

 JavaScriptではノードの機能を記述します。

▼ドキュメントはこちら

https://nodered.jp/docs/creating-nodes/node-js

 コードは以下です。operationの値で処理を分岐しています。

module.exports = function (RED) {
    function QueueNode(config) {
        RED.nodes.createNode(this, config)

        const operationKey = config.operation
        const operationType = config.operationType

        const queue = this.context().get('queue') || []
        this.context().set('queue', queue)

        this.on('input', function (msg, send, done) {
            let operation

            if (operationType === 'msg') {
                operation = RED.util.getMessageProperty(msg, operationKey)
            } else if (operationType === 'flow') {
                operation = this.context().flow.get(operationKey)
            } else if (operationType === 'global') {
                operation = this.context().global.get(operationKey)
            }

            if (!operation) {
                this.error('Operation is not defined or invalid.', msg)
                done()
                return
            }

            if (operation === 'enqueue') {
                queue.push(msg.payload)
                send({ _queue: queue })
                this.context().set('queue', queue)
                this.status({ fill: 'green', shape: 'dot', text: `Enqueued: ${queue.length}` })
            } else if (operation === 'dequeue') {
                this.context().set('queue', queue)
                msg.payload = queue.shift()
                msg._queue = queue
                send(msg)
                if (queue.length > 0) {
                    this.status({ fill: 'blue', shape: 'dot', text: `Dequeued: ${queue.length}` })
                } else {
                    this.status({ fill: 'grey', shape: 'dot', text: 'Queue is empty' })
                }
            } else if (operation === 'get') {
                msg.payload = [...queue]
                msg._queue = queue
                send(msg)
                this.status({ fill: 'yellow', shape: 'dot', text: `Get: ${queue.length}` })
            } else if (operation === 'clear') {
                queue.length = 0
                this.context().set('queue', queue)
                send({ _queue: queue })
                this.status({ fill: 'grey', shape: 'dot', text: 'Queue is empty' })
            } else {
                this.error('Invalid operation. Use "enqueue", "dequeue", "preview", or "clear".', msg)
            }

            done()
        })
    }

    RED.nodes.registerType("@background404_queue", QueueNode, {
        defaults: {
            name: { value: "" },
            operation: { value: "topic", required: true },
            operationType: { value: "msg", required: true }
        }
    })
}

 Queueの数や状態がステータスで表示されるようにしています。

▼ステータスの表示についてはこちら

https://nodered.jp/docs/creating-nodes/status

▼ノードごとに状態が表示されます。

HTMLでの記述

 Node-REDでのエディタ上の表示をHTMLファイルで記述します。

▼ドキュメントはこちら

https://nodered.jp/docs/creating-nodes/node-html

 コードは以下です。ヘルプ用のテキストも入っています。

<script type="text/javascript">
    RED.nodes.registerType('@background404_queue', {
        category: 'function',
        color: '#F3B567',
        defaults: {
            name: { value: "" },
            operation: { value: "topic", required: true },
            operationType: { value: "msg", required: true }
        },
        inputs: 1,
        outputs: 1,
        icon: "db.svg",
        label: function () {
            return this.name || "queue";
        },
        paletteLabel: function () {
            return "queue";
        },
        oneditprepare: function () {
            $("#node-input-operation").typedInput({
                type: "msg",
                types: ["msg", "flow", "global"],
                typeField: "#node-input-operationType"
            });
        }
    });
</script>

<script type="text/html" data-template-name="@background404_queue">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
    <div class="form-row">
        <label for="node-input-operation"><i class="fa fa-cogs"></i> Operation</label>
        <input type="text" id="node-input-operation">
        <input type="hidden" id="node-input-operationType">
    </div>

    <div class="form-tips">
        <b>Tip:Operation</b>
        <ul>
            <li><code>enqueue</code>: Push msg.payload to the queue.</li>
            <li><code>dequeue</code>: Shift from the queue and sends it</li>
            <li><code>get</code>: Get the current contents of the queue as an array.</li>
            <li><code>clear</code>: Clears the queue, removing all items.</li>
        </ul>
        <code>msg._queue</code> always outputs an array of queues.
    </div>
</script>

<script type="text/html" data-help-name="@background404_queue">
    <p>This node helps you manage a FIFO queue. It supports operations like enqueue, dequeue, get, and clear the queue.</p>

    <h2><strong>Operation</strong></h2>
    <p>The Operation specifies the action that will be performed on the queue.</p>
    <p>These operations can be defined based on a variable, such as <code>msg.topic</code>.
    <p>Here’s how you can define the operations:</p>
    
    <ul>
        <li><code>enqueue</code>: Push msg.payload to the queue.</li>
        <li><code>dequeue</code>: Shift from the queue and sends it</li>
        <li><code>get</code>: Get the current contents of the queue as an array.</li>
        <li><code>clear</code>: Clears the queue, removing all items.</li>
    </ul>
    <code>msg._queue</code> always outputs an array of queues.
</script>

 Operationはmsg、flow、globalオブジェクトのプロパティを指定できるようになっています。

▼TipにOperationの指定やmsg._queueについて書いています。

▼ヘルプにも記述しているのですが、ノードの設定画面の方が見る機会が多いかな?と思います。

動作確認

injectノードで実行する

 msg.topicをinjectノードで設定して、Queueノードの動作を確認してみました。

▼全体のフローはこちら

[{"id":"dde452c32e8cb7e7","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"enqueue","payload":"","payloadType":"date","x":1490,"y":580,"wires":[["28641ffc4ae2a514"]]},{"id":"eba1303f2eddea0e","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"dequeue","x":1460,"y":620,"wires":[["28641ffc4ae2a514"]]},{"id":"360462d9ca022e62","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clear","x":1450,"y":660,"wires":[["28641ffc4ae2a514"]]},{"id":"93dc9198a6e73ba7","type":"debug","z":"d6fad9e77ecc8414","name":"debug 370","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1830,"y":600,"wires":[]},{"id":"d47a965d246c2461","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"get","x":1450,"y":700,"wires":[["28641ffc4ae2a514"]]},{"id":"28641ffc4ae2a514","type":"@background404_queue","z":"d6fad9e77ecc8414","name":"","operation":"topic","operationType":"msg","x":1670,"y":600,"wires":[["93dc9198a6e73ba7"]]}]

 ステータスが正しく表示され、それぞれのOperationで処理が行われていることを確認しました。どのOperationを指定した場合でも、msg._queueにはキューの中身が出力されました。

ui-tableノードとの組み合わせ

 元々組み合わせて使いたかったui-tableノードと組み合わせてみました。

▼全体のフローはこちら

[{"id":"fb9aa9fd2006696a","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"enqueue","payload":"","payloadType":"date","x":1450,"y":340,"wires":[["ddb14dbe36152b58"]]},{"id":"b4aecd54ee3d7f8f","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"dequeue","x":1460,"y":460,"wires":[["7746647b958d4cf3"]]},{"id":"c0d51b9cee735221","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clear","x":1450,"y":500,"wires":[["7746647b958d4cf3"]]},{"id":"9531ebf80d0542cb","type":"inject","z":"d6fad9e77ecc8414","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"get","x":1450,"y":540,"wires":[["7746647b958d4cf3"]]},{"id":"7746647b958d4cf3","type":"@background404_queue","z":"d6fad9e77ecc8414","name":"","operation":"topic","operationType":"msg","x":1670,"y":440,"wires":[["663a609afe34522f","e80f9a194adc999b"]]},{"id":"77af9a673f1d3aaa","type":"ui_table","z":"d6fad9e77ecc8414","group":"b02d1141d7eb7775","name":"Queue","order":0,"width":"6","height":"10","columns":[],"outputs":0,"cts":false,"x":2010,"y":480,"wires":[]},{"id":"663a609afe34522f","type":"change","z":"d6fad9e77ecc8414","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"_queue","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1840,"y":480,"wires":[["75c0ef496a98c34e","77af9a673f1d3aaa"]]},{"id":"ddb14dbe36152b58","type":"template","z":"d6fad9e77ecc8414","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n    \"time\": {{payload}}\n}","output":"json","x":1580,"y":400,"wires":[["7746647b958d4cf3"]]},{"id":"e80f9a194adc999b","type":"debug","z":"d6fad9e77ecc8414","name":"debug 369","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1830,"y":440,"wires":[]},{"id":"75c0ef496a98c34e","type":"debug","z":"d6fad9e77ecc8414","name":"debug 371","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":2030,"y":520,"wires":[]},{"id":"b02d1141d7eb7775","type":"ui_group","name":"Test","tab":"bb162f7c39b726dc","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"bb162f7c39b726dc","type":"ui_tab","name":"Queue","icon":"dashboard","disabled":false,"hidden":false}]

 ui-tableノードの直前で、msg._queueの値をmsg.payloadに入れています。

 いずれのOperationが実行された場合も、表を更新することができていました。

▼以下のように動作しました。

▼以下のように表示されました。

最後に

 仕組み自体は単純なのですが、あるとちょっと便利なノードとして開発してみました。

 実際にNode-REDで開発していると、絶対パスからのフォルダ名の取得などのパスを扱うことが非常に多いです。半分以上はそのためのノードではないかと思うほどです。

 その処理も簡単にしたいので、またノードにまとめたいなと考えています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です