@vitormnm/node-red-simple-opcua 1.5.0 → 1.6.3
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/client/lib/opcua-client-read-service.js +14 -2
- package/client/lib/opcua-client-subscription-service.js +8 -3
- package/client/opcua-client-utils.js +206 -12
- package/examples/flows_simple_opc.json +1 -2851
- package/package.json +1 -1
- package/server/lib/opcua-address-space-builder.js +203 -9
- package/server/lib/opcua-config.js +131 -8
- package/server/lib/opcua-constants.js +1 -0
- package/server/lib/opcua-server-methods.js +2 -0
- package/server/lib/opcua-server-runtime-child.js +148 -42
- package/server/lib/opcua-server-runtime.js +14 -5
- package/server/opcua-server-io.js +9 -0
- package/server/opcua-server.html +4 -1
- package/server/opcua-server.js +27 -2
- package/server/view/opcua-server.css +4 -0
- package/server/view/opcua-server.js +178 -28
|
@@ -36,6 +36,7 @@ class OpcUaServerProcess {
|
|
|
36
36
|
throw new Error("Server already running");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
this.node.id = nodeId;
|
|
39
40
|
this.node.name = settings.name;
|
|
40
41
|
this.node.serverName = settings.serverName;
|
|
41
42
|
this.node.server = null;
|
|
@@ -192,15 +193,18 @@ class OpcUaServerProcess {
|
|
|
192
193
|
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
193
194
|
const identifierType = this.resolveIdentifierType(target);
|
|
194
195
|
|
|
195
|
-
let result = {}
|
|
196
|
+
let result = {};
|
|
197
|
+
let readArrayResults = null;
|
|
196
198
|
|
|
197
199
|
if (Array.isArray(payload)) {
|
|
198
200
|
if (!payload.length) {
|
|
199
201
|
throw new Error("msg.payload array does not contain any items");
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
readArrayResults = payload.map((item) => this.readPayloadItem(identifierType, item));
|
|
205
|
+
|
|
202
206
|
result = {
|
|
203
|
-
payload:
|
|
207
|
+
payload: readArrayResults,
|
|
204
208
|
identifiers: payload.map((item) => this.resolvePayloadItemIdentifier(item))
|
|
205
209
|
};
|
|
206
210
|
} else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
@@ -224,9 +228,18 @@ class OpcUaServerProcess {
|
|
|
224
228
|
};
|
|
225
229
|
} else {
|
|
226
230
|
const identifier = this.resolveIdentifier(target);
|
|
231
|
+
let directValue = null;
|
|
232
|
+
let directError = null;
|
|
233
|
+
try {
|
|
234
|
+
directValue = server.readValue(identifierType, identifier);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
directValue = null;
|
|
237
|
+
directError = { identifier, message: e.message || String(e) };
|
|
238
|
+
}
|
|
227
239
|
result = {
|
|
228
|
-
payload:
|
|
229
|
-
identifiers: [identifier]
|
|
240
|
+
payload: directValue,
|
|
241
|
+
identifiers: [identifier],
|
|
242
|
+
directError
|
|
230
243
|
};
|
|
231
244
|
|
|
232
245
|
}
|
|
@@ -260,6 +273,35 @@ class OpcUaServerProcess {
|
|
|
260
273
|
nodeId: nodeId
|
|
261
274
|
});
|
|
262
275
|
|
|
276
|
+
// Emit a partialError for items that could not be read (array-of-objects mode only)
|
|
277
|
+
if (readArrayResults) {
|
|
278
|
+
const failed = readArrayResults
|
|
279
|
+
.filter(item => item && item.status !== "Good")
|
|
280
|
+
.map(item => ({ name: item.name, path: item.path, status: item.status }));
|
|
281
|
+
|
|
282
|
+
if (failed.length) {
|
|
283
|
+
process.send({
|
|
284
|
+
type: "partialError",
|
|
285
|
+
error: "Some tags could not be read: " + failed.map(f => f.path).join(", "),
|
|
286
|
+
failed: failed,
|
|
287
|
+
originalMsg: msg,
|
|
288
|
+
nodeId: nodeId
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Emit a partialError for a direct single-tag read failure
|
|
294
|
+
if (result.directError) {
|
|
295
|
+
const { identifier, message } = result.directError;
|
|
296
|
+
process.send({
|
|
297
|
+
type: "partialError",
|
|
298
|
+
error: "Some tags could not be read: " + identifier,
|
|
299
|
+
failed: [{ name: "", path: identifier, status: message }],
|
|
300
|
+
originalMsg: msg,
|
|
301
|
+
nodeId: nodeId
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
263
305
|
} catch (error) {
|
|
264
306
|
|
|
265
307
|
process.send({
|
|
@@ -334,6 +376,7 @@ class OpcUaServerProcess {
|
|
|
334
376
|
try {
|
|
335
377
|
let writtenPaths = null;
|
|
336
378
|
let payload = msg ? msg.payload : undefined;
|
|
379
|
+
let directError = null;
|
|
337
380
|
|
|
338
381
|
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
339
382
|
const identifierType = this.resolveIdentifierType(target);
|
|
@@ -364,14 +407,18 @@ class OpcUaServerProcess {
|
|
|
364
407
|
if (Buffer.isBuffer(payload) || payload instanceof Uint8Array) {
|
|
365
408
|
|
|
366
409
|
const identifier = this.resolveIdentifier(target);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
410
|
+
try {
|
|
411
|
+
this.node.writeValue(
|
|
412
|
+
identifierType,
|
|
413
|
+
identifier,
|
|
414
|
+
Buffer.isBuffer(payload)
|
|
415
|
+
? payload
|
|
416
|
+
: Buffer.from(payload)
|
|
417
|
+
);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
directError = { identifier, message: e.message || String(e) };
|
|
420
|
+
msg.payload = null;
|
|
421
|
+
}
|
|
375
422
|
|
|
376
423
|
writtenPaths = [identifier];
|
|
377
424
|
}
|
|
@@ -383,14 +430,18 @@ class OpcUaServerProcess {
|
|
|
383
430
|
) {
|
|
384
431
|
|
|
385
432
|
const identifier = this.resolveIdentifier(target);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
433
|
+
try {
|
|
434
|
+
this.node.writeValue(
|
|
435
|
+
identifierType,
|
|
436
|
+
identifier,
|
|
437
|
+
isByteString
|
|
438
|
+
? Buffer.from(payload)
|
|
439
|
+
: payload
|
|
440
|
+
);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
directError = { identifier, message: e.message || String(e) };
|
|
443
|
+
msg.payload = null;
|
|
444
|
+
}
|
|
394
445
|
|
|
395
446
|
writtenPaths = [identifier];
|
|
396
447
|
}
|
|
@@ -402,13 +453,13 @@ class OpcUaServerProcess {
|
|
|
402
453
|
throw new Error("msg.payload array does not contain any items");
|
|
403
454
|
}
|
|
404
455
|
|
|
405
|
-
payload.
|
|
406
|
-
this.writePayloadItem(identifierType, item)
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
writtenPaths = payload.map(item =>
|
|
410
|
-
this.resolvePayloadItemIdentifier(item)
|
|
456
|
+
const writeArrayResults = payload.map(item =>
|
|
457
|
+
this.writePayloadItem(identifierType, item)
|
|
411
458
|
);
|
|
459
|
+
|
|
460
|
+
msg.payload = writeArrayResults;
|
|
461
|
+
|
|
462
|
+
writtenPaths = writeArrayResults.map(item => item.path);
|
|
412
463
|
}
|
|
413
464
|
|
|
414
465
|
// Objeto { path: value }
|
|
@@ -428,11 +479,15 @@ class OpcUaServerProcess {
|
|
|
428
479
|
}
|
|
429
480
|
|
|
430
481
|
identifiers.forEach(identifier => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
482
|
+
try {
|
|
483
|
+
this.node.writeValue(
|
|
484
|
+
identifierType,
|
|
485
|
+
identifier,
|
|
486
|
+
payload[identifier]
|
|
487
|
+
);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
// suppress per-item errors; unknown paths become undefined
|
|
490
|
+
}
|
|
436
491
|
});
|
|
437
492
|
|
|
438
493
|
writtenPaths = identifiers;
|
|
@@ -442,12 +497,16 @@ class OpcUaServerProcess {
|
|
|
442
497
|
else {
|
|
443
498
|
|
|
444
499
|
const identifier = this.resolveIdentifier(target);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
500
|
+
try {
|
|
501
|
+
this.node.writeValue(
|
|
502
|
+
identifierType,
|
|
503
|
+
identifier,
|
|
504
|
+
payload
|
|
505
|
+
);
|
|
506
|
+
} catch (e) {
|
|
507
|
+
directError = { identifier, message: e.message || String(e) };
|
|
508
|
+
msg.payload = null;
|
|
509
|
+
}
|
|
451
510
|
|
|
452
511
|
writtenPaths = [identifier];
|
|
453
512
|
}
|
|
@@ -484,6 +543,35 @@ class OpcUaServerProcess {
|
|
|
484
543
|
nodeId
|
|
485
544
|
});
|
|
486
545
|
|
|
546
|
+
// Emit a partialError for items that could not be written (array-of-objects mode only)
|
|
547
|
+
if (Array.isArray(msg.payload) && msg.payload.length && msg.payload[0] && typeof msg.payload[0].status === "string") {
|
|
548
|
+
const failed = msg.payload
|
|
549
|
+
.filter(item => item.status !== "Good")
|
|
550
|
+
.map(item => ({ name: item.name, path: item.path, status: item.status }));
|
|
551
|
+
|
|
552
|
+
if (failed.length) {
|
|
553
|
+
process.send({
|
|
554
|
+
type: "partialError",
|
|
555
|
+
error: "Some tags could not be written: " + failed.map(f => f.path).join(", "),
|
|
556
|
+
failed: failed,
|
|
557
|
+
originalMsg: msg,
|
|
558
|
+
nodeId
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Emit a partialError for a direct single-tag write failure
|
|
564
|
+
if (directError) {
|
|
565
|
+
const { identifier, message } = directError;
|
|
566
|
+
process.send({
|
|
567
|
+
type: "partialError",
|
|
568
|
+
error: "Some tags could not be written: " + identifier,
|
|
569
|
+
failed: [{ name: "", path: identifier, status: message }],
|
|
570
|
+
originalMsg: msg,
|
|
571
|
+
nodeId
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
487
575
|
} catch (error) {
|
|
488
576
|
|
|
489
577
|
process.send({
|
|
@@ -540,20 +628,38 @@ class OpcUaServerProcess {
|
|
|
540
628
|
|
|
541
629
|
readPayloadItem(identifierType, item) {
|
|
542
630
|
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
631
|
+
let value = null;
|
|
632
|
+
let status = "Good";
|
|
633
|
+
try {
|
|
634
|
+
value = this.node.readValue(identifierType, identifier);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
value = null;
|
|
637
|
+
status = e.message || String(e);
|
|
638
|
+
}
|
|
543
639
|
return {
|
|
544
640
|
name: item.name,
|
|
545
641
|
path: identifier,
|
|
546
|
-
value
|
|
642
|
+
value,
|
|
643
|
+
status
|
|
547
644
|
};
|
|
548
645
|
}
|
|
549
646
|
|
|
550
647
|
writePayloadItem(identifierType, item) {
|
|
551
648
|
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
552
|
-
|
|
649
|
+
let writtenValue = null;
|
|
650
|
+
let status = "Good";
|
|
651
|
+
try {
|
|
652
|
+
this.node.writeValue(identifierType, identifier, item.value);
|
|
653
|
+
writtenValue = item.value;
|
|
654
|
+
} catch (e) {
|
|
655
|
+
writtenValue = null;
|
|
656
|
+
status = e.message || String(e);
|
|
657
|
+
}
|
|
553
658
|
return {
|
|
554
659
|
name: item.name,
|
|
555
660
|
path: identifier,
|
|
556
|
-
value:
|
|
661
|
+
value: writtenValue,
|
|
662
|
+
status
|
|
557
663
|
};
|
|
558
664
|
}
|
|
559
665
|
|
|
@@ -859,7 +965,7 @@ process.on("uncaughtException", (err) => {
|
|
|
859
965
|
process.send({
|
|
860
966
|
type: "error",
|
|
861
967
|
data: "Uncaught Exception: " + err.message,
|
|
862
|
-
nodeId:
|
|
968
|
+
nodeId: serverProcess.node.id
|
|
863
969
|
});
|
|
864
970
|
});
|
|
865
971
|
|
|
@@ -869,6 +975,6 @@ process.on("unhandledRejection", (reason) => {
|
|
|
869
975
|
process.send({
|
|
870
976
|
type: "error",
|
|
871
977
|
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
872
|
-
nodeId:
|
|
978
|
+
nodeId: serverProcess.node.id
|
|
873
979
|
});
|
|
874
980
|
});
|
|
@@ -67,7 +67,8 @@ class OpcUaServerRuntime {
|
|
|
67
67
|
node: this.node,
|
|
68
68
|
serverName: this.serverName,
|
|
69
69
|
addressSpace: this.addressSpace,
|
|
70
|
-
allowAnonymous: this.allowAnonymous
|
|
70
|
+
allowAnonymous: this.allowAnonymous,
|
|
71
|
+
users: this.users
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
|
|
@@ -238,10 +239,18 @@ class OpcUaServerRuntime {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
const roles = [resolveNodeId("WellKnownRole_AuthenticatedUser")];
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
const groups = typeof user.group === "string"
|
|
243
|
+
? user.group.split(",").map(g => g.trim()).filter(Boolean)
|
|
244
|
+
: Array.isArray(user.group)
|
|
245
|
+
? user.group
|
|
246
|
+
: [];
|
|
247
|
+
|
|
248
|
+
groups.forEach((groupName) => {
|
|
249
|
+
const customRole = this.resolveGroupRoleNodeId(groupName);
|
|
250
|
+
if (customRole) {
|
|
251
|
+
roles.push(customRole);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
245
254
|
return roles;
|
|
246
255
|
}
|
|
247
256
|
|
|
@@ -152,6 +152,15 @@ module.exports = function (RED) {
|
|
|
152
152
|
node.error(msg.error);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
if (msg.type === "partialError") {
|
|
156
|
+
// Route failed items to catch node without changing the node status
|
|
157
|
+
const catchMsg = Object.assign({}, msg.originalMsg || {}, {
|
|
158
|
+
payload: msg.failed,
|
|
159
|
+
error: msg.error
|
|
160
|
+
});
|
|
161
|
+
node.error(msg.error, catchMsg);
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
if (msg.type === "sendMethod") {
|
|
156
165
|
|
|
157
166
|
node.status({
|
package/server/opcua-server.html
CHANGED
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
<a href="#" id="node-input-add-object" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add object</a>
|
|
104
104
|
<a href="#" id="node-input-add-folder" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add folder</a>
|
|
105
105
|
<a href="#" id="node-input-add-object-type" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add object type</a>
|
|
106
|
+
<a href="#" id="node-input-add-enumeration" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add enumeration</a>
|
|
106
107
|
<a href="#" id="node-input-add-namespace" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add namespace</a>
|
|
107
108
|
<a href="#" id="node-input-expand-all" class="editor-button editor-button-small"><i class="fa fa-angle-double-down"></i> Expand all</a>
|
|
108
109
|
<a href="#" id="node-input-collapse-all" class="editor-button editor-button-small"><i class="fa fa-angle-double-up"></i> Collapse all</a>
|
|
@@ -128,6 +129,8 @@
|
|
|
128
129
|
<a href="#" data-action="add-object"><i class="fa fa-plus"></i> Add Object</a>
|
|
129
130
|
<a href="#" data-action="add-variable"><i class="fa fa-plus"></i> Add Variable</a>
|
|
130
131
|
<a href="#" data-action="add-objecttype"><i class="fa fa-plus"></i> Add ObjectType</a>
|
|
132
|
+
<a href="#" data-action="add-enumeration"><i class="fa fa-plus"></i> Add Enumeration</a>
|
|
133
|
+
<a href="#" data-action="add-enum-variable"><i class="fa fa-plus"></i> Add Enum Variable</a>
|
|
131
134
|
<a href="#" data-action="add-alarm"><i class="fa fa-plus"></i> Add Alarm</a>
|
|
132
135
|
<a href="#" data-action="add-method"><i class="fa fa-plus"></i> Add Method</a>
|
|
133
136
|
<a href="#" data-action="edit"><i class="fa fa-pencil"></i> Edit</a>
|
|
@@ -150,7 +153,7 @@
|
|
|
150
153
|
<h3>Inputs</h3>
|
|
151
154
|
<p><code>msg.payload</code>: JSON object describing the OPC UA tree. When provided, the node validates the structure and rebuilds the dynamic namespace.</p>
|
|
152
155
|
<h3>Details</h3>
|
|
153
|
-
<p>Supported variable types: <code>Int32</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
|
|
156
|
+
<p>Supported variable types: <code>Int16</code>, <code>Int32</code>, <code>Int64</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
|
|
154
157
|
<p>Supported access modes: <code>readonly</code> and <code>readwrite</code>.</p>
|
|
155
158
|
<p>Authentication can be configured in the editor with multiple local users and groups stored in Node-RED credentials, and anonymous login can be disabled.</p>
|
|
156
159
|
</script>
|
package/server/opcua-server.js
CHANGED
|
@@ -21,6 +21,7 @@ module.exports = function (RED) {
|
|
|
21
21
|
node.serverName = settings.serverName;
|
|
22
22
|
node.server = null;
|
|
23
23
|
node.namespace = null;
|
|
24
|
+
node.isClosing = false;
|
|
24
25
|
|
|
25
26
|
node.status({ fill: "yellow", shape: "ring", text: "initializing OPC UA server" });
|
|
26
27
|
|
|
@@ -44,12 +45,35 @@ module.exports = function (RED) {
|
|
|
44
45
|
|
|
45
46
|
registry.registerServerNames(node.serverName, node.serverName);
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
let crashHandled = false;
|
|
49
|
+
function handleUnexpectedExit(code, signal) {
|
|
50
|
+
if (node.isClosing || crashHandled) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
crashHandled = true;
|
|
54
|
+
|
|
55
|
+
const errorDetails = `OPC UA server child process exited unexpectedly with code ${code} and signal ${signal}`;
|
|
56
|
+
node.status({ fill: "red", shape: "dot", text: "Child process crashed" });
|
|
57
|
+
|
|
58
|
+
const catchMsg = {
|
|
59
|
+
topic: node.serverName,
|
|
60
|
+
payload: {
|
|
61
|
+
status: "error",
|
|
62
|
+
error: errorDetails
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
node.send(catchMsg);
|
|
66
|
+
node.error(errorDetails, catchMsg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
child.on("exit", (code, signal) => {
|
|
48
70
|
registry.unregisterChild(node.serverName, child);
|
|
71
|
+
handleUnexpectedExit(code, signal);
|
|
49
72
|
});
|
|
50
73
|
|
|
51
|
-
child.on("close", () => {
|
|
74
|
+
child.on("close", (code, signal) => {
|
|
52
75
|
registry.unregisterChild(node.serverName, child);
|
|
76
|
+
handleUnexpectedExit(code, signal);
|
|
53
77
|
});
|
|
54
78
|
|
|
55
79
|
|
|
@@ -112,6 +136,7 @@ module.exports = function (RED) {
|
|
|
112
136
|
|
|
113
137
|
node.on("close", async function (removed, done) {
|
|
114
138
|
try {
|
|
139
|
+
node.isClosing = true;
|
|
115
140
|
registry.unregisterChild(node.serverName, child);
|
|
116
141
|
registry.unregisterServerNames(node.serverName);
|
|
117
142
|
child.kill();
|
|
@@ -457,6 +457,10 @@ body.opcua-tree-modal-open {
|
|
|
457
457
|
width: calc(100% - 100px);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
.opcua-auth-card .red-ui-typedInput-container {
|
|
461
|
+
width: calc(100% - 100px) !important;
|
|
462
|
+
}
|
|
463
|
+
|
|
460
464
|
@media (max-width: 900px) {
|
|
461
465
|
.opcua-tree-modal {
|
|
462
466
|
padding: 10px;
|