@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.1

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.
@@ -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
  }
@@ -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
  }
@@ -841,12 +882,20 @@ class OpcUaAddressSpaceBuilder {
841
882
  const objectName = instanceConfig.name;
842
883
  const nextPath = pathOverride || this.buildPath(parentPath, objectName);
843
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
+ }
844
893
 
845
894
  const options = {
846
895
  browseName: instanceConfig.displayName || objectName,
847
896
  displayName: instanceConfig.displayName || objectName,
848
897
  description: instanceConfig.description || "",
849
- nodeId: this.resolveNodeId(instanceConfig, nextPath, namespace),
898
+ nodeId: nodeId,
850
899
  rolePermissions: this.buildRolePermissions("objectTypeInstance", instanceConfig),
851
900
  typeDefinition: objectTypeEntry.node.nodeId,
852
901
  eventNotifier: 1,
@@ -860,6 +909,21 @@ class OpcUaAddressSpaceBuilder {
860
909
  }
861
910
 
862
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
+
863
927
  this.registerNodeEntry("objectTypeInstance", nextPath, parentPath, relationship, instanceConfig, objectNode, namespace);
864
928
 
865
929
  // Create the inherited children explicitly using the configs that opcua-config.js
@@ -876,8 +940,8 @@ class OpcUaAddressSpaceBuilder {
876
940
  // Extract the type's nodeId value prefix (e.g. "Motor_type2") and the
877
941
  // instance's nodeId value prefix (e.g. "server1.newObjectType") so we can
878
942
  // rewrite every child nodeId from the type to the instance on-the-fly.
879
- const typeNodeId = typeEntry.config.nodeId || "";
880
- const instanceNodeId = instanceConfig.nodeId || "";
943
+ const typeNodeId = typeEntry.config.nodeId || ("ns=2;s=" + typeEntry.config.name);
944
+ const instanceNodeId = instanceConfig.nodeId || instanceNode.nodeId.toString();
881
945
  const typePrefix = this.extractNodeIdStringValue(typeNodeId);
882
946
  const instancePrefix = this.extractNodeIdStringValue(instanceNodeId);
883
947
 
@@ -1017,19 +1081,52 @@ class OpcUaAddressSpaceBuilder {
1017
1081
 
1018
1082
  const sourceName = variableToMonitor.node.displayName[0].text
1019
1083
 
1020
- const browseName = alarmConfig.displayName || objectName
1021
- const inputNode = variableToMonitor.node
1022
-
1023
-
1084
+ const browseName = alarmConfig.displayName || objectName;
1085
+ const inputNode = variableToMonitor.node;
1024
1086
 
1025
1087
  const conditionName = alarmConfig.displayName || objectName
1026
1088
  const nodeId = this.resolveNodeId(alarmConfig, nextPath, namespace)
1027
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
+
1028
1122
  const alarmNode = this.addressSpaceAlarm.createAlarm(namespace, browseName, parentNode, inputNode, conditionName, nodeId, sourceName, alarmConfig)
1029
1123
  const alarmRolePermissions = this.buildRolePermissions("alarm", alarmConfig);
1030
1124
  if (alarmNode && alarmRolePermissions) {
1031
1125
  alarmNode.setRolePermissions(alarmRolePermissions);
1032
1126
  }
1127
+ if (alarmNode) {
1128
+ alarmNode.path = nextPath;
1129
+ }
1033
1130
 
1034
1131
 
1035
1132
 
@@ -1108,11 +1205,18 @@ class OpcUaAddressSpaceBuilder {
1108
1205
 
1109
1206
  const folderName = folderConfig.name;
1110
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
+
1111
1215
  const options = {
1112
1216
  browseName: folderConfig.displayName || folderName,
1113
1217
  displayName: folderConfig.displayName || folderName,
1114
1218
  description: folderConfig.description || "",
1115
- nodeId: this.resolveNodeId(folderConfig, nextPath, namespace),
1219
+ nodeId: nodeId,
1116
1220
  rolePermissions: this.buildRolePermissions("folder", folderConfig),
1117
1221
  typeDefinition: "FolderType",
1118
1222
  eventSourceOf: serverNode,
@@ -1153,13 +1257,93 @@ class OpcUaAddressSpaceBuilder {
1153
1257
  if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1154
1258
  initialValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1155
1259
  }
1260
+
1261
+ const dimensions = this.getArrayDimensions(initialValue, type);
1156
1262
  const state = {
1157
1263
  type,
1158
1264
  access,
1159
1265
  isArray: this.isArrayValue(initialValue),
1266
+ isMatrix: !!dimensions,
1267
+ dimensions: dimensions,
1160
1268
  currentValue: this.coerceValue(initialValue, type, this.isArrayValue(initialValue))
1161
1269
  };
1162
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
+
1163
1347
  const variableNode = namespace.addVariable({
1164
1348
  componentOf: parentNode,
1165
1349
  browseName,
@@ -1169,7 +1353,8 @@ class OpcUaAddressSpaceBuilder {
1169
1353
  rolePermissions: this.buildRolePermissions("variable", variableConfig),
1170
1354
  dataType: this.resolveDataType(type),
1171
1355
  modellingRule: this.isObjectTypePath(parentPath) ? "Mandatory" : undefined,
1172
- valueRank: state.isArray ? 1 : -1,
1356
+ valueRank: state.isMatrix ? state.dimensions.length : (state.isArray ? 1 : -1),
1357
+ arrayDimensions: state.isMatrix ? state.dimensions : undefined,
1173
1358
  accessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
1174
1359
  userAccessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
1175
1360
  minimumSamplingInterval: 500,
@@ -1178,13 +1363,6 @@ class OpcUaAddressSpaceBuilder {
1178
1363
  if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1179
1364
  state.currentValue = this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate;
1180
1365
  }
1181
- this.emitTagAccess("read", {
1182
- path,
1183
- nodeID: nodeId,
1184
- browseName,
1185
- dataType: state.type,
1186
- value: state.currentValue
1187
- });
1188
1366
 
1189
1367
  let val = state.currentValue;
1190
1368
  if (state.type === "Int64" || state.type === "UInt64") {
@@ -1204,7 +1382,7 @@ class OpcUaAddressSpaceBuilder {
1204
1382
  };
1205
1383
 
1206
1384
  if (state.isArray) {
1207
- val = Array.isArray(val) ? val.map(bigIntToInt64Array) : [bigIntToInt64Array(val)];
1385
+ val = this.recursiveMap(val, bigIntToInt64Array, state.type);
1208
1386
  } else {
1209
1387
  val = bigIntToInt64Array(val);
1210
1388
  }
@@ -1215,8 +1393,11 @@ class OpcUaAddressSpaceBuilder {
1215
1393
  value: val
1216
1394
  };
1217
1395
 
1218
- // ByteString nunca e array - Buffer nao deve ser VariantArrayType.Array
1219
- 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)) {
1220
1401
  variantOptions.arrayType = VariantArrayType.Array;
1221
1402
  } else if (state.type === "Int64" || state.type === "UInt64") {
1222
1403
  variantOptions.arrayType = VariantArrayType.Scalar;
@@ -1230,14 +1411,15 @@ class OpcUaAddressSpaceBuilder {
1230
1411
  }
1231
1412
 
1232
1413
  try {
1233
- state.currentValue = this.coerceValue(variant.value, state.type, state.isArray);
1234
- this.emitTagAccess("write", {
1235
- path,
1236
- nodeID: nodeId,
1237
- browseName,
1238
- dataType: state.type,
1239
- value: state.currentValue
1240
- });
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
+ }
1241
1423
 
1242
1424
  if (browseName === "AcceptAllCertificates" && this.server && this.server.serverCertificateManager) {
1243
1425
  this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate = !!state.currentValue;
@@ -1245,14 +1427,16 @@ class OpcUaAddressSpaceBuilder {
1245
1427
  console.log(`Server Certificate Manager automaticallyAcceptUnknownCertificate is now: ${this.server.serverCertificateManager.automaticallyAcceptUnknownCertificate}`);
1246
1428
  }
1247
1429
 
1248
- const alarm = this.variableStore.get(path).alarm
1249
- this.addressSpaceAlarm.checkAlarm(alarm, variant.value)
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
+ }
1250
1435
 
1251
1436
  return StatusCodes.Good;
1252
1437
  } catch (error) {
1253
1438
  console.error("addVariable")
1254
1439
  console.error(error)
1255
- // this.node.warn("Rejected OPC UA write for " + path + ": " + error.message);
1256
1440
  return StatusCodes.BadTypeMismatch;
1257
1441
  }
1258
1442
  }
@@ -1267,9 +1451,29 @@ class OpcUaAddressSpaceBuilder {
1267
1451
  nodeIdKey: this.normalizeNodeIdKey(nodeId),
1268
1452
  type: state.type,
1269
1453
  isArray: state.isArray,
1454
+ isMatrix: state.isMatrix,
1455
+ dimensions: state.dimensions,
1270
1456
  getValue: () => state.currentValue,
1271
1457
  setRuntimeValue: (nextValue) => {
1272
- 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
+ }
1273
1477
  return state.currentValue;
1274
1478
  },
1275
1479
  setValue: (nextValue) => {
@@ -1277,7 +1481,25 @@ class OpcUaAddressSpaceBuilder {
1277
1481
  throw new Error("Tag is read-only: " + path);
1278
1482
  }
1279
1483
 
1280
- 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
+ }
1281
1503
 
1282
1504
  const alarm = this.variableStore.get(path).alarm
1283
1505
  this.addressSpaceAlarm.checkAlarm(alarm, state.currentValue)
@@ -1287,6 +1509,7 @@ class OpcUaAddressSpaceBuilder {
1287
1509
  };
1288
1510
  this.variableStore.set(path, record);
1289
1511
  this.variableNodeIdStore.set(record.nodeIdKey, record);
1512
+ this.wrapVariableNode(variableNode, path, nodeId, browseName, state);
1290
1513
  }
1291
1514
 
1292
1515
 
@@ -1297,7 +1520,49 @@ class OpcUaAddressSpaceBuilder {
1297
1520
  const namespace = this.getNamespaceForConfig(methodConfig);
1298
1521
  const methodName = methodConfig.name;
1299
1522
  const path = pathOverride || this.buildPath(parentPath, methodName);
1300
- 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
+
1301
1566
  const methodNode = namespace.addMethod(parentNode, {
1302
1567
  browseName: methodConfig.displayName || methodName,
1303
1568
  displayName: methodConfig.displayName || methodName,
@@ -1319,6 +1584,9 @@ class OpcUaAddressSpaceBuilder {
1319
1584
 
1320
1585
  methodNode.bindMethod((inputArguments, context, callback) => {
1321
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";
1322
1590
 
1323
1591
  this.registry.emitMethodCall({
1324
1592
  methodName: methodConfig.name,
@@ -1330,7 +1598,8 @@ class OpcUaAddressSpaceBuilder {
1330
1598
  description: { text: arg.description || "" },
1331
1599
  dataType: DATA_TYPE_MAP[arg.type]
1332
1600
  })),
1333
- serverName: this.serverName
1601
+ serverName: this.serverName,
1602
+ users: [{ name: username, groups: this.getUserGroups(username) }]
1334
1603
  });
1335
1604
 
1336
1605
  this.registry.waitForMethodResponse(callId)
@@ -1445,6 +1714,238 @@ class OpcUaAddressSpaceBuilder {
1445
1714
  });
1446
1715
  }
1447
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
+
1448
1949
  getVariableRecord(identifierType, identifier) {
1449
1950
  if (identifierType === "nodeId") {
1450
1951
  return this.getVariableRecordByNodeId(identifier);
@@ -1581,7 +2082,7 @@ class OpcUaAddressSpaceBuilder {
1581
2082
  throw new Error("Expected array value for type " + type);
1582
2083
  }
1583
2084
 
1584
- return items.map((item) => this.coerceScalarValue(item, type));
2085
+ return this.recursiveMap(items, (item) => this.coerceScalarValue(item, type), type);
1585
2086
  }
1586
2087
 
1587
2088
  if (typeof value === "string") {
@@ -1777,6 +2278,113 @@ class OpcUaAddressSpaceBuilder {
1777
2278
 
1778
2279
  return null;
1779
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
+ }
1780
2388
  }
1781
2389
 
1782
2390
  function collapsePaths(paths) {