UNPKG

node-red-contrib-opcua

Version:

A Node-RED node to communicate via OPC UA based on node-opcua library.

1,195 lines (1,105 loc) 115 kB
/** Copyright 2018 Valmet Automation Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. **/ require("node-opcua"); require("util"); module.exports = function (RED) { "use strict"; let chalk = require("chalk"); let opcua = require('node-opcua'); const { NodeCrawler } = require("node-opcua-client-crawler"); // Legacy support let opcuaBasics = require('./opcua-basics'); let crypto_utils = require("node-opcua-crypto"); // opcua.crypto_utils; let fileTransfer = require("node-opcua-file-transfer"); let async = require("async"); let fs = require("fs"); let os = require("os"); let cloneDeep = require('lodash.clonedeep'); let DataType = opcua.DataType; let AttributeIds = opcua.AttributeIds; let TimestampsToReturn = opcua.TimestampsToReturn; const { createClientCertificateManager } = require("./utils"); const { stringify } = require('flatted'); function OpcUaClientNode(n) { RED.nodes.createNode(this, n); this.name = n.name; this.action = n.action; let originalAction = n.action; this.time = n.time; this.timeUnit = n.timeUnit; this.deadbandtype = n.deadbandtype; this.deadbandvalue = n.deadbandvalue; this.certificate = n.certificate; // n == NONE, l == Local file, e == Endpoint, u == Upload this.localfile = n.localfile; // Local certificate file this.localkeyfile = n.localkeyfile; // Local private key file this.useTransport = n.useTransport; this.maxChunkCount = n.maxChunkCount; this.maxMessageSize = n.maxMessageSize; this.receiveBufferSize = n.receiveBufferSize; this.sendBufferSize = n.sendBufferSize; this.setstatusandtime = n.setstatusandtime; this.keepsessionalive = n.keepsessionalive; let node = this; let opcuaEndpoint = RED.nodes.getNode(n.endpoint); // Use as global for the node let userIdentity = { type: opcua.UserTokenType.Anonymous }; // Initialize with Anonymous let connectionOption = {}; let cmdQueue = []; // queue msgs which can currently not be handled because session is not established, yet and currentStatus is 'connecting' let currentStatus = ''; // the status value set set by node.status(). Didn't find a way to read it back. let multipleItems = []; // Store & read multiple nodeIds let writeMultipleItems = []; // Store & write multiple nodeIds & values connectionOption.securityPolicy = opcuaEndpoint.securityPolicy connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode] || opcua.MessageSecurityMode.None; let userCertificate = opcuaEndpoint.userCertificate; let userPrivatekey = opcuaEndpoint.userPrivatekey; connectionOption.clientCertificateManager = createClientCertificateManager(); if (node.certificate === "l" && node.localfile) { verbose_log("Using 'own' local certificate file " + node.localfile); // User must define absolute path let certfile = node.localfile; let keyfile = node.localkeyfile; connectionOption.certificateFile = certfile; connectionOption.privateKeyFile = keyfile; if (!fs.existsSync(certfile)) { node_error("Local certificate file not found: " + certfile) } if (!fs.existsSync(keyfile)) { node_error("Local private key file not found: " + keyfile) } } // Moved needed options to client create connectionOption.requestedSessionTimeout = opcuaBasics.calc_milliseconds_by_time_and_unit(300, "s"); // DO NOT USE must be NodeOPCUA-Client !! connectionOption.applicationName = node.name; // Application name connectionOption.clientName = node.name; // This is used for the session names connectionOption.endpointMustExist = false; connectionOption.defaultSecureTokenLifetime = 40000 * 5; // From the node UI, keep min values! // Needed or not? if (!node.maxChunkCount || parseInt(node.maxChunkCount) < 1) node.maxChunkCount = 1; if (!node.maxMessageSize || parseInt(node.maxMessageSize) < 8192) node.maxMessageSize = 8192; if (!node.receiveBufferSize || parseInt(node.receiveBufferSize) < 8 * 1024) node.receiveBufferSize = 8 * 1024; if (!node.sendBufferSize || parseInt(node.sendBufferSize) < 8 * 1024) node.sendBufferSize = 8 * 1024; let transportSettings = { maxChunkCount: parseInt(node.maxChunkCount), // Default 1 maxMessageSize: parseInt(node.maxMessageSize), // should be at least 8192 receiveBufferSize: parseInt(node.receiveBufferSize), // 8 * 1024, sendBufferSize: parseInt(node.sendBufferSize) // 8 * 1024 }; if (node.useTransport === true) { connectionOption["transportSettings"] = { ...transportSettings }; verbose_log(chalk.red("Using, transport settings: ") + chalk.cyan(JSON.stringify(connectionOption["transportSettings"]))); } connectionOption.connectionStrategy = { maxRetry: 10512000, // Limited to max 10 ~5min // 10512000, // 10 years should be enough. No infinite parameter for backoff. initialDelay: 5000, // 5s maxDelay: 30 * 1000 // 30s }; // Ensure Anonymous login if (connectionOption.securityMode === opcua.MessageSecurityMode.None || opcuaEndpoint.none === true) { userIdentity = { type: opcua.UserTokenType.Anonymous }; } if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) { userIdentity = { type: opcua.UserTokenType.Anonymous }; } if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) { node.error("Cannot use username & password & user certificate at the same time!"); } // Username & password with securityMode None is allowed if (opcuaEndpoint.login === true) { // } && connectionOption.securityMode != opcua.MessageSecurityMode.None) { userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint.credentials.user.toString(), password: opcuaEndpoint.credentials.password ? opcuaEndpoint.credentials.password.toString() : "" }; verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity))); } else if (opcuaEndpoint.usercert === true) { if (!fs.existsSync(userCertificate)) { node.error("User certificate file not found: " + userCertificate); } const certificateData = crypto_utils.readCertificate(userCertificate); if (!fs.existsSync(userPrivatekey)) { node.error("User private key file not found: " + userPrivatekey); } const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey); userIdentity = { certificateData, privateKey, type: opcua.UserTokenType.Certificate // User certificate }; } else { userIdentity = { type: opcua.UserTokenType.Anonymous }; } verbose_log(chalk.green("UserIdentity: ") + chalk.cyan(JSON.stringify(userIdentity))); let items = []; let subscription; // only one subscription needed to hold multiple monitored Items let monitoredItems = new Map(); function node_error(err) { node.error(chalk.red("Client node error on: " + node.name + " error: " + stringify(err))); } function verbose_warn(logMessage) { if (opcuaEndpoint.name && node.name) { console.warn(chalk.cyan(`${opcuaEndpoint?.name}`) + chalk.yellow(":") + chalk.cyan(node.name) ? chalk.cyan(node.name) + chalk.yellow(': ') + chalk.cyan(logMessage) : chalk.yellow('OpcUaClientNode: ') + chalk.cyan(logMessage)); } node.warn(logMessage); } function verbose_log(logMessage) { if (RED.settings.verbose) { node.debug(chalk.yellow(logMessage)); } } async function getBrowseName(_session, nodeId) { const dataValue = await _session.read({ attributeId: AttributeIds.BrowseName, nodeId }); if (dataValue.statusCode.isGood()) { const browseName = dataValue.value.value.name; return browseName; } else { return "???"; } } // Fields selected alarm fields // EventFields same order returned from server array of variants (filled or empty) async function __dumpEvent(node, session, fields, eventFields, _callback) { let msg = {}; msg.payload = {}; verbose_log(chalk.yellow("Event Fields: ") + chalk.cyan(JSON.stringify(eventFields))); set_node_status_to("active event"); for (let i = 0; i < eventFields.length; i++) { let variant = eventFields[i]; let fieldName = fields[i]; verbose_log(chalk.yellow("Event Field: ") + chalk.cyan(fieldName) + " " + chalk.cyan(stringify(variant))); // Check if variant is NodeId and then get qualified name (browseName) if (variant?.dataType && variant.dataType === DataType.NodeId) { fieldName = await getBrowseName(session, variant.value); } if (!variant || variant.dataType === DataType.Null || !variant.value) { verbose_log(chalk.red("No variant or variant dataType is Null or no variant value! Variant: ") + chalk.cyan(JSON.stringify(variant))); } else { if (fieldName === "EventId" && variant?.dataType) { msg.payload[fieldName] = "0x" + variant.value.toString("hex"); // As in UaExpert msg.payload["_" + fieldName] = variant; // Keep as ByteString } else { // Added handling for LocalizedText to use OS locale / node-red __language__ if (fieldName === "Message") { const locale = Intl.DateTimeFormat().resolvedOptions().locale; if (variant.value.length > 1) { let i = 0; while (i < variant.value.length) { let localText = variant.value[i]; // Change according locale if (localText.locale === locale) { variant.value = localText; break; } i++; } } } msg.payload[fieldName] = opcuaBasics.clone_object(variant.value); } // if available, needed for Acknowledge function in client if (fieldName === "ConditionId" && variant?.dataType) { msg.topic = variant.value.toString(); } } } // Set message topic if (eventFields.length === 0) { msg.topic = "No EventFields"; } // if available, needed for Acknowledge function in client else if (msg.payload.ConditionId) { msg.topic = msg.payload.ConditionId.toString(); } else if (msg.payload.EventId) { msg.topic = msg.payload.EventId.toString(); // Set then this can be used to Acknowledge event } else if (msg.payload.EventType) { msg.topic = msg.payload.EventType.toString(); } verbose_log(chalk.yellow("Event message topic: ") + chalk.cyan(msg.topic)); node.send([msg, null, null]); _callback(); } let 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 }); } // Listener functions that can be removed on reconnect const reestablish = function () { set_node_status2_to("connected", "re-established"); }; const backoff = function (attempt, delay) { let msg = {}; msg.error = {}; msg.error.message = "reconnect"; msg.error.source = this; // node.error("reconnect", msg); verbose_log(chalk.red("reconnect")) // + chalk.cyan(stringify(msg))); // msg is TOO big to show set_node_status2_to("reconnect", "attempt #" + attempt + " retry in " + delay / 1000.0 + " sec"); }; const reconnection = function () { set_node_status2_to("reconnect", "starting..."); }; function create_opcua_client(callback) { node.client = null; let options = { securityMode: connectionOption.securityMode, securityPolicy: connectionOption.securityPolicy, defaultSecureTokenLifetime: connectionOption.defaultSecureTokenLifetime, endpointMustExist: connectionOption.endpointMustExist, connectionStrategy: connectionOption.connectionStrategy, clientName: node.name, // Fix for #664 sessionName keepSessionAlive: node.keepsessionalive, requestedSessionTimeout: 60000 * 5, // 5min, default 1min automaticallyAcceptUnknownCertificate: true // transportSettings: transportSettings // Some }; try { // Use empty 0.0.0.0 address as "no client" initial value if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") == 0) { verbose_warn(`close opcua client ${node.client} userIdentity ${userIdentity.type}`); if (node.client) { close_opcua_client("connection error: no session", 0); } items = []; node.items = items; set_node_status_to("no client"); if (callback) { callback(); } return; } // Normal client if (!node.keepsessionalive) { node.keepsessionalive = false; } verbose_log(chalk.yellow("Keep session alive: ") + chalk.cyan(node.keepsessionalive)); if (node.useTransport === true) { options["transportSettings"] = JSON.parse(JSON.stringify(connectionOption.transportSettings)); verbose_log(chalk.red("NOTE: Using transport settings: " + chalk.cyan(JSON.stringify(options)))); } verbose_log(chalk.green("1) CREATE CLIENT: ") + chalk.cyan(JSON.stringify(options))); node.client = opcua.OPCUAClient.create(options); node.client.on("connection_reestablished", reestablish); node.client.on("backoff", backoff); node.client.on("start_reconnection", reconnection); } catch (err) { node_error("Cannot create client, check connection options: " + stringify(options)); // connectionOption set_node_status_to("Cannot create client"); } 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(message, error) { // verbose_warn(`closing opcua client ${opcua.client == null} userIdentity ${JSON.stringify(userIdentity)}`) if (node.client) { node.client.removeListener("connection_reestablished", reestablish); node.client.removeListener("backoff", backoff); node.client.removeListener("start_reconnection", reconnection); try { if (!node.client.isReconnecting) { node.client.disconnect(function () { node.client = null; verbose_log("Client disconnected!"); if (error === 0) { set_node_status_to("closed"); } else { set_node_error_status_to(message, error) node.error("Client disconnected & closed: " + message + " error: " + error.toString()); } }); } else { node.client = null; set_node_status_to("closed"); } } catch (err) { node_error("Error on disconnect: " + stringify(err)); } } } function set_node_status_to(statusValue) { verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue)); let statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; let endpoint = ""; if (opcuaEndpoint && opcuaEndpoint.endpoint) { endpoint = opcuaEndpoint.endpoint } node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status, endpoint: `${endpoint}` }); node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null]) } function set_node_status2_to(statusValue, message) { verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue)); let statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; let endpoint = ""; if (opcuaEndpoint?.endpoint) { endpoint = opcuaEndpoint.endpoint } node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status + " " + message, endpoint: `${endpoint}` }); node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null]); } function set_node_error_status_to(statusValue, error) { verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue)); let statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; let endpoint = ""; if (opcuaEndpoint?.endpoint) { endpoint = opcuaEndpoint.endpoint } if (!error) { error = ""; } node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status + " " + error, endpoint: `${endpoint}` }); node.send([null, { error: error, endpoint: `${endpoint}`, status: currentStatus }, null]); } async function connect_opcua_client() { if (opcuaEndpoint?.login === true) { verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity))); if (opcuaEndpoint.credentials && opcuaEndpoint['user'] && opcuaEndpoint['password']) { userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint?.credentials?.user?.toString(), password: opcuaEndpoint?.credentials?.password?.toString() }; } else if (opcuaEndpoint['user'] && opcuaEndpoint['password']) { userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint?.user?.toString(), password: opcuaEndpoint?.password?.toString() }; } else { node_error("Please enter user or password in credentials or same level as login") } } else if (opcuaEndpoint?.usercert === true) { if (!fs.existsSync(userCertificate)) { node.error("User certificate file not found: " + userCertificate); } const certificateData = crypto_utils.readCertificate(userCertificate); if (!fs.existsSync(userPrivatekey)) { node.error("User private key file not found: " + userPrivatekey); } const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey); userIdentity = { certificateData, privateKey, type: opcua.UserTokenType.Certificate // User certificate }; } else { verbose_warn(chalk.red("userIdentity is ANONYMOUS ")); userIdentity = { type: opcua.UserTokenType.Anonymous }; } // Refactored from old async Javascript to new Typescript with await let session; // STEP 1 // First connect to server´s endpoint if (opcuaEndpoint?.endpoint) { verbose_log(chalk.yellow("Connecting to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint)); } else { node_error("No client endpoint listed! Waiting..."); return; } if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") === 0) { set_node_status_to("no client") } else { set_node_status_to("connecting"); } if (!node.client) { verbose_log("No client to connect..."); return; } verbose_log(chalk.yellow("Exact endpointUrl: ") + chalk.cyan(opcuaEndpoint?.endpoint) + chalk.yellow(" hostname: ") + chalk.cyan(os.hostname())); try { await node.client.clientCertificateManager.initialize(); } catch (error1) { set_node_status_to("invalid certificate"); let msg = {}; msg.error = {}; msg.error.message = "Certificate error: " + error1.message; msg.error.source = this; node.error("Certificate error", msg); } node.debug(chalk.yellow("Trusted folder: ") + chalk.cyan(node.client?.clientCertificateManager?.trustedFolder)); node.debug(chalk.yellow("Rejected folder: ") + chalk.cyan(node.client?.clientCertificateManager?.rejectedFolder)); node.debug(chalk.yellow("Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.crlFolder)); node.debug(chalk.yellow("Issuers Cert folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCertFolder)); node.debug(chalk.yellow("Issuers Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCrlFolder)); try { verbose_log(chalk.green("2) Connecting using endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint) + chalk.green(" securityMode: ") + chalk.cyan(connectionOption.securityMode) + chalk.green(" securityPolicy: ") + chalk.cyan(connectionOption.securityPolicy)); await node.client.connect(opcuaEndpoint?.endpoint); } catch (err) { verbose_warn("Case A: Endpoint does not contain, 1==None 2==Sign 3==Sign&Encrypt, using securityMode: " + stringify(connectionOption.securityMode)); verbose_warn(" using securityPolicy: " + stringify(connectionOption.securityPolicy)); verbose_warn("Case B: UserName & password does not match to server (needed by Sign or SignAndEncrypt), check username: " + userIdentity.userName + " and password: " + userIdentity.password); verbose_warn("Case C: With Sign you cannot use SecurityPolicy None!!"); // verbose_error("Invalid endpoint parameters: ", err); node_error("Wrong endpoint parameters: " + JSON.stringify(opcuaEndpoint)); set_node_status_to("invalid endpoint"); let msg = {}; msg.error = {}; msg.error.message = "Invalid endpoint: " + err; msg.error.source = this; node.error("Invalid endpoint", msg); return; } verbose_log(chalk.green("Connected to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint)); // STEP 2 // This will succeed first time only if security policy and mode are None // Later user can use path and local file to access server certificate file if (!node.client) { node_error("Client not yet created & connected, cannot getEndpoints!"); return; } // dumpCertificates(node.client); // TODO Wrong folder or something to solve // STEP 3 // verbose_log("Create session..."); try { // verbose_warn(`Create session with userIdentity node.client ${node.client == null} userIdentity ${JSON.stringify(userIdentity)}`) verbose_log(chalk.green("3) Create session with userIdentity at: ") + chalk.cyan(JSON.stringify(userIdentity))); // {"clientName": "Node-red OPC UA Client node " + node.name}, // sessionName = "Node-red OPC UA Client node " + node.name; if (!node.client) { node_error("Client not yet created, cannot create session"); close_opcua_client("connection error: no client", 0); return; } session = await node.client.createSession(userIdentity); if (!session) { node_error("Create session failed!"); verbose_warn(`Create session failed!`) close_opcua_client("connection error: no session", 0); return; } node.session = session; set_node_status_to("session active"); for (let i in cmdQueue) { processInputMsg(cmdQueue[i]); } cmdQueue = []; } catch (err) { node_error(node.name + " OPC UA connection error: " + err.message); verbose_log(err); node.session = null; close_opcua_client("connection error", err); } } function make_subscription(callback, msg, parameters) { let newSubscription = null; if (!node.session) { verbose_log("Subscription without session"); return newSubscription; } if (!parameters) { verbose_log("Subscription without parameters"); return newSubscription; } verbose_log("Publishing interval " + stringify(parameters)); newSubscription = opcua.ClientSubscription.create(node.session, parameters); verbose_log("Subscription " + newSubscription.toString()); 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(); }); newSubscription.on("error", function (err) { verbose_log("Subscription error on ID: " + newSubscription.subscriptionId + ". " + err); set_node_error_status_to("subscription error") subscription = null; monitoredItems.clear(); }) return newSubscription; } if (!node.client) { create_opcua_client(connect_opcua_client); } function processInputMsg(msg) { if (msg.action === "reconnect") { cmdQueue = []; // msg.endpoint can be used to change endpoint msg.action = ""; reconnect(msg); return; } if (msg.action === "connect") { cmdQueue = []; // msg.endpoint can be used to change endpoint msg.action = ""; connect_action_input(msg); return; } if (msg.action && msg.action.length > 0) { verbose_log("Override node action by msg.action: " + msg.action); node.action = msg.action; } else { verbose_log(chalk.green("Using node action: ") + chalk.cyan(originalAction)); node.action = originalAction; // Use original action from the node } // With new node-red easier to set action into payload if (msg.payload?.action && msg.payload.action.length > 0) { verbose_log("Override node action by msg.payload.action:" + msg.payload.action); node.action = msg.payload.action; } if (!node.action) { verbose_warn("Can't work without action (read, write, browse ...)"); //node.send(msg); // do not send in case of error node.send([null, { error: "Can't work without action (read, write, browse ...)", endpoint: `${opcuaEndpoint.endpoint}`, status: currentStatus }, null]); return; } if (!node.client || !node.session) { verbose_log("Not connected, current status: " + currentStatus); // Added statuses when msg must be put to queue // Added statuses when msg must be put to queue const statuses = ['', 'create client', 'connecting', 'reconnect']; if (statuses.includes(currentStatus)) { cmdQueue.push(msg); } else { verbose_warn(`can't work without OPC UA client ${node.client} client ${node.session}`); reset_opcua_client(connect_opcua_client); } node.send([null, { error: "can't work without OPC UA client", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]); return; } if (!node.session.sessionId == "terminated") { verbose_warn("terminated OPC UA Session"); reset_opcua_client(connect_opcua_client); node.send([null, { error: "terminated OPC UA Session", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]); return; } if (msg.action && (msg.action === "connect" || msg.action === "disconnect")) { // OK msg.action = ""; } else if (!msg.topic) { verbose_warn("can't work without OPC UA NodeId - msg.topic empty"); node.send([null, { error: "can't work without OPC UA NodeId", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]); return; } verbose_log(chalk.yellow("Action on input: ") + chalk.cyan(node.action) + chalk.yellow(" Item from Topic: ") + chalk.cyan(msg.topic) + chalk.yellow(" session Id: ") + chalk.cyan(node.session.sessionId)); switch (node.action) { case "connect": connect_action_input(msg); break; case "disconnect": disconnect_action_input(msg); break; case "reconnect": reconnect(msg); break; case "register": register_action_input(msg); break; case "unregister": unregister_action_input(msg); break; case "read": read_action_input(msg); break; case "history": readhistory_action_input(msg); break; case "info": info_action_input(msg); break; case "build": build_extension_object_action_input(msg); break; case "write": write_action_input(msg); break; case "subscribe": subscribe_action_input(msg); break; case "monitor": monitor_action_input(msg); break; case "unsubscribe": unsubscribe_action_input(msg); break; case "deletesubscribtion": // miss-spelled, this allows old flows to work case "deletesubscription": delete_subscription_action_input(msg); break; case "browse": browse_action_input(msg); break; case "events": subscribe_events_input(msg); break; case "readmultiple": readmultiple_action_input(msg); break; case "writemultiple": writemultiple_action_input(msg) break; case "acknowledge": acknowledge_input(msg); break; case "readfile": read_file(msg); break; case "writefile": write_file(msg); break; case "method": method_action_input(msg); break; default: verbose_warn("Unknown action: " + node.action + " with msg " + stringify(msg)); break; } } node.on("input", processInputMsg); async function acknowledge_input(msg) { // msg.topic is nodeId of the alarm object like Prosys ns=6;s=MyLevel.Alarm // msg.conditionId is actual conditionObject that contains ns=6;s=MyLevel.Alarm/0:EventId current/latest eventId will be read // msg.comment will be used as comment in the acknowledge let eventId; if (msg.conditionId) { const dataValue = await node.session.read({ nodeId: msg.conditionId, attributeId: AttributeIds.Value }); eventId = dataValue.value.value; verbose_log(chalk.yellow("Acknowledge (alarm object == topic): ") + chalk.cyan(msg.topic) + chalk.yellow(" conditionObject (nodeId of eventId): ") + chalk.cyan(msg.conditionId) + chalk.yellow(" value of eventId: 0x") + chalk.cyan(eventId.toString("hex")) + chalk.yellow(" comment: ") + chalk.cyan(msg.comment)); } // If actual eventId provided use it if (msg.eventId) { eventId = msg.eventId; } if (eventId) { try { const ackedState = await node.session.read({ nodeId: msg.topic + "/0:AckedState/0:Id", attributeId: AttributeIds.Value }); node.debug(chalk.yellow("EVENT ACKED STATE: ") + chalk.cyan(ackedState)); if (ackedState && ackedState.statusCode === opcua.StatusCodes.Good && ackedState.value.value === true) { node.status({ fill: "yellow", shape: "dot", text: "Event: " + msg.topic + " already acknowledged" }); } else { const status = await node.session.acknowledgeCondition(msg.topic, eventId, msg.comment); if (status !== opcua.StatusCodes.Good) { node_error(node.name + "Error at acknowledge, status: " + status.toString()); set_node_error_status_to("error", status.toString()); } else { node.status({ fill: "green", shape: "dot", text: "Event: " + msg.topic + " acknowledged" }); } } } catch (err) { node_error(node.name + "Error at acknowledge: " + msg.topic + " eventId: " + eventId + " error: " + err); set_node_error_status_to("error", err); } } else { node_error(node.name + " error at acknowledge, no eventId, possible wrong msg.conditionId " + msg.conditionId); } } async function read_file(msg) { verbose_log("Read file, nodeId: " + msg.topic.toString()); let file_node = opcua.coerceNodeId(msg.topic); if (node.session) { try { const clientFile = new fileTransfer.ClientFile(node.session, file_node); fileTransfer.ClientFile.useGlobalMethod = true; // Given that the file is opened in ReadMode Only await clientFile.open(fileTransfer.OpenFileMode.Read); // Read file size try { const size = await clientFile.size(); // This should read size from the file itself let buf = await clientFile.read(size); // node-opcua-file-transfer takes care of the whole file reading from v2.94.0 await clientFile.close(); msg.payload = buf; // Debug purpose, show content verbose_log("File content: " + buf.toString()); } catch (err) { msg.payload = ""; node_error(node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err); set_node_error_status_to("error", "Cannot read file!"); node.send([null, { error: node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]); } node.send([msg, null]); } catch (err) { node_error(node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err); set_node_error_status_to("error", err.toString()); node.send([null, { error: node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]); } } else { verbose_warn("No open session to read file!"); } } async function write_file(msg) { verbose_log("Write file, nodeId: " + msg.topic.toString()); let file_node = opcua.coerceNodeId(msg.topic); if (node.session) { try { let buf; if (msg.payload && msg.payload.length > 0) { buf = msg.payload; } if (msg.fileName) { verbose_log("Uploading file: " + msg.fileName); buf = fs.readFileSync(msg.fileName); } const clientFile = new fileTransfer.ClientFile(node.session, file_node); clientFile.useGlobalMethod = true; // Given that the file is opened in WriteMode await clientFile.open(fileTransfer.OpenFileMode.Write); verbose_log("Local file content: " + buf.toString()); verbose_log("Writing file to server..."); await clientFile.write(buf); await clientFile.close(); verbose_log("Write done!"); } catch (err) { node.error(chalk.red("Cannot write file, error: " + err.message)); } } else { verbose_warn("No open session to write file!"); } } async function method_action_input(msg) { verbose_log("Calling method: " + JSON.stringify(msg)); if (node.session) { try { let status = await callMethod(msg); if (status === opcua.StatusCodes.Good) { node.status({ fill: "green", shape: "dot", text: "Method executed" }); } else { node.error("Failed, method result: ", status.description); } } catch (err) { node.error(chalk.red("Cannot call method, error: " + err.message)); } } else { verbose_warn("No open session to call method!"); } } async function callMethod(msg) { if (msg.methodId && msg.inputArguments) { verbose_log("Calling method: " + JSON.stringify(msg.methodId)); verbose_log("InputArguments: " + JSON.stringify(msg.inputArguments)); if (msg.outputArguments) { verbose_log("OutputArguments: " + JSON.stringify(msg.outputArguments)); } let args=[]; let i = 0; let tmp; set_node_status_to("building method arguments"); try { while (i < msg.inputArguments.length) { tmp = msg.inputArguments[i]; if (tmp.dataType === "NodeId") { tmp.value = opcua.coerceNodeId(tmp.value); } if (tmp.dataType === "ExtensionObject") { let extensionobject = null; // tmp = {dataType: "ExtensionObject", typeid: tmp.typeid, value: tmp.value}; if (tmp.typeid) { extensionobject = await node.session.constructExtensionObject(opcua.coerceNodeId(tmp.typeid), tmp.value); // TODO make while loop to enable await tmp.value = extensionobject; } // verbose_log("ExtensionObject: " + extensionobject); // Object.assign(extensionobject, tmp.value); /* verbose_log(chalk.green("ExtensionObject value: ") + JSON.stringify(tmp.value)); tmp.value = new opcua.Variant({ dataType: opcua.DataType.ExtensionObject, value: tmp.value // JSON.stringify(tmp.value) // extensionobject }); */ verbose_log(chalk.green("ExtensionObject: ") + JSON.stringify(tmp)); } else { if (tmp.valueRank && tmp.valueRank >= 1) { tmp = {dataType: tmp.dataType, valueRank: tmp.valueRank, arrayDimensions: 1, value: tmp.value}; } else { tmp = {dataType: tmp.dataType, value: tmp.value}; } verbose_log("Basic type: " + JSON.stringify(tmp)); } args.push(tmp); i++; } } catch (err) { console.log(chalk.red("Error: "), err); let msg = {}; msg.error = {}; msg.error.message = "Invalid NodeId: " + err; msg.error.source = this; node.error("Invalid argument: ", tmp); return opcua.StatusCodes.BadNodeIdUnknown; } // verbose_log("Updated InputArguments: " + stringify(msg.inputArguments)); verbose_log("Updated InputArguments: " + JSON.stringify(args)); let callMethodRequest; let diagInfo; try { callMethodRequest = new opcua.CallMethodRequest({ objectId: opcua.coerceNodeId(msg.objectId), methodId: opcua.coerceNodeId(msg.methodId), inputArgumentDiagnosticInfos: diagInfo, inputArguments: args, // inputArguments: msg.inputArguments, outputArguments: msg.outputArguments }); } catch (err) { set_node_status_to("call method error"); node.error("Build method request failed, error: " + err.message); } verbose_log("Call request: " + callMethodRequest.toString()); verbose_log("Calling: " + callMethodRequest); try { set_node_status_to("call method"); const result = await node.session.call(callMethodRequest); if (diagInfo) { verbose_log("Diagn. info: " + stringify(diagInfo)); } if (msg.outputArguments) { verbose_log("Output args: " + stringify(msg.outputArguments)); } verbose_log("Results: " + stringify(result)); msg.result = result; if (result && result.statusCode === opcua.StatusCodes.Good) { let i = 0; msg.output = result.outputArguments; // Original outputArguments msg.payload = []; // Store values back to array if (result.outputArguments.length == 1) { verbose_log("Value: " + result.outputArguments[i].value); msg.payload = result.outputArguments[0].value; // Return only if one output argument } else { while (result.outputArguments.length > i) { verbose_log("Value[" + i + "]:" + result.outputArguments[i].toString()); msg.payload.push(result.outputArguments[i].value); // Just copy result value to payload[] array, actual value needed mostly i++; } } } else { set_node_status_to("execute method error"); node.error("Execute method result, error:" + result.statusCode.description); node.send([null, { error: "Execute method result, error:" + result.statusCode.description, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]); return result.statusCode; } node.send([msg, null]); return opcua.StatusCodes.Good; } catch (err) { set_node_status_to("execute method error"); node.error("Method execution error: " + err.message); node.send([null, { error: "Method execution error: " + err.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]); return opcua.StatusCodes.BadMethodInvalid; } } } async function connect_action_input(msg) { console.log("#1 ACTION Connect!"); verbose_log("Connecting..."); if (msg && msg.OpcUaEndpoint) { // Remove listeners if existing if (node.client) { verbose_log("Cleanup old listener events... before connecting to new client"); verbose_log("All event names:" + node.client.eventNames()); verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished")); node.client.removeListener("connection_reestablished", reestablish); verbose_log("Backoff event count:" + node.client.listenerCount("backoff")); node.client.removeListener("backoff", backoff); verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection")); node.client.removeListener("start_reconnection", reconnection); } opcuaEndpoint = {}; // Clear opcuaEndpoint = msg.OpcUaEndpoint; // Check all parameters! connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint?.securityPolicy]; // || opcua.SecurityPolicy.None; connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint?.securityMode]; // || opcua.MessageSecurityMode.None; verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode); if (opcuaEndpoint.login === true) { userIdentity = { userName: opcuaEndpoint?.user, password: opcuaEndpoint?.password, type: opcua.UserTokenType.UserName }; verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity)); } verbose_log("Using new endpoint:" + stringify(opcuaEndpoint)); } else { verbose_log("Using endpoint:" + stringify(opcuaEndpoint)); } console.log("#2 Create client"); if (!node.client) { create_opcua_client(connect_opcua_client); } } function disconnect_action_input(msg) { verbose_log("Closing session..."); if (node.session) { node.session.close(function (err) { if (err) { node_error("Session close error: " + err); } else { verbose_log("Session closed!"); } }); } else { verbose_warn("No session to close!"); } opcuaEndpoint = {}; // Clear opcuaEndpoint = msg.OpcUaEndpoint; // Now reconnect and use msg parameters subscription = null; monitoredItems.clear(); verbose_log("Disconnecting..."); if (node.client) { node.client.removeListener("connection_reestablished", reestablish); verbose_log("Backoff event count:" + node.client.listenerCount("backoff")); node.client.removeListener("backoff", backoff); verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection")); node.client.removeListener("start_reconnection", reconnection); node.client.disconnect(function () { verbose_log("Client disconnected!"); set_node_status_to("disconnected"); }); node.client = null; } } async function register_action_input(msg) { verbose_log("register nodes : " + msg.payload.toString()); // First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple // msg.topic not used, but cannot be empty // msg.payload == array of nodeIds to register if (msg.payload.length > 0) { const registeredNodes = await node.session.registerNodes(msg.payload); verbose_log("RegisteredNodes: " + registeredNodes.toString()); } else { verbose_warn("No items to register in the payload! Check node:" + node.name); } } async function unregister_action_input(msg) { verbose_log("unregister nodes : " + msg.payload.toString()); // First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple // msg.topic not used, but cannot be empty // msg.payload == array of nodeIds to register if (msg.payload.length > 0) { const unregisteredNodes = await node.session.registerNodes(msg.payload); verbose_log("UnregisteredNodes: " + unregisteredNodes.toString()); } else { verbose_warn("No items to unregister in the payload! Check node:" + node.name); } } async function read_action_input(msg) { verbose_log("reading"); let item = ""; let range = null; if (msg.topic) { let 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(stringify(msg)); } } let br = ""; // browsePath enhancement, item starts with br=/Objects/3:Simulation/3:Counter // console.log("ITEM: " + item + " topic: " + msg.topic); if (item.length > 0 && item.indexOf("br=") === 0) { verbose_log("Finding nodeId by browsePath: " + item.substring(3)); br = item.substring(3); }