@vitormnm/node-red-simple-opcua 1.4.2 → 1.4.3
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 +5 -0
- package/client/icons/opcua.svg +132 -132
- package/client/lib/opcua-client-browser.js +330 -330
- package/client/lib/opcua-client-method-service.js +88 -88
- package/client/lib/opcua-client-read-service.js +15 -15
- package/client/lib/opcua-client-subscription-id-service.js +24 -24
- package/client/lib/opcua-client-subscription-service.js +170 -170
- package/client/lib/opcua-client-write-service.js +146 -146
- package/client/opcua-client-config.html +80 -80
- package/client/opcua-client.html +140 -140
- package/client/view/opcua-client.js +1140 -1140
- package/icons/opcua.svg +132 -132
- package/icons/opcua2.svg +132 -132
- package/package.json +42 -42
- package/resources/bmc-button.svg +22 -0
- package/server/icons/opcua.svg +132 -132
- package/server/lib/opcua-address-space-alarm.js +341 -341
- package/server/lib/opcua-address-space-builder.js +1484 -1484
- package/server/lib/opcua-config.js +546 -546
- package/server/lib/opcua-constants.js +109 -109
- package/server/lib/opcua-server-events-child.js +139 -139
- package/server/lib/opcua-server-runtime-child.js +819 -819
- package/server/lib/opcua-server-runtime.js +311 -311
- package/server/lib/opcua-server-status-child.js +187 -187
- package/server/lib/server-node-utils.js +16 -16
- package/server/opcua-server-io.html +346 -346
- package/server/opcua-server-io.js +496 -496
- package/server/opcua-server-registry.js +270 -270
- package/server/opcua-server.css +265 -265
- package/server/opcua-server.html +1643 -1643
|
@@ -1,820 +1,820 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
OPCUAServer,
|
|
7
|
-
UserTokenType,
|
|
8
|
-
buildApplicationUri
|
|
9
|
-
} = require("./opcua-constants");
|
|
10
|
-
|
|
11
|
-
const { OpcUaAddressSpaceBuilder } = require("./opcua-address-space-builder");
|
|
12
|
-
const { OpcUaServerRuntime } = require("./opcua-server-runtime");
|
|
13
|
-
const { OpcUaServerConfigParser } = require("./opcua-config");
|
|
14
|
-
const { resolveRegisteredServer } = require("./server-node-utils");
|
|
15
|
-
const { OpcUaServerStatusNode } = require("./opcua-server-status-child")
|
|
16
|
-
const { eventsServer } = require("./opcua-server-events-child")
|
|
17
|
-
const registry = require("../opcua-server-registry");
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Classe responsável por gerenciar TODO o ciclo de vida do servidor OPC UA
|
|
21
|
-
*/
|
|
22
|
-
class OpcUaServerProcess {
|
|
23
|
-
constructor() {
|
|
24
|
-
this.node = {};
|
|
25
|
-
this.runtime = null;
|
|
26
|
-
this.parser = new OpcUaServerConfigParser(this.node);
|
|
27
|
-
this.lifecyclePromise = null;
|
|
28
|
-
this.isRunning = false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Cria e inicia o servidor
|
|
33
|
-
*/
|
|
34
|
-
async create(settings, nodeId) {
|
|
35
|
-
if (this.isRunning) {
|
|
36
|
-
throw new Error("Server already running");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
this.node.name = settings.name;
|
|
40
|
-
this.node.serverName = settings.serverName;
|
|
41
|
-
this.node.server = null;
|
|
42
|
-
this.node.namespace = null;
|
|
43
|
-
|
|
44
|
-
this.runtime = new OpcUaServerRuntime({
|
|
45
|
-
node: this.node,
|
|
46
|
-
registry,
|
|
47
|
-
settings
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
this.node.runtime = this.runtime;
|
|
51
|
-
|
|
52
|
-
this.node.readValueByPath = (path) => {
|
|
53
|
-
return this.runtime.readValueByPath(path);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
this.node.readValueByNodeId = (nodeId) => {
|
|
57
|
-
return this.runtime.readValueByNodeId(nodeId);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
this.node.readValue = (identifierType, identifier) => {
|
|
61
|
-
return this.runtime.readValue(identifierType, identifier);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
this.node.writeEventByPath = (valuePayload) => {
|
|
65
|
-
return this.runtime.writeEventByPath(valuePayload);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
this.node.writeValueByPath = (path, value) => {
|
|
69
|
-
return this.runtime.writeValueByPath(path, value);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
this.node.writeValueByNodeId = (nodeId, value) => {
|
|
73
|
-
return this.runtime.writeValueByNodeId(nodeId, value);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
this.node.writeValue = (identifierType, identifier, value) => {
|
|
77
|
-
return this.runtime.writeValue(identifierType, identifier, value);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
this.lifecyclePromise = this.runtime.start();
|
|
82
|
-
await this.lifecyclePromise;
|
|
83
|
-
|
|
84
|
-
this.node.server = this.runtime.server;
|
|
85
|
-
this.node.namespace = this.runtime.namespace;
|
|
86
|
-
|
|
87
|
-
this.isRunning = true;
|
|
88
|
-
|
|
89
|
-
const endpointUrl = this.runtime.getEndpointUrl();
|
|
90
|
-
|
|
91
|
-
process.send({
|
|
92
|
-
type: "status",
|
|
93
|
-
data: {
|
|
94
|
-
fill: "green",
|
|
95
|
-
shape: "dot",
|
|
96
|
-
text: endpointUrl
|
|
97
|
-
? "running " + endpointUrl
|
|
98
|
-
: "running"
|
|
99
|
-
},
|
|
100
|
-
nodeId: nodeId
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} catch (error) {
|
|
105
|
-
this.isRunning = false;
|
|
106
|
-
console.error("Failed to start OPC UA server:", error);
|
|
107
|
-
|
|
108
|
-
process.send({
|
|
109
|
-
type: "error",
|
|
110
|
-
data: "Failed to start OPC UA server: " + error.message
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
process.send({
|
|
114
|
-
type: "status",
|
|
115
|
-
data: {
|
|
116
|
-
fill: "red",
|
|
117
|
-
shape: "dot",
|
|
118
|
-
text: "Failed to start OPC UA server"
|
|
119
|
-
},
|
|
120
|
-
nodeId: nodeId
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Para o servidor
|
|
127
|
-
*/
|
|
128
|
-
async stop(nodeId) {
|
|
129
|
-
try {
|
|
130
|
-
if (!this.runtime) return;
|
|
131
|
-
|
|
132
|
-
if (this.lifecyclePromise) {
|
|
133
|
-
await this.lifecyclePromise.catch(() => { });
|
|
134
|
-
this.lifecyclePromise = null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
await this.runtime.stop();
|
|
138
|
-
|
|
139
|
-
this.node.server = null;
|
|
140
|
-
this.node.namespace = null;
|
|
141
|
-
this.isRunning = false;
|
|
142
|
-
|
|
143
|
-
process.send({
|
|
144
|
-
type: "status",
|
|
145
|
-
data: {
|
|
146
|
-
fill: "red",
|
|
147
|
-
shape: "ring",
|
|
148
|
-
text: "stopped"
|
|
149
|
-
},
|
|
150
|
-
nodeId: nodeId
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch (error) {
|
|
155
|
-
console.error("Failed to stop OPC UA server:", error);
|
|
156
|
-
|
|
157
|
-
process.send({
|
|
158
|
-
type: "error",
|
|
159
|
-
data: "Failed to stop OPC UA server: " + error.message
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
readFromPayload(msg, nodeId) {
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
|
|
168
|
-
const server = this.node.runtime
|
|
169
|
-
const payload = msg ? msg.payload : undefined;
|
|
170
|
-
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
171
|
-
const identifierType = this.resolveIdentifierType(target);
|
|
172
|
-
|
|
173
|
-
let result = {}
|
|
174
|
-
|
|
175
|
-
if (Array.isArray(payload)) {
|
|
176
|
-
if (!payload.length) {
|
|
177
|
-
throw new Error("msg.payload array does not contain any items");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
result = {
|
|
181
|
-
payload: payload.map((item) => this.readPayloadItem(identifierType, item)),
|
|
182
|
-
identifiers: payload.map((item) => this.resolvePayloadItemIdentifier(item))
|
|
183
|
-
};
|
|
184
|
-
} else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
185
|
-
const identifiers = Object.keys(payload);
|
|
186
|
-
if (!identifiers.length) {
|
|
187
|
-
throw new Error("msg.payload object does not contain any " + this.getIdentifierLabel(identifierType));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const resultPayload = {};
|
|
191
|
-
identifiers.forEach((identifier) => {
|
|
192
|
-
try {
|
|
193
|
-
resultPayload[identifier] = server.readValue(identifierType, identifier);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
resultPayload[identifier] = undefined;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
result = {
|
|
200
|
-
payload: resultPayload,
|
|
201
|
-
identifiers: identifiers
|
|
202
|
-
};
|
|
203
|
-
} else {
|
|
204
|
-
const identifier = this.resolveIdentifier(target);
|
|
205
|
-
result = {
|
|
206
|
-
payload: server.readValue(identifierType, identifier),
|
|
207
|
-
identifiers: [identifier]
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
msg.opcua = msg.opcua || {};
|
|
214
|
-
msg.payload = result.payload;
|
|
215
|
-
this.assignReadMetadata(msg, identifierType, result.identifiers);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (result.identifiers.length === 1) {
|
|
220
|
-
msg.topic = result.identifiers[0];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
process.send({
|
|
226
|
-
type: "send",
|
|
227
|
-
data: msg,
|
|
228
|
-
nodeId: nodeId
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
process.send({
|
|
232
|
-
type: "status",
|
|
233
|
-
data: {
|
|
234
|
-
fill: "green",
|
|
235
|
-
shape: "dot",
|
|
236
|
-
text: result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0]
|
|
237
|
-
},
|
|
238
|
-
nodeId: nodeId
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
} catch (error) {
|
|
242
|
-
|
|
243
|
-
process.send({
|
|
244
|
-
type: "error",
|
|
245
|
-
data: { fill: "red", shape: "ring", text: "failed read" },
|
|
246
|
-
error: error.message,
|
|
247
|
-
nodeId: nodeId
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
writeEventFromPayload(msg, nodeId) {
|
|
254
|
-
try {
|
|
255
|
-
var writtenPaths = null
|
|
256
|
-
const payload = msg ? msg.payload : undefined;
|
|
257
|
-
|
|
258
|
-
writtenPaths = payload
|
|
259
|
-
|
|
260
|
-
if (payload && Array.isArray(payload)) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
payload.forEach((valuePayload) => {
|
|
265
|
-
this.node.writeEventByPath(valuePayload);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
} else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.node.writeEventByPath(payload);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
process.send({
|
|
279
|
-
type: "send",
|
|
280
|
-
data: msg,
|
|
281
|
-
nodeId: nodeId
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
process.send({
|
|
285
|
-
type: "status",
|
|
286
|
-
data: {
|
|
287
|
-
fill: "green",
|
|
288
|
-
shape: "dot",
|
|
289
|
-
text: writtenPaths.length > 1 ? "write " + writtenPaths.length + " events" : "Event " + writtenPaths.nodePath
|
|
290
|
-
},
|
|
291
|
-
nodeId: nodeId
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} catch (error) {
|
|
296
|
-
|
|
297
|
-
process.send({
|
|
298
|
-
type: "error",
|
|
299
|
-
data: { fill: "red", shape: "ring", text: "failed write" },
|
|
300
|
-
error: error.message,
|
|
301
|
-
nodeId: nodeId
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//return [path];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
writeFromPayload(msg, nodeId) {
|
|
312
|
-
try {
|
|
313
|
-
let writtenPaths = null;
|
|
314
|
-
let payload = msg ? msg.payload : undefined;
|
|
315
|
-
|
|
316
|
-
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
317
|
-
const identifierType = this.resolveIdentifierType(target);
|
|
318
|
-
|
|
319
|
-
// Buffer serializado pelo IPC
|
|
320
|
-
if (
|
|
321
|
-
payload &&
|
|
322
|
-
typeof payload === "object" &&
|
|
323
|
-
payload.type === "Buffer" &&
|
|
324
|
-
Array.isArray(payload.data)
|
|
325
|
-
) {
|
|
326
|
-
payload = Buffer.from(payload.data);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const dataType =
|
|
330
|
-
target.dataType ||
|
|
331
|
-
target.type ||
|
|
332
|
-
target.builtInType ||
|
|
333
|
-
"";
|
|
334
|
-
|
|
335
|
-
const isByteString =
|
|
336
|
-
typeof dataType === "string" &&
|
|
337
|
-
dataType.toLowerCase() === "bytestring";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// Buffer ou Uint8Array
|
|
342
|
-
if (Buffer.isBuffer(payload) || payload instanceof Uint8Array) {
|
|
343
|
-
|
|
344
|
-
const identifier = this.resolveIdentifier(target);
|
|
345
|
-
|
|
346
|
-
this.node.writeValue(
|
|
347
|
-
identifierType,
|
|
348
|
-
identifier,
|
|
349
|
-
Buffer.isBuffer(payload)
|
|
350
|
-
? payload
|
|
351
|
-
: Buffer.from(payload)
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
writtenPaths = [identifier];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Array de números
|
|
358
|
-
else if (
|
|
359
|
-
Array.isArray(payload) &&
|
|
360
|
-
payload.every(item => typeof item === "number")
|
|
361
|
-
) {
|
|
362
|
-
|
|
363
|
-
const identifier = this.resolveIdentifier(target);
|
|
364
|
-
|
|
365
|
-
this.node.writeValue(
|
|
366
|
-
identifierType,
|
|
367
|
-
identifier,
|
|
368
|
-
isByteString
|
|
369
|
-
? Buffer.from(payload)
|
|
370
|
-
: payload
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
writtenPaths = [identifier];
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Array de objetos
|
|
377
|
-
else if (Array.isArray(payload)) {
|
|
378
|
-
|
|
379
|
-
if (!payload.length) {
|
|
380
|
-
throw new Error("msg.payload array does not contain any items");
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
payload.forEach(item => {
|
|
384
|
-
this.writePayloadItem(identifierType, item);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
writtenPaths = payload.map(item =>
|
|
388
|
-
this.resolvePayloadItemIdentifier(item)
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Objeto { path: value }
|
|
393
|
-
else if (
|
|
394
|
-
payload &&
|
|
395
|
-
typeof payload === "object" &&
|
|
396
|
-
!Array.isArray(payload)
|
|
397
|
-
) {
|
|
398
|
-
|
|
399
|
-
const identifiers = Object.keys(payload);
|
|
400
|
-
|
|
401
|
-
if (!identifiers.length) {
|
|
402
|
-
throw new Error(
|
|
403
|
-
"msg.payload object does not contain any " +
|
|
404
|
-
this.getIdentifierLabel(identifierType)
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
identifiers.forEach(identifier => {
|
|
409
|
-
this.node.writeValue(
|
|
410
|
-
identifierType,
|
|
411
|
-
identifier,
|
|
412
|
-
payload[identifier]
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
writtenPaths = identifiers;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Valor simples
|
|
420
|
-
else {
|
|
421
|
-
|
|
422
|
-
const identifier = this.resolveIdentifier(target);
|
|
423
|
-
|
|
424
|
-
this.node.writeValue(
|
|
425
|
-
identifierType,
|
|
426
|
-
identifier,
|
|
427
|
-
payload
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
writtenPaths = [identifier];
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
msg.opcua = msg.opcua || {};
|
|
434
|
-
|
|
435
|
-
this.assignWriteMetadata(
|
|
436
|
-
msg,
|
|
437
|
-
identifierType,
|
|
438
|
-
writtenPaths
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (writtenPaths.length === 1) {
|
|
443
|
-
msg.topic = writtenPaths[0];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
process.send({
|
|
447
|
-
type: "send",
|
|
448
|
-
data: msg,
|
|
449
|
-
nodeId
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
process.send({
|
|
453
|
-
type: "status",
|
|
454
|
-
data: {
|
|
455
|
-
fill: "green",
|
|
456
|
-
shape: "dot",
|
|
457
|
-
text:
|
|
458
|
-
writtenPaths.length > 1
|
|
459
|
-
? `write ${writtenPaths.length} tags`
|
|
460
|
-
: `write ${writtenPaths[0]}`
|
|
461
|
-
},
|
|
462
|
-
nodeId
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
} catch (error) {
|
|
466
|
-
|
|
467
|
-
process.send({
|
|
468
|
-
type: "error",
|
|
469
|
-
data: {
|
|
470
|
-
fill: "red",
|
|
471
|
-
shape: "ring",
|
|
472
|
-
text: "failed write"
|
|
473
|
-
},
|
|
474
|
-
error: error.message,
|
|
475
|
-
nodeId
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
resolveIdentifierType(target) {
|
|
481
|
-
return target && target.identifierType === "nodeId" ? "nodeId" : "path";
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
resolveIdentifier(target) {
|
|
485
|
-
const identifierType = this.resolveIdentifierType(target);
|
|
486
|
-
if (identifierType === "nodeId") {
|
|
487
|
-
const nodeId = String(target.tagNodeId || "").trim();
|
|
488
|
-
if (!nodeId) {
|
|
489
|
-
throw new Error("No tag nodeId configured");
|
|
490
|
-
}
|
|
491
|
-
return nodeId;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const path = String(target.tagPath || "").trim();
|
|
495
|
-
if (!path) {
|
|
496
|
-
throw new Error("No tag path configured");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return path;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
getIdentifierLabel(identifierType) {
|
|
503
|
-
return identifierType === "nodeId" ? "nodeIds" : "tag paths";
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
resolvePayloadItemIdentifier(item) {
|
|
507
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
508
|
-
throw new Error("Each payload item must be an object");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const identifier = String(item.path || "").trim();
|
|
512
|
-
if (!identifier) {
|
|
513
|
-
throw new Error("Each payload item must contain a path");
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return identifier;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
readPayloadItem(identifierType, item) {
|
|
520
|
-
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
521
|
-
return {
|
|
522
|
-
name: item.name,
|
|
523
|
-
path: identifier,
|
|
524
|
-
value: this.node.readValue(identifierType, identifier)
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
writePayloadItem(identifierType, item) {
|
|
529
|
-
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
530
|
-
this.node.writeValue(identifierType, identifier, item.value);
|
|
531
|
-
return {
|
|
532
|
-
name: item.name,
|
|
533
|
-
path: identifier,
|
|
534
|
-
value: item.value
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
assignReadMetadata(msg, identifierType, identifiers) {
|
|
539
|
-
msg.opcua.identifierType = identifierType;
|
|
540
|
-
if (identifierType === "nodeId") {
|
|
541
|
-
msg.opcua.readNodeIds = identifiers;
|
|
542
|
-
delete msg.opcua.readPaths;
|
|
543
|
-
if (identifiers.length === 1) {
|
|
544
|
-
msg.opcua.tagNodeId = identifiers[0];
|
|
545
|
-
delete msg.opcua.tagPath;
|
|
546
|
-
}
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
msg.opcua.readPaths = identifiers;
|
|
551
|
-
delete msg.opcua.readNodeIds;
|
|
552
|
-
if (identifiers.length === 1) {
|
|
553
|
-
msg.opcua.tagPath = identifiers[0];
|
|
554
|
-
delete msg.opcua.tagNodeId;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
assignWriteMetadata(msg, identifierType, identifiers) {
|
|
559
|
-
msg.opcua.identifierType = identifierType;
|
|
560
|
-
if (identifierType === "nodeId") {
|
|
561
|
-
msg.opcua.writtenNodeIds = identifiers;
|
|
562
|
-
delete msg.opcua.writtenPaths;
|
|
563
|
-
if (identifiers.length === 1) {
|
|
564
|
-
msg.opcua.tagNodeId = identifiers[0];
|
|
565
|
-
delete msg.opcua.tagPath;
|
|
566
|
-
}
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
msg.opcua.writtenPaths = identifiers;
|
|
571
|
-
delete msg.opcua.writtenNodeIds;
|
|
572
|
-
if (identifiers.length === 1) {
|
|
573
|
-
msg.opcua.tagPath = identifiers[0];
|
|
574
|
-
delete msg.opcua.tagNodeId;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Atualiza a árvore do servidor
|
|
580
|
-
*/
|
|
581
|
-
async update(payload, nodeId) {
|
|
582
|
-
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
process.send({
|
|
586
|
-
type: "status",
|
|
587
|
-
data: {
|
|
588
|
-
fill: "yellow",
|
|
589
|
-
shape: "dot",
|
|
590
|
-
text: "updating items"
|
|
591
|
-
},
|
|
592
|
-
nodeId: nodeId
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
await this.ensureReady();
|
|
596
|
-
|
|
597
|
-
const nextTree = this.parser.normalizeTreeConfig(payload);
|
|
598
|
-
|
|
599
|
-
await this.runtime.updateTree(nextTree);
|
|
600
|
-
|
|
601
|
-
const endpointUrl = await this.runtime.getEndpointUrl();
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
process.send({
|
|
605
|
-
type: "status",
|
|
606
|
-
data: {
|
|
607
|
-
fill: "green",
|
|
608
|
-
shape: "dot",
|
|
609
|
-
text: endpointUrl
|
|
610
|
-
? "running " + endpointUrl
|
|
611
|
-
: "running"
|
|
612
|
-
},
|
|
613
|
-
nodeId: nodeId
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
} catch (error) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
process.send({
|
|
621
|
-
type: "errorUpdateServer",
|
|
622
|
-
data: error.message,
|
|
623
|
-
nodeId: nodeId
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
process.send({
|
|
627
|
-
type: "status",
|
|
628
|
-
data: {
|
|
629
|
-
fill: "red",
|
|
630
|
-
shape: "dot",
|
|
631
|
-
text: "error update"
|
|
632
|
-
},
|
|
633
|
-
nodeId: nodeId
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* metodos opc ua
|
|
641
|
-
*/
|
|
642
|
-
registerMethodInput(methodName, nodeId) {
|
|
643
|
-
|
|
644
|
-
if (methodName) {
|
|
645
|
-
|
|
646
|
-
registry.registerMethodHandler(methodName, nodeId);
|
|
647
|
-
} else {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
handleMethodOutput(msg, nodeId) {
|
|
657
|
-
|
|
658
|
-
resolveRegisteredServer(this.node, msg, registry);
|
|
659
|
-
|
|
660
|
-
if (!msg._callId) {
|
|
661
|
-
throw new Error("Missing _callId for OPC UA method response");
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
registry.resolveMethodCall(msg._callId, msg.payload);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
process.send({
|
|
668
|
-
type: "status",
|
|
669
|
-
data: {
|
|
670
|
-
fill: "green",
|
|
671
|
-
shape: "dot",
|
|
672
|
-
text: "response sent"
|
|
673
|
-
},
|
|
674
|
-
nodeId: nodeId
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Garante que o servidor está pronto
|
|
682
|
-
*/
|
|
683
|
-
async ensureReady() {
|
|
684
|
-
if (!this.runtime) {
|
|
685
|
-
throw new Error("Server not created");
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (this.lifecyclePromise) {
|
|
689
|
-
await this.lifecyclePromise;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
this.runtime.ensureReady();
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
readActiveAlarms(msg, nodeId) {
|
|
697
|
-
try {
|
|
698
|
-
var result = registry.getActiveAlarms(this.node)
|
|
699
|
-
const msg2 = {
|
|
700
|
-
payload: result
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
process.send({
|
|
704
|
-
type: "send",
|
|
705
|
-
data: msg2,
|
|
706
|
-
nodeId: nodeId
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
process.send({
|
|
710
|
-
type: "status",
|
|
711
|
-
data: {
|
|
712
|
-
fill: "green",
|
|
713
|
-
shape: "dot",
|
|
714
|
-
text: result.paths.length > 1 ? "read " + result.paths.length + " tags" : "read " + result.paths[0]
|
|
715
|
-
},
|
|
716
|
-
nodeId: nodeId
|
|
717
|
-
});
|
|
718
|
-
} catch {
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Instância única do processo
|
|
727
|
-
*/
|
|
728
|
-
const serverProcess = new OpcUaServerProcess();
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Comunicação com processo pai (Node-RED)
|
|
732
|
-
*/
|
|
733
|
-
process.on("message", async (msg) => {
|
|
734
|
-
try {
|
|
735
|
-
switch (msg.type) {
|
|
736
|
-
case "createServer":
|
|
737
|
-
await serverProcess.create(msg.settings, msg.nodeId);
|
|
738
|
-
break;
|
|
739
|
-
|
|
740
|
-
case "stopServer":
|
|
741
|
-
|
|
742
|
-
await serverProcess.stop(msg.nodeId);
|
|
743
|
-
break;
|
|
744
|
-
|
|
745
|
-
case "updateServer":
|
|
746
|
-
await serverProcess.update(msg.msg.payload, msg.nodeId);
|
|
747
|
-
break;
|
|
748
|
-
|
|
749
|
-
case "writeTagServer":
|
|
750
|
-
|
|
751
|
-
serverProcess.writeFromPayload(msg.msg, msg.nodeId)
|
|
752
|
-
break;
|
|
753
|
-
|
|
754
|
-
case "writeEventServer":
|
|
755
|
-
|
|
756
|
-
serverProcess.writeEventFromPayload(msg.msg, msg.nodeId)
|
|
757
|
-
break;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
case "readTagServer":
|
|
762
|
-
|
|
763
|
-
serverProcess.readFromPayload(msg.msg, msg.nodeId)
|
|
764
|
-
break;
|
|
765
|
-
|
|
766
|
-
case "registerMethodInput":
|
|
767
|
-
serverProcess.registerMethodInput(msg.node.methodName, msg.nodeId)
|
|
768
|
-
break;
|
|
769
|
-
|
|
770
|
-
case "handleMethodOutput":
|
|
771
|
-
serverProcess.handleMethodOutput(msg.msg, msg.nodeId)
|
|
772
|
-
break;
|
|
773
|
-
|
|
774
|
-
case "buildServerSnapshot":
|
|
775
|
-
OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId)
|
|
776
|
-
break
|
|
777
|
-
case "eventsServer":
|
|
778
|
-
eventsServer(serverProcess.node, msg.node, msg.nodeId)
|
|
779
|
-
break;
|
|
780
|
-
case "readActiveAlarms":
|
|
781
|
-
serverProcess.readActiveAlarms(msg.msg, msg.nodeId)
|
|
782
|
-
break;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
default:
|
|
787
|
-
console.warn("Unknown message type2:", msg.type);
|
|
788
|
-
}
|
|
789
|
-
} catch (error) {
|
|
790
|
-
console.error("Process message error:", error);
|
|
791
|
-
|
|
792
|
-
process.send({
|
|
793
|
-
type: "error",
|
|
794
|
-
data: error.message
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Segurança: captura erros não tratados
|
|
801
|
-
*/
|
|
802
|
-
process.on("uncaughtException", (err) => {
|
|
803
|
-
console.error("Uncaught Exception:", err);
|
|
804
|
-
|
|
805
|
-
process.send({
|
|
806
|
-
type: "error",
|
|
807
|
-
data: "Uncaught Exception: " + err.message,
|
|
808
|
-
nodeId: nodeId
|
|
809
|
-
});
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
process.on("unhandledRejection", (reason) => {
|
|
813
|
-
console.error("Unhandled Rejection:", reason);
|
|
814
|
-
|
|
815
|
-
process.send({
|
|
816
|
-
type: "error",
|
|
817
|
-
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
818
|
-
nodeId: nodeId
|
|
819
|
-
});
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
OPCUAServer,
|
|
7
|
+
UserTokenType,
|
|
8
|
+
buildApplicationUri
|
|
9
|
+
} = require("./opcua-constants");
|
|
10
|
+
|
|
11
|
+
const { OpcUaAddressSpaceBuilder } = require("./opcua-address-space-builder");
|
|
12
|
+
const { OpcUaServerRuntime } = require("./opcua-server-runtime");
|
|
13
|
+
const { OpcUaServerConfigParser } = require("./opcua-config");
|
|
14
|
+
const { resolveRegisteredServer } = require("./server-node-utils");
|
|
15
|
+
const { OpcUaServerStatusNode } = require("./opcua-server-status-child")
|
|
16
|
+
const { eventsServer } = require("./opcua-server-events-child")
|
|
17
|
+
const registry = require("../opcua-server-registry");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Classe responsável por gerenciar TODO o ciclo de vida do servidor OPC UA
|
|
21
|
+
*/
|
|
22
|
+
class OpcUaServerProcess {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.node = {};
|
|
25
|
+
this.runtime = null;
|
|
26
|
+
this.parser = new OpcUaServerConfigParser(this.node);
|
|
27
|
+
this.lifecyclePromise = null;
|
|
28
|
+
this.isRunning = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cria e inicia o servidor
|
|
33
|
+
*/
|
|
34
|
+
async create(settings, nodeId) {
|
|
35
|
+
if (this.isRunning) {
|
|
36
|
+
throw new Error("Server already running");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.node.name = settings.name;
|
|
40
|
+
this.node.serverName = settings.serverName;
|
|
41
|
+
this.node.server = null;
|
|
42
|
+
this.node.namespace = null;
|
|
43
|
+
|
|
44
|
+
this.runtime = new OpcUaServerRuntime({
|
|
45
|
+
node: this.node,
|
|
46
|
+
registry,
|
|
47
|
+
settings
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.node.runtime = this.runtime;
|
|
51
|
+
|
|
52
|
+
this.node.readValueByPath = (path) => {
|
|
53
|
+
return this.runtime.readValueByPath(path);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.node.readValueByNodeId = (nodeId) => {
|
|
57
|
+
return this.runtime.readValueByNodeId(nodeId);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.node.readValue = (identifierType, identifier) => {
|
|
61
|
+
return this.runtime.readValue(identifierType, identifier);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.node.writeEventByPath = (valuePayload) => {
|
|
65
|
+
return this.runtime.writeEventByPath(valuePayload);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.node.writeValueByPath = (path, value) => {
|
|
69
|
+
return this.runtime.writeValueByPath(path, value);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.node.writeValueByNodeId = (nodeId, value) => {
|
|
73
|
+
return this.runtime.writeValueByNodeId(nodeId, value);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.node.writeValue = (identifierType, identifier, value) => {
|
|
77
|
+
return this.runtime.writeValue(identifierType, identifier, value);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
this.lifecyclePromise = this.runtime.start();
|
|
82
|
+
await this.lifecyclePromise;
|
|
83
|
+
|
|
84
|
+
this.node.server = this.runtime.server;
|
|
85
|
+
this.node.namespace = this.runtime.namespace;
|
|
86
|
+
|
|
87
|
+
this.isRunning = true;
|
|
88
|
+
|
|
89
|
+
const endpointUrl = this.runtime.getEndpointUrl();
|
|
90
|
+
|
|
91
|
+
process.send({
|
|
92
|
+
type: "status",
|
|
93
|
+
data: {
|
|
94
|
+
fill: "green",
|
|
95
|
+
shape: "dot",
|
|
96
|
+
text: endpointUrl
|
|
97
|
+
? "running " + endpointUrl
|
|
98
|
+
: "running"
|
|
99
|
+
},
|
|
100
|
+
nodeId: nodeId
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.isRunning = false;
|
|
106
|
+
console.error("Failed to start OPC UA server:", error);
|
|
107
|
+
|
|
108
|
+
process.send({
|
|
109
|
+
type: "error",
|
|
110
|
+
data: "Failed to start OPC UA server: " + error.message
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
process.send({
|
|
114
|
+
type: "status",
|
|
115
|
+
data: {
|
|
116
|
+
fill: "red",
|
|
117
|
+
shape: "dot",
|
|
118
|
+
text: "Failed to start OPC UA server"
|
|
119
|
+
},
|
|
120
|
+
nodeId: nodeId
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Para o servidor
|
|
127
|
+
*/
|
|
128
|
+
async stop(nodeId) {
|
|
129
|
+
try {
|
|
130
|
+
if (!this.runtime) return;
|
|
131
|
+
|
|
132
|
+
if (this.lifecyclePromise) {
|
|
133
|
+
await this.lifecyclePromise.catch(() => { });
|
|
134
|
+
this.lifecyclePromise = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await this.runtime.stop();
|
|
138
|
+
|
|
139
|
+
this.node.server = null;
|
|
140
|
+
this.node.namespace = null;
|
|
141
|
+
this.isRunning = false;
|
|
142
|
+
|
|
143
|
+
process.send({
|
|
144
|
+
type: "status",
|
|
145
|
+
data: {
|
|
146
|
+
fill: "red",
|
|
147
|
+
shape: "ring",
|
|
148
|
+
text: "stopped"
|
|
149
|
+
},
|
|
150
|
+
nodeId: nodeId
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("Failed to stop OPC UA server:", error);
|
|
156
|
+
|
|
157
|
+
process.send({
|
|
158
|
+
type: "error",
|
|
159
|
+
data: "Failed to stop OPC UA server: " + error.message
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
readFromPayload(msg, nodeId) {
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
|
|
168
|
+
const server = this.node.runtime
|
|
169
|
+
const payload = msg ? msg.payload : undefined;
|
|
170
|
+
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
171
|
+
const identifierType = this.resolveIdentifierType(target);
|
|
172
|
+
|
|
173
|
+
let result = {}
|
|
174
|
+
|
|
175
|
+
if (Array.isArray(payload)) {
|
|
176
|
+
if (!payload.length) {
|
|
177
|
+
throw new Error("msg.payload array does not contain any items");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
result = {
|
|
181
|
+
payload: payload.map((item) => this.readPayloadItem(identifierType, item)),
|
|
182
|
+
identifiers: payload.map((item) => this.resolvePayloadItemIdentifier(item))
|
|
183
|
+
};
|
|
184
|
+
} else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
185
|
+
const identifiers = Object.keys(payload);
|
|
186
|
+
if (!identifiers.length) {
|
|
187
|
+
throw new Error("msg.payload object does not contain any " + this.getIdentifierLabel(identifierType));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resultPayload = {};
|
|
191
|
+
identifiers.forEach((identifier) => {
|
|
192
|
+
try {
|
|
193
|
+
resultPayload[identifier] = server.readValue(identifierType, identifier);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
resultPayload[identifier] = undefined;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
result = {
|
|
200
|
+
payload: resultPayload,
|
|
201
|
+
identifiers: identifiers
|
|
202
|
+
};
|
|
203
|
+
} else {
|
|
204
|
+
const identifier = this.resolveIdentifier(target);
|
|
205
|
+
result = {
|
|
206
|
+
payload: server.readValue(identifierType, identifier),
|
|
207
|
+
identifiers: [identifier]
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
msg.opcua = msg.opcua || {};
|
|
214
|
+
msg.payload = result.payload;
|
|
215
|
+
this.assignReadMetadata(msg, identifierType, result.identifiers);
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if (result.identifiers.length === 1) {
|
|
220
|
+
msg.topic = result.identifiers[0];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
process.send({
|
|
226
|
+
type: "send",
|
|
227
|
+
data: msg,
|
|
228
|
+
nodeId: nodeId
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
process.send({
|
|
232
|
+
type: "status",
|
|
233
|
+
data: {
|
|
234
|
+
fill: "green",
|
|
235
|
+
shape: "dot",
|
|
236
|
+
text: result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0]
|
|
237
|
+
},
|
|
238
|
+
nodeId: nodeId
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
|
|
243
|
+
process.send({
|
|
244
|
+
type: "error",
|
|
245
|
+
data: { fill: "red", shape: "ring", text: "failed read" },
|
|
246
|
+
error: error.message,
|
|
247
|
+
nodeId: nodeId
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
writeEventFromPayload(msg, nodeId) {
|
|
254
|
+
try {
|
|
255
|
+
var writtenPaths = null
|
|
256
|
+
const payload = msg ? msg.payload : undefined;
|
|
257
|
+
|
|
258
|
+
writtenPaths = payload
|
|
259
|
+
|
|
260
|
+
if (payload && Array.isArray(payload)) {
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
payload.forEach((valuePayload) => {
|
|
265
|
+
this.node.writeEventByPath(valuePayload);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
} else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
this.node.writeEventByPath(payload);
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
process.send({
|
|
279
|
+
type: "send",
|
|
280
|
+
data: msg,
|
|
281
|
+
nodeId: nodeId
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
process.send({
|
|
285
|
+
type: "status",
|
|
286
|
+
data: {
|
|
287
|
+
fill: "green",
|
|
288
|
+
shape: "dot",
|
|
289
|
+
text: writtenPaths.length > 1 ? "write " + writtenPaths.length + " events" : "Event " + writtenPaths.nodePath
|
|
290
|
+
},
|
|
291
|
+
nodeId: nodeId
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
} catch (error) {
|
|
296
|
+
|
|
297
|
+
process.send({
|
|
298
|
+
type: "error",
|
|
299
|
+
data: { fill: "red", shape: "ring", text: "failed write" },
|
|
300
|
+
error: error.message,
|
|
301
|
+
nodeId: nodeId
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
//return [path];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
writeFromPayload(msg, nodeId) {
|
|
312
|
+
try {
|
|
313
|
+
let writtenPaths = null;
|
|
314
|
+
let payload = msg ? msg.payload : undefined;
|
|
315
|
+
|
|
316
|
+
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
317
|
+
const identifierType = this.resolveIdentifierType(target);
|
|
318
|
+
|
|
319
|
+
// Buffer serializado pelo IPC
|
|
320
|
+
if (
|
|
321
|
+
payload &&
|
|
322
|
+
typeof payload === "object" &&
|
|
323
|
+
payload.type === "Buffer" &&
|
|
324
|
+
Array.isArray(payload.data)
|
|
325
|
+
) {
|
|
326
|
+
payload = Buffer.from(payload.data);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const dataType =
|
|
330
|
+
target.dataType ||
|
|
331
|
+
target.type ||
|
|
332
|
+
target.builtInType ||
|
|
333
|
+
"";
|
|
334
|
+
|
|
335
|
+
const isByteString =
|
|
336
|
+
typeof dataType === "string" &&
|
|
337
|
+
dataType.toLowerCase() === "bytestring";
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
// Buffer ou Uint8Array
|
|
342
|
+
if (Buffer.isBuffer(payload) || payload instanceof Uint8Array) {
|
|
343
|
+
|
|
344
|
+
const identifier = this.resolveIdentifier(target);
|
|
345
|
+
|
|
346
|
+
this.node.writeValue(
|
|
347
|
+
identifierType,
|
|
348
|
+
identifier,
|
|
349
|
+
Buffer.isBuffer(payload)
|
|
350
|
+
? payload
|
|
351
|
+
: Buffer.from(payload)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
writtenPaths = [identifier];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Array de números
|
|
358
|
+
else if (
|
|
359
|
+
Array.isArray(payload) &&
|
|
360
|
+
payload.every(item => typeof item === "number")
|
|
361
|
+
) {
|
|
362
|
+
|
|
363
|
+
const identifier = this.resolveIdentifier(target);
|
|
364
|
+
|
|
365
|
+
this.node.writeValue(
|
|
366
|
+
identifierType,
|
|
367
|
+
identifier,
|
|
368
|
+
isByteString
|
|
369
|
+
? Buffer.from(payload)
|
|
370
|
+
: payload
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
writtenPaths = [identifier];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Array de objetos
|
|
377
|
+
else if (Array.isArray(payload)) {
|
|
378
|
+
|
|
379
|
+
if (!payload.length) {
|
|
380
|
+
throw new Error("msg.payload array does not contain any items");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
payload.forEach(item => {
|
|
384
|
+
this.writePayloadItem(identifierType, item);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
writtenPaths = payload.map(item =>
|
|
388
|
+
this.resolvePayloadItemIdentifier(item)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Objeto { path: value }
|
|
393
|
+
else if (
|
|
394
|
+
payload &&
|
|
395
|
+
typeof payload === "object" &&
|
|
396
|
+
!Array.isArray(payload)
|
|
397
|
+
) {
|
|
398
|
+
|
|
399
|
+
const identifiers = Object.keys(payload);
|
|
400
|
+
|
|
401
|
+
if (!identifiers.length) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
"msg.payload object does not contain any " +
|
|
404
|
+
this.getIdentifierLabel(identifierType)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
identifiers.forEach(identifier => {
|
|
409
|
+
this.node.writeValue(
|
|
410
|
+
identifierType,
|
|
411
|
+
identifier,
|
|
412
|
+
payload[identifier]
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
writtenPaths = identifiers;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Valor simples
|
|
420
|
+
else {
|
|
421
|
+
|
|
422
|
+
const identifier = this.resolveIdentifier(target);
|
|
423
|
+
|
|
424
|
+
this.node.writeValue(
|
|
425
|
+
identifierType,
|
|
426
|
+
identifier,
|
|
427
|
+
payload
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
writtenPaths = [identifier];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
msg.opcua = msg.opcua || {};
|
|
434
|
+
|
|
435
|
+
this.assignWriteMetadata(
|
|
436
|
+
msg,
|
|
437
|
+
identifierType,
|
|
438
|
+
writtenPaths
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if (writtenPaths.length === 1) {
|
|
443
|
+
msg.topic = writtenPaths[0];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
process.send({
|
|
447
|
+
type: "send",
|
|
448
|
+
data: msg,
|
|
449
|
+
nodeId
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
process.send({
|
|
453
|
+
type: "status",
|
|
454
|
+
data: {
|
|
455
|
+
fill: "green",
|
|
456
|
+
shape: "dot",
|
|
457
|
+
text:
|
|
458
|
+
writtenPaths.length > 1
|
|
459
|
+
? `write ${writtenPaths.length} tags`
|
|
460
|
+
: `write ${writtenPaths[0]}`
|
|
461
|
+
},
|
|
462
|
+
nodeId
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
} catch (error) {
|
|
466
|
+
|
|
467
|
+
process.send({
|
|
468
|
+
type: "error",
|
|
469
|
+
data: {
|
|
470
|
+
fill: "red",
|
|
471
|
+
shape: "ring",
|
|
472
|
+
text: "failed write"
|
|
473
|
+
},
|
|
474
|
+
error: error.message,
|
|
475
|
+
nodeId
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
resolveIdentifierType(target) {
|
|
481
|
+
return target && target.identifierType === "nodeId" ? "nodeId" : "path";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
resolveIdentifier(target) {
|
|
485
|
+
const identifierType = this.resolveIdentifierType(target);
|
|
486
|
+
if (identifierType === "nodeId") {
|
|
487
|
+
const nodeId = String(target.tagNodeId || "").trim();
|
|
488
|
+
if (!nodeId) {
|
|
489
|
+
throw new Error("No tag nodeId configured");
|
|
490
|
+
}
|
|
491
|
+
return nodeId;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const path = String(target.tagPath || "").trim();
|
|
495
|
+
if (!path) {
|
|
496
|
+
throw new Error("No tag path configured");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return path;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
getIdentifierLabel(identifierType) {
|
|
503
|
+
return identifierType === "nodeId" ? "nodeIds" : "tag paths";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
resolvePayloadItemIdentifier(item) {
|
|
507
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
508
|
+
throw new Error("Each payload item must be an object");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const identifier = String(item.path || "").trim();
|
|
512
|
+
if (!identifier) {
|
|
513
|
+
throw new Error("Each payload item must contain a path");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return identifier;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
readPayloadItem(identifierType, item) {
|
|
520
|
+
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
521
|
+
return {
|
|
522
|
+
name: item.name,
|
|
523
|
+
path: identifier,
|
|
524
|
+
value: this.node.readValue(identifierType, identifier)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
writePayloadItem(identifierType, item) {
|
|
529
|
+
const identifier = this.resolvePayloadItemIdentifier(item);
|
|
530
|
+
this.node.writeValue(identifierType, identifier, item.value);
|
|
531
|
+
return {
|
|
532
|
+
name: item.name,
|
|
533
|
+
path: identifier,
|
|
534
|
+
value: item.value
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
assignReadMetadata(msg, identifierType, identifiers) {
|
|
539
|
+
msg.opcua.identifierType = identifierType;
|
|
540
|
+
if (identifierType === "nodeId") {
|
|
541
|
+
msg.opcua.readNodeIds = identifiers;
|
|
542
|
+
delete msg.opcua.readPaths;
|
|
543
|
+
if (identifiers.length === 1) {
|
|
544
|
+
msg.opcua.tagNodeId = identifiers[0];
|
|
545
|
+
delete msg.opcua.tagPath;
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
msg.opcua.readPaths = identifiers;
|
|
551
|
+
delete msg.opcua.readNodeIds;
|
|
552
|
+
if (identifiers.length === 1) {
|
|
553
|
+
msg.opcua.tagPath = identifiers[0];
|
|
554
|
+
delete msg.opcua.tagNodeId;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
assignWriteMetadata(msg, identifierType, identifiers) {
|
|
559
|
+
msg.opcua.identifierType = identifierType;
|
|
560
|
+
if (identifierType === "nodeId") {
|
|
561
|
+
msg.opcua.writtenNodeIds = identifiers;
|
|
562
|
+
delete msg.opcua.writtenPaths;
|
|
563
|
+
if (identifiers.length === 1) {
|
|
564
|
+
msg.opcua.tagNodeId = identifiers[0];
|
|
565
|
+
delete msg.opcua.tagPath;
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
msg.opcua.writtenPaths = identifiers;
|
|
571
|
+
delete msg.opcua.writtenNodeIds;
|
|
572
|
+
if (identifiers.length === 1) {
|
|
573
|
+
msg.opcua.tagPath = identifiers[0];
|
|
574
|
+
delete msg.opcua.tagNodeId;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Atualiza a árvore do servidor
|
|
580
|
+
*/
|
|
581
|
+
async update(payload, nodeId) {
|
|
582
|
+
try {
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
process.send({
|
|
586
|
+
type: "status",
|
|
587
|
+
data: {
|
|
588
|
+
fill: "yellow",
|
|
589
|
+
shape: "dot",
|
|
590
|
+
text: "updating items"
|
|
591
|
+
},
|
|
592
|
+
nodeId: nodeId
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
await this.ensureReady();
|
|
596
|
+
|
|
597
|
+
const nextTree = this.parser.normalizeTreeConfig(payload);
|
|
598
|
+
|
|
599
|
+
await this.runtime.updateTree(nextTree);
|
|
600
|
+
|
|
601
|
+
const endpointUrl = await this.runtime.getEndpointUrl();
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
process.send({
|
|
605
|
+
type: "status",
|
|
606
|
+
data: {
|
|
607
|
+
fill: "green",
|
|
608
|
+
shape: "dot",
|
|
609
|
+
text: endpointUrl
|
|
610
|
+
? "running " + endpointUrl
|
|
611
|
+
: "running"
|
|
612
|
+
},
|
|
613
|
+
nodeId: nodeId
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
} catch (error) {
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
process.send({
|
|
621
|
+
type: "errorUpdateServer",
|
|
622
|
+
data: error.message,
|
|
623
|
+
nodeId: nodeId
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
process.send({
|
|
627
|
+
type: "status",
|
|
628
|
+
data: {
|
|
629
|
+
fill: "red",
|
|
630
|
+
shape: "dot",
|
|
631
|
+
text: "error update"
|
|
632
|
+
},
|
|
633
|
+
nodeId: nodeId
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* metodos opc ua
|
|
641
|
+
*/
|
|
642
|
+
registerMethodInput(methodName, nodeId) {
|
|
643
|
+
|
|
644
|
+
if (methodName) {
|
|
645
|
+
|
|
646
|
+
registry.registerMethodHandler(methodName, nodeId);
|
|
647
|
+
} else {
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
handleMethodOutput(msg, nodeId) {
|
|
657
|
+
|
|
658
|
+
resolveRegisteredServer(this.node, msg, registry);
|
|
659
|
+
|
|
660
|
+
if (!msg._callId) {
|
|
661
|
+
throw new Error("Missing _callId for OPC UA method response");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
registry.resolveMethodCall(msg._callId, msg.payload);
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
process.send({
|
|
668
|
+
type: "status",
|
|
669
|
+
data: {
|
|
670
|
+
fill: "green",
|
|
671
|
+
shape: "dot",
|
|
672
|
+
text: "response sent"
|
|
673
|
+
},
|
|
674
|
+
nodeId: nodeId
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Garante que o servidor está pronto
|
|
682
|
+
*/
|
|
683
|
+
async ensureReady() {
|
|
684
|
+
if (!this.runtime) {
|
|
685
|
+
throw new Error("Server not created");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (this.lifecyclePromise) {
|
|
689
|
+
await this.lifecyclePromise;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
this.runtime.ensureReady();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
readActiveAlarms(msg, nodeId) {
|
|
697
|
+
try {
|
|
698
|
+
var result = registry.getActiveAlarms(this.node)
|
|
699
|
+
const msg2 = {
|
|
700
|
+
payload: result
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
process.send({
|
|
704
|
+
type: "send",
|
|
705
|
+
data: msg2,
|
|
706
|
+
nodeId: nodeId
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
process.send({
|
|
710
|
+
type: "status",
|
|
711
|
+
data: {
|
|
712
|
+
fill: "green",
|
|
713
|
+
shape: "dot",
|
|
714
|
+
text: result.paths.length > 1 ? "read " + result.paths.length + " tags" : "read " + result.paths[0]
|
|
715
|
+
},
|
|
716
|
+
nodeId: nodeId
|
|
717
|
+
});
|
|
718
|
+
} catch {
|
|
719
|
+
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Instância única do processo
|
|
727
|
+
*/
|
|
728
|
+
const serverProcess = new OpcUaServerProcess();
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Comunicação com processo pai (Node-RED)
|
|
732
|
+
*/
|
|
733
|
+
process.on("message", async (msg) => {
|
|
734
|
+
try {
|
|
735
|
+
switch (msg.type) {
|
|
736
|
+
case "createServer":
|
|
737
|
+
await serverProcess.create(msg.settings, msg.nodeId);
|
|
738
|
+
break;
|
|
739
|
+
|
|
740
|
+
case "stopServer":
|
|
741
|
+
|
|
742
|
+
await serverProcess.stop(msg.nodeId);
|
|
743
|
+
break;
|
|
744
|
+
|
|
745
|
+
case "updateServer":
|
|
746
|
+
await serverProcess.update(msg.msg.payload, msg.nodeId);
|
|
747
|
+
break;
|
|
748
|
+
|
|
749
|
+
case "writeTagServer":
|
|
750
|
+
|
|
751
|
+
serverProcess.writeFromPayload(msg.msg, msg.nodeId)
|
|
752
|
+
break;
|
|
753
|
+
|
|
754
|
+
case "writeEventServer":
|
|
755
|
+
|
|
756
|
+
serverProcess.writeEventFromPayload(msg.msg, msg.nodeId)
|
|
757
|
+
break;
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
case "readTagServer":
|
|
762
|
+
|
|
763
|
+
serverProcess.readFromPayload(msg.msg, msg.nodeId)
|
|
764
|
+
break;
|
|
765
|
+
|
|
766
|
+
case "registerMethodInput":
|
|
767
|
+
serverProcess.registerMethodInput(msg.node.methodName, msg.nodeId)
|
|
768
|
+
break;
|
|
769
|
+
|
|
770
|
+
case "handleMethodOutput":
|
|
771
|
+
serverProcess.handleMethodOutput(msg.msg, msg.nodeId)
|
|
772
|
+
break;
|
|
773
|
+
|
|
774
|
+
case "buildServerSnapshot":
|
|
775
|
+
OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId)
|
|
776
|
+
break
|
|
777
|
+
case "eventsServer":
|
|
778
|
+
eventsServer(serverProcess.node, msg.node, msg.nodeId)
|
|
779
|
+
break;
|
|
780
|
+
case "readActiveAlarms":
|
|
781
|
+
serverProcess.readActiveAlarms(msg.msg, msg.nodeId)
|
|
782
|
+
break;
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
default:
|
|
787
|
+
console.warn("Unknown message type2:", msg.type);
|
|
788
|
+
}
|
|
789
|
+
} catch (error) {
|
|
790
|
+
console.error("Process message error:", error);
|
|
791
|
+
|
|
792
|
+
process.send({
|
|
793
|
+
type: "error",
|
|
794
|
+
data: error.message
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Segurança: captura erros não tratados
|
|
801
|
+
*/
|
|
802
|
+
process.on("uncaughtException", (err) => {
|
|
803
|
+
console.error("Uncaught Exception:", err);
|
|
804
|
+
|
|
805
|
+
process.send({
|
|
806
|
+
type: "error",
|
|
807
|
+
data: "Uncaught Exception: " + err.message,
|
|
808
|
+
nodeId: nodeId
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
process.on("unhandledRejection", (reason) => {
|
|
813
|
+
console.error("Unhandled Rejection:", reason);
|
|
814
|
+
|
|
815
|
+
process.send({
|
|
816
|
+
type: "error",
|
|
817
|
+
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
818
|
+
nodeId: nodeId
|
|
819
|
+
});
|
|
820
820
|
});
|