@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.
- package/README.md +17 -2
- package/client/lib/opcua-client-browser.js +21 -6
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.js +38 -1
- package/client/opcua-client-utils.js +195 -66
- package/client/opcua-client.html +1 -0
- package/client/view/opcua-client.js +106 -14
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +85 -28
- package/server/lib/opcua-address-space-builder.js +653 -45
- package/server/lib/opcua-config.js +29 -12
- package/server/lib/opcua-server-events-child.js +30 -4
- package/server/lib/opcua-server-runtime-child.js +141 -9
- package/server/lib/opcua-server-runtime.js +3 -0
- package/server/lib/opcua-server-status-child.js +32 -1
- package/server/opcua-server-io.html +21 -5
- package/server/opcua-server-io.js +23 -2
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +12 -0
- package/server/opcua-server.html +2 -0
- package/server/view/opcua-server.css +11 -0
- package/server/view/opcua-server.js +240 -23
|
@@ -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 =
|
|
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:
|
|
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:
|
|
816
|
+
nodeId: nodeId,
|
|
792
817
|
rolePermissions: this.buildRolePermissions("objectTypeDefinition", objectTypeConfig),
|
|
793
818
|
subtypeOf: "BaseObjectType"
|
|
794
819
|
});
|
|
795
820
|
|
|
796
821
|
|
|
797
|
-
const
|
|
822
|
+
const path2 = this.buildObjectTypePath(objectTypeConfig.name);
|
|
798
823
|
|
|
799
824
|
|
|
800
825
|
|
|
801
|
-
this.registerNodeEntry("objectTypeDefinition",
|
|
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:
|
|
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:
|
|
857
|
+
nodeId: nodeId,
|
|
817
858
|
enumeration: config.enumeration
|
|
818
859
|
});
|
|
819
860
|
|
|
820
|
-
const
|
|
861
|
+
const path2 = this.buildEnumerationPath(config.name);
|
|
821
862
|
|
|
822
|
-
this.registerNodeEntry("enumeration",
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|