@vitormnm/node-red-simple-opcua 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,5 @@
1
- # @vitormnm/node-red-simple-opcua
2
1
  OPC UA client and server with a simple graphical interface for Node-RED.
3
- fully parameterized via JSON.
2
+ Fully parameterized in JSON.
4
3
 
5
4
  It supports the following OPC UA items on only 3 nodes.
6
5
 
@@ -9,6 +8,8 @@ It supports the following OPC UA items on only 3 nodes.
9
8
  - events read and write tags in server(See which tags are being written to or read from the client directly on the server in a simple workflow)
10
9
  - methods(write methods in node-red flow)
11
10
  - variables
11
+ - variables arrays
12
+ - description and displayname nodes
12
13
  - objects
13
14
  - simple objectsType
14
15
  - custom namespace
@@ -21,8 +22,9 @@ It supports the following OPC UA items on only 3 nodes.
21
22
  **Client editor**
22
23
  ![node-red-si](/resources/editorClient.PNG)
23
24
 
24
- example json server config
25
25
 
26
+
27
+ example json server config
26
28
  ```
27
29
  {
28
30
  "objects": [],
@@ -124,5 +126,6 @@ example json server config
124
126
  ]
125
127
  }
126
128
  ```
127
-
129
+ Disclaimer
130
+ This node was only used in simulation and testing environments.
128
131
 
@@ -0,0 +1,132 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="354.000000pt" height="229.000000pt" viewBox="0 0 354.000000 229.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+
8
+ <g transform="translate(0.000000,229.000000) scale(0.100000,-0.100000)"
9
+ fill="#000000" stroke="none">
10
+ <path d="M262 2257 l-22 -23 23 -22 23 -22 22 23 22 23 -23 22 -23 22 -22 -23z"/>
11
+ <path d="M195 2170 c-14 -16 -31 -20 -82 -20 -35 0 -63 -4 -63 -10 0 -6 26
12
+ -10 59 -10 44 0 65 -5 85 -21 l27 -21 23 21 c18 17 34 21 87 20 35 0 58 -3 52
13
+ -6 -17 -6 -16 -23 0 -23 8 0 22 -9 32 -20 l18 -20 24 22 c13 12 23 27 23 35 0
14
+ 10 37 13 180 13 155 0 179 -2 174 -15 -4 -10 0 -15 11 -15 11 0 15 5 11 15 -4
15
+ 11 2 15 21 15 l27 0 -37 -38 c-40 -41 -45 -52 -22 -52 8 0 15 5 15 12 0 7 16
16
+ -3 35 -22 l35 -34 35 34 c41 40 42 46 13 77 l-21 23 253 0 254 0 -71 -34 c-40
17
+ -19 -92 -53 -116 -75 -30 -28 -53 -41 -72 -41 -27 0 -28 1 -10 19 16 19 16 21
18
+ -3 49 -39 54 -45 55 -87 12 -38 -39 -38 -40 -20 -60 16 -18 16 -20 2 -20 -10
19
+ 0 -17 7 -17 16 0 9 -6 14 -13 11 -6 -2 -11 -10 -10 -16 4 -14 -30 -14 -44 0
20
+ -8 8 -13 8 -17 0 -4 -6 -32 -11 -63 -11 -44 0 -62 5 -80 22 l-23 21 -22 -20
21
+ c-21 -19 -37 -21 -182 -22 -122 -2 -161 1 -167 11 -7 10 -9 10 -9 1 0 -19 -40
22
+ -16 -62 4 -17 15 -19 15 -36 0 -15 -14 -42 -17 -155 -17 -85 0 -137 -4 -137
23
+ -10 0 -6 49 -10 127 -10 124 0 128 -1 155 -27 l28 -27 28 27 28 27 169 0 c92
24
+ 0 165 -3 161 -7 -3 -4 10 -24 30 -44 l37 -38 38 32 c21 18 39 38 39 45 0 7 13
25
+ 12 32 12 l33 0 -33 -32 -32 -32 42 -43 42 -43 -40 0 c-22 0 -45 5 -52 12 -9 9
26
+ -15 9 -24 0 -7 -7 -30 -12 -52 -12 -34 0 -47 7 -83 42 l-43 42 -43 -42 c-41
27
+ -41 -70 -54 -65 -31 2 6 -3 14 -9 16 -7 3 -13 -2 -13 -11 0 -13 -13 -16 -72
28
+ -16 -64 0 -75 3 -100 27 l-28 27 -28 -27 c-27 -26 -32 -27 -153 -29 l-124 -2
29
+ 103 -3 c81 -3 102 -7 102 -18 0 -8 7 -15 15 -15 8 0 15 7 15 15 0 21 24 19 45
30
+ -5 17 -19 18 -19 43 0 18 14 40 20 78 20 58 0 65 -7 34 -35 -27 -25 -25 -36
31
+ 13 -73 l33 -32 37 38 38 38 -26 28 c-28 31 -30 36 -12 36 7 0 35 -22 62 -50
32
+ 27 -27 54 -50 60 -50 6 0 33 23 60 50 27 28 55 50 62 50 20 0 16 -11 -14 -42
33
+ l-27 -28 37 -38 c20 -21 37 -41 37 -45 0 -4 -30 -7 -67 -7 -55 0 -71 4 -90 22
34
+ l-23 21 -23 -21 c-23 -21 -31 -22 -308 -24 l-284 -3 280 -2 279 -3 28 -27 28
35
+ -27 28 27 c22 21 38 27 72 27 l44 0 -49 -50 c-34 -34 -51 -46 -57 -37 -5 9
36
+ -10 9 -19 0 -10 -10 -7 -14 12 -22 14 -5 31 -16 39 -26 12 -15 10 -16 -21 -14
37
+ -101 7 -659 -2 -659 -11 0 -6 127 -10 362 -10 322 0 363 -2 380 -17 17 -15 19
38
+ -15 36 0 14 13 41 16 138 16 l121 0 28 -29 c35 -37 42 -37 76 -4 l28 27 51
39
+ -46 c28 -25 74 -57 103 -71 l52 -25 -687 -1 c-452 0 -688 -3 -688 -10 0 -7
40
+ 248 -10 725 -10 399 0 725 3 725 7 0 3 -8 15 -17 25 -16 18 -14 21 37 73 30
41
+ 30 59 55 65 55 6 0 35 -25 65 -55 51 -52 53 -55 37 -73 -9 -10 -17 -22 -17
42
+ -25 0 -4 156 -6 348 -5 l347 3 3 128 3 127 97 0 c117 0 188 19 237 61 25 22
43
+ 36 27 39 17 3 -7 12 -36 21 -65 35 -112 143 -216 267 -260 63 -21 224 -22 303
44
+ -1 l50 13 3 153 3 152 -57 -31 c-101 -58 -210 -38 -259 46 -46 78 -27 163 48
45
+ 219 22 16 43 21 95 21 59 0 74 -4 118 -32 28 -18 53 -33 55 -33 2 0 3 70 1
46
+ 155 l-3 154 -42 13 c-24 7 -95 13 -162 13 -114 0 -123 -2 -186 -31 -72 -34
47
+ -136 -87 -180 -147 l-27 -38 -11 34 c-29 87 -133 160 -252 177 -36 5 -558 10
48
+ -1169 10 -1087 1 -1105 1 -1125 20 -22 20 -22 20 -40 0z m1875 -115 l0 -75
49
+ -41 0 c-35 0 -49 7 -86 41 -24 22 -76 56 -116 75 l-71 34 157 0 157 0 0 -75z
50
+ m-881 -150 c-15 -30 -29 -60 -32 -67 -3 -10 -11 -7 -29 9 l-24 23 -29 -30
51
+ c-32 -34 -49 -37 -72 -14 -20 20 -2 44 20 26 11 -9 20 -5 42 18 l29 30 -29 30
52
+ -28 29 90 0 89 -1 -27 -53z m881 -17 l0 -73 -24 58 c-13 32 -29 64 -36 72 -11
53
+ 13 -7 15 24 15 l36 0 0 -72z m-1044 21 c10 -17 -13 -36 -27 -22 -12 12 -4 33
54
+ 11 33 5 0 12 -5 16 -11z m1462 -19 c44 -42 17 -104 -53 -120 -66 -16 -65 -17
55
+ -65 65 l0 75 48 0 c35 0 54 -6 70 -20z m-628 -45 l54 -55 -59 -60 c-41 -41
56
+ -62 -56 -70 -50 -8 7 -15 2 -23 -15 -12 -25 -12 -25 -156 -25 l-145 0 -7 37
57
+ c-4 20 -3 52 2 72 l9 36 105 3 c140 4 158 22 21 22 -95 0 -102 1 -90 17 36 43
58
+ 122 55 181 24 l37 -18 32 33 c18 19 38 34 44 34 6 0 35 -25 65 -55z m-400 29
59
+ c0 -9 -5 -14 -12 -12 -18 6 -21 28 -4 28 9 0 16 -7 16 -16z m-775 -43 c7 -12
60
+ -12 -24 -25 -16 -11 7 -4 25 10 25 5 0 11 -4 15 -9z m773 -12 c-2 -6 -8 -10
61
+ -13 -10 -5 0 -11 4 -13 10 -2 6 4 11 13 11 9 0 15 -5 13 -11z m-419 -35 l23
62
+ -16 -43 -44 c-34 -34 -40 -47 -31 -56 20 -20 14 -24 -43 -26 -30 -1 -55 2 -55
63
+ 5 0 4 18 26 40 48 l40 41 -25 24 c-14 13 -25 27 -25 32 0 15 95 9 119 -8z
64
+ m286 -34 l50 -50 -53 -53 -52 -52 -52 52 -53 53 50 50 c27 27 52 50 55 50 3 0
65
+ 28 -23 55 -50z m-460 10 c-5 -8 -11 -8 -17 -2 -6 6 -7 16 -3 22 5 8 11 8 17 2
66
+ 6 -6 7 -16 3 -22z m-184 5 c-1 -15 -24 -12 -29 3 -3 9 2 13 12 10 10 -1 17 -7
67
+ 17 -13z m439 -4 c0 -14 -18 -23 -30 -16 -6 4 -8 11 -5 16 8 12 35 12 35 0z
68
+ m-252 -49 c2 -7 -3 -12 -12 -12 -9 0 -16 7 -16 16 0 17 22 14 28 -4z m247 -12
69
+ c0 -7 -6 -15 -12 -17 -8 -3 -13 4 -13 17 0 13 5 20 13 18 6 -3 12 -11 12 -18z
70
+ m305 -1 c0 -5 -7 -9 -15 -9 -15 0 -20 12 -9 23 8 8 24 -1 24 -14z m-8 -54 c0
71
+ -5 -5 -11 -11 -13 -6 -2 -11 4 -11 13 0 9 5 15 11 13 6 -2 11 -8 11 -13z
72
+ m-482 -22 c0 -12 -43 -53 -55 -53 -6 0 -24 14 -40 30 l-29 30 62 0 c34 0 62
73
+ -3 62 -7z m122 -20 l28 -27 28 27 c33 32 42 34 42 9 0 -10 -20 -38 -45 -62
74
+ -25 -24 -45 -50 -45 -57 0 -8 -13 -13 -32 -13 l-32 0 34 35 34 35 -39 40 c-31
75
+ 32 -35 40 -20 40 10 0 32 -12 47 -27z m692 16 c-3 -6 14 -32 40 -58 l46 -47
76
+ -50 -49 -50 -50 -52 52 c-44 44 -50 54 -39 67 12 14 8 16 -35 16 -53 0 -105
77
+ 25 -124 59 -11 21 -9 21 130 21 100 0 139 -3 134 -11z m-414 -20 c0 -5 -7 -9
78
+ -15 -9 -15 0 -20 12 -9 23 8 8 24 -1 24 -14z m-362 0 c-2 -6 -8 -10 -13 -10
79
+ -5 0 -11 4 -13 10 -2 6 4 11 13 11 9 0 15 -5 13 -11z m1102 -59 c0 -58 -1 -60
80
+ -26 -60 -14 0 -24 3 -22 8 2 4 12 31 23 60 11 28 21 52 22 52 2 0 3 -27 3 -60z
81
+ m-580 -25 l54 -55 -59 -60 -59 -60 -58 57 -58 57 57 58 c32 32 60 58 63 58 3
82
+ 0 30 -25 60 -55z m-525 15 c-5 -8 -11 -8 -17 -2 -6 6 -7 16 -3 22 5 8 11 8 17
83
+ 2 6 -6 7 -16 3 -22z m365 15 c0 -8 -7 -15 -15 -15 -16 0 -20 12 -8 23 11 12
84
+ 23 8 23 -8z m-410 -50 c11 -13 8 -15 -20 -15 -28 0 -31 2 -20 15 7 8 16 15 20
85
+ 15 4 0 13 -7 20 -15z m1150 -110 l0 -75 -137 0 -137 0 53 26 c29 15 76 48 104
86
+ 75 40 37 60 48 85 49 l32 0 0 -75z"/>
87
+ <path d="M1750 1825 c0 -9 5 -15 11 -13 6 2 11 8 11 13 0 5 -5 11 -11 13 -6 2
88
+ -11 -4 -11 -13z"/>
89
+ <path d="M1750 1765 c0 -9 5 -15 11 -13 6 2 11 8 11 13 0 5 -5 11 -11 13 -6 2
90
+ -11 -4 -11 -13z"/>
91
+ <path d="M1240 1735 c0 -9 5 -15 11 -13 6 2 11 8 11 13 0 5 -5 11 -11 13 -6 2
92
+ -11 -4 -11 -13z"/>
93
+ <path d="M1240 1675 c0 -9 5 -15 11 -13 6 2 11 8 11 13 0 5 -5 11 -11 13 -6 2
94
+ -11 -4 -11 -13z"/>
95
+ <path d="M1697 1574 c-8 -8 1 -24 14 -24 5 0 9 7 9 15 0 15 -12 20 -23 9z"/>
96
+ <path d="M1692 1508 c6 -18 28 -21 28 -4 0 9 -7 16 -16 16 -9 0 -14 -5 -12
97
+ -12z"/>
98
+ <path d="M1380 1480 c0 -5 7 -10 16 -10 8 0 12 5 9 10 -3 6 -10 10 -16 10 -5
99
+ 0 -9 -4 -9 -10z"/>
100
+ <path d="M1380 1425 c0 -9 5 -15 11 -13 6 2 11 8 11 13 0 5 -5 11 -11 13 -6 2
101
+ -11 -4 -11 -13z"/>
102
+ <path d="M610 2075 l-24 -26 27 -27 27 -27 27 27 27 27 -24 26 c-13 14 -26 25
103
+ -30 25 -4 0 -17 -11 -30 -25z"/>
104
+ <path d="M547 2074 c-8 -8 1 -24 14 -24 5 0 9 7 9 15 0 15 -12 20 -23 9z"/>
105
+ <path d="M1047 2064 c-8 -8 1 -24 14 -24 5 0 9 7 9 15 0 15 -12 20 -23 9z"/>
106
+ <path d="M547 2014 c-8 -8 1 -24 14 -24 5 0 9 7 9 15 0 15 -12 20 -23 9z"/>
107
+ <path d="M443 1939 c3 -9 1 -21 -4 -26 -6 -6 2 -20 19 -36 l28 -27 23 24 c22
108
+ 23 22 25 6 50 -18 27 -39 34 -51 15 -4 -8 -9 -7 -16 2 -8 11 -9 10 -5 -2z"/>
109
+ <path d="M712 1918 c6 -18 28 -21 28 -4 0 9 -7 16 -16 16 -9 0 -14 -5 -12 -12z"/>
110
+ <path d="M857 1884 c-8 -8 1 -24 14 -24 5 0 9 7 9 15 0 15 -12 20 -23 9z"/>
111
+ <path d="M250 1840 c0 -5 7 -10 15 -10 8 0 15 5 15 10 0 6 -7 10 -15 10 -8 0
112
+ -15 -4 -15 -10z"/>
113
+ <path d="M422 1749 c2 -6 8 -10 13 -10 5 0 11 4 13 10 2 6 -4 11 -13 11 -9 0
114
+ -15 -5 -13 -11z"/>
115
+ <path d="M422 1689 c2 -6 8 -10 13 -10 5 0 11 4 13 10 2 6 -4 11 -13 11 -9 0
116
+ -15 -5 -13 -11z"/>
117
+ <path d="M1114 995 c-11 -28 3 -579 15 -620 45 -143 162 -222 346 -232 196
118
+ -11 334 56 397 192 22 48 23 61 26 363 l3 313 -153 -3 -153 -3 -5 -283 c-6
119
+ -321 -3 -312 -87 -312 -75 0 -77 9 -81 327 l-3 273 -150 0 c-122 0 -151 -3
120
+ -155 -15z"/>
121
+ <path d="M2151 992 c-11 -20 -291 -825 -291 -835 0 -4 72 -7 160 -7 120 0 162
122
+ 3 165 13 2 6 9 31 15 55 l11 43 116 -3 116 -3 16 -50 16 -50 164 -3 c130 -2
123
+ 162 0 158 10 -2 7 -66 186 -142 398 -75 212 -143 400 -151 418 l-13 32 -165 0
124
+ c-150 0 -166 -2 -175 -18z m208 -392 c12 -58 25 -113 27 -122 5 -16 -2 -18
125
+ -55 -18 -45 0 -61 4 -61 13 0 8 11 65 25 127 14 62 25 115 25 119 0 3 4 1 8
126
+ -4 5 -6 19 -57 31 -115z"/>
127
+ <path d="M2590 1001 c11 -8 11 -11 2 -11 -7 0 -17 -10 -23 -22 -9 -22 -9 -21
128
+ -6 4 2 15 0 25 -4 22 -26 -15 6 -72 33 -61 8 3 5 6 -6 6 -15 1 -17 4 -8 13 8
129
+ 8 15 8 28 -3 16 -12 17 -12 9 1 -6 8 -10 24 -10 34 0 11 -7 21 -15 23 -13 4
130
+ -13 3 0 -6z"/>
131
+ </g>
132
+ </svg>
@@ -24,17 +24,56 @@ async function browseNode(session, root) {
24
24
  resultMask: 63
25
25
  });
26
26
 
27
- const references = browseResult && Array.isArray(browseResult.references)
28
- ? browseResult.references
29
- : [];
27
+ const references = browseResult?.references ?? [];
28
+ if (!references.length) return result;
29
+
30
+ // Monta lista de todos os atributos de todos os nós de uma vez
31
+ const nodeIds = references.map(ref => normalizeNodeId(ref.nodeId));
32
+ const attributesToRead = nodeIds.flatMap(nodeId => [
33
+ { nodeId, attributeId: AttributeIds.Description },
34
+ { nodeId, attributeId: AttributeIds.DataType },
35
+ { nodeId, attributeId: AttributeIds.Value },
36
+ ]);
37
+
38
+ // UMA única chamada para todos os nós e atributos
39
+ const dataValues = await session.read(attributesToRead);
40
+
41
+ // Distribui os resultados por nó (3 atributos por nó)
42
+ result.browse = await Promise.all(references.map(async (reference, i) => {
43
+ const childNodeId = nodeIds[i];
44
+ const nodeClass = resolveNodeClassName(reference.nodeClass);
45
+ const browseName = extractBrowseName(reference.browseName, childNodeId);
46
+ const displayName = extractDisplayName(reference.displayName, browseName);
47
+
48
+ const descValue = dataValues[i * 3]?.value?.value;
49
+ const description = typeof descValue === "string"
50
+ ? descValue
51
+ : (descValue?.text ?? "");
52
+
53
+ const item = { nodeID: childNodeId, nodeClass, browseName, displayName, description };
54
+
55
+ if (nodeClass === "Variable") {
56
+ const dataTypeValue = dataValues[i * 3 + 1]?.value?.value;
57
+ const rawValue = dataValues[i * 3 + 2]?.value?.value;
58
+
59
+ item.dataType = dataTypeValue?.namespace === 0 && typeof dataTypeValue?.value === "number"
60
+ ? (DataType[dataTypeValue.value] || dataTypeValue.toString())
61
+ : (dataTypeValue?.toString() ?? "");
62
+
63
+ item.value = rawValue ?? "";
64
+ }
30
65
 
31
- for (const reference of references) {
32
- result.browse.push(await mapReference(session, reference));
33
- }
66
+ if (nodeClass === "Method") {
67
+ const definition = await readMethodArguments(session, childNodeId);
68
+ item.inputArguments = definition.inputArguments;
69
+ item.outputArguments = definition.outputArguments;
70
+ }
71
+
72
+ return item;
73
+ }));
34
74
 
35
75
  return result;
36
76
  }
37
-
38
77
  function normalizeBrowseRoots(payload) {
39
78
  if (payload === undefined || payload === null) {
40
79
  return [{ name: "RootFolder", nodeID: ROOT_NODE_ID }];
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ const { dataValueToItemResult, ensureArrayPayload, resolveNodeId, resolveMethodObjectId, buildVariantFromItem, callResultToItemResult } = require("../opcua-client-utils");
4
+
5
+ class OpcUaClientMethodService {
6
+ async execute(node, msg, session, itemsResolver) {
7
+
8
+ // const items = ensureArrayPayload(msg, "OPC UA method call");
9
+ const items = itemsResolver.ensureMethodItems(node, msg, "OPC UA method");
10
+ const payload = [];
11
+
12
+
13
+
14
+ for (const item of items) {
15
+ const methodNodeId = this.resolveMethodId(item);
16
+
17
+ try {
18
+ const objectId = this.resolveMethodObjectIdFromItem(item) || await resolveMethodObjectId(
19
+ session,
20
+ methodNodeId,
21
+ node.connection.methodObjectIdCache
22
+ );
23
+ const argumentDefinition = await this.safeGetMethodArgumentDefinition(
24
+ session,
25
+ methodNodeId,
26
+ node.connection.methodDefinitionCache
27
+ );
28
+ const callRequest = {
29
+ objectId,
30
+ methodId: methodNodeId
31
+ };
32
+
33
+ if (Array.isArray(item.inputs) && item.inputs.length > 0) {
34
+ callRequest.inputArguments = item.inputs.map((input) => buildVariantFromItem(input, input.type));
35
+ }
36
+
37
+ const callResult = await session.call(callRequest);
38
+ payload.push(callResultToItemResult(item, callResult, argumentDefinition));
39
+ } catch (itemError) {
40
+ payload.push({
41
+ name: item.name || methodNodeId,
42
+ nodeID: methodNodeId,
43
+ status: itemError.message,
44
+ outputs: []
45
+ });
46
+ }
47
+ }
48
+
49
+ return payload;
50
+ }
51
+
52
+
53
+
54
+ resolveMethodId(item) {
55
+ const methodId = item && (item.methodID || item.methodId || item.nodeID || item.nodeId);
56
+ if (!methodId || !String(methodId).trim()) {
57
+ throw new Error("Each method item must contain methodId or nodeID");
58
+ }
59
+
60
+ return String(methodId).trim();
61
+ }
62
+
63
+
64
+ resolveMethodObjectIdFromItem(item) {
65
+ const objectId = item && (item.objectID || item.objectId);
66
+ if (!objectId || !String(objectId).trim()) {
67
+ return "";
68
+ }
69
+
70
+ return String(objectId).trim();
71
+ }
72
+
73
+ async safeGetMethodArgumentDefinition(session, methodNodeId, cache) {
74
+ try {
75
+ return await getMethodArgumentDefinition(session, methodNodeId, cache);
76
+ } catch (error) {
77
+ return {
78
+ inputArguments: [],
79
+ outputArguments: []
80
+ };
81
+ }
82
+ }
83
+
84
+ }
85
+
86
+ module.exports = {
87
+ OpcUaClientMethodService
88
+ };
@@ -1,53 +1,147 @@
1
1
  "use strict";
2
2
 
3
- const { DataType, coerceNodeId } = require("node-opcua");
3
+ const { AttributeIds, DataType, coerceNodeId } = require("node-opcua");
4
4
  const {
5
5
  buildVariantFromItem,
6
- dataValueToItemResult,
7
6
  normalizeTypeName,
8
7
  resolveNodeId,
9
8
  statusCodeToString
10
9
  } = require("../opcua-client-utils");
11
10
 
11
+ // Máximo de tags por chamada session.write (ajuste conforme limite do servidor)
12
+ const WRITE_BATCH_SIZE = 100;
13
+
14
+ // Batches em paralelo simultâneos
15
+ const CONCURRENCY = 5;
16
+
17
+ // Cede o event loop a cada N itens para não travar o Node-RED
18
+ const YIELD_EVERY = 50;
19
+
12
20
  class OpcUaClientWriteService {
13
21
  async execute(node, msg, session, itemsResolver) {
14
22
  const items = itemsResolver.ensureWriteItems(node, msg);
15
- const results = [];
16
-
17
- for (const item of items) {
18
- const nodeId = resolveNodeId(item);
19
-
20
- try {
21
- const explicitType = normalizeTypeName(item.type);
22
- const builtInType = await session.getBuiltInDataType(coerceNodeId(nodeId));
23
- const typeName = explicitType || DataType[builtInType];
24
- const variant = buildVariantFromItem(item, typeName);
25
- const statusCode = await session.writeSingleNode(nodeId, variant);
26
- const dataValue = await session.readVariableValue(nodeId);
27
- const result = dataValueToItemResult(item, dataValue);
28
-
29
- if (statusCode && statusCode.name && statusCode.name !== "Good") {
30
- result.status = statusCodeToString(statusCode);
31
- }
32
-
33
- results.push(result);
34
- } catch (itemError) {
35
- results.push({
36
- name: item.name || nodeId,
37
- nodeID: nodeId,
38
- value: item.value,
39
- type: normalizeTypeName(item.type) || null,
40
- status: itemError.message,
41
- sourceTimestamp: null,
42
- serverTimestamp: null
43
- });
23
+
24
+ // 1. Resolve variantes (tipo + valor) — consulta servidor só para quem não tem tipo explícito
25
+ const variants = await resolveVariants(session, items);
26
+
27
+ // 2. Escreve todos os nós em batches paralelos
28
+ const statusCodes = await writeBatches(session, items, variants);
29
+
30
+ // 3. Monta resultados — statusCode Good já confirma a escrita, sem round-trip extra
31
+ return buildResults(items, variants, statusCodes);
32
+ }
33
+ }
34
+
35
+ // ─── Resolução de tipos ──────────────────────────────────────────────────────
36
+
37
+ async function resolveVariants(session, items) {
38
+ // Separa quais itens precisam consultar o tipo no servidor
39
+ const needsLookup = items
40
+ .map((item, index) => ({ item, index }))
41
+ .filter(({ item }) => !normalizeTypeName(item.type));
42
+
43
+ // Busca tipos desconhecidos em paralelo
44
+ const resolvedTypes = new Map();
45
+
46
+ await mapConcurrent(needsLookup, CONCURRENCY * 2, async ({ item, index }) => {
47
+ try {
48
+ const builtInType = await session.getBuiltInDataType(coerceNodeId(resolveNodeId(item)));
49
+ resolvedTypes.set(index, DataType[builtInType]);
50
+ } catch {
51
+ resolvedTypes.set(index, "String");
52
+ }
53
+ });
54
+
55
+ return items.map((item, index) => {
56
+ const typeName = normalizeTypeName(item.type) || resolvedTypes.get(index) || "String";
57
+ return buildVariantFromItem(item, typeName);
58
+ });
59
+ }
60
+
61
+ // ─── Escrita em batches paralelos ────────────────────────────────────────────
62
+
63
+ async function writeBatches(session, items, variants) {
64
+ const allStatusCodes = new Array(items.length);
65
+
66
+ // Divide em batches de WRITE_BATCH_SIZE
67
+ const batches = [];
68
+ for (let i = 0; i < items.length; i += WRITE_BATCH_SIZE) {
69
+ batches.push({ start: i, end: Math.min(i + WRITE_BATCH_SIZE, items.length) });
70
+ }
71
+
72
+ let processed = 0;
73
+
74
+ await mapConcurrent(batches, CONCURRENCY, async ({ start, end }) => {
75
+ const nodesToWrite = items.slice(start, end).map((item, i) => ({
76
+ nodeId: coerceNodeId(resolveNodeId(item)),
77
+ attributeId: AttributeIds.Value,
78
+ value: { value: variants[start + i] }
79
+ }));
80
+
81
+ try {
82
+ const statusCodes = await session.write(nodesToWrite);
83
+ statusCodes.forEach((sc, i) => {
84
+ allStatusCodes[start + i] = sc;
85
+ });
86
+ } catch (batchError) {
87
+ // Se o batch falhar por completo, marca todos com erro
88
+ for (let i = start; i < end; i++) {
89
+ allStatusCodes[i] = { name: batchError.message, value: -1 };
44
90
  }
45
91
  }
46
92
 
47
- return results;
93
+ // Cede o event loop a cada YIELD_EVERY itens para não travar o Node-RED
94
+ processed += end - start;
95
+ if (processed % YIELD_EVERY === 0) {
96
+ await yieldEventLoop();
97
+ }
98
+ });
99
+
100
+ return allStatusCodes;
101
+ }
102
+
103
+ // ─── Montagem dos resultados ─────────────────────────────────────────────────
104
+
105
+ function buildResults(items, variants, statusCodes) {
106
+ return items.map((item, index) => {
107
+ const nodeId = resolveNodeId(item);
108
+ const sc = statusCodes[index];
109
+ const scName = sc && sc.name ? sc.name : "Good";
110
+ const typeName = DataType[variants[index].dataType] || null;
111
+
112
+ return {
113
+ name: item.name || nodeId,
114
+ nodeID: nodeId,
115
+ value: variants[index].value,
116
+ type: typeName,
117
+ status: scName,
118
+ sourceTimestamp: null,
119
+ serverTimestamp: null
120
+ };
121
+ });
122
+ }
123
+
124
+ // ─── Utilitários ─────────────────────────────────────────────────────────────
125
+
126
+ async function mapConcurrent(items, concurrency, fn) {
127
+ let index = 0;
128
+
129
+ async function worker() {
130
+ while (index < items.length) {
131
+ const i = index++;
132
+ await fn(items[i], i);
133
+ }
48
134
  }
135
+
136
+ await Promise.all(
137
+ Array.from({ length: Math.min(concurrency, items.length) }, worker)
138
+ );
139
+ }
140
+
141
+ function yieldEventLoop() {
142
+ return new Promise(resolve => setImmediate(resolve));
49
143
  }
50
144
 
51
145
  module.exports = {
52
146
  OpcUaClientWriteService
53
- };
147
+ };
@@ -13,11 +13,15 @@ const {
13
13
  ROOT_NODE_ID
14
14
  } = require("./lib/opcua-client-browser");
15
15
 
16
+
17
+
16
18
  module.exports = function (RED) {
17
19
  function OpcUaClientConfigNode(config) {
18
20
  RED.nodes.createNode(this, config);
19
21
  const node = this;
20
22
 
23
+
24
+
21
25
  node.name = (config.name || "").trim();
22
26
  node.endpoint = (config.endpoint || "").trim();
23
27
  node.securityPolicy = config.securityPolicy || "None";
@@ -74,6 +78,7 @@ module.exports = function (RED) {
74
78
 
75
79
  this.client = client;
76
80
  this.session = session;
81
+
77
82
  return session;
78
83
  } catch (error) {
79
84
  try {
@@ -0,0 +1,61 @@
1
+ <script type="text/html" data-help-name="opcua-client">
2
+ <p>Unified OPC UA client node for read, write, browse and subscription using a shared client configuration node.</p>
3
+
4
+ <h3>Modes</h3>
5
+ <p><b>Read</b>: reads one or more variable values.</p>
6
+ <p><b>Write</b>: writes one or more variable values.</p>
7
+ <p><b>Browse</b>: browses one or more OPC UA nodes and returns their children.</p>
8
+ <p><b>Method</b>: calls one or more OPC UA methods.</p>
9
+ <p><b>Subscription</b>: subscribes to one or more variable values and emits one message per change.</p>
10
+
11
+ <h3>Read</h3>
12
+ <p><b>Input</b> <code>msg.payload</code>:</p>
13
+ <pre><code>[
14
+ {
15
+ "name": "status",
16
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.status"
17
+ }
18
+ ]</code></pre>
19
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node reads that configured node.</p>
20
+
21
+ <h3>Write</h3>
22
+ <p><b>Input</b> <code>msg.payload</code>:</p>
23
+ <pre><code>[
24
+ {
25
+ "name": "speed",
26
+ "nodeID": "ns=2;s=Factory.Line1.Motor1.speed",
27
+ "value": 25.5,
28
+ "type": "Float"
29
+ }
30
+ ]</code></pre>
31
+
32
+ <h3>Browse</h3>
33
+ <p>If <code>msg.payload</code> is not an array and <code>NodeId</code> is configured, the node browses that configured node.</p>
34
+ <p>If <code>msg.payload = []</code>, the node browses the OPC UA <code>RootFolder</code>.</p>
35
+
36
+ <h3>Method</h3>
37
+ <p><code>nodeID</code> and <code>objectId</code> are accepted explicitly. For backward compatibility, <code>nodeID</code> is also accepted as the method id.</p>
38
+ <p><b>Input</b> <code>msg.payload</code>:</p>
39
+ <pre><code>[
40
+ {
41
+ "name": "method1",
42
+ "nodeID": "ns=2;s=motor.method1",
43
+ "inputs": [
44
+ {
45
+ "name": "input1",
46
+ "type": "Int32",
47
+ "value": 1
48
+ },
49
+ {
50
+ "name": "input2",
51
+ "type": "Int32",
52
+ "value": 1
53
+ }
54
+ ]
55
+ },
56
+
57
+
58
+ ]</code></pre>
59
+ <h3>Subscription</h3>
60
+ <p>In subscription mode the node emits one message per value change.</p>
61
+ </script>