node-red-contrib-cip-ethernet-ip
Version:
A Node-RED node to interact with Allen Bradley / Rockwell PLCs using the EtherNet/IP Protocol
431 lines (347 loc) • 13.2 kB
JavaScript
//@ts-check
/*
Copyright: (c) 2016-2020, St-One Ltda., Guilherme Francescon Cittolin <guilherme@st-one.io>
GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
*/
const net = require('net');
function nrInputShim(node, fn) {
function doErr(err) { err && node.error(err) }
node.on('input', function (msg, send, done) {
send = send || node.send;
done = done || doErr;
fn(msg, send, done);
});
}
module.exports = function (RED) {
"use strict";
const eip = require('ethernet-ip');
const {
Controller,
Tag
} = eip;
const {
EventEmitter
} = require('events');
// ---------- Ethernet-IP Endpoint ----------
function generateStatus(status, val) {
var obj;
if (typeof val != 'string' && typeof val != 'number' && typeof val != 'boolean') {
val = RED._("ethip.endpoint.status.online");
}
switch (status) {
case 'online':
obj = {
fill: 'green',
shape: 'dot',
text: val.toString()
};
break;
case 'badvalues':
obj = {
fill: 'yellow',
shape: 'dot',
text: RED._("ethip.endpoint.status.badvalues")
};
break;
case 'offline':
obj = {
fill: 'red',
shape: 'dot',
text: RED._("ethip.endpoint.status.offline")
};
break;
case 'error':
obj = {
fill: 'red',
shape: 'dot',
text: RED._("ethip.endpoint.status.error")
};
break;
case 'connecting':
obj = {
fill: 'yellow',
shape: 'dot',
text: RED._("ethip.endpoint.status.connecting")
};
break;
default:
obj = {
fill: 'grey',
shape: 'dot',
text: RED._("ethip.endpoint.status.unknown")
};
}
return obj;
}
function EthIpEndpoint(config) {
EventEmitter.call(this);
var node = this;
var status;
var isVerbose = RED.settings.get('verbose');
var connectTimeoutTimer;
var connected = false;
var closing = false;
var tags = new Map();
RED.nodes.createNode(this, config);
//avoids warnings when we have a lot of listener nodes
this.setMaxListeners(0);
//Create tags
config.vartable = config.vartable || {};
for (let prog of Object.keys(config.vartable)) {
for (let varname of Object.keys(config.vartable[prog])) {
if(!varname){
//skip empty values
continue;
}
let obj = config.vartable[prog][varname];
let type = (obj.type || '').toString().toUpperCase();
let dt = eip.EthernetIP.CIP.DataTypes.Types[type] || null;
if (isVerbose) {
node.log(RED._("ethip.info.tagregister") + `: Name:[${varname}], Prog:[${prog}], Type:[${dt}](${type})`);
}
if (!Tag.isValidTagname(varname)){
node.warn(RED._("ethip.warn.invalidtagname", {name: varname}));
continue;
}
let tag = new Tag(varname, prog || null, dt);
tag.on('Initialized', onTagChanged);
tag.on('Changed', onTagChanged);
tags.set(`${prog}:${varname}`, tag);
}
}
node.getStatus = function getStatus() {
return status;
};
node.getTag = function getTag(t) {
return tags.get(t);
};
node.getTags = function getTags(t) {
return tags;
};
node.getAllTagValues = function getAllTagValues() {
let res = {};
node._plc.forEach(tag => {
res[tag.name] = tag.controller_value;
});
return res;
};
function manageStatus(newStatus) {
if (status == newStatus) return;
status = newStatus;
node.emit('__STATUS__', {
status: status
});
}
function onTagChanged(tag, lastValue) {
node.emit('__ALL_CHANGED__', tag, lastValue);
}
function onConnect() {
clearTimeout(connectTimeoutTimer);
manageStatus('online');
connected = true;
for (let t of tags.values()) {
node._plc.subscribe(t);
}
node._plc.scan_rate = parseInt(config.cycletime) || 500;
node._plc.scan().catch(onScanError);
}
function onConnectError(err) {
let errStr = err instanceof Error ? err.toString() : JSON.stringify(err);
node.error(RED._("ethip.error.onconnect") + errStr, {});
onControllerEnd();
}
function onControllerError(err) {
let errStr = err instanceof Error ? err.toString() : JSON.stringify(err);
node.error(RED._("ethip.error.onerror") + errStr, {});
onControllerEnd();
}
function onScanError(err) {
if (closing) {
//closing the connection will cause a timeout error, so let's just skip it
return;
}
//proceed to cleanup and reconnect
onControllerError(err);
}
function onControllerEnd() {
clearTimeout(connectTimeoutTimer);
manageStatus('offline');
connected = false;
// don't restart if we're closing...
if(closing) {
destroyPLC();
return;
} else {
//reset tag values, in case we're dropping the connection because of a wrong value
node._plc.forEach((tag) => {
tag.value = null;
});
}
//try to reconnect if failed to connect
connectTimeoutTimer = setTimeout(connect, 5000);
}
function onControllerClose(err) {
try {
node._plc._handleCloseEvent(err);
} catch (e) {
node.error(`${RED._("ethip.error.onerror")} ${e.message}`, {});
}
}
function destroyPLC() {
if (node._plc) {
node._plc.destroy();
//TODO remove listeners
node._plc.removeListener("close", onControllerClose);
node._plc.removeListener("error", onControllerError);
node._plc.removeListener("end", onControllerEnd);
net.Socket.prototype.destroy.call(node._plc);
node._plc = null;
}
}
function closeConnection(done) {
//ensure we won't try to connect again if anybody wants to close it
clearTimeout(connectTimeoutTimer);
if (isVerbose) {
node.log(RED._("ethip.info.disconnect"));
}
manageStatus('offline');
connected = false;
destroyPLC();
if (typeof done == 'function') {
done();
}
}
// close the connection and remove tag listeners
function onNodeClose(done) {
closing = true;
closeConnection(() => {
for (let tag of tags.values()) {
tag.removeListener('Initialized', onTagChanged);
tag.removeListener('Changed', onTagChanged);
}
done();
});
}
function connect() {
//ensure we won't try to connect again if anybody wants to close it
clearTimeout(connectTimeoutTimer);
// don't restart if we're closing...
if(closing) return;
if (node._plc) {
closeConnection();
}
manageStatus('connecting');
if (isVerbose) {
node.log(RED._("ethip.info.connect") + `: ${config.address} / ${config.slot}`);
}
connected = false;
node._plc = new Controller();
node._plc.removeListener("close", node._plc._handleCloseEvent);
node._plc.on("close", onControllerClose);
node._plc.on("error", onControllerError);
node._plc.on("end", onControllerEnd);
node._plc.connect(config.address, Number(config.slot) || 0).then(onConnect).catch(onConnectError);
}
node.on('close', onNodeClose);
connect();
}
RED.nodes.registerType("eth-ip endpoint", EthIpEndpoint);
// ---------- Ethernet-IP In ----------
function EthIpIn(config) {
const node = this;
let statusVal, tag;
RED.nodes.createNode(this, config);
node.endpoint = RED.nodes.getNode(config.endpoint);
if (!node.endpoint) {
return node.error(RED._("ethip.error.missingconfig"));
}
function onChanged(tag, lastValue) {
let data = tag.controller_value;
let key = tag.name || '';
let msg = {
payload: data,
topic: key,
lastValue: lastValue
};
node.send(msg);
node.status(generateStatus(node.endpoint.getStatus(), config.mode === 'single' ? data : null));
}
function onChangedAllValues() {
let msg = {
payload: node.endpoint.getAllTagValues()
};
node.send(msg);
node.status(generateStatus(node.endpoint.getStatus()));
}
function onEndpointStatus(s) {
node.status(generateStatus(s.status, config.mode === 'single' ? statusVal : null));
}
if (config.mode === 'single') {
let tagName = `${config.program}:${config.variable}`;
tag = node.endpoint.getTag(tagName);
if (!tag) {
//shouldn't reach here. But just in case..
return node.error(RED._("ethip.error.invalidvar", {
varname: tagName
}));
}
tag.on('Initialized', onChanged);
tag.on('Changed', onChanged);
} else if (config.mode === 'all-split') {
node.endpoint.on('__ALL_CHANGED__', onChanged);
} else {
node.endpoint.on('__ALL_CHANGED__', onChangedAllValues);
}
node.status(generateStatus("connecting", ""));
node.endpoint.on('__STATUS__', onEndpointStatus);
node.on('close', function (done) {
node.endpoint.removeListener('__ALL_CHANGED__', onChanged);
node.endpoint.removeListener('__ALL_CHANGED__', onChangedAllValues);
node.endpoint.removeListener('__STATUS__', onEndpointStatus);
if (tag) {
tag.removeListener('Initialized', onChanged);
tag.removeListener('Changed', onChanged);
}
done();
});
}
RED.nodes.registerType("eth-ip in", EthIpIn);
// ---------- Ethernet-IP Out ----------
function EthIpOut(config) {
var node = this;
var statusVal, tag;
RED.nodes.createNode(this, config);
node.endpoint = RED.nodes.getNode(config.endpoint);
if (!node.endpoint) {
return node.error(RED._("ethip.in.error.missingconfig"));
}
function onEndpointStatus(s) {
node.status(generateStatus(s.status, statusVal));
}
function onNewMsg(msg, send, done) {
//the actual write will be performed by the scan cycle
//of the Controller on the endpoint
tag.value = statusVal = msg.payload;
// we currently have no feedback of the written value, so
// let's just call done() here
done();
node.status(generateStatus(node.endpoint.getStatus(), statusVal));
}
let tagName = `${config.program}:${config.variable}`;
tag = node.endpoint.getTag(tagName);
if (!tag) {
//shouldn't reach here. But just in case..
return node.error(RED._("ethip.error.invalidvar", {
varname: tagName
}));
}
node.status(generateStatus("connecting", ""));
nrInputShim(node, onNewMsg);
node.endpoint.on('__STATUS__', onEndpointStatus);
node.on('close', function (done) {
node.endpoint.removeListener('__STATUS__', onEndpointStatus);
done();
});
}
RED.nodes.registerType("eth-ip out", EthIpOut);
};