@t0a5tbr0t/node-red-contrib-opcua 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/LICENSE +20 -0
- package/Readme.md +239 -0
- package/images/PROSYS-OPC-UA-EXAMPLE2.png +0 -0
- package/images/nodeopcua64.png +0 -0
- package/images/opcuanode.png +0 -0
- package/images/opcuanodeLogo64.png +0 -0
- package/opcua/101-opcuaitem.html +106 -0
- package/opcua/101-opcuaitem.js +82 -0
- package/opcua/102-opcuaclient.html +274 -0
- package/opcua/102-opcuaclient.js +2741 -0
- package/opcua/103-opcuabrowser.html +71 -0
- package/opcua/103-opcuabrowser.js +230 -0
- package/opcua/104-opcuaserver.html +261 -0
- package/opcua/104-opcuaserver.js +1844 -0
- package/opcua/105-opcuaendpoint.html +224 -0
- package/opcua/105-opcuaendpoint.js +64 -0
- package/opcua/106-opcuaevent.html +110 -0
- package/opcua/106-opcuaevent.js +50 -0
- package/opcua/107-opcuamethod.html +244 -0
- package/opcua/107-opcuamethod.js +521 -0
- package/opcua/108-opcuarights.html +223 -0
- package/opcua/108-opcuarights.js +156 -0
- package/opcua/109-opcuadiscovery.html +51 -0
- package/opcua/109-opcuadiscovery.js +180 -0
- package/opcua/dump_certificates.js +55 -0
- package/opcua/icons/opcuanode.png +0 -0
- package/opcua/icons/opcuanodeLogo.png +0 -0
- package/opcua/opcua-basics.js +1061 -0
- package/opcua/public/vendor/opc-foundation/binary/OPC.ISA95.Types.bsd.xml +141 -0
- package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Adi.Types.bsd.xml +76 -0
- package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Di.Types.bsd.xml +11 -0
- package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Gds.Types.bsd.xml +25 -0
- package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Types.bsd.xml +2391 -0
- package/opcua/public/vendor/opc-foundation/csv/AttributeIds.csv +22 -0
- package/opcua/public/vendor/opc-foundation/csv/NodeIds.csv +5746 -0
- package/opcua/public/vendor/opc-foundation/csv/Opc.Ua.StatusCodes.csv +227 -0
- package/opcua/public/vendor/opc-foundation/csv/OpcUaDiModel.csv +1523 -0
- package/opcua/public/vendor/opc-foundation/csv/OpcUaGdsModel.csv +400 -0
- package/opcua/public/vendor/opc-foundation/csv/ServerCapabilities.csv +13 -0
- package/opcua/public/vendor/opc-foundation/csv/ServerCapabilityIdentifiers.csv +13 -0
- package/opcua/public/vendor/opc-foundation/csv/StatusCode.csv +227 -0
- package/opcua/public/vendor/opc-foundation/csv/StatusCodes.csv +227 -0
- package/opcua/public/vendor/opc-foundation/schema/OPC.ISA95.Types.xsd +163 -0
- package/opcua/public/vendor/opc-foundation/schema/OPCBinarySchema.xsd +119 -0
- package/opcua/public/vendor/opc-foundation/schema/Opc.Ua.Types.xsd +3938 -0
- package/opcua/public/vendor/opc-foundation/schema/SecuredApplication.xsd +106 -0
- package/opcua/public/vendor/opc-foundation/schema/UANodeSet.xsd +420 -0
- package/opcua/public/vendor/opc-foundation/xml/OPC.ISA95.NodeSet2.Nov52013.xml +3293 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.ISA95.NodeSet2.xml +3589 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.NodeSet2.xml +8301 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.bsd.xml +76 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.xsd +157 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.AutoID.NodeSet2.xml +6062 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.NodeSet2.xml +5066 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.PredefinedNodes.xml +11495 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd +86 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd.xml +11 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.xsd +155 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet.xml +15226 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet2.xml +6214 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.bsd.xml +25 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.xsd +30 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-2.xml +134881 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-3.xml +134881 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet.xml +134881 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part10.xml +832 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part11.xml +793 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part12.xml +3877 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part13.xml +344 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part3.xml +1103 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part4.xml +3091 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part5.xml +17092 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part8.xml +525 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part9.xml +2049 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part999.xml +134 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.xml +67169 -0
- package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.PredefinedNodes.xml +64500 -0
- package/opcua/public/vendor/opc-foundation/xml/OpcUaDiModel.xml +1239 -0
- package/opcua/utils.js +55 -0
- package/package.json +68 -0
- package/users.json +22 -0
|
@@ -0,0 +1,2741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
Copyright 2018 Valmet Automation Inc.
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
**/
|
|
18
|
+
|
|
19
|
+
require("node-opcua");
|
|
20
|
+
require("util");
|
|
21
|
+
|
|
22
|
+
module.exports = function (RED) {
|
|
23
|
+
"use strict";
|
|
24
|
+
let chalk = require("chalk");
|
|
25
|
+
let opcua = require('node-opcua');
|
|
26
|
+
const { NodeCrawler } = require("node-opcua-client-crawler"); // Legacy support
|
|
27
|
+
let opcuaBasics = require('./opcua-basics');
|
|
28
|
+
let crypto_utils = require("node-opcua-crypto"); // opcua.crypto_utils;
|
|
29
|
+
let fileTransfer = require("node-opcua-file-transfer");
|
|
30
|
+
let async = require("async");
|
|
31
|
+
let fs = require("fs");
|
|
32
|
+
let os = require("os");
|
|
33
|
+
let cloneDeep = require('lodash.clonedeep');
|
|
34
|
+
let DataType = opcua.DataType;
|
|
35
|
+
let AttributeIds = opcua.AttributeIds;
|
|
36
|
+
let TimestampsToReturn = opcua.TimestampsToReturn;
|
|
37
|
+
|
|
38
|
+
const { createClientCertificateManager } = require("./utils");
|
|
39
|
+
|
|
40
|
+
const { stringify } = require('flatted');
|
|
41
|
+
|
|
42
|
+
function OpcUaClientNode(n) {
|
|
43
|
+
RED.nodes.createNode(this, n);
|
|
44
|
+
this.name = n.name;
|
|
45
|
+
this.action = n.action;
|
|
46
|
+
let originalAction = n.action;
|
|
47
|
+
this.time = n.time;
|
|
48
|
+
this.timeUnit = n.timeUnit;
|
|
49
|
+
this.deadbandtype = n.deadbandtype;
|
|
50
|
+
this.deadbandvalue = n.deadbandvalue;
|
|
51
|
+
this.certificate = n.certificate; // n == NONE, l == Local file, e == Endpoint, u == Upload
|
|
52
|
+
this.localfile = n.localfile; // Local certificate file
|
|
53
|
+
this.localkeyfile = n.localkeyfile; // Local private key file
|
|
54
|
+
this.useTransport = n.useTransport;
|
|
55
|
+
this.maxChunkCount = n.maxChunkCount;
|
|
56
|
+
this.maxMessageSize = n.maxMessageSize;
|
|
57
|
+
this.receiveBufferSize = n.receiveBufferSize;
|
|
58
|
+
this.sendBufferSize = n.sendBufferSize;
|
|
59
|
+
this.setstatusandtime = n.setstatusandtime;
|
|
60
|
+
this.keepsessionalive = n.keepsessionalive;
|
|
61
|
+
this.applicationName = n.applicationName;
|
|
62
|
+
this.applicationUri = n.applicationUri;
|
|
63
|
+
let node = this;
|
|
64
|
+
let opcuaEndpoint = RED.nodes.getNode(n.endpoint); // Use as global for the node
|
|
65
|
+
let userIdentity = { type: opcua.UserTokenType.Anonymous }; // Initialize with Anonymous
|
|
66
|
+
let connectionOption = {};
|
|
67
|
+
let cmdQueue = []; // queue msgs which can currently not be handled because session is not established, yet and currentStatus is 'connecting'
|
|
68
|
+
let currentStatus = ''; // the status value set set by node.status(). Didn't find a way to read it back.
|
|
69
|
+
let multipleItems = []; // Store & read multiple nodeIds
|
|
70
|
+
let writeMultipleItems = []; // Store & write multiple nodeIds & values
|
|
71
|
+
let options;
|
|
72
|
+
connectionOption.securityPolicy = opcuaEndpoint.securityPolicy
|
|
73
|
+
connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode] || opcua.MessageSecurityMode.None;
|
|
74
|
+
let userCertificate = opcuaEndpoint.userCertificate;
|
|
75
|
+
let userPrivatekey = opcuaEndpoint.userPrivatekey;
|
|
76
|
+
|
|
77
|
+
connectionOption.clientCertificateManager = createClientCertificateManager();
|
|
78
|
+
|
|
79
|
+
if (node.certificate === "l" && node.localfile) {
|
|
80
|
+
verbose_log("Using 'own' local certificate file " + node.localfile);
|
|
81
|
+
// User must define absolute path
|
|
82
|
+
let certfile = node.localfile;
|
|
83
|
+
let keyfile = node.localkeyfile;
|
|
84
|
+
connectionOption.certificateFile = certfile;
|
|
85
|
+
connectionOption.privateKeyFile = keyfile;
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(certfile)) {
|
|
88
|
+
node_error("Local certificate file not found: " + certfile)
|
|
89
|
+
}
|
|
90
|
+
if (!fs.existsSync(keyfile)) {
|
|
91
|
+
node_error("Local private key file not found: " + keyfile)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (node.applicationName) connectionOption.applicationName = node.applicationName;
|
|
95
|
+
if (node.applicationUri) connectionOption.applicationUri = node.applicationUri;
|
|
96
|
+
// Moved needed options to client create
|
|
97
|
+
connectionOption.requestedSessionTimeout = opcuaBasics.calc_milliseconds_by_time_and_unit(300, "s");
|
|
98
|
+
// DO NOT USE must be NodeOPCUA-Client !! connectionOption.applicationName = node.name; // Application name
|
|
99
|
+
connectionOption.clientName = node.name; // This is used for the session names
|
|
100
|
+
connectionOption.endpointMustExist = false;
|
|
101
|
+
connectionOption.defaultSecureTokenLifetime = 40000 * 5;
|
|
102
|
+
// From the node UI, keep min values!
|
|
103
|
+
// Needed or not?
|
|
104
|
+
if (!node.maxChunkCount || parseInt(node.maxChunkCount) < 1) node.maxChunkCount = 1;
|
|
105
|
+
if (!node.maxMessageSize || parseInt(node.maxMessageSize) < 8192) node.maxMessageSize = 8192;
|
|
106
|
+
if (!node.receiveBufferSize || parseInt(node.receiveBufferSize) < 8 * 1024) node.receiveBufferSize = 8 * 1024;
|
|
107
|
+
if (!node.sendBufferSize || parseInt(node.sendBufferSize) < 8 * 1024) node.sendBufferSize = 8 * 1024;
|
|
108
|
+
|
|
109
|
+
let transportSettings = {
|
|
110
|
+
maxChunkCount: parseInt(node.maxChunkCount), // Default 1
|
|
111
|
+
maxMessageSize: parseInt(node.maxMessageSize), // should be at least 8192
|
|
112
|
+
receiveBufferSize: parseInt(node.receiveBufferSize), // 8 * 1024,
|
|
113
|
+
sendBufferSize: parseInt(node.sendBufferSize) // 8 * 1024
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (node.useTransport === true) {
|
|
117
|
+
connectionOption["transportSettings"] = { ...transportSettings };
|
|
118
|
+
verbose_log(chalk.red("Using, transport settings: ") + chalk.cyan(JSON.stringify(connectionOption["transportSettings"])));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
connectionOption.connectionStrategy = {
|
|
122
|
+
maxRetry: 10512000, // Limited to max 10 ~5min // 10512000, // 10 years should be enough. No infinite parameter for backoff.
|
|
123
|
+
initialDelay: 5000, // 5s
|
|
124
|
+
maxDelay: 30 * 1000 // 30s
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Ensure Anonymous login
|
|
128
|
+
if (connectionOption.securityMode === opcua.MessageSecurityMode.None || opcuaEndpoint.none === true) {
|
|
129
|
+
userIdentity = { type: opcua.UserTokenType.Anonymous };
|
|
130
|
+
}
|
|
131
|
+
if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) {
|
|
132
|
+
userIdentity = { type: opcua.UserTokenType.Anonymous };
|
|
133
|
+
}
|
|
134
|
+
if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) {
|
|
135
|
+
node.error("Cannot use username & password & user certificate at the same time!");
|
|
136
|
+
}
|
|
137
|
+
// Username & password with securityMode None is allowed
|
|
138
|
+
if (opcuaEndpoint.login === true) { // } && connectionOption.securityMode != opcua.MessageSecurityMode.None) {
|
|
139
|
+
userIdentity = {
|
|
140
|
+
type: opcua.UserTokenType.UserName,
|
|
141
|
+
userName: opcuaEndpoint.credentials.user.toString(),
|
|
142
|
+
password: opcuaEndpoint.credentials.password ? opcuaEndpoint.credentials.password.toString() : ""
|
|
143
|
+
};
|
|
144
|
+
verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity)));
|
|
145
|
+
}
|
|
146
|
+
else if (opcuaEndpoint.usercert === true) {
|
|
147
|
+
if (!fs.existsSync(userCertificate)) {
|
|
148
|
+
node.error("User certificate file not found: " + userCertificate);
|
|
149
|
+
}
|
|
150
|
+
const certificateData = crypto_utils.readCertificate(userCertificate);
|
|
151
|
+
|
|
152
|
+
if (!fs.existsSync(userPrivatekey)) {
|
|
153
|
+
node.error("User private key file not found: " + userPrivatekey);
|
|
154
|
+
}
|
|
155
|
+
const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey);
|
|
156
|
+
userIdentity = {
|
|
157
|
+
certificateData,
|
|
158
|
+
privateKey,
|
|
159
|
+
type: opcua.UserTokenType.Certificate // User certificate
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
userIdentity = { type: opcua.UserTokenType.Anonymous };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
verbose_log(chalk.green("UserIdentity: ") + chalk.cyan(JSON.stringify(userIdentity)));
|
|
167
|
+
let items = [];
|
|
168
|
+
let subscription; // only one subscription needed to hold multiple monitored Items
|
|
169
|
+
|
|
170
|
+
let monitoredItems = new Map();
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
function node_error(err) {
|
|
174
|
+
node.error(chalk.red("Client node error on: " + node.name + " error: " + stringify(err)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function verbose_warn(logMessage) {
|
|
178
|
+
if (opcuaEndpoint?.name && node.name) { // Optional name
|
|
179
|
+
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));
|
|
180
|
+
}
|
|
181
|
+
node.warn(logMessage);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function verbose_log(logMessage) {
|
|
185
|
+
if (RED.settings.verbose) {
|
|
186
|
+
node.debug(chalk.yellow(logMessage));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function getBrowseName(_session, nodeId) {
|
|
191
|
+
const dataValue = await _session.read({
|
|
192
|
+
attributeId: AttributeIds.BrowseName,
|
|
193
|
+
nodeId
|
|
194
|
+
});
|
|
195
|
+
if (dataValue.statusCode.isGood()) {
|
|
196
|
+
const browseName = dataValue?.value?.value?.name || "UnknownEvent";
|
|
197
|
+
return browseName;
|
|
198
|
+
} else {
|
|
199
|
+
return "???";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Fields selected alarm fields
|
|
203
|
+
// EventFields same order returned from server array of variants (filled or empty)
|
|
204
|
+
async function __dumpEvent(node, session, fields, eventFields, _callback) {
|
|
205
|
+
let msg = {};
|
|
206
|
+
msg.payload = {};
|
|
207
|
+
verbose_log(chalk.yellow("Event Fields: ") + chalk.cyan(JSON.stringify(eventFields)));
|
|
208
|
+
set_node_status_to("active event");
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < eventFields.length; i++) {
|
|
211
|
+
let variant = eventFields[i];
|
|
212
|
+
let fieldName = fields[i];
|
|
213
|
+
verbose_log(chalk.yellow("Event Field: ") + chalk.cyan(fieldName) + " " + chalk.cyan(stringify(variant)));
|
|
214
|
+
// Check if variant is NodeId and then get qualified name (browseName)
|
|
215
|
+
if (variant?.dataType && variant.dataType === DataType.NodeId) {
|
|
216
|
+
fieldName = await getBrowseName(session, variant.value);
|
|
217
|
+
}
|
|
218
|
+
if (!variant || variant.dataType === DataType.Null || !variant.value) {
|
|
219
|
+
verbose_log(chalk.red("No variant or variant dataType is Null or no variant value! Variant: ") + chalk.cyan(JSON.stringify(variant)));
|
|
220
|
+
} else {
|
|
221
|
+
if (fieldName === "EventId" && variant?.dataType) {
|
|
222
|
+
msg.payload[fieldName] = "0x" + variant.value.toString("hex"); // As in UaExpert
|
|
223
|
+
msg.payload["_" + fieldName] = variant; // Keep as ByteString
|
|
224
|
+
} else {
|
|
225
|
+
// Added handling for LocalizedText to use OS locale / node-red __language__
|
|
226
|
+
if (fieldName === "Message") {
|
|
227
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
228
|
+
if (variant.value.length > 1) {
|
|
229
|
+
let i = 0;
|
|
230
|
+
while (i < variant.value.length) {
|
|
231
|
+
let localText = variant.value[i];
|
|
232
|
+
// Change according locale
|
|
233
|
+
if (localText.locale === locale) {
|
|
234
|
+
variant.value = localText;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
i++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
msg.payload[fieldName] = opcuaBasics.clone_object(variant.value);
|
|
242
|
+
}
|
|
243
|
+
// if available, needed for Acknowledge function in client
|
|
244
|
+
if (fieldName === "ConditionId" && variant?.dataType) {
|
|
245
|
+
msg.topic = variant.value.toString();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Set message topic
|
|
251
|
+
if (eventFields.length === 0) {
|
|
252
|
+
msg.topic = "No EventFields";
|
|
253
|
+
}
|
|
254
|
+
// if available, needed for Acknowledge function in client
|
|
255
|
+
else if (msg.payload.ConditionId) {
|
|
256
|
+
msg.topic = msg.payload.ConditionId.toString();
|
|
257
|
+
}
|
|
258
|
+
else if (msg.payload.EventId) {
|
|
259
|
+
msg.topic = msg.payload.EventId.toString(); // Set then this can be used to Acknowledge event
|
|
260
|
+
}
|
|
261
|
+
else if (msg.payload.EventType) {
|
|
262
|
+
msg.topic = msg.payload.EventType.toString();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
verbose_log(chalk.yellow("Event message topic: ") + chalk.cyan(msg.topic));
|
|
266
|
+
node.send([msg, null, null]);
|
|
267
|
+
|
|
268
|
+
_callback();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let eventQueue = new async.queue(function (task, callback) {
|
|
272
|
+
__dumpEvent(task.node, task.session, task.fields, task.eventFields, callback);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
function dumpEvent(node, session, fields, eventFields, _callback) {
|
|
276
|
+
eventQueue.push({
|
|
277
|
+
node: node,
|
|
278
|
+
session: session,
|
|
279
|
+
fields: fields,
|
|
280
|
+
eventFields: eventFields,
|
|
281
|
+
_callback: _callback
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Listener functions that can be removed on reconnect
|
|
286
|
+
const reestablish = function () {
|
|
287
|
+
set_node_status2_to("connected", "re-established");
|
|
288
|
+
};
|
|
289
|
+
const backoff = function (attempt, delay) {
|
|
290
|
+
let msg = {};
|
|
291
|
+
msg.error = {};
|
|
292
|
+
msg.error.message = "reconnect";
|
|
293
|
+
msg.error.source = this;
|
|
294
|
+
// node.error("reconnect", msg);
|
|
295
|
+
verbose_log(chalk.red("reconnect")) // + chalk.cyan(stringify(msg))); // msg is TOO big to show
|
|
296
|
+
set_node_status2_to("reconnect", "attempt #" + attempt + " retry in " + delay / 1000.0 + " sec");
|
|
297
|
+
};
|
|
298
|
+
const reconnection = function () {
|
|
299
|
+
set_node_status2_to("reconnect", "starting...");
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
function create_opcua_client(callback) {
|
|
303
|
+
node.client = null;
|
|
304
|
+
options = {
|
|
305
|
+
securityMode: connectionOption.securityMode,
|
|
306
|
+
securityPolicy: connectionOption.securityPolicy,
|
|
307
|
+
defaultSecureTokenLifetime: connectionOption.defaultSecureTokenLifetime,
|
|
308
|
+
endpointMustExist: connectionOption.endpointMustExist,
|
|
309
|
+
connectionStrategy: connectionOption.connectionStrategy,
|
|
310
|
+
clientCertificateManager: connectionOption.clientCertificateManager,
|
|
311
|
+
clientName: node.name, // Fix for #664 sessionName
|
|
312
|
+
keepSessionAlive: node.keepsessionalive,
|
|
313
|
+
requestedSessionTimeout: 60000 * 5, // 5min, default 1min
|
|
314
|
+
automaticallyAcceptUnknownCertificate: true,
|
|
315
|
+
// transportSettings: transportSettings // Some
|
|
316
|
+
applicationName: connectionOption.applicationName,
|
|
317
|
+
applicationUri: connectionOption.applicationUri
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
// Use empty 0.0.0.0 address as "no client" initial value
|
|
321
|
+
if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") == 0) {
|
|
322
|
+
verbose_warn(`close opcua client ${node.client} userIdentity ${userIdentity.type}`);
|
|
323
|
+
if (node.client) {
|
|
324
|
+
close_opcua_client("connection error: no session", 0);
|
|
325
|
+
}
|
|
326
|
+
items = [];
|
|
327
|
+
node.items = items;
|
|
328
|
+
set_node_status_to("no client");
|
|
329
|
+
if (callback) {
|
|
330
|
+
callback();
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Normal client
|
|
335
|
+
if (!node.keepsessionalive) {
|
|
336
|
+
node.keepsessionalive = false;
|
|
337
|
+
}
|
|
338
|
+
verbose_log(chalk.yellow("Keep session alive: ") + chalk.cyan(node.keepsessionalive));
|
|
339
|
+
if (node.useTransport === true) {
|
|
340
|
+
options["transportSettings"] = JSON.parse(JSON.stringify(connectionOption.transportSettings));
|
|
341
|
+
verbose_log(chalk.red("NOTE: Using transport settings: " + chalk.cyan(stringify(options))));
|
|
342
|
+
}
|
|
343
|
+
verbose_log(chalk.green("1) CREATE CLIENT: ") + chalk.cyan(stringify(options)));
|
|
344
|
+
node.client = opcua.OPCUAClient.create(options);
|
|
345
|
+
node.client.on("connection_reestablished", reestablish);
|
|
346
|
+
node.client.on("backoff", backoff);
|
|
347
|
+
node.client.on("start_reconnection", reconnection);
|
|
348
|
+
node.client.clientCertificateManager = connectionOption.clientCertificateManager; // FIX connection issue
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
node_error("Cannot create client, check connection options: " + stringify(options)); // connectionOption
|
|
352
|
+
set_node_status_to("Cannot create client");
|
|
353
|
+
}
|
|
354
|
+
items = [];
|
|
355
|
+
node.items = items;
|
|
356
|
+
set_node_status_to("create client");
|
|
357
|
+
if (callback) {
|
|
358
|
+
callback();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function reset_opcua_client(callback) {
|
|
363
|
+
if (node.client) {
|
|
364
|
+
node.client.disconnect(function () {
|
|
365
|
+
verbose_log("Client disconnected!");
|
|
366
|
+
create_opcua_client(callback);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function close_opcua_client(message, error) {
|
|
372
|
+
// verbose_warn(`closing opcua client ${opcua.client == null} userIdentity ${JSON.stringify(userIdentity)}`)
|
|
373
|
+
if (node.client) {
|
|
374
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
375
|
+
node.client.removeListener("backoff", backoff);
|
|
376
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
377
|
+
try {
|
|
378
|
+
// Always call disconnect() regardless of isReconnecting state.
|
|
379
|
+
// Skipping disconnect when isReconnecting=true leaves the TCP socket
|
|
380
|
+
// open on the PLC, causing connections to accumulate over time.
|
|
381
|
+
node.client.disconnect(function () {
|
|
382
|
+
node.client = null;
|
|
383
|
+
verbose_log("Client disconnected!");
|
|
384
|
+
if (error === 0) {
|
|
385
|
+
set_node_status_to("closed");
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
set_node_error_status_to(message, error)
|
|
389
|
+
node.error("Client disconnected & closed: " + message + " error: " + error.toString());
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
node.client = null;
|
|
395
|
+
node_error("Error on disconnect: " + stringify(err));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function set_node_status_to(statusValue) {
|
|
401
|
+
verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
|
|
402
|
+
let statusParameter = opcuaBasics.get_node_status(statusValue);
|
|
403
|
+
currentStatus = statusValue;
|
|
404
|
+
let endpoint = "";
|
|
405
|
+
if (opcuaEndpoint && opcuaEndpoint.endpoint) {
|
|
406
|
+
endpoint = opcuaEndpoint.endpoint
|
|
407
|
+
}
|
|
408
|
+
node.status({
|
|
409
|
+
fill: statusParameter.fill,
|
|
410
|
+
shape: statusParameter.shape,
|
|
411
|
+
text: statusParameter.status,
|
|
412
|
+
endpoint: `${endpoint}`
|
|
413
|
+
});
|
|
414
|
+
node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null])
|
|
415
|
+
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function set_node_status2_to(statusValue, message) {
|
|
419
|
+
verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
|
|
420
|
+
let statusParameter = opcuaBasics.get_node_status(statusValue);
|
|
421
|
+
currentStatus = statusValue;
|
|
422
|
+
let endpoint = "";
|
|
423
|
+
if (opcuaEndpoint?.endpoint) {
|
|
424
|
+
endpoint = opcuaEndpoint.endpoint
|
|
425
|
+
}
|
|
426
|
+
node.status({
|
|
427
|
+
fill: statusParameter.fill,
|
|
428
|
+
shape: statusParameter.shape,
|
|
429
|
+
text: statusParameter.status + " " + message,
|
|
430
|
+
endpoint: `${endpoint}`
|
|
431
|
+
});
|
|
432
|
+
node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function set_node_error_status_to(statusValue, error) {
|
|
436
|
+
verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
|
|
437
|
+
let statusParameter = opcuaBasics.get_node_status(statusValue);
|
|
438
|
+
currentStatus = statusValue;
|
|
439
|
+
let endpoint = "";
|
|
440
|
+
if (opcuaEndpoint?.endpoint) {
|
|
441
|
+
endpoint = opcuaEndpoint.endpoint
|
|
442
|
+
}
|
|
443
|
+
if (!error) {
|
|
444
|
+
error = "";
|
|
445
|
+
}
|
|
446
|
+
node.status({
|
|
447
|
+
fill: statusParameter.fill,
|
|
448
|
+
shape: statusParameter.shape,
|
|
449
|
+
text: statusParameter.status + " " + error,
|
|
450
|
+
endpoint: `${endpoint}`
|
|
451
|
+
});
|
|
452
|
+
node.send([null, { error: error, endpoint: `${endpoint}`, status: currentStatus }, null]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function connect_opcua_client() {
|
|
456
|
+
if (opcuaEndpoint?.login === true) {
|
|
457
|
+
verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity)));
|
|
458
|
+
if (opcuaEndpoint.credentials && opcuaEndpoint['user'] && opcuaEndpoint['password']) {
|
|
459
|
+
userIdentity = {
|
|
460
|
+
type: opcua.UserTokenType.UserName,
|
|
461
|
+
userName: opcuaEndpoint?.credentials?.user?.toString(),
|
|
462
|
+
password: opcuaEndpoint?.credentials?.password?.toString()
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
else if (opcuaEndpoint['user'] && opcuaEndpoint['password']) {
|
|
466
|
+
userIdentity = {
|
|
467
|
+
type: opcua.UserTokenType.UserName,
|
|
468
|
+
userName: opcuaEndpoint?.user?.toString(),
|
|
469
|
+
password: opcuaEndpoint?.password?.toString()
|
|
470
|
+
};
|
|
471
|
+
} else {
|
|
472
|
+
node_error("Please enter user or password in credentials or same level as login")
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else if (opcuaEndpoint?.usercert === true) {
|
|
476
|
+
if (!fs.existsSync(userCertificate)) {
|
|
477
|
+
node.error("User certificate file not found: " + userCertificate);
|
|
478
|
+
}
|
|
479
|
+
const certificateData = crypto_utils.readCertificate(userCertificate);
|
|
480
|
+
if (!fs.existsSync(userPrivatekey)) {
|
|
481
|
+
node.error("User private key file not found: " + userPrivatekey);
|
|
482
|
+
}
|
|
483
|
+
const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey);
|
|
484
|
+
userIdentity = {
|
|
485
|
+
certificateData,
|
|
486
|
+
privateKey,
|
|
487
|
+
type: opcua.UserTokenType.Certificate // User certificate
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
verbose_warn(chalk.red("userIdentity is ANONYMOUS "));
|
|
492
|
+
userIdentity = { type: opcua.UserTokenType.Anonymous };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Refactored from old async Javascript to new Typescript with await
|
|
496
|
+
let session;
|
|
497
|
+
// STEP 1
|
|
498
|
+
// First connect to server´s endpoint
|
|
499
|
+
if (opcuaEndpoint?.endpoint) {
|
|
500
|
+
verbose_log(chalk.yellow("Connecting to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
node_error("No client endpoint listed! Waiting...");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") === 0) {
|
|
508
|
+
set_node_status_to("no client")
|
|
509
|
+
}
|
|
510
|
+
/*
|
|
511
|
+
else {
|
|
512
|
+
set_node_status_to("connecting");
|
|
513
|
+
}
|
|
514
|
+
if (!node.client) {
|
|
515
|
+
verbose_log("No client to connect...");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
*/
|
|
519
|
+
verbose_log(chalk.yellow("Exact endpointUrl: ") + chalk.cyan(opcuaEndpoint?.endpoint) + chalk.yellow(" hostname: ") + chalk.cyan(os.hostname()));
|
|
520
|
+
try {
|
|
521
|
+
node.client = opcua.OPCUAClient.create(options);
|
|
522
|
+
node.client.clientCertificateManager = connectionOption.clientCertificateManager;
|
|
523
|
+
await node.client.clientCertificateManager.initialize();
|
|
524
|
+
}
|
|
525
|
+
catch (error1) {
|
|
526
|
+
set_node_status_to("invalid certificate");
|
|
527
|
+
let msg = {};
|
|
528
|
+
msg.error = {};
|
|
529
|
+
msg.error.message = "Certificate error: " + error1.message;
|
|
530
|
+
msg.error.source = this;
|
|
531
|
+
node.error("Certificate error", msg);
|
|
532
|
+
}
|
|
533
|
+
node.debug(chalk.yellow("Trusted folder: ") + chalk.cyan(node.client?.clientCertificateManager?.trustedFolder));
|
|
534
|
+
node.debug(chalk.yellow("Rejected folder: ") + chalk.cyan(node.client?.clientCertificateManager?.rejectedFolder));
|
|
535
|
+
node.debug(chalk.yellow("Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.crlFolder));
|
|
536
|
+
node.debug(chalk.yellow("Issuers Cert folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCertFolder));
|
|
537
|
+
node.debug(chalk.yellow("Issuers Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCrlFolder));
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
verbose_log(chalk.green("2) Connecting using endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint) +
|
|
541
|
+
chalk.green(" securityMode: ") + chalk.cyan(connectionOption.securityMode) +
|
|
542
|
+
chalk.green(" securityPolicy: ") + chalk.cyan(connectionOption.securityPolicy));
|
|
543
|
+
await node.client.connect(opcuaEndpoint?.endpoint);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
verbose_warn("Case A: Endpoint does not contain, 1==None 2==Sign 3==Sign&Encrypt, using securityMode: " + stringify(connectionOption.securityMode));
|
|
546
|
+
verbose_warn(" using securityPolicy: " + stringify(connectionOption.securityPolicy));
|
|
547
|
+
verbose_warn("Case B: UserName & password does not match to server (needed by Sign or SignAndEncrypt), check username: " + userIdentity.userName + " and password: " + userIdentity.password);
|
|
548
|
+
verbose_warn("Case C: With Sign you cannot use SecurityPolicy None!!");
|
|
549
|
+
verbose_warn("Case D: Certificate chain not valid, check server certificate chain against:");
|
|
550
|
+
verbose_warn(" CRL folder: " + node.client?.clientCertificateManager?.crlFolder);
|
|
551
|
+
verbose_warn(" Issuer folder: " + node.client?.clientCertificateManager?.issuersCertFolder);
|
|
552
|
+
verbose_warn(" Issuer CRL folder: " + node.client?.clientCertificateManager?.issuersCrlFolder);
|
|
553
|
+
// verbose_error("Invalid endpoint parameters: ", err);
|
|
554
|
+
node_error("Wrong endpoint parameters: " + JSON.stringify(opcuaEndpoint) + ", error: " + JSON.stringify(err));
|
|
555
|
+
set_node_status_to("invalid endpoint");
|
|
556
|
+
let msg = {};
|
|
557
|
+
msg.error = {};
|
|
558
|
+
msg.error.message = "Invalid endpoint: " + err;
|
|
559
|
+
msg.error.source = this;
|
|
560
|
+
node.error("Invalid endpoint", msg);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
verbose_log(chalk.green("Connected to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint));
|
|
564
|
+
|
|
565
|
+
// STEP 2
|
|
566
|
+
// This will succeed first time only if security policy and mode are None
|
|
567
|
+
// Later user can use path and local file to access server certificate file
|
|
568
|
+
|
|
569
|
+
if (!node.client) {
|
|
570
|
+
node_error("Client not yet created & connected, cannot getEndpoints!");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// dumpCertificates(node.client); // TODO Wrong folder or something to solve
|
|
575
|
+
|
|
576
|
+
// STEP 3
|
|
577
|
+
// verbose_log("Create session...");
|
|
578
|
+
try {
|
|
579
|
+
// verbose_warn(`Create session with userIdentity node.client ${node.client == null} userIdentity ${JSON.stringify(userIdentity)}`)
|
|
580
|
+
verbose_log(chalk.green("3) Create session with userIdentity at: ") + chalk.cyan(JSON.stringify(userIdentity)));
|
|
581
|
+
// {"clientName": "Node-red OPC UA Client node " + node.name},
|
|
582
|
+
// sessionName = "Node-red OPC UA Client node " + node.name;
|
|
583
|
+
if (!node.client) {
|
|
584
|
+
node_error("Client not yet created, cannot create session");
|
|
585
|
+
close_opcua_client("connection error: no client", 0);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
session = await node.client.createSession(userIdentity);
|
|
589
|
+
if (!session) {
|
|
590
|
+
node_error("Create session failed!");
|
|
591
|
+
verbose_warn(`Create session failed!`)
|
|
592
|
+
|
|
593
|
+
close_opcua_client("connection error: no session", 0);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
node.session = session;
|
|
597
|
+
set_node_status_to("session active");
|
|
598
|
+
for (let i in cmdQueue) {
|
|
599
|
+
processInputMsg(cmdQueue[i]);
|
|
600
|
+
}
|
|
601
|
+
cmdQueue = [];
|
|
602
|
+
} catch (err) {
|
|
603
|
+
node_error(node.name + " OPC UA connection error: " + err.message);
|
|
604
|
+
verbose_log(err);
|
|
605
|
+
node.session = null;
|
|
606
|
+
close_opcua_client("connection error", err);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function make_subscription(callback, msg, parameters) {
|
|
611
|
+
let newSubscription = null;
|
|
612
|
+
|
|
613
|
+
if (!node.session) {
|
|
614
|
+
verbose_log("Subscription without session");
|
|
615
|
+
return newSubscription;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!parameters) {
|
|
619
|
+
verbose_log("Subscription without parameters");
|
|
620
|
+
return newSubscription;
|
|
621
|
+
}
|
|
622
|
+
verbose_log("Publishing interval " + stringify(parameters));
|
|
623
|
+
newSubscription = opcua.ClientSubscription.create(node.session, parameters);
|
|
624
|
+
verbose_log("Subscription " + newSubscription.toString());
|
|
625
|
+
newSubscription.on("initialized", function () {
|
|
626
|
+
verbose_log("Subscription initialized");
|
|
627
|
+
set_node_status_to("initialized");
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
newSubscription.on("started", function () {
|
|
631
|
+
verbose_log("Subscription subscribed ID: " + newSubscription.subscriptionId);
|
|
632
|
+
set_node_status_to("subscribed");
|
|
633
|
+
monitoredItems.clear();
|
|
634
|
+
callback(newSubscription, msg);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
newSubscription.on("keepalive", function () {
|
|
638
|
+
verbose_log("Subscription keepalive ID: " + newSubscription.subscriptionId);
|
|
639
|
+
set_node_status_to("keepalive");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
newSubscription.on("terminated", function () {
|
|
643
|
+
verbose_log("Subscription terminated ID: " + newSubscription.subscriptionId);
|
|
644
|
+
set_node_status_to("terminated");
|
|
645
|
+
subscription = null;
|
|
646
|
+
monitoredItems.clear();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
newSubscription.on("error", function (err) {
|
|
650
|
+
verbose_log("Subscription error on ID: " + newSubscription.subscriptionId + ". " + err);
|
|
651
|
+
set_node_error_status_to("subscription error")
|
|
652
|
+
subscription = null;
|
|
653
|
+
monitoredItems.clear();
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
return newSubscription;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!node.client) {
|
|
660
|
+
create_opcua_client(connect_opcua_client);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function processInputMsg(msg) {
|
|
664
|
+
if (msg.action === "reconnect") {
|
|
665
|
+
cmdQueue = [];
|
|
666
|
+
// msg.endpoint can be used to change endpoint
|
|
667
|
+
msg.action = "";
|
|
668
|
+
reconnect(msg);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (msg.action === "connect") {
|
|
672
|
+
cmdQueue = [];
|
|
673
|
+
// msg.endpoint can be used to change endpoint
|
|
674
|
+
msg.action = "";
|
|
675
|
+
connect_action_input(msg);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (msg.action && msg.action.length > 0) {
|
|
679
|
+
verbose_log("Override node action by msg.action: " + msg.action);
|
|
680
|
+
node.action = msg.action;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
verbose_log(chalk.green("Using node action: ") + chalk.cyan(originalAction));
|
|
684
|
+
node.action = originalAction; // Use original action from the node
|
|
685
|
+
}
|
|
686
|
+
// With new node-red easier to set action into payload
|
|
687
|
+
if (msg.payload?.action && msg.payload.action.length > 0) {
|
|
688
|
+
verbose_log("Override node action by msg.payload.action:" + msg.payload.action);
|
|
689
|
+
node.action = msg.payload.action;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!node.action) {
|
|
693
|
+
verbose_warn("Can't work without action (read, write, browse ...)");
|
|
694
|
+
//node.send(msg); // do not send in case of error
|
|
695
|
+
node.send([null, { error: "Can't work without action (read, write, browse ...)", endpoint: `${opcuaEndpoint.endpoint}`, status: currentStatus }, null]);
|
|
696
|
+
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!node.client || !node.session) {
|
|
701
|
+
verbose_log("Not connected, current status: " + currentStatus);
|
|
702
|
+
// Added statuses when msg must be put to queue
|
|
703
|
+
// Added statuses when msg must be put to queue
|
|
704
|
+
const statuses = ['', 'create client', 'connecting', 'reconnect'];
|
|
705
|
+
if (statuses.includes(currentStatus)) {
|
|
706
|
+
cmdQueue.push(msg);
|
|
707
|
+
} else {
|
|
708
|
+
verbose_warn(`can't work without OPC UA client ${node.client} client ${node.session}`);
|
|
709
|
+
reset_opcua_client(connect_opcua_client);
|
|
710
|
+
}
|
|
711
|
+
node.send([null, { error: "can't work without OPC UA client", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!node.session.sessionId == "terminated") {
|
|
716
|
+
verbose_warn("terminated OPC UA Session");
|
|
717
|
+
reset_opcua_client(connect_opcua_client);
|
|
718
|
+
|
|
719
|
+
node.send([null, { error: "terminated OPC UA Session", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
720
|
+
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (msg.action && (msg.action === "connect" || msg.action === "disconnect")) {
|
|
725
|
+
// OK
|
|
726
|
+
msg.action = "";
|
|
727
|
+
} else if (!msg.topic) {
|
|
728
|
+
verbose_warn("can't work without OPC UA NodeId - msg.topic empty");
|
|
729
|
+
node.send([null, { error: "can't work without OPC UA NodeId", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
verbose_log(chalk.yellow("Action on input: ") + chalk.cyan(node.action) +
|
|
734
|
+
chalk.yellow(" Item from Topic: ") + chalk.cyan(msg.topic) +
|
|
735
|
+
chalk.yellow(" session Id: ") + chalk.cyan(node.session.sessionId));
|
|
736
|
+
|
|
737
|
+
switch (node.action) {
|
|
738
|
+
case "connect":
|
|
739
|
+
connect_action_input(msg);
|
|
740
|
+
break;
|
|
741
|
+
case "disconnect":
|
|
742
|
+
disconnect_action_input(msg);
|
|
743
|
+
break;
|
|
744
|
+
case "reconnect":
|
|
745
|
+
reconnect(msg);
|
|
746
|
+
break;
|
|
747
|
+
case "register":
|
|
748
|
+
register_action_input(msg);
|
|
749
|
+
break;
|
|
750
|
+
case "unregister":
|
|
751
|
+
unregister_action_input(msg);
|
|
752
|
+
break;
|
|
753
|
+
case "read":
|
|
754
|
+
read_action_input(msg);
|
|
755
|
+
break;
|
|
756
|
+
case "history":
|
|
757
|
+
readhistory_action_input(msg);
|
|
758
|
+
break;
|
|
759
|
+
case "info":
|
|
760
|
+
info_action_input(msg);
|
|
761
|
+
break;
|
|
762
|
+
case "build":
|
|
763
|
+
build_extension_object_action_input(msg);
|
|
764
|
+
break;
|
|
765
|
+
case "write":
|
|
766
|
+
write_action_input(msg);
|
|
767
|
+
break;
|
|
768
|
+
case "subscribe":
|
|
769
|
+
subscribe_action_input(msg);
|
|
770
|
+
break;
|
|
771
|
+
case "monitor":
|
|
772
|
+
monitor_action_input(msg);
|
|
773
|
+
break;
|
|
774
|
+
case "unsubscribe":
|
|
775
|
+
unsubscribe_action_input(msg);
|
|
776
|
+
break;
|
|
777
|
+
case "deletesubscribtion": // miss-spelled, this allows old flows to work
|
|
778
|
+
case "deletesubscription":
|
|
779
|
+
delete_subscription_action_input(msg);
|
|
780
|
+
break;
|
|
781
|
+
case "browse":
|
|
782
|
+
browse_action_input(msg);
|
|
783
|
+
break;
|
|
784
|
+
case "events":
|
|
785
|
+
subscribe_events_input(msg);
|
|
786
|
+
break;
|
|
787
|
+
case "readmultiple":
|
|
788
|
+
readmultiple_action_input(msg);
|
|
789
|
+
break;
|
|
790
|
+
case "writemultiple":
|
|
791
|
+
writemultiple_action_input(msg)
|
|
792
|
+
break;
|
|
793
|
+
case "acknowledge":
|
|
794
|
+
acknowledge_input(msg);
|
|
795
|
+
break;
|
|
796
|
+
case "readfile":
|
|
797
|
+
read_file(msg);
|
|
798
|
+
break;
|
|
799
|
+
case "writefile":
|
|
800
|
+
write_file(msg);
|
|
801
|
+
break;
|
|
802
|
+
case "method":
|
|
803
|
+
method_action_input(msg);
|
|
804
|
+
break;
|
|
805
|
+
default:
|
|
806
|
+
verbose_warn("Unknown action: " + node.action + " with msg " + stringify(msg));
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
node.on("input", processInputMsg);
|
|
811
|
+
|
|
812
|
+
async function acknowledge_input(msg) {
|
|
813
|
+
// msg.topic is nodeId of the alarm object like Prosys ns=6;s=MyLevel.Alarm
|
|
814
|
+
// msg.conditionId is actual conditionObject that contains ns=6;s=MyLevel.Alarm/0:EventId current/latest eventId will be read
|
|
815
|
+
// msg.comment will be used as comment in the acknowledge
|
|
816
|
+
let eventId;
|
|
817
|
+
if (msg.conditionId) {
|
|
818
|
+
const dataValue = await node.session.read({ nodeId: msg.conditionId, attributeId: AttributeIds.Value });
|
|
819
|
+
|
|
820
|
+
eventId = dataValue.value.value;
|
|
821
|
+
verbose_log(chalk.yellow("Acknowledge (alarm object == topic): ") + chalk.cyan(msg.topic) +
|
|
822
|
+
chalk.yellow(" conditionObject (nodeId of eventId): ") + chalk.cyan(msg.conditionId) +
|
|
823
|
+
chalk.yellow(" value of eventId: 0x") + chalk.cyan(eventId.toString("hex")) +
|
|
824
|
+
chalk.yellow(" comment: ") + chalk.cyan(msg.comment));
|
|
825
|
+
}
|
|
826
|
+
// If actual eventId provided use it
|
|
827
|
+
if (msg.eventId) {
|
|
828
|
+
eventId = msg.eventId;
|
|
829
|
+
}
|
|
830
|
+
if (eventId) {
|
|
831
|
+
try {
|
|
832
|
+
const ackedState = await node.session.read({ nodeId: msg.topic + "/0:AckedState/0:Id", attributeId: AttributeIds.Value });
|
|
833
|
+
node.debug(chalk.yellow("EVENT ACKED STATE: ") + chalk.cyan(ackedState));
|
|
834
|
+
if (ackedState && ackedState.statusCode === opcua.StatusCodes.Good && ackedState.value.value === true) {
|
|
835
|
+
node.status({
|
|
836
|
+
fill: "yellow",
|
|
837
|
+
shape: "dot",
|
|
838
|
+
text: "Event: " + msg.topic + " already acknowledged"
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
const status = await node.session.acknowledgeCondition(msg.topic, eventId, msg.comment);
|
|
843
|
+
if (status !== opcua.StatusCodes.Good) {
|
|
844
|
+
node_error(node.name + "Error at acknowledge, status: " + status.toString());
|
|
845
|
+
set_node_error_status_to("error", status.toString());
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
node.status({
|
|
849
|
+
fill: "green",
|
|
850
|
+
shape: "dot",
|
|
851
|
+
text: "Event: " + msg.topic + " acknowledged"
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
node_error(node.name + "Error at acknowledge: " + msg.topic + " eventId: " + eventId + " error: " + err);
|
|
858
|
+
set_node_error_status_to("error", err);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
node_error(node.name + " error at acknowledge, no eventId, possible wrong msg.conditionId " + msg.conditionId);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function read_file(msg) {
|
|
867
|
+
verbose_log("Read file, nodeId: " + msg.topic.toString());
|
|
868
|
+
let file_node = opcua.coerceNodeId(msg.topic);
|
|
869
|
+
if (node.session) {
|
|
870
|
+
try {
|
|
871
|
+
const clientFile = new fileTransfer.ClientFile(node.session, file_node);
|
|
872
|
+
fileTransfer.ClientFile.useGlobalMethod = true;
|
|
873
|
+
// Given that the file is opened in ReadMode Only
|
|
874
|
+
await clientFile.open(fileTransfer.OpenFileMode.Read);
|
|
875
|
+
|
|
876
|
+
// Read file size
|
|
877
|
+
try {
|
|
878
|
+
const size = await clientFile.size(); // This should read size from the file itself
|
|
879
|
+
let buf = await clientFile.read(size);
|
|
880
|
+
// node-opcua-file-transfer takes care of the whole file reading from v2.94.0
|
|
881
|
+
await clientFile.close();
|
|
882
|
+
msg.payload = buf;
|
|
883
|
+
// Debug purpose, show content
|
|
884
|
+
verbose_log("File content: " + buf.toString());
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
msg.payload = "";
|
|
888
|
+
node_error(node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err);
|
|
889
|
+
set_node_error_status_to("error", "Cannot read file!");
|
|
890
|
+
node.send([null, { error: node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]);
|
|
891
|
+
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
node.send([msg, null]);
|
|
895
|
+
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
node_error(node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err);
|
|
899
|
+
set_node_error_status_to("error", err.toString());
|
|
900
|
+
node.send([null, { error: node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
901
|
+
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
verbose_warn("No open session to read file!");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function write_file(msg) {
|
|
910
|
+
verbose_log("Write file, nodeId: " + msg.topic.toString());
|
|
911
|
+
let file_node = opcua.coerceNodeId(msg.topic);
|
|
912
|
+
if (node.session) {
|
|
913
|
+
try {
|
|
914
|
+
let buf;
|
|
915
|
+
if (msg.payload && msg.payload.length > 0) {
|
|
916
|
+
buf = msg.payload;
|
|
917
|
+
}
|
|
918
|
+
if (msg.fileName) {
|
|
919
|
+
verbose_log("Uploading file: " + msg.fileName);
|
|
920
|
+
buf = fs.readFileSync(msg.fileName);
|
|
921
|
+
}
|
|
922
|
+
const clientFile = new fileTransfer.ClientFile(node.session, file_node);
|
|
923
|
+
clientFile.useGlobalMethod = true;
|
|
924
|
+
// Given that the file is opened in WriteMode
|
|
925
|
+
await clientFile.open(fileTransfer.OpenFileMode.Write);
|
|
926
|
+
verbose_log("Local file content: " + buf.toString());
|
|
927
|
+
verbose_log("Writing file to server...");
|
|
928
|
+
await clientFile.write(buf);
|
|
929
|
+
await clientFile.close();
|
|
930
|
+
verbose_log("Write done!");
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
node.error(chalk.red("Cannot write file, error: " + err.message));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
verbose_warn("No open session to write file!");
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function method_action_input(msg) {
|
|
942
|
+
verbose_log("Calling method: " + JSON.stringify(msg));
|
|
943
|
+
if (node.session) {
|
|
944
|
+
try {
|
|
945
|
+
let status = await callMethod(msg);
|
|
946
|
+
if (status === opcua.StatusCodes.Good) {
|
|
947
|
+
node.status({
|
|
948
|
+
fill: "green",
|
|
949
|
+
shape: "dot",
|
|
950
|
+
text: "Method executed"
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
node.error("Failed, method result: ", status.description);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
node.error(chalk.red("Cannot call method, error: " + err.message));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
verbose_warn("No open session to call method!");
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function callMethod(msg) {
|
|
967
|
+
if (msg.methodId && msg.inputArguments) {
|
|
968
|
+
verbose_log("Calling method: " + JSON.stringify(msg.methodId));
|
|
969
|
+
verbose_log("InputArguments: " + JSON.stringify(msg.inputArguments));
|
|
970
|
+
if (msg.outputArguments) {
|
|
971
|
+
verbose_log("OutputArguments: " + JSON.stringify(msg.outputArguments));
|
|
972
|
+
}
|
|
973
|
+
let args=[];
|
|
974
|
+
let i = 0;
|
|
975
|
+
let tmp;
|
|
976
|
+
set_node_status_to("building method arguments");
|
|
977
|
+
try {
|
|
978
|
+
while (i < msg.inputArguments.length) {
|
|
979
|
+
tmp = msg.inputArguments[i];
|
|
980
|
+
if (tmp.dataType === "NodeId") {
|
|
981
|
+
tmp.value = opcua.coerceNodeId(tmp.value);
|
|
982
|
+
}
|
|
983
|
+
if (tmp.dataType === "ExtensionObject") {
|
|
984
|
+
let extensionobject = null;
|
|
985
|
+
// tmp = {dataType: "ExtensionObject", typeid: tmp.typeid, value: tmp.value};
|
|
986
|
+
if (tmp.typeid) {
|
|
987
|
+
extensionobject = await node.session.constructExtensionObject(opcua.coerceNodeId(tmp.typeid), tmp.value); // TODO make while loop to enable await
|
|
988
|
+
tmp.value = extensionobject;
|
|
989
|
+
}
|
|
990
|
+
// verbose_log("ExtensionObject: " + extensionobject);
|
|
991
|
+
// Object.assign(extensionobject, tmp.value);
|
|
992
|
+
/*
|
|
993
|
+
verbose_log(chalk.green("ExtensionObject value: ") + JSON.stringify(tmp.value));
|
|
994
|
+
tmp.value = new opcua.Variant({
|
|
995
|
+
dataType: opcua.DataType.ExtensionObject,
|
|
996
|
+
value: tmp.value // JSON.stringify(tmp.value) // extensionobject
|
|
997
|
+
});
|
|
998
|
+
*/
|
|
999
|
+
verbose_log(chalk.green("ExtensionObject: ") + JSON.stringify(tmp));
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
if (tmp.valueRank && tmp.valueRank >= 1) {
|
|
1003
|
+
tmp = {dataType: tmp.dataType, valueRank: tmp.valueRank, arrayDimensions: 1, value: tmp.value};
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
tmp = {dataType: tmp.dataType, value: tmp.value};
|
|
1007
|
+
}
|
|
1008
|
+
verbose_log("Basic type: " + JSON.stringify(tmp));
|
|
1009
|
+
}
|
|
1010
|
+
args.push(tmp);
|
|
1011
|
+
i++;
|
|
1012
|
+
}
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
console.log(chalk.red("Error: "), err);
|
|
1015
|
+
let msg = {};
|
|
1016
|
+
msg.error = {};
|
|
1017
|
+
msg.error.message = "Invalid NodeId: " + err;
|
|
1018
|
+
msg.error.source = this;
|
|
1019
|
+
node.error("Invalid argument: ", tmp);
|
|
1020
|
+
return opcua.StatusCodes.BadNodeIdUnknown;
|
|
1021
|
+
}
|
|
1022
|
+
// verbose_log("Updated InputArguments: " + stringify(msg.inputArguments));
|
|
1023
|
+
verbose_log("Updated InputArguments: " + JSON.stringify(args));
|
|
1024
|
+
let callMethodRequest;
|
|
1025
|
+
let diagInfo;
|
|
1026
|
+
try {
|
|
1027
|
+
callMethodRequest = new opcua.CallMethodRequest({
|
|
1028
|
+
objectId: opcua.coerceNodeId(msg.objectId),
|
|
1029
|
+
methodId: opcua.coerceNodeId(msg.methodId),
|
|
1030
|
+
inputArgumentDiagnosticInfos: diagInfo,
|
|
1031
|
+
inputArguments: args,
|
|
1032
|
+
// inputArguments: msg.inputArguments,
|
|
1033
|
+
outputArguments: msg.outputArguments
|
|
1034
|
+
});
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
set_node_status_to("call method error");
|
|
1037
|
+
node.error("Build method request failed, error: " + err.message);
|
|
1038
|
+
}
|
|
1039
|
+
verbose_log("Call request: " + callMethodRequest.toString());
|
|
1040
|
+
verbose_log("Calling: " + callMethodRequest);
|
|
1041
|
+
try {
|
|
1042
|
+
set_node_status_to("call method");
|
|
1043
|
+
const result = await node.session.call(callMethodRequest);
|
|
1044
|
+
if (diagInfo) {
|
|
1045
|
+
verbose_log("Diagn. info: " + stringify(diagInfo));
|
|
1046
|
+
}
|
|
1047
|
+
if (msg.outputArguments) {
|
|
1048
|
+
verbose_log("Output args: " + stringify(msg.outputArguments));
|
|
1049
|
+
}
|
|
1050
|
+
verbose_log("Results: " + stringify(result));
|
|
1051
|
+
msg.result = result;
|
|
1052
|
+
if (result && result.statusCode === opcua.StatusCodes.Good) {
|
|
1053
|
+
let i = 0;
|
|
1054
|
+
msg.output = result.outputArguments; // Original outputArguments
|
|
1055
|
+
msg.payload = []; // Store values back to array
|
|
1056
|
+
if (result.outputArguments.length == 1) {
|
|
1057
|
+
verbose_log("Value: " + result.outputArguments[i].value);
|
|
1058
|
+
msg.payload = result.outputArguments[0].value; // Return only if one output argument
|
|
1059
|
+
} else {
|
|
1060
|
+
while (result.outputArguments.length > i) {
|
|
1061
|
+
verbose_log("Value[" + i + "]:" + result.outputArguments[i].toString());
|
|
1062
|
+
msg.payload.push(result.outputArguments[i].value); // Just copy result value to payload[] array, actual value needed mostly
|
|
1063
|
+
i++;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
set_node_status_to("execute method error");
|
|
1068
|
+
node.error("Execute method result, error:" + result.statusCode.description);
|
|
1069
|
+
node.send([null, { error: "Execute method result, error:" + result.statusCode.description, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]);
|
|
1070
|
+
|
|
1071
|
+
return result.statusCode;
|
|
1072
|
+
}
|
|
1073
|
+
node.send([msg, null]);
|
|
1074
|
+
|
|
1075
|
+
return opcua.StatusCodes.Good;
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
set_node_status_to("execute method error");
|
|
1078
|
+
node.error("Method execution error: " + err.message);
|
|
1079
|
+
node.send([null, { error: "Method execution error: " + err.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1080
|
+
|
|
1081
|
+
return opcua.StatusCodes.BadMethodInvalid;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function connect_action_input(msg) {
|
|
1087
|
+
console.log("#1 ACTION Connect!");
|
|
1088
|
+
verbose_log("Connecting...");
|
|
1089
|
+
if (msg && msg.OpcUaEndpoint) {
|
|
1090
|
+
// Remove listeners if existing
|
|
1091
|
+
if (node.client) {
|
|
1092
|
+
verbose_log("Cleanup old listener events... before connecting to new client");
|
|
1093
|
+
verbose_log("All event names:" + node.client.eventNames());
|
|
1094
|
+
verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished"));
|
|
1095
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
1096
|
+
verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
|
|
1097
|
+
node.client.removeListener("backoff", backoff);
|
|
1098
|
+
verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
|
|
1099
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
1100
|
+
}
|
|
1101
|
+
opcuaEndpoint = {}; // Clear
|
|
1102
|
+
opcuaEndpoint = msg.OpcUaEndpoint; // Check all parameters!
|
|
1103
|
+
connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint?.securityPolicy]; // || opcua.SecurityPolicy.None;
|
|
1104
|
+
connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint?.securityMode]; // || opcua.MessageSecurityMode.None;
|
|
1105
|
+
verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode);
|
|
1106
|
+
if (opcuaEndpoint.login === true) {
|
|
1107
|
+
userIdentity = {
|
|
1108
|
+
userName: opcuaEndpoint?.user,
|
|
1109
|
+
password: opcuaEndpoint?.password,
|
|
1110
|
+
type: opcua.UserTokenType.UserName
|
|
1111
|
+
};
|
|
1112
|
+
verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity));
|
|
1113
|
+
}
|
|
1114
|
+
verbose_log("Using new endpoint:" + stringify(opcuaEndpoint));
|
|
1115
|
+
} else {
|
|
1116
|
+
verbose_log("Using endpoint:" + stringify(opcuaEndpoint));
|
|
1117
|
+
}
|
|
1118
|
+
console.log("#2 Create client");
|
|
1119
|
+
if (!node.client) {
|
|
1120
|
+
create_opcua_client(connect_opcua_client);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function disconnect_action_input(msg) {
|
|
1125
|
+
verbose_log("Closing session...");
|
|
1126
|
+
if (node.session) {
|
|
1127
|
+
node.session.close(function (err) {
|
|
1128
|
+
if (err) {
|
|
1129
|
+
node_error("Session close error: " + err);
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
verbose_log("Session closed!");
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
verbose_warn("No session to close!");
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
opcuaEndpoint = {}; // Clear
|
|
1141
|
+
opcuaEndpoint = msg.OpcUaEndpoint;
|
|
1142
|
+
// Now reconnect and use msg parameters
|
|
1143
|
+
subscription = null;
|
|
1144
|
+
monitoredItems.clear();
|
|
1145
|
+
verbose_log("Disconnecting...");
|
|
1146
|
+
if (node.client) {
|
|
1147
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
1148
|
+
verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
|
|
1149
|
+
node.client.removeListener("backoff", backoff);
|
|
1150
|
+
verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
|
|
1151
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
1152
|
+
node.client.disconnect(function () {
|
|
1153
|
+
verbose_log("Client disconnected!");
|
|
1154
|
+
node.client = null;
|
|
1155
|
+
set_node_status_to("disconnected");
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function register_action_input(msg) {
|
|
1161
|
+
verbose_log("register nodes : " + msg.payload.toString());
|
|
1162
|
+
// First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple
|
|
1163
|
+
// msg.topic not used, but cannot be empty
|
|
1164
|
+
// msg.payload == array of nodeIds to register
|
|
1165
|
+
if (msg.payload.length > 0) {
|
|
1166
|
+
const registeredNodes = await node.session.registerNodes(msg.payload);
|
|
1167
|
+
verbose_log("RegisteredNodes: " + registeredNodes.toString());
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
verbose_warn("No items to register in the payload! Check node:" + node.name);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function unregister_action_input(msg) {
|
|
1175
|
+
verbose_log("unregister nodes : " + msg.payload.toString());
|
|
1176
|
+
// First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple
|
|
1177
|
+
// msg.topic not used, but cannot be empty
|
|
1178
|
+
// msg.payload == array of nodeIds to register
|
|
1179
|
+
if (msg.payload.length > 0) {
|
|
1180
|
+
const unregisteredNodes = await node.session.registerNodes(msg.payload);
|
|
1181
|
+
verbose_log("UnregisteredNodes: " + unregisteredNodes.toString());
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
verbose_warn("No items to unregister in the payload! Check node:" + node.name);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function read_action_input(msg) {
|
|
1189
|
+
|
|
1190
|
+
verbose_log("reading");
|
|
1191
|
+
let item = "";
|
|
1192
|
+
let range = null;
|
|
1193
|
+
if (msg.topic) {
|
|
1194
|
+
let n = msg.topic.indexOf("datatype=");
|
|
1195
|
+
if (n > 0) {
|
|
1196
|
+
msg.datatype = msg.topic.substring(n + 9);
|
|
1197
|
+
item = msg.topic.substring(0, n - 1);
|
|
1198
|
+
msg.topic = item;
|
|
1199
|
+
verbose_log(stringify(msg));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
let br = "";
|
|
1203
|
+
// browsePath enhancement, item starts with br=/Objects/3:Simulation/3:Counter
|
|
1204
|
+
// console.log("ITEM: " + item + " topic: " + msg.topic);
|
|
1205
|
+
if (item.length > 0 && item.indexOf("br=") === 0) {
|
|
1206
|
+
verbose_log("Finding nodeId by browsePath: " + item.substring(3));
|
|
1207
|
+
br = item.substring(3);
|
|
1208
|
+
}
|
|
1209
|
+
if (msg.topic.length > 0 && msg.topic.startsWith("br=")) {
|
|
1210
|
+
verbose_log("Finding nodeId by browsePath: " + msg.topic.substring(3));
|
|
1211
|
+
br = msg.topic.substring(3);
|
|
1212
|
+
}
|
|
1213
|
+
if (br.length > 0) {
|
|
1214
|
+
const translate = [opcua.makeBrowsePath("RootFolder", br)];
|
|
1215
|
+
const results = await node.session.translateBrowsePath(translate);
|
|
1216
|
+
if (results && results[0].statusCode === opcua.StatusCodes.Good &&
|
|
1217
|
+
results[0].targets[0]?.targetId) {
|
|
1218
|
+
msg.topic = results[0].targets[0].targetId; // replace msg.topic with the nodeId that is get by using browsePath
|
|
1219
|
+
verbose_log("Found browsePath => nodeId: " + msg.topic);
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
node_error("Cannot translate browsePathToNodeId: " + br + "/" + translate + " error: " + results);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (item.length > 0) {
|
|
1227
|
+
items[0] = item;
|
|
1228
|
+
} else {
|
|
1229
|
+
items[0] = msg.topic;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Added support for indexRange, payload can be just one number as string "5" or "2:5"
|
|
1233
|
+
if (msg.payload?.range) {
|
|
1234
|
+
range = new opcua.NumericRange(msg.payload.range);
|
|
1235
|
+
}
|
|
1236
|
+
if (node.session) {
|
|
1237
|
+
// With Single Read using now read to get sourceTimeStamp and serverTimeStamp
|
|
1238
|
+
verbose_log("Reading value by nodeId: " + items[0]);
|
|
1239
|
+
try {
|
|
1240
|
+
node.session.read({
|
|
1241
|
+
nodeId: items[0],
|
|
1242
|
+
attributeId: 13,
|
|
1243
|
+
indexRange: range,
|
|
1244
|
+
timestampsToReturn: opcua.TimestampsToReturn.Both
|
|
1245
|
+
},
|
|
1246
|
+
function (err, dataValue, diagnostics) {
|
|
1247
|
+
if (err) {
|
|
1248
|
+
if (diagnostics) {
|
|
1249
|
+
verbose_log('diagnostics:' + diagnostics);
|
|
1250
|
+
}
|
|
1251
|
+
node_error(node.name + " error at active reading: " + err.message);
|
|
1252
|
+
set_node_error_status_to("error", err);
|
|
1253
|
+
// No actual error session created, this case cause connections to server
|
|
1254
|
+
// reset_opcua_client(connect_opcua_client);
|
|
1255
|
+
} else {
|
|
1256
|
+
set_node_status_to("active reading");
|
|
1257
|
+
verbose_log("Node : " + msg.topic);
|
|
1258
|
+
verbose_log(dataValue.toString());
|
|
1259
|
+
if (dataValue) {
|
|
1260
|
+
try {
|
|
1261
|
+
verbose_log("Value : " + dataValue.value.value);
|
|
1262
|
+
verbose_log("DataType: " + dataValue.value.dataType + " (" + DataType[dataValue.value.dataType] + ")");
|
|
1263
|
+
verbose_log("Message: " + msg.topic + " (" + msg.datatype + ")");
|
|
1264
|
+
if (dataValue.value.dataType === opcua.DataType.UInt16) {
|
|
1265
|
+
verbose_log("UInt16:" + dataValue.value.value + " -> Int32:" + opcuaBasics.toInt32(dataValue.value.value));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// clone payload via JSON stringify/parse only if datavalue is an ExtensionObject or an array of ExtensionObjects
|
|
1269
|
+
msg.payload = dataValue.value.dataType === opcua.DataType.ExtensionObject
|
|
1270
|
+
? JSON.parse(JSON.stringify((dataValue.value.value)))
|
|
1271
|
+
: dataValue.value.value;
|
|
1272
|
+
|
|
1273
|
+
msg.statusCode = dataValue.statusCode;
|
|
1274
|
+
msg.serverTimestamp = dataValue.serverTimestamp;
|
|
1275
|
+
msg.sourceTimestamp = dataValue.sourceTimestamp;
|
|
1276
|
+
|
|
1277
|
+
if (dataValue.statusCode && dataValue.statusCode.isGoodish() === false) {
|
|
1278
|
+
verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
|
|
1279
|
+
}
|
|
1280
|
+
node.send([msg, null]);
|
|
1281
|
+
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
if (dataValue != null) {
|
|
1284
|
+
node_error("Bad read: " + (dataValue.statusCode.toString(16)));
|
|
1285
|
+
node_error("Message:" + msg.topic + " dataType:" + msg.datatype);
|
|
1286
|
+
node_error("Data:" + stringify(dataValue));
|
|
1287
|
+
node.send([null, { error: "Bad read: " + (dataValue.statusCode.toString(16)), endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1288
|
+
} else {
|
|
1289
|
+
node_error(e.message);
|
|
1290
|
+
node.send([null, { error: e.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
node_error("Cannot read: " + error);
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
set_node_status_to("invalid session");
|
|
1303
|
+
node_error("Session is not active!")
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function readhistory_action_input(msg) {
|
|
1308
|
+
verbose_log("Read historical values from msg.topic = nodeId: " + msg.topic + " msg.aggregate (raw|min|ave|max|interpolative), aggregate: " + msg.aggregate);
|
|
1309
|
+
let start;
|
|
1310
|
+
let end = Date.now();
|
|
1311
|
+
if (msg.end) {
|
|
1312
|
+
verbose_log("msg.end : " + msg.end.toString());
|
|
1313
|
+
end = msg.end;
|
|
1314
|
+
}
|
|
1315
|
+
if (!msg.start) {
|
|
1316
|
+
start = end - (1 * 60 * 60 * 1000); // read last 1 hour of history
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
start = msg.start;
|
|
1320
|
+
verbose_log("msg.start: " + msg.start.toString());
|
|
1321
|
+
}
|
|
1322
|
+
verbose_log("Start time, msg.start or default start 1h ago, start: " + new Date(start));
|
|
1323
|
+
verbose_log("End time, msg.end or default to now, end: " + new Date(end));
|
|
1324
|
+
// For aggregates
|
|
1325
|
+
let processingInterval = end - start; // Whole range 10 * 1000; // 10s interval
|
|
1326
|
+
if (node.session) {
|
|
1327
|
+
if (msg.aggregate && msg.aggregate === "raw") {
|
|
1328
|
+
let numValues = 1000;
|
|
1329
|
+
if (msg.numValuesPerNode) {
|
|
1330
|
+
numValues = parseInt(msg.numValuesPerNode);
|
|
1331
|
+
}
|
|
1332
|
+
let returnBounds = false;
|
|
1333
|
+
if (msg.returnBounds) {
|
|
1334
|
+
returnBounds = msg.returnBounds;
|
|
1335
|
+
}
|
|
1336
|
+
// Old one: ReadRawModifiedDetails
|
|
1337
|
+
// No constructor: ExtraReadHistoryValueParameters
|
|
1338
|
+
let historyReadDetails = new Object({
|
|
1339
|
+
isReadModified: false, // Fixed, any need to modify?
|
|
1340
|
+
numValuesPerNode: numValues,
|
|
1341
|
+
returnBounds: returnBounds,
|
|
1342
|
+
timestampsToReturn: opcua.TimestampsToReturn.Both // Fixed, return always both
|
|
1343
|
+
});
|
|
1344
|
+
verbose_log("NodeId: " + msg.topic + " from: " + new Date(start) + " to: " + new Date(end) + " options: " + JSON.stringify(historyReadDetails));
|
|
1345
|
+
let historicalReadResult = await node.session.readHistoryValue({ nodeId: msg.topic }, new Date(start), new Date(end), historyReadDetails);
|
|
1346
|
+
msg.payload = historicalReadResult;
|
|
1347
|
+
node.send([msg, null]);
|
|
1348
|
+
|
|
1349
|
+
}
|
|
1350
|
+
if (msg.aggregate && msg.aggregate === "max") {
|
|
1351
|
+
let resultMax = await node.session.readAggregateValue(
|
|
1352
|
+
{ nodeId: msg.topic },
|
|
1353
|
+
new Date(start),
|
|
1354
|
+
new Date(end),
|
|
1355
|
+
opcua.AggregateFunction.Maximum,
|
|
1356
|
+
processingInterval);
|
|
1357
|
+
msg.payload = resultMax;
|
|
1358
|
+
node.send([msg, null, null]);
|
|
1359
|
+
|
|
1360
|
+
if (resultMax.statusCode === opcua.StatusCodes.Good) {
|
|
1361
|
+
verbose_log(chalk.green("History max: ") + chalk.cyan(resultMax.historyData.dataValues[0].value.value));
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (msg.aggregate && msg.aggregate === "min") {
|
|
1365
|
+
let resultMin = await node.session.readAggregateValue(
|
|
1366
|
+
{ nodeId: msg.topic },
|
|
1367
|
+
new Date(start),
|
|
1368
|
+
new Date(end),
|
|
1369
|
+
opcua.AggregateFunction.Minimum,
|
|
1370
|
+
processingInterval);
|
|
1371
|
+
msg.payload = resultMin;
|
|
1372
|
+
node.send([msg, null, null]);
|
|
1373
|
+
|
|
1374
|
+
if (resultMin.statusCode === opcua.StatusCodes.Good) {
|
|
1375
|
+
verbose_log(chalk.green("History min: ") + chalk.cyan(resultMin.historyData.dataValues[0].value.value));
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (msg.aggregate && msg.aggregate === "ave") {
|
|
1379
|
+
let resultAve = await node.session.readAggregateValue(
|
|
1380
|
+
{ nodeId: msg.topic },
|
|
1381
|
+
new Date(start),
|
|
1382
|
+
new Date(end),
|
|
1383
|
+
opcua.AggregateFunction.Average,
|
|
1384
|
+
processingInterval);
|
|
1385
|
+
msg.payload = resultAve;
|
|
1386
|
+
node.send([msg, null, null]);
|
|
1387
|
+
|
|
1388
|
+
if (resultAve.statusCode === opcua.StatusCodes.Good) {
|
|
1389
|
+
verbose_log(chalk.green("History ave: ") + chalk.cyan(resultAve.historyData.dataValues[0].value.value));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (msg.aggregate && msg.aggregate === "interpolative") {
|
|
1393
|
+
let resultInter = await node.session.readAggregateValue(
|
|
1394
|
+
{ nodeId: msg.topic },
|
|
1395
|
+
new Date(start),
|
|
1396
|
+
new Date(end),
|
|
1397
|
+
opcua.AggregateFunction.Interpolative,
|
|
1398
|
+
processingInterval);
|
|
1399
|
+
msg.payload = resultInter;
|
|
1400
|
+
node.send([msg, null, null]);
|
|
1401
|
+
|
|
1402
|
+
if (resultInter.statusCode === opcua.StatusCodes.Good) {
|
|
1403
|
+
verbose_log(chalk.green("History interpolative: ") + chalk.cyan(resultInter.historyData.dataValues[0].value.value));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function readmultiple_action_input(msg) {
|
|
1411
|
+
|
|
1412
|
+
verbose_log("read multiple...");
|
|
1413
|
+
let item = "";
|
|
1414
|
+
//
|
|
1415
|
+
if (msg.topic) {
|
|
1416
|
+
let n = msg.topic.indexOf("datatype=");
|
|
1417
|
+
if (n > 0) {
|
|
1418
|
+
msg.datatype = msg.topic.substring(n + 9);
|
|
1419
|
+
item = msg.topic.substring(0, n - 1);
|
|
1420
|
+
msg.topic = item;
|
|
1421
|
+
verbose_log(stringify(msg));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Store nodeId and browseName to read multipleItems array
|
|
1426
|
+
if (msg.topic !== "readmultiple" && msg.topic !== "clearitems") {
|
|
1427
|
+
if (item.length > 0) {
|
|
1428
|
+
multipleItems.push({ nodeId: item, browseName: msg.browseName, attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both });
|
|
1429
|
+
} else {
|
|
1430
|
+
// msg.topic
|
|
1431
|
+
multipleItems.push({ nodeId: msg.topic, browseName: msg.browseName, attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both }); // support for multiple item reading
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (msg.topic === "clearitems") {
|
|
1436
|
+
verbose_log("clear items...");
|
|
1437
|
+
multipleItems = [];
|
|
1438
|
+
set_node_status_to("clear items");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (msg.topic !== "readmultiple") {
|
|
1443
|
+
set_node_status_to("nodeId stored");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
// Read multiple, payload contains all nodeIds that will be read
|
|
1447
|
+
if (node.session && msg.topic === "readmultiple" && Array.isArray(msg.payload)) {
|
|
1448
|
+
multipleItems = []; // Clear as payload contains always nodeIds that we want to read
|
|
1449
|
+
let i = 0;
|
|
1450
|
+
while (i < msg.payload.length) {
|
|
1451
|
+
multipleItems.push({ nodeId: msg.payload[i], attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both });
|
|
1452
|
+
i = i + 1;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (node.session && msg.topic === "readmultiple") {
|
|
1457
|
+
// node.session.read({timestampsToReturn: TimestampsToReturn.Both, nodesToRead: multipleItems}, function (err, dataValues, diagnostics) {
|
|
1458
|
+
verbose_log("Reading items: " + stringify(multipleItems));
|
|
1459
|
+
if (multipleItems.length === 0) {
|
|
1460
|
+
node_error(node.name + " no items to read");
|
|
1461
|
+
set_node_status_to("no items to read");
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
node.session.read(multipleItems,
|
|
1465
|
+
function (err, dataValues, diagnostics) {
|
|
1466
|
+
if (err) {
|
|
1467
|
+
if (diagnostics) {
|
|
1468
|
+
verbose_log('diagnostics:' + diagnostics);
|
|
1469
|
+
}
|
|
1470
|
+
node_error(node.name + " error at active reading: " + err.message);
|
|
1471
|
+
set_node_error_status_to("error", err);
|
|
1472
|
+
// No actual error session existing, this case cause connections to server
|
|
1473
|
+
// reset_opcua_client(connect_opcua_client);
|
|
1474
|
+
}
|
|
1475
|
+
else {
|
|
1476
|
+
set_node_status_to("active multiple reading");
|
|
1477
|
+
|
|
1478
|
+
if (msg.payload === "ALL") {
|
|
1479
|
+
msg.payload = dataValues;
|
|
1480
|
+
msg.items = multipleItems;
|
|
1481
|
+
node.send([msg, null, null]);// node.send([{ "topic": "ALL", "payload": dataValues, "items": multipleItems }, null, null]);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
for (let i = 0; i < dataValues.length; i++) {
|
|
1486
|
+
let dataValue = dataValues[i];
|
|
1487
|
+
verbose_log("Node : " + msg.topic);
|
|
1488
|
+
verbose_log(dataValue.toString());
|
|
1489
|
+
if (dataValue) {
|
|
1490
|
+
try {
|
|
1491
|
+
verbose_log("Value : " + dataValue.value.value);
|
|
1492
|
+
verbose_log("DataType: " + dataValue.value.dataType + " (" + DataType[dataValue.value.dataType] + ")");
|
|
1493
|
+
if (dataValue.value.dataType === opcua.DataType.UInt16) {
|
|
1494
|
+
verbose_log("UInt16:" + dataValue.value.value + " -> Int32:" + opcuaBasics.toInt32(dataValue.value.value));
|
|
1495
|
+
}
|
|
1496
|
+
if (dataValue.statusCode && dataValue.statusCode.isGoodish() === false) {
|
|
1497
|
+
verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
|
|
1498
|
+
}
|
|
1499
|
+
let serverTs = dataValue.serverTimestamp;
|
|
1500
|
+
let sourceTs = dataValue.sourceTimestamp;
|
|
1501
|
+
if (serverTs === null) {
|
|
1502
|
+
serverTs = new Date();
|
|
1503
|
+
}
|
|
1504
|
+
if (sourceTs === null) {
|
|
1505
|
+
sourceTs = new Date();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
let value = dataValue.value.dataType === opcua.DataType.ExtensionObject
|
|
1509
|
+
? JSON.parse(JSON.stringify(dataValue.value.value))
|
|
1510
|
+
: dataValue.value.value;
|
|
1511
|
+
|
|
1512
|
+
// Use nodeId in topic, arrays are same length
|
|
1513
|
+
// Output pin 1 for each value by value
|
|
1514
|
+
// Clone msg per item: Node-RED passes the first node.send()
|
|
1515
|
+
// by reference, so mutating the shared msg in this loop
|
|
1516
|
+
// corrupts the per-item topic of already-sent messages.
|
|
1517
|
+
let outMsg = RED.util.cloneMessage(msg);
|
|
1518
|
+
outMsg.topic = multipleItems[i].nodeId.toString();
|
|
1519
|
+
outMsg.payload = value;
|
|
1520
|
+
outMsg.statusCode = dataValue.statusCode;
|
|
1521
|
+
outMsg.serverTimestamp = serverTs;
|
|
1522
|
+
outMsg.sourceTimestamp = sourceTs;
|
|
1523
|
+
node.send([outMsg, null, null]);
|
|
1524
|
+
} catch (e) {
|
|
1525
|
+
if (dataValue != null) {
|
|
1526
|
+
node_error("Bad read, statusCode: " + (dataValue.statusCode.toString(16)));
|
|
1527
|
+
node_error("Data:" + stringify(dataValue));
|
|
1528
|
+
} else {
|
|
1529
|
+
node_error(e.message);
|
|
1530
|
+
// Output pin 2 for errors
|
|
1531
|
+
// verbose_log("Output pin2, error: " + e.message);
|
|
1532
|
+
node.send([msg, { error: e.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// Send all values in one msg to output 3
|
|
1539
|
+
// verbose_log("Output pin3, topic: " + multipleItems + " payload: " + dataValues);
|
|
1540
|
+
msg.topic = multipleItems;
|
|
1541
|
+
msg.payload = dataValues;
|
|
1542
|
+
node.send([null, null, msg]); // node.send([null, null, {topic: multipleItems, payload: dataValues}]);
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
} else {
|
|
1546
|
+
set_node_status_to("invalid session");
|
|
1547
|
+
node_error("Session is not active!")
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function info_action_input(msg) {
|
|
1552
|
+
verbose_log("meta-data reading");
|
|
1553
|
+
let item = "";
|
|
1554
|
+
if (msg.topic) {
|
|
1555
|
+
let n = msg.topic.indexOf("datatype=");
|
|
1556
|
+
|
|
1557
|
+
if (n > 0) {
|
|
1558
|
+
msg.datatype = msg.topic.substring(n + 9);
|
|
1559
|
+
item = msg.topic.substring(0, n - 1);
|
|
1560
|
+
msg.topic = item;
|
|
1561
|
+
verbose_log(stringify(msg));
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (item.length > 0) {
|
|
1566
|
+
items[0] = item;
|
|
1567
|
+
} else {
|
|
1568
|
+
items[0] = msg.topic;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (node.session) {
|
|
1572
|
+
// TODO this could loop through all items
|
|
1573
|
+
node.session.readAllAttributes(opcua.coerceNodeId(items[0]), function (err, result) {
|
|
1574
|
+
if (!err) {
|
|
1575
|
+
let newMsg = Object.assign(msg, result);
|
|
1576
|
+
node.send([newMsg, null, null]);
|
|
1577
|
+
}
|
|
1578
|
+
else {
|
|
1579
|
+
set_node_status_to("error");
|
|
1580
|
+
node_error("Cannot read attributes from nodeId: " + items[0])
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
} else {
|
|
1584
|
+
set_node_status_to("invalid session");
|
|
1585
|
+
node_error("Session is not active!")
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
async function build_extension_object_action_input(msg) {
|
|
1590
|
+
verbose_log("Construct ExtensionObject from " + JSON.stringify(msg));
|
|
1591
|
+
let item = "";
|
|
1592
|
+
if (msg.topic) {
|
|
1593
|
+
let n = msg.topic.indexOf("datatype=");
|
|
1594
|
+
|
|
1595
|
+
if (n > 0) {
|
|
1596
|
+
msg.datatype = msg.topic.substring(n + 9);
|
|
1597
|
+
item = msg.topic.substring(0, n - 1);
|
|
1598
|
+
msg.topic = item;
|
|
1599
|
+
verbose_log(stringify(msg));
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (item.length > 0) {
|
|
1604
|
+
items[0] = item;
|
|
1605
|
+
} else {
|
|
1606
|
+
items[0] = msg.topic;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (node.session) {
|
|
1610
|
+
try {
|
|
1611
|
+
const ExtensionNodeId = opcua.coerceNodeId(items[0]);
|
|
1612
|
+
verbose_log("ExtensionNodeId: " + ExtensionNodeId);
|
|
1613
|
+
const ExtensionTypeDefinition = await node.session.read({ nodeId: ExtensionNodeId, attributeId: opcua.AttributeIds.DataTypeDefinition });
|
|
1614
|
+
if (ExtensionTypeDefinition.statusCode != opcua.StatusCodes.Good) {
|
|
1615
|
+
node_error("Failed to find extension type for nodeId: " + ExtensionNodeId + " error: " + ExtensionTypeDefinition.statusCode.description);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
verbose_log("ExtensionType: " + JSON.stringify(ExtensionTypeDefinition));
|
|
1620
|
+
}
|
|
1621
|
+
let newmsg = {};
|
|
1622
|
+
const ExtensionData = await node.session.constructExtensionObject(ExtensionNodeId, {});
|
|
1623
|
+
if (ExtensionData) {
|
|
1624
|
+
verbose_log("ExtensionData: " + ExtensionData.toString());
|
|
1625
|
+
}
|
|
1626
|
+
// Simplified
|
|
1627
|
+
newmsg.topic = msg.topic;
|
|
1628
|
+
newmsg.payload = JSON.parse(JSON.stringify(ExtensionData)); // JSON.stringify(ExtensionData); // New value with default values
|
|
1629
|
+
verbose_log("Extension Object msg: " + stringify(newmsg))
|
|
1630
|
+
node.send([newmsg, null, null]);
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
if (err) {
|
|
1634
|
+
node_error("Failed to build ExtensionObject, error: " + err);
|
|
1635
|
+
set_node_status_to("extensionObject error");
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
set_node_status_to("invalid session");
|
|
1640
|
+
node_error("Session is not active!")
|
|
1641
|
+
|
|
1642
|
+
node.send([null, { error: "Session is not active!", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1643
|
+
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
async function write_action_input(msg) {
|
|
1648
|
+
if (msg?.topic && msg.topic.indexOf("ns=") != 0) return; // NOT an item
|
|
1649
|
+
|
|
1650
|
+
// Topic value: ns=2;s=1:PST-007-Alarm-Level@Training?SETPOINT
|
|
1651
|
+
let dIndex = msg.topic.indexOf("datatype=");
|
|
1652
|
+
let range = null;
|
|
1653
|
+
|
|
1654
|
+
if (msg.datatype == null && dIndex > 0) {
|
|
1655
|
+
msg.datatype = msg.topic.substring(dIndex + 9);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
let nodeid;
|
|
1659
|
+
verbose_log(opcua.makeBrowsePath(msg.topic, "."));
|
|
1660
|
+
// TODO ns=10 TEST 2 digits namespace
|
|
1661
|
+
if (dIndex > 0) {
|
|
1662
|
+
nodeid = opcua.coerceNodeId(msg.topic.substring(0, dIndex - 1));
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
nodeid = opcua.coerceNodeId(msg.topic);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Less output
|
|
1669
|
+
// verbose_log("namespace=" + ns);
|
|
1670
|
+
// verbose_log("string=" + s);
|
|
1671
|
+
// verbose_log("NodeId= " + nodeid.toString + " type=" + msg.datatype);
|
|
1672
|
+
|
|
1673
|
+
let opcuaDataValue = msg.datatype && msg.datatype.indexOf("ExtensionObject") >= 0 && node.session
|
|
1674
|
+
? await build_new_extensionObject_dataValue(msg.datatype, msg.topic, msg.payload, node.session)
|
|
1675
|
+
: opcuaBasics.build_new_dataValue(msg.datatype, msg.payload);
|
|
1676
|
+
|
|
1677
|
+
async function build_new_extensionObject_dataValue(datatype, topic, payload, session) {
|
|
1678
|
+
let defaultExtensionObject = null;
|
|
1679
|
+
|
|
1680
|
+
if (topic.indexOf("typeId=") > 0) {
|
|
1681
|
+
let typeId = topic.substring(topic.indexOf("typeId=") + 7);
|
|
1682
|
+
verbose_log("ExtensionObject TypeId= " + typeId);
|
|
1683
|
+
defaultExtensionObject = await session.constructExtensionObject(opcua.coerceNodeId(typeId), {}); // Create first with default values
|
|
1684
|
+
verbose_log("ExtensionObject=" + stringify(defaultExtensionObject));
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
let nValue = null;
|
|
1688
|
+
|
|
1689
|
+
if (datatype.indexOf("Array") > 0) {
|
|
1690
|
+
// datatype is array of extension object
|
|
1691
|
+
payload.value.forEach(function (extensionObject, index) {
|
|
1692
|
+
// deep clone default extension object
|
|
1693
|
+
let duplicatedDefaultExtensionObject = cloneDeep(defaultExtensionObject);
|
|
1694
|
+
payload.value[index] = Object.assign(duplicatedDefaultExtensionObject, extensionObject);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
nValue = {
|
|
1698
|
+
dataType: opcua.DataType.ExtensionObject,
|
|
1699
|
+
value: payload.value,
|
|
1700
|
+
arrayType: opcua.VariantArrayType.Array
|
|
1701
|
+
};
|
|
1702
|
+
} else {
|
|
1703
|
+
// datatype is extension object
|
|
1704
|
+
verbose_log("payload: " + stringify(payload));
|
|
1705
|
+
let extensionObject;
|
|
1706
|
+
if (!defaultExtensionObject) {
|
|
1707
|
+
extensionObject = payload; // use payload directly, payload must be ExtensionObject ready/like not a JSON object
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
extensionObject = Object.assign(defaultExtensionObject, payload); // MERGE payload over default values
|
|
1711
|
+
}
|
|
1712
|
+
/* TEST
|
|
1713
|
+
nValue = {
|
|
1714
|
+
dataType: opcua.DataType.ExtensionObject,
|
|
1715
|
+
value: extensionObject,
|
|
1716
|
+
arrayType: opcua.VariantArrayType.Array // Quick FIX
|
|
1717
|
+
};
|
|
1718
|
+
console.log("New nValue: " + JSON.stringify(nValue));
|
|
1719
|
+
*/
|
|
1720
|
+
if (extensionObject && extensionObject.length !== undefined) {
|
|
1721
|
+
nValue = {
|
|
1722
|
+
dataType: opcua.DataType.ExtensionObject,
|
|
1723
|
+
value: extensionObject,
|
|
1724
|
+
arrayType: opcua.VariantArrayType.Array // Quick FIX
|
|
1725
|
+
};
|
|
1726
|
+
// console.log("New nValue: " + JSON.stringify(nValue));
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
nValue = {
|
|
1730
|
+
dataType: opcua.DataType.ExtensionObject,
|
|
1731
|
+
value: extensionObject
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
return nValue
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// TODO Fix object array according range
|
|
1740
|
+
// Added support for indexRange, payload can be just one number as string "5" or "2:5"
|
|
1741
|
+
|
|
1742
|
+
// Helper for node-red server write
|
|
1743
|
+
if (msg.range) {
|
|
1744
|
+
verbose_log(chalk.red("Range: " + msg.range));
|
|
1745
|
+
range = new opcua.NumericRange(msg.range);
|
|
1746
|
+
verbose_log(chalk.red("Range: " + JSON.stringify(range) + " values: " + JSON.stringify(opcuaDataValue)));
|
|
1747
|
+
// TODO write to node-red server still work to do
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
let nodeToWrite;
|
|
1751
|
+
if (node.session && !node.session.isReconnecting && node.session.isChannelValid()) {
|
|
1752
|
+
if (range) {
|
|
1753
|
+
nodeToWrite = {
|
|
1754
|
+
nodeId: nodeid.toString(),
|
|
1755
|
+
attributeId: opcua.AttributeIds.Value,
|
|
1756
|
+
indexRange: range,
|
|
1757
|
+
value: new opcua.DataValue({ value: new opcua.Variant(opcuaDataValue) })
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
// console.log("Writing datatype: " + opcuaDataValue.dataType + " value: " + opcuaDataValue.value);
|
|
1762
|
+
// Added overflow/underflow check Int32, Int16, UInt64, UInt32, UInt16, Byte, SByte
|
|
1763
|
+
if (opcuaDataValue.dataType === opcua.DataType.Int32) {
|
|
1764
|
+
if (opcuaDataValue.value > 2147483647) {
|
|
1765
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1766
|
+
set_node_error_status_to("error", "overflow");
|
|
1767
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int32");
|
|
1768
|
+
opcuaDataValue.value = 2147483647;
|
|
1769
|
+
}
|
|
1770
|
+
if (opcuaDataValue.value < -2147483648) {
|
|
1771
|
+
set_node_error_status_to("error", "underflow");
|
|
1772
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int32");
|
|
1773
|
+
opcuaDataValue.value = -2147483648;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (opcuaDataValue.dataType === opcua.DataType.Int16) {
|
|
1777
|
+
if (opcuaDataValue.value > 32767) {
|
|
1778
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1779
|
+
set_node_error_status_to("error", "overflow");
|
|
1780
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int16");
|
|
1781
|
+
opcuaDataValue.value = 32767;
|
|
1782
|
+
}
|
|
1783
|
+
if (opcuaDataValue.value < -32768) {
|
|
1784
|
+
set_node_error_status_to("error", "underflow");
|
|
1785
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int16");
|
|
1786
|
+
opcuaDataValue.value = -32768;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (opcuaDataValue.dataType === opcua.DataType.UInt64) {
|
|
1790
|
+
if (Array.isArray(opcuaDataValue.value)){ //64 bit ints are sometimes passed as arrays of 32 bit ints
|
|
1791
|
+
if(!isNaN(opcuaDataValue.value[0]) && !isNaN(opcuaDataValue.value[1])){
|
|
1792
|
+
opcuaDataValue.value = opcuaDataValue.value[0] * (2**32) + opcuaDataValue.value[1];
|
|
1793
|
+
} else if(!isNaN(opcuaDataValue.value[0])){
|
|
1794
|
+
opcuaDataValue.value = opcuaDataValue.value[0];
|
|
1795
|
+
} else if(!isNaN(opcuaDataValue.value[1])){
|
|
1796
|
+
opcuaDataValue.value = opcuaDataValue.value[1];
|
|
1797
|
+
} else {
|
|
1798
|
+
node.warn("Input to UInt64 write is array of null values")
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (opcuaDataValue.value > 2**64 - 1) {
|
|
1802
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1803
|
+
set_node_error_status_to("error", "overflow");
|
|
1804
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int32");
|
|
1805
|
+
opcuaDataValue.value = 2**64 - 1;
|
|
1806
|
+
}
|
|
1807
|
+
if (opcuaDataValue.value < 0) {
|
|
1808
|
+
set_node_error_status_to("error", "underflow");
|
|
1809
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int32");
|
|
1810
|
+
opcuaDataValue.value = 0;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (opcuaDataValue.dataType === opcua.DataType.UInt32) {
|
|
1814
|
+
if (opcuaDataValue.value > 4294967295) {
|
|
1815
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1816
|
+
set_node_error_status_to("error", "overflow");
|
|
1817
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max UInt32");
|
|
1818
|
+
opcuaDataValue.value = 4294967295;
|
|
1819
|
+
}
|
|
1820
|
+
if (opcuaDataValue.value < 0) {
|
|
1821
|
+
set_node_error_status_to("error", "underflow");
|
|
1822
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min UInt32");
|
|
1823
|
+
opcuaDataValue.value = 0;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (opcuaDataValue.dataType === opcua.DataType.UInt16) {
|
|
1827
|
+
if (opcuaDataValue.value > 65535) {
|
|
1828
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1829
|
+
set_node_error_status_to("error", "overflow");
|
|
1830
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max UInt16");
|
|
1831
|
+
opcuaDataValue.value = 65535;
|
|
1832
|
+
}
|
|
1833
|
+
if (opcuaDataValue.value < 0) {
|
|
1834
|
+
set_node_error_status_to("error", "underflow");
|
|
1835
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min UInt16");
|
|
1836
|
+
opcuaDataValue.value = 0;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (opcuaDataValue.dataType === opcua.DataType.Byte) {
|
|
1840
|
+
if (opcuaDataValue.value > 255) {
|
|
1841
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1842
|
+
set_node_error_status_to("error", "overflow");
|
|
1843
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Byte");
|
|
1844
|
+
opcuaDataValue.value = 255;
|
|
1845
|
+
}
|
|
1846
|
+
if (opcuaDataValue.value < 0) {
|
|
1847
|
+
set_node_error_status_to("error", "underflow");
|
|
1848
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Byte");
|
|
1849
|
+
opcuaDataValue.value = 0;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (opcuaDataValue.dataType === opcua.DataType.SByte) {
|
|
1853
|
+
if (opcuaDataValue.value > 127) {
|
|
1854
|
+
// See note below, some servers will NOT accept statusCode write
|
|
1855
|
+
set_node_error_status_to("error", "overflow");
|
|
1856
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max SByte");
|
|
1857
|
+
opcuaDataValue.value = 127;
|
|
1858
|
+
}
|
|
1859
|
+
if (opcuaDataValue.value < -128) {
|
|
1860
|
+
set_node_error_status_to("error", "underflow");
|
|
1861
|
+
node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min SByte");
|
|
1862
|
+
opcuaDataValue.value = -128;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
nodeToWrite = {
|
|
1867
|
+
nodeId: nodeid.toString(),
|
|
1868
|
+
attributeId: opcua.AttributeIds.Value,
|
|
1869
|
+
value: new opcua.DataValue({
|
|
1870
|
+
value: new opcua.Variant(opcuaDataValue),
|
|
1871
|
+
// sourceTimestamp: new Date(), // NOTE: Some servers do NOT accept time writing
|
|
1872
|
+
// statusCode: opcua.StatusCodes.Good // NOTE: Same with status writing, NOT accepted always
|
|
1873
|
+
})
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
if (node.setstatusandtime && msg.timestamp) {
|
|
1877
|
+
verbose_log("NEW sourceTimestamp: " + new Date(msg.timestamp).toISOString());
|
|
1878
|
+
nodeToWrite.value.sourceTimestamp = new Date(msg.timestamp).getTime();
|
|
1879
|
+
}
|
|
1880
|
+
if (node.setstatusandtime && msg.sourceTimestamp) {
|
|
1881
|
+
verbose_log("NEW sourceTimestamp: " + new Date(msg.sourceTimestamp).toISOString());
|
|
1882
|
+
nodeToWrite.value.sourceTimestamp = new Date(msg.sourceTimestamp).getTime();
|
|
1883
|
+
}
|
|
1884
|
+
if (node.setstatusandtime && msg.statusCode) {
|
|
1885
|
+
verbose_log("NEW statusCode: " + opcua.StatusCode.makeStatusCode(msg.statusCode));
|
|
1886
|
+
nodeToWrite.value.statusCode = msg.statusCode;
|
|
1887
|
+
}
|
|
1888
|
+
verbose_log("VALUE TO WRITE: " + JSON.stringify(nodeToWrite));
|
|
1889
|
+
set_node_status_to("writing");
|
|
1890
|
+
try {
|
|
1891
|
+
node.session.write(nodeToWrite, function (err, statusCode) {
|
|
1892
|
+
if (err) {
|
|
1893
|
+
set_node_error_status_to("error", err);
|
|
1894
|
+
node_error(node.name + " Cannot write value (" + stringify(msg.payload) + ") to msg.topic:" + msg.topic + " error:" + err);
|
|
1895
|
+
// No actual error session existing, this case cause connections to server
|
|
1896
|
+
// reset_opcua_client(connect_opcua_client);
|
|
1897
|
+
msg.payload = err;
|
|
1898
|
+
node.send([msg, null]);
|
|
1899
|
+
} else {
|
|
1900
|
+
set_node_status_to("value written");
|
|
1901
|
+
verbose_log("Value written! Result:" + statusCode + " " + statusCode.description);
|
|
1902
|
+
if (statusCode.isGoodish() === false) {
|
|
1903
|
+
verbose_warn("StatusCode: " + statusCode.toString(16) + " " + statusCode.description);
|
|
1904
|
+
set_node_error_status_to("error", statusCode.description);
|
|
1905
|
+
}
|
|
1906
|
+
msg.payload = statusCode;
|
|
1907
|
+
node.send([msg, null, null]);
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
catch(error) {
|
|
1912
|
+
set_node_status_to("error");
|
|
1913
|
+
node_error("Write failed, error: " + error);
|
|
1914
|
+
}
|
|
1915
|
+
} else {
|
|
1916
|
+
set_node_status_to("invalid session");
|
|
1917
|
+
node_error("Session is not active!")
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
async function writemultiple_action_input(msg) {
|
|
1922
|
+
verbose_log("writing multiple");
|
|
1923
|
+
// Store as with readmultiple item
|
|
1924
|
+
if (msg.topic && msg.topic !== "writemultiple" && !Array.isArray(msg.payload)) {
|
|
1925
|
+
// Topic value: ns=2;s=1:PST-007-Alarm-Level@Training?SETPOINT
|
|
1926
|
+
let dIndex = msg.topic.indexOf("datatype=");
|
|
1927
|
+
|
|
1928
|
+
if (msg.datatype == null && dIndex > 0) {
|
|
1929
|
+
msg.datatype = msg.topic.substring(dIndex + 9);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Store nodeId to read multipleItems array
|
|
1933
|
+
if (msg.topic !== "writemultiple" && msg.topic !== "clearitems") {
|
|
1934
|
+
let opcuaDataValue = opcuaBasics.build_new_dataValue(msg.datatype, msg.payload);
|
|
1935
|
+
let item = {
|
|
1936
|
+
nodeId: msg.topic,
|
|
1937
|
+
datatype: msg.datatype,
|
|
1938
|
+
attributeId: opcua.AttributeIds.Value,
|
|
1939
|
+
indexRange: null,
|
|
1940
|
+
value: new opcua.DataValue({ value: opcuaDataValue })
|
|
1941
|
+
};
|
|
1942
|
+
item.value.sourceTimestamp = new Date(msg.timestamp).getTime();
|
|
1943
|
+
verbose_log("ITEM: " + stringify(item));
|
|
1944
|
+
writeMultipleItems.push(item);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (msg.topic === "clearitems") {
|
|
1948
|
+
verbose_log("clear items...");
|
|
1949
|
+
writeMultipleItems = [];
|
|
1950
|
+
set_node_status_to("clear items");
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (msg.topic !== "writemultiple") {
|
|
1954
|
+
set_node_status_to("nodeId stored");
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (node.session && !node.session.isReconnecting && node.session.isChannelValid() && msg.topic === "writemultiple") {
|
|
1959
|
+
if (Array.isArray(msg.payload)) {
|
|
1960
|
+
writeMultipleItems = msg.payload; // expecting payload format is correct
|
|
1961
|
+
}
|
|
1962
|
+
verbose_log("Writing items: " + stringify(writeMultipleItems));
|
|
1963
|
+
if (writeMultipleItems.length === 0) {
|
|
1964
|
+
node_error(node.name + " no items to write");
|
|
1965
|
+
set_node_status_to("no items to write");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
await node.session.write(writeMultipleItems, function (err, statusCode) {
|
|
1969
|
+
if (err) {
|
|
1970
|
+
set_node_error_status_to("error", err);
|
|
1971
|
+
node_error(node.name + " Cannot write values (" + msg.payload + ") to msg.topic:" + msg.topic + " error:" + err);
|
|
1972
|
+
node.send([{ payload: err }, { error: `${err}`, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
1973
|
+
|
|
1974
|
+
} else {
|
|
1975
|
+
set_node_status_to("active writing");
|
|
1976
|
+
verbose_log("Values written!");
|
|
1977
|
+
node.send([{ payload: statusCode }, null, null]);
|
|
1978
|
+
set_node_status_to("values written");
|
|
1979
|
+
return; // Do not try to run old way
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
if (!node.session || node.session.isReconnecting || !node.session.isChannelValid()) {
|
|
1986
|
+
set_node_status_to("invalid session");
|
|
1987
|
+
node_error("Write multiple items session is not active!")
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// OLD original way to use payload
|
|
1992
|
+
let nodesToWrite; // Define here so it can write array of values
|
|
1993
|
+
if (node.session && !node.session.isReconnecting && node.session.isChannelValid()) {
|
|
1994
|
+
if (Array.isArray(msg.payload)) {
|
|
1995
|
+
nodesToWrite = msg.payload.map(function (msgToWrite) {
|
|
1996
|
+
let opcuaDataValue = opcuaBasics.build_new_dataValue(msgToWrite.datatype || msg.datatype, msgToWrite.value);
|
|
1997
|
+
const nodeToWrite = {
|
|
1998
|
+
nodeId: msgToWrite.nodeId || (node && node.toString()),
|
|
1999
|
+
attributeId: opcua.AttributeIds.Value,
|
|
2000
|
+
indexRange: null,
|
|
2001
|
+
value: new opcua.DataValue({ value: opcuaDataValue })
|
|
2002
|
+
};
|
|
2003
|
+
if (msgToWrite.timestamp || msg.timestamp) {
|
|
2004
|
+
nodeToWrite.value.sourceTimestamp = new Date(msgToWrite.timestamp || msg.timestamp).getTime();
|
|
2005
|
+
}
|
|
2006
|
+
return nodeToWrite;
|
|
2007
|
+
});
|
|
2008
|
+
verbose_log("Writing nodes with values:" + stringify(nodesToWrite));
|
|
2009
|
+
|
|
2010
|
+
await node.session.write(nodesToWrite, function (err, statusCode) {
|
|
2011
|
+
if (err) {
|
|
2012
|
+
set_node_error_status_to("error", err);
|
|
2013
|
+
node_error(node.name + " Cannot write values (" + msg.payload + ") to msg.topic:" + msg.topic + " error:" + err);
|
|
2014
|
+
node.send([{ payload: err }, { error: `${err}`, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
|
|
2015
|
+
} else {
|
|
2016
|
+
set_node_status_to("active writing");
|
|
2017
|
+
verbose_log("Values written!");
|
|
2018
|
+
node.send([{ payload: statusCode }, null, null]);
|
|
2019
|
+
set_node_status_to("values written");
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
} else {
|
|
2024
|
+
set_node_status_to("invalid session");
|
|
2025
|
+
node_error("Write multiple as array session is not active!")
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function subscribe_action_input(msg) {
|
|
2030
|
+
verbose_log("subscribing");
|
|
2031
|
+
if (!subscription) {
|
|
2032
|
+
// first build and start subscription and subscribe on its started event by callback
|
|
2033
|
+
let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
|
|
2034
|
+
if (msg?.interval) {
|
|
2035
|
+
timeMilliseconds = parseInt(msg.interval); // Use this instead of node.time and node.timeUnit
|
|
2036
|
+
}
|
|
2037
|
+
verbose_log("Using subscription with publish interval: " + timeMilliseconds);
|
|
2038
|
+
subscription = make_subscription(subscribe_monitoredItem, msg, opcuaBasics.getSubscriptionParameters(timeMilliseconds));
|
|
2039
|
+
let message = { "topic": "subscriptionId", "payload": subscription.subscriptionId };
|
|
2040
|
+
// node.send(message); // Make it possible to store
|
|
2041
|
+
node.send([message, null, null]);
|
|
2042
|
+
} else if (subscription.subscriptionId != "terminated") {
|
|
2043
|
+
// otherwise check if its terminated start to renew the subscription
|
|
2044
|
+
set_node_status_to("active subscribing");
|
|
2045
|
+
subscribe_monitoredItem(subscription, msg);
|
|
2046
|
+
} else {
|
|
2047
|
+
monitoredItems.clear();
|
|
2048
|
+
set_node_status_to("terminated");
|
|
2049
|
+
// No actual error session existing, this case cause connections to server
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
async function monitor_action_input(msg) {
|
|
2054
|
+
verbose_log("monitoring");
|
|
2055
|
+
if (!subscription) {
|
|
2056
|
+
// first build and start subscription and subscribe on its started event by callback
|
|
2057
|
+
let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
|
|
2058
|
+
subscription = make_subscription(monitor_monitoredItem, msg, opcuaBasics.getSubscriptionParameters(timeMilliseconds));
|
|
2059
|
+
} else if (subscription.subscriptionId != "terminated") {
|
|
2060
|
+
// otherwise check if its terminated start to renew the subscription
|
|
2061
|
+
set_node_status_to("active monitoring");
|
|
2062
|
+
await monitor_monitoredItem(subscription, msg);
|
|
2063
|
+
} else {
|
|
2064
|
+
subscription = null;
|
|
2065
|
+
monitoredItems.clear();
|
|
2066
|
+
set_node_status_to("terminated");
|
|
2067
|
+
// No actual error session created, this case cause connections to server
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function unsubscribe_action_input(msg) {
|
|
2072
|
+
verbose_log("unsubscribing");
|
|
2073
|
+
if (!subscription) {
|
|
2074
|
+
// first build and start subscription and subscribe on its started event by callback
|
|
2075
|
+
verbose_warn("Cannot unscubscribe, no subscription");
|
|
2076
|
+
} else if (subscription.subscriptionId != "terminated") {
|
|
2077
|
+
// otherwise check if its terminated start to renew the subscription
|
|
2078
|
+
set_node_status_to("unsubscribing");
|
|
2079
|
+
unsubscribe_monitoredItem(subscription, msg); // Call to terminate monitoredItem
|
|
2080
|
+
} else {
|
|
2081
|
+
subscription = null;
|
|
2082
|
+
monitoredItems.clear();
|
|
2083
|
+
set_node_status_to("terminated");
|
|
2084
|
+
// No actual error session exists, this case cause connections to server
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function convertAndCheckInterval(interval) {
|
|
2089
|
+
let n = Number(interval);
|
|
2090
|
+
if (isNaN(n)) {
|
|
2091
|
+
n = 100;
|
|
2092
|
+
}
|
|
2093
|
+
return n;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
async function subscribe_monitoredItem(subscription, msg) {
|
|
2097
|
+
verbose_log("Session subscriptionId: " + subscription.subscriptionId);
|
|
2098
|
+
|
|
2099
|
+
// Simplified
|
|
2100
|
+
if (msg.topic === "multiple") {
|
|
2101
|
+
verbose_log("Create monitored itemGroup for " + JSON.stringify(msg.payload));
|
|
2102
|
+
let interval = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
|
|
2103
|
+
if (msg?.interval) {
|
|
2104
|
+
interval = parseInt(msg.interval);
|
|
2105
|
+
}
|
|
2106
|
+
const monitorItems = [];
|
|
2107
|
+
for (const element of msg.payload) {
|
|
2108
|
+
monitorItems.push({ attributeId: AttributeIds.Value, nodeId: element.nodeId });
|
|
2109
|
+
}
|
|
2110
|
+
verbose_log("Using samplingInterval:" + interval);
|
|
2111
|
+
const monitoringParameters = {
|
|
2112
|
+
// clientHandle?: UInt32;
|
|
2113
|
+
samplingInterval: interval, // read from msg.interval
|
|
2114
|
+
// filter?: (ExtensionObject | null);
|
|
2115
|
+
queueSize: 1,
|
|
2116
|
+
discardOldest: true
|
|
2117
|
+
};
|
|
2118
|
+
verbose_log("MONITOR ITEMS: " + JSON.stringify(monitorItems));
|
|
2119
|
+
const group = await subscription.monitorItems(monitorItems, monitoringParameters, TimestampsToReturn.Both);
|
|
2120
|
+
group.on("initialized", async () => {
|
|
2121
|
+
verbose_log(chalk.green("Initialized monitoredItemsGroup !"));
|
|
2122
|
+
});
|
|
2123
|
+
group.on("changed", (_monitoredItem, dataValue, index) => {
|
|
2124
|
+
verbose_log("Group change on item, index: " + index + " item: " + monitorItems[index].nodeId + " value: " + dataValue.value.value);
|
|
2125
|
+
|
|
2126
|
+
const nodeId = monitorItems[index].nodeId.toString();
|
|
2127
|
+
|
|
2128
|
+
let value = dataValue.value.dataType === opcua.DataType.ExtensionObject
|
|
2129
|
+
? JSON.parse(JSON.stringify(dataValue))
|
|
2130
|
+
: dataValue;
|
|
2131
|
+
|
|
2132
|
+
if (nodeId) {
|
|
2133
|
+
let msg = {};
|
|
2134
|
+
msg.topic = nodeId;
|
|
2135
|
+
msg.payload = value; // if users want to get dataValue.value.value example contains function node
|
|
2136
|
+
node.send([msg, null, null]);
|
|
2137
|
+
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
let nodeStr = msg.topic;
|
|
2144
|
+
if (msg?.topic) {
|
|
2145
|
+
if (nodeStr && nodeStr.length > 1) {
|
|
2146
|
+
let dTypeIndex = nodeStr.indexOf(";datatype=");
|
|
2147
|
+
if (dTypeIndex > 0) {
|
|
2148
|
+
nodeStr = nodeStr.substring(0, dTypeIndex);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
let monitoredItem = monitoredItems.get(msg.topic);
|
|
2153
|
+
|
|
2154
|
+
if (!monitoredItem) {
|
|
2155
|
+
verbose_log("Msg " + stringify(msg));
|
|
2156
|
+
// Set as default if no payload
|
|
2157
|
+
let queueSize = 10;
|
|
2158
|
+
let interval = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit); // Use value given at client node
|
|
2159
|
+
// Interval from the payload (old existing feature still supported), but do not accept timestamp, it is too big
|
|
2160
|
+
if (msg.payload && parseInt(msg.payload) > 100 && parseInt(msg.payload) < 1608935031227) {
|
|
2161
|
+
interval = convertAndCheckInterval(msg.payload);
|
|
2162
|
+
}
|
|
2163
|
+
if (msg.interval && parseInt(msg.interval) > 100) {
|
|
2164
|
+
interval = convertAndCheckInterval(msg.interval);
|
|
2165
|
+
}
|
|
2166
|
+
if (msg.queueSize && parseInt(msg.queueSize) > 0) {
|
|
2167
|
+
queueSize = msg.queueSize;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
verbose_log(msg.topic + " samplingInterval " + interval + " queueSize " + queueSize);
|
|
2171
|
+
verbose_log("Monitoring value: " + msg.topic + ' by interval of ' + interval.toString() + " ms");
|
|
2172
|
+
|
|
2173
|
+
// Validate nodeId
|
|
2174
|
+
try {
|
|
2175
|
+
let nodeId = opcua.coerceNodeId(nodeStr);
|
|
2176
|
+
if (nodeId?.isEmpty()) {
|
|
2177
|
+
node_error(" Invalid empty node in getObject");
|
|
2178
|
+
}
|
|
2179
|
+
//makeNodeId(nodeStr); // above is enough
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
node_error(err);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
try {
|
|
2186
|
+
monitoredItem = opcua.ClientMonitoredItem.create(subscription, {
|
|
2187
|
+
nodeId: nodeStr,
|
|
2188
|
+
attributeId: opcua.AttributeIds.Value
|
|
2189
|
+
}, {
|
|
2190
|
+
samplingInterval: interval,
|
|
2191
|
+
queueSize: queueSize,
|
|
2192
|
+
discardOldest: true
|
|
2193
|
+
},
|
|
2194
|
+
TimestampsToReturn.Both, // Other valid values: Source | Server | Neither | Both
|
|
2195
|
+
);
|
|
2196
|
+
verbose_log("Storing monitoredItem: " + nodeStr + " ItemId: " + monitoredItem.toString());
|
|
2197
|
+
monitoredItems.set(nodeStr, monitoredItem);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
node_error("Check topic format for nodeId:" + msg.topic)
|
|
2200
|
+
node_error('subscription.monitorItem:' + err);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
monitoredItem.on("initialized", function () {
|
|
2204
|
+
verbose_log("initialized monitoredItem on " + nodeStr);
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
monitoredItem.on("changed", function (dataValue) {
|
|
2208
|
+
let msgToSend = JSON.parse(JSON.stringify(msg)); // clone original msg if it contains other needed properties {};
|
|
2209
|
+
|
|
2210
|
+
set_node_status_to("active subscribed");
|
|
2211
|
+
// if (dataValue.statusCode != opcua.StatusCodes.Good) {
|
|
2212
|
+
// Skip Overflow and limitLow, limitHigh and constant bits
|
|
2213
|
+
if (dataValue.statusCode.isGoodish() === false) {
|
|
2214
|
+
verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
msgToSend.statusCode = dataValue.statusCode;
|
|
2218
|
+
msgToSend.topic = msg.topic;
|
|
2219
|
+
|
|
2220
|
+
// Check if timestamps exists otherwise simulate them
|
|
2221
|
+
if (dataValue.serverTimestamp != null) {
|
|
2222
|
+
msgToSend.serverTimestamp = dataValue.serverTimestamp;
|
|
2223
|
+
msgToSend.serverPicoseconds = dataValue.serverPicoseconds;
|
|
2224
|
+
} else {
|
|
2225
|
+
msgToSend.serverTimestamp = new Date().getTime();;
|
|
2226
|
+
msgToSend.serverPicoseconds = 0;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if (dataValue.sourceTimestamp != null) {
|
|
2230
|
+
msgToSend.sourceTimestamp = dataValue.sourceTimestamp;
|
|
2231
|
+
msgToSend.sourcePicoseconds = dataValue.sourcePicoseconds;
|
|
2232
|
+
} else {
|
|
2233
|
+
msgToSend.sourceTimestamp = new Date().getTime();;
|
|
2234
|
+
msgToSend.sourcePicoseconds = 0;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
msgToSend.payload = dataValue.value.value;
|
|
2238
|
+
node.send([msgToSend, null, null]);
|
|
2239
|
+
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
monitoredItem.on("keepalive", function () {
|
|
2243
|
+
verbose_log("keepalive monitoredItem on " + nodeStr);
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
monitoredItem.on("terminated", function () {
|
|
2247
|
+
verbose_log("terminated monitoredItem on " + nodeStr);
|
|
2248
|
+
if (monitoredItems.has(nodeStr)) {
|
|
2249
|
+
monitoredItems.delete(nodeStr);
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
return monitoredItem;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
async function monitor_monitoredItem(subscription, msg) {
|
|
2258
|
+
verbose_log("Session subscriptionId: " + subscription.subscriptionId);
|
|
2259
|
+
let nodeStr = msg.topic;
|
|
2260
|
+
if (msg?.topic) {
|
|
2261
|
+
let dTypeIndex = nodeStr.indexOf(";datatype=");
|
|
2262
|
+
if (dTypeIndex > 0) {
|
|
2263
|
+
nodeStr = nodeStr.substring(0, dTypeIndex);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
let monitoredItem = monitoredItems.get(msg.topic);
|
|
2267
|
+
if (!monitoredItem) {
|
|
2268
|
+
verbose_log("Msg " + stringify(msg));
|
|
2269
|
+
let interval = 100; // Set as default if no payload
|
|
2270
|
+
let queueSize = 10;
|
|
2271
|
+
// Interval from the payload (old existing feature still supported)
|
|
2272
|
+
if (msg.payload && parseInt(msg.payload) > 100) {
|
|
2273
|
+
interval = convertAndCheckInterval(msg.payload);
|
|
2274
|
+
}
|
|
2275
|
+
if (msg.interval && parseInt(msg.interval) > 100) {
|
|
2276
|
+
interval = convertAndCheckInterval(msg.interval);
|
|
2277
|
+
}
|
|
2278
|
+
if (msg.queueSize && parseInt(msg.queueSize) > 0) {
|
|
2279
|
+
queueSize = msg.queueSize;
|
|
2280
|
+
}
|
|
2281
|
+
verbose_log("Monitoring " + msg.topic + " samplingInterval " + interval + "ms, queueSize " + queueSize);
|
|
2282
|
+
// Validate nodeId
|
|
2283
|
+
try {
|
|
2284
|
+
let nodeId = opcua.coerceNodeId(nodeStr);
|
|
2285
|
+
if (nodeId?.isEmpty()) {
|
|
2286
|
+
node_error(" Invalid empty node in getObject");
|
|
2287
|
+
}
|
|
2288
|
+
//makeNodeId(nodeStr); // above is enough
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
node_error(err);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
let deadbandtype = opcua.DeadbandType.Absolute;
|
|
2294
|
+
// NOTE differs from standard subscription monitor
|
|
2295
|
+
if (node.deadbandType == "a") {
|
|
2296
|
+
deadbandtype = opcua.DeadbandType.Absolute;
|
|
2297
|
+
}
|
|
2298
|
+
if (node.deadbandType == "p") {
|
|
2299
|
+
deadbandtype = opcua.DeadbandType.Percent;
|
|
2300
|
+
}
|
|
2301
|
+
// Check if msg contains deadbandtype, use it instead of value given in client node
|
|
2302
|
+
if (msg.deadbandType && msg.deadbandType == "a") {
|
|
2303
|
+
deadbandtype = opcua.DeadbandType.Absolute;
|
|
2304
|
+
}
|
|
2305
|
+
if (msg.deadbandType && msg.deadbandType == "p") {
|
|
2306
|
+
deadbandtype = opcua.DeadbandType.Percent;
|
|
2307
|
+
}
|
|
2308
|
+
let deadbandvalue = node.deadbandvalue;
|
|
2309
|
+
// Check if msg contains deadbandValue, use it instead of value given in client node
|
|
2310
|
+
if (msg.deadbandValue) {
|
|
2311
|
+
deadbandvalue = msg.deadbandValue;
|
|
2312
|
+
}
|
|
2313
|
+
verbose_log("Deadband type (a==absolute, p==percent) " + deadbandtype + " deadband value " + deadbandvalue);
|
|
2314
|
+
let dataChangeFilter = new opcua.DataChangeFilter({
|
|
2315
|
+
trigger: opcua.DataChangeTrigger.StatusValue,
|
|
2316
|
+
deadbandType: deadbandtype,
|
|
2317
|
+
deadbandValue: deadbandvalue
|
|
2318
|
+
});
|
|
2319
|
+
const group = await subscription.monitorItems(
|
|
2320
|
+
[{
|
|
2321
|
+
nodeId: nodeStr,
|
|
2322
|
+
attributeId: opcua.AttributeIds.Value
|
|
2323
|
+
}],
|
|
2324
|
+
{
|
|
2325
|
+
samplingInterval: interval,
|
|
2326
|
+
queueSize: queueSize,
|
|
2327
|
+
discardOldest: true,
|
|
2328
|
+
filter: dataChangeFilter
|
|
2329
|
+
},
|
|
2330
|
+
TimestampsToReturn.Both, // Other valid values: Source | Server | Neither | Both
|
|
2331
|
+
);
|
|
2332
|
+
monitoredItems.set(nodeStr, monitoredItem);
|
|
2333
|
+
|
|
2334
|
+
group.on("err", () => {
|
|
2335
|
+
node.error("Monitored items error!");
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
group.on("changed", (monitoredItem, dataValue, index) => {
|
|
2339
|
+
verbose_log(chalk.green("Received changes: " + monitoredItem + " value: " + dataValue + " index: " + index));
|
|
2340
|
+
set_node_status_to("active monitoring");
|
|
2341
|
+
verbose_log(dataValue.toString());
|
|
2342
|
+
|
|
2343
|
+
if (dataValue.statusCode.isGoodish() === false) {
|
|
2344
|
+
verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
|
|
2345
|
+
}
|
|
2346
|
+
let msgToSend = {};
|
|
2347
|
+
msgToSend.statusCode = dataValue.statusCode;
|
|
2348
|
+
msgToSend.topic = msg.topic;
|
|
2349
|
+
|
|
2350
|
+
// Check if timestamps exists otherwise simulate them
|
|
2351
|
+
if (dataValue.serverTimestamp != null) {
|
|
2352
|
+
msgToSend.serverTimestamp = dataValue.serverTimestamp;
|
|
2353
|
+
msgToSend.serverPicoseconds = dataValue.serverPicoseconds;
|
|
2354
|
+
} else {
|
|
2355
|
+
msgToSend.serverTimestamp = new Date().getTime();
|
|
2356
|
+
msgToSend.serverPicoseconds = 0;
|
|
2357
|
+
}
|
|
2358
|
+
if (dataValue.sourceTimestamp != null) {
|
|
2359
|
+
msgToSend.sourceTimestamp = dataValue.sourceTimestamp;
|
|
2360
|
+
msgToSend.sourcePicoseconds = dataValue.sourcePicoseconds;
|
|
2361
|
+
} else {
|
|
2362
|
+
msgToSend.sourceTimestamp = new Date().getTime();
|
|
2363
|
+
msgToSend.sourcePicoseconds = 0;
|
|
2364
|
+
}
|
|
2365
|
+
msgToSend.payload = dataValue.value.value;
|
|
2366
|
+
node.send([msgToSend, null, null]);
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
function unsubscribe_monitoredItem(subscription, msg) {
|
|
2373
|
+
verbose_log("Session subscriptionId: " + subscription.subscriptionId);
|
|
2374
|
+
let nodeStr = msg.topic; // nodeId needed as topic
|
|
2375
|
+
if (msg?.topic) {
|
|
2376
|
+
let dTypeIndex = nodeStr.indexOf(";datatype=");
|
|
2377
|
+
if (dTypeIndex > 0) {
|
|
2378
|
+
nodeStr = nodeStr.substring(0, dTypeIndex);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
let monitoredItem = monitoredItems.get(msg.topic);
|
|
2382
|
+
if (monitoredItem) {
|
|
2383
|
+
verbose_log("Got ITEM: " + monitoredItem);
|
|
2384
|
+
verbose_log("Unsubscribing monitored item: " + msg.topic + " item:" + monitoredItem.toString());
|
|
2385
|
+
monitoredItem.terminate();
|
|
2386
|
+
monitoredItems.delete(msg.topic);
|
|
2387
|
+
}
|
|
2388
|
+
else {
|
|
2389
|
+
node_error("NodeId " + nodeStr + " is not subscribed!");
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function delete_subscription_action_input(msg) {
|
|
2394
|
+
verbose_log("delete subscription msg= " + stringify(msg));
|
|
2395
|
+
if (!subscription) {
|
|
2396
|
+
verbose_warn("Cannot delete, no subscription existing!");
|
|
2397
|
+
} else if (subscription.isActive) {
|
|
2398
|
+
// otherwise check if its terminated start to renew the subscription
|
|
2399
|
+
|
|
2400
|
+
node.session.deleteSubscriptions({
|
|
2401
|
+
subscriptionIds: [subscription.subscriptionId]
|
|
2402
|
+
}, function (err, response) {
|
|
2403
|
+
if (err) {
|
|
2404
|
+
node_error("Delete subscription error " + err);
|
|
2405
|
+
}
|
|
2406
|
+
else {
|
|
2407
|
+
verbose_log("Subscription deleted, response:" + stringify(response));
|
|
2408
|
+
subscription.terminate(); // Added to allow new subscription
|
|
2409
|
+
}
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
node.add_item = function (item) {
|
|
2415
|
+
if (item) {
|
|
2416
|
+
if (!node.items) {
|
|
2417
|
+
node.items = new Array();
|
|
2418
|
+
}
|
|
2419
|
+
node.items.push({
|
|
2420
|
+
'item': item
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
function node_error(err) {
|
|
2426
|
+
node.error("Browse node error!", err);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
async function browse_action_input(msg) {
|
|
2430
|
+
verbose_log("browsing");
|
|
2431
|
+
node.items = [];
|
|
2432
|
+
|
|
2433
|
+
if (node.session) {
|
|
2434
|
+
const client = opcua.OPCUAClient.create(connectionOption);
|
|
2435
|
+
set_node_status_to("active browsing");
|
|
2436
|
+
|
|
2437
|
+
const nodeId = msg.topic;
|
|
2438
|
+
node.debug("nodeId: " + nodeId);
|
|
2439
|
+
|
|
2440
|
+
try {
|
|
2441
|
+
const browseResult = await node.session.browse(nodeId);
|
|
2442
|
+
|
|
2443
|
+
for(const reference of browseResult.references)
|
|
2444
|
+
{
|
|
2445
|
+
var ref_obj = Object.assign({}, reference);
|
|
2446
|
+
const dataValue = await node.session.read({nodeId: ref_obj.nodeId, attributeId: opcua.AttributeIds.Value});
|
|
2447
|
+
ref_obj["value"] = dataValue.value.value;
|
|
2448
|
+
ref_obj["dataType"] = opcua.DataType[dataValue.value.dataType];
|
|
2449
|
+
node.add_item(ref_obj);
|
|
2450
|
+
}
|
|
2451
|
+
msg.payload = node.items
|
|
2452
|
+
node.send([msg, null, null]);
|
|
2453
|
+
node.status({
|
|
2454
|
+
fill: "green",
|
|
2455
|
+
shape: "dot",
|
|
2456
|
+
text: "Items: " + node.items.length,
|
|
2457
|
+
source: { id: node.id, type: node.type, name: "OPC UA Browser"}
|
|
2458
|
+
});
|
|
2459
|
+
} catch (err) {
|
|
2460
|
+
node_error("Browse error: " + err.message);
|
|
2461
|
+
verbose_log(err);
|
|
2462
|
+
node.status({
|
|
2463
|
+
fill: "yellow",
|
|
2464
|
+
shape: "ring",
|
|
2465
|
+
text: "browse error",
|
|
2466
|
+
source: { id: node.id, type: node.type, name: "OPC UA Browser" }
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
} else {
|
|
2470
|
+
node_error("Session is not active!");
|
|
2471
|
+
set_node_status_to("invalid session");
|
|
2472
|
+
reset_opcua_client(connect_opcua_client);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function subscribe_monitoredEvent(subscription, msg) {
|
|
2477
|
+
verbose_log("Session subscriptionId: " + subscription.subscriptionId);
|
|
2478
|
+
|
|
2479
|
+
let monitoredItem = monitoredItems.get(msg.topic);
|
|
2480
|
+
if (monitoredItem === undefined) {
|
|
2481
|
+
verbose_log("Msg " + stringify(msg));
|
|
2482
|
+
let interval = convertAndCheckInterval(msg.payload);
|
|
2483
|
+
verbose_log(msg.topic + " samplingInterval " + interval);
|
|
2484
|
+
verbose_log("Monitoring Event: " + msg.topic + ' by interval of ' + interval + " ms");
|
|
2485
|
+
// TODO read nodeId to validate it before subscription
|
|
2486
|
+
try {
|
|
2487
|
+
monitoredItem = opcua.ClientMonitoredItem.create(subscription,
|
|
2488
|
+
{
|
|
2489
|
+
nodeId: msg.topic, // serverObjectId
|
|
2490
|
+
attributeId: AttributeIds.EventNotifier
|
|
2491
|
+
}, {
|
|
2492
|
+
samplingInterval: interval,
|
|
2493
|
+
queueSize: 100000,
|
|
2494
|
+
filter: msg.eventFilter,
|
|
2495
|
+
discardOldest: true
|
|
2496
|
+
},
|
|
2497
|
+
TimestampsToReturn.Neither
|
|
2498
|
+
);
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
node_error('subscription.monitorEvent:' + err);
|
|
2501
|
+
// No actual error session exists, this case cause connections to server
|
|
2502
|
+
// reset_opcua_client(connect_opcua_client);
|
|
2503
|
+
}
|
|
2504
|
+
monitoredItems.set(msg.topic, monitoredItem.monitoredItemId);
|
|
2505
|
+
monitoredItem.on("initialized", async function () {
|
|
2506
|
+
if (node.session && subscription) {
|
|
2507
|
+
await opcua.callConditionRefresh(node.session, subscription.subscriptionId); // FIXED
|
|
2508
|
+
verbose_log("monitored Event initialized");
|
|
2509
|
+
set_node_status_to("initialized");
|
|
2510
|
+
}
|
|
2511
|
+
else {
|
|
2512
|
+
set_node_status_to("invalid session");
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
monitoredItem.on("changed", function (eventFields) {
|
|
2517
|
+
dumpEvent(node, node.session, msg.eventFields, eventFields, function () { });
|
|
2518
|
+
set_node_status_to("changed");
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2521
|
+
monitoredItem.on("error", function (err_message) {
|
|
2522
|
+
verbose_log("error monitored Event on " + msg.topic);
|
|
2523
|
+
if (monitoredItems.has(msg.topic)) {
|
|
2524
|
+
monitoredItems.delete(msg.topic);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
node_error("monitored Event " + msg.eventTypeId + " ERROR" + err_message);
|
|
2528
|
+
set_node_error_status_to("error", err_message);
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
monitoredItem.on("keepalive", function () {
|
|
2532
|
+
verbose_log("keepalive monitored Event on " + msg.topic);
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
monitoredItem.on("terminated", function () {
|
|
2536
|
+
verbose_log("terminated monitored Event on " + msg.topic);
|
|
2537
|
+
if (monitoredItems.has(msg.topic)) {
|
|
2538
|
+
monitoredItems.delete(msg.topic);
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
return monitoredItem;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
async function subscribe_events_async(msg) {
|
|
2547
|
+
verbose_log("subscribing events for " + msg.eventTypeIds);
|
|
2548
|
+
let eventTypeId = opcua.resolveNodeId(msg.eventTypeIds);
|
|
2549
|
+
let fields = await opcua.extractConditionFields(node.session, eventTypeId); // works with all eventTypes
|
|
2550
|
+
|
|
2551
|
+
// FIX for issue #623
|
|
2552
|
+
if (msg.hasOwnProperty("customEventFields")) {
|
|
2553
|
+
if (Array.isArray(msg.customEventFields)) {
|
|
2554
|
+
verbose_log("customEventFields: " + msg.customEventFields);
|
|
2555
|
+
fields = fields.concat(msg.customEventFields);
|
|
2556
|
+
} else {
|
|
2557
|
+
verbose_log("customEventFields is NOT Array!"); // Good to show
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
else {
|
|
2561
|
+
verbose_log("msg object does not have customEventFields!");
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
msg.eventFilter = opcua.constructEventFilter(fields, opcua.ofType(eventTypeId));
|
|
2565
|
+
msg.eventFields = fields;
|
|
2566
|
+
verbose_log("EventFields: " + msg.eventFields);
|
|
2567
|
+
|
|
2568
|
+
if (!subscription) {
|
|
2569
|
+
// first build and start subscription and subscribe on its started event by callback
|
|
2570
|
+
let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
|
|
2571
|
+
subscription = make_subscription(subscribe_monitoredEvent, msg, opcuaBasics.getEventSubscriptionParameters(timeMilliseconds));
|
|
2572
|
+
} else if (subscription.subscriptionId != "terminated") {
|
|
2573
|
+
// otherwise check if its terminated start to renew the subscription
|
|
2574
|
+
set_node_status_to("active subscribing");
|
|
2575
|
+
subscribe_monitoredEvent(subscription, msg);
|
|
2576
|
+
} else {
|
|
2577
|
+
subscription = null;
|
|
2578
|
+
monitoredItems.clear();
|
|
2579
|
+
set_node_status_to("terminated");
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
function subscribe_events_input(msg) {
|
|
2584
|
+
subscribe_events_async(msg);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
function reconnect(msg) {
|
|
2588
|
+
if (msg && msg.OpcUaEndpoint) {
|
|
2589
|
+
// Remove listeners if existing
|
|
2590
|
+
if (node.client) {
|
|
2591
|
+
verbose_log("Cleanup old listener events... before connecting to new client");
|
|
2592
|
+
verbose_log("All event names:" + node.client.eventNames());
|
|
2593
|
+
verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished"));
|
|
2594
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
2595
|
+
verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
|
|
2596
|
+
node.client.removeListener("backoff", backoff);
|
|
2597
|
+
verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
|
|
2598
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
2599
|
+
}
|
|
2600
|
+
opcuaEndpoint = msg.OpcUaEndpoint; // Use global variable! Check all parameters!
|
|
2601
|
+
connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint?.securityPolicy]; // || opcua.SecurityPolicy.None;
|
|
2602
|
+
connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint?.securityMode]; // || opcua.MessageSecurityMode.None;
|
|
2603
|
+
verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode);
|
|
2604
|
+
if (opcuaEndpoint.login === true) {
|
|
2605
|
+
let userIdentity = {
|
|
2606
|
+
userName: opcuaEndpoint?.user,
|
|
2607
|
+
password: opcuaEndpoint?.password,
|
|
2608
|
+
type: opcua.UserTokenType.UserName
|
|
2609
|
+
};
|
|
2610
|
+
verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity));
|
|
2611
|
+
}
|
|
2612
|
+
verbose_log("Using new endpoint:" + stringify(opcuaEndpoint));
|
|
2613
|
+
} else {
|
|
2614
|
+
verbose_log("Using endpoint:" + stringify(opcuaEndpoint));
|
|
2615
|
+
}
|
|
2616
|
+
// First close subscriptions etc.
|
|
2617
|
+
if (subscription && subscription.isActive) {
|
|
2618
|
+
subscription.terminate();
|
|
2619
|
+
}
|
|
2620
|
+
subscription = null;
|
|
2621
|
+
monitoredItems.clear();
|
|
2622
|
+
|
|
2623
|
+
set_node_status_to("reconnect");
|
|
2624
|
+
|
|
2625
|
+
// Sequential teardown: close session first, then disconnect client,
|
|
2626
|
+
// and only create the new client once the old TCP socket is fully closed.
|
|
2627
|
+
// This prevents stale connections from accumulating on the PLC.
|
|
2628
|
+
const doReconnect = () => {
|
|
2629
|
+
if (node.client) {
|
|
2630
|
+
verbose_log("Disconnecting old client before reconnect...");
|
|
2631
|
+
node.client.disconnect(function () {
|
|
2632
|
+
verbose_log("Old client disconnected, creating new client...");
|
|
2633
|
+
node.client = null;
|
|
2634
|
+
create_opcua_client(connect_opcua_client);
|
|
2635
|
+
});
|
|
2636
|
+
} else {
|
|
2637
|
+
create_opcua_client(connect_opcua_client);
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
|
|
2641
|
+
if (node.session) {
|
|
2642
|
+
node.session.close(function (err) {
|
|
2643
|
+
if (err) {
|
|
2644
|
+
node_error("Session close error: " + err);
|
|
2645
|
+
} else {
|
|
2646
|
+
verbose_log("Session closed!");
|
|
2647
|
+
}
|
|
2648
|
+
node.session = null;
|
|
2649
|
+
doReconnect();
|
|
2650
|
+
});
|
|
2651
|
+
} else {
|
|
2652
|
+
verbose_warn("No session to close!");
|
|
2653
|
+
node.session = null;
|
|
2654
|
+
doReconnect();
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
node.on("close", async (done) => {
|
|
2659
|
+
if (subscription?.isActive) {
|
|
2660
|
+
subscription.terminate();
|
|
2661
|
+
// subscription becomes null by its terminated event
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
if (node.session) {
|
|
2665
|
+
try {
|
|
2666
|
+
await node.session.close();
|
|
2667
|
+
verbose_log("Session closed");
|
|
2668
|
+
set_node_status_to("session closed");
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
node_error(node.name + " " + err);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
node.session = null;
|
|
2675
|
+
|
|
2676
|
+
// Move done() inside the disconnect callback so Node-RED waits for the
|
|
2677
|
+
// TCP socket to fully close before considering this node shut down.
|
|
2678
|
+
// Without this, a redeploy starts a new connection while the old one is
|
|
2679
|
+
// still being torn down, causing the PLC to accumulate stale connections.
|
|
2680
|
+
if (node.client) {
|
|
2681
|
+
if(!node.disconnecting) return done();
|
|
2682
|
+
node.disconnecting = true;
|
|
2683
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
2684
|
+
node.client.removeListener("backoff", backoff);
|
|
2685
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
2686
|
+
await node.client.disconnect(function () {
|
|
2687
|
+
verbose_log("Client disconnected on node close");
|
|
2688
|
+
node.disconnecting = false;
|
|
2689
|
+
node.client = null;
|
|
2690
|
+
done();
|
|
2691
|
+
});
|
|
2692
|
+
} else {
|
|
2693
|
+
done();
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
node.on("error", function () {
|
|
2698
|
+
if (subscription?.isActive) {
|
|
2699
|
+
subscription.terminate();
|
|
2700
|
+
// subscription becomes null by its terminated event
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (node.session) {
|
|
2704
|
+
node.session.close(function (err) {
|
|
2705
|
+
verbose_log("Session closed on error emit");
|
|
2706
|
+
if (err) {
|
|
2707
|
+
node_error(node.name + " " + err);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
set_node_status_to("session closed");
|
|
2711
|
+
node.session = null;
|
|
2712
|
+
close_opcua_client("node error", err);
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
} else {
|
|
2716
|
+
node.session = null;
|
|
2717
|
+
close_opcua_client("node error", 0);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
verbose_log("Disconnecting...");
|
|
2722
|
+
|
|
2723
|
+
verbose_warn(`node.client == null ? ${node.client == null}`)
|
|
2724
|
+
if (node.client) {
|
|
2725
|
+
verbose_warn("Node CLient Error")
|
|
2726
|
+
node.client.removeListener("connection_reestablished", reestablish);
|
|
2727
|
+
verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
|
|
2728
|
+
node.client.removeListener("backoff", backoff);
|
|
2729
|
+
verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
|
|
2730
|
+
node.client.removeListener("start_reconnection", reconnection);
|
|
2731
|
+
node.client.disconnect(function () {
|
|
2732
|
+
verbose_log("Client disconnected!");
|
|
2733
|
+
node.client = null;
|
|
2734
|
+
set_node_status_to("disconnected");
|
|
2735
|
+
});
|
|
2736
|
+
close_opcua_client("node error", 0);
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
RED.nodes.registerType("OpcUa-Client", OpcUaClientNode);
|
|
2741
|
+
}
|