UNPKG

@frangoteam/fuxa

Version:

Web-based Process Visualization (SCADA/HMI/Dashboard) software

816 lines (769 loc) 31.7 kB
/** * OPC UA Client Driver */ var opcua; const async = require('async'); const utils = require('../../utils'); const deviceUtils = require('../device-utils'); function OpcUAclient(_data, _logger, _events, _runtime) { var runtime = _runtime; var data = _data; // Current Device data { id, name, tags, enabled, ... } var logger = _logger; // Logger var working = false; // Working flag to manage overloading polling and connection var connected = false; // Connected flag var monitored = false; // Monitored flag var lastStatus = ''; // Last Device status var events = _events; // Events to commit change to runtime var the_session; var the_subscription = null; var options = { connectionStrategy: { maxRetry: 1 }, keepSessionAlive: true, endpointMustExist: false }; // Connections options var client = opcua.OPCUAClient.create(options); const attributeKeys = Object.keys(opcua.AttributeIds).filter((x) => x === 'DataType' || x === 'AccessLevel' || x === 'UserAccessLevel');//x !== "INVALID" && x[0].match(/[a-zA-Z]/)); var varsValue = {}; // Signals to send to frontend { id, type, value } var getProperty = null; // Function to ask property (security) var lastTimestampValue; // Last Timestamp of asked values var tagsIdMap = {}; // Map of tag id with opc nodeId /** * Connect the client to OPC UA server * Open Session, Create Subscription, Emit connection status, Clear the memory Tags value */ this.connect = function () { var self = this; return new Promise(function (resolve, reject) { if (!_checkWorking(true)) { reject(); } else { var property = null; async.series([ // step 1 check property function (callback) { if (getProperty) { getProperty({query: 'security', name: data.id}).then(result => { if (result && result.value && result.value !== 'null') { // property security mode property = JSON.parse(result.value); var opts = { // applicationName: 'Myclient', endpointMustExist: false, keepSessionAlive: true, connectionStrategy: { maxRetry: 1 } }; if (property.mode) { if (property.mode.securityMode) { opts['securityMode'] = property.mode.securityMode; } if (property.mode.securityPolicy) { opts['securityPolicy'] = property.mode.securityPolicy; } } client = opcua.OPCUAClient.create(opts); } callback(); }).catch(function (err) { callback(err); }); } else { callback(); } }, // step 2 connect function (callback) { const endpoint = data.property.address; client.connect(endpoint, function (err) { if (err) { _clearVarsValue(); logger.error(`'${data.name}' connect failure! ${err}`); } else { logger.info(`'${data.name}' connection`, true); } callback(err); }); client.on("connection_lost", () => { logger.error(`'${data.name}' connection lost!`); self.disconnect().then(function () { }); }); client.on("backoff", (retry, delay) => { logger.error(`'${data.name}' retry to connect! ${retry}`); }); }, // step 3 create session function (callback) { const userIdentityInfo = { }; if (property && property.uid && property.pwd) { userIdentityInfo['userName'] = property.uid; userIdentityInfo['password'] = property.pwd; } client.createSession(userIdentityInfo, function (err, session) { if (err) { _clearVarsValue(); logger.error(`'${data.name}' createSession failure! ${err}`); } else { the_session = session; the_session.on('session_closed', () => { logger.warn(`'${data.name}' Warning => Session closed`); }); the_session.on('keepalive', () => { logger.info(`'${data.name}' session keepalive`, true); }); the_session.on('keepalive_failure', () => { logger.error(`'${data.name}' session keepalive failure!`); self.disconnect().then(function () { }); }); the_session.on("terminated", function() { console.log("terminated"); }); _createSubscription(); } callback(err); }); }], function (err) { if (err) { logger.error(`'${data.name}' try to connect error! ${err}`); _emitStatus('connect-error'); _clearVarsValue(); connected = false; reject(); client.disconnect(function () { }); } else { logger.info(`'${data.name}' connected!`, true); _emitStatus('connect-ok'); connected = true; resolve(); } _checkWorking(false); }); } }); } /** * Disconnect the OPC UA server * Emit connection status, Clear the memory Tags value */ this.disconnect = function () { return new Promise(function (resolve, reject) { _disconnect(function (err) { if (err) { logger.error(`'${data.name}' disconnect failure! ${err}`); } connected = false; monitored = false; _checkWorking(false); _emitStatus('connect-off'); _clearVarsValue(); resolve(true); }); }); } /** * Browse Server Nodes, read the childres of gived node * The browser callback have a children nodes count limit and have to use _browseNext */ this.browse = function (node) { let nodeId = (node) ? node.id : opcua.resolveNodeId('RootFolder'); return new Promise(function (resolve, reject) { // "RootFolder" if (the_session) { the_session.browse(nodeId, function (err, browseResult) { if (!err) { let opcNodes = []; browseResult.references.forEach(function (reference) { let node = new OpcNode(reference.browseName.toString()); if (reference.displayName) { node.name = reference.displayName.text; } node.id = reference.nodeId; node.class = reference.nodeClass; opcNodes.push(node); }); if (browseResult.continuationPoint) { var nextresult = _browseNext(browseResult.continuationPoint).then(nodes => { for (let i = 0; i < nodes.length; i++) { opcNodes.push(nodes[i]); } resolve(opcNodes); }); } else { resolve(opcNodes); } } else { reject(err); } }); } else { reject('Session Error'); } }); } /** * Browser the next children nodes after contipoint position * @param {*} contipoint */ var _browseNext = function (contipoint) { var opcNodes = []; var browseNextRequest = new opcua.BrowseNextRequest({ continuationPoints: [contipoint] }); return new Promise(function (resolve, reject) { the_session.performMessageTransaction(browseNextRequest, function (err, response) { if (err) { reject(err); } else { if (response.results && response.results[0]) { let browseResult = response.results[0]; browseResult.references.forEach(function (reference) { let node = new OpcNode(reference.browseName.toString()); if (reference.displayName) { node.name = reference.displayName.text; } node.id = reference.nodeId; node.class = reference.nodeClass; opcNodes.push(node); }); if (browseResult.continuationPoint) { _browseNext(browseResult.continuationPoint).then(nodes => { for (let i = 0; i < nodes.length; i++) { opcNodes.push(nodes[i]); } return resolve(opcNodes); }); } else { return resolve(opcNodes); } } else { return resolve(opcNodes); } } }); }); } /** * Read node attribute */ this.readAttribute = function (node) { const attr = []; const nodesToRead = attributeKeys.map((attr) => ({ nodeId: node.id, attributeId: opcua.AttributeIds[attr] })); return new Promise(function (resolve, reject) { the_session.read(nodesToRead, function (err, dataValues) { if (err) { reject('#readAllAttributes returned ' + err.message); } else { node.attribute = {}; for (let i = 0; i < nodesToRead.length; i++) { const obj = _attrToObject(nodesToRead[i].attributeId, dataValues[i]); if (obj) { node.attribute[nodesToRead[i].attributeId] = obj.attribute; } // if (dataValue.statusCode !== opcua.StatusCodes.Good) { // continue; // } // const s = toString1(nodeToRead.attributeId, dataValue); // append_text(attributeIdtoString[nodeToRead.attributeId], s, attr); } resolve(node); } }); }); } /** * Take the current Tags value (only changed), Reset the change flag, Emit Tags value * Save DAQ value */ this.polling = async function () { if (_checkWorking(true)) { if (!monitored) { _startMonitor().then(ok => { if (ok && connected) { monitored = true; } _checkWorking(false); }).catch(function (err) { logger.error(`'${data.name}' polling error (_startMonitor): ${err}`); _checkWorking(false); }); } else if (the_session && client) { try { var varsValueChanged = await _checkVarsChanged(); lastTimestampValue = new Date().getTime(); _emitValues(varsValue); if (this.addDaq && !utils.isEmptyObject(varsValueChanged)) { this.addDaq(varsValueChanged, data.name, data.id); } } catch (err) { logger.error(`'${data.name}' polling error: ${err}`); } _checkWorking(false); } else { _checkWorking(false); } } } /** * Load Tags to read by polling */ this.load = function (_data) { data = JSON.parse(JSON.stringify(_data)); try { var count = Object.keys(data.tags).length; logger.info(`'${data.name}' data loaded (${count})`, true); } catch (err) { logger.error(`'${data.name}' load error! ${err}`); } } /** * Return Tags values array { id: <name>, value: <value>, type: <type> } */ this.getValues = function () { return data.tags; } /** * Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> } */ this.getValue = function (id) { if (varsValue[id]) { return {id: id, value: varsValue[id].value, ts: lastTimestampValue }; } return null; } /** * Return Device status Connected/Disconnected 'connect-off', 'connect-ok', 'connect-error' */ this.getStatus = function () { return lastStatus; } /** * Return Tag property */ this.getTagProperty = function (id) { if (data.tags[id]) { return { id: id, name: data.tags[id].name, type: data.tags[id].type, format: data.tags[id].format }; } else { return null; } } /** * Set Tag value, used to set value from frontend */ this.setValue = async function (tagId, value) { if (the_session && data.tags[tagId]) { let opcType = _toDataType(data.tags[tagId].type); if (data.tags[tagId].dataType) { opcType = data.tags[tagId].dataType; // use the actual dataType from read } let valueToSend = _toValue(opcType, value); valueToSend = await deviceUtils.tagRawCalculator(valueToSend, data.tags[tagId], runtime); if (opcType === opcua.DataType.String || opcType === opcua.DataType.ByteString) { valueToSend = valueToSend?.toString(); } var nodesToWrite = [ { nodeId: data.tags[tagId].address, attributeId: opcua.AttributeIds.Value, value: /*new DataValue(*/{ value: {/* Variant */ dataType: opcType, value: valueToSend } } } ]; the_session.write(nodesToWrite, function (err, statusCodes) { if (err) { logger.error(`'${data.name}' setValue error! ${err}`); } else { logger.info(`'${data.name}' setValue(${tagId}, ${value})`, true, true); } }); return true; } return false; } /** * Is Connected true/false */ this.isConnected = function () { return connected; } /** * Set the callback to set value to DAQ */ this.bindAddDaq = function (fnc) { this.addDaq = fnc; // Add the DAQ value to db history } this.addDaq = null; // Callback to add the DAQ value to db history /** * Return the timestamp of last read tag operation on polling * @returns */ this.lastReadTimestamp = () => { return lastTimestampValue; } /** * Set function to ask property (security) */ this.bindGetProperty = function (fnc) { getProperty = fnc; } /** * Return the Daq settings of Tag * @returns */ this.getTagDaqSettings = (tagId) => { return data.tags[tagId] ? data.tags[tagId].daq : null; } /** * Set Daq settings of Tag * @returns */ this.setTagDaqSettings = (tagId, settings) => { if (data.tags[tagId]) { utils.mergeObjectsValues(data.tags[tagId].daq, settings); } } /** * Disconnect the OPC UA client and close session if used * @param {*} callback */ var _disconnect = function (callback) { if (!the_session) { client.disconnect(function (err) { callback(err); }); } else { the_session.close(function () { client.disconnect(function (err) { callback(err); }); }); } } /** * Create a session subscription to refresh Tags value */ var _createSubscription = function () { if (the_session) { const parameters = { requestedPublishingInterval: 500, requestedLifetimeCount: 600, requestedMaxKeepAliveCount: 10, maxNotificationsPerPublish: 0, publishingEnabled: true, priority: 0 }; the_session.createSubscription2( parameters, (err, subscription) => { if (err) { logger.error(`'${data.name}' can't create subscription! ${err.message}`); return; } the_subscription = subscription; logger.info(`'${data.name}' subscription created!`, true); }); } } /** * Start the monitor by subsribe the Tags to check if value change * samplingInterval = 1000 msec. * @param {*} callback */ var _startMonitor = function () { return new Promise(async function (resolve, reject) { if (the_session && the_subscription) { tagsIdMap = {}; var count = 0; for (var id in data.tags) { count++; try { var nodeId = data.tags[id].address; tagsIdMap[nodeId] = id; var monitoredItem = await the_subscription.monitor( { nodeId: nodeId, attributeId: opcua.AttributeIds.Value }, { samplingInterval: data.polling || 1000, discardOldest: true, queueSize: 1 }, opcua.TimestampsToReturn.Both ); monitoredItem.on('changed', _monitorcallback(nodeId)); } catch (err) { logger.error(`'${nodeId}' _startMonitor ${err}`); } } resolve(true); } else { reject(); } }); } /** * Callback from monitor of changed Tag value * And set the changed value to local Tags * @param {*} _nodeId */ var _monitorcallback = function (_nodeId) { var nodeId = _nodeId; return function (dataValue) { if (dataValue && dataValue.value) { // console.log(nodeId.toString(), '\t value : ', dataValue.value.value.toString()); let id = tagsIdMap[nodeId]; if (data.tags[id]) { let rawVal = dataValue.value.value; let parsed = false; if (Array.isArray(rawVal) && rawVal.length > 0) { rawVal = rawVal[rawVal.length - 1]; parsed = true; } data.tags[id].rawValue = rawVal; data.tags[id].dataType = dataValue.value.dataType; data.tags[id].serverTimestamp = dataValue.serverTimestamp.toString(); data.tags[id].timestamp = new Date().getTime(); data.tags[id].changed = true; } } }; } /** * Clear local Tags value by set all to null */ var _clearVarsValue = function () { for (let id in varsValue) { varsValue[id].value = null; } _emitValues(varsValue); } /** * Return the Tags that have value changed and clear value changed flag of all Tags */ var _checkVarsChanged = async () => { const timestamp = new Date().getTime(); var result = {}; for (var id in data.tags) { data.tags[id].value = await deviceUtils.tagValueCompose(data.tags[id].rawValue, varsValue[id] ? varsValue[id].value : null, data.tags[id], runtime); if (this.addDaq && !utils.isNullOrUndefined(data.tags[id].value) && deviceUtils.tagDaqToSave(data.tags[id], timestamp)) { result[id] = data.tags[id]; } data.tags[id].changed = false; varsValue[id] = data.tags[id]; } return result; } /** * To manage a overloading connection * @param {*} check */ var _checkWorking = function (check) { if (check && working) { if (monitored) { logger.warn(`'${data.name}' working (connection || polling) overload!`); } return false; } working = check; return true; } /** * Emit Tags in application * @param {*} values */ var _emitValues = function (values) { events.emit('device-value:changed', { id: data.id, values: values }); } /** * Emit status in application * @param {*} status */ var _emitStatus = function (status) { lastStatus = status; events.emit('device-status:changed', { id: data.id, status: status }); } /** * Return formatted Tag attribute * @param {*} attribute * @param {*} dataValue */ var _attrToObject = function (attribute, dataValue) { if (!dataValue || !dataValue.value || !dataValue.value.hasOwnProperty('value')) { return null; } switch (attribute) { case opcua.AttributeIds.DataType: let dtype = opcua.DataType[dataValue.value.value.value]; return { attribute: dtype }; // case opcua.AttributeIds.NodeClass: // return NodeClass[dataValue.value.value] + " (" + dataValue.value.value + ")"; // case opcua.AttributeIds.IsAbstract: // case opcua.AttributeIds.Historizing: // case opcua.AttributeIds.EventNotifier: // return dataValue.value.value ? "true" : "false" // case opcua.AttributeIds.WriteMask: // case opcua.AttributeIds.UserWriteMask: // return " (" + dataValue.value.value + ")"; // case opcua.AttributeIds.NodeId: // case opcua.AttributeIds.BrowseName: // case opcua.AttributeIds.DisplayName: // case opcua.AttributeIds.Description: // case opcua.AttributeIds.ValueRank: // case opcua.AttributeIds.ArrayDimensions: // case opcua.AttributeIds.Executable: // case opcua.AttributeIds.UserExecutable: // case opcua.AttributeIds.MinimumSamplingInterval: // if (!dataValue.value.value) { // return "null"; // } // return dataValue.value.value.toString(); case opcua.AttributeIds.UserAccessLevel: case opcua.AttributeIds.AccessLevel: if (!dataValue.value.value) { return null; } let rlev = opcua.AccessLevelFlag[dataValue.value.value & 1]; let wlev = opcua.AccessLevelFlag[dataValue.value.value & 2]; let lev = ''; if (rlev) { lev += 'R'; } if (wlev) { if (rlev) { lev += '/'; } lev += 'W'; } return { attribute: lev }; default: return null; } } /** * Convert OPCUA data type from string * @param {*} type */ var _toDataType = function (type) { if (type === 'Boolean') { return opcua.DataType.Boolean; } else if (type === 'SByte') { return opcua.DataType.SByte; } else if (type === 'Byte') { return opcua.DataType.Byte; } else if (type === 'Int16') { return opcua.DataType.Int16; } else if (type === 'UInt16') { return opcua.DataType.UInt16; } else if (type === 'Int32') { return opcua.DataType.Int32; } else if (type === 'UInt32') { return opcua.DataType.UInt32; } else if (type === 'Int64') { return opcua.DataType.Int64; } else if (type === 'UInt64') { return opcua.DataType.UInt64; } else if (type === 'Float') { return opcua.DataType.Float; } else if (type === 'Double') { return opcua.DataType.Double; } else if (type === 'String') { return opcua.DataType.String; } else if (type === 'DateTime') { return opcua.DataType.DateTime; } else if (type === 'Guid') { return opcua.DataType.Guid; } else if (type === 'ByteString') { return opcua.DataType.ByteString; } } /** * Convert value from string depending of type * @param {*} type * @param {*} value */ var _toValue = function (type, value) { switch (type) { case opcua.DataType.Boolean: if (typeof value === 'boolean') { return value; } if (typeof value === 'string') { const val = value.toLowerCase(); return val === 'true' || val === '1'; } if (typeof value === 'number') { return value === 1; } return false; case opcua.DataType.SByte: case opcua.DataType.Byte: case opcua.DataType.Int16: case opcua.DataType.UInt16: case opcua.DataType.Int32: case opcua.DataType.UInt32: case opcua.DataType.Int64: case opcua.DataType.UInt64: return parseInt(value); case opcua.DataType.Float: case opcua.DataType.Double: return parseFloat(value); default: return value; } } } /** * Return security and encryption mode supported from server endpoint */ function getEndPoints(endpointUrl) { return new Promise(function (resolve, reject) { if (loadOpcUALib()) { let opts = { connectionStrategy: { maxRetry: 1 } }; let client = opcua.OPCUAClient.create(opts); try { client.connect(endpointUrl, function (err) { if (err) { reject('getendpoints-connect-error: ' + err.message); } else { const endpoints = client.getEndpoints().then(endpoints => { const reducedEndpoints = endpoints.map(endpoint => ({ endpointUrl: endpoint.endpointUrl, securityMode: endpoint.securityMode.toString(), securityPolicy: endpoint.securityPolicyUri.toString(), })); resolve( reducedEndpoints); client.disconnect(); }, reason => { reject('getendpoints-error: ' + reason); client.disconnect(); }); } }); } catch (err) { reject('getendpoints-error: ' + err); } } else { reject('getendpoints-error: node-opcua not found!'); } }); } function loadOpcUALib() { if (!opcua) { try { opcua = require('node-opcua'); } catch { } if (!opcua && manager) { try { opcua = manager.require('node-opcua'); } catch { } } } return (opcua) ? true : false; } module.exports = { init: function (settings) { // deviceCloseTimeout = settings.deviceCloseTimeout || 15000; }, create: function (data, logger, events, manager, runtime) { if (!loadOpcUALib()) return null; return new OpcUAclient(data, logger, events, runtime); }, getEndPoints: getEndPoints } function OpcNode(name) { this.name = name; this.id = ''; this.class = ''; this.type = ''; this.value = ''; this.timestamp = ''; this.attribute; }