@tier0/opcua-client 0.1.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/BENCHMARK.md +83 -0
- package/LICENSE +17 -0
- package/README.md +141 -0
- package/binding.gyp +24 -0
- package/index.js +5 -0
- package/lib/browse-tree.js +55 -0
- package/lib/client.js +321 -0
- package/lib/native.js +32 -0
- package/lib/points.js +231 -0
- package/nodes/browse.html +258 -0
- package/nodes/browse.js +121 -0
- package/nodes/connection.html +44 -0
- package/nodes/connection.js +49 -0
- package/nodes/read.html +187 -0
- package/nodes/read.js +66 -0
- package/nodes/subscribe.html +44 -0
- package/nodes/subscribe.js +285 -0
- package/nodes/write.html +209 -0
- package/nodes/write.js +46 -0
- package/package.json +61 -0
- package/scripts/build-open62541.sh +26 -0
- package/src/addon.cc +1410 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.opcua-advanced { margin: 8px 0 12px; }
|
|
3
|
+
.opcua-advanced summary { cursor: pointer; font-weight: 600; padding: 6px 0; }
|
|
4
|
+
.opcua-standard-note { margin: 0 0 10px 105px; color: var(--red-ui-form-text-color, #666); font-size: 0.9em; }
|
|
5
|
+
</style>
|
|
6
|
+
<script type="text/html" data-template-name="tier0-opcua-connection">
|
|
7
|
+
<div class="form-row"><label for="node-config-input-endpoint">Endpoint</label><input type="text" id="node-config-input-endpoint" placeholder="opc.tcp://127.0.0.1:4840"></div>
|
|
8
|
+
<div class="form-row"><label for="node-config-input-securityMode">Security</label><select id="node-config-input-securityMode"><option value="None">None</option><option value="Sign">Sign</option><option value="SignAndEncrypt">SignAndEncrypt</option></select></div>
|
|
9
|
+
<div class="opcua-standard-note">Standard defaults include automatic reconnect, connectivity checks, and a bounded NodeId cache.</div>
|
|
10
|
+
<details class="opcua-advanced">
|
|
11
|
+
<summary>Security and advanced connection settings</summary>
|
|
12
|
+
<div class="form-row"><label for="node-config-input-securityPolicy">Policy</label><select id="node-config-input-securityPolicy"><option value="None">None</option><option value="Basic256Sha256">Basic256Sha256</option><option value="Aes128Sha256RsaOaep">Aes128Sha256RsaOaep</option><option value="Aes256Sha256RsaPss">Aes256Sha256RsaPss</option></select></div>
|
|
13
|
+
<div class="form-row"><label for="node-config-input-user">User</label><input type="text" id="node-config-input-user"></div>
|
|
14
|
+
<div class="form-row"><label for="node-config-input-password">Password</label><input type="password" id="node-config-input-password"></div>
|
|
15
|
+
<div class="form-row"><label for="node-config-input-certificate">Certificate</label><input type="text" id="node-config-input-certificate"></div>
|
|
16
|
+
<div class="form-row"><label for="node-config-input-privateKey">Private key</label><input type="text" id="node-config-input-privateKey"></div>
|
|
17
|
+
<div class="form-row"><label for="node-config-input-requestedSessionTimeout">Session timeout</label><input type="number" id="node-config-input-requestedSessionTimeout"></div>
|
|
18
|
+
<div class="form-row"><label for="node-config-input-requestTimeout">Request timeout</label><input type="number" id="node-config-input-requestTimeout"></div>
|
|
19
|
+
<div class="form-row"><label for="node-config-input-connectivityCheckInterval">Connectivity check</label><input type="number" id="node-config-input-connectivityCheckInterval"></div>
|
|
20
|
+
<div class="form-row"><label for="node-config-input-reconnectInterval">Reconnect interval</label><input type="number" id="node-config-input-reconnectInterval"></div>
|
|
21
|
+
<div class="form-row"><label for="node-config-input-maxReconnectInterval">Max reconnect</label><input type="number" id="node-config-input-maxReconnectInterval"></div>
|
|
22
|
+
<div class="form-row"><label for="node-config-input-nodeIdCacheSize">NodeId cache</label><input type="number" id="node-config-input-nodeIdCacheSize"></div>
|
|
23
|
+
</details>
|
|
24
|
+
<div class="form-row"><label for="node-config-input-name">Name</label><input type="text" id="node-config-input-name"></div>
|
|
25
|
+
</script>
|
|
26
|
+
<script type="text/javascript">
|
|
27
|
+
RED.nodes.registerType('tier0-opcua-connection', {
|
|
28
|
+
category: 'config',
|
|
29
|
+
defaults: {
|
|
30
|
+
endpoint: { value: '', required: true },
|
|
31
|
+
securityMode: { value: 'None' }, securityPolicy: { value: 'None' },
|
|
32
|
+
user: { value: '' }, certificate: { value: '' }, privateKey: { value: '' },
|
|
33
|
+
requestedSessionTimeout: { value: 60000, validate: RED.validators.number() },
|
|
34
|
+
requestTimeout: { value: 10000, validate: RED.validators.number() },
|
|
35
|
+
connectivityCheckInterval: { value: 5000, validate: RED.validators.number() },
|
|
36
|
+
reconnectInterval: { value: 5000, validate: RED.validators.number() },
|
|
37
|
+
maxReconnectInterval: { value: 30000, validate: RED.validators.number() },
|
|
38
|
+
nodeIdCacheSize: { value: 100000, validate: RED.validators.number() },
|
|
39
|
+
name: { value: '' }
|
|
40
|
+
},
|
|
41
|
+
credentials: { password: { type: 'password' } },
|
|
42
|
+
label: function () { return this.name || this.endpoint || 'tier0-opcua-connection'; }
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { OpcUaClient } = require('../lib/client');
|
|
4
|
+
|
|
5
|
+
module.exports = function registerConnectionNode(RED) {
|
|
6
|
+
function OpcUaConnectionNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
node.client = new OpcUaClient({
|
|
11
|
+
endpoint: config.endpoint,
|
|
12
|
+
securityMode: config.securityMode || 'None',
|
|
13
|
+
securityPolicy: config.securityPolicy || 'None',
|
|
14
|
+
user: config.user || '',
|
|
15
|
+
password: node.credentials?.password || '',
|
|
16
|
+
certificate: config.certificate || '',
|
|
17
|
+
privateKey: config.privateKey || '',
|
|
18
|
+
requestedSessionTimeout: Number(config.requestedSessionTimeout) || 60000,
|
|
19
|
+
requestTimeout: Number(config.requestTimeout) || 10000,
|
|
20
|
+
connectivityCheckInterval: Number.isFinite(Number(config.connectivityCheckInterval))
|
|
21
|
+
? Math.max(0, Math.floor(Number(config.connectivityCheckInterval)))
|
|
22
|
+
: 5000,
|
|
23
|
+
reconnectInterval: Number(config.reconnectInterval) || 5000,
|
|
24
|
+
maxReconnectInterval: Number(config.maxReconnectInterval) || 30000,
|
|
25
|
+
nodeIdCacheSize: Number.isFinite(Number(config.nodeIdCacheSize))
|
|
26
|
+
? Math.max(0, Math.floor(Number(config.nodeIdCacheSize)))
|
|
27
|
+
: 100000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
node.client.on('fault', (error) => node.warn(`OPC UA connection fault: ${error.message}`));
|
|
31
|
+
node.client.on('state', (state) => {
|
|
32
|
+
if (state.state === 'failed') node.warn(`OPC UA connection failed: ${state.statusCode}`);
|
|
33
|
+
});
|
|
34
|
+
node.on('close', async (done) => {
|
|
35
|
+
try {
|
|
36
|
+
await node.client.disconnect();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
node.warn(`OPC UA disconnect failed: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
done();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
RED.nodes.registerType('tier0-opcua-connection', OpcUaConnectionNode, {
|
|
45
|
+
credentials: {
|
|
46
|
+
password: { type: 'password' },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
};
|
package/nodes/read.html
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.opcua-advanced { margin: 8px 0 12px; }
|
|
3
|
+
.opcua-advanced summary { cursor: pointer; font-weight: 600; padding: 6px 0; }
|
|
4
|
+
.opcua-point-editor { box-sizing: border-box; display: inline-block; vertical-align: top; width: calc(100% - 110px); }
|
|
5
|
+
.opcua-point-toolbar { display: flex; gap: 6px; margin-bottom: 8px; }
|
|
6
|
+
.opcua-point-add { display: flex; gap: 6px; margin-bottom: 8px; }
|
|
7
|
+
.opcua-point-add input { flex: 1; min-width: 0; }
|
|
8
|
+
.opcua-point-list { border: 1px solid var(--red-ui-form-input-border-color, #ccc); border-radius: 2px; box-sizing: border-box; height: 180px; list-style: none; margin: 0 !important; overflow: auto; padding: 0 !important; width: 100%; }
|
|
9
|
+
.opcua-point-list li { align-items: center; border-bottom: 1px solid var(--red-ui-secondary-border-color, #eee); display: flex; gap: 8px; min-height: 34px; padding: 4px 8px; }
|
|
10
|
+
.opcua-point-list li:last-child { border-bottom: 0; }
|
|
11
|
+
.opcua-point-list .opcua-point-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
12
|
+
.opcua-point-list .opcua-point-empty { color: var(--red-ui-form-placeholder-color, #888); justify-content: center; }
|
|
13
|
+
.opcua-point-list .opcua-point-remove { border: 0; color: var(--red-ui-text-color, #555); flex: 0 0 auto; }
|
|
14
|
+
.opcua-point-status { color: var(--red-ui-form-text-color, #666); font-size: 0.9em; margin-top: 5px; }
|
|
15
|
+
</style>
|
|
16
|
+
<script type="text/html" data-template-name="tier0-opcua-read">
|
|
17
|
+
<div class="form-row"><label for="node-input-connection">Connection</label><input type="text" id="node-input-connection"></div>
|
|
18
|
+
<input type="hidden" id="node-input-nodeId">
|
|
19
|
+
<input type="hidden" id="node-input-pointList">
|
|
20
|
+
<input type="hidden" id="node-input-pointListFileName">
|
|
21
|
+
<input type="file" id="node-input-pointFile" accept=".json,.csv,application/json,text/csv" style="display:none">
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label>Points</label>
|
|
24
|
+
<div class="opcua-point-editor">
|
|
25
|
+
<div class="opcua-point-toolbar">
|
|
26
|
+
<button type="button" class="red-ui-button" id="node-input-importPointList"><i class="fa fa-upload"></i> Import</button>
|
|
27
|
+
<button type="button" class="red-ui-button" id="node-input-clearPointList"><i class="fa fa-trash"></i> Clear all</button>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="opcua-point-add">
|
|
30
|
+
<input type="text" id="node-input-newPointNodeId" placeholder="NodeId, for example ns=2;s=Machine.Speed">
|
|
31
|
+
<button type="button" class="red-ui-button" id="node-input-addPoint"><i class="fa fa-plus"></i> Add</button>
|
|
32
|
+
</div>
|
|
33
|
+
<ol class="opcua-point-list" id="node-input-pointListItems"></ol>
|
|
34
|
+
<div class="opcua-point-status" id="node-input-pointListStatus">0 points</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<details class="opcua-advanced">
|
|
38
|
+
<summary>Advanced settings</summary>
|
|
39
|
+
<div class="form-row"><label for="node-input-batchSize">Batch size</label><input type="number" id="node-input-batchSize"></div>
|
|
40
|
+
<div class="form-row"><label for="node-input-outputMode">Output</label><select id="node-input-outputMode"><option value="batch">Batch</option><option value="split">One message per point</option></select></div>
|
|
41
|
+
</details>
|
|
42
|
+
<div class="form-row"><label for="node-input-name">Name</label><input type="text" id="node-input-name"></div>
|
|
43
|
+
</script>
|
|
44
|
+
<script type="text/html" data-help-name="tier0-opcua-read">
|
|
45
|
+
<p>Add NodeIds to the point list or import JSON/CSV. Imported points are parsed and shown in the same list.</p>
|
|
46
|
+
<p>CSV requires a <code>nodeId</code> header. A JSON/CSV point list may also arrive dynamically in <code>msg.payload</code>.</p>
|
|
47
|
+
</script>
|
|
48
|
+
<script type="text/javascript">
|
|
49
|
+
RED.nodes.registerType('tier0-opcua-read', {
|
|
50
|
+
category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
|
|
51
|
+
defaults: {
|
|
52
|
+
connection: { value: '', type: 'tier0-opcua-connection', required: true },
|
|
53
|
+
nodeId: { value: '' }, pointList: { value: '' }, pointListFileName: { value: '' },
|
|
54
|
+
batchSize: { value: 500, validate: RED.validators.number() },
|
|
55
|
+
outputMode: { value: 'batch' }, name: { value: '' }
|
|
56
|
+
},
|
|
57
|
+
oneditprepare: function () {
|
|
58
|
+
const points = new Map();
|
|
59
|
+
|
|
60
|
+
const csvLine = (line) => {
|
|
61
|
+
const fields = [];
|
|
62
|
+
let value = '';
|
|
63
|
+
let quoted = false;
|
|
64
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
65
|
+
const character = line[index];
|
|
66
|
+
if (character === '"') {
|
|
67
|
+
if (quoted && line[index + 1] === '"') { value += '"'; index += 1; }
|
|
68
|
+
else quoted = !quoted;
|
|
69
|
+
} else if (character === ',' && !quoted) {
|
|
70
|
+
fields.push(value.trim()); value = '';
|
|
71
|
+
} else value += character;
|
|
72
|
+
}
|
|
73
|
+
fields.push(value.trim());
|
|
74
|
+
return fields;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parse = (content) => {
|
|
78
|
+
const text = String(content || '').replace(/^\uFEFF/, '').trim();
|
|
79
|
+
if (!text) return [];
|
|
80
|
+
if (text.startsWith('[') || text.startsWith('{')) {
|
|
81
|
+
const parsed = JSON.parse(text);
|
|
82
|
+
return Array.isArray(parsed) ? parsed : (Array.isArray(parsed.items) ? parsed.items : [parsed]);
|
|
83
|
+
}
|
|
84
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim() && !line.trimStart().startsWith('#'));
|
|
85
|
+
if (!lines.length) return [];
|
|
86
|
+
const headers = csvLine(lines.shift());
|
|
87
|
+
if (!headers.includes('nodeId')) throw new Error('CSV header must contain nodeId');
|
|
88
|
+
return lines.map((line) => {
|
|
89
|
+
const values = csvLine(line);
|
|
90
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || '']));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const normalize = (item) => {
|
|
95
|
+
const source = typeof item === 'string' ? { nodeId: item } : { ...(item || {}) };
|
|
96
|
+
source.nodeId = String(source.nodeId || source.topic || '').trim();
|
|
97
|
+
if (!source.nodeId) throw new Error('Point is missing nodeId');
|
|
98
|
+
return source;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const persist = () => {
|
|
102
|
+
const items = [...points.values()];
|
|
103
|
+
$('#node-input-pointList').val(items.length ? JSON.stringify(items) : '');
|
|
104
|
+
if (items.length) $('#node-input-nodeId').val('');
|
|
105
|
+
$('#node-input-pointListStatus').text(`${items.length} point${items.length === 1 ? '' : 's'}`);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const render = () => {
|
|
109
|
+
const list = document.getElementById('node-input-pointListItems');
|
|
110
|
+
list.textContent = '';
|
|
111
|
+
if (!points.size) {
|
|
112
|
+
const empty = document.createElement('li');
|
|
113
|
+
empty.className = 'opcua-point-empty';
|
|
114
|
+
empty.textContent = 'No points. Add a NodeId or import a file.';
|
|
115
|
+
list.appendChild(empty);
|
|
116
|
+
} else {
|
|
117
|
+
const fragment = document.createDocumentFragment();
|
|
118
|
+
points.forEach((point, nodeId) => {
|
|
119
|
+
const row = document.createElement('li');
|
|
120
|
+
const text = document.createElement('span');
|
|
121
|
+
text.className = 'opcua-point-text';
|
|
122
|
+
text.textContent = point.alias ? `${point.alias} — ${nodeId}` : nodeId;
|
|
123
|
+
text.title = text.textContent;
|
|
124
|
+
const remove = document.createElement('button');
|
|
125
|
+
remove.type = 'button';
|
|
126
|
+
remove.className = 'red-ui-button opcua-point-remove';
|
|
127
|
+
remove.title = `Remove ${nodeId}`;
|
|
128
|
+
remove.innerHTML = '<i class="fa fa-times"></i>';
|
|
129
|
+
remove.addEventListener('click', () => { points.delete(nodeId); render(); });
|
|
130
|
+
row.append(text, remove);
|
|
131
|
+
fragment.appendChild(row);
|
|
132
|
+
});
|
|
133
|
+
list.appendChild(fragment);
|
|
134
|
+
}
|
|
135
|
+
persist();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const merge = (items) => {
|
|
139
|
+
items.forEach((item) => { const point = normalize(item); points.set(point.nodeId, point); });
|
|
140
|
+
render();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const stored = $('#node-input-pointList').val();
|
|
145
|
+
if (stored) merge(parse(stored));
|
|
146
|
+
else if ($('#node-input-nodeId').val()) merge([{ nodeId: $('#node-input-nodeId').val() }]);
|
|
147
|
+
else render();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
RED.notify(`Cannot load point list: ${error.message}`, 'error');
|
|
150
|
+
render();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$('#node-input-addPoint').on('click', () => {
|
|
154
|
+
try {
|
|
155
|
+
merge([{ nodeId: $('#node-input-newPointNodeId').val() }]);
|
|
156
|
+
$('#node-input-newPointNodeId').val('').focus();
|
|
157
|
+
} catch (error) { RED.notify(error.message, 'error'); }
|
|
158
|
+
});
|
|
159
|
+
$('#node-input-newPointNodeId').on('keydown', function (event) {
|
|
160
|
+
if (event.key === 'Enter') { event.preventDefault(); $('#node-input-addPoint').click(); }
|
|
161
|
+
});
|
|
162
|
+
$('#node-input-importPointList').on('click', () => $('#node-input-pointFile').click());
|
|
163
|
+
$('#node-input-pointFile').on('change', function () {
|
|
164
|
+
const file = this.files && this.files[0];
|
|
165
|
+
if (!file) return;
|
|
166
|
+
const reader = new FileReader();
|
|
167
|
+
reader.onload = () => {
|
|
168
|
+
try {
|
|
169
|
+
const imported = parse(reader.result);
|
|
170
|
+
merge(imported);
|
|
171
|
+
$('#node-input-pointListFileName').val(file.name);
|
|
172
|
+
RED.notify(`${imported.length} point(s) imported`, 'success');
|
|
173
|
+
} catch (error) { RED.notify(`Import failed: ${error.message}`, 'error'); }
|
|
174
|
+
$('#node-input-pointFile').val('');
|
|
175
|
+
};
|
|
176
|
+
reader.readAsText(file);
|
|
177
|
+
});
|
|
178
|
+
$('#node-input-clearPointList').on('click', () => {
|
|
179
|
+
points.clear();
|
|
180
|
+
$('#node-input-nodeId').val('');
|
|
181
|
+
$('#node-input-pointListFileName').val('');
|
|
182
|
+
render();
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
label: function () { return this.name || 'tier0-opcua-read'; }
|
|
186
|
+
});
|
|
187
|
+
</script>
|
package/nodes/read.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { looksLikePointList, messagePoints } = require('../lib/points');
|
|
4
|
+
|
|
5
|
+
module.exports = function registerReadNode(RED) {
|
|
6
|
+
function OpcUaReadNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
node.connection = RED.nodes.getNode(config.connection);
|
|
10
|
+
|
|
11
|
+
node.on('input', async (msg, send, done) => {
|
|
12
|
+
if (!node.connection) return done(new Error('OPC UA connection not configured'));
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const points = messagePoints(msg, config.nodeId, {}, {
|
|
16
|
+
configuredPayload: config.pointList,
|
|
17
|
+
format: 'auto',
|
|
18
|
+
});
|
|
19
|
+
node.status({ fill: 'blue', shape: 'dot', text: `reading ${points.length}` });
|
|
20
|
+
const values = await node.connection.client.readMany(
|
|
21
|
+
points,
|
|
22
|
+
Number(config.batchSize) || 500
|
|
23
|
+
);
|
|
24
|
+
const descriptors = new Map(points.map((point) => [point.nodeId, point]));
|
|
25
|
+
const results = values.map((value) => ({
|
|
26
|
+
...descriptors.get(value.nodeId),
|
|
27
|
+
...value,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
if ((config.outputMode || 'batch') === 'split') {
|
|
31
|
+
results.forEach((result) => send({
|
|
32
|
+
...msg,
|
|
33
|
+
topic: result.nodeId,
|
|
34
|
+
payload: result.value,
|
|
35
|
+
opcua: result,
|
|
36
|
+
}));
|
|
37
|
+
} else if (points.length === 1 && !looksLikePointList(msg.payload) &&
|
|
38
|
+
!Array.isArray(msg.items) && !config.pointList) {
|
|
39
|
+
const result = results[0];
|
|
40
|
+
msg.topic = result.nodeId;
|
|
41
|
+
msg.payload = result.value;
|
|
42
|
+
msg.opcua = result;
|
|
43
|
+
send(msg);
|
|
44
|
+
} else {
|
|
45
|
+
msg.payload = results;
|
|
46
|
+
msg.opcua = {
|
|
47
|
+
...(msg.opcua || {}),
|
|
48
|
+
operation: 'read',
|
|
49
|
+
total: results.length,
|
|
50
|
+
good: results.filter((item) => item.statusCode === 'Good').length,
|
|
51
|
+
clientDiagnostics: node.connection.client.getDiagnostics(),
|
|
52
|
+
};
|
|
53
|
+
send(msg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
node.status({ fill: 'green', shape: 'dot', text: `${results.length} read` });
|
|
57
|
+
done();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
node.status({ fill: 'red', shape: 'ring', text: error.message });
|
|
60
|
+
done(error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
RED.nodes.registerType('tier0-opcua-read', OpcUaReadNode);
|
|
66
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.opcua-advanced { margin: 8px 0 12px; }
|
|
3
|
+
.opcua-advanced summary { cursor: pointer; font-weight: 600; padding: 6px 0; }
|
|
4
|
+
.opcua-standard-note { margin: 0 0 10px 105px; color: var(--red-ui-form-text-color, #666); font-size: 0.9em; }
|
|
5
|
+
</style>
|
|
6
|
+
<script type="text/html" data-template-name="tier0-opcua-subscribe">
|
|
7
|
+
<div class="form-row"><label for="node-input-connection">Connection</label><input type="text" id="node-input-connection"></div>
|
|
8
|
+
<div class="form-row"><label for="node-input-nodeId">NodeId</label><input type="text" id="node-input-nodeId" placeholder="Optional when input supplies points"></div>
|
|
9
|
+
<div class="form-row"><label for="node-input-samplingInterval">Sampling (ms)</label><input type="number" id="node-input-samplingInterval"></div>
|
|
10
|
+
<div class="opcua-standard-note">Standard mode batches changes and automatically rebuilds the subscription after reconnect.</div>
|
|
11
|
+
<details class="opcua-advanced">
|
|
12
|
+
<summary>Advanced subscription and queue settings</summary>
|
|
13
|
+
<div class="form-row"><label for="node-input-publishingInterval">Publishing interval</label><input type="number" id="node-input-publishingInterval"></div>
|
|
14
|
+
<div class="form-row"><label for="node-input-queueSize">Server queue size</label><input type="number" id="node-input-queueSize"></div>
|
|
15
|
+
<div class="form-row"><label for="node-input-discardOldest">Discard oldest</label><input type="checkbox" id="node-input-discardOldest" style="width:auto"></div>
|
|
16
|
+
<div class="form-row"><label for="node-input-deadbandType">Deadband</label><select id="node-input-deadbandType"><option value="none">None</option><option value="absolute">Absolute</option><option value="percent">Percent</option></select></div>
|
|
17
|
+
<div class="form-row"><label for="node-input-deadbandValue">Deadband value</label><input type="number" id="node-input-deadbandValue"></div>
|
|
18
|
+
<div class="form-row"><label for="node-input-batchSize">Create batch size</label><input type="number" id="node-input-batchSize"></div>
|
|
19
|
+
<div class="form-row"><label for="node-input-outputMode">Change output</label><select id="node-input-outputMode"><option value="batch">Native batch</option><option value="single">One message per change</option></select></div>
|
|
20
|
+
<div class="form-row"><label for="node-input-callbackBatchSize">Native batch size</label><input type="number" id="node-input-callbackBatchSize"></div>
|
|
21
|
+
<div class="form-row"><label for="node-input-callbackQueueSize">Native queue capacity</label><input type="number" id="node-input-callbackQueueSize"></div>
|
|
22
|
+
<div class="form-row"><label for="node-input-queueMode">Queue mode</label><select id="node-input-queueMode"><option value="all">All changes</option><option value="latest">Latest value per point</option></select></div>
|
|
23
|
+
<div class="form-row"><label for="node-input-overflowPolicy">Overflow policy</label><select id="node-input-overflowPolicy"><option value="drop-oldest">Drop oldest</option><option value="drop-newest">Drop newest</option></select></div>
|
|
24
|
+
</details>
|
|
25
|
+
<div class="form-row"><label for="node-input-name">Name</label><input type="text" id="node-input-name"></div>
|
|
26
|
+
</script>
|
|
27
|
+
<script type="text/html" data-help-name="tier0-opcua-subscribe"><p>Creates one OPC UA subscription with many monitored items. Input may be an array, JSON, CSV, or Browse output. Commands are <code>replace</code>, <code>add</code>, <code>remove</code>, and <code>clear</code> in <code>msg.opcua.command</code>.</p><p>Standard mode preserves all changes in bounded native batches and reports queue diagnostics in <code>msg.opcua.queue</code>.</p></script>
|
|
28
|
+
<script type="text/javascript">
|
|
29
|
+
RED.nodes.registerType('tier0-opcua-subscribe', {
|
|
30
|
+
category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
|
|
31
|
+
defaults: {
|
|
32
|
+
connection: { value: '', type: 'tier0-opcua-connection', required: true }, nodeId: { value: '' },
|
|
33
|
+
publishingInterval: { value: 1000, validate: RED.validators.number() }, samplingInterval: { value: 1000, validate: RED.validators.number() },
|
|
34
|
+
queueSize: { value: 10, validate: RED.validators.number() }, discardOldest: { value: true },
|
|
35
|
+
deadbandType: { value: 'none' }, deadbandValue: { value: 0, validate: RED.validators.number() },
|
|
36
|
+
batchSize: { value: 500, validate: RED.validators.number() }, outputMode: { value: 'batch' },
|
|
37
|
+
callbackBatchSize: { value: 500, validate: RED.validators.number() }, callbackQueueSize: { value: 10000, validate: RED.validators.number() },
|
|
38
|
+
queueMode: { value: 'all' }, overflowPolicy: { value: 'drop-oldest' },
|
|
39
|
+
lifetimeCount: { value: 120 }, maxKeepAliveCount: { value: 10 }, maxNotificationsPerPublish: { value: 0 },
|
|
40
|
+
reconnectInterval: { value: 5000 }, name: { value: '' }
|
|
41
|
+
},
|
|
42
|
+
label: function () { return this.name || 'tier0-opcua-subscribe'; }
|
|
43
|
+
});
|
|
44
|
+
</script>
|