PLCnext で簡単IoT③ ~Node-RED+シグナルウォッチャーでデータ収集~

  • URLをコピーしました!

本記事ではPLCnextを使用して”シグナルウォッチャー”のデータ収集の方法についてご紹介します。
※PLCnext で簡単IoT② ~セットアップ編~ でNode-RED がインストールされている前提で記載していますのでセットアップがまだの場合は前回記事 PLCnext で簡単IoT② ~セットアップ編~ を参照してください。

目次

本記事の環境

使用機器一覧

  • 【フエニックス・コンタクト】PLCnext EPC 1522 – エッジデバイス 1185423
  • 【因幡電機産業】シグナルウォッチャー SE-SW001A
  • 【因幡電機産業】EnOceanゲートウェイ シグナルウォッチャー用受信機 NE-GW001A
  • 【Pro-face】積層式LED表示灯 EZタワーライト XVGU3SWG

※別途 開発用PCをご用意して下さい。(筆者環境は macOSを使用しています)
※デバッグで使用するEZタワーライトについてはメーカーページよりサンプルソフトのダウンロードが可能です。(PCからEZタワーライトの点灯指示が可能)

sshでPLCnextに接続
※ipアドレスは環境に合わせてください。
※接続後のパスワードはPLCnext本体の背面シールに記載があります。
※Windows環境の場合 Tera Term等のターミナルエミュレータソフトウェアでSSH接続が便利です。

sudo ssh admin@192.168.3.110

接続後にNode-REDの設定ファイル(ymlファイル)にアクセス

admin@epc1522:/$ cd /opt/plcnext/appshome/data/60002172000551/
admin@epc1522:/opt/plcnext/appshome/data/60002172000551$ vi docker-compose.yml

※/data/以下のディレクトリ名(60002172000551)は管理画面(Administration-PLCnext Apps)に表示されているApp IDで確認可能です

vi docker-compose.ymlに接続ポート(10000)を追加

version: "3.7"
services:
  node-red:
    image: ${IMAGE_NAME}:${IMAGE_TAG}
    ports:
      - 51880:1880
      - 10000:10000
    user: ${USER_ID}
    volumes:
      - ./volumes/node-red:/data
    restart: unless-stopped

設定が完了したら 管理画面の(Edge -Settings)からRestartを実行してください。

シグナルウォッチャーの製品ページよりSeagull Viewerをダウンロードしてインストール後に起動してください。

Seagull Viewer が起動すると下記画面が表示されますので”ゲートウェイ設定”を実行して下さい。
※Seagull Viewerには本ソフト単体で稼働監視の実現が可能です。そちらについてはまた別の記事でご紹介させていただきます。 

設定画面が開いたら、Enoceanゲートウェイへの接続方法に合わせて接続先の選択を行い”接続” ボタンを押してください。
正しく接続されると ”接続中”の表示に変わります。

接続完了後にゲートウェイ本体から読み込みを行うと Enoceanゲートウェイの設定情報が読み込まれます。

PLCnextと接続を行う場合は下記項目の設定を行い”ゲートウェイ本体への書き込み”を実行してください。
※実行時にパスワードの入力を求められます(初期値はEnoceanゲートウェイの取扱説明書に記載があります)

  • 動作モード: Enocean スルー
  • 本体ネットワーク設定 ※ご使用の環境に合わせてください
  • 接続(出力)先設定:LAN
  • 接続(出力)先設定 ホストIPアドレス:PLCnextのIP アドレス
  • 接続(出力)先設定 ホストポート番号:10000
    ※”STEP1 Node-RED の通信設定”で設定したポート番号
    ※ポート番号の変更を行う場合は docker-compose.ymlファイルの設定を変更してください。

シグナルウォッチャーはEnOcean Radio Protocol 2(ERP2)の通信使用に準拠した無線データが送信されます。
送信されたデータはEnOceanゲートウェイで受信し PLCnextのTCP ポート10000に転送されます。
以下に受信データフォーマットを掲載します。
※詳細はSE-SW001A シグナルウォッチャー本体取扱説明書を参照ください。

シグナルウォッチャー本体取扱説明書より抜粋
シグナルウォッチャー本体取扱説明書より抜粋
シグナルウォッチャー本体取扱説明書より抜粋

今回はシンプルな構成のフローを作成してみました。
処理の流れとしては下記に記載します。

  1. tcp in ノードで 待ち受け(ポート10000)
  2. 受信データを function ノードで データ変換
  3. 変換したデータを後処理で扱いやすい形(JSONデータ)に変換
  4. 結果出力としては debug1でtcp in ノードの受信データを出力とデータ変換後(JSON形式)のデータをdebug2 で出力
フロー全体

JSON形式のフローデータ

[
    {
        "id": "88cda9d44b51e39e",
        "type": "tab",
        "label": "PLCnext-signalwatcher",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "180024e257ef9d64",
        "type": "tcp in",
        "z": "88cda9d44b51e39e",
        "name": "",
        "server": "server",
        "host": "",
        "port": "10000",
        "datamode": "stream",
        "datatype": "buffer",
        "newline": "",
        "topic": "",
        "trim": false,
        "base64": false,
        "tls": "",
        "x": 200,
        "y": 300,
        "wires": [
            [
                "8e7815328b0f761c",
                "e4cc53e0cbcbb954"
            ]
        ]
    },
    {
        "id": "0b0e7ee27eeee616",
        "type": "debug",
        "z": "88cda9d44b51e39e",
        "name": "debug 2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 940,
        "y": 300,
        "wires": []
    },
    {
        "id": "8e7815328b0f761c",
        "type": "function",
        "z": "88cda9d44b51e39e",
        "name": "signalwatcher",
        "func": "// Node-REDのファンクションノード用のコード\n// バイト配列をESP3プロトコルデータに変換し、Teach-inパケット構造(ERP2)およびDATAパケット構造(ERP2)に変換する\n\n// Teach-inパケット構造(ERP2)を解析する関数\nfunction parseTeachInPacket(data) {\n    const uniqueID = data.slice(2, 6);\n    const uniqueIDHexString = uniqueID[0].toString(16).padStart(2, '0').toUpperCase() + uniqueID[1].toString(16).padStart(2, '0').toUpperCase() + uniqueID[2].toString(16).padStart(2, '0').toUpperCase() + uniqueID[3].toString(16).padStart(2, '0').toUpperCase();\n    return {\n        extendedHeader: data[0],\n        extendedTelegramType: data[1],\n        uniqueID: uniqueIDHexString,\n        teachInRequestHeader: data.slice(6, 8),\n        heartbeatDefinition: data.slice(8, 10),\n        batteryDefinition: data.slice(10, 12),\n        ch1StatusDefinition: data.slice(12, 14),\n        ch2StatusDefinition: data.slice(14, 16),\n        ch3StatusDefinition: data.slice(16, 18),\n        ch4StatusDefinition: data.slice(18, 20),\n        crc: data[20]\n    };\n}\n\n// DATAパケット構造(ERP2)を解析する関数\nfunction parseDataPacket(data) {\n    const uniqueID = data.slice(2, 6);\n    const uniqueIDHexString = uniqueID[0].toString(16).padStart(2, '0').toUpperCase() + uniqueID[1].toString(16).padStart(2, '0').toUpperCase() + uniqueID[2].toString(16).padStart(2, '0').toUpperCase() + uniqueID[3].toString(16).padStart(2, '0').toUpperCase();\n    return {\n        extendedHeader: data[0],\n        extendedTelegramType: data[1],\n        uniqueID: uniqueIDHexString,\n        data1: {\n            heartBeat: (data[6] & 0xC0) >> 6, // Bit7-6\n            batteryStatus: (data[6] & 0x30) >> 4, // Bit5-4\n            ch1LightStatus: data[6] & 0x0F // Bit3-0\n        },\n        data2: {\n            ch2LightStatus: (data[7] & 0xF0) >> 4, // Bit7-4\n            ch3LightStatus: data[7] & 0x0F // Bit3-0\n        },\n        data3: {\n            ch4LightStatus: (data[8] & 0xF0) >> 4, // Bit7-4\n            fwVersion: data[8] & 0x0F // Bit3-0\n        },\n        crc: data[9]\n    };\n}\n\n\n// バイト配列をESP3プロトコルのデータに変換する関数\nfunction parseESP3Data(byteArray) {\n    let data = {\n        syncByte: byteArray[0],\n        header: {\n            dataLength: (byteArray[1] << 8) | byteArray[2],\n            optionalLength: byteArray[3],\n            packetType: byteArray[4]\n        },\n        crc8h: byteArray[5],\n        data: byteArray.slice(6, 6 + ((byteArray[1] << 8) | byteArray[2])),\n        optionalData: byteArray.slice(6 + ((byteArray[1] << 8) | byteArray[2]), 6 + ((byteArray[1] << 8) | byteArray[2]) + byteArray[3]),\n        crc8d: byteArray[6 + ((byteArray[1] << 8) | byteArray[2]) + byteArray[3]]\n    };\n\n    // データフィールドを解析\n    let parsedData;\n    if (data.data[0] === 0x2F && data.data[1] === 0x05) {\n        parsedData = parseTeachInPacket(data.data);\n    } else if (data.data[0] === 0x2F && data.data[1] === 0x07) {\n        parsedData = parseDataPacket(data.data);\n    } else {\n        parsedData = { error: \"Unknown packet type\" };\n    }\n\n    return {\n        ...data,\n        parsedData: parsedData\n    };\n}\n\n// Node-REDのmsg.payloadからバイト配列を取得\nlet byteArray = msg.payload;\n\n// ESP3プロトコルデータに変換\nlet esp3Data = parseESP3Data(byteArray);\n\n// JSON形式に変換\nmsg.payload = JSON.stringify(esp3Data);\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 440,
        "y": 300,
        "wires": [
            [
                "a9b368867e35330b"
            ]
        ]
    },
    {
        "id": "a9b368867e35330b",
        "type": "json",
        "z": "88cda9d44b51e39e",
        "name": "",
        "property": "payload",
        "action": "",
        "pretty": false,
        "x": 710,
        "y": 300,
        "wires": [
            [
                "0b0e7ee27eeee616"
            ]
        ]
    },
    {
        "id": "e4cc53e0cbcbb954",
        "type": "debug",
        "z": "88cda9d44b51e39e",
        "name": "debug 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 420,
        "y": 200,
        "wires": []
    }
]

※上記 JSON形式のフローデータは Node-REDのメニュー 読み込み→フローを読み込み画面にデータを貼り付けることでご利用可能です。

Node-REDのメニュー

functionノードのコード部分(上記 JSON形式のフローデータにも同一のコードが含まれています)を以下に記載します。

// Node-REDのファンクションノード用のコード
// バイト配列をESP3プロトコルデータに変換し、Teach-inパケット構造(ERP2)およびDATAパケット構造(ERP2)に変換する

// Teach-inパケット構造(ERP2)を解析する関数
function parseTeachInPacket(data) {
    const uniqueID = data.slice(2, 6);
    const uniqueIDHexString = uniqueID[0].toString(16).padStart(2, '0').toUpperCase() + uniqueID[1].toString(16).padStart(2, '0').toUpperCase() + uniqueID[2].toString(16).padStart(2, '0').toUpperCase() + uniqueID[3].toString(16).padStart(2, '0').toUpperCase();
    return {
        extendedHeader: data[0],
        extendedTelegramType: data[1],
        uniqueID: uniqueIDHexString,
        teachInRequestHeader: data.slice(6, 8),
        heartbeatDefinition: data.slice(8, 10),
        batteryDefinition: data.slice(10, 12),
        ch1StatusDefinition: data.slice(12, 14),
        ch2StatusDefinition: data.slice(14, 16),
        ch3StatusDefinition: data.slice(16, 18),
        ch4StatusDefinition: data.slice(18, 20),
        crc: data[20]
    };
}

// DATAパケット構造(ERP2)を解析する関数
function parseDataPacket(data) {
    const uniqueID = data.slice(2, 6);
    const uniqueIDHexString = uniqueID[0].toString(16).padStart(2, '0').toUpperCase() + uniqueID[1].toString(16).padStart(2, '0').toUpperCase() + uniqueID[2].toString(16).padStart(2, '0').toUpperCase() + uniqueID[3].toString(16).padStart(2, '0').toUpperCase();
    return {
        extendedHeader: data[0],
        extendedTelegramType: data[1],
        uniqueID: uniqueIDHexString,
        data1: {
            heartBeat: (data[6] & 0xC0) >> 6, // Bit7-6
            batteryStatus: (data[6] & 0x30) >> 4, // Bit5-4
            ch1LightStatus: data[6] & 0x0F // Bit3-0
        },
        data2: {
            ch2LightStatus: (data[7] & 0xF0) >> 4, // Bit7-4
            ch3LightStatus: data[7] & 0x0F // Bit3-0
        },
        data3: {
            ch4LightStatus: (data[8] & 0xF0) >> 4, // Bit7-4
            fwVersion: data[8] & 0x0F // Bit3-0
        },
        crc: data[9]
    };
}


// バイト配列をESP3プロトコルのデータに変換する関数
function parseESP3Data(byteArray) {
    let data = {
        syncByte: byteArray[0],
        header: {
            dataLength: (byteArray[1] << 8) | byteArray[2],
            optionalLength: byteArray[3],
            packetType: byteArray[4]
        },
        crc8h: byteArray[5],
        data: byteArray.slice(6, 6 + ((byteArray[1] << 8) | byteArray[2])),
        optionalData: byteArray.slice(6 + ((byteArray[1] << 8) | byteArray[2]), 6 + ((byteArray[1] << 8) | byteArray[2]) + byteArray[3]),
        crc8d: byteArray[6 + ((byteArray[1] << 8) | byteArray[2]) + byteArray[3]]
    };

    // データフィールドを解析
    let parsedData;
    if (data.data[0] === 0x2F && data.data[1] === 0x05) {
        parsedData = parseTeachInPacket(data.data);
    } else if (data.data[0] === 0x2F && data.data[1] === 0x07) {
        parsedData = parseDataPacket(data.data);
    } else {
        parsedData = { error: "Unknown packet type" };
    }

    return {
        ...data,
        parsedData: parsedData
    };
}

// Node-REDのmsg.payloadからバイト配列を取得
let byteArray = msg.payload;

// ESP3プロトコルデータに変換
let esp3Data = parseESP3Data(byteArray);

// JSON形式に変換
msg.payload = JSON.stringify(esp3Data);

return msg;

シグナルタワーの3段目を点灯
以下Node-REDのデバッグ情報

tcp-in ノードの受信データ(ポート 10000)

2025/1/7 0:05:38ノード: debug 1
msg.payload : buffer[19]
buffer[19]
[0 … 9]
0: 0x55
1: 0x0
2: 0xa
3: 0x2
4: 0xa
5: 0x9b
6: 0x2f
7: 0x7
8: 0x5
9: 0x86
[10 … 18]
10: 0x6
11: 0x4f
12: 0x30
13: 0x1
14: 0x2
15: 0x95
16: 0x1
17: 0x37
18: 0x90

functionノードでデータのParseを行い、Jsonフォーマットに変換したデータ
※ch3LightStatusが 点灯(1)として出力されていることが確認できます。

2025/1/7 0:05:38ノード: debug 2
msg.payload : Object
object
syncByte: 85
header: object
crc8h: 155
data: buffer[10]
optionalData: buffer[2]
0: 0x1
1: 0x37
crc8d: 144
parsedData: object
extendedHeader: 47
extendedTelegramType: 7
uniqueID: "0586064F"
data1: object
heartBeat: 0
batteryStatus: 3
ch1LightStatus: 0
data2: object
ch2LightStatus: 0
ch3LightStatus: 1
data3: object
ch4LightStatus: 0
fwVersion: 2
crc: 149

おわりに

以上でPLCnextを使用した~Node-RED+シグナルウォッチャーでデータ収集~が完了となります。
次回の記事では本記事の稼働信号データをPLC(MELSEC)に書き込みする方法をご紹介します。

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

FA向けアプリケーション開発、IoT、Azure、M5Stack、Arduino
Linux、Node-RED等 何でも屋

目次