@vitormnm/node-red-simple-opcua 1.5.0 → 1.6.2
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 +3 -2
- package/server/lib/opcua-server-runtime.js +14 -5
- package/server/opcua-server.html +4 -1
- 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)) {
|
|
@@ -36,6 +36,7 @@ class OpcUaServerProcess {
|
|
|
36
36
|
throw new Error("Server already running");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
this.node.id = nodeId;
|
|
39
40
|
this.node.name = settings.name;
|
|
40
41
|
this.node.serverName = settings.serverName;
|
|
41
42
|
this.node.server = null;
|
|
@@ -859,7 +860,7 @@ process.on("uncaughtException", (err) => {
|
|
|
859
860
|
process.send({
|
|
860
861
|
type: "error",
|
|
861
862
|
data: "Uncaught Exception: " + err.message,
|
|
862
|
-
nodeId:
|
|
863
|
+
nodeId: serverProcess.node.id
|
|
863
864
|
});
|
|
864
865
|
});
|
|
865
866
|
|
|
@@ -869,6 +870,6 @@ process.on("unhandledRejection", (reason) => {
|
|
|
869
870
|
process.send({
|
|
870
871
|
type: "error",
|
|
871
872
|
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
872
|
-
nodeId:
|
|
873
|
+
nodeId: serverProcess.node.id
|
|
873
874
|
});
|
|
874
875
|
});
|
|
@@ -67,7 +67,8 @@ class OpcUaServerRuntime {
|
|
|
67
67
|
node: this.node,
|
|
68
68
|
serverName: this.serverName,
|
|
69
69
|
addressSpace: this.addressSpace,
|
|
70
|
-
allowAnonymous: this.allowAnonymous
|
|
70
|
+
allowAnonymous: this.allowAnonymous,
|
|
71
|
+
users: this.users
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
|
|
@@ -238,10 +239,18 @@ class OpcUaServerRuntime {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
const roles = [resolveNodeId("WellKnownRole_AuthenticatedUser")];
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
const groups = typeof user.group === "string"
|
|
243
|
+
? user.group.split(",").map(g => g.trim()).filter(Boolean)
|
|
244
|
+
: Array.isArray(user.group)
|
|
245
|
+
? user.group
|
|
246
|
+
: [];
|
|
247
|
+
|
|
248
|
+
groups.forEach((groupName) => {
|
|
249
|
+
const customRole = this.resolveGroupRoleNodeId(groupName);
|
|
250
|
+
if (customRole) {
|
|
251
|
+
roles.push(customRole);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
245
254
|
return roles;
|
|
246
255
|
}
|
|
247
256
|
|
package/server/opcua-server.html
CHANGED
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
<a href="#" id="node-input-add-object" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add object</a>
|
|
104
104
|
<a href="#" id="node-input-add-folder" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add folder</a>
|
|
105
105
|
<a href="#" id="node-input-add-object-type" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add object type</a>
|
|
106
|
+
<a href="#" id="node-input-add-enumeration" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add enumeration</a>
|
|
106
107
|
<a href="#" id="node-input-add-namespace" class="editor-button editor-button-small"><i class="fa fa-plus"></i> Add namespace</a>
|
|
107
108
|
<a href="#" id="node-input-expand-all" class="editor-button editor-button-small"><i class="fa fa-angle-double-down"></i> Expand all</a>
|
|
108
109
|
<a href="#" id="node-input-collapse-all" class="editor-button editor-button-small"><i class="fa fa-angle-double-up"></i> Collapse all</a>
|
|
@@ -128,6 +129,8 @@
|
|
|
128
129
|
<a href="#" data-action="add-object"><i class="fa fa-plus"></i> Add Object</a>
|
|
129
130
|
<a href="#" data-action="add-variable"><i class="fa fa-plus"></i> Add Variable</a>
|
|
130
131
|
<a href="#" data-action="add-objecttype"><i class="fa fa-plus"></i> Add ObjectType</a>
|
|
132
|
+
<a href="#" data-action="add-enumeration"><i class="fa fa-plus"></i> Add Enumeration</a>
|
|
133
|
+
<a href="#" data-action="add-enum-variable"><i class="fa fa-plus"></i> Add Enum Variable</a>
|
|
131
134
|
<a href="#" data-action="add-alarm"><i class="fa fa-plus"></i> Add Alarm</a>
|
|
132
135
|
<a href="#" data-action="add-method"><i class="fa fa-plus"></i> Add Method</a>
|
|
133
136
|
<a href="#" data-action="edit"><i class="fa fa-pencil"></i> Edit</a>
|
|
@@ -150,7 +153,7 @@
|
|
|
150
153
|
<h3>Inputs</h3>
|
|
151
154
|
<p><code>msg.payload</code>: JSON object describing the OPC UA tree. When provided, the node validates the structure and rebuilds the dynamic namespace.</p>
|
|
152
155
|
<h3>Details</h3>
|
|
153
|
-
<p>Supported variable types: <code>Int32</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
|
|
156
|
+
<p>Supported variable types: <code>Int16</code>, <code>Int32</code>, <code>Int64</code>, <code>Float</code>, <code>Boolean</code>, <code>String</code>.</p>
|
|
154
157
|
<p>Supported access modes: <code>readonly</code> and <code>readwrite</code>.</p>
|
|
155
158
|
<p>Authentication can be configured in the editor with multiple local users and groups stored in Node-RED credentials, and anonymous login can be disabled.</p>
|
|
156
159
|
</script>
|
|
@@ -457,6 +457,10 @@ body.opcua-tree-modal-open {
|
|
|
457
457
|
width: calc(100% - 100px);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
.opcua-auth-card .red-ui-typedInput-container {
|
|
461
|
+
width: calc(100% - 100px) !important;
|
|
462
|
+
}
|
|
463
|
+
|
|
460
464
|
@media (max-width: 900px) {
|
|
461
465
|
.opcua-tree-modal {
|
|
462
466
|
padding: 10px;
|