@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +20 -0
  3. package/Readme.md +239 -0
  4. package/images/PROSYS-OPC-UA-EXAMPLE2.png +0 -0
  5. package/images/nodeopcua64.png +0 -0
  6. package/images/opcuanode.png +0 -0
  7. package/images/opcuanodeLogo64.png +0 -0
  8. package/opcua/101-opcuaitem.html +106 -0
  9. package/opcua/101-opcuaitem.js +82 -0
  10. package/opcua/102-opcuaclient.html +274 -0
  11. package/opcua/102-opcuaclient.js +2741 -0
  12. package/opcua/103-opcuabrowser.html +71 -0
  13. package/opcua/103-opcuabrowser.js +230 -0
  14. package/opcua/104-opcuaserver.html +261 -0
  15. package/opcua/104-opcuaserver.js +1844 -0
  16. package/opcua/105-opcuaendpoint.html +224 -0
  17. package/opcua/105-opcuaendpoint.js +64 -0
  18. package/opcua/106-opcuaevent.html +110 -0
  19. package/opcua/106-opcuaevent.js +50 -0
  20. package/opcua/107-opcuamethod.html +244 -0
  21. package/opcua/107-opcuamethod.js +521 -0
  22. package/opcua/108-opcuarights.html +223 -0
  23. package/opcua/108-opcuarights.js +156 -0
  24. package/opcua/109-opcuadiscovery.html +51 -0
  25. package/opcua/109-opcuadiscovery.js +180 -0
  26. package/opcua/dump_certificates.js +55 -0
  27. package/opcua/icons/opcuanode.png +0 -0
  28. package/opcua/icons/opcuanodeLogo.png +0 -0
  29. package/opcua/opcua-basics.js +1061 -0
  30. package/opcua/public/vendor/opc-foundation/binary/OPC.ISA95.Types.bsd.xml +141 -0
  31. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Adi.Types.bsd.xml +76 -0
  32. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Di.Types.bsd.xml +11 -0
  33. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Gds.Types.bsd.xml +25 -0
  34. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Types.bsd.xml +2391 -0
  35. package/opcua/public/vendor/opc-foundation/csv/AttributeIds.csv +22 -0
  36. package/opcua/public/vendor/opc-foundation/csv/NodeIds.csv +5746 -0
  37. package/opcua/public/vendor/opc-foundation/csv/Opc.Ua.StatusCodes.csv +227 -0
  38. package/opcua/public/vendor/opc-foundation/csv/OpcUaDiModel.csv +1523 -0
  39. package/opcua/public/vendor/opc-foundation/csv/OpcUaGdsModel.csv +400 -0
  40. package/opcua/public/vendor/opc-foundation/csv/ServerCapabilities.csv +13 -0
  41. package/opcua/public/vendor/opc-foundation/csv/ServerCapabilityIdentifiers.csv +13 -0
  42. package/opcua/public/vendor/opc-foundation/csv/StatusCode.csv +227 -0
  43. package/opcua/public/vendor/opc-foundation/csv/StatusCodes.csv +227 -0
  44. package/opcua/public/vendor/opc-foundation/schema/OPC.ISA95.Types.xsd +163 -0
  45. package/opcua/public/vendor/opc-foundation/schema/OPCBinarySchema.xsd +119 -0
  46. package/opcua/public/vendor/opc-foundation/schema/Opc.Ua.Types.xsd +3938 -0
  47. package/opcua/public/vendor/opc-foundation/schema/SecuredApplication.xsd +106 -0
  48. package/opcua/public/vendor/opc-foundation/schema/UANodeSet.xsd +420 -0
  49. package/opcua/public/vendor/opc-foundation/xml/OPC.ISA95.NodeSet2.Nov52013.xml +3293 -0
  50. package/opcua/public/vendor/opc-foundation/xml/Opc.ISA95.NodeSet2.xml +3589 -0
  51. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.NodeSet2.xml +8301 -0
  52. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.bsd.xml +76 -0
  53. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.xsd +157 -0
  54. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.AutoID.NodeSet2.xml +6062 -0
  55. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.NodeSet2.xml +5066 -0
  56. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.PredefinedNodes.xml +11495 -0
  57. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd +86 -0
  58. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd.xml +11 -0
  59. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.xsd +155 -0
  60. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet.xml +15226 -0
  61. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet2.xml +6214 -0
  62. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.bsd.xml +25 -0
  63. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.xsd +30 -0
  64. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-2.xml +134881 -0
  65. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-3.xml +134881 -0
  66. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet.xml +134881 -0
  67. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part10.xml +832 -0
  68. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part11.xml +793 -0
  69. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part12.xml +3877 -0
  70. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part13.xml +344 -0
  71. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part3.xml +1103 -0
  72. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part4.xml +3091 -0
  73. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part5.xml +17092 -0
  74. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part8.xml +525 -0
  75. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part9.xml +2049 -0
  76. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part999.xml +134 -0
  77. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.xml +67169 -0
  78. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.PredefinedNodes.xml +64500 -0
  79. package/opcua/public/vendor/opc-foundation/xml/OpcUaDiModel.xml +1239 -0
  80. package/opcua/utils.js +55 -0
  81. package/package.json +68 -0
  82. package/users.json +22 -0
@@ -0,0 +1,1844 @@
1
+ /**
2
+
3
+ Copyright 2015 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
+ module.exports = function (RED) {
20
+ "use strict";
21
+ var opcua = require('node-opcua');
22
+ const DataValue = opcua.DataValue;
23
+ const ObjectIds = require("node-opcua-constants");
24
+ var fileTransfer = require("node-opcua-file-transfer");
25
+ const { NodeCrawler } = require("node-opcua-client-crawler");
26
+ var path = require('path');
27
+ var os = require("os");
28
+ var fs = require("fs");
29
+ var chalk = require("chalk");
30
+ var opcuaBasics = require('./opcua-basics');
31
+ const {parse, stringify} = require('flatted');
32
+ const { createServerCertificateManager, createUserCertificateManager } = require("./utils");
33
+
34
+ function OpcUaServerNode(n) {
35
+
36
+ RED.nodes.createNode(this, n);
37
+
38
+ this.name = n.name;
39
+ this.port = n.port;
40
+ this.endpoint = n.endpoint;
41
+ this.usersFile = n.usersFile;
42
+ this.nodesetDir = n.nodesetDir;
43
+ this.autoAcceptUnknownCertificate = n.autoAcceptUnknownCertificate;
44
+ this.allowAnonymous = n.allowAnonymous;
45
+ this.endpointNone = n.endpointNone;
46
+ this.endpointSign = n.endpointSign;
47
+ this.endpointSignEncrypt = n.endpointSignEncrypt;
48
+ this.endpointBasic128Rsa15 = n.endpointBasic128Rsa15;
49
+ this.endpointBasic256 = n.endpointBasic256;
50
+ this.endpointBasic256Sha256 = n.endpointBasic256Sha256;
51
+ // Operating limits:
52
+ this.maxNodesPerBrowse = n.maxNodesPerBrowse;
53
+ this.maxNodesPerHistoryReadData = n.maxNodesPerHistoryReadData;
54
+ this.maxNodesPerHistoryReadEvents = n.maxNodesPerHistoryReadEvents;
55
+ this.maxNodesPerHistoryUpdateData = n.maxNodesPerHistoryUpdateData;
56
+ this.maxNodesPerRead = n.maxNodesPerRead;
57
+ this.maxNodesPerWrite = n.maxNodesPerWrite;
58
+ this.maxNodesPerMethodCall = n.maxNodesPerMethodCall;
59
+ this.maxNodesPerRegisterNodes = n.maxNodesPerRegisterNodes;
60
+ this.maxNodesPerNodeManagement = n.maxNodesPerNodeManagement;
61
+ this.maxMonitoredItemsPerCall = n.maxMonitoredItemsPerCall;
62
+ this.maxNodesPerHistoryUpdateEvents = n.maxNodesPerHistoryUpdateEvents;
63
+ this.maxNodesPerTranslateBrowsePathsToNodeIds = n.maxNodesPerTranslateBrowsePathsToNodeIds;
64
+ this.registerToDiscovery = n.registerToDiscovery;
65
+ this.constructDefaultAddressSpace = n.constructDefaultAddressSpace;
66
+
67
+ this.maxSessions = Math.max(10,n.maxSessions); // Always have minimum of 10 sessions.
68
+
69
+
70
+ var autoAcceptUnknownCertificate = n.autoAcceptUnknownCertificate;
71
+ var maxConnectionsPerEndpoint = 20;
72
+ if (n.maxConnectionsPerEndpoint > 20) {
73
+ maxConnectionsPerEndpoint = n.maxConnectionsPerEndpoint;
74
+ }
75
+ var maxMessageSize = 4096;
76
+ if (n.maxMessageSize > 0) {
77
+ maxMessageSize = n.maxMessageSize;
78
+ }
79
+ var maxBufferSize = 4096;
80
+ if (n.maxBufferSize > 0) {
81
+ maxBufferSize = n.maxBufferSize;
82
+ }
83
+ var node = this;
84
+ var variables = { Counter: 0 };
85
+ var variablesTs = { Counter: 0 };
86
+ var variablesStatus = { Counter: 0 };
87
+ var equipmentCounter = 0;
88
+ var physicalAssetCounter = 0;
89
+ var equipment;
90
+ var physicalAssets;
91
+ var vendorName;
92
+ var myVar2 = new opcua.Variant({dataType: opcua.DataType.Double, value: 0.0});
93
+ var freeMem = new opcua.Variant({dataType: opcua.DataType.Double, value: 0.0});
94
+ var equipmentNotFound = true;
95
+ var initialized = false;
96
+ var folder = null;
97
+ let userManager; // users with username, password and role
98
+ let users = [{ username: "", password: "", roles: "" }]; // Empty as default
99
+ let savedAddressSpace = "";
100
+
101
+ if (node.usersFile && node.usersFile.length > 0) {
102
+ verbose_log(chalk.yellow("Trying to load default users from file: ") + chalk.cyan(node.usersFile) + chalk.yellow(" folder: ") + chalk.cyan(__dirname));
103
+ if (fs.existsSync(node.usersFile)) {
104
+ users = JSON.parse(fs.readFileSync(node.usersFile, "utf8"));
105
+ verbose_log(chalk.green("Loaded users: ") + chalk.cyan(JSON.stringify(users)));
106
+ setUsers();
107
+ }
108
+ else {
109
+ // Current working directory
110
+ let fileName = path.join(process.cwd(), node.usersFile);
111
+ verbose_log(chalk.yellow("Trying to load default users from file: ") + chalk.cyan(node.usersFile) + chalk.yellow(" folder: ") + chalk.cyan(fileName));
112
+ if (fs.existsSync(fileName)) {
113
+ users = JSON.parse(fs.readFileSync(fileName, "utf8"));
114
+ verbose_log(chalk.green("Loaded users: ") + chalk.cyan(JSON.stringify(users)));
115
+ setUsers();
116
+ }
117
+ else {
118
+ let fileName = path.join(process.cwd(), ".node-red", node.usersFile);
119
+ verbose_log(chalk.yellow("Trying to load default users from file: ") + chalk.cyan(node.usersFile) + chalk.yellow(" folder: ") + chalk.cyan(fileName));
120
+ if (fs.existsSync(fileName)) {
121
+ users = JSON.parse(fs.readFileSync(fileName, "utf8"));
122
+ verbose_log(chalk.green("Loaded users: ") + chalk.cyan(JSON.stringify(users)));
123
+ setUsers();
124
+ }
125
+ else {
126
+ verbose_log(chalk.red("File: " + node.usersFile + " not found! You can inject users to server or add file to folder: " + fileName));
127
+ node.error("File: " + node.usersFile + " not found! You can inject users to server or add file to folder: " + fileName);
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ // Server endpoints active configuration
134
+ var policies = [];
135
+ var modes = [];
136
+
137
+ // Security modes None | Sign | SignAndEncrypt
138
+ if (this.endpointNone === true) {
139
+ policies.push(opcua.SecurityPolicy.None);
140
+ modes.push(opcua.MessageSecurityMode.None);
141
+ }
142
+ if (this.endpointSign === true) {
143
+ modes.push(opcua.MessageSecurityMode.Sign);
144
+ }
145
+ if (this.endpointSignEncrypt === true) {
146
+ modes.push(opcua.MessageSecurityMode.SignAndEncrypt);
147
+ }
148
+ // Security policies
149
+ if (this.endpointBasic128Rsa15 === true) {
150
+ policies.push(opcua.SecurityPolicy.Basic128Rsa15);
151
+ }
152
+ if (this.endpointBasic256 === true) {
153
+ policies.push(opcua.SecurityPolicy.Basic256);
154
+ }
155
+ if (this.endpointBasic256Sha256 === true) {
156
+ policies.push(opcua.SecurityPolicy.Basic256Sha256);
157
+ }
158
+
159
+ // This should be possible to inject for server
160
+ function setUsers() {
161
+ // User manager
162
+ userManager = {
163
+ isValidUser: (username, password) => {
164
+ const uIndex = users.findIndex(function(u) { return u.username === username; });
165
+ if (uIndex < 0) {
166
+ // console.log(chalk.red("No such user:" + username));
167
+ return false;
168
+ }
169
+ if (users[uIndex].password !== password) {
170
+ // console.log(chalk.red("Wrong password for username: " + username + " tried with wrong password:" + password));
171
+ return false;
172
+ }
173
+ // console.log(chalk.green("Login successful for username: " + username));
174
+ return true;
175
+ },
176
+ getUserRoles: (username) => {
177
+ if (username === "Anonymous" || username === "anonymous") {
178
+ return opcua.makeRoles(opcua.WellKnownRoles.Anonymous);
179
+ }
180
+ const uIndex = users.findIndex(function(x) { return x.username === username; });
181
+ if (uIndex < 0) {
182
+ // Check this TODO
183
+ return opcua.makeRoles("AuthenticatedUser"); // opcua.WellKnownRoles.Anonymous; // by default were guest! ( i.e anonymous), read-only access
184
+ }
185
+ let userRoles;
186
+ if (users[uIndex].hasOwnProperty("roles")) {
187
+ userRoles = users[uIndex].roles; // user can have multiple roles Observer;Engineer
188
+ }
189
+ else {
190
+ console.error("Your users.json is missing roles field for user role! Using Anonymous as default role.");
191
+ return opcua.makeRoles(opcua.WellKnownRoles.Anonymous); // By default use Anonymous
192
+ }
193
+ return opcua.makeRoles(userRoles);
194
+ }
195
+ };
196
+ }
197
+
198
+ function node_error(err) {
199
+ // console.error(chalk.red("[Error] Server node error on: " + node.name + " error: " + JSON.stringify(err)));
200
+ node.error("Server node error on: " + node.name + " error: " + JSON.stringify(err));
201
+ }
202
+
203
+ function verbose_warn(logMessage) {
204
+ //if (RED.settings.verbose) {
205
+ // console.warn(chalk.yellow("[Warning] "+ (node.name) ? node.name + ': ' + logMessage : 'OpcUaServerNode: ' + logMessage));
206
+ node.warn((node.name) ? node.name + ': ' + logMessage : 'OpcUaServerNode: ' + logMessage);
207
+ //}
208
+ }
209
+
210
+ function verbose_log(logMessage) {
211
+ //if (RED.settings.verbose) {
212
+ // console.log(chalk.cyan(logMessage));
213
+ node.debug(logMessage);
214
+ //}
215
+ }
216
+
217
+ // Method input / output argument types from string to opcua DataType
218
+ function getUaDatatype(methodArgType) {
219
+ if (methodArgType === "String") {
220
+ return opcua.DataType.String;
221
+ }
222
+ if (methodArgType === "Byte") {
223
+ return opcua.DataType.Byte;
224
+ }
225
+ if (methodArgType === "SByte") {
226
+ return opcua.DataType.SByte;
227
+ }
228
+ if (methodArgType === "UInt16") {
229
+ return opcua.DataType.UInt32;
230
+ }
231
+ if (methodArgType === "UInt32") {
232
+ return opcua.DataType.UInt32;
233
+ }
234
+ if (methodArgType === "UInt64") {
235
+ return opcua.DataType.UInt64;
236
+ }
237
+ if (methodArgType === "Int16") {
238
+ return opcua.DataType.Int32;
239
+ }
240
+ if (methodArgType === "Int32") {
241
+ return opcua.DataType.Int32;
242
+ }
243
+ if (methodArgType === "Double") {
244
+ return opcua.DataType.Double;
245
+ }
246
+ if (methodArgType === "Float") {
247
+ return opcua.DataType.Float;
248
+ }
249
+ node.error("Cannot convert given argument: " + methodArgType + " to OPC UA DataType!");
250
+ }
251
+
252
+ node.status({
253
+ fill: "red",
254
+ shape: "ring",
255
+ text: "Not running"
256
+ });
257
+
258
+ var xmlFiles = [path.join(__dirname, 'public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.xml'), // Standard & basic types
259
+ path.join(__dirname, 'public/vendor/opc-foundation/xml/Opc.Ua.Di.NodeSet2.xml'), // Support for DI Device Information model
260
+ path.join(__dirname, 'public/vendor/opc-foundation/xml/Opc.Ua.AutoID.NodeSet2.xml'), // Support for RFID Readers
261
+ path.join(__dirname, 'public/vendor/opc-foundation/xml/Opc.ISA95.NodeSet2.xml') // ISA95
262
+ ];
263
+ if (savedAddressSpace && savedAddressSpace.length>0) {
264
+ xmlFiles.push(savedAddressSpace);
265
+ }
266
+ // Add custom nodesets (xml-files) for server
267
+ if (node.nodesetDir && fs.existsSync(node.nodesetDir)) {
268
+ fs.readdirSync(node.nodesetDir).forEach(fileName => {
269
+ if (path.extname(fileName).toLowerCase() === '.xml') {
270
+ xmlFiles.push(path.join(node.nodesetDir, fileName));
271
+ };
272
+ });
273
+ }
274
+ verbose_log(chalk.yellow("NodeSet: ") + chalk.cyan(xmlFiles.toString()));
275
+ let serverPort = parseInt(n.port);
276
+ if (process.env.SERVER_PORT) {
277
+ serverPort = parseInt(process.env.SERVER_PORT);
278
+ }
279
+ async function initNewServer() {
280
+ initialized = false;
281
+ verbose_log(chalk.yellow("Create Server from XML..."));
282
+ // DO NOT USE "%FQDN%" anymore, hostname is OK
283
+ const applicationUri = opcua.makeApplicationUrn(os.hostname(), "node-red-contrib-opcua-server");
284
+ verbose_log(chalk.yellow("ApplicationUrn: ") + chalk.cyan(applicationUri));
285
+ verbose_log(chalk.yellow("Server certificate manager"));
286
+ const serverCertificateManager = createServerCertificateManager(autoAcceptUnknownCertificate);
287
+ verbose_log(chalk.yellow("User Certificate manager"));
288
+ const userCertificateManager = createUserCertificateManager();
289
+ verbose_log(chalk.yellow("Initializing certificate managers"));
290
+ await serverCertificateManager.initialize();
291
+ await userCertificateManager.initialize();
292
+ verbose_log(chalk.green("Certificate managers initialized"));
293
+ var registerMethod = null;
294
+ if (node.registerToDiscovery === true) {
295
+ registerMethod = opcua.RegisterServerMethod.LDS;
296
+ }
297
+ node.server_options = {
298
+ serverCertificateManager,
299
+ userCertificateManager,
300
+ securityPolicies: policies,
301
+ securityModes: modes,
302
+ allowAnonymous: n.allowAnonymous,
303
+ port: serverPort,
304
+ resourcePath: "/" + node.endpoint, // Option was missing / can be
305
+ // maxAllowedSessionNumber: 1000,
306
+ maxConnectionsPerEndpoint: maxConnectionsPerEndpoint,
307
+ maxMessageSize: maxMessageSize,
308
+ maxBufferSize: maxBufferSize,
309
+ nodeset_filename: xmlFiles,
310
+ serverInfo: {
311
+ applicationUri,
312
+ productUri: "Node-RED NodeOPCUA-Server",
313
+ // applicationName: { text: "Mini NodeOPCUA Server", locale: "en" }, // Set later
314
+ gatewayServerUri: null,
315
+ discoveryProfileUri: null,
316
+ discoveryUrls: []
317
+ },
318
+ buildInfo: {
319
+ buildNumber: "",
320
+ buildDate: ""
321
+ },
322
+ serverCapabilities: {
323
+ maxBrowseContinuationPoints: 10,
324
+ maxHistoryContinuationPoints: 10,
325
+ maxSessions: node.maxSessions,
326
+ // maxInactiveLockTime,
327
+ // Get these from the node parameters
328
+ operationLimits: {
329
+ maxNodesPerBrowse: node.maxNodesPerBrowse,
330
+ maxNodesPerHistoryReadData: node.maxNodesPerHistoryReadData,
331
+ maxNodesPerHistoryReadEvents: node.maxNodesPerHistoryReadEvents,
332
+ maxNodesPerHistoryUpdateData: node.maxNodesPerHistoryUpdateData,
333
+ maxNodesPerRead: node.maxNodesPerRead,
334
+ maxNodesPerWrite: node.maxNodesPerWrite,
335
+ maxNodesPerMethodCall: node.maxNodesPerMethodCall,
336
+ maxNodesPerRegisterNodes: node.maxNodesPerRegisterNodes,
337
+ maxNodesPerNodeManagement: node.maxNodesPerNodeManagement,
338
+ maxMonitoredItemsPerCall: node.maxMonitoredItemsPerCall,
339
+ maxNodesPerHistoryUpdateEvents: node.maxNodesPerHistoryUpdateEvents,
340
+ maxNodesPerTranslateBrowsePathsToNodeIds: node.maxNodesPerTranslateBrowsePathsToNodeIds
341
+ }
342
+ },
343
+ userManager, // users with username, password & role, see file users.json
344
+ isAuditing: false,
345
+ registerServerMethod: registerMethod
346
+ };
347
+ node.server_options.serverInfo = {
348
+ applicationName: { text: "Node-RED OPCUA" }
349
+ };
350
+
351
+ node.server_options.buildInfo = {
352
+ buildNumber: "0.2.354",
353
+ buildDate: "2026-07-02T12:51:00"
354
+ };
355
+
356
+ var hostname = os.hostname();
357
+ var discovery_server_endpointUrl = "opc.tcp://" + hostname + ":4840"; // /UADiscovery"; // Do not use resource path
358
+ if (node.registerToDiscovery === true) {
359
+ verbose_log("Registering server to :" + discovery_server_endpointUrl);
360
+ }
361
+ }
362
+
363
+ function construct_my_address_space(addressSpace) {
364
+ verbose_log(chalk.yellow("Server: add VendorName ..."));
365
+ vendorName = addressSpace.getOwnNamespace().addObject({
366
+ organizedBy: addressSpace.rootFolder.objects,
367
+ nodeId: "ns=1;s=VendorName",
368
+ browseName: "VendorName"
369
+ });
370
+ equipment = addressSpace.getOwnNamespace().addObject({
371
+ organizedBy: vendorName,
372
+ nodeId: "ns=1;s=Equipment",
373
+ browseName: "Equipment"
374
+ });
375
+
376
+ physicalAssets = addressSpace.getOwnNamespace().addObject({
377
+ organizedBy: vendorName,
378
+ nodeId: "ns=1;s=PhysicalAssets",
379
+ browseName: "Physical Assets"
380
+ });
381
+
382
+ verbose_log(chalk.yellow('Server: add MyVariable2 ...'));
383
+ var variable2 = 10.0;
384
+ try {
385
+ myVar2 = new opcua.Variant({dataType: opcua.DataType.Double, value: variable2});
386
+ addressSpace.getOwnNamespace().addVariable({
387
+ componentOf: vendorName,
388
+ nodeId: "ns=1;s=MyVariable2",
389
+ browseName: "MyVariable2",
390
+ dataType: "Double",
391
+ minimumSamplingInterval: 500,
392
+ value: new opcua.Variant({ dataType: opcua.DataType.Double, value: variable2 })
393
+ /*
394
+ {
395
+ get: () => {
396
+ // return myVar2;
397
+ return new opcua.Variant({ dataType: opcua.DataType.Double, value: variable2 });
398
+ },
399
+ set: function (variant) {
400
+ myVar2.value = parseFloat(variant.value);
401
+ return opcua.StatusCodes.Good;
402
+ }
403
+ }
404
+ */
405
+ });
406
+ }
407
+ catch(error) {
408
+ node_error("Cannot add variable; MyVariable2, error: " + error);
409
+ }
410
+ verbose_log(chalk.yellow('Server: add FreeMemory ...'));
411
+ try {
412
+ freeMem = new opcua.Variant({
413
+ dataType: opcua.DataType.Double,
414
+ value: available_memory()
415
+ });
416
+ node.freeMem = addressSpace.getOwnNamespace().addVariable({
417
+ componentOf: vendorName,
418
+ nodeId: "ns=1;s=FreeMemory",
419
+ browseName: "FreeMemory",
420
+ dataType: "Double",
421
+ minimumSamplingInterval: 500,
422
+ value: new opcua.Variant({
423
+ dataType: opcua.DataType.Double,
424
+ value: available_memory()
425
+ })
426
+ /*
427
+ {
428
+ get: function () {
429
+ freeMem.value = available_memory();
430
+ // return freeMem;
431
+ return new opcua.Variant({
432
+ dataType: opcua.DataType.Double,
433
+ value: available_memory()
434
+ });
435
+ }
436
+ }
437
+ */
438
+ });
439
+ }
440
+ catch (error) {
441
+ node_error("Cannot add variable; freeMem, error: " + error)
442
+ }
443
+ verbose_log(chalk.yellow('Server: add Counter ...'));
444
+ try {
445
+ node.vendorName = addressSpace.getOwnNamespace().addVariable({
446
+ componentOf: vendorName,
447
+ nodeId: "ns=1;s=Counter",
448
+ browseName: "Variables Counter",
449
+ displayName: "Variables Counter",
450
+ dataType: "UInt16",
451
+ minimumSamplingInterval: 500,
452
+ value: new opcua.Variant({
453
+ dataType: opcua.DataType.UInt16,
454
+ value: Object.keys(variables).length // Counter will show amount of created variables
455
+ })
456
+ /*
457
+ {
458
+ get: function () {
459
+ return new opcua.Variant({
460
+ dataType: opcua.DataType.UInt16,
461
+ value: Object.keys(variables).length // Counter will show amount of created variables
462
+ });
463
+ }
464
+ }
465
+ */
466
+ });
467
+ }
468
+ catch (error) {
469
+ node_error("Cannot add variable; Counter, error: " + error);
470
+ }
471
+
472
+ var method = addressSpace.getOwnNamespace().addMethod(
473
+ vendorName, {
474
+ browseName: "Bark",
475
+
476
+ inputArguments: [{
477
+ name: "nbBarks",
478
+ description: {
479
+ text: "specifies the number of time I should bark"
480
+ },
481
+ dataType: opcua.DataType.UInt32
482
+ }, {
483
+ name: "volume",
484
+ description: {
485
+ text: "specifies the sound volume [0 = quiet ,100 = loud]"
486
+ },
487
+ dataType: opcua.DataType.UInt32
488
+ }],
489
+
490
+ outputArguments: [{
491
+ name: "Barks",
492
+ description: {
493
+ text: "the generated barks"
494
+ },
495
+ dataType: opcua.DataType.String,
496
+ valueRank: 1
497
+ }]
498
+ });
499
+
500
+ method.bindMethod(function (inputArguments, context, callback) {
501
+
502
+ var nbBarks = inputArguments[0].value;
503
+ var volume = inputArguments[1].value;
504
+
505
+ verbose_log("Hello World ! I will bark ", nbBarks, " times");
506
+ verbose_log("the requested volume is ", volume, "");
507
+ var sound_volume = new Array(volume).join("!");
508
+
509
+ var barks = [];
510
+ for (var i = 0; i < nbBarks; i++) {
511
+ barks.push("Whaff" + sound_volume);
512
+ }
513
+
514
+ var callMethodResult = {
515
+ statusCode: opcua.StatusCodes.Good,
516
+ outputArguments: [{
517
+ dataType: opcua.DataType.String,
518
+ arrayType: opcua.VariantArrayType.Array,
519
+ value: barks
520
+ }]
521
+ };
522
+ callback(null, callMethodResult);
523
+ });
524
+ }
525
+
526
+ function available_memory() {
527
+ return os.freemem() / os.totalmem() * 100.0;
528
+ }
529
+
530
+ (async () => {
531
+ try {
532
+ await initNewServer(); // Read & set parameters
533
+ node.server = new opcua.OPCUAServer(node.server_options);
534
+
535
+ await node.server.initialize();
536
+ if (node.constructDefaultAddressSpace === true) {
537
+ construct_my_address_space(node.server.engine.addressSpace);
538
+ }
539
+ await node.server.start();
540
+
541
+ verbose_log(chalk.yellow("Using server certificate ") + chalk.cyan(node.server.certificateFile));
542
+ verbose_log(chalk.yellow("Using PKI folder ") + chalk.cyan(node.server.serverCertificateManager.rootDir));
543
+ verbose_log(chalk.yellow("Using UserPKI folder ") + chalk.cyan(node.server.userCertificateManager.rootDir));
544
+ verbose_log(chalk.yellow("Trusted certificate folder ") + chalk.cyan(node.server.serverCertificateManager.trustedFolder));
545
+ verbose_log(chalk.yellow("Rejected certificate folder ") + chalk.cyan(node.server.serverCertificateManager.rejectedFolder));
546
+
547
+ // Needed for Alarms and Conditions
548
+ node.server.engine.addressSpace.installAlarmsAndConditionsService();
549
+ opcua.addAggregateSupport(node.server.engine.addressSpace);
550
+ // Client connects with userName
551
+ node.server.on("session_activated", (session) => {
552
+ if (session.userIdentityToken && session.userIdentityToken.userName) {
553
+ var msg = {};
554
+ msg.topic="Username";
555
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString();
556
+ node.send(msg);
557
+ }
558
+ });
559
+ // Client connected
560
+ node.server.on("create_session", function(session) {
561
+ var msg = {};
562
+ msg.topic="Client-connected";
563
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString();
564
+ node.send(msg);
565
+ });
566
+ // Client disconnected
567
+ node.server.on("session_closed", function(session, reason) {
568
+ node.debug(chalk.yellow("Reason: ") + chalk.cyan(reason));
569
+ var msg = {};
570
+ msg.topic="Client-disconnected";
571
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString() + " " + session.sessionName ? session.sessionName.toString() : "<null>";
572
+ node.send(msg);
573
+ });
574
+ node.status({
575
+ fill: "green",
576
+ shape: "dot",
577
+ text: "running"
578
+ });
579
+ initialized = true;
580
+ }
581
+ catch (err) {
582
+ var msg = {};
583
+ msg.error = {};
584
+ msg.error.message = "Disconnect error: " + err;
585
+ msg.error.source = this;
586
+ node.error("Disconnect error: ", msg);
587
+ }
588
+ })();
589
+
590
+ //######################################################################################
591
+ node.on("input", function (msg) {
592
+ try {
593
+ verbose_log(JSON.stringify(msg));
594
+ } catch (e) {
595
+ verbose_log(JSON.stringify(msg.payload));
596
+ }
597
+ if (!node.server || !initialized) {
598
+ node_error("Server is not running");
599
+ return false;
600
+ }
601
+ var payload = msg.payload;
602
+ // Support both single object and array
603
+ if (contains_necessaryProperties(msg)) {
604
+ // Always process as array - wrap single objects
605
+ var payloadArray = Array.isArray(payload) ? payload : [payload];
606
+ read_messages(payloadArray);
607
+ } else {
608
+ if (Array.isArray(payload)) {
609
+ node.warn('warning: one or more items in the array are missing properties like messageType, namespace, variableName or variableValue.');
610
+ } else {
611
+ node.warn('warning: properties like messageType, namespace, variableName or VariableValue is missing.');
612
+ }
613
+ }
614
+
615
+ if (contains_opcua_command(payload)) {
616
+ msg.payload = execute_opcua_command(msg);
617
+ }
618
+
619
+ if (equipmentNotFound) {
620
+ var addressSpace = node.server.engine.addressSpace; // node.addressSpace;
621
+ if (addressSpace === undefined || addressSpace === null) {
622
+ node_error("addressSpace undefined");
623
+ return false;
624
+ }
625
+
626
+ if (node.constructDefaultAddressSpace === true) {
627
+ var rootFolder = addressSpace.findNode("ns=1;s=VendorName");
628
+ if (!rootFolder) {
629
+ node_error("VerdorName not found!");
630
+ return false;
631
+ }
632
+ var references = rootFolder.findReferences("Organizes", true);
633
+
634
+ if (findReference(references, equipment.nodeId)) {
635
+ verbose_log("Equipment Reference found in VendorName");
636
+ equipmentNotFound = false;
637
+ } else {
638
+ verbose_warn("Equipment Reference not found in VendorName");
639
+ }
640
+ }
641
+ }
642
+
643
+ node.send(msg);
644
+ });
645
+
646
+ function findReference(references, nodeId) {
647
+ return references.filter(function (r) {
648
+ return r.nodeId.toString() === nodeId.toString();
649
+ });
650
+ }
651
+ // check json object - modify 5/03/2022
652
+ function contains_messageType(payload) {
653
+ return payload.hasOwnProperty('messageType');
654
+ }
655
+ function contains_namespace(payload) {
656
+ if (!payload.hasOwnProperty('namespace'))
657
+ node.warn("Mandatory parameter 'namespace' is missing")
658
+ return payload.hasOwnProperty('namespace');
659
+ }
660
+
661
+ function contains_variableName(payload) {
662
+ if (!payload.hasOwnProperty('variableName'))
663
+ node.warn("Mandatory parameter 'variableName' missing");
664
+ return payload.hasOwnProperty('variableName');
665
+ }
666
+
667
+ function contains_variableValue(payload) {
668
+ if (!payload.hasOwnProperty('variableValue'))
669
+ node.warn("Optional parameter 'variableValue' missing")
670
+ return payload.hasOwnProperty('variableValue');
671
+ }
672
+
673
+ function contains_necessaryProperties(msg) {
674
+ // Check if payload is an array
675
+ if (Array.isArray(msg.payload)) {
676
+ // For arrays, check that all elements have necessary properties
677
+ return msg.payload.every(item => {
678
+ return contains_messageType(item) &&
679
+ contains_namespace(item) &&
680
+ contains_variableName(item) &&
681
+ contains_variableValue(item);
682
+ });
683
+ }
684
+ // Original single object logic
685
+ if (contains_messageType(msg.payload)) {
686
+ return(contains_namespace(msg.payload) &&
687
+ contains_variableName(msg.payload) &&
688
+ contains_variableValue(msg.payload));
689
+ }
690
+ else {
691
+ if (msg.payload.hasOwnProperty('opcuaCommand') && msg.payload.opcuaCommand === "addVariable") {
692
+ // msg.topic with nodeId and datatype
693
+ if (msg.topic.indexOf("ns=")>=0 && msg.topic.indexOf("datatype=")>0) {
694
+ return true;
695
+ }
696
+ else {
697
+ node.warn("msg.topic must contain nodeId and datatype!");
698
+ }
699
+ }
700
+ else {
701
+ return contains_opcua_command(msg.payload);
702
+ }
703
+ }
704
+ }
705
+
706
+ function read_messages(payloadArray) {
707
+ if (!Array.isArray(payloadArray)) {
708
+ node.error("read_messages expects an array");
709
+ return;
710
+ }
711
+
712
+ var addressSpace = node.server.engine.addressSpace;
713
+ var nodesToWrite = [];
714
+
715
+ // Process all variable updates
716
+ for (var i = 0; i < payloadArray.length; i++) {
717
+ var payload = payloadArray[i];
718
+
719
+ switch (payload.messageType) {
720
+ case 'Variable':
721
+ var ns = payload.namespace.toString();
722
+ const variableId = `${ns}:${payload.variableName}`;
723
+
724
+ verbose_log("BEFORE: " + ns + ":" + payload.variableName + " value: " + JSON.stringify(variables[variableId]));
725
+
726
+ var value = payload.variableValue;
727
+ if (payload.variableValue === "true" || payload.variableValue === true || payload.variableValue === 1) {
728
+ value = true;
729
+ }
730
+ if (payload.variableValue === "false" || payload.variableValue === false || payload.variableValue === 0) {
731
+ value = false;
732
+ }
733
+ variables[variableId] = value;
734
+
735
+ // Find the variable node
736
+ var vnode = null;
737
+ if(typeof(payload.variableName) === 'number') {
738
+ verbose_log("findNode(ns="+ns+";i="+payload.variableName);
739
+ vnode = addressSpace.findNode("ns="+ns+";i="+payload.variableName);
740
+ if(vnode === null) {
741
+ verbose_warn("vnode is null, findNode did not succeeded");
742
+ }
743
+ }
744
+ else {
745
+ // if( typeof(payload.variableName)==='string')
746
+ // this must be string - a plain variable name
747
+ // TODO opaque
748
+ verbose_log("findNode ns="+ns+";s="+payload.variableName);
749
+ vnode = addressSpace.findNode("ns="+ns+";s="+payload.variableName);
750
+ }
751
+
752
+ if (vnode) {
753
+ verbose_log("Found variable, nodeId: " + vnode.nodeId);
754
+
755
+ variables[variableId] = opcuaBasics.build_new_value_by_datatype(payload.datatype, payload.variableValue);
756
+ // var newValue = opcuaBasics.build_new_variant(payload.datatype, payload.variableValue);
757
+ var newValue = opcuaBasics.build_new_dataValue(payload.datatype, payload.variableValue);
758
+ vnode.setValueFromSource(newValue); // This fixes if variable if not bound eq. bindVariables is not called
759
+
760
+ // Check if this item has quality and timestamp
761
+ if (payload.quality && payload.sourceTimestamp) {
762
+ // var statusCode = opcua.StatusCodes.BadDeviceFailure;
763
+ // var statusCode = opcua.StatusCodes.BadDataLost;
764
+ // Bad 0x80000000
765
+ if(typeof(payload.quality)==='string') {
766
+ // a name of Quality was given -> convert it to number
767
+ verbose_log("Getting numeric status code of quality: " + payload.quality);
768
+ payload.quality = opcua.StatusCodes[payload.quality].value;
769
+ }
770
+ // else // typeof(payload.quality)==='number', e.g. 2161770496
771
+ var statusCode = opcua.StatusCode.makeStatusCode(payload.quality);
772
+ verbose_log("StatusCode from value: " + payload.quality + " (0x" + payload.quality.toString(16) + ") description: " + statusCode.description);
773
+ var ts = new Date(payload.sourceTimestamp);
774
+ verbose_log("Timestamp: " + ts.toISOString());
775
+ verbose_log("Set variable, newValue:" + JSON.stringify(newValue) + " statusCode: " + statusCode.description + " sourceTimestamp: " + ts);
776
+ vnode.setValueFromSource(newValue, statusCode, ts);
777
+ // Dummy & quick fix for statusCode & timeStamp
778
+ variablesStatus[variableId] = statusCode;
779
+ variablesTs[variableId] = ts;
780
+
781
+ // Add to batch write array
782
+ nodesToWrite.push({
783
+ nodeId: vnode.nodeId,
784
+ attributeId: opcua.AttributeIds.Value,
785
+ value: new opcua.DataValue({
786
+ value: newValue,
787
+ statusCode: statusCode,
788
+ sourceTimestamp: ts
789
+ })
790
+ });
791
+ }
792
+
793
+ verbose_log("AFTER : " + ns + ":" + payload.variableName + " value: " + JSON.stringify(variables[variableId]));
794
+ } else {
795
+ node.error("Variable not found from server address space: " + payload.namespace + ":" + payload.variableName);
796
+ }
797
+ break;
798
+ default:
799
+ break;
800
+ }
801
+ }
802
+
803
+ // Perform batch write if there are nodes with quality/timestamp
804
+ if (nodesToWrite.length > 0) {
805
+ var session = new opcua.PseudoSession(addressSpace);
806
+ verbose_log("Batch writing " + nodesToWrite.length + " nodes");
807
+ session.write(nodesToWrite, function (err, statusCodes) {
808
+ if (err) {
809
+ node.error("Batch write error: " + err);
810
+ } else {
811
+ verbose_log("Batch write succeeded for " + statusCodes.length + " nodes");
812
+ }
813
+ });
814
+ }
815
+ }
816
+
817
+ function contains_opcua_command(payload) {
818
+ return payload.hasOwnProperty('opcuaCommand');
819
+ }
820
+
821
+ function execute_opcua_command(msg) {
822
+ var payload = msg.payload;
823
+ var addressSpace = node.server.engine.addressSpace;
824
+ var name;
825
+ var returnValue = "";
826
+
827
+ switch (payload.opcuaCommand) {
828
+
829
+ case "restartOPCUAServer":
830
+ restart_server();
831
+ break;
832
+
833
+ case "addEquipment":
834
+ verbose_log(chalk.cyan("Adding node: ".concat(payload.nodeName)));
835
+ equipmentCounter++;
836
+ name = payload.nodeName.concat(equipmentCounter);
837
+
838
+ addressSpace.getOwnNamespace().addObject({
839
+ organizedBy: addressSpace.findNode(equipment.nodeId),
840
+ nodeId: "ns=1;s=".concat(name),
841
+ browseName: name
842
+ });
843
+ break;
844
+
845
+ case "addPhysicalAsset":
846
+ verbose_log(chalk.cyan("Adding node: ".concat(payload.nodeName)));
847
+ physicalAssetCounter++;
848
+ name = payload.nodeName.concat(physicalAssetCounter);
849
+
850
+ addressSpace.getOwnNamespace().addObject({
851
+ organizedBy: addressSpace.findNode(physicalAssets.nodeId),
852
+ nodeId: "ns=1;s=".concat(name),
853
+ browseName: name
854
+ });
855
+ break;
856
+
857
+ case "setFolder":
858
+ verbose_log(chalk.cyan("Set Folder: ".concat(msg.topic))); // Example topic format ns=4;s=FolderName
859
+ folder = addressSpace.findNode(msg.topic);
860
+ if (folder) {
861
+ verbose_log(chalk.yellow("Found folder: ") + chalk.cyan(folder));
862
+ }
863
+ else {
864
+ verbose_warn(chalk.red("Folder not found for topic: ") + chalk.cyan(msg.topic));
865
+ }
866
+ break;
867
+
868
+ case "addFolder":
869
+ var nodeId = msg.topic;
870
+ var description = "";
871
+ var d = msg.topic.indexOf("description=");
872
+ if (d > 0) {
873
+ nodeId = nodeId.substring(0, d - 1); // format is msg.topic="ns=1;s=TEST;description=MyTestFolder"
874
+ description = msg.topic.substring(d + 12);
875
+ if (description.indexOf(";") >= 0) {
876
+ description = description.substring(0, description.indexOf(";"));
877
+ }
878
+ }
879
+ var browseName = "";
880
+ var d = msg.topic.indexOf("browseName=");
881
+ if (d > 0) {
882
+ nodeId = msg.topic.substring(0, d - 1);
883
+ // format is msg.topic="ns=1;s=Main.Test;browseName=Test"
884
+ browseName = msg.topic.substring(d + 11);
885
+ if (browseName.indexOf(";") >= 0) {
886
+ browseName = browseName.substring(0, browseName.indexOf(";"));
887
+ verbose_log("Folder browseName: " + browseName);
888
+ }
889
+ }
890
+ verbose_log("Adding Folder: ".concat(nodeId)); // Example topic format ns=4;s=FolderName
891
+ var parentFolder = node.server.engine.addressSpace.rootFolder.objects;
892
+ if (folder) {
893
+ parentFolder = folder; // Use previously created folder as parentFolder or setFolder() can be used to set parentFolder
894
+ }
895
+ // Check & add from msg accessLevel userAccessLevel, role & permissions
896
+ var accessLevel = opcua.makeAccessLevelFlag("CurrentRead|CurrentWrite"); // Use as default
897
+ var userAccessLevel = opcua.makeAccessLevelFlag("CurrentRead|CurrentWrite"); // Use as default
898
+ if (msg.accessLevel) {
899
+ accessLevel = msg.accessLevel;
900
+ }
901
+ if (msg.userAccessLevel) {
902
+ userAccessLevel = msg.userAccessLevel;
903
+ }
904
+ // permissions collected from multiple opcua-rights
905
+ let permissions = [
906
+ { roleId: opcua.WellKnownRoles.Anonymous, permissions: opcua.allPermissions },
907
+ { roleId: opcua.WellKnownRoles.AuthenticatedUser, permissions: opcua.allPermissions },
908
+ ];
909
+ if (msg.permissions) {
910
+ permissions = msg.permissions;
911
+ }
912
+
913
+ // Own namespace
914
+ if (nodeId.indexOf("ns=1;") >= 0) {
915
+ parentFolder = node.server.engine.addressSpace.rootFolder.objects;
916
+ if (folder) {
917
+ parentFolder = folder; // Use previously created folder as parentFolder or setFolder() can be used to set parentFolder
918
+ }
919
+ if (browseName.length === 0) {
920
+ browseName = nodeId.substring(7);
921
+ }
922
+ console.log("### nodeId: " + nodeId + " parent: " + parentFolder.nodeId + " description: '" + description + "' browseName: '" + browseName + "'");
923
+ folder = addressSpace.getOwnNamespace().addObject({
924
+ organizedBy: addressSpace.findNode(parentFolder.nodeId),
925
+ nodeId: nodeId, // msg.topic,
926
+ description: description || "",
927
+ accessLevel: accessLevel, // TEST more
928
+ userAccessLevel: userAccessLevel, // TEST more
929
+ rolePermissions: [].concat(permissions),
930
+ accessRestrictions: opcua.AccessRestrictionsFlag.None, // TODO from msg
931
+ browseName: browseName // || nodeId.substring(7) // browseName cannot be empty
932
+ });
933
+ }
934
+ else {
935
+ verbose_log("Topic: " + nodeId + " index: " + nodeId.substring(3));
936
+ const index = parseInt(nodeId.substring(3));
937
+ verbose_log("ns index: " + index);
938
+ const uri = addressSpace.getNamespaceUri(index);
939
+ verbose_log("ns uri: " + uri);
940
+ const ns = addressSpace.getNamespace(uri); // Or index
941
+ var name = nodeId; // msg.topic;
942
+ var browseName = name; // Use full name by default
943
+ // NodeId can be string or integer or Guid or Opaque: ns=10;i=1000 or ns=5;g=
944
+ var bIndex = name.indexOf(";s="); // String
945
+ if (bIndex>0) {
946
+ browseName = name.substring(bIndex+3);
947
+ }
948
+ bIndex = name.indexOf(";i="); // Integer
949
+ if (bIndex>0) {
950
+ browseName = name.substring(bIndex+3);
951
+ }
952
+ bIndex = name.indexOf(";g="); // Guid
953
+ if (bIndex>0) {
954
+ browseName = name.substring(bIndex+3);
955
+ }
956
+ bIndex = name.indexOf(";b="); // Opaque base64
957
+ if (bIndex>0) {
958
+ browseName = name.substring(bIndex+3);
959
+ }
960
+ folder = ns.addObject({
961
+ organizedBy: addressSpace.findNode(parentFolder.nodeId),
962
+ nodeId: nodeId, // msg.topic,
963
+ description: description,
964
+ accessLevel: accessLevel, // TEST more
965
+ userAccessLevel: userAccessLevel, // TEST more
966
+ rolePermissions: [].concat(permissions),
967
+ accessRestrictions: opcua.AccessRestrictionsFlag.None, // TODO from msg
968
+ browseName: browseName // msg.topic.substring(msg.topic.indexOf(";s=")+3)
969
+ })
970
+ }
971
+ break;
972
+
973
+ case "addVariable":
974
+ verbose_log("Adding node: ".concat(msg.topic)); // Example topic format ns=4;s=VariableName;datatype=Double
975
+ var datatype = "";
976
+ var description = "";
977
+ var displayName = "";
978
+ var browseNameTopic = "";
979
+ var opcuaDataType = null;
980
+ var e = msg.topic.indexOf("datatype=");
981
+ if (e<0) {
982
+ node_error("no datatype=Float or other type in addVariable ".concat(msg.topic)); // Example topic format ns=4;s=FolderName
983
+ }
984
+ var parentFolder = node.server.engine.addressSpace.rootFolder.objects;
985
+ if (folder != null) {
986
+ parentFolder = folder; // Use previous folder as parent or setFolder() can be use to set parent
987
+ }
988
+ var d = msg.topic.indexOf("description=");
989
+ if (d > 0) {
990
+ description = msg.topic.substring(d + 12);
991
+ if (description.indexOf(";") >= 0) {
992
+ description = description.substring(0, description.indexOf(";"));
993
+ }
994
+ }
995
+ var d = msg.topic.indexOf("browseName=");
996
+ if (d > 0) {
997
+ browseNameTopic = msg.topic.substring(d + 11);
998
+ if (browseNameTopic.indexOf(";") >= 0) {
999
+ browseNameTopic = browseNameTopic.substring(0, browseNameTopic.indexOf(";"));
1000
+ }
1001
+ }
1002
+ const dn = msg.topic.indexOf("displayName=");
1003
+ if (dn > 0) {
1004
+ displayName = msg.topic.substring(dn + 12);
1005
+ // console.log(displayName);
1006
+ if (displayName.indexOf(";") >= 0) {
1007
+ displayName = displayName.substring(0, displayName.indexOf(";"));
1008
+ }
1009
+ }
1010
+ if (e > 0) {
1011
+ name = msg.topic.substring(0, e - 1);
1012
+ datatype = msg.topic.substring(e + 9);
1013
+ // ExtentionObject contains extra info like typeId
1014
+ if (datatype.indexOf(";") >= 0) {
1015
+ datatype = datatype.substring(0, datatype.indexOf(";"));
1016
+ }
1017
+ var arrayType = opcua.VariantArrayType.Scalar;
1018
+ var arr = datatype.indexOf("Array");
1019
+ var dim1 = 0; // Fix for the scalars
1020
+ var dim2 = 0; // Matrix
1021
+ var dim3 = 0; // Cube
1022
+ var indexStr = "";
1023
+ var valueRank = -1; // Fix for the scalars
1024
+ if (arr > 0) {
1025
+ arrayType = opcua.VariantArrayType.Array;
1026
+ dim1 = datatype.substring(arr+6);
1027
+ indexStr = dim1.substring(0, dim1.length-1);
1028
+ dim1 = parseInt(dim1.substring(0, dim1.length-1));
1029
+ valueRank = 1; // 1-dim Array
1030
+ datatype = datatype.substring(0, arr);
1031
+ // valueRank = 2; // 2-dim Matrix FloatArray[5,5]
1032
+ // valueRank = 3; // 3-dim Matrix FloatArray[5,5,5]
1033
+ var indexes = indexStr.split(",");
1034
+ node.debug("INDEXES[" + indexes.length + "] = " + JSON.stringify(indexes) + " from " + indexStr);
1035
+ if (indexes.length === 1) {
1036
+ dim1 = parseInt(indexes[0]);
1037
+ valueRank = 1;
1038
+ }
1039
+ if (indexes.length === 2) {
1040
+ dim1 = parseInt(indexes[0]);
1041
+ dim2 = parseInt(indexes[1]);
1042
+ valueRank = 2;
1043
+ }
1044
+ if (indexes.length === 3) {
1045
+ dim1 = parseInt(indexes[0]);
1046
+ dim2 = parseInt(indexes[1]);
1047
+ dim3 = parseInt(indexes[2]);
1048
+ valueRank = 3;
1049
+ }
1050
+ }
1051
+
1052
+ var namespace = addressSpace.getOwnNamespace(); // Default
1053
+ var nsindex=1;
1054
+ if (msg.topic.indexOf("ns=1;") !== 0) {
1055
+ var allNamespaces = addressSpace.getNamespaceArray();
1056
+ // console.log("ALL ns: " + stringify(allNamespaces));
1057
+ // Select namespace by index
1058
+ nsindex = parseInt(msg.topic.substring(3));
1059
+ namespace = allNamespaces[nsindex];
1060
+ }
1061
+
1062
+ var ns = nsindex.toString();
1063
+ var dimensions = valueRank <= 0 ? null : [dim1]; // Fix for conformance check TODO dim2, dim3
1064
+ var browseName = name; // Use full name by default
1065
+
1066
+ // NodeId can be string or integer or Guid or Opaque: ns=10;i=1000 or ns=5;g=
1067
+ var bIndex = name.indexOf(";s="); // String
1068
+ if (bIndex>0) {
1069
+ browseName = name.substring(bIndex+3);
1070
+ }
1071
+ bIndex = name.indexOf(";i="); // Integer
1072
+ if (bIndex>0) {
1073
+ browseName = name.substring(bIndex+3);
1074
+ }
1075
+ bIndex = name.indexOf(";g="); // Guid
1076
+ if (bIndex>0) {
1077
+ browseName = name.substring(bIndex+3);
1078
+ }
1079
+ bIndex = name.indexOf(";b="); // Opaque base64
1080
+ if (bIndex>0) {
1081
+ browseName = name.substring(bIndex+3);
1082
+ }
1083
+
1084
+ const variableId = `${ns}:${browseName}`;
1085
+
1086
+ verbose_log(chalk.cyan(`addVariable: variableId: ${variableId}`));
1087
+
1088
+ variables[variableId] = 0;
1089
+ if (valueRank == 1) {
1090
+ arrayType = opcua.VariantArrayType.Array;
1091
+ dimensions = [dim1];
1092
+ variables[variableId] = new Float32Array(dim1);
1093
+ for (var i=0; i<dim1; i++) {
1094
+ variables[variableId][i] = 0;
1095
+ }
1096
+ }
1097
+ if (valueRank == 2) {
1098
+ arrayType = opcua.VariantArrayType.Matrix;
1099
+ dimensions = [dim1, dim2];
1100
+ variables[variableId] = new Float32Array(dim1*dim2);
1101
+ for (var i=0; i<dim1*dim2; i++) {
1102
+ variables[variableId][i] = 0;
1103
+ }
1104
+ }
1105
+ if (valueRank == 3) {
1106
+ arrayType = opcua.VariantArrayType.Matrix; // Actually no Cube => Matrix with 3 dims
1107
+ dimensions = [dim1, dim2, dim3];
1108
+ variables[variableId] = new Float32Array(dim1*dim2*dim3);
1109
+ for (var i=0; i<dim1*dim2*dim3; i++) {
1110
+ variables[variableId][i] = 0;
1111
+ }
1112
+ }
1113
+
1114
+ if (datatype == "Int32") {
1115
+ opcuaDataType = opcua.DataType.Int32;
1116
+ }
1117
+ if (datatype == "Int16") {
1118
+ opcuaDataType = opcua.DataType.Int16;
1119
+ }
1120
+ if (datatype == "UInt32") {
1121
+ opcuaDataType = opcua.DataType.UInt32;
1122
+ }
1123
+ if (datatype == "UInt64") {
1124
+ opcuaDataType = opcua.DataType.UInt64;
1125
+ }
1126
+ if (datatype == "UInt16") {
1127
+ opcuaDataType = opcua.DataType.UInt16;
1128
+ }
1129
+ if (datatype == "Double") {
1130
+ opcuaDataType = opcua.DataType.Double;
1131
+ }
1132
+ if (datatype == "Float") {
1133
+ opcuaDataType = opcua.DataType.Float;
1134
+ }
1135
+ if (datatype == "Byte") {
1136
+ opcuaDataType = opcua.DataType.Byte;
1137
+ }
1138
+ if (datatype == "SByte") {
1139
+ opcuaDataType = opcua.DataType.SByte;
1140
+ }
1141
+ if (datatype == "DateTime") {
1142
+ opcuaDataType = opcua.DataType.DateTime;
1143
+ variables[variableId] = new Date();
1144
+ }
1145
+ if (datatype == "ExtensionObject") {
1146
+ opcuaDataType = opcua.DataType.ExtensionObject;
1147
+ variables[variableId] = {};
1148
+ }
1149
+ if (datatype == "ByteString") {
1150
+ opcuaDataType = opcua.DataType.ByteString;
1151
+ variables[variableId] = Buffer.from("");
1152
+ }
1153
+ if (datatype == "String") {
1154
+ opcuaDataType = opcua.DataType.String;
1155
+ variables[variableId] = "";
1156
+ }
1157
+ if (datatype == "Boolean") {
1158
+ opcuaDataType = opcua.DataType.Boolean;
1159
+ variables[variableId] = false;
1160
+ }
1161
+ if (opcuaDataType === null) {
1162
+ verbose_warn(chalk.red("Cannot addVariable, datatype: ") + chalk.cyan(datatype) + chalk.red(" is not valid OPC UA datatype!"));
1163
+ break;
1164
+ }
1165
+ verbose_log("Datatype: " + datatype);
1166
+ verbose_log("OPC UA type id: "+ opcuaDataType.toString() + " dims[" + dim1 + "," + dim2 +"," + dim3 +"] == " + dimensions);
1167
+ // Initial value for server variable
1168
+ var init = msg.topic.indexOf("value=");
1169
+ if (init > 0) {
1170
+ var initialValue = msg.topic.substring(init+6);
1171
+ // Only up to ";".
1172
+ if (initialValue.indexOf(";") >= 0) {
1173
+ initialValue = initialValue.substring(0, initialValue.indexOf(";"));
1174
+ }
1175
+ verbose_log("BrowseName: " + ns + ":" + browseName + " initial value: " + initialValue);
1176
+ variables[variableId] = opcuaBasics.build_new_value_by_datatype(datatype, initialValue);
1177
+ }
1178
+ if (datatype === "ExtensionObject") {
1179
+ var typeId = msg.topic.substring(msg.topic.indexOf("typeId=") + 7);
1180
+ verbose_log("ExtensionObject typeId: " + typeId);
1181
+ var DataTypeNode = addressSpace.findDataType(opcua.coerceNodeId(typeId));
1182
+ var extVar = addressSpace.constructExtensionObject(DataTypeNode, {}); // build default value for extension object
1183
+ verbose_log("Server returned: " + JSON.stringify(extVar));
1184
+ var extNode = namespace.addVariable({
1185
+ organizedBy: addressSpace.findNode(parentFolder.nodeId),
1186
+ nodeId: name,
1187
+ browseName: browseNameTopic || browseName,
1188
+ description: description,
1189
+ displayName: displayName || browseNameTopic || browseName,
1190
+ dataType: opcua.coerceNodeId(typeId), // "ExtensionObject", // "StructureDefinition", // typeId,
1191
+ minimumSamplingInterval: 500,
1192
+ valueRank,
1193
+ value: { dataType: opcua.DataType.ExtensionObject, value: extVar },
1194
+ // value: { dataType: DataType.StructureDefinition, value: extVar },
1195
+ });
1196
+ var newext = { "payload" : { "messageType" : "Variable", "variableName": browseName, "nodeId": extNode.nodeId.toString() }};
1197
+ node.send(newext);
1198
+ // TODO get/set functions and other tricks as with normal scalar
1199
+ return opcua.StatusCodes.Good;
1200
+ }
1201
+ // Check & add from msg accessLevel userAccessLevel, role & permissions
1202
+ var accessLevel = opcua.makeAccessLevelFlag("CurrentRead | CurrentWrite"); // Use as default
1203
+ var userAccessLevel = opcua.makeAccessLevelFlag("CurrentRead | CurrentWrite"); // Use as default
1204
+ if (msg.accessLevel) {
1205
+ accessLevel = msg.accessLevel;
1206
+ }
1207
+ if (msg.userAccessLevel) {
1208
+ userAccessLevel = msg.userAccessLevel;
1209
+ }
1210
+ // permissions collected from multiple opcua-rights
1211
+ let permissions = [
1212
+ { roleId: opcua.WellKnownRoles.Anonymous, permissions: opcua.allPermissions },
1213
+ { roleId: opcua.WellKnownRoles.AuthenticatedUser, permissions: opcua.allPermissions },
1214
+ ];
1215
+ if (msg.permissions) {
1216
+ permissions = msg.permissions;
1217
+ }
1218
+ verbose_log(chalk.yellow("Using access level: ") + chalk.cyan(accessLevel) + chalk.yellow(" user access level: ") + chalk.cyan(userAccessLevel) + chalk.yellow(" permissions: ") + chalk.cyan(JSON.stringify(permissions)));
1219
+
1220
+ var ts = new Date();
1221
+ if (variablesTs[variableId]) {
1222
+ ts = variablesTs[variableId];
1223
+ }
1224
+ var st = opcua.StatusCodes.Good;
1225
+ if (variablesStatus[variableId]) {
1226
+ st = variablesStatus[variableId];
1227
+ }
1228
+ let value;
1229
+ if (valueRank>=2) {
1230
+ value = new opcua.Variant({
1231
+ arrayType,
1232
+ dimensions,
1233
+ dataType: opcuaDataType,
1234
+ value: variables[variableId]
1235
+ });
1236
+ }
1237
+ else {
1238
+ value = new opcua.Variant({
1239
+ arrayType,
1240
+ dataType: opcuaDataType,
1241
+ value: variables[variableId]
1242
+ });
1243
+ }
1244
+
1245
+ const newValue = new DataValue({
1246
+ serverPicoseconds: 0,
1247
+ serverTimestamp: new Date(),
1248
+ sourcePicoseconds: 0,
1249
+ sourceTimestamp: ts,
1250
+ statusCode: st,
1251
+ value: value // new opcua.Variant({arrayType, dataType: opcuaDataType, value: variables[variableId]})
1252
+ });
1253
+ var newVAR = namespace.addVariable({
1254
+ organizedBy: addressSpace.findNode(parentFolder.nodeId),
1255
+ nodeId: name,
1256
+ accessLevel: accessLevel,
1257
+ userAccessLevel: userAccessLevel,
1258
+ rolePermissions: [].concat(permissions),
1259
+ accessRestrictions: opcua.AccessRestrictionsFlag.None, // TODO from msg
1260
+ browseName: browseNameTopic || browseName, // or displayName
1261
+ description: description,
1262
+ displayName: displayName || browseNameTopic || browseName,
1263
+ dataType: datatype, // opcuaDataType,
1264
+ minimumSamplingInterval: 500,
1265
+ valueRank,
1266
+ arrayDimensions: dimensions,
1267
+ value: // new opcua.Variant({arrayType, dataType: opcuaDataType, value: variables[variableId]})
1268
+ {
1269
+ // Build a fresh DataValue from the live variables[] dictionary on every read.
1270
+ // The previous implementation returned a closed-over `newValue` captured at variable
1271
+ // creation, which meant scalar variables were frozen at their initial value despite
1272
+ // the `set` callback updating `variables[variableId]`. See issue #855.
1273
+ timestamped_get: () => {
1274
+ return new DataValue({
1275
+ serverPicoseconds: 0,
1276
+ serverTimestamp: new Date(),
1277
+ sourcePicoseconds: 0,
1278
+ sourceTimestamp: variablesTs[variableId] || ts,
1279
+ statusCode: variablesStatus[variableId] || st,
1280
+ value: new opcua.Variant({
1281
+ arrayType,
1282
+ dataType: opcuaDataType,
1283
+ value: variables[variableId]
1284
+ })
1285
+ });
1286
+ },
1287
+ set: function (variant) {
1288
+ verbose_log(chalk.yellow("Server set new variable value : ") + chalk.cyan(variables[variableId]) + chalk.yellow(" browseName: ") + chalk.cyan(ns) + ":" + chalk.cyan(browseName) + chalk.yellow(" new: ") + chalk.cyan(stringify(variant)));
1289
+ variables[variableId] = opcuaBasics.build_new_value_by_datatype(variant.dataType.toString(), variant.value);
1290
+ // }
1291
+ // variables[variableId] = Object.assign(variables[variableId], opcuaBasics.build_new_value_by_datatype(variant.dataType.toString(), variant.value));
1292
+ verbose_log(chalk.yellow("Server variable: ") + chalk.cyan(variables[variableId]) + chalk.yellow(" browseName: ") + chalk.cyan(ns) + ":" + chalk.cyan(browseName));
1293
+ var SetMsg = { "payload" : { "messageType" : "Variable", "variableName": ns + ":" + browseName, "variableValue": variables[variableId] }};
1294
+ verbose_log(chalk.yellow("msg Payload: ") + chalk.cyan(JSON.stringify(SetMsg)));
1295
+ node.send(SetMsg);
1296
+ return opcua.StatusCodes.Good;
1297
+ }
1298
+
1299
+ }
1300
+ });
1301
+
1302
+ var newvar = { "payload" : { "messageType" : "Variable", "variableName": ns + ":" + browseName, "nodeId": newVAR.nodeId.toString() }};
1303
+ node.send(newvar);
1304
+
1305
+ }
1306
+ break;
1307
+
1308
+ case "installHistorian":
1309
+ verbose_log("Install historian for node: ".concat(msg.topic)); // Example topic format ns=1;s=VariableName;datatype=Double
1310
+ var datatype = "";
1311
+ var opcuaDataType = null;
1312
+ var nodeStr = msg.topic.substring(0, msg.topic.indexOf(";datatype="));
1313
+ var e = msg.topic.indexOf("datatype=");
1314
+ if (e<0) {
1315
+ node_error("no datatype=Float or other type in install historian ".concat(msg.topic)); // Example topic format ns=1;s=variable
1316
+ }
1317
+ var nodeId = addressSpace.findNode(nodeStr);
1318
+ if (nodeId) {
1319
+ addressSpace.installHistoricalDataNode(nodeId); // no options, use memory as storage
1320
+ }
1321
+ else {
1322
+ node_error("Cannot find node: " + msg.topic + " nodeId: " + nodeStr);
1323
+ }
1324
+ break;
1325
+
1326
+ case "addMethod":
1327
+ verbose_log("Add method for node: ".concat(msg.topic)); // Example topic format ns=1;s=VariableName;datatype=Double
1328
+ verbose_log("Parameters: " + JSON.stringify(msg));
1329
+ const parentNode = addressSpace.getOwnNamespace().findNode(msg.topic);
1330
+ if (!parentNode) {
1331
+ node.error("Method needs parent node, wrong nodeId in the msg.topic: ", msg);
1332
+ }
1333
+ const newMethod = addressSpace.getOwnNamespace().addMethod(
1334
+ parentNode, {
1335
+ // nodeId: "ns=1;s=" + msg.browseName, // Issue #654
1336
+ nodeId: msg.topic.substring(0, msg.topic.indexOf(";s=")) + ";s=" + msg.browseName,
1337
+ browseName: msg.browseName,
1338
+ inputArguments: [{
1339
+ name: msg.inputArguments[0].name,
1340
+ description: {
1341
+ text: msg.inputArguments[0].text
1342
+ },
1343
+ dataType: getUaDatatype(msg.inputArguments[0].type)
1344
+ }],
1345
+ outputArguments: [{
1346
+ name: msg.outputArguments[0].name,
1347
+ description: {
1348
+ text: msg.outputArguments[0].text,
1349
+ },
1350
+ dataType: getUaDatatype(msg.outputArguments[0].type),
1351
+ valueRank: 1 // TODO Array, Matrix later
1352
+ }]
1353
+ });
1354
+ newMethod.bindMethod(async function (inputArguments, context, callback) {
1355
+ const status = opcua.StatusCodes.BadNotImplemented; // Current implementation does not support as setServerConfiguration is void and no return array
1356
+ const response = "";
1357
+ const callMethodResult = {
1358
+ statusCode: status,
1359
+ // TODO check if any outputArguments
1360
+ outputArguments: [ {
1361
+ dataType: getUaDatatype(msg.outputArguments[0].type),
1362
+ // arrayType: VariantArrayType.Array,
1363
+ value: response
1364
+ }]
1365
+ };
1366
+ callback(null, callMethodResult);
1367
+ });
1368
+ break;
1369
+ case "deleteNode":
1370
+ if (addressSpace === undefined) {
1371
+ node_error("addressSpace undefined");
1372
+ return false;
1373
+ }
1374
+
1375
+ var searchedNode = addressSpace.findNode(payload.nodeId);
1376
+ if (searchedNode === undefined) {
1377
+ verbose_warn("Cannot find node: " + payload.nodeId + " from addressSpace")
1378
+ } else {
1379
+ addressSpace.deleteNode(searchedNode);
1380
+ }
1381
+ break;
1382
+
1383
+ case "registerNamespace":
1384
+ var ns = addressSpace.registerNamespace(msg.topic);
1385
+ // verbose_log("namespace: " + stringify(ns));
1386
+ var index = addressSpace.getNamespaceIndex(msg.topic);
1387
+ returnValue = "ns=" + index.toString();
1388
+ break;
1389
+
1390
+ case "getNamespaceIndex":
1391
+ returnValue = "ns=" + addressSpace.getNamespaceIndex(msg.topic);
1392
+ break;
1393
+
1394
+ case "getNamespaces":
1395
+ returnValue = addressSpace.getNamespaceArray().reduce((dict, namespace, index) => (dict[namespace.namespaceUri] = index, dict), {});
1396
+ break;
1397
+
1398
+ case "setUsers":
1399
+ if (msg.payload.hasOwnProperty("users")) {
1400
+ users = msg.payload.users;
1401
+ verbose_log("NEW USERS: " + JSON.stringify(users));
1402
+ setUsers();
1403
+ }
1404
+ else {
1405
+ verbose_warn("No users defined in the input msg)");
1406
+ }
1407
+ break;
1408
+
1409
+ case "installDiscreteAlarm":
1410
+ verbose_log("Install discrete alarm for node: ".concat(msg.topic)); // Example topic format ns=1;s=VariableName;datatype=Double
1411
+ var alarmText = msg.alarmText;
1412
+ var priority = msg.priority;
1413
+ var nodeStr = msg.topic.substring(0, msg.topic.indexOf(";datatype="));
1414
+ var nodeId = addressSpace.findNode(nodeStr);
1415
+ // var boolVar = false;
1416
+ if (nodeId) {
1417
+ var namespace = addressSpace.getOwnNamespace(); // Default
1418
+ var alarmState = namespace.addVariable({
1419
+ nodeId: nodeStr + "-" + "AlarmState",
1420
+ browseName: nodeStr.substring(7) + "-" + "AlarmState",
1421
+ displayName: nodeStr.substring(7) + "-" + "AlarmState",
1422
+ propertyOf: nodeId,
1423
+ dataType: "Boolean",
1424
+ minimumSamplingInterval: 500, // NOTE alarm is event based DO NOT USE THIS! ???
1425
+ eventSourceOf: "i=2253", // Use server as default event source
1426
+ /* Use default
1427
+ value: {
1428
+ get: function () {
1429
+ return new opcua.Variant({
1430
+ dataType: "Boolean",
1431
+ value: boolVar
1432
+ });
1433
+ },
1434
+ set: function (variant) {
1435
+ boolVar = variant.value;
1436
+ return opcua.StatusCodes.Good;
1437
+ }
1438
+ }
1439
+ */
1440
+ });
1441
+ alarmState.setValueFromSource({dataType: "Boolean", value: false});
1442
+ var discreteAlarm = addressSpace.findEventType("DiscreteAlarmType");
1443
+ var alarm = namespace.instantiateDiscreteAlarm(discreteAlarm,
1444
+ {
1445
+ nodeId: nodeStr + "-" + "DiscreteAlarm",
1446
+ browseName: nodeStr.substring(7) + "-" + "DiscreteAlarm",
1447
+ displayName: nodeStr.substring(7) + "-" + "DiscreteAlarm",
1448
+ organizedBy: nodeId,
1449
+ conditionSource: alarmState,
1450
+ severity: priority,
1451
+ conditionName: "Node-Red OPC UA Event",
1452
+ browseName: "DiscreteAlarmInstance",
1453
+ inputNode: alarmState, // the variable that will be monitored for change, generate below
1454
+ optionals: [ "Acknowledge", "ConfirmedState", "Confirm" ], // confirm state and confirm Method
1455
+ }
1456
+ );
1457
+ alarm.setEnabledState(true);
1458
+ // alarm.setSeverity(priority);
1459
+ // Do not use default
1460
+ try {
1461
+ alarmState.on("value_changed", function (newDataValue) {
1462
+ // console.log("DISCRETE value: " + newDataValue.value.value);
1463
+ // This works OK
1464
+ if (newDataValue.value.value === true) {
1465
+ alarm.activeState.setValue(true);
1466
+ alarm.ackedState.setValue(false);
1467
+ alarm.raiseNewCondition({
1468
+ severity: priority,
1469
+ message: alarmText,
1470
+ retain: true
1471
+ });
1472
+ }
1473
+ if (newDataValue.value.value === false) {
1474
+ alarm.deactivateAlarm();
1475
+ }
1476
+ });
1477
+ }
1478
+ catch(error) {
1479
+ node.error("Error: " + error.toString());
1480
+ }
1481
+ }
1482
+ else {
1483
+ node_error("Cannot find node: " + msg.topic + " nodeId: " + nodeStr);
1484
+ }
1485
+ break;
1486
+
1487
+ case "installLimitAlarm":
1488
+ verbose_log("install limit alarm for node: ".concat(msg.topic)); // Example topic format ns=1;s=VariableName;datatype=Double
1489
+ var highhighLimit = msg.hh;
1490
+ var highLimit = msg.h;
1491
+ var lowLimit = msg.l;
1492
+ var lowlowLimit = msg.ll;
1493
+ var nodeStr = msg.topic.substring(0, msg.topic.indexOf(";datatype="));
1494
+ var nodeId = addressSpace.findNode(nodeStr);
1495
+ var levelVar = 5.0;
1496
+ if (nodeId) {
1497
+ var namespace = addressSpace.getOwnNamespace(); // Default
1498
+
1499
+ var alarmState = namespace.addVariable({
1500
+ nodeId: nodeStr + "-" + "LimitState",
1501
+ browseName: nodeStr.substring(7) + "-" + "LimitState",
1502
+ displayName: nodeStr.substring(7) + "-" + "LimitState",
1503
+ propertyOf: nodeId,
1504
+ dataType: "Double",
1505
+ minimumSamplingInterval: 500,
1506
+ eventSourceOf: "i=2253", // nodeId, // Use server!
1507
+ inputNode: msg.topic,
1508
+ value: {
1509
+ get: function () {
1510
+ return new opcua.Variant({
1511
+ dataType: "Double",
1512
+ value: levelVar
1513
+ });
1514
+ },
1515
+ set: function (variant) {
1516
+ levelVar = variant.value;
1517
+ return opcua.StatusCodes.Good;
1518
+ }
1519
+ }
1520
+ });
1521
+ alarmState.setValueFromSource({datatype: "Double", value: 5.0});
1522
+ var alarm = namespace.instantiateNonExclusiveLimitAlarm("NonExclusiveLimitAlarmType",
1523
+ {
1524
+ nodeId: nodeStr + "-" + "LimitAlarm",
1525
+ browseName: nodeStr.substring(7) + "-" + "LimitAlarm",
1526
+ displayName: nodeStr.substring(7) + "-" + "LimitAlarm",
1527
+ organizedBy: nodeId,
1528
+ conditionSource: alarmState,
1529
+ conditionName: "Node-Red OPC UA Event",
1530
+ eventSourceOf: "i=2253", // nodeId, // Use server!
1531
+ minimumSamplingInterval: 500,
1532
+ browseName: "LimitAlarmInstance",
1533
+ inputNode: alarmState, // the variable that will be monitored for change, generate below
1534
+ highHighLimit: highhighLimit,
1535
+ highLimit: highLimit,
1536
+ lowLimit: lowLimit,
1537
+ lowLowLimit: lowlowLimit,
1538
+ severity: priority,
1539
+ optionals: [ "Acknowledge", "ConfirmedState", "Confirm" ], // confirm state and confirm Method
1540
+ }
1541
+ );
1542
+ alarm.setEnabledState(true);
1543
+ /*
1544
+ alarm._onInputDataValueChange = function (dataValue) {
1545
+ // Overwrite node-opcua default alarm event
1546
+ console.log("LIMIT ALARM NEW VALUE: " + dataValue.value.value + " message: " + msg.alarmText);
1547
+ // This is not working anymore for some reason???
1548
+ alarm.activeState.setValue(true);
1549
+ alarm.ackedState.setValue(false);
1550
+ alarm.raiseNewCondition({severity: priority,
1551
+ message: msg.alarmText + " " + dataValue.value.value,
1552
+ retain: true});
1553
+ }; // Do not generate events by default node-opcua, use code below
1554
+ */
1555
+ // alarm.setSeverity(priority);
1556
+ // Do not use default change event messages
1557
+ try {
1558
+ alarmState.on("value_changed", function (newDataValue) {
1559
+ // console.log("NEW ALARM LIMIT VALUE: " + newDataValue.value.value + " message: " + msg.alarmText);
1560
+ // This is not working anymore for some reason???
1561
+ alarm.activeState.setValue(true);
1562
+ alarm.ackedState.setValue(false);
1563
+ alarm.raiseNewCondition({severity: priority,
1564
+ message: msg.alarmText + " " + newDataValue.value.value,
1565
+ retain: true});
1566
+ });
1567
+ }
1568
+ catch(error) {
1569
+ console.error("Error: " + error.message);
1570
+ }
1571
+ }
1572
+ else {
1573
+ node_error("Cannot find node: " + msg.topic + " nodeId: " + nodeStr);
1574
+ }
1575
+ break;
1576
+ case "addFile":
1577
+ // msg.topic == nodeId for the file object
1578
+ // msg.payload == fileName
1579
+ if (msg.topic && msg.payload && msg.payload.fileName) {
1580
+ var file_node;
1581
+ file_node = opcua.coerceNodeId(msg.topic);
1582
+ var fileName = msg.payload.fileName;
1583
+ verbose_log("New file nodeId:" + file_node + " fileName: " + fileName);
1584
+ const fileType = addressSpace.findObjectType("FileType");
1585
+ if (folder) {
1586
+ parentFolder = folder; // Use previously created folder as parentFolder or setFolder() can be used to set parentFolder
1587
+ }
1588
+ var parentId = addressSpace.findNode(parentFolder.nodeId)
1589
+ if (!parentId) {
1590
+ parentId = addressSpace.rootFolder.objects; // Use this as exists always
1591
+ }
1592
+ var index = fileName.lastIndexOf("/");
1593
+ var fname = fileName;
1594
+ // Hide path from the filename
1595
+ if (index > 0) {
1596
+ fname = fileName.substring(index+1); // Skip / charater to get just filename
1597
+ }
1598
+ const newFile = fileType.instantiate({
1599
+ nodeId: file_node,
1600
+ browseName: fname,
1601
+ displayName: fname,
1602
+ organizedBy: parentId
1603
+ });
1604
+ fileTransfer.installFileType(newFile, { filename: fileName });
1605
+ // Make file writable, can be one extra parameter later inside msg object
1606
+ const Wnode = addressSpace.findNode(file_node.toString() + "-Writable");
1607
+ Wnode.setValueFromSource({ dataType: "Boolean", value: true});
1608
+ const userWnode = addressSpace.findNode(file_node.toString() + "-UserWritable");
1609
+ userWnode.setValueFromSource({ dataType: "Boolean", value: true});
1610
+ }
1611
+ else {
1612
+ verbose_warn("Check msg object, it must contain msg.payload.filename!");
1613
+ }
1614
+ break;
1615
+ case "saveAddressSpace":
1616
+ if (msg.payload && msg.filename) {
1617
+ // Save current server namespace objects to file
1618
+ var namespace = addressSpace.getOwnNamespace();
1619
+ // Use msg.topic to select namespace
1620
+ if (msg.topic) {
1621
+ verbose_log("Saving namespace index: " + msg.topic);
1622
+ namespace = addressSpace.getNamespace(parseInt(msg.topic));
1623
+ }
1624
+ if (namespace) {
1625
+ const xmlstr = namespace.toNodeset2XML();
1626
+ fs.writeFileSync(msg.filename, xmlstr.toString(), {encoding: "utf8"});
1627
+ }
1628
+ else {
1629
+ verbose_warn("No namespace to save to XML file, check namespace index: " + msg.topic);
1630
+ }
1631
+ }
1632
+ else {
1633
+ verbose_warn("Check msg object, it must contain msg.filename for the address space in XML format!");
1634
+ }
1635
+ break;
1636
+ case "loadAddressSpace":
1637
+ verbose_log("Loading nodeset from file: " + msg.filename);
1638
+ if (msg.payload && msg.filename && fs.existsSync(msg.filename)) {
1639
+ savedAddressSpace = msg.filename;
1640
+ node.server_options.nodeset_filename.push(msg.filename);
1641
+ restart_server();
1642
+ }
1643
+ else {
1644
+ verbose_warn("Check msg object, it must contain msg.filename for the address space in XML format!");
1645
+ }
1646
+ break;
1647
+ case "bindVariables":
1648
+ // browse address space and bind method for each variable node setter
1649
+ // report all existing Variables
1650
+ var session = new opcua.PseudoSession(addressSpace);
1651
+ const objectsFolder = addressSpace.findNode("ObjectsFolder");
1652
+ var results=[];
1653
+ const crawler = new NodeCrawler(session); // Legacy support
1654
+ crawler.on("browsed", (element) => {
1655
+ // Limit to variables and skip ns=0
1656
+ if (element.nodeId.toString().indexOf("ns=0;") < 0 && element.nodeClass == opcua.NodeClass.Variable) {
1657
+ // console.log("Element: " + element);
1658
+ var item = Object.assign({}, element); // Clone element
1659
+ results.push(item);
1660
+ }
1661
+ });
1662
+ crawler.read(objectsFolder.nodeId, function (err, obj) {
1663
+ if (!err) {
1664
+ // console.log("Obj: " + obj);
1665
+ // Nothing to do here
1666
+ }
1667
+ else {
1668
+ verbose_warn("Crawling variables failed: " + err);
1669
+ }
1670
+ crawler.dispose();
1671
+ });
1672
+
1673
+ results.forEach(function(item) {
1674
+ var variableNode = addressSpace.findNode(id);
1675
+ node.debug("NodeId:" + variableNode.nodeId + " NodeClass: " + variableNode.nodeClass);
1676
+ if (variableNode.nodeClass == opcua.NodeClass.Variable) {
1677
+ var newext = {"payload": {"messageType":"Variable", "variableName":browseName.toString(), "nodeId":id.toString()}};
1678
+ node.send(newext);
1679
+ bindCallbacks(variableNode, browseName.toString());
1680
+ }
1681
+ });
1682
+
1683
+ function bindCallbacks(variable, browseName) {
1684
+ const variableId = `${variable.namespaceIndex}:${browseName}`;
1685
+ var ts = new Date();
1686
+ if (variablesTs[variableId]) {
1687
+ ts = variablesTs[variableId];
1688
+ }
1689
+ var st = opcua.StatusCodes.Good;
1690
+ if (variablesStatus[variableId]) {
1691
+ st = variablesStatus[variableId];
1692
+ }
1693
+ const newValue = new DataValue({
1694
+ serverPicoseconds: 0,
1695
+ serverTimestamp: new Date(),
1696
+ sourcePicoseconds: 0,
1697
+ sourceTimestamp: ts,
1698
+ statusCode: st,
1699
+ value: opcua.Variant({dataType: opcuaBasics.convertToString(variable.dataType.toString()),
1700
+ value: variables[variableId]})
1701
+ });
1702
+
1703
+ var options = {
1704
+ // Build a fresh DataValue from the live variables[] dictionary on every read,
1705
+ // for the same reason as in addVariable above (issue #855).
1706
+ timestamped_get: () => {
1707
+ return new DataValue({
1708
+ serverPicoseconds: 0,
1709
+ serverTimestamp: new Date(),
1710
+ sourcePicoseconds: 0,
1711
+ sourceTimestamp: variablesTs[variableId] || ts,
1712
+ statusCode: variablesStatus[variableId] || st,
1713
+ value: new opcua.Variant({
1714
+ arrayType,
1715
+ dataType: opcuaDataType,
1716
+ value: variables[variableId]
1717
+ })
1718
+ });
1719
+ },
1720
+ set: (variant) => {
1721
+ // Store value
1722
+ variables[variableId] = variant.value;
1723
+ variable.value = new opcua.DataValue(new opcua.Variant({ dataType: variable.DataType, value: variant.value}),
1724
+ opcua.StatusCodes.Good);
1725
+ // Report new value to server node output
1726
+ var SetMsg = { "payload" : { "messageType" : "Variable", "variableName": browseName, "variableValue": variables[variableId] }};
1727
+ verbose_log("msg Payload:" + JSON.stringify(SetMsg));
1728
+ node.send(SetMsg);
1729
+ return opcua.StatusCodes.Good;
1730
+ }
1731
+
1732
+ };
1733
+ if (variable.nodeClass.toString() === "2") {
1734
+ variable.bindVariable(options, true); // overwrite
1735
+ }
1736
+ }
1737
+ break;
1738
+ case "bindMethod":
1739
+ // Find MethodId and bindMethod with the give function into it
1740
+ // Skeleton for methodFunc
1741
+ /*
1742
+ async function methodFunc(inputArguments, context) {
1743
+ console.log("Method Input arguments: " + JSON.stringify(inputArguments));
1744
+ return { statusCode: opcua.StatusCodes.Good };
1745
+ };
1746
+ */
1747
+ const methodId = msg.topic;
1748
+ const methodFunc = msg.code;
1749
+ const method = addressSpace.findNode(opcua.coerceNodeId(msg.topic));
1750
+ if (method) {
1751
+ method.bindMethod(methodFunc);
1752
+ }
1753
+ else {
1754
+ verbose_warn("Method not found!");
1755
+ }
1756
+ break;
1757
+ default:
1758
+ node_error("Unknown OPC UA Command");
1759
+ }
1760
+ return returnValue;
1761
+ }
1762
+
1763
+ async function restart_server() {
1764
+ verbose_log("Restarting OPC UA Server");
1765
+ if (node.server) {
1766
+ node.server.engine.setShutdownReason("Shutdown command received");
1767
+ // Wait 10s before shutdown
1768
+ await node.server.shutdown(10000).then(() => {
1769
+ verbose_log("Server has shutdown");
1770
+ node.server.dispose();
1771
+ node.server = null;
1772
+ vendorName = null;
1773
+ folder = null;
1774
+ });
1775
+ // Start server again
1776
+ await initNewServer();
1777
+ node.server = new opcua.OPCUAServer(node.server_options);
1778
+ node.server.on("post_initialize", () => {
1779
+ if (node.constructDefaultAddressSpace === true) {
1780
+ construct_my_address_space(node.server.engine.addressSpace);
1781
+ }
1782
+ });
1783
+ await node.server.start();
1784
+ // Client connects with userName
1785
+ node.server.on("session_activated", (session) => {
1786
+ if (session.userIdentityToken && session.userIdentityToken.userName) {
1787
+ var msg = {};
1788
+ msg.topic="Username";
1789
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString();
1790
+ node.send(msg);
1791
+ }
1792
+ });
1793
+ // Client connected
1794
+ node.server.on("create_session", function(session) {
1795
+ var msg = {};
1796
+ msg.topic="Client-connected";
1797
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString();
1798
+ node.send(msg);
1799
+ });
1800
+ // Client disconnected
1801
+ node.server.on("session_closed", function(session, reason) {
1802
+ node.debug("Reason: " + reason);
1803
+ var msg = {};
1804
+ msg.topic="Client-disconnected";
1805
+ msg.payload = session.sessionName.toString(); // session.clientDescription.applicationName.toString() + " " + session.sessionName ? session.sessionName.toString() : "<null>";
1806
+ node.send(msg);
1807
+ });
1808
+ node.status({
1809
+ fill: "green",
1810
+ shape: "dot",
1811
+ text: "running"
1812
+ });
1813
+ initialized = true;
1814
+ }
1815
+
1816
+ if (node.server) {
1817
+ verbose_log("Restart OPC UA Server done");
1818
+ } else {
1819
+ node_error("Cannot restart OPC UA Server");
1820
+ }
1821
+ }
1822
+
1823
+ node.on("close", function () {
1824
+ verbose_log("Closing...");
1825
+ close_server();
1826
+ });
1827
+
1828
+ async function close_server() {
1829
+ if (node.server) {
1830
+ await node.server.shutdown(0, function () {
1831
+ node.server = null;
1832
+ vendorName = null;
1833
+ });
1834
+
1835
+ } else {
1836
+ node.server = null;
1837
+ vendorName = null;
1838
+ }
1839
+
1840
+ }
1841
+ }
1842
+
1843
+ RED.nodes.registerType("OpcUa-Server", OpcUaServerNode);
1844
+ };