はじめに
本記事の環境
使用機器一覧
- 【フエニックス・コンタクト】PLCnext EPC 1522 – エッジデバイス 1185423
- 【因幡電機産業】シグナルウォッチャー SE-SW001A
- 【因幡電機産業】EnOceanゲートウェイ シグナルウォッチャー用受信機 NE-GW001A
- 【Pro-face】積層式LED表示灯 EZタワーライト XVGU3SWG
※別途 開発用PCをご用意して下さい。(筆者環境は macOSを使用しています)
※デバッグで使用するEZタワーライトについてはメーカーページよりサンプルソフトのダウンロードが可能です。(PCからEZタワーライトの点灯指示が可能)
STEP1 Node-RED の通信設定
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を実行してください。
STEP2 EnOcean Gatewayの設定
シグナルウォッチャーの製品ページよりSeagull Viewerをダウンロードしてインストール後に起動してください。
Seagull Viewer が起動すると下記画面が表示されますので”ゲートウェイ設定”を実行して下さい。
※Seagull Viewerには本ソフト単体で稼働監視の実現が可能です。そちらについてはまた別の記事でご紹介させていただきます。
設定画面が開いたら、Enoceanゲートウェイへの接続方法に合わせて接続先の選択を行い”接続” ボタンを押してください。
正しく接続されると ”接続中”の表示に変わります。
接続完了後にゲートウェイ本体から読み込みを行うと Enoceanゲートウェイの設定情報が読み込まれます。
PLCnextと接続を行う場合は下記項目の設定を行い”ゲートウェイ本体への書き込み”を実行してください。
※実行時にパスワードの入力を求められます(初期値はEnoceanゲートウェイの取扱説明書に記載があります)
- 動作モード: Enocean スルー
- 本体ネットワーク設定 ※ご使用の環境に合わせてください
- 接続(出力)先設定:LAN
- 接続(出力)先設定 ホストIPアドレス:PLCnextのIP アドレス
- 接続(出力)先設定 ホストポート番号:10000
※”STEP1 Node-RED の通信設定”で設定したポート番号
※ポート番号の変更を行う場合は docker-compose.ymlファイルの設定を変更してください。
STEP3 Node-RED プログラミング
シグナルウォッチャーはEnOcean Radio Protocol 2(ERP2)の通信使用に準拠した無線データが送信されます。
送信されたデータはEnOceanゲートウェイで受信し PLCnextのTCP ポート10000に転送されます。
以下に受信データフォーマットを掲載します。
※詳細はSE-SW001A シグナルウォッチャー本体取扱説明書を参照ください。
今回はシンプルな構成のフローを作成してみました。
処理の流れとしては下記に記載します。
- tcp in ノードで 待ち受け(ポート10000)
- 受信データを function ノードで データ変換
- 変換したデータを後処理で扱いやすい形(JSONデータ)に変換
- 結果出力としては 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のメニュー 読み込み→フローを読み込み画面にデータを貼り付けることでご利用可能です。
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;
STEP4 動作確認(デバッグ)
シグナルタワーの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)に書き込みする方法をご紹介します。