UNPKG

prisme-flow

Version:

prisme platform flow engine

816 lines (710 loc) 27.8 kB
/** * Created by prisme.io on 09/06/2017. */ module.exports = function (RED) { "use strict"; var opcua = require('node-opcua') var opcuaBasics = require('./opcua-basics'); var nodeId = require('node-opcua/lib/datamodel/nodeid'); var UAProxyManager = require("node-opcua/lib/client/proxy").UAProxyManager; var coerceNodeId = require("node-opcua/lib/datamodel/nodeid").coerceNodeId; var makeNodeId = require("node-opcua/lib/datamodel/nodeid").makeNodeId; var browse_service = require("node-opcua/lib/services/browse_service"); var async = require("async"); var treeify = require('treeify'); var Set = require("collections/set"); var DataType = opcua.DataType; var AttributeIds = opcua.AttributeIds; function OpcUaClientNode(n) { RED.nodes.createNode(this, n); this.name = n.name; this.action = n.action; this.time = n.time; this.timeUnit = n.timeUnit; var node = this; var opcuaEndpoint = RED.nodes.getNode(n.endpoint); var userIdentity = {}; var connectionOption = {}; connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint.securityPolicy] || opcua.SecurityPolicy.None; connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode] || opcua.MessageSecurityMode.NONE; verbose_log(opcuaEndpoint); if (opcuaEndpoint.login) { userIdentity.userName = opcuaEndpoint.credentials.user; userIdentity.password = opcuaEndpoint.credentials.password; } var items = []; var subscription; // only one subscription needed to hold multiple monitored Items var monitoredItems = new Set(null, function (a, b) { return a.topicName === b.topicName; }, function (object) { return object.topicName; }); // multiple monitored Items should be registered only once function verbose_warn(logMessage) { if (RED.settings.verbose) { node.warn((node.name) ? node.name + ': ' + logMessage : 'OpcUaClientNode: ' + logMessage); } } function verbose_log(logMessage) { if (RED.settings.verbose) { node.log(logMessage); } } function getBrowseName(session, nodeId, callback) { session.read([{nodeId: nodeId, attributeId: AttributeIds.BrowseName}], function (err, org, readValue) { if (!err) { if (readValue[0].statusCode === opcua.StatusCodes.Good) { var browseName = readValue[0].value.value.name; return callback(null, browseName); } } callback(err, "<??>"); }) } // Fields selected alarm fields // EventFields same order returned from server array of variants (filled or empty) function __dumpEvent(node, session, fields, eventFields, _callback) { var msg = {}; msg.payload = []; verbose_log("EventFields=" + eventFields); async.forEachOf(eventFields, function (variant, index, callback) { if (variant.dataType === DataType.Null) { return callback("variants dataType is Null"); } if (variant.dataType === DataType.NodeId) { getBrowseName(session, variant.value, function (err, name) { if (!err) { opcuaBasics.collectAlarmFields(fields[index], variant.dataType.key.toString(), variant.value, msg); set_node_status_to("active event"); node.send(msg); } callback(err); }); } else { setImmediate(function () { opcuaBasics.collectAlarmFields(fields[index], variant.dataType.key.toString(), variant.value, msg); set_node_status_to("active event"); callback(); }) } }, _callback); } var eventQueue = new async.queue(function (task, callback) { __dumpEvent(task.node, task.session, task.fields, task.eventFields, callback); }); function dumpEvent(node, session, fields, eventFields, _callback) { eventQueue.push({ node: node, session: session, fields: fields, eventFields: eventFields, _callback: _callback }); } function create_opcua_client(callback) { node.client = null; verbose_warn("create Client ..."); verbose_log(connectionOption); node.client = new opcua.OPCUAClient(connectionOption); items = []; node.items = items; set_node_status_to("create client"); if (callback) { callback(); } } function reset_opcua_client(callback) { if (node.client) { node.client.disconnect(function () { verbose_log("Client disconnected!"); create_opcua_client(callback); }); } } function close_opcua_client(callback) { if (node.client) { node.client.disconnect(function () { node.client = null; verbose_log("Client disconnected!"); if (callback) { callback(); } }); } } function set_node_status_to(statusValue) { verbose_log("Client status: " + statusValue); var statusParameter = opcuaBasics.get_node_status(statusValue); node.status({fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status}); } function connect_opcua_client() { node.session = null; async.series([ // First connect to server´s endpoint function (callback) { verbose_log("async series - connecting ", opcuaEndpoint.endpoint); try { set_node_status_to("connecting"); node.client.connect(opcuaEndpoint.endpoint, callback); } catch (err) { callback(err); } }, function (callback) { verbose_log("async series - create session ..."); try { node.client.createSession(userIdentity, function (err, session) { if (!err) { node.session = session; node.session.timeout = opcuaBasics.calc_milliseconds_by_time_and_unit(10, "s"); node.session.startKeepAliveManager(); // General for read/write/subscriptions/events verbose_log("session active"); set_node_status_to("session active"); callback(); } else { set_node_status_to("session error"); callback(err); } }); } catch (err) { callback(err); } } ], function (err) { if (err) { node.error(node.name + " OPC UA connection error: " + err.message); verbose_log(err); node.session = null; close_opcua_client(set_node_status_to("connection error")); } }); } function make_subscription(callback, msg, parameters) { var newSubscription = null; if (!node.session) { verbose_log("Subscription without session"); return newSubscription; } if (!parameters) { verbose_log("Subscription without parameters"); return newSubscription; } newSubscription = new opcua.ClientSubscription(node.session, parameters); newSubscription.on("initialized", function () { verbose_log("Subscription initialized"); set_node_status_to("initialized"); }); newSubscription.on("started", function () { verbose_log("Subscription subscribed ID: " + newSubscription.subscriptionId); set_node_status_to("subscribed"); monitoredItems.clear(); callback(newSubscription, msg); }); newSubscription.on("keepalive", function () { verbose_log("Subscription keepalive ID: " + newSubscription.subscriptionId); set_node_status_to("keepalive"); }); newSubscription.on("terminated", function () { verbose_log("Subscription terminated ID: " + newSubscription.subscriptionId); set_node_status_to("terminated"); subscription = null; monitoredItems.clear(); }); return newSubscription; } create_opcua_client(connect_opcua_client); node.on("input", function (msg) { if (!node.action) { verbose_warn("can't work without action (read, write, browse ...)"); //node.send(msg); // do not send in case of error return; } if (!node.client || !node.session) { verbose_warn("can't work without OPC UA Session"); reset_opcua_client(connect_opcua_client); //node.send(msg); // do not send in case of error return; } // node.warn("secureChannelId:" + node.session.secureChannelId); if (!node.session.sessionId == "terminated") { verbose_warn("terminated OPC UA Session"); reset_opcua_client(connect_opcua_client); //node.send(msg); // do not send in case of error return; } if (!msg || !msg.topic) { verbose_warn("can't work without OPC UA NodeId - msg.topic"); //node.send(msg); // do not send in case of error return; } verbose_log("Action on input:" + node.action + " Item from Topic: " + msg.topic + " session Id: " + node.session.sessionId); switch (node.action) { case "read": read_action_input(msg); break; case "info": info_action_input(msg); break; case "write": write_action_input(msg); break; case "subscribe": subscribe_action_input(msg); break; case "browse": browse_action_input(msg); break; case "events": subscribe_events_input(msg); break; default: break; } //node.send(msg); // msg.payload is here actual inject caused wrong values }); function read_action_input(msg) { verbose_log("reading"); var item=""; if (msg.topic) { var n = msg.topic.indexOf("datatype="); if (n>0) { msg.datatype=msg.topic.substring(n+9); item=msg.topic.substring(0,n-1); msg.topic=item; verbose_log(JSON.stringify(msg)); } } if (item.length>0) { items[0]=item; } else { items[0] = msg.topic; // TODO support for multiple item reading } if (node.session) { node.session.readVariableValue(items, function (err, dataValues, diagnostics) { if (err) { verbose_log('diagnostics:' + diagnostics); node.error(err); set_node_status_to("error"); reset_opcua_client(connect_opcua_client); } else { set_node_status_to("active reading"); for (var i = 0; i < dataValues.length; i++) { var dataValue = dataValues[i]; verbose_log("\tNode : " + (msg.topic).cyan.bold); if (dataValue) { try { verbose_log("\tValue : " + dataValue.value.value); verbose_log("\tDataType: " + dataValue.value.dataType + " ("+dataValue.value.dataType.toString()+")"); verbose_log("\tMessage: " + msg.topic + " ("+msg.datatype+")"); if (msg.datatype!=null && msg.datatype.localeCompare(dataValue.value.dataType.toString())!=0) { node.error("\tMessage types are not matching: " + msg.topic + " types: " + msg.datatype + " <> " + dataValue.value.dataType.toString()); } if (msg.datatype==null) { node.warn("msg.datatype == null, if you use inject check topic is format 'ns=2;s=MyLevel;datatype=Float'"); } if (dataValue.value.dataType === opcua.DataType.UInt16) { verbose_log("UInt16:" + dataValue.value.value + " -> Int32:" + opcuaBasics.toInt32(dataValue.value.value)); } msg.payload = dataValue.value.value; if (dataValue.statusCode && dataValue.statusCode.toString(16) == "Good (0x00000)") { verbose_log("\tStatus-Code:" + (dataValue.statusCode.toString(16)).green.bold); } else { verbose_log("\tStatus-Code:" + dataValue.statusCode.toString(16).red.bold); } node.send(msg); } catch (e) { if (dataValue) { node.error("\tBad read: " + (dataValue.statusCode.toString(16)).red.bold); node.error("\tMessage:" + msg.topic + " dataType:" + msg.datatype); node.error("\tData:" + JSON.stringify(dataValue)); } else { node.error(e.message); } } } } } }); } else { set_node_status_to("Session invalid"); node.error("Session is not active!") } } function info_action_input(msg) { verbose_log("meta-data reading"); var item=""; if (msg.topic) { var n = msg.topic.indexOf("datatype="); if (n>0) { msg.datatype=msg.topic.substring(n+9); item=msg.topic.substring(0,n-1); msg.topic=item; verbose_log(JSON.stringify(msg)); } } if (item.length>0) { items[0]=item; } else { items[0] = msg.topic; // TODO support for multiple item reading } if (node.session) { var nodeId = coerceNodeId(items[0]); var typeStr=""; node.session.readVariableValue(nodeId, function(err,dataValue) { if (!err) { console.log(dataValue.value.dataType.toString()); typeStr = dataValue.value.dataType.toString(); } }); var proxyManager = new UAProxyManager(node.session); proxyManager.getObject(nodeId, function(err,data) { if (!err) { //console.log(data); if (data.typeDefinition!="FolderType") { var object = {}; try { object = JSON.parse(JSON.stringify(data)); } catch(err) { node.error(err); node.warn(data); return; } // console.log(object); msg.payload = {}; if (object.description!=null){ msg.payload.description = object.description; } else { msg.payload.description = ""; } msg.payload.browseName = object.browseName.name; msg.payload.userAccessLevel = object.userAccessLevel; msg.payload.accessLevel = object.accessLevel; msg.payload.type = typeStr; node.send(msg); } } else { node.error(err); set_node_status_to("error"); reset_opcua_client(connect_opcua_client); } }) } else { set_node_status_to("Session invalid"); node.error("Session is not active!") } } function write_action_input(msg) { verbose_log("writing"); // Topic value: ns=2;s=1:PST-007-Alarm-Level@Training?SETPOINT var ns = msg.topic.substring(3, 4); // Parse namespace, ns=2 var dIndex = msg.topic.indexOf("datatype="); var s = ""; if (msg.datatype==null && dIndex>0) { msg.datatype=msg.topic.substring(dIndex+9); s = msg.topic.substring(7, dIndex-1); } else { s = msg.topic.substring(7); // Parse nodeId string, s=1:PST-007-Alarm-Level@Training?SETPOINT } var nodeid = {}; // new nodeId.NodeId(nodeId.NodeIdType.STRING, s, ns); verbose_log(opcua.browse_service.makeBrowsePath(msg.topic, ".")); if (msg.topic.substring(5,6)=='s') { nodeid = new nodeId.NodeId(nodeId.NodeIdType.STRING, s, parseInt(ns)); } else{ nodeid = new nodeId.NodeId(nodeId.NodeIdType.NUMERIC, parseInt(s), parseInt(ns)); } verbose_log("msg=" + JSON.stringify(msg)); verbose_log("namespace=" + ns); verbose_log("string=" + s); verbose_log("type=" + msg.datatype); verbose_log("value=" + msg.payload); verbose_log(nodeid.toString()); var opcuaVariant = opcuaBasics.build_new_variant(opcua, msg.datatype, msg.payload); if (node.session) { node.session.writeSingleNode(nodeid, opcuaVariant, function (err) { if (err) { set_node_status_to("error"); node.error(node.name + " Cannot write value (" + msg.payload + ") to msg.topic:" + msg.topic + " error:" + err); reset_opcua_client(connect_opcua_client); } else { set_node_status_to("active writing"); verbose_log("Value written!"); } }); } else { set_node_status_to("Session invalid"); node.error("Session is not active!") } } function subscribe_action_input(msg) { verbose_log("subscribing"); if (!subscription) { // first build and start subscription and subscribe on its started event by callback var timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit); subscription = make_subscription(subscribe_monitoredItem, msg, opcuaBasics.getSubscriptionParameters(timeMilliseconds)); } else { // otherwise check if its terminated start to renew the subscription if (subscription.subscriptionId != "terminated") { set_node_status_to("active subscribing"); subscribe_monitoredItem(subscription, msg); } else { subscription = null; monitoredItems.clear(); set_node_status_to("terminated"); reset_opcua_client(connect_opcua_client); } } } function subscribe_monitoredItem(subscription, msg) { verbose_log("Session subscriptionId: " + subscription.subscriptionId); var nodeStr=msg.topic; var dTypeIndex = nodeStr.indexOf(";datatype="); if (dTypeIndex>0) { nodeStr=nodeStr.substring(0,dTypeIndex); } console.log("nodeStr="+nodeStr); var monitoredItem = monitoredItems.get({"topicName": msg.topic}); if (!monitoredItem) { var interval = 100; if (typeof msg.payload === 'number') { interval = Number(msg.payload); } verbose_log(msg.topic + " samplingInterval " + interval); verbose_warn("Monitoring Event: " + msg.topic + ' by interval of ' + interval + " ms"); // Validate nodeId try { var nodeId = coerceNodeId(nodeStr); if (nodeId && nodeId.isEmpty()) { node.error(" Invalid empty node in getObject"); } //makeNodeId(nodeStr); // above is enough } catch(err) { node.error(err); return; } monitoredItem = subscription.monitor( { nodeId: nodeStr, attributeId: opcua.AttributeIds.Value }, { samplingInterval: interval, queueSize: 10, discardOldest: true }, 3, function (err) { if (err) { node.error("Check topic format for nodeId:"+msg.topic) node.error('subscription.monitorItem:' + err); // reset_opcua_client(connect_opcua_client); // not actually needed } else { monitoredItems.add({"topicName": nodeStr, mItem: monitoredItem}); } } ); monitoredItem.on("initialized", function () { verbose_log("initialized monitoredItem on " + nodeStr); }); monitoredItem.on("changed", function (dataValue) { set_node_status_to("active subscribed"); verbose_log(msg.topic + " value has changed to " + dataValue.value.value); if (dataValue.statusCode === opcua.StatusCodes.Good) { verbose_log("\tStatus-Code:" + (dataValue.statusCode.toString(16)).green.bold); } else { verbose_log("\tStatus-Code:" + dataValue.statusCode.toString(16)); } // Check if timestamps exists otherwise simulate them if (dataValue.serverTimestamp!=null) { msg.serverTimestamp = dataValue.serverTimestamp; msg.serverPicoseconds = dataValue.serverPicoseconds; } else { msg.serverTimestamp = new Date().getTime();; msg.serverPicoseconds = 0; } if (dataValue.sourceTimestamp!=null) { msg.sourceTimestamp = dataValue.sourceTimestamp; msg.sourcePicoseconds = dataValue.sourcePicoseconds; } else { msg.sourceTimestamp = new Date().getTime();; msg.sourcPicoseconds = 0; } msg.payload = dataValue.value.value; node.send(msg); }); monitoredItem.on("keepalive", function () { verbose_log("keepalive monitoredItem on " + nodeStr); }); monitoredItem.on("terminated", function () { verbose_log("terminated monitoredItem on " + nodeStr); if (monitoredItems.get({"topicName": nodeStr})) { monitoredItems.delete({"topicName": nodeStr}); } }); } return monitoredItem; } function browse_action_input(msg) { verbose_log("browsing"); var NodeCrawler = opcua.NodeCrawler; var crawler = new NodeCrawler(node.session); crawler.read(msg.topic, function (err, obj) { var newMessage = opcuaBasics.buildBrowseMessage(msg.topic); if (!err) { set_node_status_to("active browsing"); treeify.asLines(obj, true, true, function (line) { verbose_log(line); if (line.indexOf("browseName") > 0) { newMessage.browseName = line.substring(line.indexOf("browseName") + 12); } if (line.indexOf("nodeId") > 0) { newMessage.nodeId = line.substring(line.indexOf("nodeId") + 8); newMessage.nodeId = newMessage.nodeId.replace("&#x2F;", "\/"); } if (line.indexOf("nodeClass") > 0) { newMessage.nodeClassType = line.substring(line.indexOf("nodeClass") + 11); } if (line.indexOf("typeDefinition") > 0) { newMessage.typeDefinition = line.substring(line.indexOf("typeDefinition") + 16); newMessage.payload = Date.now(); node.send(newMessage); } set_node_status_to("browse done"); }); } else { node.error(err.message); set_node_status_to("error browsing"); reset_opcua_client(connect_opcua_client); } }); } function subscribe_monitoredEvent(subscription, msg) { verbose_log("Session subscriptionId: " + subscription.subscriptionId); var monitoredItem = monitoredItems.get({"topicName": msg.topic}); if (!monitoredItem) { var interval = 100; if (typeof msg.payload === 'number') { interval = Number(msg.payload); } verbose_log(msg.topic + " samplingInterval " + interval); verbose_warn("Monitoring Event: " + msg.topic + ' by interval of ' + interval + " ms"); // TODO read nodeId to validate it before subscription monitoredItem = subscription.monitor( { nodeId: msg.topic, // serverObjectId attributeId: AttributeIds.EventNotifier }, { samplingInterval: interval, queueSize: 100000, filter: msg.eventFilter, discardOldest: true }, 3, function (err) { if (err) { node.error('subscription.monitorEvent:' + err); reset_opcua_client(connect_opcua_client); } else { //valid=true; } } ); monitoredItems.add({"topicName": msg.topic, mItem: monitoredItem}); monitoredItem.on("initialized", function () { verbose_log("monitored Event initialized"); set_node_status_to("initialized"); }); monitoredItem.on("changed", function (eventFields) { dumpEvent(node, node.session, msg.eventFields, eventFields, function () { }); set_node_status_to("changed"); }); monitoredItem.on("error", function (err_message) { verbose_log("error monitored Event on " + msg.topic); if (monitoredItems.get({"topicName": msg.topic})) { monitoredItems.delete({"topicName": msg.topic}); } node.err("monitored Event ", msg.eventTypeId, " ERROR".red, err_message); set_node_status_to("error"); }); monitoredItem.on("keepalive", function () { verbose_log("keepalive monitored Event on " + msg.topic); }); monitoredItem.on("terminated", function () { verbose_log("terminated monitored Event on " + msg.topic); if (monitoredItems.get({"topicName": msg.topic})) { monitoredItems.delete({"topicName": msg.topic}); } }); } return monitoredItem; } function subscribe_events_input(msg) { verbose_log("subscribing events"); if (!subscription) { // first build and start subscription and subscribe on its started event by callback var timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit); subscription = make_subscription(subscribe_monitoredEvent, msg, opcuaBasics.getEventSubscribtionParameters(timeMilliseconds)); } else { // otherwise check if its terminated start to renew the subscription if (subscription.subscriptionId != "terminated") { set_node_status_to("active subscribing"); subscribe_monitoredEvent(subscription, msg); } else { subscription = null; monitoredItems.clear(); set_node_status_to("terminated"); reset_opcua_client(connect_opcua_client); } } } node.on("close", function () { if (subscription && subscription.isActive()) { subscription.terminate(); // subscription becomes null by its terminated event } if (node.session) { node.session.close(function (err) { verbose_log("Session closed"); set_node_status_to("session closed"); if (err) { node.error(node.name + " " + err); } node.session = null; close_opcua_client(set_node_status_to("closed")); }); } else { node.session = null; close_opcua_client(set_node_status_to("closed")); } }); node.on("error", function () { if (subscription && subscription.isActive()) { subscription.terminate(); // subscription becomes null by its terminated event } if (node.session) { node.session.close(function (err) { verbose_log("Session closed on error emit"); if (err) { node.error(node.name + " " + err); } set_node_status_to("session closed"); node.session = null; close_opcua_client(set_node_status_to("node error")); }); } else { node.session = null; close_opcua_client(set_node_status_to("node error")); } }); } RED.nodes.registerType("OpcUa-Client", OpcUaClientNode); };