@vitormnm/node-red-simple-opcua 1.6.3 → 1.8.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 (45) hide show
  1. package/README.md +104 -136
  2. package/client/lib/opcua-client-browser.js +254 -11
  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/lib/opcua-client-write-service.js +14 -4
  6. package/client/opcua-client-config.html +118 -1
  7. package/client/opcua-client-config.js +112 -9
  8. package/client/opcua-client-help.html +6 -0
  9. package/client/opcua-client-utils.js +158 -10
  10. package/client/opcua-client.html +8 -0
  11. package/client/opcua-client.js +97 -1
  12. package/client/view/opcua-client.js +106 -14
  13. package/examples/flows_simple_opc.json +1 -1
  14. package/package.json +2 -2
  15. package/server/lib/opcua-address-space-alarm.js +95 -32
  16. package/server/lib/opcua-address-space-builder.js +717 -59
  17. package/server/lib/opcua-config.js +110 -35
  18. package/server/lib/opcua-server-events-child.js +31 -5
  19. package/server/lib/opcua-server-runtime-child.js +424 -27
  20. package/server/lib/opcua-server-runtime.js +52 -5
  21. package/server/lib/opcua-server-status-child.js +46 -15
  22. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  25. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  26. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  27. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  31. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  32. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  36. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  37. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  38. package/server/opcua-server-io.html +93 -1
  39. package/server/opcua-server-io.js +153 -29
  40. package/server/opcua-server-registry.js +8 -2
  41. package/server/opcua-server.css +64 -0
  42. package/server/opcua-server.html +168 -44
  43. package/server/opcua-server.js +115 -5
  44. package/server/view/opcua-server.css +100 -6
  45. package/server/view/opcua-server.js +746 -48
@@ -14,6 +14,8 @@ const {
14
14
 
15
15
  const { OpcUaAddressSpaceAlarm } = require("./opcua-address-space-alarm")
16
16
 
17
+ const activeReads = new WeakMap();
18
+
17
19
  class OpcUaAddressSpaceBuilder {
18
20
  constructor(options) {
19
21
  this.namespace = options.namespace;
@@ -24,6 +26,7 @@ class OpcUaAddressSpaceBuilder {
24
26
  this.serverName = options.serverName;
25
27
  const hasUsers = Array.isArray(options.users) && options.users.length > 0;
26
28
  this.authorizationDisabled = !hasUsers;
29
+ this.users = options.users || [];
27
30
  this.nodeEntries = new Map();
28
31
  this.variableStore = new Map();
29
32
  this.variableNodeIdStore = new Map();
@@ -144,7 +147,7 @@ class OpcUaAddressSpaceBuilder {
144
147
  return Number.isFinite(num) ? num : v;
145
148
  };
146
149
  if (record.isArray) {
147
- val = Array.isArray(val) ? val.map(convertToNumber) : convertToNumber(val);
150
+ val = this.recursiveMap(val, convertToNumber, record.type);
148
151
  } else {
149
152
  val = convertToNumber(val);
150
153
  }
@@ -196,7 +199,7 @@ class OpcUaAddressSpaceBuilder {
196
199
  const path = this.buildObjectTypePath(config.name);
197
200
  desiredEntries.set(path, this.buildEntryDefinition("objectTypeDefinition", config, path, "", "typeDefinition"));
198
201
  this.collectBranchChildren(desiredEntries, config, path, "componentOf", objectTypeConfigs, {
199
- skipAlarms: false,
202
+ skipAlarms: true,
200
203
  preserveCollectionNames: true,
201
204
  typeRootPath: path
202
205
  });
@@ -758,11 +761,18 @@ class OpcUaAddressSpaceBuilder {
758
761
 
759
762
  const objectName = objectConfig.name;
760
763
  const nextPath = pathOverride || this.buildPath(parentPath, objectName);
764
+ const nodeId = this.resolveNodeId(objectConfig, nextPath, namespace);
765
+ const existingNode = addressSpace.findNode(nodeId);
766
+ if (existingNode) {
767
+ this.registerNodeEntry("object", nextPath, parentPath, relationship, objectConfig, existingNode, namespace);
768
+ return;
769
+ }
770
+
761
771
  const options = {
762
772
  browseName: objectConfig.displayName || objectName,
763
773
  displayName: objectConfig.displayName || objectName,
764
774
  description: objectConfig.description || "",
765
- nodeId: this.resolveNodeId(objectConfig, nextPath, namespace),
775
+ nodeId: nodeId,
766
776
  rolePermissions: this.buildRolePermissions("object", objectConfig),
767
777
  eventNotifier: 1, //enabled_events,
768
778
  eventSourceOf: serverNode
@@ -784,47 +794,78 @@ class OpcUaAddressSpaceBuilder {
784
794
 
785
795
  addObjectTypeDefinition(objectTypeConfig) {
786
796
  const namespace = this.getNamespaceForConfig(objectTypeConfig);
797
+ const path = this.buildObjectTypePath(objectTypeConfig.name);
798
+ const nodeId = this.resolveNodeId(objectTypeConfig, path, namespace);
799
+ const addressSpace = this.server.engine.addressSpace;
800
+ const existingNode = addressSpace.findNode(nodeId);
801
+ if (existingNode) {
802
+ this.registerNodeEntry("objectTypeDefinition", path, "", "typeDefinition", objectTypeConfig, existingNode, namespace);
803
+ this.objectTypeStore.set(objectTypeConfig.name, {
804
+ node: existingNode,
805
+ config: objectTypeConfig,
806
+ path: path,
807
+ namespace: namespace
808
+ });
809
+ return;
810
+ }
811
+
787
812
  const objectTypeNode = namespace.addObjectType({
788
813
  browseName: objectTypeConfig.name,
789
814
  displayName: objectTypeConfig.displayName || objectTypeConfig.name,
790
815
  description: objectTypeConfig.description || "",
791
- nodeId: this.resolveNodeId(objectTypeConfig, this.buildObjectTypePath(objectTypeConfig.name), namespace),
816
+ nodeId: nodeId,
792
817
  rolePermissions: this.buildRolePermissions("objectTypeDefinition", objectTypeConfig),
793
818
  subtypeOf: "BaseObjectType"
794
819
  });
795
820
 
796
821
 
797
- const path = this.buildObjectTypePath(objectTypeConfig.name);
822
+ const path2 = this.buildObjectTypePath(objectTypeConfig.name);
798
823
 
799
824
 
800
825
 
801
- this.registerNodeEntry("objectTypeDefinition", path, "", "typeDefinition", objectTypeConfig, objectTypeNode, namespace);
826
+ this.registerNodeEntry("objectTypeDefinition", path2, "", "typeDefinition", objectTypeConfig, objectTypeNode, namespace);
802
827
  this.objectTypeStore.set(objectTypeConfig.name, {
803
828
  node: objectTypeNode,
804
829
  config: objectTypeConfig,
805
- path: path,
830
+ path: path2,
806
831
  namespace: namespace
807
832
  });
808
833
  }
809
834
 
810
835
  addEnumerationTypeDefinition(config) {
811
836
  const namespace = this.getNamespaceForConfig(config);
837
+ const path = this.buildEnumerationPath(config.name);
838
+ const nodeId = this.resolveNodeId(config, path, namespace);
839
+ const addressSpace = this.server.engine.addressSpace;
840
+ const existingNode = addressSpace.findNode(nodeId);
841
+ if (existingNode) {
842
+ this.registerNodeEntry("enumeration", path, "", "typeDefinition", config, existingNode, namespace);
843
+ if (!this.enumerationStore) this.enumerationStore = new Map();
844
+ this.enumerationStore.set(config.name, {
845
+ node: existingNode,
846
+ config: config,
847
+ path: path,
848
+ namespace: namespace
849
+ });
850
+ return;
851
+ }
852
+
812
853
  const enumTypeNode = namespace.addEnumerationType({
813
854
  browseName: config.displayName || config.name,
814
855
  displayName: config.displayName || config.name,
815
856
  description: config.description || "",
816
- nodeId: this.resolveNodeId(config, this.buildEnumerationPath(config.name), namespace),
857
+ nodeId: nodeId,
817
858
  enumeration: config.enumeration
818
859
  });
819
860
 
820
- const path = this.buildEnumerationPath(config.name);
861
+ const path2 = this.buildEnumerationPath(config.name);
821
862
 
822
- this.registerNodeEntry("enumeration", path, "", "typeDefinition", config, enumTypeNode, namespace);
863
+ this.registerNodeEntry("enumeration", path2, "", "typeDefinition", config, enumTypeNode, namespace);
823
864
  if (!this.enumerationStore) this.enumerationStore = new Map();
824
865
  this.enumerationStore.set(config.name, {
825
866
  node: enumTypeNode,
826
867
  config: config,
827
- path: path,
868
+ path: path2,
828
869
  namespace: namespace
829
870
  });
830
871
  }
@@ -835,18 +876,30 @@ class OpcUaAddressSpaceBuilder {
835
876
  throw new Error("Object type is not available for instance " + instanceConfig.name + ": " + instanceConfig.objectsType);
836
877
  }
837
878
 
879
+ const addressSpace = this.server.engine.addressSpace;
880
+ const serverNode = addressSpace.rootFolder.objects.server;
881
+
838
882
  const objectName = instanceConfig.name;
839
883
  const nextPath = pathOverride || this.buildPath(parentPath, objectName);
840
884
  const namespace = this.getNamespaceForConfig(instanceConfig);
885
+ const nodeId = this.resolveNodeId(instanceConfig, nextPath, namespace);
886
+
887
+ const existingNode = addressSpace.findNode(nodeId);
888
+ if (existingNode) {
889
+ this.registerNodeEntry("objectTypeInstance", nextPath, parentPath, relationship, instanceConfig, existingNode, namespace);
890
+ this.createInheritedChildren(existingNode, nextPath, instanceConfig, namespace);
891
+ return;
892
+ }
841
893
 
842
894
  const options = {
843
895
  browseName: instanceConfig.displayName || objectName,
844
896
  displayName: instanceConfig.displayName || objectName,
845
897
  description: instanceConfig.description || "",
846
- nodeId: this.resolveNodeId(instanceConfig, nextPath, namespace),
898
+ nodeId: nodeId,
847
899
  rolePermissions: this.buildRolePermissions("objectTypeInstance", instanceConfig),
848
900
  typeDefinition: objectTypeEntry.node.nodeId,
849
- eventNotifier: 1
901
+ eventNotifier: 1,
902
+ eventSourceOf: serverNode
850
903
  };
851
904
 
852
905
  if (relationship === "organizedBy") {
@@ -856,6 +909,21 @@ class OpcUaAddressSpaceBuilder {
856
909
  }
857
910
 
858
911
  const objectNode = namespace.addObject(options);
912
+
913
+ // Remove automatically instantiated children to let createInheritedChildren
914
+ // create them with the correct rewritten NodeIds.
915
+ const autoChildren = [];
916
+ if (typeof objectNode.getComponents === "function") autoChildren.push(...objectNode.getComponents());
917
+ if (typeof objectNode.getProperties === "function") autoChildren.push(...objectNode.getProperties());
918
+ if (typeof objectNode.getMethods === "function") autoChildren.push(...objectNode.getMethods());
919
+ for (const child of autoChildren) {
920
+ try {
921
+ addressSpace.deleteNode(child.nodeId);
922
+ } catch (err) {
923
+ // Ignore
924
+ }
925
+ }
926
+
859
927
  this.registerNodeEntry("objectTypeInstance", nextPath, parentPath, relationship, instanceConfig, objectNode, namespace);
860
928
 
861
929
  // Create the inherited children explicitly using the configs that opcua-config.js
@@ -872,8 +940,8 @@ class OpcUaAddressSpaceBuilder {
872
940
  // Extract the type's nodeId value prefix (e.g. "Motor_type2") and the
873
941
  // instance's nodeId value prefix (e.g. "server1.newObjectType") so we can
874
942
  // rewrite every child nodeId from the type to the instance on-the-fly.
875
- const typeNodeId = typeEntry.config.nodeId || "";
876
- const instanceNodeId = instanceConfig.nodeId || "";
943
+ const typeNodeId = typeEntry.config.nodeId || ("ns=2;s=" + typeEntry.config.name);
944
+ const instanceNodeId = instanceConfig.nodeId || instanceNode.nodeId.toString();
877
945
  const typePrefix = this.extractNodeIdStringValue(typeNodeId);
878
946
  const instancePrefix = this.extractNodeIdStringValue(instanceNodeId);
879
947
 
@@ -881,24 +949,55 @@ class OpcUaAddressSpaceBuilder {
881
949
  }
882
950
 
883
951
  extractNodeIdStringValue(nodeId) {
884
- const m = String(nodeId || "").match(/(?:^|;)s=(.+)$/);
952
+ const m = String(nodeId || "").match(/(?:^|;)[si]=(.+)$/);
885
953
  return m ? m[1] : "";
886
954
  }
887
955
 
888
956
  rewriteInheritedNodeId(nodeId, typePrefix, instancePrefix) {
889
957
  if (!nodeId || !typePrefix || !instancePrefix) return nodeId;
890
- const m = String(nodeId).match(/^(ns=\d+;s=)([\s\S]*)$/);
958
+ const m = String(nodeId).match(/^(ns=\d+;[si]=)([\s\S]*)$/);
891
959
  if (m && m[2].startsWith(typePrefix)) {
892
960
  return m[1] + instancePrefix + m[2].slice(typePrefix.length);
893
961
  }
894
962
  return nodeId;
895
963
  }
896
964
 
965
+ /**
966
+ * Rewrites an alarm variableNodeId from the type to the instance.
967
+ * Handles three formats:
968
+ * 1. Full nodeId: "ns=2;s=motor.status" → "ns=2;s=myserver.motor01.status"
969
+ * 2. Plain type-relative path: "motor.status" → resolved to instance variable path
970
+ * 3. Bare variable name: "status" → resolved relative to instance path
971
+ */
972
+ rewriteInheritedAlarmVariableRef(variableRef, typePrefix, instancePrefix, instancePath) {
973
+ if (!variableRef) return variableRef;
974
+ const ref = String(variableRef).trim();
975
+
976
+ // Try full nodeId rewrite first (ns=X;s=...)
977
+ const rewritten = this.rewriteInheritedNodeId(ref, typePrefix, instancePrefix);
978
+ if (rewritten !== ref) {
979
+ return rewritten;
980
+ }
981
+
982
+ // Plain path starting with typePrefix (e.g. "motor.status" → "myserver.motor01.status")
983
+ if (typePrefix && instancePrefix && ref.startsWith(typePrefix)) {
984
+ return instancePrefix + ref.slice(typePrefix.length);
985
+ }
986
+
987
+ // Bare variable name (e.g. "status") → resolve relative to instance path
988
+ if (ref.indexOf(".") === -1 && instancePath) {
989
+ return this.buildPath(instancePath, ref);
990
+ }
991
+
992
+ return ref;
993
+ }
994
+
897
995
  createInheritedBranchChildren(typeConfig, parentOpcNode, parentPath, typePrefix, instancePrefix) {
898
996
  const variables = Array.isArray(typeConfig.variables) ? typeConfig.variables : [];
899
997
  const methods = Array.isArray(typeConfig.methods) ? typeConfig.methods : [];
900
998
  const folders = Array.isArray(typeConfig.folders) ? typeConfig.folders : [];
901
999
  const objects = Array.isArray(typeConfig.objects) ? typeConfig.objects : [];
1000
+ const alarms = Array.isArray(typeConfig.alarms) ? typeConfig.alarms : [];
902
1001
 
903
1002
  variables.forEach((varConfig) => {
904
1003
  const childPath = this.buildPath(parentPath, varConfig.name);
@@ -939,6 +1038,17 @@ class OpcUaAddressSpaceBuilder {
939
1038
  this.createInheritedBranchChildren(objectConfig, childNode, childPath, typePrefix, instancePrefix);
940
1039
  }
941
1040
  });
1041
+
1042
+ alarms.forEach((alarmConfig) => {
1043
+ const childPath = this.buildPath(parentPath, alarmConfig.name);
1044
+ const rewrittenConfig = Object.assign({}, alarmConfig, {
1045
+ nodeId: this.rewriteInheritedNodeId(alarmConfig.nodeId, typePrefix, instancePrefix),
1046
+ variableNodeId: this.rewriteInheritedAlarmVariableRef(
1047
+ alarmConfig.variableNodeId, typePrefix, instancePrefix, parentPath
1048
+ )
1049
+ });
1050
+ this.addAlarm(parentOpcNode, rewrittenConfig, parentPath, "componentOf", childPath);
1051
+ });
942
1052
  }
943
1053
 
944
1054
  addAlarm(parentNode, alarmConfig, parentPath, relationship, pathOverride) {
@@ -971,19 +1081,52 @@ class OpcUaAddressSpaceBuilder {
971
1081
 
972
1082
  const sourceName = variableToMonitor.node.displayName[0].text
973
1083
 
974
- const browseName = alarmConfig.displayName || objectName
975
- const inputNode = variableToMonitor.node
976
-
977
-
1084
+ const browseName = alarmConfig.displayName || objectName;
1085
+ const inputNode = variableToMonitor.node;
978
1086
 
979
1087
  const conditionName = alarmConfig.displayName || objectName
980
1088
  const nodeId = this.resolveNodeId(alarmConfig, nextPath, namespace)
981
1089
 
1090
+ const addressSpace = this.server.engine.addressSpace;
1091
+ const existingNode = addressSpace.findNode(nodeId);
1092
+ if (existingNode) {
1093
+ if (alarmConfig.type === "levelAlarm") {
1094
+ const lowLow = existingNode.getPropertyByName("lowLowLimit");
1095
+ if (lowLow) lowLow.setValueFromSource({ dataType: DataType.Int32, value: alarmConfig.lowLowLimit });
1096
+ const low = existingNode.getPropertyByName("lowLimit");
1097
+ if (low) low.setValueFromSource({ dataType: DataType.Int32, value: alarmConfig.lowLimit });
1098
+ const highHigh = existingNode.getPropertyByName("highHighLimit");
1099
+ if (highHigh) highHigh.setValueFromSource({ dataType: DataType.Int32, value: alarmConfig.highHighLimit });
1100
+ const high = existingNode.getPropertyByName("highLimit");
1101
+ if (high) high.setValueFromSource({ dataType: DataType.Int32, value: alarmConfig.highLimit });
1102
+ }
1103
+ const enabled = existingNode.getPropertyByName("enabled");
1104
+ if (enabled) enabled.setValueFromSource({ dataType: DataType.Boolean, value: alarmConfig.enabled });
1105
+ existingNode.sourceName.setValueFromSource({ dataType: DataType.String, value: sourceName });
1106
+ existingNode.alarmConfig = alarmConfig;
1107
+
1108
+ const alarmRolePermissions = this.buildRolePermissions("alarm", alarmConfig);
1109
+ if (alarmRolePermissions) {
1110
+ existingNode.setRolePermissions(alarmRolePermissions);
1111
+ }
1112
+ existingNode.path = nextPath;
1113
+
1114
+ variableToMonitor.alarm = {
1115
+ node: existingNode,
1116
+ alarmConfig: alarmConfig,
1117
+ };
1118
+ this.registerNodeEntry("alarm", nextPath, parentPath, relationship, alarmConfig, existingNode, namespace);
1119
+ return;
1120
+ }
1121
+
982
1122
  const alarmNode = this.addressSpaceAlarm.createAlarm(namespace, browseName, parentNode, inputNode, conditionName, nodeId, sourceName, alarmConfig)
983
1123
  const alarmRolePermissions = this.buildRolePermissions("alarm", alarmConfig);
984
1124
  if (alarmNode && alarmRolePermissions) {
985
1125
  alarmNode.setRolePermissions(alarmRolePermissions);
986
1126
  }
1127
+ if (alarmNode) {
1128
+ alarmNode.path = nextPath;
1129
+ }
987
1130
 
988
1131
 
989
1132
 
@@ -1062,11 +1205,18 @@ class OpcUaAddressSpaceBuilder {
1062
1205
 
1063
1206
  const folderName = folderConfig.name;
1064
1207
  const nextPath = pathOverride || this.buildPath(parentPath, folderName);
1208
+ const nodeId = this.resolveNodeId(folderConfig, nextPath, namespace);
1209
+ const existingNode = addressSpace.findNode(nodeId);
1210
+ if (existingNode) {
1211
+ this.registerNodeEntry("folder", nextPath, parentPath, relationship, folderConfig, existingNode, namespace);
1212
+ return;
1213
+ }
1214
+
1065
1215
  const options = {
1066
1216
  browseName: folderConfig.displayName || folderName,
1067
1217
  displayName: folderConfig.displayName || folderName,
1068
1218
  description: folderConfig.description || "",
1069
- nodeId: this.resolveNodeId(folderConfig, nextPath, namespace),
1219
+ nodeId: nodeId,
1070
1220
  rolePermissions: this.buildRolePermissions("folder", folderConfig),
1071
1221
  typeDefinition: "FolderType",
1072
1222
  eventSourceOf: serverNode,
@@ -1103,13 +1253,97 @@ class OpcUaAddressSpaceBuilder {
1103
1253
  const path = pathOverride || this.buildPath(parentPath, name);
1104
1254
  const nodeId = this.resolveNodeId(variableConfig, path, namespace);
1105
1255
  const browseName = variableConfig.displayName || name;
1256
+ let initialValue = variableConfig.value;
1257
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1258
+ initialValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1259
+ }
1260
+
1261
+ const dimensions = this.getArrayDimensions(initialValue, type);
1106
1262
  const state = {
1107
1263
  type,
1108
1264
  access,
1109
- isArray: this.isArrayValue(variableConfig.value),
1110
- currentValue: this.coerceValue(variableConfig.value, type, this.isArrayValue(variableConfig.value))
1265
+ isArray: this.isArrayValue(initialValue),
1266
+ isMatrix: !!dimensions,
1267
+ dimensions: dimensions,
1268
+ currentValue: this.coerceValue(initialValue, type, this.isArrayValue(initialValue))
1111
1269
  };
1112
1270
 
1271
+ const addressSpace = this.server.engine.addressSpace;
1272
+ const existingNode = addressSpace.findNode(nodeId);
1273
+ if (existingNode) {
1274
+ this.registerNodeEntry("variable", path, parentPath, "componentOf", variableConfig, existingNode, namespace);
1275
+ const record = {
1276
+ node: existingNode,
1277
+ path: path,
1278
+ nodeId: nodeId,
1279
+ nodeIdKey: this.normalizeNodeIdKey(nodeId),
1280
+ type: state.type,
1281
+ isArray: state.isArray,
1282
+ isMatrix: state.isMatrix,
1283
+ dimensions: state.dimensions,
1284
+ getValue: () => state.currentValue,
1285
+ setRuntimeValue: (nextValue) => {
1286
+ if (state.isMatrix) {
1287
+ let parsedVal = nextValue;
1288
+ if (typeof nextValue === "string") {
1289
+ const trimmed = nextValue.trim();
1290
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1291
+ parsedVal = JSON.parse(trimmed);
1292
+ }
1293
+ }
1294
+ const dims = this.getArrayDimensions(parsedVal, state.type);
1295
+ if (dims) {
1296
+ state.dimensions = dims;
1297
+ state.currentValue = this.coerceValue(parsedVal, state.type, true);
1298
+ } else {
1299
+ const coercedFlat = this.coerceValue(parsedVal, state.type, true);
1300
+ state.currentValue = this.reshapeArray(coercedFlat, state.dimensions);
1301
+ }
1302
+ } else {
1303
+ state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1304
+ }
1305
+ return state.currentValue;
1306
+ },
1307
+ setValue: (nextValue) => {
1308
+ if (state.access !== "readwrite") {
1309
+ throw new Error("Tag is read-only: " + path);
1310
+ }
1311
+
1312
+ if (state.isMatrix) {
1313
+ let parsedVal = nextValue;
1314
+ if (typeof nextValue === "string") {
1315
+ const trimmed = nextValue.trim();
1316
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1317
+ parsedVal = JSON.parse(trimmed);
1318
+ }
1319
+ }
1320
+ const dims = this.getArrayDimensions(parsedVal, state.type);
1321
+ if (dims) {
1322
+ state.dimensions = dims;
1323
+ state.currentValue = this.coerceValue(parsedVal, state.type, true);
1324
+ } else {
1325
+ const coercedFlat = this.coerceValue(parsedVal, state.type, true);
1326
+ state.currentValue = this.reshapeArray(coercedFlat, state.dimensions);
1327
+ }
1328
+ } else {
1329
+ state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1330
+ }
1331
+
1332
+ const alarm = this.variableStore.get(path).alarm;
1333
+ if (alarm) this.addressSpaceAlarm.checkAlarm(alarm, state.currentValue);
1334
+
1335
+ return state.currentValue;
1336
+ }
1337
+ };
1338
+ this.variableStore.set(path, record);
1339
+ this.variableNodeIdStore.set(record.nodeIdKey, record);
1340
+ if (initialValue !== undefined) {
1341
+ record.setRuntimeValue(initialValue);
1342
+ }
1343
+ this.wrapVariableNode(existingNode, path, nodeId, browseName, state);
1344
+ return;
1345
+ }
1346
+
1113
1347
  const variableNode = namespace.addVariable({
1114
1348
  componentOf: parentNode,
1115
1349
  browseName,
@@ -1119,19 +1353,16 @@ class OpcUaAddressSpaceBuilder {
1119
1353
  rolePermissions: this.buildRolePermissions("variable", variableConfig),
1120
1354
  dataType: this.resolveDataType(type),
1121
1355
  modellingRule: this.isObjectTypePath(parentPath) ? "Mandatory" : undefined,
1122
- valueRank: state.isArray ? 1 : -1,
1356
+ valueRank: state.isMatrix ? state.dimensions.length : (state.isArray ? 1 : -1),
1357
+ arrayDimensions: state.isMatrix ? state.dimensions : undefined,
1123
1358
  accessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
1124
1359
  userAccessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
1125
1360
  minimumSamplingInterval: 500,
1126
1361
  value: {
1127
1362
  get: () => {
1128
- this.emitTagAccess("read", {
1129
- path,
1130
- nodeID: nodeId,
1131
- browseName,
1132
- dataType: state.type,
1133
- value: state.currentValue
1134
- });
1363
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1364
+ state.currentValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1365
+ }
1135
1366
 
1136
1367
  let val = state.currentValue;
1137
1368
  if (state.type === "Int64" || state.type === "UInt64") {
@@ -1151,7 +1382,7 @@ class OpcUaAddressSpaceBuilder {
1151
1382
  };
1152
1383
 
1153
1384
  if (state.isArray) {
1154
- val = Array.isArray(val) ? val.map(bigIntToInt64Array) : [bigIntToInt64Array(val)];
1385
+ val = this.recursiveMap(val, bigIntToInt64Array, state.type);
1155
1386
  } else {
1156
1387
  val = bigIntToInt64Array(val);
1157
1388
  }
@@ -1162,8 +1393,11 @@ class OpcUaAddressSpaceBuilder {
1162
1393
  value: val
1163
1394
  };
1164
1395
 
1165
- // ByteString nunca e array - Buffer nao deve ser VariantArrayType.Array
1166
- if (state.type !== "ByteString" && this.isArrayValue(state.currentValue)) {
1396
+ if (state.isMatrix) {
1397
+ variantOptions.arrayType = VariantArrayType.Matrix;
1398
+ variantOptions.dimensions = state.dimensions;
1399
+ variantOptions.value = this.flattenMatrix(val, state.type);
1400
+ } else if (state.type !== "ByteString" && this.isArrayValue(state.currentValue)) {
1167
1401
  variantOptions.arrayType = VariantArrayType.Array;
1168
1402
  } else if (state.type === "Int64" || state.type === "UInt64") {
1169
1403
  variantOptions.arrayType = VariantArrayType.Scalar;
@@ -1177,25 +1411,32 @@ class OpcUaAddressSpaceBuilder {
1177
1411
  }
1178
1412
 
1179
1413
  try {
1180
- state.currentValue = this.coerceValue(variant.value, state.type, state.isArray);
1181
- this.emitTagAccess("write", {
1182
- path,
1183
- nodeID: nodeId,
1184
- browseName,
1185
- dataType: state.type,
1186
- value: state.currentValue
1187
- });
1188
-
1189
- const alarm = this.variableStore.get(path).alarm
1190
- this.addressSpaceAlarm.checkAlarm(alarm, variant.value)
1414
+ let nextValue = variant.value;
1415
+ if (state.isMatrix) {
1416
+ const dims = variant.dimensions || state.dimensions;
1417
+ state.dimensions = dims;
1418
+ const coercedFlat = this.coerceValue(nextValue, state.type, true);
1419
+ state.currentValue = this.reshapeArray(coercedFlat, dims);
1420
+ } else {
1421
+ state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1422
+ }
1191
1423
 
1424
+ if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1425
+ this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate = !!state.currentValue;
1426
+ console.log(`AcceptAllCertificates updated by client to: ${state.currentValue}`);
1427
+ console.log(`Server Certificate Manager automaticallyAcceptUnknownCertificate is now: ${this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate}`);
1428
+ }
1192
1429
 
1430
+ const record = this.variableStore.get(path);
1431
+ const alarm = record ? record.alarm : null;
1432
+ if (alarm) {
1433
+ this.addressSpaceAlarm.checkAlarm(alarm, variant.value);
1434
+ }
1193
1435
 
1194
1436
  return StatusCodes.Good;
1195
1437
  } catch (error) {
1196
1438
  console.error("addVariable")
1197
1439
  console.error(error)
1198
- // this.node.warn("Rejected OPC UA write for " + path + ": " + error.message);
1199
1440
  return StatusCodes.BadTypeMismatch;
1200
1441
  }
1201
1442
  }
@@ -1210,9 +1451,29 @@ class OpcUaAddressSpaceBuilder {
1210
1451
  nodeIdKey: this.normalizeNodeIdKey(nodeId),
1211
1452
  type: state.type,
1212
1453
  isArray: state.isArray,
1454
+ isMatrix: state.isMatrix,
1455
+ dimensions: state.dimensions,
1213
1456
  getValue: () => state.currentValue,
1214
1457
  setRuntimeValue: (nextValue) => {
1215
- state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1458
+ if (state.isMatrix) {
1459
+ let parsedVal = nextValue;
1460
+ if (typeof nextValue === "string") {
1461
+ const trimmed = nextValue.trim();
1462
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1463
+ parsedVal = JSON.parse(trimmed);
1464
+ }
1465
+ }
1466
+ const dims = this.getArrayDimensions(parsedVal, state.type);
1467
+ if (dims) {
1468
+ state.dimensions = dims;
1469
+ state.currentValue = this.coerceValue(parsedVal, state.type, true);
1470
+ } else {
1471
+ const coercedFlat = this.coerceValue(parsedVal, state.type, true);
1472
+ state.currentValue = this.reshapeArray(coercedFlat, state.dimensions);
1473
+ }
1474
+ } else {
1475
+ state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1476
+ }
1216
1477
  return state.currentValue;
1217
1478
  },
1218
1479
  setValue: (nextValue) => {
@@ -1220,7 +1481,25 @@ class OpcUaAddressSpaceBuilder {
1220
1481
  throw new Error("Tag is read-only: " + path);
1221
1482
  }
1222
1483
 
1223
- state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1484
+ if (state.isMatrix) {
1485
+ let parsedVal = nextValue;
1486
+ if (typeof nextValue === "string") {
1487
+ const trimmed = nextValue.trim();
1488
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1489
+ parsedVal = JSON.parse(trimmed);
1490
+ }
1491
+ }
1492
+ const dims = this.getArrayDimensions(parsedVal, state.type);
1493
+ if (dims) {
1494
+ state.dimensions = dims;
1495
+ state.currentValue = this.coerceValue(parsedVal, state.type, true);
1496
+ } else {
1497
+ const coercedFlat = this.coerceValue(parsedVal, state.type, true);
1498
+ state.currentValue = this.reshapeArray(coercedFlat, state.dimensions);
1499
+ }
1500
+ } else {
1501
+ state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
1502
+ }
1224
1503
 
1225
1504
  const alarm = this.variableStore.get(path).alarm
1226
1505
  this.addressSpaceAlarm.checkAlarm(alarm, state.currentValue)
@@ -1230,6 +1509,7 @@ class OpcUaAddressSpaceBuilder {
1230
1509
  };
1231
1510
  this.variableStore.set(path, record);
1232
1511
  this.variableNodeIdStore.set(record.nodeIdKey, record);
1512
+ this.wrapVariableNode(variableNode, path, nodeId, browseName, state);
1233
1513
  }
1234
1514
 
1235
1515
 
@@ -1240,7 +1520,49 @@ class OpcUaAddressSpaceBuilder {
1240
1520
  const namespace = this.getNamespaceForConfig(methodConfig);
1241
1521
  const methodName = methodConfig.name;
1242
1522
  const path = pathOverride || this.buildPath(parentPath, methodName);
1243
- const nodeId = this.resolveNodeId(methodConfig, path, namespace)
1523
+ const nodeId = this.resolveNodeId(methodConfig, path, namespace);
1524
+
1525
+ const addressSpace = this.server.engine.addressSpace;
1526
+ const existingNode = addressSpace.findNode(nodeId);
1527
+ if (existingNode) {
1528
+ existingNode.bindMethod((inputArguments, context, callback) => {
1529
+ const callId = Date.now() + "_" + Math.random();
1530
+ const username = (context && context.session && context.session.userIdentityToken && context.session.userIdentityToken.userName)
1531
+ ? context.session.userIdentityToken.userName
1532
+ : "anonymous";
1533
+
1534
+ this.registry.emitMethodCall({
1535
+ methodName: methodConfig.name,
1536
+ nodeId: nodeId,
1537
+ callId,
1538
+ inputArguments,
1539
+ outputArguments: methodConfig.outputs.map((arg) => ({
1540
+ name: arg.name,
1541
+ description: { text: arg.description || "" },
1542
+ dataType: DATA_TYPE_MAP[arg.type]
1543
+ })),
1544
+ serverName: this.serverName,
1545
+ users: [{ name: username, groups: this.getUserGroups(username) }]
1546
+ });
1547
+
1548
+ this.registry.waitForMethodResponse(callId)
1549
+ .then((outputs) => {
1550
+ callback(null, {
1551
+ statusCode: StatusCodes.Good,
1552
+ outputArguments: outputs.map((output) => new Variant(output))
1553
+ });
1554
+ })
1555
+ .catch(() => {
1556
+ callback(null, {
1557
+ statusCode: StatusCodes.BadInternalError
1558
+ });
1559
+ });
1560
+ });
1561
+
1562
+ this.registerNodeEntry("method", path, parentPath, "componentOf", methodConfig, existingNode, namespace);
1563
+ return;
1564
+ }
1565
+
1244
1566
  const methodNode = namespace.addMethod(parentNode, {
1245
1567
  browseName: methodConfig.displayName || methodName,
1246
1568
  displayName: methodConfig.displayName || methodName,
@@ -1262,6 +1584,9 @@ class OpcUaAddressSpaceBuilder {
1262
1584
 
1263
1585
  methodNode.bindMethod((inputArguments, context, callback) => {
1264
1586
  const callId = Date.now() + "_" + Math.random();
1587
+ const username = (context && context.session && context.session.userIdentityToken && context.session.userIdentityToken.userName)
1588
+ ? context.session.userIdentityToken.userName
1589
+ : "anonymous";
1265
1590
 
1266
1591
  this.registry.emitMethodCall({
1267
1592
  methodName: methodConfig.name,
@@ -1273,7 +1598,8 @@ class OpcUaAddressSpaceBuilder {
1273
1598
  description: { text: arg.description || "" },
1274
1599
  dataType: DATA_TYPE_MAP[arg.type]
1275
1600
  })),
1276
- serverName: this.serverName
1601
+ serverName: this.serverName,
1602
+ users: [{ name: username, groups: this.getUserGroups(username) }]
1277
1603
  });
1278
1604
 
1279
1605
  this.registry.waitForMethodResponse(callId)
@@ -1388,6 +1714,238 @@ class OpcUaAddressSpaceBuilder {
1388
1714
  });
1389
1715
  }
1390
1716
 
1717
+ getUserGroups(username) {
1718
+ const normalized = String(username || "").trim();
1719
+ if (!normalized || normalized.toLowerCase() === "anonymous") {
1720
+ return [];
1721
+ }
1722
+ const user = this.users.find(u => u && u.username === normalized);
1723
+ if (!user) {
1724
+ return [];
1725
+ }
1726
+ return typeof user.group === "string"
1727
+ ? user.group.split(",").map(g => g.trim()).filter(Boolean)
1728
+ : Array.isArray(user.group)
1729
+ ? user.group
1730
+ : [];
1731
+ }
1732
+
1733
+ emitTagAccessWithContext(operation, details, context) {
1734
+ let val = details.value;
1735
+ if (details.dataType === "Int64" || details.dataType === "UInt64") {
1736
+ const convertToNumber = (v) => {
1737
+ const num = Number(v);
1738
+ return Number.isFinite(num) ? num : v;
1739
+ };
1740
+ const isArray = this.isArrayValue(val);
1741
+ if (isArray) {
1742
+ const items = this.extractArrayItems(val);
1743
+ val = Array.isArray(items) ? items.map(convertToNumber) : convertToNumber(val);
1744
+ } else {
1745
+ val = convertToNumber(val);
1746
+ }
1747
+ }
1748
+
1749
+ const users = [];
1750
+ if (context && context.session) {
1751
+ const session = context.session;
1752
+ const username = (session.userIdentityToken && session.userIdentityToken.userName)
1753
+ ? session.userIdentityToken.userName
1754
+ : "anonymous";
1755
+ const groups = this.getUserGroups(username);
1756
+ users.push({
1757
+ name: username,
1758
+ groups: groups
1759
+ });
1760
+ } else {
1761
+ users.push({
1762
+ name: "anonymous",
1763
+ groups: []
1764
+ });
1765
+ }
1766
+
1767
+ this.registry.emitTagAccess({
1768
+ operation,
1769
+ serverId: this.node.id,
1770
+ serverNodeName: this.node.name || "",
1771
+ serverName: this.serverName,
1772
+ timestamp: new Date().toISOString(),
1773
+ path: details.path,
1774
+ nodeID: details.nodeID,
1775
+ browseName: details.browseName,
1776
+ dataType: details.dataType,
1777
+ value: val,
1778
+ users: users
1779
+ });
1780
+ }
1781
+
1782
+ updateUsers(users) {
1783
+ this.users = Array.isArray(users) ? users : [];
1784
+ }
1785
+
1786
+ wrapVariableNode(variableNode, path, nodeId, browseName, state) {
1787
+ // Guard: only wrap once per node instance — re-sync must not double-wrap
1788
+ if (variableNode._opcuaWrapped) {
1789
+ return;
1790
+ }
1791
+ variableNode._opcuaWrapped = true;
1792
+ const self = this;
1793
+
1794
+ const originalReadValue = variableNode.readValue;
1795
+ variableNode.readValue = function(...args) {
1796
+ const context = args[0];
1797
+ const hasActiveAsync = context && typeof context === "object" && activeReads.has(context);
1798
+ try {
1799
+ const dataValue = originalReadValue.apply(this, args);
1800
+ if (!hasActiveAsync && dataValue && dataValue.statusCode.isGoodish()) {
1801
+ self.emitTagAccessWithContext("read", {
1802
+ path,
1803
+ nodeID: nodeId,
1804
+ browseName,
1805
+ dataType: state.type,
1806
+ value: (dataValue.value) ? dataValue.value.value : null
1807
+ }, context);
1808
+ }
1809
+ return dataValue;
1810
+ } finally {
1811
+ // no-op
1812
+ }
1813
+ };
1814
+
1815
+ const originalReadValueAsync = variableNode.readValueAsync;
1816
+ variableNode.readValueAsync = function(...args) {
1817
+ let callback = null;
1818
+ if (args.length > 0 && typeof args[args.length - 1] === "function") {
1819
+ callback = args[args.length - 1];
1820
+ }
1821
+
1822
+ const context = args[0];
1823
+
1824
+ if (context && typeof context === "object") {
1825
+ activeReads.set(context, true);
1826
+ }
1827
+
1828
+ if (callback) {
1829
+ args[args.length - 1] = function(err, dataValue) {
1830
+ if (context && typeof context === "object") {
1831
+ activeReads.delete(context);
1832
+ }
1833
+ if (!err && dataValue && dataValue.statusCode.isGoodish()) {
1834
+ self.emitTagAccessWithContext("read", {
1835
+ path,
1836
+ nodeID: nodeId,
1837
+ browseName,
1838
+ dataType: state.type,
1839
+ value: (dataValue.value) ? dataValue.value.value : null
1840
+ }, context);
1841
+ }
1842
+ callback(err, dataValue);
1843
+ };
1844
+ try {
1845
+ return originalReadValueAsync.apply(this, args);
1846
+ } catch (e) {
1847
+ if (context && typeof context === "object") {
1848
+ activeReads.delete(context);
1849
+ }
1850
+ throw e;
1851
+ }
1852
+ } else {
1853
+ try {
1854
+ const promise = originalReadValueAsync.apply(this, args);
1855
+ if (promise && typeof promise.then === "function") {
1856
+ return promise.then((dataValue) => {
1857
+ if (context && typeof context === "object") {
1858
+ activeReads.delete(context);
1859
+ }
1860
+ if (dataValue && dataValue.statusCode.isGoodish()) {
1861
+ self.emitTagAccessWithContext("read", {
1862
+ path,
1863
+ nodeID: nodeId,
1864
+ browseName,
1865
+ dataType: state.type,
1866
+ value: (dataValue.value) ? dataValue.value.value : null
1867
+ }, context);
1868
+ }
1869
+ return dataValue;
1870
+ }, (err) => {
1871
+ if (context && typeof context === "object") {
1872
+ activeReads.delete(context);
1873
+ }
1874
+ throw err;
1875
+ });
1876
+ }
1877
+ if (context && typeof context === "object") {
1878
+ activeReads.delete(context);
1879
+ }
1880
+ return promise;
1881
+ } catch (e) {
1882
+ if (context && typeof context === "object") {
1883
+ activeReads.delete(context);
1884
+ }
1885
+ throw e;
1886
+ }
1887
+ }
1888
+ };
1889
+
1890
+ const originalWriteValue = variableNode.writeValue;
1891
+ variableNode.writeValue = function(...args) {
1892
+ let callback = null;
1893
+ if (args.length > 0 && typeof args[args.length - 1] === "function") {
1894
+ callback = args[args.length - 1];
1895
+ }
1896
+
1897
+ const context = args[0];
1898
+ const dataValue = args[1];
1899
+
1900
+ const prevContext = self.registry.activeWriteContext;
1901
+ self.registry.activeWriteContext = context;
1902
+
1903
+ if (callback) {
1904
+ args[args.length - 1] = function(err, statusCode) {
1905
+ self.registry.activeWriteContext = prevContext;
1906
+ if (!err && statusCode && statusCode.isGoodish()) {
1907
+ self.emitTagAccessWithContext("write", {
1908
+ path,
1909
+ nodeID: nodeId,
1910
+ browseName,
1911
+ dataType: state.type,
1912
+ value: (dataValue && dataValue.value) ? dataValue.value.value : null
1913
+ }, context);
1914
+ }
1915
+ callback(err, statusCode);
1916
+ };
1917
+ try {
1918
+ return originalWriteValue.apply(this, args);
1919
+ } catch (err) {
1920
+ self.registry.activeWriteContext = prevContext;
1921
+ throw err;
1922
+ }
1923
+ } else {
1924
+ try {
1925
+ return originalWriteValue.apply(this, args).then((statusCode) => {
1926
+ self.registry.activeWriteContext = prevContext;
1927
+ if (statusCode && statusCode.isGoodish()) {
1928
+ self.emitTagAccessWithContext("write", {
1929
+ path,
1930
+ nodeID: nodeId,
1931
+ browseName,
1932
+ dataType: state.type,
1933
+ value: (dataValue && dataValue.value) ? dataValue.value.value : null
1934
+ }, context);
1935
+ }
1936
+ return statusCode;
1937
+ }).catch((err) => {
1938
+ self.registry.activeWriteContext = prevContext;
1939
+ throw err;
1940
+ });
1941
+ } catch (err) {
1942
+ self.registry.activeWriteContext = prevContext;
1943
+ throw err;
1944
+ }
1945
+ }
1946
+ };
1947
+ }
1948
+
1391
1949
  getVariableRecord(identifierType, identifier) {
1392
1950
  if (identifierType === "nodeId") {
1393
1951
  return this.getVariableRecordByNodeId(identifier);
@@ -1425,13 +1983,6 @@ class OpcUaAddressSpaceBuilder {
1425
1983
 
1426
1984
  if (reference.indexOf(".") === 0) {
1427
1985
  const relativeReference = this.resolveObjectTypeRelativeReference(parentPath, reference);
1428
-
1429
- //not work
1430
- var corrente = parentNode.getComponentByName("corrent")
1431
-
1432
- return {
1433
- node: corrente
1434
- }
1435
1986
  if (this.variableStore.has(relativeReference)) {
1436
1987
  return this.variableStore.get(relativeReference);
1437
1988
  }
@@ -1531,7 +2082,7 @@ class OpcUaAddressSpaceBuilder {
1531
2082
  throw new Error("Expected array value for type " + type);
1532
2083
  }
1533
2084
 
1534
- return items.map((item) => this.coerceScalarValue(item, type));
2085
+ return this.recursiveMap(items, (item) => this.coerceScalarValue(item, type), type);
1535
2086
  }
1536
2087
 
1537
2088
  if (typeof value === "string") {
@@ -1727,6 +2278,113 @@ class OpcUaAddressSpaceBuilder {
1727
2278
 
1728
2279
  return null;
1729
2280
  }
2281
+
2282
+ getArrayDimensions(value, type) {
2283
+ const arr = this.extractArrayItems(value);
2284
+ if (!arr) {
2285
+ return null;
2286
+ }
2287
+
2288
+ // Standard check: is it an Int64/UInt64 scalar represented as [high, low]?
2289
+ if ((type === "Int64" || type === "UInt64") && arr.length === 2 && typeof arr[0] === "number" && typeof arr[1] === "number") {
2290
+ return null;
2291
+ }
2292
+
2293
+ const hasNestedArray = arr.some(item => {
2294
+ if (Array.isArray(item)) {
2295
+ if ((type === "Int64" || type === "UInt64") && item.length === 2 && typeof item[0] === "number" && typeof item[1] === "number") {
2296
+ return false;
2297
+ }
2298
+ return true;
2299
+ }
2300
+ return false;
2301
+ });
2302
+
2303
+ if (!hasNestedArray) {
2304
+ return null;
2305
+ }
2306
+
2307
+ const dimensions = [];
2308
+ let current = arr;
2309
+ while (Array.isArray(current)) {
2310
+ if ((type === "Int64" || type === "UInt64") && current.length === 2 && typeof current[0] === "number" && typeof current[1] === "number") {
2311
+ break;
2312
+ }
2313
+ dimensions.push(current.length);
2314
+ if (current.length === 0) {
2315
+ break;
2316
+ }
2317
+ current = current[0];
2318
+ }
2319
+ return dimensions;
2320
+ }
2321
+
2322
+ flattenMatrix(value, type) {
2323
+ const arr = this.extractArrayItems(value);
2324
+ if (!arr) {
2325
+ return value;
2326
+ }
2327
+ if ((type === "Int64" || type === "UInt64") && arr.length === 2 && typeof arr[0] === "number" && typeof arr[1] === "number") {
2328
+ return [arr];
2329
+ }
2330
+
2331
+ const flat = [];
2332
+ const recurse = (a) => {
2333
+ for (const item of a) {
2334
+ if (Array.isArray(item)) {
2335
+ if ((type === "Int64" || type === "UInt64") && item.length === 2 && typeof item[0] === "number" && typeof item[1] === "number") {
2336
+ flat.push(item);
2337
+ } else {
2338
+ recurse(item);
2339
+ }
2340
+ } else {
2341
+ flat.push(item);
2342
+ }
2343
+ }
2344
+ };
2345
+ recurse(arr);
2346
+ return flat;
2347
+ }
2348
+
2349
+ reshapeArray(flatArray, dimensions) {
2350
+ if (!dimensions || dimensions.length <= 1) {
2351
+ return flatArray;
2352
+ }
2353
+
2354
+ const reshape = (arr, dims, offset) => {
2355
+ const size = dims[0];
2356
+ if (dims.length === 1) {
2357
+ return {
2358
+ result: arr.slice(offset, offset + size),
2359
+ nextOffset: offset + size
2360
+ };
2361
+ }
2362
+
2363
+ const result = [];
2364
+ let currentOffset = offset;
2365
+ for (let i = 0; i < size; i++) {
2366
+ const step = reshape(arr, dims.slice(1), currentOffset);
2367
+ result.push(step.result);
2368
+ currentOffset = step.nextOffset;
2369
+ }
2370
+ return {
2371
+ result: result,
2372
+ nextOffset: currentOffset
2373
+ };
2374
+ };
2375
+
2376
+ return reshape(flatArray, dimensions, 0).result;
2377
+ }
2378
+
2379
+ recursiveMap(val, fn, type) {
2380
+ if (Array.isArray(val)) {
2381
+ if ((type === "Int64" || type === "UInt64") && val.length === 2 && typeof val[0] === "number" && typeof val[1] === "number") {
2382
+ return fn(val);
2383
+ }
2384
+ return val.map(item => this.recursiveMap(item, fn, type));
2385
+ }
2386
+ return fn(val);
2387
+ }
1730
2388
  }
1731
2389
 
1732
2390
  function collapsePaths(paths) {