@vitormnm/node-red-simple-opcua 1.6.3 → 1.8.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.
- package/README.md +104 -136
- package/client/lib/opcua-client-browser.js +254 -11
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +112 -9
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +158 -10
- package/client/opcua-client.html +8 -0
- package/client/opcua-client.js +97 -1
- package/client/view/opcua-client.js +106 -14
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +95 -32
- package/server/lib/opcua-address-space-builder.js +717 -59
- package/server/lib/opcua-config.js +110 -35
- package/server/lib/opcua-server-events-child.js +31 -5
- package/server/lib/opcua-server-runtime-child.js +424 -27
- package/server/lib/opcua-server-runtime.js +52 -5
- package/server/lib/opcua-server-status-child.js +46 -15
- package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
- package/server/opcua-server-io.html +93 -1
- package/server/opcua-server-io.js +153 -29
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +64 -0
- package/server/opcua-server.html +168 -44
- package/server/opcua-server.js +115 -5
- package/server/view/opcua-server.css +100 -6
- package/server/view/opcua-server.js +746 -48
|
@@ -26,14 +26,18 @@ class OpcUaServerConfigParser {
|
|
|
26
26
|
serverName: config.serverName || DEFAULT_SERVER_NAME,
|
|
27
27
|
port: normalizePort(config.port),
|
|
28
28
|
maxConnections: this.normalizeMaxConnections(config.maxConnections),
|
|
29
|
+
minSessionTimeout: this.normalizeSessionTimeout(config.minSessionTimeout),
|
|
30
|
+
defaultSessionTimeout: this.normalizeSessionTimeout(config.defaultSessionTimeout),
|
|
31
|
+
maxSessionTimeout: this.normalizeSessionTimeout(config.maxSessionTimeout),
|
|
29
32
|
namespaceUri: config.namespaceUri || DEFAULT_NAMESPACE_URI,
|
|
30
33
|
resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
|
|
31
34
|
treeConfig: this.parseTreeConfig(config.tree),
|
|
32
35
|
allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
|
|
36
|
+
automaticallyAcceptUnknownCertificate: this.normalizeAutomaticallyAcceptUnknownCertificate(config.automaticallyAcceptUnknownCertificate),
|
|
33
37
|
groups: auth.groups,
|
|
34
38
|
users: auth.users,
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
securityPolicies: security.securityPolicies,
|
|
40
|
+
securityModes: security.securityModes
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -509,17 +513,18 @@ class OpcUaServerConfigParser {
|
|
|
509
513
|
nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
|
|
510
514
|
namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
|
|
511
515
|
accessPermission: this.normalizeAccessPermissions(alarmConfig.accessPermission || alarmConfig.accessPermissions),
|
|
512
|
-
enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
|
|
516
|
+
enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true,
|
|
517
|
+
sendValue: typeof alarmConfig.sendValue === "boolean" ? alarmConfig.sendValue : true
|
|
513
518
|
};
|
|
514
519
|
|
|
515
520
|
if (type === "levelAlarm") {
|
|
516
|
-
base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) :
|
|
521
|
+
base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 90;
|
|
517
522
|
base.highHighMessage = typeof alarmConfig.highHighMessage === "string" ? alarmConfig.highHighMessage : "High High alarm";
|
|
518
523
|
base.highLimit = Number.isFinite(Number(alarmConfig.highLimit)) ? Number(alarmConfig.highLimit) : 80;
|
|
519
524
|
base.highMessage = typeof alarmConfig.highMessage === "string" ? alarmConfig.highMessage : "High alarm";
|
|
520
525
|
base.lowLimit = Number.isFinite(Number(alarmConfig.lowLimit)) ? Number(alarmConfig.lowLimit) : 20;
|
|
521
526
|
base.lowMessage = typeof alarmConfig.lowMessage === "string" ? alarmConfig.lowMessage : "Low alarm";
|
|
522
|
-
base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) :
|
|
527
|
+
base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 10;
|
|
523
528
|
base.lowLowMessage = typeof alarmConfig.lowLowMessage === "string" ? alarmConfig.lowLowMessage : "Low Low alarm";
|
|
524
529
|
} else if (type === "digitalAlarm") {
|
|
525
530
|
base.normalStateValue = Number.isFinite(Number(alarmConfig.normalStateValue)) ? Number(alarmConfig.normalStateValue) : 0;
|
|
@@ -643,6 +648,17 @@ class OpcUaServerConfigParser {
|
|
|
643
648
|
return value !== false;
|
|
644
649
|
}
|
|
645
650
|
|
|
651
|
+
normalizeAutomaticallyAcceptUnknownCertificate(value) {
|
|
652
|
+
if (value === undefined) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
if (typeof value === "string") {
|
|
656
|
+
return value !== "false";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return value !== false;
|
|
660
|
+
}
|
|
661
|
+
|
|
646
662
|
normalizeMaxConnections(value) {
|
|
647
663
|
const parsed = Number(value);
|
|
648
664
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -651,50 +667,109 @@ class OpcUaServerConfigParser {
|
|
|
651
667
|
return parsed;
|
|
652
668
|
}
|
|
653
669
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
670
|
+
normalizeSessionTimeout(value) {
|
|
671
|
+
if (value === undefined || value === null || value === "") {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
const parsed = Number(value);
|
|
675
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
return parsed;
|
|
679
|
+
}
|
|
657
680
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
? SECURITY_MODE_MAP[rawMode]
|
|
663
|
-
: SECURITY_MODE_MAP.None;
|
|
681
|
+
applySecuritySettings(policies, modes) {
|
|
682
|
+
// policies and modes can be comma-separated strings, e.g. "None,Basic256Sha256"
|
|
683
|
+
const rawPolicies = typeof policies === "string" ? policies.split(",").map(p => p.trim()).filter(Boolean) : ["None"];
|
|
684
|
+
const rawModes = typeof modes === "string" ? modes.split(",").map(m => m.trim()).filter(Boolean) : ["None"];
|
|
664
685
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
|
|
686
|
+
let securityPolicies = [];
|
|
687
|
+
rawPolicies.forEach(policy => {
|
|
688
|
+
if (Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, policy)) {
|
|
689
|
+
securityPolicies.push(SECURITY_POLICY_MAP[policy]);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
if (securityPolicies.length === 0) {
|
|
693
|
+
securityPolicies.push(SECURITY_POLICY_MAP.None);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let securityModes = [];
|
|
697
|
+
rawModes.forEach(mode => {
|
|
698
|
+
if (Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, mode)) {
|
|
699
|
+
securityModes.push(SECURITY_MODE_MAP[mode]);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
if (securityModes.length === 0) {
|
|
703
|
+
securityModes.push(SECURITY_MODE_MAP.None);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const hasNoneMode = securityModes.includes(MessageSecurityMode.None);
|
|
707
|
+
const hasSignedMode = securityModes.some(m => m !== MessageSecurityMode.None);
|
|
708
|
+
|
|
709
|
+
if (hasNoneMode && !hasSignedMode) {
|
|
710
|
+
// ONLY None mode is selected, so policy MUST be None
|
|
711
|
+
securityPolicies = [SecurityPolicy.None];
|
|
712
|
+
} else if (!hasNoneMode && hasSignedMode) {
|
|
713
|
+
// ONLY signed modes are selected, so policy cannot be None
|
|
714
|
+
securityPolicies = securityPolicies.filter(p => p !== SecurityPolicy.None);
|
|
715
|
+
if (securityPolicies.length === 0) {
|
|
716
|
+
securityPolicies.push(SecurityPolicy.Basic256Sha256);
|
|
717
|
+
this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
// Both None and signed modes are selected. Policies can be a mix.
|
|
721
|
+
// If the user selected a signed mode but only selected "None" policy, we must add a default signed policy.
|
|
722
|
+
const hasSignedPolicy = securityPolicies.some(p => p !== SecurityPolicy.None);
|
|
723
|
+
if (!hasSignedPolicy) {
|
|
724
|
+
securityPolicies.push(SecurityPolicy.Basic256Sha256);
|
|
725
|
+
this.node.warn("Added default Security Policy Basic256Sha256 because signed modes require a policy");
|
|
726
|
+
}
|
|
727
|
+
// Ensure None policy is present for the None mode
|
|
728
|
+
if (!securityPolicies.includes(SecurityPolicy.None)) {
|
|
729
|
+
securityPolicies.push(SecurityPolicy.None);
|
|
669
730
|
}
|
|
670
|
-
} else if (securityPolicy === SecurityPolicy.None) {
|
|
671
|
-
securityPolicy = SecurityPolicy.Basic256Sha256;
|
|
672
|
-
this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
|
|
673
731
|
}
|
|
674
732
|
|
|
675
733
|
return {
|
|
676
|
-
|
|
677
|
-
|
|
734
|
+
securityPolicies,
|
|
735
|
+
securityModes
|
|
678
736
|
};
|
|
679
737
|
}
|
|
680
738
|
|
|
681
739
|
coerceValue(value, type) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
740
|
+
const recursiveMap = (val, fn) => {
|
|
741
|
+
if (Array.isArray(val)) {
|
|
742
|
+
if ((type === "Int64" || type === "UInt64") && val.length === 2 && typeof val[0] === "number" && typeof val[1] === "number") {
|
|
743
|
+
return fn(val);
|
|
744
|
+
}
|
|
745
|
+
return val.map(item => recursiveMap(item, fn));
|
|
746
|
+
}
|
|
747
|
+
return fn(val);
|
|
748
|
+
};
|
|
685
749
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
750
|
+
const extractArray = (val) => {
|
|
751
|
+
if (Array.isArray(val)) {
|
|
752
|
+
return val;
|
|
753
|
+
}
|
|
754
|
+
if (typeof val === "string") {
|
|
755
|
+
const trimmed = val.trim();
|
|
756
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
757
|
+
try {
|
|
758
|
+
const parsed = JSON.parse(trimmed);
|
|
759
|
+
if (Array.isArray(parsed)) {
|
|
760
|
+
return parsed;
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
throw new Error("Invalid array value for type " + type + ": " + error.message);
|
|
693
764
|
}
|
|
694
|
-
} catch (error) {
|
|
695
|
-
throw new Error("Invalid array value for type " + type + ": " + error.message);
|
|
696
765
|
}
|
|
697
766
|
}
|
|
767
|
+
return null;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const items = extractArray(value);
|
|
771
|
+
if (items) {
|
|
772
|
+
return recursiveMap(items, (item) => this.coerceScalarValue(item, type));
|
|
698
773
|
}
|
|
699
774
|
|
|
700
775
|
return this.coerceScalarValue(value, type);
|
|
@@ -9,7 +9,7 @@ const { resolveRegisteredServer } = require("./server-node-utils");
|
|
|
9
9
|
|
|
10
10
|
function eventsServer(node, rootNode, nodeId) {
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
node.intervalMs = rootNode.intervalMs;
|
|
13
13
|
|
|
14
14
|
// 🔥 ALTERADO: usar Map para garantir unicidade por nodeID
|
|
15
15
|
node.queue = {
|
|
@@ -110,10 +110,36 @@ function flushQueue(node, nodeId) {
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
function upsertEvent(map, event) {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
const nodeKey = String(event.nodeID || "").trim();
|
|
114
|
+
if (!nodeKey) return;
|
|
115
|
+
|
|
116
|
+
// Key by nodeID only so the same variable is merged into one entry
|
|
117
|
+
const key = nodeKey;
|
|
118
|
+
const existing = map.get(key);
|
|
119
|
+
|
|
120
|
+
if (!existing) {
|
|
121
|
+
// First access for this variable in this interval — store a copy
|
|
122
|
+
map.set(key, Object.assign({}, event, { users: Array.isArray(event.users) ? [...event.users] : [] }));
|
|
123
|
+
} else {
|
|
124
|
+
// Variable already seen — update value and merge any new users
|
|
125
|
+
existing.value = event.value;
|
|
126
|
+
if (event.message !== undefined) existing.message = event.message;
|
|
127
|
+
if (event.severity !== undefined) existing.severity = event.severity;
|
|
128
|
+
if (event.retain !== undefined) existing.retain = event.retain;
|
|
129
|
+
if (event.activeState !== undefined) existing.activeState = event.activeState;
|
|
130
|
+
if (event.sourceName !== undefined) existing.sourceName = event.sourceName;
|
|
131
|
+
if (event.conditionName !== undefined) existing.conditionName = event.conditionName;
|
|
132
|
+
if (event.ConfirmedState !== undefined) existing.ConfirmedState = event.ConfirmedState;
|
|
133
|
+
if (event.ackedState !== undefined) existing.ackedState = event.ackedState;
|
|
134
|
+
if (event.alarmNode !== undefined) existing.alarmNode = event.alarmNode;
|
|
135
|
+
const existingNames = new Set((existing.users || []).map(u => u.name));
|
|
136
|
+
for (const user of (event.users || [])) {
|
|
137
|
+
if (!existingNames.has(user.name)) {
|
|
138
|
+
existing.users.push(user);
|
|
139
|
+
existingNames.add(user.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
function matchesServer(serverRef, event) {
|