node-red-contrib-bnr
Version:
A Node-RED Node to communicate with BnR PLCs over UDP
600 lines (480 loc) • 18.6 kB
JavaScript
/*
Copyright: (c) 2022, ST-One
GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
*/
try{
var BnR = require('@protocols/node-bnr').INACpu
var {itemGroup} = require('@protocols/node-bnr')
}catch(error){
var BnR = null;
var itemGroup = null;
};
const MIN_CYCLE_TIME = 500;
module.exports = function (RED) {
// ----------- BnR Endpoint -----------
function generateStatus(status, val) {
var obj;
if (typeof val != 'string' && typeof val != 'number' && typeof val != 'boolean') {
val = RED._('bnr.endpoint.status.online');
}
switch (status) {
case 'online':
obj = {
fill: 'green',
shape: 'dot',
text: val.toString()
};
break;
case 'offline':
obj = {
fill: 'red',
shape: 'dot',
text: RED._('bnr.endpoint.status.offline')
};
break;
case 'connecting':
obj = {
fill: 'yellow',
shape: 'dot',
text: RED._('bnr.endpoint.status.connecting')
};
break;
default:
obj = {
fill: 'grey',
shape: 'dot',
text: RED._('bnr.endpoint.status.unknown')
};
}
return obj;
}
function createTranslationTable(vars) {
var res = {};
vars.forEach(function (elm) {
if (!elm.name || !elm.addr) {
//skip incomplete entries
return;
}
res[elm.name] = elm.addr;
});
return res;
}
function equals(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
return false;
}
function nrInputShim(node, fn) {
node.on('input', function (msg, send, done) {
send = send || node.send;
done = done || (err => err && node.error(err, msg));
fn(msg, send, done);
});
}
async function getAllVariables(inacpu, res) {
let intervalId;
console.log('Waiting for connection...')
const sendWaitingMessage = () => {
console.log('Still waiting for connection...');
};
// Sends 'connecting' message periodically, while not connected
intervalId = setInterval(sendWaitingMessage, 5000);
inacpu.on('error', (e) => {
clearInterval(intervalId);
console.log(`ERROR: ${e}`)
return res.status(500).send(`ERROR: ${e}`).send();
});
inacpu.on('timeout', async () => {
clearInterval(intervalId);
console.log('Connection timeout')
inacpu.disconnect();
inacpu.destroy();
return res.status(500).send( `Connection timeout`).send();
});
inacpu.on('disconnected', () => {
clearInterval(intervalId);
inacpu.removeAllListeners();
console.log("Finished and Disconnected")
});
inacpu.on('connected', async () => {
clearInterval(intervalId);
console.log('Connected! Getting variable list....');
try {
let hasData = false;
res.attachment('varList.csv');
res.write('Variable\n'); // CSV headers
for await (const elm of inacpu.getVariableListGenerator()) {
// Ensure each variable is formatted in a separated line in csv
res.write(`${elm}\n`);
hasData = true;
}
console.log('Success! Downloading Variables List..');
if (!hasData) {
console.warn('No variables available.');
}
res.end();
}
catch (e) {
console.log(`Error retrieving variables: ${e.message}`)
return res.send(
`Error retrieving variables : ${e.message}`
);
}
finally {
await inacpu.disconnect();
await inacpu.destroy();
}
})
try {
await inacpu.connect();
// inacpu.emit("connected")
} catch (e) {
clearInterval(intervalId);
console.log(`Connection failed: ${e.message}`)
return res.status(500).send(`Connection failed: ${e.message}`);
}
}
RED.httpAdmin.get('/__node-red-contrib-bnr/getallvar', async function (req, res) {
const { port, ip, sa, timeout,cycletime } = req.query;
if (!ip||!sa||!port||!timeout){
console.log("Error: Missing parameter for connection")
return res.status(500).send('Missing parameter for connection.')
}
//creates new instance for connection
var inacpu = new BnR(ip, {sa, port, timeout});
if (!inacpu){
console.log("Error: Unable to create instance for connection")
return res.status(500).stend('Error: Unable to create instance for connecion');
}
try {
await getAllVariables(inacpu,res);
} catch (e) {
console.log(`Error : ${e.message}`)
return res.status(500).send(`Error : ${e.message}`);
}
});
// <Begin> --- Endpoint ---
function BNREndpoint(config) {
let oldValues = {};
let readInProgress = false;
let readDeferred = 0;
let currentCycleTime = config.cycletime;
let address = config.ip
let port = config.port
let timeout = config.timeout
let sa = config.sa
let _cycleInterval;
let _reconnectTimeout = null;
let connected = false;
let status;
let that = this;
let bnr = null;
let addressGroup = null;
RED.nodes.createNode(this, config);
//avoids warnings when we have a lot of bnr In nodes
this.setMaxListeners(0);
function manageStatus(newStatus) {
if (status == newStatus) return;
status = newStatus;
that.emit('__STATUS__', status);
}
function doCycle() {
if (!readInProgress && connected) {
readInProgress = true;
addressGroup.readAllitems()
.then(result => {
cycleCallback(result);
})
.catch(error => {
onError(error);
readInProgress = false;
});
} else {
readDeferred++;
}
}
function cycleCallback(values) {
readInProgress = false;
if (readDeferred && connected) {
doCycle();
readDeferred = 0;
}
manageStatus('online');
var changed = false;
that.emit('__ALL__', values);
Object.keys(values).forEach(function (key) {
if (!equals(oldValues[key], values[key])) {
changed = true;
that.emit(key, values[key]);
that.emit('__CHANGED__', {
key: key,
value: values[key]
});
oldValues[key] = values[key];
}
});
if (changed) that.emit('__ALL_CHANGED__', values);
}
function updateCycleTime(interval) {
let time = parseInt(interval);
if (isNaN(time) || time < 0) {
that.error(RED._("bnr.endpoint.error.invalidtimeinterval", { interval: interval }));
return false
}
clearInterval(_cycleInterval);
// don't set a new timer if value is zero
if (!time) return false;
if (time < MIN_CYCLE_TIME) {
that.warn(RED._("bnr.endpoint.info.cycletimetooshort", { min: MIN_CYCLE_TIME }));
time = MIN_CYCLE_TIME;
}
currentCycleTime = time;
_cycleInterval = setInterval(doCycle, time);
return true;
}
function removeListeners() {
if (bnr !== null) {
bnr.removeListener('connected', onConnect);
bnr.removeListener('disconnected', onDisconnect);
bnr.removeListener('error', onError);
bnr.removeListener('timeout', onTimeout);
}
}
/**
* Destroys the bnr connection
* @param {Boolean} [reconnect=true]
* @returns {Promise}
*/
async function disconnect(reconnect = true) {
connected = false;
clearInterval(_cycleInterval);
_cycleInterval = null;
if (bnr) {
if (!reconnect) bnr.removeListener('disconnected', onDisconnect);
try {
await bnr.destroy();
removeListeners();
bnr = null;
} catch (err) {
onError(err);
}
}
console.log("Endpoint - disconnect");
}
async function connect() {
if (!BnR || !itemGroup) return that.error('Missing "@protocols/node-bnr" dependency, avaliable only on the ST-One hardware. Please contact us at "st-one.io" for pricing and more information.')
manageStatus('connecting');
if (_reconnectTimeout !== null) {
clearTimeout(_reconnectTimeout);
_reconnectTimeout = null;
}
if (bnr !== null) {
await disconnect();
}
bnr = new BnR(address, {sa, port, timeout});
bnr.on('connected', onConnect);
bnr.on('disconnected', onDisconnect);
bnr.on('error', onError);
bnr.on('timeout', onTimeout);
bnr.connect()
}
function onConnect() {
readInProgress = false;
readDeferred = 0;
connected = true;
addressGroup = new itemGroup(bnr);
manageStatus('online');
let _vars = createTranslationTable(config.vartable);
addressGroup.setTranslationCB(k => _vars[k]);
let varKeys = Object.keys(_vars);
if (!varKeys || !varKeys.length) {
that.warn(RED._("bnr.endpoint.info.novars"));
} else {
addressGroup.addItems(varKeys);
updateCycleTime(currentCycleTime);
}
}
function onDisconnect() {
manageStatus('offline');
if (!_reconnectTimeout) {
_reconnectTimeout = setTimeout(connect, 4000);
}
}
function onError(e) {
manageStatus('offline');
that.error(e && e.toString());
disconnect();
}
function onTimeout(e) {
manageStatus('offline');
that.error(e && e.toString());
disconnect();
}
function getStatus() {
that.emit('__STATUS__', status);
}
function updateCycleEvent(obj) {
obj.err = updateCycleTime(obj.msg.payload);
that.emit('__UPDATE_CYCLE_RES__', obj);
}
manageStatus('offline');
this.on('__DO_CYCLE__', doCycle);
this.on('__UPDATE_CYCLE__', updateCycleEvent);
this.on('__GET_STATUS__', getStatus);
connect();
this.on('close', done => {
manageStatus('offline');
clearInterval(_cycleInterval);
clearTimeout(_reconnectTimeout);
_cycleInterval = null
_reconnectTimeout = null;
that.removeListener('__DO_CYCLE__', doCycle);
that.removeListener('__UPDATE_CYCLE__', updateCycleEvent);
that.removeListener('__GET_STATUS__', getStatus);
disconnect(false)
.then(done)
.catch(err => onError(err))//TODO:
console.log("Endpoint - on close!");
});
}
RED.nodes.registerType('bnr endpoint', BNREndpoint);
// <End> --- Endpoint
// <Begin> --- BnR In
function BNRIn(config) {
RED.nodes.createNode(this, config);
let statusVal;
let that = this
let endpoint = RED.nodes.getNode(config.endpoint);
if (!endpoint) {
that.error(RED._("bnr.error.missingconfig"));
return;
}
function sendMsg(data, key, status) {
if (key === undefined) key = '';
if (data instanceof Date) data = data.getTime();
var msg = {
payload: data,
topic: key
};
statusVal = status !== undefined ? status : data;
that.send(msg);
endpoint.emit('__GET_STATUS__');
}
function onChanged(variable) {
sendMsg(variable.value, variable.key, null);
}
function onDataSplit(data) {
Object.keys(data).forEach(function (key) {
sendMsg(data[key], key, null);
});
}
function onData(data) {
sendMsg(data, config.mode == 'single' ? config.variable : '');
}
function onDataSelect(data) {
onData(data[config.variable]);
}
function onEndpointStatus(status) {
that.status(generateStatus(status, statusVal));
}
endpoint.on('__STATUS__', onEndpointStatus);
endpoint.emit('__GET_STATUS__');
if (config.diff) {
switch (config.mode) {
case 'all-split':
endpoint.on('__CHANGED__', onChanged);
break;
case 'single':
endpoint.on(config.variable, onData);
break;
case 'all':
default:
endpoint.on('__ALL_CHANGED__', onData);
}
} else {
switch (config.mode) {
case 'all-split':
endpoint.on('__ALL__', onDataSplit);
break;
case 'single':
endpoint.on('__ALL__', onDataSelect);
break;
case 'all':
default:
endpoint.on('__ALL__', onData);
}
}
this.on('close', function (done) {
endpoint.removeListener('__ALL__', onDataSelect);
endpoint.removeListener('__ALL__', onDataSplit);
endpoint.removeListener('__ALL__', onData);
endpoint.removeListener('__ALL_CHANGED__', onData);
endpoint.removeListener('__CHANGED__', onChanged);
endpoint.removeListener('__STATUS__', onEndpointStatus);
endpoint.removeListener(config.variable, onData);
done();
});
}
RED.nodes.registerType('bnr in', BNRIn);
// <End> --- BnR In
// <Begin> --- BnR Control
function BNRControl(config) {
let that = this;
RED.nodes.createNode(this, config);
let endpoint = RED.nodes.getNode(config.endpoint);
if (!endpoint) {
this.error(RED._("bnr.error.missingconfig"));
return;
}
function onEndpointStatus(status) {
that.status(generateStatus(status));
}
function onMessage(msg, send, done) {
let func = config.function || msg.function;
switch (func) {
case 'cycletime':
endpoint.emit('__UPDATE_CYCLE__', {
msg: msg,
send: send,
done: done
});
break;
case 'trigger':
endpoint.emit('__DO_CYCLE__');
send(msg);
done();
break;
default:
this.error(RED._("bnr.error.invalidcontrolfunction", { function: config.function }), msg);
}
}
function onUpdateCycle(res) {
let err = res.err;
if (!err) {
res.done(err);
} else {
res.send(res.msg);
res.done();
}
}
endpoint.on('__STATUS__', onEndpointStatus);
endpoint.on('__UPDATE_CYCLE_RES__', onUpdateCycle);
endpoint.emit('__GET_STATUS__');
nrInputShim(this, onMessage);
this.on('close', function (done) {
endpoint.removeListener('__STATUS__', onEndpointStatus);
endpoint.removeListener('__UPDATE_CYCLE_RES__', onUpdateCycle);
done();
});
}
RED.nodes.registerType("bnr control", BNRControl);
// <End> --- BnR Control
};