@vitormnm/node-red-simple-opcua 1.6.3 → 1.7.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 (42) hide show
  1. package/README.md +89 -136
  2. package/client/lib/opcua-client-browser.js +238 -10
  3. package/client/lib/opcua-client-method-service.js +1 -1
  4. package/client/lib/opcua-client-subscription-service.js +0 -2
  5. package/client/opcua-client-config.html +118 -1
  6. package/client/opcua-client-config.js +74 -8
  7. package/client/opcua-client-help.html +6 -0
  8. package/client/opcua-client-utils.js +34 -10
  9. package/client/opcua-client.html +7 -0
  10. package/client/opcua-client.js +97 -1
  11. package/examples/flows_simple_opc.json +1 -1
  12. package/package.json +1 -1
  13. package/server/lib/opcua-address-space-alarm.js +11 -5
  14. package/server/lib/opcua-address-space-builder.js +65 -15
  15. package/server/lib/opcua-config.js +81 -23
  16. package/server/lib/opcua-server-events-child.js +1 -1
  17. package/server/lib/opcua-server-runtime-child.js +284 -19
  18. package/server/lib/opcua-server-runtime.js +49 -5
  19. package/server/lib/opcua-server-status-child.js +14 -14
  20. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  21. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  22. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  25. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  26. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  27. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  31. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  32. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  36. package/server/opcua-server-io.html +76 -0
  37. package/server/opcua-server-io.js +130 -27
  38. package/server/opcua-server.css +52 -0
  39. package/server/opcua-server.html +166 -44
  40. package/server/opcua-server.js +115 -5
  41. package/server/view/opcua-server.css +89 -6
  42. package/server/view/opcua-server.js +523 -42
@@ -196,7 +196,7 @@ class OpcUaAddressSpaceBuilder {
196
196
  const path = this.buildObjectTypePath(config.name);
197
197
  desiredEntries.set(path, this.buildEntryDefinition("objectTypeDefinition", config, path, "", "typeDefinition"));
198
198
  this.collectBranchChildren(desiredEntries, config, path, "componentOf", objectTypeConfigs, {
199
- skipAlarms: false,
199
+ skipAlarms: true,
200
200
  preserveCollectionNames: true,
201
201
  typeRootPath: path
202
202
  });
@@ -835,6 +835,9 @@ class OpcUaAddressSpaceBuilder {
835
835
  throw new Error("Object type is not available for instance " + instanceConfig.name + ": " + instanceConfig.objectsType);
836
836
  }
837
837
 
838
+ const addressSpace = this.server.engine.addressSpace;
839
+ const serverNode = addressSpace.rootFolder.objects.server;
840
+
838
841
  const objectName = instanceConfig.name;
839
842
  const nextPath = pathOverride || this.buildPath(parentPath, objectName);
840
843
  const namespace = this.getNamespaceForConfig(instanceConfig);
@@ -846,7 +849,8 @@ class OpcUaAddressSpaceBuilder {
846
849
  nodeId: this.resolveNodeId(instanceConfig, nextPath, namespace),
847
850
  rolePermissions: this.buildRolePermissions("objectTypeInstance", instanceConfig),
848
851
  typeDefinition: objectTypeEntry.node.nodeId,
849
- eventNotifier: 1
852
+ eventNotifier: 1,
853
+ eventSourceOf: serverNode
850
854
  };
851
855
 
852
856
  if (relationship === "organizedBy") {
@@ -881,24 +885,55 @@ class OpcUaAddressSpaceBuilder {
881
885
  }
882
886
 
883
887
  extractNodeIdStringValue(nodeId) {
884
- const m = String(nodeId || "").match(/(?:^|;)s=(.+)$/);
888
+ const m = String(nodeId || "").match(/(?:^|;)[si]=(.+)$/);
885
889
  return m ? m[1] : "";
886
890
  }
887
891
 
888
892
  rewriteInheritedNodeId(nodeId, typePrefix, instancePrefix) {
889
893
  if (!nodeId || !typePrefix || !instancePrefix) return nodeId;
890
- const m = String(nodeId).match(/^(ns=\d+;s=)([\s\S]*)$/);
894
+ const m = String(nodeId).match(/^(ns=\d+;[si]=)([\s\S]*)$/);
891
895
  if (m && m[2].startsWith(typePrefix)) {
892
896
  return m[1] + instancePrefix + m[2].slice(typePrefix.length);
893
897
  }
894
898
  return nodeId;
895
899
  }
896
900
 
901
+ /**
902
+ * Rewrites an alarm variableNodeId from the type to the instance.
903
+ * Handles three formats:
904
+ * 1. Full nodeId: "ns=2;s=motor.status" → "ns=2;s=myserver.motor01.status"
905
+ * 2. Plain type-relative path: "motor.status" → resolved to instance variable path
906
+ * 3. Bare variable name: "status" → resolved relative to instance path
907
+ */
908
+ rewriteInheritedAlarmVariableRef(variableRef, typePrefix, instancePrefix, instancePath) {
909
+ if (!variableRef) return variableRef;
910
+ const ref = String(variableRef).trim();
911
+
912
+ // Try full nodeId rewrite first (ns=X;s=...)
913
+ const rewritten = this.rewriteInheritedNodeId(ref, typePrefix, instancePrefix);
914
+ if (rewritten !== ref) {
915
+ return rewritten;
916
+ }
917
+
918
+ // Plain path starting with typePrefix (e.g. "motor.status" → "myserver.motor01.status")
919
+ if (typePrefix && instancePrefix && ref.startsWith(typePrefix)) {
920
+ return instancePrefix + ref.slice(typePrefix.length);
921
+ }
922
+
923
+ // Bare variable name (e.g. "status") → resolve relative to instance path
924
+ if (ref.indexOf(".") === -1 && instancePath) {
925
+ return this.buildPath(instancePath, ref);
926
+ }
927
+
928
+ return ref;
929
+ }
930
+
897
931
  createInheritedBranchChildren(typeConfig, parentOpcNode, parentPath, typePrefix, instancePrefix) {
898
932
  const variables = Array.isArray(typeConfig.variables) ? typeConfig.variables : [];
899
933
  const methods = Array.isArray(typeConfig.methods) ? typeConfig.methods : [];
900
934
  const folders = Array.isArray(typeConfig.folders) ? typeConfig.folders : [];
901
935
  const objects = Array.isArray(typeConfig.objects) ? typeConfig.objects : [];
936
+ const alarms = Array.isArray(typeConfig.alarms) ? typeConfig.alarms : [];
902
937
 
903
938
  variables.forEach((varConfig) => {
904
939
  const childPath = this.buildPath(parentPath, varConfig.name);
@@ -939,6 +974,17 @@ class OpcUaAddressSpaceBuilder {
939
974
  this.createInheritedBranchChildren(objectConfig, childNode, childPath, typePrefix, instancePrefix);
940
975
  }
941
976
  });
977
+
978
+ alarms.forEach((alarmConfig) => {
979
+ const childPath = this.buildPath(parentPath, alarmConfig.name);
980
+ const rewrittenConfig = Object.assign({}, alarmConfig, {
981
+ nodeId: this.rewriteInheritedNodeId(alarmConfig.nodeId, typePrefix, instancePrefix),
982
+ variableNodeId: this.rewriteInheritedAlarmVariableRef(
983
+ alarmConfig.variableNodeId, typePrefix, instancePrefix, parentPath
984
+ )
985
+ });
986
+ this.addAlarm(parentOpcNode, rewrittenConfig, parentPath, "componentOf", childPath);
987
+ });
942
988
  }
943
989
 
944
990
  addAlarm(parentNode, alarmConfig, parentPath, relationship, pathOverride) {
@@ -1103,11 +1149,15 @@ class OpcUaAddressSpaceBuilder {
1103
1149
  const path = pathOverride || this.buildPath(parentPath, name);
1104
1150
  const nodeId = this.resolveNodeId(variableConfig, path, namespace);
1105
1151
  const browseName = variableConfig.displayName || name;
1152
+ let initialValue = variableConfig.value;
1153
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1154
+ initialValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1155
+ }
1106
1156
  const state = {
1107
1157
  type,
1108
1158
  access,
1109
- isArray: this.isArrayValue(variableConfig.value),
1110
- currentValue: this.coerceValue(variableConfig.value, type, this.isArrayValue(variableConfig.value))
1159
+ isArray: this.isArrayValue(initialValue),
1160
+ currentValue: this.coerceValue(initialValue, type, this.isArrayValue(initialValue))
1111
1161
  };
1112
1162
 
1113
1163
  const variableNode = namespace.addVariable({
@@ -1125,6 +1175,9 @@ class OpcUaAddressSpaceBuilder {
1125
1175
  minimumSamplingInterval: 500,
1126
1176
  value: {
1127
1177
  get: () => {
1178
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1179
+ state.currentValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1180
+ }
1128
1181
  this.emitTagAccess("read", {
1129
1182
  path,
1130
1183
  nodeID: nodeId,
@@ -1186,11 +1239,15 @@ class OpcUaAddressSpaceBuilder {
1186
1239
  value: state.currentValue
1187
1240
  });
1188
1241
 
1242
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1243
+ this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate = !!state.currentValue;
1244
+ console.log(`AcceptAllCertificates updated by client to: ${state.currentValue}`);
1245
+ console.log(`Server Certificate Manager automaticallyAcceptUnknownCertificate is now: ${this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate}`);
1246
+ }
1247
+
1189
1248
  const alarm = this.variableStore.get(path).alarm
1190
1249
  this.addressSpaceAlarm.checkAlarm(alarm, variant.value)
1191
1250
 
1192
-
1193
-
1194
1251
  return StatusCodes.Good;
1195
1252
  } catch (error) {
1196
1253
  console.error("addVariable")
@@ -1425,13 +1482,6 @@ class OpcUaAddressSpaceBuilder {
1425
1482
 
1426
1483
  if (reference.indexOf(".") === 0) {
1427
1484
  const relativeReference = this.resolveObjectTypeRelativeReference(parentPath, reference);
1428
-
1429
- //not work
1430
- var corrente = parentNode.getComponentByName("corrent")
1431
-
1432
- return {
1433
- node: corrente
1434
- }
1435
1485
  if (this.variableStore.has(relativeReference)) {
1436
1486
  return this.variableStore.get(relativeReference);
1437
1487
  }
@@ -26,14 +26,18 @@ class OpcUaServerConfigParser {
26
26
  serverName: config.serverName || DEFAULT_SERVER_NAME,
27
27
  port: normalizePort(config.port),
28
28
  maxConnections: this.normalizeMaxConnections(config.maxConnections),
29
+ minSessionTimeout: this.normalizeSessionTimeout(config.minSessionTimeout),
30
+ defaultSessionTimeout: this.normalizeSessionTimeout(config.defaultSessionTimeout),
31
+ maxSessionTimeout: this.normalizeSessionTimeout(config.maxSessionTimeout),
29
32
  namespaceUri: config.namespaceUri || DEFAULT_NAMESPACE_URI,
30
33
  resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
31
34
  treeConfig: this.parseTreeConfig(config.tree),
32
35
  allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
36
+ automaticallyAcceptUnknownCertificate: this.normalizeAutomaticallyAcceptUnknownCertificate(config.automaticallyAcceptUnknownCertificate),
33
37
  groups: auth.groups,
34
38
  users: auth.users,
35
- securityPolicy: security.securityPolicy,
36
- securityMode: security.securityMode
39
+ securityPolicies: security.securityPolicies,
40
+ securityModes: security.securityModes
37
41
  };
38
42
  }
39
43
 
@@ -509,17 +513,18 @@ class OpcUaServerConfigParser {
509
513
  nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
510
514
  namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
511
515
  accessPermission: this.normalizeAccessPermissions(alarmConfig.accessPermission || alarmConfig.accessPermissions),
512
- enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
516
+ enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true,
517
+ sendValue: typeof alarmConfig.sendValue === "boolean" ? alarmConfig.sendValue : true
513
518
  };
514
519
 
515
520
  if (type === "levelAlarm") {
516
- base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 100;
521
+ base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 90;
517
522
  base.highHighMessage = typeof alarmConfig.highHighMessage === "string" ? alarmConfig.highHighMessage : "High High alarm";
518
523
  base.highLimit = Number.isFinite(Number(alarmConfig.highLimit)) ? Number(alarmConfig.highLimit) : 80;
519
524
  base.highMessage = typeof alarmConfig.highMessage === "string" ? alarmConfig.highMessage : "High alarm";
520
525
  base.lowLimit = Number.isFinite(Number(alarmConfig.lowLimit)) ? Number(alarmConfig.lowLimit) : 20;
521
526
  base.lowMessage = typeof alarmConfig.lowMessage === "string" ? alarmConfig.lowMessage : "Low alarm";
522
- base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 0;
527
+ base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 10;
523
528
  base.lowLowMessage = typeof alarmConfig.lowLowMessage === "string" ? alarmConfig.lowLowMessage : "Low Low alarm";
524
529
  } else if (type === "digitalAlarm") {
525
530
  base.normalStateValue = Number.isFinite(Number(alarmConfig.normalStateValue)) ? Number(alarmConfig.normalStateValue) : 0;
@@ -643,6 +648,17 @@ class OpcUaServerConfigParser {
643
648
  return value !== false;
644
649
  }
645
650
 
651
+ normalizeAutomaticallyAcceptUnknownCertificate(value) {
652
+ if (value === undefined) {
653
+ return true;
654
+ }
655
+ if (typeof value === "string") {
656
+ return value !== "false";
657
+ }
658
+
659
+ return value !== false;
660
+ }
661
+
646
662
  normalizeMaxConnections(value) {
647
663
  const parsed = Number(value);
648
664
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -651,30 +667,72 @@ class OpcUaServerConfigParser {
651
667
  return parsed;
652
668
  }
653
669
 
654
- applySecuritySettings(policy, mode) {
655
- const rawPolicy = typeof policy === "string" ? policy.trim() : "None";
656
- const rawMode = typeof mode === "string" ? mode.trim() : "None";
670
+ normalizeSessionTimeout(value) {
671
+ if (value === undefined || value === null || value === "") {
672
+ return undefined;
673
+ }
674
+ const parsed = Number(value);
675
+ if (!Number.isInteger(parsed) || parsed < 0) {
676
+ return undefined;
677
+ }
678
+ return parsed;
679
+ }
680
+
681
+ applySecuritySettings(policies, modes) {
682
+ // policies and modes can be comma-separated strings, e.g. "None,Basic256Sha256"
683
+ const rawPolicies = typeof policies === "string" ? policies.split(",").map(p => p.trim()).filter(Boolean) : ["None"];
684
+ const rawModes = typeof modes === "string" ? modes.split(",").map(m => m.trim()).filter(Boolean) : ["None"];
657
685
 
658
- let securityPolicy = Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, rawPolicy)
659
- ? SECURITY_POLICY_MAP[rawPolicy]
660
- : SECURITY_POLICY_MAP.None;
661
- let securityMode = Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, rawMode)
662
- ? SECURITY_MODE_MAP[rawMode]
663
- : SECURITY_MODE_MAP.None;
686
+ let securityPolicies = [];
687
+ rawPolicies.forEach(policy => {
688
+ if (Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, policy)) {
689
+ securityPolicies.push(SECURITY_POLICY_MAP[policy]);
690
+ }
691
+ });
692
+ if (securityPolicies.length === 0) {
693
+ securityPolicies.push(SECURITY_POLICY_MAP.None);
694
+ }
664
695
 
665
- if (securityMode === MessageSecurityMode.None) {
666
- securityPolicy = SecurityPolicy.None;
667
- if (rawPolicy !== "None") {
668
- this.node.warn("Security policy adjusted to None because security mode is None");
696
+ let securityModes = [];
697
+ rawModes.forEach(mode => {
698
+ if (Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, mode)) {
699
+ securityModes.push(SECURITY_MODE_MAP[mode]);
700
+ }
701
+ });
702
+ if (securityModes.length === 0) {
703
+ securityModes.push(SECURITY_MODE_MAP.None);
704
+ }
705
+
706
+ const hasNoneMode = securityModes.includes(MessageSecurityMode.None);
707
+ const hasSignedMode = securityModes.some(m => m !== MessageSecurityMode.None);
708
+
709
+ if (hasNoneMode && !hasSignedMode) {
710
+ // ONLY None mode is selected, so policy MUST be None
711
+ securityPolicies = [SecurityPolicy.None];
712
+ } else if (!hasNoneMode && hasSignedMode) {
713
+ // ONLY signed modes are selected, so policy cannot be None
714
+ securityPolicies = securityPolicies.filter(p => p !== SecurityPolicy.None);
715
+ if (securityPolicies.length === 0) {
716
+ securityPolicies.push(SecurityPolicy.Basic256Sha256);
717
+ this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
718
+ }
719
+ } else {
720
+ // Both None and signed modes are selected. Policies can be a mix.
721
+ // If the user selected a signed mode but only selected "None" policy, we must add a default signed policy.
722
+ const hasSignedPolicy = securityPolicies.some(p => p !== SecurityPolicy.None);
723
+ if (!hasSignedPolicy) {
724
+ securityPolicies.push(SecurityPolicy.Basic256Sha256);
725
+ this.node.warn("Added default Security Policy Basic256Sha256 because signed modes require a policy");
726
+ }
727
+ // Ensure None policy is present for the None mode
728
+ if (!securityPolicies.includes(SecurityPolicy.None)) {
729
+ securityPolicies.push(SecurityPolicy.None);
669
730
  }
670
- } else if (securityPolicy === SecurityPolicy.None) {
671
- securityPolicy = SecurityPolicy.Basic256Sha256;
672
- this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
673
731
  }
674
732
 
675
733
  return {
676
- securityPolicy,
677
- securityMode
734
+ securityPolicies,
735
+ securityModes
678
736
  };
679
737
  }
680
738
 
@@ -9,7 +9,7 @@ const { resolveRegisteredServer } = require("./server-node-utils");
9
9
 
10
10
  function eventsServer(node, rootNode, nodeId) {
11
11
 
12
-
12
+ node.intervalMs = rootNode.intervalMs;
13
13
 
14
14
  // 🔥 ALTERADO: usar Map para garantir unicidade por nodeID
15
15
  node.queue = {