@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.
- package/README.md +104 -136
- package/client/lib/opcua-client-browser.js +254 -11
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +112 -9
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +158 -10
- package/client/opcua-client.html +8 -0
- package/client/opcua-client.js +97 -1
- package/client/view/opcua-client.js +106 -14
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +95 -32
- package/server/lib/opcua-address-space-builder.js +717 -59
- package/server/lib/opcua-config.js +110 -35
- package/server/lib/opcua-server-events-child.js +31 -5
- package/server/lib/opcua-server-runtime-child.js +424 -27
- package/server/lib/opcua-server-runtime.js +52 -5
- package/server/lib/opcua-server-status-child.js +46 -15
- package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
- package/server/opcua-server-io.html +93 -1
- package/server/opcua-server-io.js +153 -29
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +64 -0
- package/server/opcua-server.html +168 -44
- package/server/opcua-server.js +115 -5
- package/server/view/opcua-server.css +100 -6
- 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 =
|
|
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:
|
|
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:
|
|
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
|
}
|
|
@@ -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:
|
|
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(/(?:^|;)
|
|
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+;
|
|
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:
|
|
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(
|
|
1110
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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 =
|
|
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
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|