@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.
@@ -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: payload.map((item) => this.readPayloadItem(identifierType, item)),
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: server.readValue(identifierType, identifier),
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
- this.node.writeValue(
369
- identifierType,
370
- identifier,
371
- Buffer.isBuffer(payload)
372
- ? payload
373
- : Buffer.from(payload)
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
- this.node.writeValue(
388
- identifierType,
389
- identifier,
390
- isByteString
391
- ? Buffer.from(payload)
392
- : payload
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.forEach(item => {
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
- this.node.writeValue(
432
- identifierType,
433
- identifier,
434
- payload[identifier]
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
- this.node.writeValue(
447
- identifierType,
448
- identifier,
449
- payload
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: this.node.readValue(identifierType, identifier)
642
+ value,
643
+ status
547
644
  };
548
645
  }
549
646
 
550
647
  writePayloadItem(identifierType, item) {
551
648
  const identifier = this.resolvePayloadItemIdentifier(item);
552
- this.node.writeValue(identifierType, identifier, item.value);
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: item.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: 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: 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 customRole = this.resolveGroupRoleNodeId(user.group);
242
- if (customRole) {
243
- roles.push(customRole);
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({
@@ -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>
@@ -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
- child.on("exit", () => {
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;