@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitormnm/node-red-simple-opcua",
3
- "version": "1.5.0",
3
+ "version": "1.6.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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
- this.authorizationDisabled = !!options.allowAnonymous;
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
- return this.getVariableRecord(identifierType, identifier).getValue();
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: state.currentValue
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: details.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
- throw new Error("Expected scalar value for type " + type + " but received array");
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) => addGroup(user && user.group));
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
- const group = typeof userConfig.group === "string"
489
- ? userConfig.group.trim()
490
- : typeof userConfig.role === "string"
491
- ? userConfig.role.trim()
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)) {
@@ -45,6 +45,7 @@ const DATA_TYPE_MAP = {
45
45
  UInt16: DataType.UInt16,
46
46
  Int32: DataType.Int32,
47
47
  UInt32: DataType.UInt32,
48
+ Int64: DataType.Int64,
48
49
  Float: DataType.Float,
49
50
  Boolean: DataType.Boolean,
50
51
  String: DataType.String,
@@ -113,6 +113,8 @@ class OpcUaServerMethods {
113
113
  const eventId = inputArguments[0].value;
114
114
  const comment = inputArguments[1].value;
115
115
 
116
+
117
+
116
118
  const alarm = context.object;
117
119
  // severity atual
118
120
  const severity = alarm.severity.readValue().value.value;
@@ -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: 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: 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 customRole = this.resolveGroupRoleNodeId(user.group);
242
- if (customRole) {
243
- roles.push(customRole);
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
 
@@ -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;