@vitormnm/node-red-simple-opcua 1.4.0 → 1.4.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.
@@ -20,18 +20,25 @@ 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
+
25
29
 
26
30
  module.exports = function (RED) {
27
31
  function OpcUaClientNode(config) {
28
32
  RED.nodes.createNode(this, config);
29
33
  const node = this;
30
34
 
35
+
36
+
31
37
  node.name = (config.name || "").trim();
32
38
  node.connection = RED.nodes.getNode(config.connection);
33
39
  node.mode = config.mode || "read";
34
40
  node.selectedItems = parseConfiguredItems(config.selectedItems);
41
+ node.methodItems = parseConfiguredMethodsItems(config.selectedItems);
35
42
  node.valueProperty = (config.valueProperty || "payload").trim();
36
43
  node.valuePropertyType = config.valuePropertyType || "msg";
37
44
  node.samplingInterval = Math.max(Number(config.samplingInterval) || 250, 50);
@@ -42,6 +49,7 @@ module.exports = function (RED) {
42
49
  const itemsResolver = createItemsResolver(RED);
43
50
  const readService = new OpcUaClientReadService();
44
51
  const writeService = new OpcUaClientWriteService();
52
+ const methodService = new OpcUaClientMethodService();
45
53
  const subscriptionService = new OpcUaClientSubscriptionService();
46
54
  const subscriptionIdService = new OpcUaClientSubscriptionIdService();
47
55
 
@@ -70,19 +78,24 @@ module.exports = function (RED) {
70
78
  }
71
79
 
72
80
  const session = await node.connection.getSession();
81
+
73
82
  let payload;
74
83
 
75
84
  if (node.mode === "read") {
85
+
76
86
  payload = await readService.execute(node, msg, session, itemsResolver);
77
87
  node.status({ fill: "green", shape: "dot", text: "read " + payload.length + " nodes" });
78
88
  } else if (node.mode === "write") {
89
+
79
90
  payload = await writeService.execute(node, msg, session, itemsResolver);
80
91
  node.status({ fill: "green", shape: "dot", text: "write " + payload.length + " nodes" });
81
92
  } else if (node.mode === "browse") {
82
93
  payload = await executeBrowse(node, msg, session);
83
94
  node.status({ fill: "green", shape: "dot", text: "browsed " + payload.length + " nodes" });
84
95
  } else if (node.mode === "method") {
85
- payload = await executeMethod(node, msg, session);
96
+ //payload = await executeMethod(node, msg, session);
97
+
98
+ payload = await methodService.execute(node, msg, session, itemsResolver);
86
99
  node.status({ fill: "green", shape: "dot", text: "called " + payload.length + " methods" });
87
100
  } else if (node.mode === "getSubscriptionId") {
88
101
  payload = await subscriptionIdService.execute(node);
@@ -121,63 +134,14 @@ module.exports = function (RED) {
121
134
  return payload;
122
135
  }
123
136
 
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
137
 
163
- return payload;
164
- }
165
138
 
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
139
 
177
140
  function createItemsResolver(REDRuntime) {
178
141
  return {
179
142
  ensureClientItems,
180
- ensureWriteItems
143
+ ensureWriteItems,
144
+ ensureMethodItems
181
145
  };
182
146
 
183
147
  function ensureClientItems(node, msg, contextName) {
@@ -206,13 +170,49 @@ module.exports = function (RED) {
206
170
  return node.selectedItems.map((item, index) => ({
207
171
  name: item.name,
208
172
  nodeID: item.nodeID,
209
- type: item.type,
173
+ type: item.dataType,
210
174
  value: resolveWriteValueForItem(node, msg, item, index, configuredValue, REDRuntime)
211
175
  }));
212
176
  }
213
177
 
214
178
  return ensureArrayPayload(msg, "OPC UA write");
215
179
  }
180
+
181
+ function ensureMethodItems(node, msg) {
182
+ const payload = msg ? msg.payload : undefined;
183
+
184
+ if (Array.isArray(payload) && payload.length > 0) {
185
+ return payload;
186
+ }
187
+
188
+ if (node.methodItems?.length > 0) {
189
+ return node.methodItems.map(item => ({
190
+ name: item.name,
191
+ nodeID: item.nodeID,
192
+ objectID: item.objectId,
193
+ inputs: item.inputs.map(input => {
194
+ const value = resolveConfiguredWriteValue(
195
+ {
196
+ ...node,
197
+ valueProperty: input.valueProperty,
198
+ valuePropertyType: input.valuePropertyType
199
+ },
200
+ msg,
201
+ REDRuntime
202
+ );
203
+
204
+ return {
205
+ name: input.name,
206
+ type: input.dataType,
207
+ value
208
+ };
209
+ }),
210
+ outputs: item.outputs || []
211
+ }));
212
+ }
213
+
214
+ return ensureArrayPayload(msg, "OPC UA method");
215
+ }
216
216
  }
217
217
 
218
218
  function normalizeBrowseRoots(node, payload) {
@@ -268,6 +268,25 @@ module.exports = function (RED) {
268
268
  return String(methodId).trim();
269
269
  }
270
270
 
271
+ function parseConfiguredMethodsItems(rawValue) {
272
+ try {
273
+ if (!rawValue || typeof rawValue !== "string") {
274
+ return [];
275
+ }
276
+
277
+
278
+ const parsed = JSON.parse(rawValue);
279
+ if (!Array.isArray(parsed)) {
280
+ return [];
281
+ }
282
+
283
+ return parsed
284
+
285
+ } catch (error) {
286
+ return [];
287
+ }
288
+ }
289
+
271
290
  function parseConfiguredItems(rawValue) {
272
291
  if (!rawValue || typeof rawValue !== "string") {
273
292
  return [];
@@ -344,6 +363,25 @@ module.exports = function (RED) {
344
363
  return REDRuntime.util.getMessageProperty(msg, property);
345
364
  }
346
365
 
366
+ function resolveConfiguredMethodValue(node, msg, REDRuntime) {
367
+ const property = node.valueProperty || "payload";
368
+ const type = node.valuePropertyType || "msg";
369
+
370
+ if (type === "msg") {
371
+ return REDRuntime.util.getMessageProperty(msg, property);
372
+ }
373
+
374
+ if (type === "flow") {
375
+ return node.context().flow.get(property);
376
+ }
377
+
378
+ if (type === "global") {
379
+ return node.context().global.get(property);
380
+ }
381
+
382
+ return REDRuntime.util.getMessageProperty(msg, property);
383
+ }
384
+
347
385
  function selectWriteValueForItem(configuredValue, item, index) {
348
386
  if (Array.isArray(configuredValue)) {
349
387
  const byName = item && item.name ? configuredValue.find((entry) => entry && entry.name === item.name) : undefined;
@@ -376,5 +414,16 @@ module.exports = function (RED) {
376
414
  return configuredValue;
377
415
  }
378
416
 
417
+ RED.httpAdmin.get("/opcua-client-resource/style.css", function (req, res) {
418
+ const cssPath = path.join(__dirname, "view", "opcua-client.css");
419
+ res.sendFile(cssPath);
420
+ });
421
+
422
+ RED.httpAdmin.get("/opcua-client-resource/script.js", function (req, res) {
423
+ const jsPath = path.join(__dirname, "view", "opcua-client.js");
424
+ res.sendFile(jsPath);
425
+ });
426
+
427
+
379
428
  RED.nodes.registerType("opcua-client", OpcUaClientNode);
380
429
  };
@@ -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
+ }