@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 +7 -4
- package/client/icons/opcua.svg +132 -0
- package/client/lib/opcua-client-browser.js +46 -7
- package/client/lib/opcua-client-method-service.js +88 -0
- package/client/lib/opcua-client-write-service.js +127 -33
- package/client/opcua-client-config.js +5 -0
- package/client/opcua-client-help.html +61 -0
- package/client/opcua-client-utils.js +68 -2
- package/client/opcua-client.html +14 -1098
- package/client/opcua-client.js +108 -53
- package/client/testClient.json +18 -0
- package/client/view/opcua-client.css +411 -0
- package/client/view/opcua-client.js +1141 -0
- package/icons/opcua.svg +132 -0
- package/icons/opcua2.svg +132 -0
- package/package.json +3 -3
- package/server/icons/opcua.svg +132 -0
- package/server/lib/opcua-address-space-builder.js +24 -2
- package/server/lib/opcua-server-runtime-child.js +117 -26
- package/server/opcua-server-io.html +1 -1
- package/server/opcua-server-io.js +31 -2
- package/server/opcua-server.html +154 -58
- package/server/opcua-server.js +7 -3
- package/object.json +0 -65
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
|
-
|
|
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
|

|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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>
|