@tier0/opcua-client 0.1.0 → 0.2.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 (45) hide show
  1. package/BENCHMARK.md +53 -2
  2. package/README.md +131 -14
  3. package/THIRD_PARTY_NOTICES.md +20 -0
  4. package/binding.gyp +1 -1
  5. package/lib/client.js +48 -7
  6. package/lib/native.js +25 -10
  7. package/lib/points.js +187 -21
  8. package/lib/prebuild.js +42 -0
  9. package/lib/security.js +96 -0
  10. package/node_modules/node-addon-api/LICENSE.md +9 -0
  11. package/node_modules/node-addon-api/README.md +95 -0
  12. package/node_modules/node-addon-api/common.gypi +21 -0
  13. package/node_modules/node-addon-api/except.gypi +25 -0
  14. package/node_modules/node-addon-api/index.js +14 -0
  15. package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  16. package/node_modules/node-addon-api/napi-inl.h +7244 -0
  17. package/node_modules/node-addon-api/napi.h +3420 -0
  18. package/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  19. package/node_modules/node-addon-api/node_api.gyp +9 -0
  20. package/node_modules/node-addon-api/noexcept.gypi +26 -0
  21. package/node_modules/node-addon-api/nothing.c +0 -0
  22. package/node_modules/node-addon-api/package-support.json +21 -0
  23. package/node_modules/node-addon-api/package.json +481 -0
  24. package/node_modules/node-addon-api/tools/README.md +73 -0
  25. package/node_modules/node-addon-api/tools/check-napi.js +99 -0
  26. package/node_modules/node-addon-api/tools/clang-format.js +71 -0
  27. package/node_modules/node-addon-api/tools/conversion.js +301 -0
  28. package/nodes/connection.html +165 -6
  29. package/nodes/connection.js +99 -20
  30. package/nodes/read.html +3 -0
  31. package/nodes/read.js +3 -1
  32. package/nodes/subscribe.html +9 -3
  33. package/nodes/subscribe.js +330 -99
  34. package/nodes/write.html +3 -0
  35. package/package.json +15 -4
  36. package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
  37. package/scripts/build-offline-prebuild.sh +15 -0
  38. package/scripts/build-open62541.sh +14 -4
  39. package/scripts/build-test-server.sh +23 -0
  40. package/scripts/install.js +28 -0
  41. package/scripts/verify-offline-package.js +20 -0
  42. package/scripts/verify-runtime.js +20 -0
  43. package/src/addon.cc +707 -119
  44. package/vendor/open62541-LICENSE +373 -0
  45. package/vendor/open62541-v1.4.16.tar.gz +0 -0
@@ -2,24 +2,34 @@
2
2
  .opcua-advanced { margin: 8px 0 12px; }
3
3
  .opcua-advanced summary { cursor: pointer; font-weight: 600; padding: 6px 0; }
4
4
  .opcua-standard-note { margin: 0 0 10px 105px; color: var(--red-ui-form-text-color, #666); font-size: 0.9em; }
5
+ .opcua-discovery-result { margin: 6px 0 8px 105px; max-width: 70%; }
6
+ .opcua-discovery-result select { width: 100%; }
5
7
  </style>
6
8
  <script type="text/html" data-template-name="tier0-opcua-connection">
7
9
  <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>
10
+ <div class="form-row"><label></label><button type="button" class="red-ui-button" id="node-config-input-discoverEndpoints">Discover endpoints</button> <button type="button" class="red-ui-button" id="node-config-input-testConnection">Test connection</button></div>
11
+ <div class="opcua-discovery-result" id="node-config-input-discoveryResult" style="display:none"><select id="node-config-input-discoveredEndpoint"></select></div>
12
+ <div class="opcua-standard-note" id="node-config-input-testResult"></div>
8
13
  <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>
14
+ <div class="opcua-standard-note">Standard defaults include automatic reconnect, connectivity checks, and bounded command/NodeId queues.</div>
10
15
  <details class="opcua-advanced">
11
16
  <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>
17
+ <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></select></div>
13
18
  <div class="form-row"><label for="node-config-input-user">User</label><input type="text" id="node-config-input-user"></div>
14
19
  <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>
20
+ <div class="form-row opcua-secure-field"><label for="node-config-input-certificate">Client certificate</label><input type="text" id="node-config-input-certificate" placeholder="/path/to/client.der"></div>
21
+ <div class="form-row opcua-secure-field"><label for="node-config-input-privateKey">Client private key</label><input type="text" id="node-config-input-privateKey" placeholder="/path/to/client-key.pem"></div>
22
+ <div class="form-row opcua-secure-field"><label for="node-config-input-privateKeyPassword">Key password</label><input type="password" id="node-config-input-privateKeyPassword"></div>
23
+ <div class="form-row opcua-secure-field"><label for="node-config-input-applicationUri">Application URI</label><input type="text" id="node-config-input-applicationUri" placeholder="Must match the client certificate"></div>
24
+ <div class="form-row opcua-secure-field"><label for="node-config-input-trustedCertificates">Trusted certificates</label><textarea id="node-config-input-trustedCertificates" rows="3" placeholder="One Server or CA certificate path per line"></textarea></div>
25
+ <div class="form-row opcua-secure-field"><label for="node-config-input-revocationList">Revocation list</label><textarea id="node-config-input-revocationList" rows="2" placeholder="Optional CRL path, one per line"></textarea></div>
17
26
  <div class="form-row"><label for="node-config-input-requestedSessionTimeout">Session timeout</label><input type="number" id="node-config-input-requestedSessionTimeout"></div>
18
27
  <div class="form-row"><label for="node-config-input-requestTimeout">Request timeout</label><input type="number" id="node-config-input-requestTimeout"></div>
19
28
  <div class="form-row"><label for="node-config-input-connectivityCheckInterval">Connectivity check</label><input type="number" id="node-config-input-connectivityCheckInterval"></div>
20
29
  <div class="form-row"><label for="node-config-input-reconnectInterval">Reconnect interval</label><input type="number" id="node-config-input-reconnectInterval"></div>
21
30
  <div class="form-row"><label for="node-config-input-maxReconnectInterval">Max reconnect</label><input type="number" id="node-config-input-maxReconnectInterval"></div>
22
31
  <div class="form-row"><label for="node-config-input-nodeIdCacheSize">NodeId cache</label><input type="number" id="node-config-input-nodeIdCacheSize"></div>
32
+ <div class="form-row"><label for="node-config-input-commandQueueSize">Command queue</label><input type="number" id="node-config-input-commandQueueSize"></div>
23
33
  </details>
24
34
  <div class="form-row"><label for="node-config-input-name">Name</label><input type="text" id="node-config-input-name"></div>
25
35
  </script>
@@ -30,15 +40,164 @@
30
40
  endpoint: { value: '', required: true },
31
41
  securityMode: { value: 'None' }, securityPolicy: { value: 'None' },
32
42
  user: { value: '' }, certificate: { value: '' }, privateKey: { value: '' },
43
+ applicationUri: { value: '' }, trustedCertificates: { value: '' }, revocationList: { value: '' },
33
44
  requestedSessionTimeout: { value: 60000, validate: RED.validators.number() },
34
45
  requestTimeout: { value: 10000, validate: RED.validators.number() },
35
46
  connectivityCheckInterval: { value: 5000, validate: RED.validators.number() },
36
47
  reconnectInterval: { value: 5000, validate: RED.validators.number() },
37
48
  maxReconnectInterval: { value: 30000, validate: RED.validators.number() },
38
49
  nodeIdCacheSize: { value: 100000, validate: RED.validators.number() },
50
+ commandQueueSize: { value: 256, validate: RED.validators.number() },
39
51
  name: { value: '' }
40
52
  },
41
- credentials: { password: { type: 'password' } },
42
- label: function () { return this.name || this.endpoint || 'tier0-opcua-connection'; }
53
+ credentials: {
54
+ password: { type: 'password' },
55
+ privateKeyPassword: { type: 'password' }
56
+ },
57
+ label: function () { return this.name || this.endpoint || 'tier0-opcua-connection'; },
58
+ oneditprepare: function () {
59
+ const connectionId = this.id;
60
+ const mode = $('#node-config-input-securityMode');
61
+ const policy = $('#node-config-input-securityPolicy');
62
+ const result = $('#node-config-input-discoveryResult');
63
+ const discovered = $('#node-config-input-discoveredEndpoint');
64
+ const policyName = function (uri) {
65
+ return String(uri || '').split('#').pop() || 'None';
66
+ };
67
+ const updateSecurityFields = function () {
68
+ const secure = mode.val() !== 'None';
69
+ $('.opcua-secure-field').toggle(secure);
70
+ if (!secure) policy.val('None');
71
+ else if (policy.val() === 'None') policy.val('Basic256Sha256');
72
+ };
73
+ mode.on('change', updateSecurityFields);
74
+ updateSecurityFields();
75
+
76
+ const connectionSettings = function () {
77
+ return {
78
+ connectionId,
79
+ endpoint: String($('#node-config-input-endpoint').val() || '').trim(),
80
+ securityMode: mode.val(),
81
+ securityPolicy: policy.val(),
82
+ user: $('#node-config-input-user').val(),
83
+ password: $('#node-config-input-password').val(),
84
+ certificate: $('#node-config-input-certificate').val(),
85
+ privateKey: $('#node-config-input-privateKey').val(),
86
+ privateKeyPassword: $('#node-config-input-privateKeyPassword').val(),
87
+ applicationUri: $('#node-config-input-applicationUri').val(),
88
+ trustedCertificates: $('#node-config-input-trustedCertificates').val(),
89
+ revocationList: $('#node-config-input-revocationList').val(),
90
+ requestedSessionTimeout: $('#node-config-input-requestedSessionTimeout').val(),
91
+ requestTimeout: $('#node-config-input-requestTimeout').val(),
92
+ connectivityCheckInterval: $('#node-config-input-connectivityCheckInterval').val(),
93
+ reconnectInterval: $('#node-config-input-reconnectInterval').val(),
94
+ maxReconnectInterval: $('#node-config-input-maxReconnectInterval').val(),
95
+ nodeIdCacheSize: $('#node-config-input-nodeIdCacheSize').val(),
96
+ commandQueueSize: $('#node-config-input-commandQueueSize').val()
97
+ };
98
+ };
99
+
100
+ $('#node-config-input-discoverEndpoints').on('click', function () {
101
+ const button = $(this);
102
+ const endpoint = String($('#node-config-input-endpoint').val() || '').trim();
103
+ if (!endpoint) {
104
+ RED.notify('Enter an OPC UA endpoint first', 'warning');
105
+ return;
106
+ }
107
+ button.prop('disabled', true).text('Discovering...');
108
+ $.ajax({
109
+ url: 'opcua-client/endpoints',
110
+ method: 'POST',
111
+ contentType: 'application/json',
112
+ data: JSON.stringify({
113
+ endpoint,
114
+ requestTimeout: Number($('#node-config-input-requestTimeout').val()) || 10000
115
+ })
116
+ }).done(function (response) {
117
+ const allEndpoints = Array.isArray(response.endpoints) ? response.endpoints : [];
118
+ const identityType = String($('#node-config-input-user').val() || '').trim()
119
+ ? 'Username' : 'Anonymous';
120
+ const supported = allEndpoints.filter(function (item) {
121
+ const itemPolicy = policyName(item.securityPolicyUri);
122
+ return itemPolicy === 'None' || itemPolicy === 'Basic256Sha256';
123
+ });
124
+ const matchingIdentity = supported.filter(function (item) {
125
+ return (item.userIdentityTokens || []).some(function (token) {
126
+ return token.tokenType === identityType;
127
+ });
128
+ });
129
+ const endpoints = matchingIdentity.length ? matchingIdentity : supported;
130
+ discovered.empty();
131
+ endpoints.sort(function (left, right) {
132
+ return Number(right.securityLevel || 0) - Number(left.securityLevel || 0);
133
+ }).forEach(function (item, index) {
134
+ const identities = (item.userIdentityTokens || []).map(function (token) {
135
+ return token.tokenType;
136
+ }).filter(function (value, tokenIndex, values) {
137
+ return values.indexOf(value) === tokenIndex;
138
+ }).join(', ');
139
+ $('<option></option>')
140
+ .val(String(index))
141
+ .text(`${item.securityMode} / ${policyName(item.securityPolicyUri)} / ${identities || 'No identity token'}`)
142
+ .data('endpoint', item)
143
+ .appendTo(discovered);
144
+ });
145
+ if (!endpoints.length) {
146
+ RED.notify('The server returned no supported None or Basic256Sha256 endpoint', 'warning');
147
+ result.hide();
148
+ return;
149
+ }
150
+ if (!matchingIdentity.length) {
151
+ RED.notify(`No endpoint advertises ${identityType}; showing other supported endpoints`, 'warning');
152
+ }
153
+ result.show();
154
+ discovered.trigger('change');
155
+ }).fail(function (xhr) {
156
+ RED.notify(xhr.responseJSON?.error || 'Endpoint discovery failed', 'error');
157
+ result.hide();
158
+ }).always(function () {
159
+ button.prop('disabled', false).text('Discover endpoints');
160
+ });
161
+ });
162
+
163
+ discovered.on('change', function () {
164
+ const selected = discovered.find(':selected').data('endpoint');
165
+ if (!selected) return;
166
+ const selectedPolicy = policyName(selected.securityPolicyUri);
167
+ mode.val(selected.securityMode);
168
+ if (selectedPolicy === 'None' || selectedPolicy === 'Basic256Sha256') {
169
+ policy.val(selectedPolicy);
170
+ }
171
+ updateSecurityFields();
172
+ });
173
+
174
+ $('#node-config-input-testConnection').on('click', function () {
175
+ const button = $(this);
176
+ const testResult = $('#node-config-input-testResult');
177
+ const settings = connectionSettings();
178
+ if (!settings.endpoint) {
179
+ RED.notify('Enter an OPC UA endpoint first', 'warning');
180
+ return;
181
+ }
182
+ button.prop('disabled', true).text('Testing...');
183
+ testResult.text('');
184
+ $.ajax({
185
+ url: 'opcua-client/test-connection',
186
+ method: 'POST',
187
+ contentType: 'application/json',
188
+ data: JSON.stringify(settings)
189
+ }).done(function (response) {
190
+ const connection = response.connection || {};
191
+ testResult.text(`Connected: ${connection.securityMode || settings.securityMode} / ${policyName(connection.securityPolicyUri)}`);
192
+ RED.notify('OPC UA connection test succeeded', 'success');
193
+ }).fail(function (xhr) {
194
+ const message = xhr.responseJSON?.error || 'OPC UA connection test failed';
195
+ testResult.text(message);
196
+ RED.notify(message, 'error');
197
+ }).always(function () {
198
+ button.prop('disabled', false).text('Test connection');
199
+ });
200
+ });
201
+ }
43
202
  });
44
203
  </script>
@@ -1,31 +1,109 @@
1
1
  'use strict';
2
2
 
3
- const { OpcUaClient } = require('../lib/client');
3
+ const { OpcUaClient, discoverEndpoints } = require('../lib/client');
4
+
5
+ function pathList(value) {
6
+ const source = Array.isArray(value) ? value : String(value || '').split(/\r?\n|;/);
7
+ return source.map((entry) => String(entry || '').trim()).filter(Boolean);
8
+ }
9
+
10
+ function clientOptions(config, credentials = {}) {
11
+ return {
12
+ endpoint: config.endpoint,
13
+ securityMode: config.securityMode || 'None',
14
+ securityPolicy: config.securityPolicy || 'None',
15
+ user: config.user || '',
16
+ password: credentials.password || '',
17
+ certificate: config.certificate || '',
18
+ privateKey: config.privateKey || '',
19
+ privateKeyPassword: credentials.privateKeyPassword || '',
20
+ applicationUri: config.applicationUri || '',
21
+ trustedCertificates: pathList(config.trustedCertificates),
22
+ revocationList: pathList(config.revocationList),
23
+ requestedSessionTimeout: Number(config.requestedSessionTimeout) || 60000,
24
+ requestTimeout: Number(config.requestTimeout) || 10000,
25
+ connectivityCheckInterval: Number.isFinite(Number(config.connectivityCheckInterval))
26
+ ? Math.max(0, Math.floor(Number(config.connectivityCheckInterval)))
27
+ : 5000,
28
+ reconnectInterval: Number(config.reconnectInterval) || 5000,
29
+ maxReconnectInterval: Number(config.maxReconnectInterval) || 30000,
30
+ nodeIdCacheSize: Number.isFinite(Number(config.nodeIdCacheSize))
31
+ ? Math.max(0, Math.floor(Number(config.nodeIdCacheSize)))
32
+ : 100000,
33
+ commandQueueSize: Number.isFinite(Number(config.commandQueueSize))
34
+ ? Math.max(1, Math.floor(Number(config.commandQueueSize)))
35
+ : 256,
36
+ };
37
+ }
4
38
 
5
39
  module.exports = function registerConnectionNode(RED) {
40
+ RED.httpAdmin.post(
41
+ '/opcua-client/endpoints',
42
+ RED.auth.needsPermission('opcua-client.discovery'),
43
+ async (req, res) => {
44
+ try {
45
+ const endpoints = await discoverEndpoints({
46
+ endpoint: req.body?.endpoint,
47
+ requestTimeout: req.body?.requestTimeout,
48
+ });
49
+ res.send({ endpoints });
50
+ } catch (error) {
51
+ res.status(error?.code === 'OPCUA_RECONNECTING' ? 503 : 400).send({
52
+ error: error.message || String(error),
53
+ code: error.code || 'OPCUA_DISCOVERY_FAILED',
54
+ });
55
+ }
56
+ }
57
+ );
58
+
59
+ RED.httpAdmin.post(
60
+ '/opcua-client/test-connection',
61
+ RED.auth.needsPermission('opcua-client.test'),
62
+ async (req, res) => {
63
+ const body = req.body || {};
64
+ const deployed = body.connectionId ? RED.nodes.getNode(String(body.connectionId)) : null;
65
+ const deployedOptions = deployed?.client?.options || {};
66
+ const suppliedPassword = body.password && body.password !== '__PWRD__'
67
+ ? String(body.password) : deployedOptions.password;
68
+ const suppliedPrivateKeyPassword = body.privateKeyPassword &&
69
+ body.privateKeyPassword !== '__PWRD__'
70
+ ? String(body.privateKeyPassword) : deployedOptions.privateKeyPassword;
71
+ const options = {
72
+ ...deployedOptions,
73
+ ...clientOptions(body, {
74
+ password: suppliedPassword,
75
+ privateKeyPassword: suppliedPrivateKeyPassword,
76
+ }),
77
+ };
78
+ const client = new OpcUaClient(options);
79
+ client.on('fault', () => {});
80
+ try {
81
+ await client.connect();
82
+ const diagnostics = client.getDiagnostics();
83
+ const state = client.getState();
84
+ await client.disconnect();
85
+ res.send({
86
+ connected: true,
87
+ state,
88
+ connection: diagnostics.connection || {},
89
+ operationLimits: diagnostics.operationLimits || {},
90
+ });
91
+ } catch (error) {
92
+ try { await client.disconnect(); } catch {}
93
+ res.status(400).send({
94
+ connected: false,
95
+ error: error.message || String(error),
96
+ code: error.code || 'OPCUA_CONNECTION_TEST_FAILED',
97
+ });
98
+ }
99
+ }
100
+ );
101
+
6
102
  function OpcUaConnectionNode(config) {
7
103
  RED.nodes.createNode(this, config);
8
104
  const node = this;
9
105
 
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
- });
106
+ node.client = new OpcUaClient(clientOptions(config, node.credentials));
29
107
 
30
108
  node.client.on('fault', (error) => node.warn(`OPC UA connection fault: ${error.message}`));
31
109
  node.client.on('state', (state) => {
@@ -44,6 +122,7 @@ module.exports = function registerConnectionNode(RED) {
44
122
  RED.nodes.registerType('tier0-opcua-connection', OpcUaConnectionNode, {
45
123
  credentials: {
46
124
  password: { type: 'password' },
125
+ privateKeyPassword: { type: 'password' },
47
126
  },
48
127
  });
49
128
  };
package/nodes/read.html CHANGED
@@ -36,6 +36,7 @@
36
36
  </div>
37
37
  <details class="opcua-advanced">
38
38
  <summary>Advanced settings</summary>
39
+ <div class="form-row"><label for="node-input-indexRange">Index range</label><input type="text" id="node-input-indexRange" placeholder="For example 0:9 or 0:1,2:3"></div>
39
40
  <div class="form-row"><label for="node-input-batchSize">Batch size</label><input type="number" id="node-input-batchSize"></div>
40
41
  <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
42
  </details>
@@ -44,6 +45,7 @@
44
45
  <script type="text/html" data-help-name="tier0-opcua-read">
45
46
  <p>Add NodeIds to the point list or import JSON/CSV. Imported points are parsed and shown in the same list.</p>
46
47
  <p>CSV requires a <code>nodeId</code> header. A JSON/CSV point list may also arrive dynamically in <code>msg.payload</code>.</p>
48
+ <p>Use <code>indexRange</code> (or legacy <code>range</code>) per point or in <code>msg.indexRange</code> to read part of an array or matrix.</p>
47
49
  </script>
48
50
  <script type="text/javascript">
49
51
  RED.nodes.registerType('tier0-opcua-read', {
@@ -51,6 +53,7 @@
51
53
  defaults: {
52
54
  connection: { value: '', type: 'tier0-opcua-connection', required: true },
53
55
  nodeId: { value: '' }, pointList: { value: '' }, pointListFileName: { value: '' },
56
+ indexRange: { value: '' },
54
57
  batchSize: { value: 500, validate: RED.validators.number() },
55
58
  outputMode: { value: 'batch' }, name: { value: '' }
56
59
  },
package/nodes/read.js CHANGED
@@ -12,7 +12,9 @@ module.exports = function registerReadNode(RED) {
12
12
  if (!node.connection) return done(new Error('OPC UA connection not configured'));
13
13
 
14
14
  try {
15
- const points = messagePoints(msg, config.nodeId, {}, {
15
+ const points = messagePoints(msg, config.nodeId, {
16
+ indexRange: config.indexRange,
17
+ }, {
16
18
  configuredPayload: config.pointList,
17
19
  format: 'auto',
18
20
  });
@@ -7,7 +7,7 @@
7
7
  <div class="form-row"><label for="node-input-connection">Connection</label><input type="text" id="node-input-connection"></div>
8
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
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>
10
+ <div class="opcua-standard-note">Standard mode shards large point sets across subscriptions, batches changes, and automatically restores them after reconnect.</div>
11
11
  <details class="opcua-advanced">
12
12
  <summary>Advanced subscription and queue settings</summary>
13
13
  <div class="form-row"><label for="node-input-publishingInterval">Publishing interval</label><input type="number" id="node-input-publishingInterval"></div>
@@ -15,16 +15,19 @@
15
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
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
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-timestampMode">Timestamps</label><select id="node-input-timestampMode"><option value="source">Source</option><option value="both">Source and server</option><option value="server">Server</option><option value="neither">None</option></select></div>
18
19
  <div class="form-row"><label for="node-input-batchSize">Create batch size</label><input type="number" id="node-input-batchSize"></div>
20
+ <div class="form-row"><label for="node-input-maxItemsPerSubscription">Items / subscription</label><input type="number" id="node-input-maxItemsPerSubscription"></div>
19
21
  <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>
22
+ <div class="form-row"><label for="node-input-callbackBatchSize">Output batch size</label><input type="number" id="node-input-callbackBatchSize"></div>
23
+ <div class="form-row"><label for="node-input-callbackFlushIntervalMs">Batch flush (ms)</label><input type="number" id="node-input-callbackFlushIntervalMs"></div>
21
24
  <div class="form-row"><label for="node-input-callbackQueueSize">Native queue capacity</label><input type="number" id="node-input-callbackQueueSize"></div>
22
25
  <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
26
  <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
27
  </details>
25
28
  <div class="form-row"><label for="node-input-name">Name</label><input type="text" id="node-input-name"></div>
26
29
  </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>
30
+ <script type="text/html" data-help-name="tier0-opcua-subscribe"><p>Creates one or more OPC UA subscriptions for 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>The node automatically honors the server subscription limit when available. Batch output flushes when its size threshold is reached or its time window expires. Queue diagnostics are reported in <code>msg.opcua.queue</code>.</p></script>
28
31
  <script type="text/javascript">
29
32
  RED.nodes.registerType('tier0-opcua-subscribe', {
30
33
  category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
@@ -33,8 +36,11 @@
33
36
  publishingInterval: { value: 1000, validate: RED.validators.number() }, samplingInterval: { value: 1000, validate: RED.validators.number() },
34
37
  queueSize: { value: 10, validate: RED.validators.number() }, discardOldest: { value: true },
35
38
  deadbandType: { value: 'none' }, deadbandValue: { value: 0, validate: RED.validators.number() },
39
+ timestampMode: { value: 'source' },
36
40
  batchSize: { value: 500, validate: RED.validators.number() }, outputMode: { value: 'batch' },
41
+ maxItemsPerSubscription: { value: 5000, validate: RED.validators.number() },
37
42
  callbackBatchSize: { value: 500, validate: RED.validators.number() }, callbackQueueSize: { value: 10000, validate: RED.validators.number() },
43
+ callbackFlushIntervalMs: { value: 50, validate: RED.validators.number() },
38
44
  queueMode: { value: 'all' }, overflowPolicy: { value: 'drop-oldest' },
39
45
  lifetimeCount: { value: 120 }, maxKeepAliveCount: { value: 10 }, maxNotificationsPerPublish: { value: 0 },
40
46
  reconnectInterval: { value: 5000 }, name: { value: '' }