Node-REDのカスタムノードを作って電源装置を制御する

Node-REDのノードはインストール時に用意されたものやネットワークで提供されたものが多数ありますが、ここでは独自のカスタムノードを作る方法を紹介します。菊水電子工業株式会社製の直流安定化電源装置を題材にしてネットワークから制御する時に使うノードを作成し、ダッシュボードからコントロールするフローを作成します。

対象とする直流安定化電源装置の仕様を確認

題材として、LANを経由して各種のコントロールが可能な直流安定化電源装置、菊水電子製PMX-Aを使用します。まずこの機材の仕様を確認します。


この電源装置は取扱説明書によると各種インターフェースで外部からの制御が可能です。資料によると、この電源装置のLANポートに組み込まれたWebサーバにWebブラウザでアクセスして「組み込みWebサイト」からコントロールする方法と、SCPI-RAWプロトコルでSCPI (Standard Commands for Programmable Instruments) コマンドを送信して制御する方法があります。

内蔵のWebインターフェース画面  vs  SCPIコマンド          

SCPIコマンドは、試験・計測装置向けに考案されたASCIIベースのコマンドです。制御コマンドリストは菊水電子のサイトから入手できます。今回のカスタムノード作成は、後者のSCPIコマンドを使ってNode-REDから装置をコントロールするように進めます。

ネットワークの設定と通信の基本動作の確認

まず、この機材PMX-Aは初期状態ではDHCPでサーバからIPアドレスが割り当てられる設定になっています。IPアドレスをスタティックアドレスに変更して、接続時にIPアドレスの指定で確実にこの機材にアクセスできるように設定します。この設定は機器内蔵のWebインターフェース画面から行います。
LAN Config/IP Address Assignmentの画面を開き、Assignment/Method の項目でStaticのチェックボックスをONにして[Apply]ボタンを押して設定を書き込みます。

次に、どのような接続をしてSCPIコマンドによる制御が可能か確認します。取扱説明書によるとSCPI-RAW プロトコル(ポート: 5025)でコマンドを送信し、続けてプログラムターミネータとしてLF(0x0A)を送信します。SCPIコマンドは階層構造を持つコマンドですが、コマンドストリングを送信すると、階層はルートレベルに戻ります。この条件に沿って、Tera Termを使用して接続テストを行いました。

<Tera Termでのテスト時の設定>

<Tera Termでの通信テスト>

通信テストで機材も正常に反応していますので、この方法でNode-REDから利用できるようにします。

カスタムノード実装の方針

事前テストで動かしてみたところ、通信ポートを設定し、SCPIコマンドとプログラムターミネータのLF(0x0A)を送信するとコントロールが可能です。SCPIコマンドは計測器制御等に利用されるコマンドとして広く利用されており、この機材についても多くのコマンドが準備されているので、このSCPIコマンドをNode-REDから簡単に送信できるようにするのが良さそうです。

電源装置との通信にはNode-REDのコアノードの「TCP Request」ノードが使用できますが、ASCII文字列のSCPIコマンドを送信する際にターミネータとしてLF (0x0A)を付加する必要があります。
送信コマンドをセットする時に全てにターミネータを付けるよりも、ノードを使ってTCP Requestノードの手前で付ける方が良さそうです。

(参考)ターミネータとしては、行の区切りを示す制御コードが使われることが多くあります。
LF (Line Feed) : 0x0A「次の行に移る」という指示を出す制御コード
CR (Carriage Return) : 0X0D「カーソルを先頭に戻す」という指示を出す制御コード
・Windows系では改行コードとしてCR+LFを、Unix系ではLFを使用する事が一般的です。
 今回のSCPIコマンドではLF(0x0A)をコマンドの区切りを示す制御コードとして扱い、コマンドの受信バッファでコマンドを受信する際にLF(0x0A)を受信すると、そこまでの文字列をコマンドとして取り扱い、そのコマンドの処理を実行します。

 

まずファンクションノードでこの処理を行って動作を確認します。

この構成で上手く動作する事が確認できたので、今回はこの仕組みでカスタムノード作成のサンプルとして、「送信する文字列の後ろにLF(0x0A)を付加する」という動作のノードを作ってみます。

Node-REDのカスタムノードの作成とインストール

カスタムノードを作成するためのフォルダ、例えばhome直下に空のフォルダを作成し、[node-red-contrib-add-lf]という名称にして、フォルダ内にadd-lf.js とadd-lf.htmlの2つのファイルを作成します。w

カスタムノードの開発では、基本的にこの2つのファイルでノードの処理の記述と、Node-REDフローエディタ上での見え方やプロパティ編集画面の内容などを作成します。そして、npmでの管理のためにpackage.jsonファイルを作成し、これを加えた3つのファイルで構成されます。

 

内容は下記の通りです。

1.add-lf.js

module.exports = function(RED) {
    function AddLfNode(config) {
        RED.nodes.createNode(this,config);
        this.name = config.name;
        let node = this;
        node.on('input', function(msg) {
            msg.payload = msg.payload + String.fromCharCode(0x0A);
            node.send(msg);
        });
    }
    RED.nodes.registerType("add-lf",AddLfNode);
}

この中で、「node.on('input', function(msg) {... }); 」の部分に目的の処理が入ります。
ノードの入力端子に接続されたワイヤで送られてきたmsgオブジェクトが引数として渡されます。
一方、フローエディタでノードを開いて表示されるダイアログで設定されたプロパティはconfigオブジェクトに含まれています。この例では各ノードで共通して持っているノード名称を表示するためのconfig.nameだけ使用しています。

2.add-lf.html

<script type="text/javascript">
    RED.nodes.registerType('add-lf',{
        category: 'function',
        color: '#c44646',
        defaults: {
            name: {value:""}
        },
        inputs: 1,
        outputs: 1,
        icon: "arrow-in.svg",
        label: function() {
            return this.name||"add-lf";
        }
    });
</script>


<script type="text/html" data-template-name="add-lf">
    <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>
</script>


<script type="text/html" data-help-name="add-lf">
    <p>立命館大学カスタムノードサンプル:菊水電源装置との通信のためにメッセージの最後に LF(0x0A) を付加します。Rits simple custom node sample, add LF(0x0A) at the end of the message to communicate Kikusui Power Supply.</p>
</script>

このhtmlファイルでは、3つのscriptが置かれています。それぞれ以下の役割を持ちます。

・フローエディタに登録するノードの設定
  ノードのアイコンやカラーもここで設定します。
  このサンプルでは「立命館カラー」に近いエンジ色で作成してみました。

・ノードを開いたときのプロパティ編集テンプレート
  ノードの名称を設定する「name」のフィールドだけ設けています。

・ヘルプ
  簡単なヘルプを記述します。

3. packaje.jsonの作成

ターミナルを開いて[node-red-contrib-add-lf]フォルダに移動し、コマンド[npm init]を実行してパッケージ化します。質問が表示されますので、順次答えていきます。
・package name :node-red-contrib-add-lf を入力します。
・version:テスト用サンプルですので、仮に0.1.0 としておきます。
・description:簡単な説明を入力します。
・entry point:Enterを押してデフォルト値をセットします。
・test command:今回は用意しませんのでEnterを押して次に行きます。
・git repository:今回はローカルのテスト用なので用意しません。Enterを押して次に行きます。
・keywords:今回はローカルのテスト用なので用意しません。Enterを押して次に行きます。
・author: Rits:作者名を入力します。
・license: (ISC):今回はローカルのテスト用なのでそのままEnterを押して次に行きます。
入力が完了すると、内容確認の後にyes/noを聞かれますのでyesを入力して抜けます。

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (red-contrib-add-lf2) node-red-contrib-add-lf2
version: (1.0.0) 0.1.0
description: a simple node to add LF to communicate with kikusui power supply
entry point: (index.js)
test command:
git repository:
keywords:
author: Rits
license: (ISC)
About to write to C:\Users\USER\nodedev\node-red-contrib-add-lf2\package.json:

{
"name": "node-red-contrib-add-lf2",
"version": "0.1.0",
"description": "a simple node to add LF to communicate with kikusui power supply",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Rits",
"license": "ISC"
}


Is this OK? (yes) yes

この操作でディレクトリ内に[package.json]ファイルができますので、これをエディタで開いて最後の方の”license”の項目の次に以下の内容を追加します。

"node-red": {
  "nodes": {
    "add-lf": "add-lf.js"
  }
},

追加後のpackage.jsonの全体は次のようになります。

{
  "name": "node-red-contrib-add-lf",
  "version": "0.1.0",
  "description": "a simple node to add LF to communicate with kikusui power supply",
  "main": "add-lf.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Rits",
  "license": "ISC",
  "node-red": {
    "nodes": {
      "add-lf": "add-lf.js"
    }
  }
}

これで準備ができましたので、[node-red-contrib-add-lf]のディレクトリでパッケージ化のコマンドを実行します。

npm pack --pack-destination ..

ひとつ上の階層に、[node-red-contrib-add-lf-0.1.0.tgz]が作成されます。
このファイルをNode-RED フローエディタの「パレットの管理」から「ノードを追加」タブを開き、「モジュールのtgzファイルをアップロード」ボタンを押してアップロードします。

これでフローエディタのパレットに新しいノードが追加されます。

カスタムノードを使ったフローの作成

先ほど作成してインストールしたカスタムノードを使ってフローを組み立てます。
SCPIコマンドはASCIIベースなので文字列として格納します。設定値を追加する際はchangeノード内で追加します。フローを再起動したときに設定した値を復帰するよう、flowコンテキストを使用して設定値を保存しています。

上記のフローに一時的に動作確認用のデバッグノードを追加して送信するコマンドと、その後に機器から返送された応答の内容を確認します。上段では機器IDの確認コマンド、”*IDN?”を送信します。作成したadd-lfノードを通過すると末尾にLF(0x0A)が追加されてデバッグウインドウでは矢印マークとして見えています。 このコマンドを送信すると、機器からは”KIKUSUI,PMX35-3A, …” といった応答が返っています。

電源装置のコントロールパネル

作成したフローを使用して、ネットワーク経由でNode-RED Dashboardから菊水製電源装置のコントロールをすることができました。

カスタムノード改良版の方針

 電源装置との通信をサポートするカスタムノードを作成しましたが、メッセージにLF(0x0A)を追加するだけのシンプルなノードでしたので、これにもう少し改良を加えます。

 リモートで電源装置に限らず計測器などをコントロールする場合、ロボットでも同様ですが制御コマンドの流れをコントロールする仕組みとしてゲート機能があれば安全に便利に使えます。そこで、先に作成したadd-lfノードにgateを追加したadd-lf-gateノードを新たに作成します。

追加するゲート機能としては、次のようにします。
・gate機能は選択肢をEnableにしたときに有効となる。デフォルトはgate機能をDisable(無効)とします。
・gate機能がEnableの時に、設定したflowまたはglobalコンテキストの値を参照し、trueであれば通過、falseであればコマンドをブロックして機器に送らないようにします。

この動作を実現するため、ノードのプロパティウインドウには次の入力項目を設けます。
1)ゲート機能のDisable / Enable を選択するプルダウンメニュー
2)ゲートを開くキーとなるflow/globalコンテキストを指定する入力フィールド

  また、状態が分かるようにmsgが到着した際にノードのステータス表示も行います。
3)msgが到着した時のgateの状態で、ノードのステータス表示を出す
   緑:Pass ,  赤:Block,  黄:gate-keyで使用するコンテキストが未設定( Undefined)

上記の変更を加えたソースコードが下記のようになります。

// add-lf-gate.js


module.exports = function(RED) {
    function AddLfGateNode(config) {
        RED.nodes.createNode(this,config);
        this.name = config.name;
        this.gateenable = config.gateenable;
        this.gatecontext = config.gatecontext;
        this.gatecontexttype = config.gatecontexttype;


        let node = this;
        let flowContext = this.context().flow;
        let globalContext = this.context().global;
        node.on('input', function(msg) {
            msg.payload = msg.payload + String.fromCharCode(0x0A);
            if (this.gateenable === "Disable") {
                this.status({fill:"green",shape:"dot",text:"passed"});
                setTimeout(() => {
                    this.status({});
                  }, 1000);
                node.send(msg);
                return;
            }
            let keystate;
            if (this.gatecontexttype === "flow") {
                keystate = flowContext.get(this.gatecontext);
            }
            else if (this.gatecontexttype === "global") {
                keystate = globalContext.get(this.gatecontext);
            }
            if (keystate == undefined){
                this.status({fill:"yellow",shape:"dot",text:"undefined"});
                setTimeout(() => {
                    this.status({});
                  }, 1000);
                node.done;
                return
            }
            if (keystate === true) {
                this.status({fill:"green",shape:"dot",text:"passed"});
                setTimeout(() => {
                    this.status({});
                  }, 1000);
                node.send(msg);
            }
            else if (keystate === false) {
                this.status({fill:"red",shape:"dot",text:"blocked"});
                setTimeout(() => {
                    this.status({});
                  }, 1000);
                node.done;
            }
        });
    }
    RED.nodes.registerType("add-lf-gate",AddLfGateNode);
}
続いて、.htmlは次のようになります。
<script type="text/javascript">
    RED.nodes.registerType('add-lf-gate',{
        category: 'function',
        color: '#c44646',
        defaults: {
            name: {value: ""},
            gateenable: {value: "Disable"},
            gatecontext: {value: "gate-key"},
            gatecontexttype: {value: "flow"},
        },
        inputs: 1,
        outputs: 1,
        icon: "arrow-in.svg",
        label: function() {
            return this.name||"add-lf-gate";
        },
        oneditprepare: function(){
            const div =
                $("#node-input-gateenable").typedInput({
                    types: [{
                            value: "gateenable",
                            options: [
                                { value: "Disable", label: "Disable"},
                                { value: "Enable", label: "Enable"},
                            ]
                        }]
                })
                $("#node-input-gatecontext").typedInput({
                    type:"msg",
                    types:["flow","global"],
                    typeField: "#node-input-gatecontexttype"
                })
        },
    });
</script>


<script type="text/html" data-template-name="add-lf-gate">
    <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-gateenable"><i class="fa fa-lock"></i> Gate</label>      
        <input type="text" id="node-input-gateenable">
    </div>
    <div class="form-row">
        <label for="node-input-gatecontext"><i class="fa fa-key"></i> Gate Key</label>
        <input type="text" id="node-input-gatecontext" placeholder="Gate key" >
        <input type="hidden" id="node-input-gatecontexttype">
    </div>
</script>


<script type="text/html" data-help-name="add-lf-gate">
    <p>立命館大学カスタムノードサンプル2:菊水電源装置との通信用にLF(0x0A) を付加しgate機能を加えます。</p>
</script>
これを、先のadd-lfと同様にpackage-jsonを作成してパッケージ化してNode-REDにインストールします。先ほど作成したadd-lfノードと入れ替えて、gate-keyコンテキストを操作するフローを設けます。
これでゲート動作ができるようになりました。
Gate-OPENとGate-CLOSEのインジェクトノードを操作してgate-keyコンテキストの真偽値を変更すると、add-lf-gateノードからのコマンド通過/遮断がコントロールできます。装置のモードを固定した状態で運用したい場合のロック機能や、装置の制御の可否を設定するインタロックとしても利用できます。