@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.
@@ -20,18 +20,31 @@ const {
20
20
 
21
21
  const { OpcUaClientReadService } = require("./lib/opcua-client-read-service");
22
22
  const { OpcUaClientWriteService } = require("./lib/opcua-client-write-service");
23
+ const { OpcUaClientMethodService } = require("./lib/opcua-client-method-service");
23
24
  const { OpcUaClientSubscriptionService } = require("./lib/opcua-client-subscription-service");
24
25
  const { OpcUaClientSubscriptionIdService } = require("./lib/opcua-client-subscription-id-service");
26
+ const path = require("path");
27
+
28
+
29
+ const fs = require("fs");
30
+ const arquivo = path.join(__dirname, "testClient.json");
25
31
 
26
32
  module.exports = function (RED) {
27
33
  function OpcUaClientNode(config) {
28
34
  RED.nodes.createNode(this, config);
29
35
  const node = this;
30
36
 
37
+ fs.writeFileSync(
38
+ arquivo,
39
+ JSON.stringify(config, null, 2),
40
+ "utf8"
41
+ );
42
+
31
43
  node.name = (config.name || "").trim();
32
44
  node.connection = RED.nodes.getNode(config.connection);
33
45
  node.mode = config.mode || "read";
34
46
  node.selectedItems = parseConfiguredItems(config.selectedItems);
47
+ node.methodItems = parseConfiguredMethodsItems(config.selectedItems);
35
48
  node.valueProperty = (config.valueProperty || "payload").trim();
36
49
  node.valuePropertyType = config.valuePropertyType || "msg";
37
50
  node.samplingInterval = Math.max(Number(config.samplingInterval) || 250, 50);
@@ -42,6 +55,7 @@ module.exports = function (RED) {
42
55
  const itemsResolver = createItemsResolver(RED);
43
56
  const readService = new OpcUaClientReadService();
44
57
  const writeService = new OpcUaClientWriteService();
58
+ const methodService = new OpcUaClientMethodService();
45
59
  const subscriptionService = new OpcUaClientSubscriptionService();
46
60
  const subscriptionIdService = new OpcUaClientSubscriptionIdService();
47
61
 
@@ -70,19 +84,24 @@ module.exports = function (RED) {
70
84
  }
71
85
 
72
86
  const session = await node.connection.getSession();
87
+
73
88
  let payload;
74
89
 
75
90
  if (node.mode === "read") {
91
+
76
92
  payload = await readService.execute(node, msg, session, itemsResolver);
77
93
  node.status({ fill: "green", shape: "dot", text: "read " + payload.length + " nodes" });
78
94
  } else if (node.mode === "write") {
95
+
79
96
  payload = await writeService.execute(node, msg, session, itemsResolver);
80
97
  node.status({ fill: "green", shape: "dot", text: "write " + payload.length + " nodes" });
81
98
  } else if (node.mode === "browse") {
82
99
  payload = await executeBrowse(node, msg, session);
83
100
  node.status({ fill: "green", shape: "dot", text: "browsed " + payload.length + " nodes" });
84
101
  } else if (node.mode === "method") {
85
- payload = await executeMethod(node, msg, session);
102
+ //payload = await executeMethod(node, msg, session);
103
+
104
+ payload = await methodService.execute(node, msg, session, itemsResolver);
86
105
  node.status({ fill: "green", shape: "dot", text: "called " + payload.length + " methods" });
87
106
  } else if (node.mode === "getSubscriptionId") {
88
107
  payload = await subscriptionIdService.execute(node);
@@ -121,63 +140,14 @@ module.exports = function (RED) {
121
140
  return payload;
122
141
  }
123
142
 
124
- async function executeMethod(node, msg, session) {
125
- const items = ensureArrayPayload(msg, "OPC UA method call");
126
- const payload = [];
127
-
128
- for (const item of items) {
129
- const methodNodeId = resolveMethodId(item);
130
-
131
- try {
132
- const objectId = resolveMethodObjectIdFromItem(item) || await resolveMethodObjectId(
133
- session,
134
- methodNodeId,
135
- node.connection.methodObjectIdCache
136
- );
137
- const argumentDefinition = await safeGetMethodArgumentDefinition(
138
- session,
139
- methodNodeId,
140
- node.connection.methodDefinitionCache
141
- );
142
- const callRequest = {
143
- objectId,
144
- methodId: methodNodeId
145
- };
146
-
147
- if (Array.isArray(item.inputs) && item.inputs.length > 0) {
148
- callRequest.inputArguments = item.inputs.map((input) => buildVariantFromItem(input, input.type));
149
- }
150
-
151
- const callResult = await session.call(callRequest);
152
- payload.push(callResultToItemResult(item, callResult, argumentDefinition));
153
- } catch (itemError) {
154
- payload.push({
155
- name: item.name || methodNodeId,
156
- nodeID: methodNodeId,
157
- status: itemError.message,
158
- outputs: []
159
- });
160
- }
161
- }
162
143
 
163
- return payload;
164
- }
165
144
 
166
- async function safeGetMethodArgumentDefinition(session, methodNodeId, cache) {
167
- try {
168
- return await getMethodArgumentDefinition(session, methodNodeId, cache);
169
- } catch (error) {
170
- return {
171
- inputArguments: [],
172
- outputArguments: []
173
- };
174
- }
175
- }
176
145
 
177
146
  function createItemsResolver(REDRuntime) {
178
147
  return {
179
148
  ensureClientItems,
180
- ensureWriteItems
149
+ ensureWriteItems,
150
+ ensureMethodItems
181
151
  };
182
152
 
183
153
  function ensureClientItems(node, msg, contextName) {
@@ -206,13 +176,49 @@ module.exports = function (RED) {
206
176
  return node.selectedItems.map((item, index) => ({
207
177
  name: item.name,
208
178
  nodeID: item.nodeID,
209
- type: item.type,
179
+ type: item.dataType,
210
180
  value: resolveWriteValueForItem(node, msg, item, index, configuredValue, REDRuntime)
211
181
  }));
212
182
  }
213
183
 
214
184
  return ensureArrayPayload(msg, "OPC UA write");
215
185
  }
186
+
187
+ function ensureMethodItems(node, msg) {
188
+ const payload = msg ? msg.payload : undefined;
189
+
190
+ if (Array.isArray(payload) && payload.length > 0) {
191
+ return payload;
192
+ }
193
+
194
+ if (node.methodItems?.length > 0) {
195
+ return node.methodItems.map(item => ({
196
+ name: item.name,
197
+ nodeID: item.nodeID,
198
+ objectID: item.objectId,
199
+ inputs: item.inputs.map(input => {
200
+ const value = resolveConfiguredWriteValue(
201
+ {
202
+ ...node,
203
+ valueProperty: input.valueProperty,
204
+ valuePropertyType: input.valuePropertyType
205
+ },
206
+ msg,
207
+ REDRuntime
208
+ );
209
+
210
+ return {
211
+ name: input.name,
212
+ type: input.dataType,
213
+ value
214
+ };
215
+ }),
216
+ outputs: item.outputs || []
217
+ }));
218
+ }
219
+
220
+ return ensureArrayPayload(msg, "OPC UA method");
221
+ }
216
222
  }
217
223
 
218
224
  function normalizeBrowseRoots(node, payload) {
@@ -268,6 +274,25 @@ module.exports = function (RED) {
268
274
  return String(methodId).trim();
269
275
  }
270
276
 
277
+ function parseConfiguredMethodsItems(rawValue) {
278
+ try {
279
+ if (!rawValue || typeof rawValue !== "string") {
280
+ return [];
281
+ }
282
+
283
+
284
+ const parsed = JSON.parse(rawValue);
285
+ if (!Array.isArray(parsed)) {
286
+ return [];
287
+ }
288
+
289
+ return parsed
290
+
291
+ } catch (error) {
292
+ return [];
293
+ }
294
+ }
295
+
271
296
  function parseConfiguredItems(rawValue) {
272
297
  if (!rawValue || typeof rawValue !== "string") {
273
298
  return [];
@@ -344,6 +369,25 @@ module.exports = function (RED) {
344
369
  return REDRuntime.util.getMessageProperty(msg, property);
345
370
  }
346
371
 
372
+ function resolveConfiguredMethodValue(node, msg, REDRuntime) {
373
+ const property = node.valueProperty || "payload";
374
+ const type = node.valuePropertyType || "msg";
375
+
376
+ if (type === "msg") {
377
+ return REDRuntime.util.getMessageProperty(msg, property);
378
+ }
379
+
380
+ if (type === "flow") {
381
+ return node.context().flow.get(property);
382
+ }
383
+
384
+ if (type === "global") {
385
+ return node.context().global.get(property);
386
+ }
387
+
388
+ return REDRuntime.util.getMessageProperty(msg, property);
389
+ }
390
+
347
391
  function selectWriteValueForItem(configuredValue, item, index) {
348
392
  if (Array.isArray(configuredValue)) {
349
393
  const byName = item && item.name ? configuredValue.find((entry) => entry && entry.name === item.name) : undefined;
@@ -376,5 +420,16 @@ module.exports = function (RED) {
376
420
  return configuredValue;
377
421
  }
378
422
 
423
+ RED.httpAdmin.get("/opcua-client-resource/style.css", function (req, res) {
424
+ const cssPath = path.join(__dirname, "view", "opcua-client.css");
425
+ res.sendFile(cssPath);
426
+ });
427
+
428
+ RED.httpAdmin.get("/opcua-client-resource/script.js", function (req, res) {
429
+ const jsPath = path.join(__dirname, "view", "opcua-client.js");
430
+ res.sendFile(jsPath);
431
+ });
432
+
433
+
379
434
  RED.nodes.registerType("opcua-client", OpcUaClientNode);
380
435
  };
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "72c8a9a049a63b1c",
3
+ "type": "opcua-client",
4
+ "z": "7a59646bfdbe5f29",
5
+ "name": "client browse",
6
+ "connection": "b09a1570957d3c44",
7
+ "mode": "browse",
8
+ "selectedItems": "[\n {\n \"name\": \"myServer1\",\n \"nodeID\": \"ns=2;s=myServer1\",\n \"nodeClass\": \"Object\",\n \"valueProperty\": \"payload\",\n \"valuePropertyType\": \"msg\"\n }\n]",
9
+ "samplingInterval": 250,
10
+ "publishingInterval": 250,
11
+ "x": 590,
12
+ "y": 1200,
13
+ "wires": [
14
+ [
15
+ "ac4d1f63af2598d4"
16
+ ]
17
+ ]
18
+ }
@@ -0,0 +1,411 @@
1
+
2
+ body.opcua-tree-modal-open {
3
+ overflow: hidden;
4
+ }
5
+
6
+ .opcua-tree-editor {
7
+ width: 100%;
8
+ min-height: 120px;
9
+ border: 1px solid var(--red-ui-form-input-border-color, #d9d9d9);
10
+ background: var(--red-ui-form-input-background, #fff);
11
+ border-radius: 4px;
12
+ overflow: auto;
13
+ }
14
+
15
+ .opcua-tree-row {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 5px;
19
+ min-height: 24px;
20
+ padding: 2px 6px;
21
+ font-size: 12px;
22
+ border-bottom: 1px solid transparent;
23
+ }
24
+
25
+ .opcua-tree-row:hover {
26
+ background: var(--red-ui-list-item-background-hover, #f3f7fd);
27
+ }
28
+
29
+ .opcua-tree-row.is-selected {
30
+ background: var(--red-ui-list-item-background-selected, #d9ecff);
31
+ color: var(--red-ui-list-item-color-selected, inherit);
32
+ }
33
+
34
+ .opcua-tree-indent {
35
+ flex: 0 0 auto;
36
+ width: 14px;
37
+ }
38
+
39
+ .opcua-tree-twisty {
40
+ width: 16px;
41
+ text-align: center;
42
+ color: #777;
43
+ flex: 0 0 16px;
44
+ cursor: pointer;
45
+ }
46
+
47
+ .opcua-tree-icon {
48
+ width: 14px;
49
+ text-align: center;
50
+ color: #777;
51
+ flex: 0 0 14px;
52
+ }
53
+
54
+ .opcua-tree-label {
55
+ flex: 1 1 auto;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .opcua-tree-type {
62
+ color: #777;
63
+ font-size: 11px;
64
+ flex: 0 0 auto;
65
+ }
66
+
67
+ .opcua-tree-title {
68
+ min-width: 100px;
69
+ font-weight: 600;
70
+ color: #444;
71
+ }
72
+
73
+ .opcua-client-nodeid-label {
74
+ margin-left: auto;
75
+ text-align: right;
76
+ word-break: break-all;
77
+ max-width: 320px;
78
+ color: #777;
79
+ font-size: 12px;
80
+ }
81
+
82
+ .opcua-tree-actions {
83
+ margin-left: auto;
84
+ display: flex;
85
+ gap: 6px;
86
+ align-items: center;
87
+ }
88
+
89
+ .opcua-tree-empty {
90
+ padding: 14px;
91
+ border: 1px dashed #d9d9d9;
92
+ border-radius: 4px;
93
+ background: #fafafa;
94
+ color: #777;
95
+ text-align: center;
96
+ }
97
+
98
+ .opcua-client-json {
99
+ width: 100%;
100
+ box-sizing: border-box;
101
+ }
102
+
103
+ .opcua-client-tag-box {
104
+ width: 100%;
105
+ min-height: 70px;
106
+ }
107
+
108
+ .opcua-client-tag-chip {
109
+ display: grid;
110
+ grid-template-columns: 18px 1fr auto;
111
+ grid-template-areas:
112
+ "icon name name"
113
+ "icon write right";
114
+ column-gap: 8px;
115
+ row-gap: 6px;
116
+ align-items: center;
117
+ border: 1px solid #d9d9d9;
118
+ border-radius: 4px;
119
+ background: #fafafa;
120
+ padding: 6px 8px;
121
+ margin-bottom: 6px;
122
+ }
123
+
124
+ .opcua-client-tag-chip .opcua-tree-icon {
125
+ grid-area: icon;
126
+ }
127
+
128
+ .opcua-client-tag-chip .opcua-tree-title {
129
+ grid-area: name;
130
+ min-width: 0;
131
+ }
132
+
133
+ .opcua-client-tag-right {
134
+ grid-area: right;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ }
139
+
140
+ .opcua-client-tag-right .opcua-client-nodeid-label {
141
+ margin: 0;
142
+ max-width: 320px;
143
+ }
144
+
145
+ .opcua-client-tag-right .opcua-tree-actions {
146
+ margin: 0;
147
+ }
148
+
149
+ .opcua-client-tag-chip-meta,
150
+ .opcua-client-description {
151
+ color: #777;
152
+ font-size: 12px;
153
+ }
154
+
155
+ .opcua-client-tag-write {
156
+ display: flex;
157
+ gap: 8px;
158
+ align-items: center;
159
+ width: 100%;
160
+ margin-top: 0;
161
+ margin-left: 0;
162
+ grid-area: write;
163
+ }
164
+
165
+ .opcua-client-tag-write input {
166
+ font-size: 12px;
167
+ }
168
+
169
+ .opcua-client-tag-write input {
170
+ flex: 1 1 auto;
171
+ min-width: 120px;
172
+ }
173
+
174
+ .opcua-client-tag-write .red-ui-typedInput-container {
175
+ flex: 1 1 auto;
176
+ min-width: 220px;
177
+ width: 100% !important;
178
+ }
179
+
180
+ .opcua-client-tag-write .red-ui-typedInput-container input.red-ui-typedInput-input {
181
+ width: calc(100% - 92px);
182
+ min-width: 120px;
183
+ }
184
+
185
+ .opcua-client-load-row {
186
+ display: flex;
187
+ gap: 8px;
188
+ align-items: center;
189
+ }
190
+
191
+ .opcua-client-load-row input {
192
+ flex: 1 1 auto;
193
+ }
194
+
195
+ .opcua-tree-search {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 8px;
199
+ width: 100%;
200
+ padding: 6px 10px;
201
+ border: 1px solid #d9d9d9;
202
+ border-radius: 6px;
203
+ background: #fafafa;
204
+ }
205
+
206
+ .opcua-tree-search input {
207
+ flex: 1 1 auto;
208
+ min-width: 120px;
209
+ border: 0;
210
+ outline: 0;
211
+ background: transparent;
212
+ box-shadow: none;
213
+ margin: 0;
214
+ }
215
+
216
+ .opcua-tree-modal {
217
+ position: fixed;
218
+ inset: 0;
219
+ z-index: 2000;
220
+ background: rgba(0, 0, 0, 0.45);
221
+ padding: 24px;
222
+ box-sizing: border-box;
223
+ }
224
+
225
+ .opcua-tree-modal__dialog {
226
+ display: flex;
227
+ flex-direction: column;
228
+ width: 100%;
229
+ height: 100%;
230
+ background: #f3f3f3;
231
+ border-radius: 8px;
232
+ overflow: hidden;
233
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
234
+ }
235
+
236
+ .opcua-tree-modal__header,
237
+ .opcua-tree-modal__toolbar {
238
+ display: flex;
239
+ align-items: center;
240
+ flex-wrap: wrap;
241
+ gap: 10px;
242
+ padding: 14px 18px;
243
+ background: #fff;
244
+ border-bottom: 1px solid #d9d9d9;
245
+ }
246
+
247
+ .opcua-tree-modal__title {
248
+ font-size: 18px;
249
+ font-weight: 600;
250
+ color: #333;
251
+ }
252
+
253
+ .opcua-tree-modal__header .editor-button-small {
254
+ margin-left: auto;
255
+ }
256
+
257
+ .opcua-tree-modal__body {
258
+ flex: 1 1 auto;
259
+ overflow: auto;
260
+ padding: 18px;
261
+ }
262
+
263
+ .opcua-tree-context-menu {
264
+ position: fixed;
265
+ z-index: 2200;
266
+ min-width: 170px;
267
+ background: #fff;
268
+ border: 1px solid #d9d9d9;
269
+ border-radius: 6px;
270
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
271
+ padding: 6px 0;
272
+ }
273
+
274
+ .opcua-tree-context-menu a {
275
+ display: block;
276
+ padding: 7px 12px;
277
+ color: #333;
278
+ text-decoration: none;
279
+ font-size: 12px;
280
+ }
281
+
282
+ .opcua-tree-context-menu a:hover {
283
+ background: #f3f7fd;
284
+ }
285
+
286
+ /* ── Method mode ── */
287
+
288
+ .opcua-client-method-chip {
289
+ border: 1px solid #d9d9d9;
290
+ border-radius: 4px;
291
+ background: #fafafa;
292
+ padding: 6px 8px;
293
+ margin-bottom: 6px;
294
+ }
295
+
296
+ .opcua-client-method-header {
297
+ display: grid;
298
+ grid-template-columns: 18px 1fr auto auto;
299
+ column-gap: 8px;
300
+ align-items: center;
301
+ margin-bottom: 2px;
302
+ }
303
+
304
+ .opcua-client-method-header .opcua-tree-icon {
305
+ grid-column: 1;
306
+ }
307
+
308
+ .opcua-client-method-title {
309
+ font-weight: 600;
310
+ font-size: 12px;
311
+ min-width: 0;
312
+ overflow: hidden;
313
+ text-overflow: ellipsis;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .opcua-client-method-header .opcua-client-nodeid-label {
318
+ font-size: 11px;
319
+ color: #777;
320
+ margin: 0;
321
+ overflow: hidden;
322
+ text-overflow: ellipsis;
323
+ white-space: nowrap;
324
+ max-width: 260px;
325
+ }
326
+
327
+ .opcua-client-method-section-label {
328
+ font-size: 11px;
329
+ font-weight: 600;
330
+ color: #555;
331
+ text-transform: uppercase;
332
+ letter-spacing: 0.04em;
333
+ margin: 6px 0 3px 26px;
334
+ }
335
+
336
+ .opcua-client-method-inp-chip {
337
+ display: grid;
338
+ grid-template-columns: 18px 1fr auto;
339
+ grid-template-areas:
340
+ "icon name name"
341
+ "icon write right";
342
+ column-gap: 8px;
343
+ row-gap: 4px;
344
+ align-items: center;
345
+ border: 1px solid #e8e8e8;
346
+ border-radius: 4px;
347
+ background: #fff;
348
+ padding: 4px 8px;
349
+ margin-bottom: 4px;
350
+ font-size: 12px;
351
+ }
352
+
353
+ .opcua-client-method-inp-chip .opcua-tree-icon {
354
+ grid-area: icon;
355
+ color: #aaa;
356
+ }
357
+
358
+ .opcua-client-method-inp-chip .opcua-tree-title {
359
+ grid-area: name;
360
+ min-width: 0;
361
+ font-weight: normal;
362
+ }
363
+
364
+ .opcua-client-method-inp-chip .opcua-client-tag-write {
365
+ grid-area: write;
366
+ }
367
+
368
+ .opcua-client-method-inp-chip .opcua-client-tag-right {
369
+ grid-area: right;
370
+ }
371
+
372
+ .opcua-client-method-out-chip {
373
+ display: grid;
374
+ grid-template-columns: 18px 1fr auto;
375
+ grid-template-areas: "icon name right";
376
+ column-gap: 8px;
377
+ align-items: center;
378
+ border: 1px solid #e8e8e8;
379
+ border-radius: 4px;
380
+ background: #f7f7f7;
381
+ padding: 4px 8px;
382
+ margin-bottom: 4px;
383
+ font-size: 12px;
384
+ color: #999;
385
+ }
386
+
387
+ .opcua-client-method-out-chip .opcua-tree-icon {
388
+ grid-area: icon;
389
+ color: #ccc;
390
+ }
391
+
392
+ .opcua-client-method-out-chip .opcua-tree-title {
393
+ grid-area: name;
394
+ min-width: 0;
395
+ font-weight: normal;
396
+ }
397
+
398
+ .opcua-client-method-out-chip .opcua-client-tag-right {
399
+ grid-area: right;
400
+ }
401
+
402
+ .opcua-method-section-row {
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 8px;
406
+ margin: 4px 0 2px 26px;
407
+ }
408
+
409
+ .opcua-method-section-row a {
410
+ flex-shrink: 0;
411
+ }