@vitormnm/node-red-simple-opcua 1.5.0 → 1.6.3
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/client/lib/opcua-client-read-service.js +14 -2
- package/client/lib/opcua-client-subscription-service.js +8 -3
- package/client/opcua-client-utils.js +206 -12
- package/examples/flows_simple_opc.json +1 -2851
- package/package.json +1 -1
- package/server/lib/opcua-address-space-builder.js +203 -9
- package/server/lib/opcua-config.js +131 -8
- package/server/lib/opcua-constants.js +1 -0
- package/server/lib/opcua-server-methods.js +2 -0
- package/server/lib/opcua-server-runtime-child.js +148 -42
- package/server/lib/opcua-server-runtime.js +14 -5
- package/server/opcua-server-io.js +9 -0
- package/server/opcua-server.html +4 -1
- package/server/opcua-server.js +27 -2
- package/server/view/opcua-server.css +4 -0
- package/server/view/opcua-server.js +178 -28
package/package.json
CHANGED
|
@@ -22,7 +22,8 @@ class OpcUaAddressSpaceBuilder {
|
|
|
22
22
|
this.registry = options.registry;
|
|
23
23
|
this.node = options.node;
|
|
24
24
|
this.serverName = options.serverName;
|
|
25
|
-
|
|
25
|
+
const hasUsers = Array.isArray(options.users) && options.users.length > 0;
|
|
26
|
+
this.authorizationDisabled = !hasUsers;
|
|
26
27
|
this.nodeEntries = new Map();
|
|
27
28
|
this.variableStore = new Map();
|
|
28
29
|
this.variableNodeIdStore = new Map();
|
|
@@ -82,6 +83,7 @@ class OpcUaAddressSpaceBuilder {
|
|
|
82
83
|
this.variableStore.clear();
|
|
83
84
|
this.variableNodeIdStore.clear();
|
|
84
85
|
this.objectTypeStore.clear();
|
|
86
|
+
if (this.enumerationStore) this.enumerationStore.clear();
|
|
85
87
|
this.alarmStore.clear(); // Adicione esta linha
|
|
86
88
|
this.pendingAlarms = [];
|
|
87
89
|
}
|
|
@@ -134,7 +136,20 @@ class OpcUaAddressSpaceBuilder {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
readValue(identifierType, identifier) {
|
|
137
|
-
|
|
139
|
+
const record = this.getVariableRecord(identifierType, identifier);
|
|
140
|
+
let val = record.getValue();
|
|
141
|
+
if (record.type === "Int64" || record.type === "UInt64") {
|
|
142
|
+
const convertToNumber = (v) => {
|
|
143
|
+
const num = Number(v);
|
|
144
|
+
return Number.isFinite(num) ? num : v;
|
|
145
|
+
};
|
|
146
|
+
if (record.isArray) {
|
|
147
|
+
val = Array.isArray(val) ? val.map(convertToNumber) : convertToNumber(val);
|
|
148
|
+
} else {
|
|
149
|
+
val = convertToNumber(val);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return val;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
writeValue(identifierType, identifier, value) {
|
|
@@ -149,6 +164,10 @@ class OpcUaAddressSpaceBuilder {
|
|
|
149
164
|
this.collectObjectTypeDefinition(desiredEntries, objectTypeConfig, objectTypeConfigs);
|
|
150
165
|
});
|
|
151
166
|
|
|
167
|
+
(Array.isArray(treeConfig.enumerations) ? treeConfig.enumerations : []).forEach((enumerationConfig) => {
|
|
168
|
+
this.collectEnumerationDefinition(desiredEntries, enumerationConfig);
|
|
169
|
+
});
|
|
170
|
+
|
|
152
171
|
(Array.isArray(treeConfig.folders) ? treeConfig.folders : []).forEach((folderConfig) => {
|
|
153
172
|
this.collectBranch(desiredEntries, "folder", folderConfig, "", "organizedBy", objectTypeConfigs);
|
|
154
173
|
});
|
|
@@ -183,6 +202,11 @@ class OpcUaAddressSpaceBuilder {
|
|
|
183
202
|
});
|
|
184
203
|
}
|
|
185
204
|
|
|
205
|
+
collectEnumerationDefinition(desiredEntries, config) {
|
|
206
|
+
const path = this.buildEnumerationPath(config.name);
|
|
207
|
+
desiredEntries.set(path, this.buildEntryDefinition("enumeration", config, path, "", "typeDefinition"));
|
|
208
|
+
}
|
|
209
|
+
|
|
186
210
|
collectBranch(desiredEntries, kind, config, parentPath, relationship, objectTypeConfigs, options) {
|
|
187
211
|
const settings = options || {};
|
|
188
212
|
const path = settings.preserveCollectionNames
|
|
@@ -635,6 +659,11 @@ class OpcUaAddressSpaceBuilder {
|
|
|
635
659
|
return;
|
|
636
660
|
}
|
|
637
661
|
|
|
662
|
+
if (definition.kind === "enumeration") {
|
|
663
|
+
this.addEnumerationTypeDefinition(definition.config);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
638
667
|
if (definition.kind === "object") {
|
|
639
668
|
this.addObject(parentNode, definition.config, definition.parentPath, definition.relationship, definition.path);
|
|
640
669
|
return;
|
|
@@ -702,6 +731,10 @@ class OpcUaAddressSpaceBuilder {
|
|
|
702
731
|
this.objectTypeStore.delete(entry.config.name);
|
|
703
732
|
}
|
|
704
733
|
|
|
734
|
+
if (entry.kind === "enumeration" && this.enumerationStore) {
|
|
735
|
+
this.enumerationStore.delete(entry.config.name);
|
|
736
|
+
}
|
|
737
|
+
|
|
705
738
|
try {
|
|
706
739
|
entry.namespace.deleteNode(entry.node);
|
|
707
740
|
|
|
@@ -774,6 +807,28 @@ class OpcUaAddressSpaceBuilder {
|
|
|
774
807
|
});
|
|
775
808
|
}
|
|
776
809
|
|
|
810
|
+
addEnumerationTypeDefinition(config) {
|
|
811
|
+
const namespace = this.getNamespaceForConfig(config);
|
|
812
|
+
const enumTypeNode = namespace.addEnumerationType({
|
|
813
|
+
browseName: config.displayName || config.name,
|
|
814
|
+
displayName: config.displayName || config.name,
|
|
815
|
+
description: config.description || "",
|
|
816
|
+
nodeId: this.resolveNodeId(config, this.buildEnumerationPath(config.name), namespace),
|
|
817
|
+
enumeration: config.enumeration
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const path = this.buildEnumerationPath(config.name);
|
|
821
|
+
|
|
822
|
+
this.registerNodeEntry("enumeration", path, "", "typeDefinition", config, enumTypeNode, namespace);
|
|
823
|
+
if (!this.enumerationStore) this.enumerationStore = new Map();
|
|
824
|
+
this.enumerationStore.set(config.name, {
|
|
825
|
+
node: enumTypeNode,
|
|
826
|
+
config: config,
|
|
827
|
+
path: path,
|
|
828
|
+
namespace: namespace
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
777
832
|
addObjectTypeInstance(parentNode, instanceConfig, parentPath, relationship, pathOverride) {
|
|
778
833
|
const objectTypeEntry = this.objectTypeStore.get(instanceConfig.objectsType);
|
|
779
834
|
if (!objectTypeEntry || !objectTypeEntry.node) {
|
|
@@ -1032,6 +1087,14 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1032
1087
|
this.registerNodeEntry("folder", nextPath, parentPath, relationship, folderConfig, folderNode, namespace);
|
|
1033
1088
|
}
|
|
1034
1089
|
|
|
1090
|
+
resolveDataType(type) {
|
|
1091
|
+
if (DATA_TYPE_MAP[type]) return DATA_TYPE_MAP[type];
|
|
1092
|
+
if (this.enumerationStore && this.enumerationStore.has(type)) {
|
|
1093
|
+
return this.enumerationStore.get(type).node.nodeId;
|
|
1094
|
+
}
|
|
1095
|
+
return type;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1035
1098
|
addVariable(parentNode, variableConfig, parentPath, pathOverride) {
|
|
1036
1099
|
const namespace = this.getNamespaceForConfig(variableConfig);
|
|
1037
1100
|
const name = variableConfig.name;
|
|
@@ -1044,7 +1107,7 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1044
1107
|
type,
|
|
1045
1108
|
access,
|
|
1046
1109
|
isArray: this.isArrayValue(variableConfig.value),
|
|
1047
|
-
currentValue: variableConfig.value
|
|
1110
|
+
currentValue: this.coerceValue(variableConfig.value, type, this.isArrayValue(variableConfig.value))
|
|
1048
1111
|
};
|
|
1049
1112
|
|
|
1050
1113
|
const variableNode = namespace.addVariable({
|
|
@@ -1054,7 +1117,7 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1054
1117
|
description: variableConfig.description || "",
|
|
1055
1118
|
nodeId,
|
|
1056
1119
|
rolePermissions: this.buildRolePermissions("variable", variableConfig),
|
|
1057
|
-
dataType: type,
|
|
1120
|
+
dataType: this.resolveDataType(type),
|
|
1058
1121
|
modellingRule: this.isObjectTypePath(parentPath) ? "Mandatory" : undefined,
|
|
1059
1122
|
valueRank: state.isArray ? 1 : -1,
|
|
1060
1123
|
accessLevel: access === "readwrite" ? "CurrentRead | CurrentWrite" : "CurrentRead",
|
|
@@ -1070,14 +1133,40 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1070
1133
|
value: state.currentValue
|
|
1071
1134
|
});
|
|
1072
1135
|
|
|
1136
|
+
let val = state.currentValue;
|
|
1137
|
+
if (state.type === "Int64" || state.type === "UInt64") {
|
|
1138
|
+
const bigIntToInt64Array = (v) => {
|
|
1139
|
+
let bigintVal;
|
|
1140
|
+
try {
|
|
1141
|
+
bigintVal = BigInt(v);
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
bigintVal = 0n;
|
|
1144
|
+
}
|
|
1145
|
+
const mask = 0xFFFFFFFFFFFFFFFFn;
|
|
1146
|
+
bigintVal = bigintVal & mask;
|
|
1147
|
+
|
|
1148
|
+
const high = Number(bigintVal >> 32n);
|
|
1149
|
+
const low = Number(bigintVal & 0xFFFFFFFFn);
|
|
1150
|
+
return [high, low];
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
if (state.isArray) {
|
|
1154
|
+
val = Array.isArray(val) ? val.map(bigIntToInt64Array) : [bigIntToInt64Array(val)];
|
|
1155
|
+
} else {
|
|
1156
|
+
val = bigIntToInt64Array(val);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1073
1160
|
const variantOptions = {
|
|
1074
|
-
dataType: DATA_TYPE_MAP[state.type],
|
|
1075
|
-
value:
|
|
1161
|
+
dataType: DATA_TYPE_MAP[state.type] || DataType.Int32,
|
|
1162
|
+
value: val
|
|
1076
1163
|
};
|
|
1077
1164
|
|
|
1078
1165
|
// ByteString nunca e array - Buffer nao deve ser VariantArrayType.Array
|
|
1079
1166
|
if (state.type !== "ByteString" && this.isArrayValue(state.currentValue)) {
|
|
1080
1167
|
variantOptions.arrayType = VariantArrayType.Array;
|
|
1168
|
+
} else if (state.type === "Int64" || state.type === "UInt64") {
|
|
1169
|
+
variantOptions.arrayType = VariantArrayType.Scalar;
|
|
1081
1170
|
}
|
|
1082
1171
|
|
|
1083
1172
|
return new Variant(variantOptions);
|
|
@@ -1119,6 +1208,8 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1119
1208
|
path: path,
|
|
1120
1209
|
nodeId: nodeId,
|
|
1121
1210
|
nodeIdKey: this.normalizeNodeIdKey(nodeId),
|
|
1211
|
+
type: state.type,
|
|
1212
|
+
isArray: state.isArray,
|
|
1122
1213
|
getValue: () => state.currentValue,
|
|
1123
1214
|
setRuntimeValue: (nextValue) => {
|
|
1124
1215
|
state.currentValue = this.coerceValue(nextValue, state.type, state.isArray);
|
|
@@ -1228,6 +1319,10 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1228
1319
|
return "__objectTypes." + name;
|
|
1229
1320
|
}
|
|
1230
1321
|
|
|
1322
|
+
buildEnumerationPath(name) {
|
|
1323
|
+
return "__enumerations." + name;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1231
1326
|
buildCollectionPath(parentPath, collectionName, name) {
|
|
1232
1327
|
|
|
1233
1328
|
// return parentPath ? parentPath + "." + collectionName + "." + name : collectionName + "." + name;
|
|
@@ -1264,6 +1359,21 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1264
1359
|
}
|
|
1265
1360
|
|
|
1266
1361
|
emitTagAccess(operation, details) {
|
|
1362
|
+
let val = details.value;
|
|
1363
|
+
if (details.dataType === "Int64" || details.dataType === "UInt64") {
|
|
1364
|
+
const convertToNumber = (v) => {
|
|
1365
|
+
const num = Number(v);
|
|
1366
|
+
return Number.isFinite(num) ? num : v;
|
|
1367
|
+
};
|
|
1368
|
+
const isArray = this.isArrayValue(val);
|
|
1369
|
+
if (isArray) {
|
|
1370
|
+
const items = this.extractArrayItems(val);
|
|
1371
|
+
val = Array.isArray(items) ? items.map(convertToNumber) : convertToNumber(val);
|
|
1372
|
+
} else {
|
|
1373
|
+
val = convertToNumber(val);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1267
1377
|
this.registry.emitTagAccess({
|
|
1268
1378
|
operation,
|
|
1269
1379
|
serverId: this.node.id,
|
|
@@ -1274,7 +1384,7 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1274
1384
|
nodeID: details.nodeID,
|
|
1275
1385
|
browseName: details.browseName,
|
|
1276
1386
|
dataType: details.dataType,
|
|
1277
|
-
value:
|
|
1387
|
+
value: val
|
|
1278
1388
|
});
|
|
1279
1389
|
}
|
|
1280
1390
|
|
|
@@ -1443,7 +1553,11 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1443
1553
|
}
|
|
1444
1554
|
|
|
1445
1555
|
if (this.extractArrayItems(value)) {
|
|
1446
|
-
|
|
1556
|
+
if ((type === "Int64" || type === "UInt64") && Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
|
|
1557
|
+
// Do not throw, this is a standard scalar Int64/UInt64 represented as [high, low]
|
|
1558
|
+
} else {
|
|
1559
|
+
throw new Error("Expected scalar value for type " + type + " but received array");
|
|
1560
|
+
}
|
|
1447
1561
|
}
|
|
1448
1562
|
|
|
1449
1563
|
return this.coerceScalarValue(value, type);
|
|
@@ -1468,6 +1582,86 @@ class OpcUaAddressSpaceBuilder {
|
|
|
1468
1582
|
return Math.trunc(parsed);
|
|
1469
1583
|
}
|
|
1470
1584
|
|
|
1585
|
+
if (type === "Int64") {
|
|
1586
|
+
const minVal = -9223372036854775808n;
|
|
1587
|
+
const maxVal = 9223372036854775807n;
|
|
1588
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
1589
|
+
try {
|
|
1590
|
+
const h = BigInt(value[0]);
|
|
1591
|
+
const l = BigInt(value[1]);
|
|
1592
|
+
const signMask = 1n << 31n;
|
|
1593
|
+
const shiftHigh = 1n << 32n;
|
|
1594
|
+
let bigintVal;
|
|
1595
|
+
if ((h & signMask) === signMask) {
|
|
1596
|
+
bigintVal = (h & ~signMask) * shiftHigh + l - 0x8000000000000000n;
|
|
1597
|
+
} else {
|
|
1598
|
+
bigintVal = h * shiftHigh + l;
|
|
1599
|
+
}
|
|
1600
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1601
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1602
|
+
return String(bigintVal);
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
return "0";
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
let bigintVal = BigInt(value);
|
|
1609
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1610
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1611
|
+
return String(bigintVal);
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
const parsed = Number(value);
|
|
1614
|
+
if (Number.isFinite(parsed)) {
|
|
1615
|
+
try {
|
|
1616
|
+
let bigintVal = BigInt(Math.trunc(parsed));
|
|
1617
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1618
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1619
|
+
return String(bigintVal);
|
|
1620
|
+
} catch (e2) {
|
|
1621
|
+
return "0";
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return "0";
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (type === "UInt64") {
|
|
1629
|
+
const minVal = 0n;
|
|
1630
|
+
const maxVal = 18446744073709551615n;
|
|
1631
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
1632
|
+
try {
|
|
1633
|
+
const h = BigInt(value[0]);
|
|
1634
|
+
const l = BigInt(value[1]);
|
|
1635
|
+
const shiftHigh = 1n << 32n;
|
|
1636
|
+
let bigintVal = h * shiftHigh + l;
|
|
1637
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1638
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1639
|
+
return String(bigintVal);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
return "0";
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
let bigintVal = BigInt(value);
|
|
1646
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1647
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1648
|
+
return String(bigintVal);
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
const parsed = Number(value);
|
|
1651
|
+
if (Number.isFinite(parsed)) {
|
|
1652
|
+
try {
|
|
1653
|
+
let bigintVal = BigInt(Math.trunc(parsed));
|
|
1654
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
1655
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
1656
|
+
return String(bigintVal);
|
|
1657
|
+
} catch (e2) {
|
|
1658
|
+
return "0";
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return "0";
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1471
1665
|
if (type === "Float") {
|
|
1472
1666
|
const parsed = Number(value);
|
|
1473
1667
|
if (!Number.isFinite(parsed)) {
|
|
@@ -1574,7 +1768,7 @@ function compareEntryCreationOrder(left, right) {
|
|
|
1574
1768
|
}
|
|
1575
1769
|
|
|
1576
1770
|
function kindRank(kind) {
|
|
1577
|
-
if (kind === "objectTypeDefinition") {
|
|
1771
|
+
if (kind === "objectTypeDefinition" || kind === "enumeration") {
|
|
1578
1772
|
return -1;
|
|
1579
1773
|
}
|
|
1580
1774
|
if (kind === "folder") {
|
|
@@ -92,7 +92,17 @@ class OpcUaServerConfigParser {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
(Array.isArray(groups) ? groups : []).forEach(addGroup);
|
|
95
|
-
(Array.isArray(users) ? users : []).forEach((user) =>
|
|
95
|
+
(Array.isArray(users) ? users : []).forEach((user) => {
|
|
96
|
+
if (user && user.group) {
|
|
97
|
+
if (typeof user.group === "string") {
|
|
98
|
+
user.group.split(",").forEach(addGroup);
|
|
99
|
+
} else if (Array.isArray(user.group)) {
|
|
100
|
+
user.group.forEach(addGroup);
|
|
101
|
+
} else {
|
|
102
|
+
addGroup(user.group);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
96
106
|
|
|
97
107
|
return resolved;
|
|
98
108
|
}
|
|
@@ -121,10 +131,17 @@ class OpcUaServerConfigParser {
|
|
|
121
131
|
this._objectsTypesMap[typeDef.name] = typeDef;
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
const enumerations = this.normalizeEnumerations(parsed.enumerations || parsed.enumeration || []);
|
|
135
|
+
this._enumerationsMap = {};
|
|
136
|
+
for (const enumDef of enumerations) {
|
|
137
|
+
this._enumerationsMap[enumDef.name] = enumDef;
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
return {
|
|
125
141
|
objects: this.normalizeObjects(parsed.objects || []),
|
|
126
142
|
folders: this.normalizeFolders(parsed.folders || []),
|
|
127
143
|
objectsTypes,
|
|
144
|
+
enumerations,
|
|
128
145
|
nameSpaces: this.normalizeNamespaces(parsed.nameSpaces || parsed.namespaces || [])
|
|
129
146
|
};
|
|
130
147
|
}
|
|
@@ -366,6 +383,41 @@ class OpcUaServerConfigParser {
|
|
|
366
383
|
return normalizedBranch;
|
|
367
384
|
}
|
|
368
385
|
|
|
386
|
+
normalizeEnumerations(enumerations) {
|
|
387
|
+
if (!Array.isArray(enumerations)) {
|
|
388
|
+
throw new Error("'enumerations' must be an array");
|
|
389
|
+
}
|
|
390
|
+
return enumerations.map((config) => this.normalizeEnumeration(config));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
normalizeEnumeration(enumerationConfig) {
|
|
394
|
+
if (!enumerationConfig || typeof enumerationConfig !== "object" || Array.isArray(enumerationConfig)) {
|
|
395
|
+
throw new Error("Each enumeration must be an object");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const name = this.requiredName(enumerationConfig, "enumeration");
|
|
399
|
+
|
|
400
|
+
let enumerationStates = [];
|
|
401
|
+
if (Array.isArray(enumerationConfig.enumeration)) {
|
|
402
|
+
enumerationStates = enumerationConfig.enumeration.map(state => {
|
|
403
|
+
return {
|
|
404
|
+
value: Number.isFinite(Number(state.value)) ? Number(state.value) : 0,
|
|
405
|
+
displayName: typeof state.displayName === "string" ? state.displayName : ""
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
name,
|
|
412
|
+
displayName: enumerationConfig.displayName || name,
|
|
413
|
+
description: enumerationConfig.description || "",
|
|
414
|
+
nodeId: this.normalizeOptionalNodeId(enumerationConfig.nodeId),
|
|
415
|
+
namespaceId: this.normalizeNamespaceId(enumerationConfig.namespaceId),
|
|
416
|
+
accessPermission: this.normalizeAccessPermissions(enumerationConfig.accessPermission || enumerationConfig.accessPermissions),
|
|
417
|
+
enumeration: enumerationStates
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
369
421
|
// Returns the string value after "s=" in a nodeId like "ns=2;s=Motor_type2"
|
|
370
422
|
_extractNodeIdValue(nodeId) {
|
|
371
423
|
if (!nodeId) return "";
|
|
@@ -485,11 +537,21 @@ class OpcUaServerConfigParser {
|
|
|
485
537
|
const username = typeof userConfig.username === "string" ? userConfig.username.trim() : "";
|
|
486
538
|
const passwordHash = typeof userConfig.passwordHash === "string" ? userConfig.passwordHash : "";
|
|
487
539
|
const password = typeof userConfig.password === "string" ? userConfig.password : "";
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
: "";
|
|
540
|
+
|
|
541
|
+
let group = "";
|
|
542
|
+
if (userConfig.group !== undefined) {
|
|
543
|
+
if (Array.isArray(userConfig.group)) {
|
|
544
|
+
group = userConfig.group.map(g => typeof g === "string" ? g.trim() : "").filter(Boolean).join(",");
|
|
545
|
+
} else if (typeof userConfig.group === "string") {
|
|
546
|
+
group = userConfig.group.trim();
|
|
547
|
+
}
|
|
548
|
+
} else if (userConfig.role !== undefined) {
|
|
549
|
+
if (Array.isArray(userConfig.role)) {
|
|
550
|
+
group = userConfig.role.map(r => typeof r === "string" ? r.trim() : "").filter(Boolean).join(",");
|
|
551
|
+
} else if (typeof userConfig.role === "string") {
|
|
552
|
+
group = userConfig.role.trim();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
493
555
|
|
|
494
556
|
if (!username) {
|
|
495
557
|
throw new Error("Each user requires a non-empty username");
|
|
@@ -543,6 +605,7 @@ class OpcUaServerConfigParser {
|
|
|
543
605
|
uint16: "UInt16",
|
|
544
606
|
int32: "Int32",
|
|
545
607
|
uint32: "UInt32",
|
|
608
|
+
int64: "Int64",
|
|
546
609
|
float: "Float",
|
|
547
610
|
boolean: "Boolean",
|
|
548
611
|
string: "String",
|
|
@@ -550,7 +613,7 @@ class OpcUaServerConfigParser {
|
|
|
550
613
|
localizedText: "LocalizedText",
|
|
551
614
|
};
|
|
552
615
|
const canonical = aliases[normalized.toLowerCase()] || normalized;
|
|
553
|
-
if (!DATA_TYPE_MAP[canonical]) {
|
|
616
|
+
if (!DATA_TYPE_MAP[canonical] && (!this._enumerationsMap || !this._enumerationsMap[canonical])) {
|
|
554
617
|
throw new Error("Unsupported variable type: " + type);
|
|
555
618
|
}
|
|
556
619
|
|
|
@@ -638,7 +701,7 @@ class OpcUaServerConfigParser {
|
|
|
638
701
|
}
|
|
639
702
|
|
|
640
703
|
coerceScalarValue(value, type) {
|
|
641
|
-
if (type === "Int32") {
|
|
704
|
+
if (type === "Int32" || (this._enumerationsMap && this._enumerationsMap[type])) {
|
|
642
705
|
const parsed = Number(value);
|
|
643
706
|
if (!Number.isFinite(parsed)) {
|
|
644
707
|
return 0;
|
|
@@ -646,6 +709,66 @@ class OpcUaServerConfigParser {
|
|
|
646
709
|
return Math.trunc(parsed);
|
|
647
710
|
}
|
|
648
711
|
|
|
712
|
+
if (type === "Int64") {
|
|
713
|
+
const minVal = -9223372036854775808n;
|
|
714
|
+
const maxVal = 9223372036854775807n;
|
|
715
|
+
try {
|
|
716
|
+
let bigintVal = BigInt(value);
|
|
717
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
718
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
719
|
+
return String(bigintVal);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
const parsed = Number(value);
|
|
722
|
+
if (Number.isFinite(parsed)) {
|
|
723
|
+
if (parsed >= 9223372036854775807) {
|
|
724
|
+
return String(maxVal);
|
|
725
|
+
}
|
|
726
|
+
if (parsed <= -9223372036854775808) {
|
|
727
|
+
return String(minVal);
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
let bigintVal = BigInt(Math.trunc(parsed));
|
|
731
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
732
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
733
|
+
return String(bigintVal);
|
|
734
|
+
} catch (e2) {
|
|
735
|
+
return "0";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return "0";
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (type === "UInt64") {
|
|
743
|
+
const minVal = 0n;
|
|
744
|
+
const maxVal = 18446744073709551615n;
|
|
745
|
+
try {
|
|
746
|
+
let bigintVal = BigInt(value);
|
|
747
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
748
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
749
|
+
return String(bigintVal);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
const parsed = Number(value);
|
|
752
|
+
if (Number.isFinite(parsed)) {
|
|
753
|
+
if (parsed >= 18446744073709551615) {
|
|
754
|
+
return String(maxVal);
|
|
755
|
+
}
|
|
756
|
+
if (parsed <= 0) {
|
|
757
|
+
return String(minVal);
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
let bigintVal = BigInt(Math.trunc(parsed));
|
|
761
|
+
if (bigintVal < minVal) bigintVal = minVal;
|
|
762
|
+
else if (bigintVal > maxVal) bigintVal = maxVal;
|
|
763
|
+
return String(bigintVal);
|
|
764
|
+
} catch (e2) {
|
|
765
|
+
return "0";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return "0";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
649
772
|
if (type === "Float") {
|
|
650
773
|
const parsed = Number(value);
|
|
651
774
|
if (!Number.isFinite(parsed)) {
|