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