@vitormnm/node-red-simple-opcua 1.0.0

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 (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/client/lib/opcua-client-browser.js +291 -0
  4. package/client/lib/opcua-client-read-service.js +16 -0
  5. package/client/lib/opcua-client-subscription-id-service.js +25 -0
  6. package/client/lib/opcua-client-subscription-service.js +171 -0
  7. package/client/lib/opcua-client-write-service.js +53 -0
  8. package/client/opcua-client-config.html +80 -0
  9. package/client/opcua-client-config.js +159 -0
  10. package/client/opcua-client-utils.js +320 -0
  11. package/client/opcua-client.html +1225 -0
  12. package/client/opcua-client.js +380 -0
  13. package/object.json +65 -0
  14. package/package.json +38 -0
  15. package/resources/editorClient.PNG +0 -0
  16. package/resources/editorServer.PNG +0 -0
  17. package/server/lib/opcua-address-space-alarm.js +341 -0
  18. package/server/lib/opcua-address-space-builder.js +1456 -0
  19. package/server/lib/opcua-config.js +543 -0
  20. package/server/lib/opcua-constants.js +106 -0
  21. package/server/lib/opcua-server-events-child.js +140 -0
  22. package/server/lib/opcua-server-methods.js +198 -0
  23. package/server/lib/opcua-server-runtime-child.js +729 -0
  24. package/server/lib/opcua-server-runtime.js +311 -0
  25. package/server/lib/opcua-server-status-child.js +188 -0
  26. package/server/lib/server-node-utils.js +16 -0
  27. package/server/opcua-server-io.html +347 -0
  28. package/server/opcua-server-io.js +463 -0
  29. package/server/opcua-server-registry.js +270 -0
  30. package/server/opcua-server.css +265 -0
  31. package/server/opcua-server.html +1548 -0
  32. package/server/opcua-server.js +143 -0
@@ -0,0 +1,729 @@
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
+ if (result.identifiers.length === 1) {
218
+ msg.topic = result.identifiers[0];
219
+ }
220
+
221
+
222
+
223
+ process.send({
224
+ type: "send",
225
+ data: msg,
226
+ nodeId: nodeId
227
+ });
228
+
229
+ process.send({
230
+ type: "status",
231
+ data: {
232
+ fill: "green",
233
+ shape: "dot",
234
+ text: result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0]
235
+ },
236
+ nodeId: nodeId
237
+ });
238
+
239
+ } catch (error) {
240
+
241
+ process.send({
242
+ type: "error",
243
+ data: { fill: "red", shape: "ring", text: "failed read" },
244
+ error: error.message,
245
+ nodeId: nodeId
246
+ });
247
+ }
248
+
249
+ }
250
+
251
+ writeEventFromPayload(msg, nodeId) {
252
+ try {
253
+ var writtenPaths = null
254
+ const payload = msg ? msg.payload : undefined;
255
+
256
+ writtenPaths = payload
257
+
258
+ if (payload && Array.isArray(payload)) {
259
+
260
+
261
+
262
+ payload.forEach((valuePayload) => {
263
+ this.node.writeEventByPath(valuePayload);
264
+ });
265
+
266
+
267
+ } else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
268
+
269
+
270
+ this.node.writeEventByPath(payload);
271
+
272
+
273
+
274
+ }
275
+
276
+ process.send({
277
+ type: "send",
278
+ data: msg,
279
+ nodeId: nodeId
280
+ });
281
+
282
+ process.send({
283
+ type: "status",
284
+ data: {
285
+ fill: "green",
286
+ shape: "dot",
287
+ text: writtenPaths.length > 1 ? "write " + writtenPaths.length + " events" : "Event " + writtenPaths.nodePath
288
+ },
289
+ nodeId: nodeId
290
+ });
291
+
292
+
293
+ } catch (error) {
294
+
295
+ process.send({
296
+ type: "error",
297
+ data: { fill: "red", shape: "ring", text: "failed write" },
298
+ error: error.message,
299
+ nodeId: nodeId
300
+ });
301
+ }
302
+
303
+
304
+
305
+ //return [path];
306
+ }
307
+
308
+
309
+
310
+ writeFromPayload(msg, nodeId) {
311
+ try {
312
+ var writtenPaths = null
313
+ const payload = msg ? msg.payload : undefined;
314
+ const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
315
+ const identifierType = this.resolveIdentifierType(target);
316
+
317
+
318
+
319
+ if (Array.isArray(payload)) {
320
+ if (!payload.length) {
321
+ throw new Error("msg.payload array does not contain any items");
322
+ }
323
+
324
+ payload.forEach((item) => {
325
+ this.writePayloadItem(identifierType, item);
326
+ });
327
+
328
+ writtenPaths = payload.map((item) => this.resolvePayloadItemIdentifier(item));
329
+ } else if (payload && typeof payload === "object" && !Array.isArray(payload)) {
330
+
331
+ const identifiers = Object.keys(payload);
332
+ if (!identifiers.length) {
333
+ throw new Error("msg.payload object does not contain any " + this.getIdentifierLabel(identifierType));
334
+ }
335
+
336
+ identifiers.forEach((identifier) => {
337
+ this.node.writeValue(identifierType, identifier, payload[identifier]);
338
+ });
339
+
340
+ writtenPaths = identifiers;
341
+ } else {
342
+
343
+ const identifier = this.resolveIdentifier(target);
344
+ this.node.writeValue(identifierType, identifier, payload);
345
+
346
+
347
+ writtenPaths = [identifier]
348
+ }
349
+
350
+ msg.opcua = msg.opcua || {};
351
+ this.assignWriteMetadata(msg, identifierType, writtenPaths);
352
+ if (writtenPaths.length === 1) {
353
+ msg.topic = writtenPaths[0];
354
+ }
355
+
356
+ process.send({
357
+ type: "send",
358
+ data: msg,
359
+ nodeId: nodeId
360
+ });
361
+
362
+ process.send({
363
+ type: "status",
364
+ data: {
365
+ fill: "green",
366
+ shape: "dot",
367
+ text: writtenPaths.length > 1 ? "write " + writtenPaths.length + " tags" : "write " + writtenPaths[0]
368
+ },
369
+ nodeId: nodeId
370
+ });
371
+
372
+
373
+ } catch (error) {
374
+
375
+ process.send({
376
+ type: "error",
377
+ data: { fill: "red", shape: "ring", text: "failed write" },
378
+ error: error.message,
379
+ nodeId: nodeId
380
+ });
381
+ }
382
+
383
+
384
+
385
+ //return [path];
386
+ }
387
+
388
+
389
+ resolveIdentifierType(target) {
390
+ return target && target.identifierType === "nodeId" ? "nodeId" : "path";
391
+ }
392
+
393
+ resolveIdentifier(target) {
394
+ const identifierType = this.resolveIdentifierType(target);
395
+ if (identifierType === "nodeId") {
396
+ const nodeId = String(target.tagNodeId || "").trim();
397
+ if (!nodeId) {
398
+ throw new Error("No tag nodeId configured");
399
+ }
400
+ return nodeId;
401
+ }
402
+
403
+ const path = String(target.tagPath || "").trim();
404
+ if (!path) {
405
+ throw new Error("No tag path configured");
406
+ }
407
+
408
+ return path;
409
+ }
410
+
411
+ getIdentifierLabel(identifierType) {
412
+ return identifierType === "nodeId" ? "nodeIds" : "tag paths";
413
+ }
414
+
415
+ resolvePayloadItemIdentifier(item) {
416
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
417
+ throw new Error("Each payload item must be an object");
418
+ }
419
+
420
+ const identifier = String(item.path || "").trim();
421
+ if (!identifier) {
422
+ throw new Error("Each payload item must contain a path");
423
+ }
424
+
425
+ return identifier;
426
+ }
427
+
428
+ readPayloadItem(identifierType, item) {
429
+ const identifier = this.resolvePayloadItemIdentifier(item);
430
+ return {
431
+ name: item.name,
432
+ path: identifier,
433
+ value: this.node.readValue(identifierType, identifier)
434
+ };
435
+ }
436
+
437
+ writePayloadItem(identifierType, item) {
438
+ const identifier = this.resolvePayloadItemIdentifier(item);
439
+ this.node.writeValue(identifierType, identifier, item.value);
440
+ return {
441
+ name: item.name,
442
+ path: identifier,
443
+ value: item.value
444
+ };
445
+ }
446
+
447
+ assignReadMetadata(msg, identifierType, identifiers) {
448
+ msg.opcua.identifierType = identifierType;
449
+ if (identifierType === "nodeId") {
450
+ msg.opcua.readNodeIds = identifiers;
451
+ delete msg.opcua.readPaths;
452
+ if (identifiers.length === 1) {
453
+ msg.opcua.tagNodeId = identifiers[0];
454
+ delete msg.opcua.tagPath;
455
+ }
456
+ return;
457
+ }
458
+
459
+ msg.opcua.readPaths = identifiers;
460
+ delete msg.opcua.readNodeIds;
461
+ if (identifiers.length === 1) {
462
+ msg.opcua.tagPath = identifiers[0];
463
+ delete msg.opcua.tagNodeId;
464
+ }
465
+ }
466
+
467
+ assignWriteMetadata(msg, identifierType, identifiers) {
468
+ msg.opcua.identifierType = identifierType;
469
+ if (identifierType === "nodeId") {
470
+ msg.opcua.writtenNodeIds = identifiers;
471
+ delete msg.opcua.writtenPaths;
472
+ if (identifiers.length === 1) {
473
+ msg.opcua.tagNodeId = identifiers[0];
474
+ delete msg.opcua.tagPath;
475
+ }
476
+ return;
477
+ }
478
+
479
+ msg.opcua.writtenPaths = identifiers;
480
+ delete msg.opcua.writtenNodeIds;
481
+ if (identifiers.length === 1) {
482
+ msg.opcua.tagPath = identifiers[0];
483
+ delete msg.opcua.tagNodeId;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Atualiza a árvore do servidor
489
+ */
490
+ async update(payload, nodeId) {
491
+ try {
492
+
493
+
494
+ process.send({
495
+ type: "status",
496
+ data: {
497
+ fill: "yellow",
498
+ shape: "dot",
499
+ text: "updating items"
500
+ },
501
+ nodeId: nodeId
502
+ });
503
+
504
+ await this.ensureReady();
505
+
506
+ const nextTree = this.parser.normalizeTreeConfig(payload);
507
+
508
+ await this.runtime.updateTree(nextTree);
509
+
510
+ const endpointUrl = await this.runtime.getEndpointUrl();
511
+
512
+
513
+ process.send({
514
+ type: "status",
515
+ data: {
516
+ fill: "green",
517
+ shape: "dot",
518
+ text: endpointUrl
519
+ ? "running " + endpointUrl
520
+ : "running"
521
+ },
522
+ nodeId: nodeId
523
+ });
524
+
525
+
526
+ } catch (error) {
527
+
528
+
529
+ process.send({
530
+ type: "errorUpdateServer",
531
+ data: error.message,
532
+ nodeId: nodeId
533
+ });
534
+
535
+ process.send({
536
+ type: "status",
537
+ data: {
538
+ fill: "red",
539
+ shape: "dot",
540
+ text: "error update"
541
+ },
542
+ nodeId: nodeId
543
+ });
544
+ }
545
+ }
546
+
547
+
548
+ /**
549
+ * metodos opc ua
550
+ */
551
+ registerMethodInput(methodName, nodeId) {
552
+
553
+ if (methodName) {
554
+
555
+ registry.registerMethodHandler(methodName, nodeId);
556
+ } else {
557
+
558
+
559
+ }
560
+
561
+
562
+ }
563
+
564
+
565
+ handleMethodOutput(msg, nodeId) {
566
+
567
+ resolveRegisteredServer(this.node, msg, registry);
568
+
569
+ if (!msg._callId) {
570
+ throw new Error("Missing _callId for OPC UA method response");
571
+ }
572
+
573
+ registry.resolveMethodCall(msg._callId, msg.payload);
574
+
575
+
576
+ process.send({
577
+ type: "status",
578
+ data: {
579
+ fill: "green",
580
+ shape: "dot",
581
+ text: "response sent"
582
+ },
583
+ nodeId: nodeId
584
+ });
585
+
586
+
587
+ }
588
+
589
+ /**
590
+ * Garante que o servidor está pronto
591
+ */
592
+ async ensureReady() {
593
+ if (!this.runtime) {
594
+ throw new Error("Server not created");
595
+ }
596
+
597
+ if (this.lifecyclePromise) {
598
+ await this.lifecyclePromise;
599
+ }
600
+
601
+ this.runtime.ensureReady();
602
+ }
603
+
604
+
605
+ readActiveAlarms(msg, nodeId) {
606
+ try {
607
+ var result = registry.getActiveAlarms(this.node)
608
+ const msg2 = {
609
+ payload: result
610
+ }
611
+
612
+ process.send({
613
+ type: "send",
614
+ data: msg2,
615
+ nodeId: nodeId
616
+ });
617
+
618
+ process.send({
619
+ type: "status",
620
+ data: {
621
+ fill: "green",
622
+ shape: "dot",
623
+ text: result.paths.length > 1 ? "read " + result.paths.length + " tags" : "read " + result.paths[0]
624
+ },
625
+ nodeId: nodeId
626
+ });
627
+ } catch {
628
+
629
+ }
630
+ }
631
+
632
+ }
633
+
634
+ /**
635
+ * Instância única do processo
636
+ */
637
+ const serverProcess = new OpcUaServerProcess();
638
+
639
+ /**
640
+ * Comunicação com processo pai (Node-RED)
641
+ */
642
+ process.on("message", async (msg) => {
643
+ try {
644
+ switch (msg.type) {
645
+ case "createServer":
646
+ await serverProcess.create(msg.settings, msg.nodeId);
647
+ break;
648
+
649
+ case "stopServer":
650
+
651
+ await serverProcess.stop(msg.nodeId);
652
+ break;
653
+
654
+ case "updateServer":
655
+ await serverProcess.update(msg.msg.payload, msg.nodeId);
656
+ break;
657
+
658
+ case "writeTagServer":
659
+
660
+ serverProcess.writeFromPayload(msg.msg, msg.nodeId)
661
+ break;
662
+
663
+ case "writeEventServer":
664
+
665
+ serverProcess.writeEventFromPayload(msg.msg, msg.nodeId)
666
+ break;
667
+
668
+
669
+
670
+ case "readTagServer":
671
+
672
+ serverProcess.readFromPayload(msg.msg, msg.nodeId)
673
+ break;
674
+
675
+ case "registerMethodInput":
676
+ serverProcess.registerMethodInput(msg.node.methodName, msg.nodeId)
677
+ break;
678
+
679
+ case "handleMethodOutput":
680
+ serverProcess.handleMethodOutput(msg.msg, msg.nodeId)
681
+ break;
682
+
683
+ case "buildServerSnapshot":
684
+ OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId)
685
+ break
686
+ case "eventsServer":
687
+ eventsServer(serverProcess.node, msg.node, msg.nodeId)
688
+ break;
689
+ case "readActiveAlarms":
690
+ serverProcess.readActiveAlarms(msg.msg, msg.nodeId)
691
+ break;
692
+
693
+
694
+
695
+ default:
696
+ console.warn("Unknown message type2:", msg.type);
697
+ }
698
+ } catch (error) {
699
+ console.error("Process message error:", error);
700
+
701
+ process.send({
702
+ type: "error",
703
+ data: error.message
704
+ });
705
+ }
706
+ });
707
+
708
+ /**
709
+ * Segurança: captura erros não tratados
710
+ */
711
+ process.on("uncaughtException", (err) => {
712
+ console.error("Uncaught Exception:", err);
713
+
714
+ process.send({
715
+ type: "error",
716
+ data: "Uncaught Exception: " + err.message,
717
+ nodeId: nodeId
718
+ });
719
+ });
720
+
721
+ process.on("unhandledRejection", (reason) => {
722
+ console.error("Unhandled Rejection:", reason);
723
+
724
+ process.send({
725
+ type: "error",
726
+ data: "Unhandled Rejection: " + (reason?.message || reason),
727
+ nodeId: nodeId
728
+ });
729
+ });