@tier0/opcua-client 0.5.0 → 0.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.
package/README.md CHANGED
@@ -178,19 +178,34 @@ finishes.
178
178
 
179
179
  Node editors show only the standard settings by default. Connection security/timeouts/cache, Browse limits/paging, Read/Write batching/output, and Subscribe queue/deadband tuning remain available under a closed **Advanced settings** section. Existing flows retain their stored advanced values.
180
180
 
181
+ ## Server compatibility profiles
182
+
183
+ Connection nodes default to the standard OPC UA profile. Select **Siemens
184
+ WinCC** for classic WinCC or Unified servers that do not advertise usable
185
+ OperationLimits: Read, Write, and MonitoredItem service calls then use a
186
+ conservative fallback of 200 items per request, while advertised non-zero
187
+ server limits still take precedence. Subscribe nodes use at most 1000
188
+ MonitoredItems per Subscription and remove Deadband filters because WinCC does
189
+ not support `ModifyMonitoredItems`/Deadband behavior consistently. The selected
190
+ profile and effective subscription behavior are included in status diagnostics.
191
+
181
192
  ## Secure connections
182
193
 
183
194
  Connection nodes support `None/None` and `Sign` or `SignAndEncrypt` with
184
- `Basic256Sha256`. Secure connections require a client certificate, its private
185
- key, and at least one trusted Server or CA certificate. Trust is strict: an
195
+ `Basic256Sha256`, `Aes128_Sha256_RsaOaep`, or `Aes256_Sha256_RsaPss`. Secure
196
+ connections require a client certificate, its private key, and at least one
197
+ trusted Server or CA certificate. Trust is strict: an
186
198
  empty trust list is rejected instead of silently accepting every Server
187
199
  certificate. Optional CRL files can be supplied as a newline-separated list.
188
200
  Password-protected PEM private keys use the separate Node-RED credential field,
189
201
  so the password is not stored in the normal Flow configuration.
190
202
 
191
- The client Application URI can be configured when the URI embedded in the
192
- client certificate differs from the open62541 default. Username authentication
193
- uses the selected secure endpoint and its advertised UserTokenPolicy.
203
+ Before opening the native client, the package checks certificate validity,
204
+ certificate/private-key matching, RSA key type and minimum 2048-bit strength,
205
+ trusted-certificate validity, and Application URI consistency. When the client
206
+ certificate contains exactly one URI, leaving Application URI empty selects it
207
+ automatically. Username authentication uses the selected secure endpoint and
208
+ its advertised UserTokenPolicy.
194
209
 
195
210
  The Connection editor can call `GetEndpoints`, filters the response to the
196
211
  implemented policies, and selects an endpoint compatible with Anonymous or
@@ -212,7 +227,7 @@ Node-RED Alpine image, the pinned open62541 v1.4.16 source archive, and its
212
227
  not contact npm or GitHub and does not invoke a compiler:
213
228
 
214
229
  ```bash
215
- npm install ./tier0-opcua-client-0.5.0.tgz --offline
230
+ npm install ./tier0-opcua-client-0.6.0.tgz --offline
216
231
  ```
217
232
 
218
233
  The loader uses the bundled native addon directly. Set
@@ -332,3 +347,56 @@ See [BENCHMARK.md](BENCHMARK.md) for the recorded 1,000, 5,000, 10,000, and sust
332
347
  - Native command queue: 256 pending commands per connection.
333
348
 
334
349
  Configured service batches are automatically clamped to the server's advertised OperationLimits.
350
+
351
+ ## Status and error output (0.6.1)
352
+
353
+ Browse, Read, Write and Subscribe now have two outputs. Output 1 retains data;
354
+ output 2 emits structured diagnostic messages. Connect output 2 to Debug, a
355
+ JSON/file node or your logging service. The shared Connection configuration
356
+ has no output ports: its state is reported by each referencing operation node,
357
+ identified by nodeId and connectionId.
358
+
359
+ 诊断输出可独立接 Debug、JSON + File 或日志服务。payload.level 为 info / warn / error;
360
+ 连接断开、订阅无响应、恢复失败、点位 Bad 状态和队列丢数据都会输出对应事件。
361
+ 恢复成功也会输出 info,但连接恢复或订阅重建不代表已经收到新数据。
362
+
363
+ Messages contain payload.level, event, message, time (ISO 8601), nodeId,
364
+ connectionId, operation, retrying and optional statusCode/counts. Subscription
365
+ recovery includes rebuilt and dataGapPossible. The envelope contains
366
+ opcua.operation = "diagnostic". No input values, endpoint URLs, credentials,
367
+ certificates or raw SDK error text are copied to this output. Existing runtime
368
+ logs and Catch handling remain unchanged; output 2 is additional reporting.
369
+
370
+ Identical event/status-code pairs are emitted at most once per 30 seconds per
371
+ node. The next emitted matching event includes suppressed repeats; there is
372
+ no delayed timer flush. Connection restoration resets this suppression window.
373
+ This limit applies only to diagnostic output, not the existing runtime logs.
374
+
375
+ For Subscribe, **Status messages → Both outputs (legacy compatible)** is the
376
+ default: existing subscription-status messages remain on output 1 and the new
377
+ structured events appear on output 2. Choose **Output 2 only** to keep output 1
378
+ strictly for data. Existing Flow JSON without statusOutput keeps its old behavior;
379
+ all existing wires on output 1 remain unchanged. New diagnostic events are not
380
+ sent to output 1. Structured events contain summaries; existing detailed
381
+ subscription-status payloads are preserved only in compatibility mode.
382
+
383
+ This change adds reporting; it does not add a data-silence watchdog or change
384
+ OPC UA reconnection/monitoring behavior.
385
+
386
+ ## Point failure details (0.6.2)
387
+
388
+ Diagnostic output now includes payload.failures: an array of nodeId (OPC UA
389
+ point identifier), optional alias, statusCode and a short explanation. The
390
+ outer payload.nodeId still identifies the Node-RED flow node. Read/Write
391
+ partial failures display yellow status with success/total and failure counts.
392
+ The detail list is available even when data output is set to Value only.
393
+
394
+ Each event includes at most 100 failed points; failed reports the full failure
395
+ count, failuresTruncated indicates truncation, and omittedFailures reports the
396
+ remaining count. Use Full metadata on output 1 for complete Read/Write results.
397
+ Subscription compatibility output also retains its existing bounded failures.
398
+ Duplicate event suppression compares the failing point identities and status
399
+ codes so a different batch of failing points is not hidden by the first batch.
400
+ No point values or raw input messages are copied. A whole-service exception
401
+ without per-point results is still an operation failure: it cannot identify
402
+ which individual points failed.
package/lib/client.js CHANGED
@@ -3,7 +3,8 @@
3
3
  const { EventEmitter } = require('events');
4
4
  const { chunks } = require('./points');
5
5
  const { loadAddon } = require('./native');
6
- const { normalizeConnectionOptions } = require('./security');
6
+ const { preflightConnectionSecurity } = require('./security');
7
+ const { SIEMENS_WINCC_DEFAULTS, isSiemensWinCC } = require('./compatibility');
7
8
 
8
9
  function normalizeNotificationBatch(notification) {
9
10
  if (notification && Array.isArray(notification.changes)) {
@@ -16,13 +17,17 @@ function normalizeNotificationBatch(notification) {
16
17
  };
17
18
  }
18
19
 
19
- function effectiveBatchSize(requested, serverLimit) {
20
+ function effectiveBatchSize(requested, serverLimit, fallbackLimit) {
20
21
  const configured = Number.isFinite(Number(requested)) && Number(requested) > 0
21
22
  ? Math.floor(Number(requested))
22
23
  : 500;
23
24
  const limit = Number(serverLimit);
24
- return Number.isFinite(limit) && limit > 0
25
- ? Math.min(configured, Math.floor(limit))
25
+ if (Number.isFinite(limit) && limit > 0) {
26
+ return Math.min(configured, Math.floor(limit));
27
+ }
28
+ const fallback = Number(fallbackLimit);
29
+ return Number.isFinite(fallback) && fallback > 0
30
+ ? Math.min(configured, Math.floor(fallback))
26
31
  : configured;
27
32
  }
28
33
 
@@ -55,6 +60,7 @@ class OpcUaClient extends EventEmitter {
55
60
  operationLimits: {},
56
61
  nodeIdCache: {},
57
62
  connection: {},
63
+ compatibility: this.#compatibilityDiagnostics(),
58
64
  };
59
65
  }
60
66
 
@@ -85,7 +91,8 @@ class OpcUaClient extends EventEmitter {
85
91
  }
86
92
 
87
93
  async #connect() {
88
- const options = normalizeConnectionOptions(this.options);
94
+ const options = preflightConnectionSecurity(this.options);
95
+ this.diagnostics.securityPreflight = options.securityPreflight;
89
96
  const addon = loadAddon();
90
97
  const token = ++this.connectionToken;
91
98
  this.connectionState = 'connecting';
@@ -192,7 +199,8 @@ class OpcUaClient extends EventEmitter {
192
199
  const results = [];
193
200
  await this.connect();
194
201
  const effective = effectiveBatchSize(
195
- batchSize, this.diagnostics.operationLimits?.maxNodesPerRead
202
+ batchSize, this.diagnostics.operationLimits?.maxNodesPerRead,
203
+ this.#compatibilityBatchFallback()
196
204
  );
197
205
  for (const batch of chunks(points, effective)) {
198
206
  results.push(...await this.#invoke('readManyAsync', batch));
@@ -204,7 +212,8 @@ class OpcUaClient extends EventEmitter {
204
212
  const results = [];
205
213
  await this.connect();
206
214
  const effective = effectiveBatchSize(
207
- batchSize, this.diagnostics.operationLimits?.maxNodesPerWrite
215
+ batchSize, this.diagnostics.operationLimits?.maxNodesPerWrite,
216
+ this.#compatibilityBatchFallback()
208
217
  );
209
218
  for (const batch of chunks(points, effective)) {
210
219
  results.push(...await this.#invoke('writeManyAsync', batch));
@@ -222,7 +231,8 @@ class OpcUaClient extends EventEmitter {
222
231
  const results = [];
223
232
  await this.connect();
224
233
  const effective = effectiveBatchSize(
225
- batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall
234
+ batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall,
235
+ this.#compatibilityBatchFallback()
226
236
  );
227
237
  for (const batch of chunks(points, effective)) {
228
238
  results.push(...await this.#invoke(
@@ -236,7 +246,8 @@ class OpcUaClient extends EventEmitter {
236
246
  const results = [];
237
247
  await this.connect();
238
248
  const effective = effectiveBatchSize(
239
- batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall
249
+ batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall,
250
+ this.#compatibilityBatchFallback()
240
251
  );
241
252
  for (const batch of chunks(monitoredItemIds, effective)) {
242
253
  results.push(...await this.#invoke('removeMonitoredItemsAsync', subscriptionId, batch));
@@ -261,6 +272,22 @@ class OpcUaClient extends EventEmitter {
261
272
  return this.diagnostics;
262
273
  }
263
274
 
275
+ #compatibilityBatchFallback() {
276
+ return isSiemensWinCC(this.options.compatibilityProfile)
277
+ ? SIEMENS_WINCC_DEFAULTS.requestBatchSize
278
+ : 0;
279
+ }
280
+
281
+ #compatibilityDiagnostics() {
282
+ const siemensWincc = isSiemensWinCC(this.options.compatibilityProfile);
283
+ return {
284
+ profile: siemensWincc ? 'siemens-wincc' : 'standard',
285
+ requestBatchFallback: siemensWincc
286
+ ? SIEMENS_WINCC_DEFAULTS.requestBatchSize
287
+ : null,
288
+ };
289
+ }
290
+
264
291
  #refreshDiagnostics() {
265
292
  if (this.handle == null) return;
266
293
  const addon = loadAddon();
@@ -270,6 +297,8 @@ class OpcUaClient extends EventEmitter {
270
297
  this.diagnostics = {
271
298
  ...nativeDiagnostics,
272
299
  browse: this.diagnostics.browse,
300
+ securityPreflight: this.diagnostics.securityPreflight,
301
+ compatibility: this.#compatibilityDiagnostics(),
273
302
  connection: {
274
303
  ...(nativeDiagnostics.connection || {}),
275
304
  supervisor: { ...this.supervisorDiagnostics },
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const COMPATIBILITY_PROFILES = Object.freeze({
4
+ Standard: 'standard',
5
+ SiemensWinCC: 'siemens-wincc',
6
+ });
7
+
8
+ const SIEMENS_WINCC_DEFAULTS = Object.freeze({
9
+ requestBatchSize: 200,
10
+ monitoredItemsPerSubscription: 1000,
11
+ });
12
+
13
+ function normalizeCompatibilityProfile(value) {
14
+ return value === COMPATIBILITY_PROFILES.SiemensWinCC
15
+ ? COMPATIBILITY_PROFILES.SiemensWinCC
16
+ : COMPATIBILITY_PROFILES.Standard;
17
+ }
18
+
19
+ function isSiemensWinCC(value) {
20
+ return normalizeCompatibilityProfile(value) === COMPATIBILITY_PROFILES.SiemensWinCC;
21
+ }
22
+
23
+ module.exports = {
24
+ COMPATIBILITY_PROFILES,
25
+ SIEMENS_WINCC_DEFAULTS,
26
+ isSiemensWinCC,
27
+ normalizeCompatibilityProfile,
28
+ };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const FAILURE_LIMIT = 100;
5
+ const POINT_MESSAGES = {
6
+ BadNotReadable: '访问级别不允许读取或订阅该点位',
7
+ BadUserAccessDenied: '当前用户无权执行该操作',
8
+ BadNodeIdUnknown: '服务端找不到该 NodeId',
9
+ BadNodeIdInvalid: 'NodeId 格式无效',
10
+ BadNoCommunication: '服务端无法与该点位的数据源通信',
11
+ BadWaitingForInitialData: '等待该点位的首次数据',
12
+ BadTimeout: '该点位操作超时',
13
+ BadNotWritable: '该点位不允许写入',
14
+ BadTypeMismatch: '写入值类型与点位类型不匹配',
15
+ BadTooManyMonitoredItems: '服务端监控点数量超限',
16
+ };
17
+
18
+ // Output only allowlisted metadata: never forward SDK messages, credentials,
19
+ // endpoint URLs, input payloads or raw state/diagnostic objects.
20
+ function statusCode(error) {
21
+ const text = String(error?.code || error?.statusCode || error?.message || error || '');
22
+ return /\b(?:Bad[A-Za-z0-9]+|Uncertain[A-Za-z0-9]*|OPCUA_[A-Z_]+)\b/.exec(text)?.[0] || 'OperationFailed';
23
+ }
24
+
25
+ function createDiagnostics(node, config, operation) {
26
+ const recent = new Map();
27
+ let closed = false;
28
+ let interrupted = false;
29
+ const client = node.connection?.client;
30
+ function emit(level, event, message, details = {}, send) {
31
+ if (closed) return;
32
+ const now = Date.now();
33
+ const failures = details.failures;
34
+ // Different failing points must not be hidden by another batch's throttle.
35
+ const fingerprint = createHash('sha256');
36
+ if (failures) for (const item of failures) {
37
+ fingerprint.update(JSON.stringify([item.nodeId, item.alias, item.statusCode]));
38
+ }
39
+ const key = `${event}:${details.statusCode || ''}:${failures ? fingerprint.digest('hex') : ''}`;
40
+ const previous = recent.get(key);
41
+ if (previous && now - previous.time < 30000) {
42
+ previous.suppressed += 1;
43
+ return;
44
+ }
45
+ const payload = {
46
+ level, event, message, time: new Date(now).toISOString(),
47
+ nodeId: node.id || config.id, connectionId: config.connection, operation,
48
+ retrying: details.retrying === true,
49
+ };
50
+ if (details.statusCode) payload.statusCode = statusCode(details.statusCode);
51
+ if (failures) {
52
+ payload.failures = failures.slice(0, FAILURE_LIMIT).map((item) => {
53
+ const code = statusCode(item.statusCode);
54
+ return {
55
+ ...(typeof item.nodeId === 'string' ? { nodeId: item.nodeId } : {}),
56
+ ...(typeof item.alias === 'string' && item.alias ? { alias: item.alias } : {}),
57
+ statusCode: code,
58
+ message: POINT_MESSAGES[code] || '点位返回非 Good 状态,请根据 statusCode 排查',
59
+ };
60
+ });
61
+ payload.failuresTruncated = failures.length > FAILURE_LIMIT;
62
+ payload.omittedFailures = Math.max(0, failures.length - FAILURE_LIMIT);
63
+ }
64
+ for (const field of ['failed', 'total', 'monitored', 'desired', 'dropped', 'subscriptionId']) {
65
+ if (Number.isFinite(details[field])) payload[field] = details[field];
66
+ }
67
+ for (const field of ['rebuilt', 'dataGapPossible']) {
68
+ if (typeof details[field] === 'boolean') payload[field] = details[field];
69
+ }
70
+ if (previous?.suppressed) payload.suppressed = previous.suppressed;
71
+ if (recent.size >= 128) recent.delete(recent.keys().next().value);
72
+ recent.set(key, { time: now, suppressed: 0 });
73
+ const output = send || (node.send && node.send.bind(node));
74
+ output?.([null, { payload, opcua: { operation: 'diagnostic' } }]);
75
+ }
76
+ function failure(error, send, event = `${operation}_failed`, retrying = false) {
77
+ emit('error', event, `OPC UA ${operation} failed`, { statusCode: statusCode(error), retrying }, send);
78
+ }
79
+ function results(items, send, descriptors) {
80
+ const failed = items.filter((item) => item.statusCode && item.statusCode !== 'Good');
81
+ const failures = failed.map((item) => ({
82
+ nodeId: item.nodeId,
83
+ alias: item.alias ?? descriptors?.get(item.nodeId)?.alias,
84
+ statusCode: item.statusCode,
85
+ }));
86
+ if (failed.length) emit('warn', 'points_failed', 'Some OPC UA points returned a non-Good status', {
87
+ failed: failed.length, total: items.length, statusCode: statusCode(failed[0].statusCode),
88
+ failures,
89
+ }, send);
90
+ return failed.length;
91
+ }
92
+ const onFault = (error) => {
93
+ interrupted = true;
94
+ emit('warn', 'connection_fault', 'OPC UA connection fault', { statusCode: statusCode(error), retrying: true });
95
+ };
96
+ const onState = (state) => {
97
+ if (!state) return;
98
+ if (state.state === 'connected') {
99
+ if (interrupted) {
100
+ recent.clear();
101
+ emit('info', 'connection_restored', 'OPC UA connection restored; subscription recovery may still be pending');
102
+ }
103
+ interrupted = false;
104
+ } else if (['reconnecting', 'backoff', 'failed'].includes(state.state)) {
105
+ interrupted = true;
106
+ emit(state.state === 'failed' ? 'error' : 'warn', `connection_${state.state}`,
107
+ 'OPC UA connection unavailable; recovery pending', {
108
+ retrying: true, ...(state.statusCode && state.statusCode !== 'Good' ? { statusCode: statusCode(state.statusCode) } : {}),
109
+ });
110
+ }
111
+ };
112
+ client?.on?.('fault', onFault);
113
+ client?.on?.('state', onState);
114
+ node.on('close', () => {
115
+ closed = true;
116
+ recent.clear();
117
+ client?.removeListener?.('fault', onFault);
118
+ client?.removeListener?.('state', onState);
119
+ });
120
+ return { emit, failure, results };
121
+ }
122
+
123
+ module.exports = { createDiagnostics, statusCode };
package/lib/security.js CHANGED
@@ -1,18 +1,111 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
4
+ const crypto = require('node:crypto');
5
+
3
6
  const SECURITY_POLICY_URIS = Object.freeze({
4
7
  None: 'http://opcfoundation.org/UA/SecurityPolicy#None',
5
8
  Basic256Sha256: 'http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256',
9
+ Aes128_Sha256_RsaOaep: 'http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep',
10
+ Aes256_Sha256_RsaPss: 'http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss',
6
11
  });
7
12
 
8
13
  const SECURITY_MODES = new Set(['None', 'Sign', 'SignAndEncrypt']);
9
14
 
10
- function configurationError(message) {
15
+ function configurationError(message, code = 'OPCUA_INVALID_SECURITY_CONFIGURATION', cause) {
11
16
  const error = new Error(message);
12
- error.code = 'OPCUA_INVALID_SECURITY_CONFIGURATION';
17
+ error.code = code;
18
+ if (cause) error.cause = cause;
13
19
  return error;
14
20
  }
15
21
 
22
+ function readSecurityFile(filePath, label, readFileSync = fs.readFileSync) {
23
+ try {
24
+ return readFileSync(filePath);
25
+ } catch (error) {
26
+ throw configurationError(
27
+ `Cannot read OPC UA ${label}: ${filePath}`,
28
+ 'OPCUA_SECURITY_FILE_UNREADABLE',
29
+ error
30
+ );
31
+ }
32
+ }
33
+
34
+ function parseCertificate(buffer, label, X509Certificate = crypto.X509Certificate) {
35
+ try {
36
+ return new X509Certificate(buffer);
37
+ } catch (error) {
38
+ throw configurationError(
39
+ `Invalid OPC UA ${label}`,
40
+ 'OPCUA_CERTIFICATE_INVALID',
41
+ error
42
+ );
43
+ }
44
+ }
45
+
46
+ function applicationUris(certificate) {
47
+ const output = [];
48
+ const expression = /(?:^|,\s*)URI:((?:"[^"]+")|[^,]+)/g;
49
+ for (const match of String(certificate.subjectAltName || '').matchAll(expression)) {
50
+ const value = match[1].replace(/^"|"$/g, '').trim();
51
+ if (value && !output.includes(value)) output.push(value);
52
+ }
53
+ return output;
54
+ }
55
+
56
+ function validateCertificateTime(certificate, label, now = Date.now()) {
57
+ const validFrom = Date.parse(certificate.validFrom);
58
+ const validTo = Date.parse(certificate.validTo);
59
+ if (Number.isFinite(validFrom) && now < validFrom) {
60
+ throw configurationError(
61
+ `OPC UA ${label} is not valid before ${certificate.validFrom}`,
62
+ 'OPCUA_CERTIFICATE_NOT_YET_VALID'
63
+ );
64
+ }
65
+ if (Number.isFinite(validTo) && now > validTo) {
66
+ throw configurationError(
67
+ `OPC UA ${label} expired at ${certificate.validTo}`,
68
+ 'OPCUA_CERTIFICATE_EXPIRED'
69
+ );
70
+ }
71
+ }
72
+
73
+ function loadPrivateKey(buffer, passphrase, createPrivateKey = crypto.createPrivateKey) {
74
+ const password = passphrase ? String(passphrase) : undefined;
75
+ const attempts = [
76
+ { key: buffer, passphrase: password },
77
+ { key: buffer, format: 'der', type: 'pkcs8', passphrase: password },
78
+ { key: buffer, format: 'der', type: 'pkcs1', passphrase: password },
79
+ ];
80
+ let lastError;
81
+ for (const options of attempts) {
82
+ try {
83
+ return createPrivateKey(options);
84
+ } catch (error) {
85
+ lastError = error;
86
+ }
87
+ }
88
+ throw configurationError(
89
+ 'Invalid OPC UA client private key or key password',
90
+ 'OPCUA_PRIVATE_KEY_INVALID',
91
+ lastError
92
+ );
93
+ }
94
+
95
+ function certificateSummary(certificate, uri) {
96
+ const publicKey = certificate.publicKey;
97
+ return {
98
+ subject: certificate.subject,
99
+ issuer: certificate.issuer,
100
+ validFrom: certificate.validFrom,
101
+ validTo: certificate.validTo,
102
+ fingerprint256: certificate.fingerprint256,
103
+ applicationUri: uri || null,
104
+ publicKeyType: publicKey?.asymmetricKeyType || null,
105
+ modulusLength: publicKey?.asymmetricKeyDetails?.modulusLength || null,
106
+ };
107
+ }
108
+
16
109
  function normalizePathList(value) {
17
110
  const source = Array.isArray(value)
18
111
  ? value
@@ -72,6 +165,92 @@ function normalizeConnectionOptions(options = {}) {
72
165
  return normalized;
73
166
  }
74
167
 
168
+ function preflightConnectionSecurity(options = {}, dependencies = {}) {
169
+ const normalized = normalizeConnectionOptions(options);
170
+ const runtime = {
171
+ readFileSync: dependencies.readFileSync || fs.readFileSync,
172
+ X509Certificate: dependencies.X509Certificate || crypto.X509Certificate,
173
+ createPrivateKey: dependencies.createPrivateKey || crypto.createPrivateKey,
174
+ now: dependencies.now == null ? Date.now() : Number(dependencies.now),
175
+ };
176
+ if (normalized.securityMode === 'None') {
177
+ return {
178
+ ...normalized,
179
+ securityPreflight: { enabled: false, policy: normalized.securityPolicy },
180
+ };
181
+ }
182
+
183
+ const certificate = parseCertificate(
184
+ readSecurityFile(normalized.certificate, 'client certificate', runtime.readFileSync),
185
+ 'client certificate',
186
+ runtime.X509Certificate
187
+ );
188
+ validateCertificateTime(certificate, 'client certificate', runtime.now);
189
+
190
+ const uris = applicationUris(certificate);
191
+ const applicationUri = normalized.applicationUri || (uris.length === 1 ? uris[0] : '');
192
+ if (!applicationUri) {
193
+ throw configurationError(
194
+ 'The OPC UA client certificate must contain one Application URI or Application URI must be configured',
195
+ 'OPCUA_CERTIFICATE_URI_MISSING'
196
+ );
197
+ }
198
+ if (!uris.includes(applicationUri)) {
199
+ throw configurationError(
200
+ `Configured Application URI does not match the client certificate: ${applicationUri}`,
201
+ 'OPCUA_CERTIFICATE_URI_MISMATCH'
202
+ );
203
+ }
204
+
205
+ const privateKey = loadPrivateKey(
206
+ readSecurityFile(normalized.privateKey, 'client private key', runtime.readFileSync),
207
+ normalized.privateKeyPassword,
208
+ runtime.createPrivateKey
209
+ );
210
+ if (!certificate.checkPrivateKey(privateKey)) {
211
+ throw configurationError(
212
+ 'The OPC UA client private key does not match the client certificate',
213
+ 'OPCUA_PRIVATE_KEY_MISMATCH'
214
+ );
215
+ }
216
+
217
+ const keyType = certificate.publicKey?.asymmetricKeyType || '';
218
+ const modulusLength = Number(certificate.publicKey?.asymmetricKeyDetails?.modulusLength) || 0;
219
+ if (!keyType.startsWith('rsa')) {
220
+ throw configurationError(
221
+ `OPC UA ${normalized.securityPolicy} requires an RSA client certificate`,
222
+ 'OPCUA_CERTIFICATE_KEY_TYPE_UNSUPPORTED'
223
+ );
224
+ }
225
+ if (modulusLength > 0 && modulusLength < 2048) {
226
+ throw configurationError(
227
+ `OPC UA client certificate RSA key is too short: ${modulusLength} bits; at least 2048 bits required`,
228
+ 'OPCUA_CERTIFICATE_KEY_TOO_SHORT'
229
+ );
230
+ }
231
+
232
+ const trust = normalized.trustedCertificates.map((filePath) => {
233
+ const trusted = parseCertificate(
234
+ readSecurityFile(filePath, 'trusted certificate', runtime.readFileSync),
235
+ `trusted certificate: ${filePath}`,
236
+ runtime.X509Certificate
237
+ );
238
+ validateCertificateTime(trusted, `trusted certificate: ${filePath}`, runtime.now);
239
+ return certificateSummary(trusted);
240
+ });
241
+
242
+ return {
243
+ ...normalized,
244
+ applicationUri,
245
+ securityPreflight: {
246
+ enabled: true,
247
+ policy: normalized.securityPolicy,
248
+ clientCertificate: certificateSummary(certificate, applicationUri),
249
+ trustedCertificates: trust,
250
+ },
251
+ };
252
+ }
253
+
75
254
  function endpointIdentityTypes(endpoint) {
76
255
  return [...new Set((endpoint?.userIdentityTokens || []).map((token) => token.tokenType))];
77
256
  }
@@ -92,5 +271,6 @@ module.exports = {
92
271
  matchingEndpoints,
93
272
  normalizeConnectionOptions,
94
273
  normalizePathList,
274
+ preflightConnectionSecurity,
95
275
  securityPolicyName,
96
276
  };
package/nodes/browse.html CHANGED
@@ -46,6 +46,7 @@
46
46
  </script>
47
47
  <script type="text/html" data-help-name="tier0-opcua-browse">
48
48
  <section lang="zh-CN">
49
+ <p>输出 2 为独立状态/异常消息:payload.level 为 info、warn 或 error。可连接 Debug、文件或日志服务;原有 Catch 和日志仍然有效。同类事件每 30 秒最多输出一次,下一次包含 suppressed 计数。输出不含原始值、凭据或完整错误文本。</p>
49
50
  <h3>浏览(Browse)</h3>
50
51
  <p>浏览 OPC UA 地址空间并选择变量点位。Browse 返回节点描述信息,不读取实时值。</p>
51
52
  <h4>配置与输入</h4>
@@ -60,6 +61,7 @@
60
61
  </section>
61
62
  <hr>
62
63
  <section lang="en">
64
+ <p>Output 2 emits status/error events with payload.level (info, warn, error). Connect it to Debug, file or logging nodes. Catch and existing logs remain supported. Repeats are limited to one per 30 seconds; the next matching event includes suppressed counts. Raw values, credentials and SDK error text are omitted.</p>
63
65
  <h3>Browse</h3>
64
66
  <p>Browses the OPC UA address space and selects Variable nodes. Browse returns node descriptors; it does not read live values.</p>
65
67
  <h4>Configuration and input</h4>
@@ -75,7 +77,7 @@
75
77
  </script>
76
78
  <script type="text/javascript">
77
79
  RED.nodes.registerType('tier0-opcua-browse', {
78
- category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'search.png',
80
+ category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 2, outputLabels: ['Data', 'Status / errors'], icon: 'search.png',
79
81
  defaults: {
80
82
  connection: { value: '', type: 'tier0-opcua-connection', required: true }, rootNodeId: { value: 'ns=0;i=85' },
81
83
  selectedNodes: { value: [] }, recursive: { value: true }, variablesOnly: { value: true },
package/nodes/browse.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { createDiagnostics } = require('../lib/diagnostics');
4
+
3
5
  const { chunks } = require('../lib/points');
4
6
  const { buildBrowseTree, selectedVariables } = require('../lib/browse-tree');
5
7
 
@@ -65,12 +67,17 @@ module.exports = function registerBrowseNode(RED) {
65
67
  RED.nodes.createNode(this, config);
66
68
  const node = this;
67
69
  node.connection = RED.nodes.getNode(config.connection);
70
+ const diagnostics = createDiagnostics(node, config, 'browse');
68
71
  const consumer = node.connection?.registerConsumer?.('browse', {
69
72
  nodeId: config.id, name: config.name,
70
73
  });
71
74
 
72
75
  node.on('input', async (msg, send, done) => {
73
- if (!node.connection) return done(new Error('OPC UA connection not configured'));
76
+ if (!node.connection) {
77
+ const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
78
+ diagnostics.failure(error, send);
79
+ return done(error);
80
+ }
74
81
 
75
82
  try {
76
83
  consumer?.markUsed('browse');
@@ -124,6 +131,10 @@ module.exports = function registerBrowseNode(RED) {
124
131
  }
125
132
 
126
133
  const partial = browseDiagnostics?.truncated || browseDiagnostics?.failures?.length;
134
+ if (partial) diagnostics.emit('warn', 'browse_partial', 'OPC UA Browse returned partial results', {
135
+ failed: browseDiagnostics?.failures?.length || 0, total: points.length,
136
+ failures: browseDiagnostics?.failures || [],
137
+ }, send);
127
138
  node.status({
128
139
  fill: partial ? 'yellow' : 'green', shape: partial ? 'ring' : 'dot',
129
140
  text: `${points.length} points` +
@@ -133,6 +144,7 @@ module.exports = function registerBrowseNode(RED) {
133
144
  done();
134
145
  } catch (error) {
135
146
  node.status({ fill: 'red', shape: 'ring', text: error.message });
147
+ diagnostics.failure(error, send);
136
148
  done(error);
137
149
  }
138
150
  });
@@ -15,11 +15,13 @@
15
15
  <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>
16
16
  <div class="opcua-discovery-result" id="node-config-input-discoveryResult" style="display:none"><select id="node-config-input-discoveredEndpoint"></select></div>
17
17
  <div class="opcua-standard-note" id="node-config-input-testResult"></div>
18
+ <div class="form-row"><label for="node-config-input-compatibilityProfile">Server profile</label><select id="node-config-input-compatibilityProfile"><option value="standard">Standard OPC UA</option><option value="siemens-wincc">Siemens WinCC</option></select></div>
19
+ <div class="opcua-standard-note">Siemens WinCC uses conservative 200-item service batches and disables unsupported Subscription Deadband filters when the Server does not advertise usable limits.</div>
18
20
  <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>
19
21
  <div class="opcua-standard-note">Standard defaults include automatic reconnect, connectivity checks, and bounded command/NodeId queues.</div>
20
22
  <details class="opcua-advanced">
21
23
  <summary>Security and advanced connection settings</summary>
22
- <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>
24
+ <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="Aes128_Sha256_RsaOaep">Aes128_Sha256_RsaOaep</option><option value="Aes256_Sha256_RsaPss">Aes256_Sha256_RsaPss</option></select></div>
23
25
  <div class="form-row"><label for="node-config-input-user">User</label><input type="text" id="node-config-input-user"></div>
24
26
  <div class="form-row"><label for="node-config-input-password">Password</label><input type="password" id="node-config-input-password"></div>
25
27
  <div class="form-row opcua-secure-field"><label for="node-config-input-certificate">Client certificate</label><input type="text" class="opcua-security-path" id="node-config-input-certificate" placeholder="Upload or enter /path/to/client.der"> <button type="button" class="red-ui-button" id="node-config-upload-certificate"><i class="fa fa-upload"></i> Upload</button><input type="file" class="opcua-hidden-file" id="node-config-file-certificate" accept=".der,.pem,.crt,.cer"></div>
@@ -50,8 +52,9 @@
50
52
  <li><b>Endpoint</b>:服务器地址,例如 <code>opc.tcp://192.168.1.10:4840</code>。</li>
51
53
  <li><b>Discover endpoints</b>:读取服务器公开的安全模式、安全策略和用户认证方式。</li>
52
54
  <li><b>Test connection</b>:使用当前配置建立临时连接并验证认证。</li>
53
- <li><b>Security</b>:支持 None、Sign、SignAndEncrypt;安全模式、策略和认证方式必须与服务器 Endpoint 匹配。</li>
54
- <li><b>证书</b>:安全连接可上传或指定客户端证书、私钥、信任证书和 CRL。Application URI 应与客户端证书一致。</li>
55
+ <li><b>Server profile</b>:默认使用标准 OPC UA 行为;Siemens WinCC 模式在服务器未公布有效 OperationLimits 时按每批 200 点请求,并在订阅中禁用 WinCC 不支持的 Deadband。</li>
56
+ <li><b>Security</b>:支持 None、Sign、SignAndEncrypt,以及 Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss;安全模式、策略和认证方式必须与服务器 Endpoint 匹配。</li>
57
+ <li><b>证书</b>:安全连接可上传或指定客户端证书、私钥、信任证书和 CRL。连接前会检查有效期、证书和私钥是否匹配、RSA 密钥强度及 Application URI。若证书只包含一个 URI,可留空并自动采用。</li>
55
58
  </ul>
56
59
  <h4>建议</h4>
57
60
  <p>一个 Connection 配置对应一条客户端 Session。多个业务节点应复用同一个 Connection,避免为相同 Endpoint 重复创建 Session。节点内置自动重连、连通性检查以及有界命令队列。</p>
@@ -65,8 +68,9 @@
65
68
  <li><b>Endpoint</b>: Server URL, for example <code>opc.tcp://192.168.1.10:4840</code>.</li>
66
69
  <li><b>Discover endpoints</b>: Reads advertised security modes, policies, and user token types.</li>
67
70
  <li><b>Test connection</b>: Opens a temporary connection with the current settings and validates authentication.</li>
68
- <li><b>Security</b>: Supports None, Sign, and SignAndEncrypt. Mode, policy, and identity must match an advertised endpoint.</li>
69
- <li><b>Certificates</b>: Secure connections can upload or reference a client certificate, private key, trust list, and CRLs. The Application URI should match the client certificate.</li>
71
+ <li><b>Server profile</b>: Standard uses normal OPC UA behavior. Siemens WinCC falls back to 200-item service batches when usable OperationLimits are not advertised and removes unsupported Subscription Deadband filters.</li>
72
+ <li><b>Security</b>: Supports None, Sign, and SignAndEncrypt with Basic256Sha256, Aes128_Sha256_RsaOaep, or Aes256_Sha256_RsaPss. Mode, policy, and identity must match an advertised endpoint.</li>
73
+ <li><b>Certificates</b>: Secure connections can upload or reference a client certificate, private key, trust list, and CRLs. Validity, key matching, RSA strength, and Application URI are checked before connecting. An empty URI is filled automatically when the certificate contains exactly one URI.</li>
70
74
  </ul>
71
75
  <h4>Recommendation</h4>
72
76
  <p>One Connection configuration owns one client Session. Reuse it across nodes targeting the same server instead of creating duplicate Sessions. Automatic reconnect, connectivity checks, and a bounded command queue are enabled.</p>
@@ -78,6 +82,7 @@
78
82
  defaults: {
79
83
  endpoint: { value: '', required: true },
80
84
  securityMode: { value: 'None' }, securityPolicy: { value: 'None' },
85
+ compatibilityProfile: { value: 'standard' },
81
86
  user: { value: '' }, certificate: { value: '' }, privateKey: { value: '' },
82
87
  applicationUri: { value: '' }, trustedCertificates: { value: '' }, revocationList: { value: '' },
83
88
  requestedSessionTimeout: { value: 60000, validate: RED.validators.number() },
@@ -207,6 +212,7 @@
207
212
  endpoint: String($('#node-config-input-endpoint').val() || '').trim(),
208
213
  securityMode: mode.val(),
209
214
  securityPolicy: policy.val(),
215
+ compatibilityProfile: $('#node-config-input-compatibilityProfile').val(),
210
216
  user: $('#node-config-input-user').val(),
211
217
  password: $('#node-config-input-password').val(),
212
218
  certificate: $('#node-config-input-certificate').val(),
@@ -245,9 +251,12 @@
245
251
  const allEndpoints = Array.isArray(response.endpoints) ? response.endpoints : [];
246
252
  const identityType = String($('#node-config-input-user').val() || '').trim()
247
253
  ? 'Username' : 'Anonymous';
254
+ const supportedPolicies = new Set([
255
+ 'None', 'Basic256Sha256',
256
+ 'Aes128_Sha256_RsaOaep', 'Aes256_Sha256_RsaPss',
257
+ ]);
248
258
  const supported = allEndpoints.filter(function (item) {
249
- const itemPolicy = policyName(item.securityPolicyUri);
250
- return itemPolicy === 'None' || itemPolicy === 'Basic256Sha256';
259
+ return supportedPolicies.has(policyName(item.securityPolicyUri));
251
260
  });
252
261
  const matchingIdentity = supported.filter(function (item) {
253
262
  return (item.userIdentityTokens || []).some(function (token) {
@@ -271,7 +280,7 @@
271
280
  .appendTo(discovered);
272
281
  });
273
282
  if (!endpoints.length) {
274
- RED.notify('The server returned no supported None or Basic256Sha256 endpoint', 'warning');
283
+ RED.notify('The server returned no endpoint supported by this client', 'warning');
275
284
  result.hide();
276
285
  return;
277
286
  }
@@ -293,7 +302,8 @@
293
302
  if (!selected) return;
294
303
  const selectedPolicy = policyName(selected.securityPolicyUri);
295
304
  mode.val(selected.securityMode);
296
- if (selectedPolicy === 'None' || selectedPolicy === 'Basic256Sha256') {
305
+ if (['None', 'Basic256Sha256', 'Aes128_Sha256_RsaOaep',
306
+ 'Aes256_Sha256_RsaPss'].includes(selectedPolicy)) {
297
307
  policy.val(selectedPolicy);
298
308
  }
299
309
  updateSecurityFields();
@@ -3,6 +3,7 @@
3
3
  const { OpcUaClient, discoverEndpoints } = require('../lib/client');
4
4
  const { ConnectionRegistry } = require('../lib/connection-registry');
5
5
  const { SecurityFileStore } = require('../lib/security-file-store');
6
+ const { normalizeCompatibilityProfile } = require('../lib/compatibility');
6
7
 
7
8
  function pathList(value) {
8
9
  const source = Array.isArray(value) ? value : String(value || '').split(/\r?\n|;/);
@@ -14,6 +15,7 @@ function clientOptions(config, credentials = {}) {
14
15
  endpoint: config.endpoint,
15
16
  securityMode: config.securityMode || 'None',
16
17
  securityPolicy: config.securityPolicy || 'None',
18
+ compatibilityProfile: normalizeCompatibilityProfile(config.compatibilityProfile),
17
19
  user: config.user || '',
18
20
  password: credentials.password || '',
19
21
  certificate: config.certificate || '',
package/nodes/read.html CHANGED
@@ -45,6 +45,7 @@
45
45
  </script>
46
46
  <script type="text/html" data-help-name="tier0-opcua-read">
47
47
  <section lang="zh-CN">
48
+ <p>输出 2 为独立状态/异常消息:payload.level 为 info、warn 或 error。可连接 Debug、文件或日志服务;原有 Catch 和日志仍然有效。同类事件每 30 秒最多输出一次,下一次包含 suppressed 计数。输出不含原始值、凭据或完整错误文本。</p>
48
49
  <h3>读取(Read)</h3>
49
50
  <p>每收到一条输入消息执行一次 OPC UA Read。它不是定时器;读取频率由上游 Inject、Function 或其他节点的触发频率决定。</p>
50
51
  <h4>输入</h4>
@@ -65,6 +66,7 @@
65
66
  </section>
66
67
  <hr>
67
68
  <section lang="en">
69
+ <p>Output 2 emits status/error events with payload.level (info, warn, error). Connect it to Debug, file or logging nodes. Catch and existing logs remain supported. Repeats are limited to one per 30 seconds; the next matching event includes suppressed counts. Raw values, credentials and SDK error text are omitted.</p>
68
70
  <h3>Read</h3>
69
71
  <p>Executes one OPC UA Read for each input message. It is not a timer; the upstream Inject, Function, or other node controls the read frequency.</p>
70
72
  <h4>Input</h4>
@@ -86,7 +88,7 @@
86
88
  </script>
87
89
  <script type="text/javascript">
88
90
  RED.nodes.registerType('tier0-opcua-read', {
89
- category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
91
+ category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 2, outputLabels: ['Data', 'Status / errors'], icon: 'bridge.png',
90
92
  defaults: {
91
93
  connection: { value: '', type: 'tier0-opcua-connection', required: true },
92
94
  nodeId: { value: '' }, pointList: { value: '' }, pointListFileName: { value: '' },
package/nodes/read.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { createDiagnostics } = require('../lib/diagnostics');
4
+
3
5
  const { looksLikePointList, messagePoints } = require('../lib/points');
4
6
 
5
7
  const COMPACT_RESULT_FIELDS = Object.freeze([
@@ -27,12 +29,17 @@ module.exports = function registerReadNode(RED) {
27
29
  RED.nodes.createNode(this, config);
28
30
  const node = this;
29
31
  node.connection = RED.nodes.getNode(config.connection);
32
+ const diagnostics = createDiagnostics(node, config, 'read');
30
33
  const consumer = node.connection?.registerConsumer?.('read', {
31
34
  nodeId: config.id, name: config.name,
32
35
  });
33
36
 
34
37
  node.on('input', async (msg, send, done) => {
35
- if (!node.connection) return done(new Error('OPC UA connection not configured'));
38
+ if (!node.connection) {
39
+ const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
40
+ diagnostics.failure(error, send);
41
+ return done(error);
42
+ }
36
43
 
37
44
  try {
38
45
  consumer?.markUsed('read');
@@ -55,6 +62,7 @@ module.exports = function registerReadNode(RED) {
55
62
 
56
63
  // Undefined preserves the original output for Flow JSON created before
57
64
  // resultDetail was introduced. New editor nodes explicitly save compact.
65
+ const failed = diagnostics.results(results, send);
58
66
  const resultDetail = config.resultDetail || 'full';
59
67
  const outputResults = resultDetail === 'compact'
60
68
  ? results.map(compactResult)
@@ -96,10 +104,12 @@ module.exports = function registerReadNode(RED) {
96
104
  send(msg);
97
105
  }
98
106
 
99
- node.status({ fill: 'green', shape: 'dot', text: `${results.length} read` });
107
+ node.status({ fill: failed ? 'yellow' : 'green', shape: failed ? 'ring' : 'dot',
108
+ text: `${results.length - failed}/${results.length} read` + (failed ? ` / ${failed} failed` : '') });
100
109
  done();
101
110
  } catch (error) {
102
111
  node.status({ fill: 'red', shape: 'ring', text: error.message });
112
+ diagnostics.failure(error, send);
103
113
  done(error);
104
114
  }
105
115
  });
@@ -47,6 +47,7 @@
47
47
  <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>
48
48
  <div class="form-row"><label for="node-input-batchSize">Create batch size</label><input type="number" id="node-input-batchSize"></div>
49
49
  <div class="form-row"><label for="node-input-maxItemsPerSubscription">Items / subscription</label><input type="number" id="node-input-maxItemsPerSubscription"></div>
50
+ <div class="form-row"><label for="node-input-statusOutput">Status messages</label><select id="node-input-statusOutput"><option value="both">Both outputs (legacy compatible)</option><option value="separate">Output 2 only</option></select></div>
50
51
  <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>
51
52
  <div class="form-row"><label for="node-input-resultDetail">Result detail</label><select id="node-input-resultDetail"><option value="compact">Compact</option><option value="value">Value only</option><option value="full">Full metadata</option></select></div>
52
53
  <div class="form-row"><label for="node-input-callbackBatchSize">Output batch size</label><input type="number" id="node-input-callbackBatchSize"></div>
@@ -59,6 +60,7 @@
59
60
  </script>
60
61
  <script type="text/html" data-help-name="tier0-opcua-subscribe">
61
62
  <section lang="zh-CN">
63
+ <p>输出 2 为独立状态/异常消息:payload.level 为 info、warn 或 error。可连接 Debug、文件或日志服务;原有 Catch 和日志仍然有效。同类事件每 30 秒最多输出一次,下一次包含 suppressed 计数。输出不含原始值、凭据或完整错误文本。</p>
62
64
  <h3>订阅(Subscribe)</h3>
63
65
  <p>创建 OPC UA Subscription 和 MonitoredItems,由服务器在值变化时推送通知;它不是客户端定时 Read。大量点位会按照服务器限制自动拆分到多个 Subscription。</p>
64
66
  <h4>输入与命令</h4>
@@ -71,6 +73,7 @@
71
73
  <li>服务器可以把请求间隔修订为它支持的值。</li>
72
74
  </ul>
73
75
  <p>新节点默认 Server queue size=1、Discard oldest=true、Queue mode=latest,适合实时状态采集。脉冲、报警转换或完整历史不能丢失时,应改为 all 并增大服务器队列。</p>
76
+ <p>Connection 选择 Siemens WinCC 后,如果服务器没有公布有效限制,每个 Subscription 最多建立 1000 个 MonitoredItem,并自动移除 WinCC 不支持的 Deadband 配置;节点状态消息会报告实际兼容策略。</p>
74
77
  <h4>输出</h4>
75
78
  <ul>
76
79
  <li><b>Compact</b>:NodeId/alias、值、状态和源时间戳。</li>
@@ -81,6 +84,7 @@
81
84
  </section>
82
85
  <hr>
83
86
  <section lang="en">
87
+ <p>Output 2 emits status/error events with payload.level (info, warn, error). Connect it to Debug, file or logging nodes. Catch and existing logs remain supported. Repeats are limited to one per 30 seconds; the next matching event includes suppressed counts. Raw values, credentials and SDK error text are omitted.</p>
84
88
  <h3>Subscribe</h3>
85
89
  <p>Creates one or more OPC UA subscriptions and MonitoredItems so the server sends data-change notifications. It is not a periodic client Read. Large point sets are automatically sharded across Subscriptions according to server limits.</p>
86
90
  <h4>Input and commands</h4>
@@ -93,6 +97,7 @@
93
97
  <li>The server may revise requested intervals to supported values.</li>
94
98
  </ul>
95
99
  <p>New nodes default to server queue size 1, discard-oldest, and local latest coalescing for real-time state. Select all and increase the server queue for pulses, alarm transitions, or history where every change must be retained.</p>
100
+ <p>With the Siemens WinCC Connection profile, each Subscription is conservatively capped at 1000 MonitoredItems when the server advertises no usable limit, and unsupported Deadband filters are removed automatically. Status output reports the active compatibility behavior.</p>
96
101
  <h4>Output</h4>
97
102
  <ul>
98
103
  <li><b>Compact</b>: NodeId/alias, value, status, and source timestamp.</li>
@@ -104,8 +109,9 @@
104
109
  </script>
105
110
  <script type="text/javascript">
106
111
  RED.nodes.registerType('tier0-opcua-subscribe', {
107
- category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
112
+ category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 2, outputLabels: ['Data', 'Status / errors'], icon: 'bridge.png',
108
113
  defaults: {
114
+ statusOutput: { value: 'both' },
109
115
  connection: { value: '', type: 'tier0-opcua-connection', required: true },
110
116
  nodeId: { value: '' }, pointList: { value: '' }, pointListFileName: { value: '' },
111
117
  publishingInterval: { value: 1000, validate: RED.validators.number() }, samplingInterval: { value: 1000, validate: RED.validators.number() },
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const { createDiagnostics } = require('../lib/diagnostics');
4
+
3
5
  const { looksLikePointList, messagePoints } = require('../lib/points');
6
+ const { SIEMENS_WINCC_DEFAULTS, isSiemensWinCC } = require('../lib/compatibility');
4
7
 
5
8
  const NOTIFICATION_CONFIG_FIELDS = new Set([
6
9
  'nodeId',
@@ -55,19 +58,19 @@ function positiveInteger(value, fallback) {
55
58
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
56
59
  }
57
60
 
58
- function monitoringSettings(point, config) {
61
+ function monitoringSettings(point, config, siemensWincc = false) {
59
62
  return {
60
63
  samplingInterval: Number(point.samplingInterval) || Number(config.samplingInterval) || 1000,
61
64
  queueSize: positiveInteger(point.queueSize, positiveInteger(config.queueSize, 1)),
62
65
  discardOldest: point.discardOldest !== false,
63
- deadbandType: point.deadbandType || config.deadbandType || 'none',
64
- deadbandValue: Number(point.deadbandValue ?? config.deadbandValue) || 0,
66
+ deadbandType: siemensWincc ? 'none' : point.deadbandType || config.deadbandType || 'none',
67
+ deadbandValue: siemensWincc ? 0 : Number(point.deadbandValue ?? config.deadbandValue) || 0,
65
68
  };
66
69
  }
67
70
 
68
- function sameMonitoringSettings(left, right, config) {
69
- const leftSettings = monitoringSettings(left, config);
70
- const rightSettings = monitoringSettings(right, config);
71
+ function sameMonitoringSettings(left, right, config, siemensWincc = false) {
72
+ const leftSettings = monitoringSettings(left, config, siemensWincc);
73
+ const rightSettings = monitoringSettings(right, config, siemensWincc);
71
74
  return Object.keys(leftSettings).every(
72
75
  (key) => leftSettings[key] === rightSettings[key]
73
76
  );
@@ -97,6 +100,7 @@ module.exports = function registerSubscribeNode(RED) {
97
100
  RED.nodes.createNode(this, config);
98
101
  const node = this;
99
102
  node.connection = RED.nodes.getNode(config.connection);
103
+ const diagnostics = createDiagnostics(node, config, 'subscribe');
100
104
  node.subscriptions = new Map();
101
105
  node.desired = new Map();
102
106
  node.monitored = new Map();
@@ -111,6 +115,7 @@ module.exports = function registerSubscribeNode(RED) {
111
115
  node.closing = false;
112
116
  node.lastDropped = 0;
113
117
  node.lastStatusAt = 0;
118
+ node.compatibilityWarningIssued = false;
114
119
  const consumer = node.connection?.registerConsumer?.('subscribe', {
115
120
  nodeId: config.id, name: config.name,
116
121
  });
@@ -118,7 +123,16 @@ module.exports = function registerSubscribeNode(RED) {
118
123
  const batchSize = positiveInteger(config.batchSize, 500);
119
124
  const callbackBatchSize = positiveInteger(config.callbackBatchSize, 500);
120
125
  const callbackFlushIntervalMs = positiveInteger(config.callbackFlushIntervalMs, 50);
121
- const configuredSubscriptionCapacity = positiveInteger(config.maxItemsPerSubscription, 5000);
126
+ const siemensWincc = isSiemensWinCC(
127
+ node.connection?.client?.options?.compatibilityProfile
128
+ );
129
+ const requestedSubscriptionCapacity = positiveInteger(config.maxItemsPerSubscription, 5000);
130
+ const configuredSubscriptionCapacity = siemensWincc
131
+ ? Math.min(
132
+ requestedSubscriptionCapacity,
133
+ SIEMENS_WINCC_DEFAULTS.monitoredItemsPerSubscription
134
+ )
135
+ : requestedSubscriptionCapacity;
122
136
  // Undefined preserves the descriptor-rich output used by Flow JSON created
123
137
  // before resultDetail existed. New editor nodes explicitly save compact.
124
138
  const resultDetail = ['compact', 'value', 'full'].includes(config.resultDetail)
@@ -129,6 +143,24 @@ module.exports = function registerSubscribeNode(RED) {
129
143
  return [...node.subscriptions.keys()];
130
144
  }
131
145
 
146
+ function applyCompatibility(points) {
147
+ if (!siemensWincc) return points;
148
+ let removedDeadband = false;
149
+ const compatible = points.map((point) => {
150
+ const configuredType = point.deadbandType || config.deadbandType || 'none';
151
+ if (configuredType === 'none') return point;
152
+ removedDeadband = true;
153
+ return { ...point, deadbandType: 'none', deadbandValue: 0 };
154
+ });
155
+ if (removedDeadband && !node.compatibilityWarningIssued) {
156
+ node.compatibilityWarningIssued = true;
157
+ node.warn(
158
+ 'Siemens WinCC does not support OPC UA DataChange Deadband filters; Deadband was disabled.'
159
+ );
160
+ }
161
+ return compatible;
162
+ }
163
+
132
164
  function primarySubscriptionId() {
133
165
  return subscriptionIds()[0] ?? null;
134
166
  }
@@ -175,6 +207,7 @@ module.exports = function registerSubscribeNode(RED) {
175
207
  function updateQueueWarning(diagnostics) {
176
208
  const dropped = Number(diagnostics.dropped) || 0;
177
209
  if (dropped > node.lastDropped) {
210
+ diagnostics.emit('warn', 'queue_overflow', 'OPC UA notification queue dropped changes', { dropped });
178
211
  node.warn(`OPC UA notification queue dropped ${dropped - node.lastDropped} change(s); total ${dropped}`);
179
212
  }
180
213
  node.lastDropped = dropped;
@@ -243,9 +276,10 @@ module.exports = function registerSubscribeNode(RED) {
243
276
 
244
277
  function onNativeBatch(record, batch) {
245
278
  if (node.closing || !record.id || !node.subscriptions.has(record.id)) return;
246
- const diagnostics = batch.diagnostics || {};
247
- node.subscriptionDiagnostics.set(record.id, diagnostics);
279
+ const queueDiagnostics = batch.diagnostics || {};
280
+ node.subscriptionDiagnostics.set(record.id, queueDiagnostics);
248
281
  const sourceChanges = batch.changes || [];
282
+ diagnostics.results(sourceChanges.filter((change) => node.desired.has(change.nodeId)), undefined, node.desired);
249
283
  const changes = [];
250
284
  for (let index = 0; index < sourceChanges.length; index += 1) {
251
285
  const change = sourceChanges[index];
@@ -325,8 +359,8 @@ module.exports = function registerSubscribeNode(RED) {
325
359
  samplingInterval: Number(config.samplingInterval) || 1000,
326
360
  queueSize: Number(config.queueSize) || 10,
327
361
  discardOldest: config.discardOldest !== false,
328
- deadbandType: config.deadbandType || 'none',
329
- deadbandValue: Number(config.deadbandValue) || 0,
362
+ deadbandType: siemensWincc ? 'none' : config.deadbandType || 'none',
363
+ deadbandValue: siemensWincc ? 0 : Number(config.deadbandValue) || 0,
330
364
  timestampMode: config.timestampMode || 'source',
331
365
  },
332
366
  batchSize
@@ -497,6 +531,11 @@ module.exports = function registerSubscribeNode(RED) {
497
531
  queueSizeMin: revisedQueues.minimum,
498
532
  queueSizeMax: revisedQueues.maximum,
499
533
  },
534
+ compatibility: {
535
+ profile: siemensWincc ? 'siemens-wincc' : 'standard',
536
+ deadbandSupported: !siemensWincc,
537
+ monitoredItemsPerSubscription: configuredSubscriptionCapacity,
538
+ },
500
539
  connectionGovernance: node.connection?.getConnectionGovernance?.(),
501
540
  connectionUsage: node.connection?.getConnectionUsage?.(),
502
541
  };
@@ -513,7 +552,9 @@ module.exports = function registerSubscribeNode(RED) {
513
552
  let rebuilt = false;
514
553
  if (node.subscriptions.size) {
515
554
  let valid = false;
516
- try { valid = await validateSubscriptions(); } catch {}
555
+ try { valid = await validateSubscriptions(); } catch (error) {
556
+ diagnostics.failure(error, undefined, 'subscription_validation_failed', true);
557
+ }
517
558
  if (!valid) {
518
559
  await clearSubscriptions(true, true);
519
560
  rebuilt = true;
@@ -534,6 +575,7 @@ module.exports = function registerSubscribeNode(RED) {
534
575
  }
535
576
  await pruneEmptySubscriptions();
536
577
 
578
+ diagnostics.results(failures, undefined, node.desired);
537
579
  node.reconcileAttempt = 0;
538
580
  node.status({
539
581
  fill: failures.length ? 'yellow' : 'green',
@@ -542,7 +584,12 @@ module.exports = function registerSubscribeNode(RED) {
542
584
  (failures.length ? ` / ${failures.length} failed` : ''),
543
585
  });
544
586
  if (reason !== 'command') {
545
- node.send({
587
+ diagnostics.emit(failures.length ? 'warn' : 'info', failures.length ? 'subscription_recovery_partial' : 'subscription_restored',
588
+ 'OPC UA subscription reconciliation completed; data receipt is not yet confirmed', {
589
+ failed: failures.length, monitored: node.monitored.size, desired: node.desired.size,
590
+ rebuilt, dataGapPossible: rebuilt,
591
+ });
592
+ if (config.statusOutput !== 'separate') node.send({
546
593
  payload: { ...statusPayload('recover'), rebuilt, reason },
547
594
  opcua: {
548
595
  operation: 'subscription-status',
@@ -563,7 +610,7 @@ module.exports = function registerSubscribeNode(RED) {
563
610
  for (const [nodeId, monitored] of node.monitored) {
564
611
  const previous = node.desired.get(nodeId);
565
612
  const next = nextDesired.get(nodeId);
566
- if (!next || !previous || !sameMonitoringSettings(previous, next, config)) {
613
+ if (!next || !previous || !sameMonitoringSettings(previous, next, config, siemensWincc)) {
567
614
  changed.add(nodeId);
568
615
  }
569
616
  }
@@ -591,6 +638,7 @@ module.exports = function registerSubscribeNode(RED) {
591
638
  try {
592
639
  await runSerialized(() => reconcileSubscriptions(reason));
593
640
  } catch (error) {
641
+ diagnostics.failure(error, undefined, 'subscription_recovery_failed', true);
594
642
  node.warn(`OPC UA resubscribe failed: ${error.message}`);
595
643
  node.status({ fill: 'yellow', shape: 'ring', text: 'waiting to recover' });
596
644
  scheduleReconnect(reason);
@@ -609,6 +657,7 @@ module.exports = function registerSubscribeNode(RED) {
609
657
  scheduleReconnect(state.reason || 'connected', true);
610
658
  } else if (state.state === 'subscription-inactive' &&
611
659
  (!state.subscriptionId || node.subscriptions.has(state.subscriptionId))) {
660
+ diagnostics.emit('warn', 'subscription_inactive', 'OPC UA subscription is inactive; recovery pending', { retrying: true, subscriptionId: state.subscriptionId });
612
661
  node.status({ fill: 'yellow', shape: 'ring', text: 'subscription inactive' });
613
662
  scheduleReconnect('subscription-inactive');
614
663
  } else if (['reconnecting', 'backoff', 'failed'].includes(state.state)) {
@@ -619,21 +668,25 @@ module.exports = function registerSubscribeNode(RED) {
619
668
  node.connection?.client.on('state', onState);
620
669
 
621
670
  node.on('input', async (msg, send, done) => {
622
- if (!node.connection) return done(new Error('OPC UA connection not configured'));
671
+ if (!node.connection) {
672
+ const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
673
+ diagnostics.failure(error, send);
674
+ return done(error);
675
+ }
623
676
  try {
624
677
  const command = msg.opcua?.command || 'replace';
625
678
  const dynamicNodeId = msg.nodeId || msg.topic;
626
679
  const configuredPayload = !looksLikePointList(msg.payload) && !Array.isArray(msg.items) &&
627
680
  !dynamicNodeId ? config.pointList : '';
628
681
  const staticNodeId = dynamicNodeId ? '' : config.nodeId;
629
- const points = command === 'clear' ? [] : messagePoints(msg, staticNodeId, {
682
+ const points = command === 'clear' ? [] : applyCompatibility(messagePoints(msg, staticNodeId, {
630
683
  samplingInterval: Number(config.samplingInterval) || 1000,
631
684
  queueSize: Number(config.queueSize) || 10,
632
685
  discardOldest: config.discardOldest !== false,
633
686
  }, {
634
687
  configuredPayload,
635
688
  format: 'auto',
636
- });
689
+ }));
637
690
 
638
691
  node.status({ fill: 'blue', shape: 'dot', text: `${command} ${points.length}` });
639
692
  await runSerialized(async () => {
@@ -642,7 +695,7 @@ module.exports = function registerSubscribeNode(RED) {
642
695
  const changed = new Set();
643
696
  points.forEach((point) => {
644
697
  const previous = node.desired.get(point.nodeId);
645
- if (previous && !sameMonitoringSettings(previous, point, config)) {
698
+ if (previous && !sameMonitoringSettings(previous, point, config, siemensWincc)) {
646
699
  changed.add(point.nodeId);
647
700
  }
648
701
  node.desired.set(point.nodeId, point);
@@ -666,7 +719,10 @@ module.exports = function registerSubscribeNode(RED) {
666
719
  text: `${node.monitored.size} monitored / ${node.subscriptions.size} sub` +
667
720
  (failed ? ` / ${failed} failed` : ''),
668
721
  });
669
- send({
722
+ diagnostics.emit(failed ? 'warn' : 'info', 'subscription_updated', 'OPC UA subscription configuration updated', {
723
+ failed, monitored: node.monitored.size, desired: node.desired.size,
724
+ }, send);
725
+ if (config.statusOutput !== 'separate') send({
670
726
  ...msg,
671
727
  payload: statusPayload(command),
672
728
  opcua: { operation: 'subscription-status' },
@@ -674,6 +730,7 @@ module.exports = function registerSubscribeNode(RED) {
674
730
  done();
675
731
  } catch (error) {
676
732
  node.status({ fill: 'red', shape: 'ring', text: error.message });
733
+ diagnostics.failure(error, send);
677
734
  done(error);
678
735
  }
679
736
  });
@@ -710,16 +767,17 @@ module.exports = function registerSubscribeNode(RED) {
710
767
  if (node.closing) return;
711
768
  let points;
712
769
  try {
713
- points = messagePoints({}, config.nodeId, {
770
+ points = applyCompatibility(messagePoints({}, config.nodeId, {
714
771
  samplingInterval: Number(config.samplingInterval) || 1000,
715
772
  queueSize: Number(config.queueSize) || 10,
716
773
  discardOldest: config.discardOldest !== false,
717
774
  }, {
718
775
  configuredPayload: config.pointList,
719
776
  format: 'auto',
720
- });
777
+ }));
721
778
  } catch (error) {
722
779
  node.status({ fill: 'red', shape: 'ring', text: error.message });
780
+ diagnostics.failure(error, undefined, 'subscription_configuration_failed');
723
781
  node.warn(`OPC UA saved subscription point list is invalid: ${error.message}`);
724
782
  return;
725
783
  }
@@ -730,6 +788,7 @@ module.exports = function registerSubscribeNode(RED) {
730
788
  });
731
789
  }).catch((error) => {
732
790
  node.status({ fill: 'yellow', shape: 'ring', text: 'waiting to subscribe' });
791
+ diagnostics.failure(error, undefined, 'subscription_startup_failed', true);
733
792
  node.warn(`OPC UA startup subscription failed: ${error.message}`);
734
793
  scheduleReconnect('startup');
735
794
  });
package/nodes/write.html CHANGED
@@ -47,6 +47,7 @@
47
47
  </script>
48
48
  <script type="text/html" data-help-name="tier0-opcua-write">
49
49
  <section lang="zh-CN">
50
+ <p>输出 2 为独立状态/异常消息:payload.level 为 info、warn 或 error。可连接 Debug、文件或日志服务;原有 Catch 和日志仍然有效。同类事件每 30 秒最多输出一次,下一次包含 suppressed 计数。输出不含原始值、凭据或完整错误文本。</p>
50
51
  <h3>写入(Write)</h3>
51
52
  <p>向一个或多个 OPC UA Variable 写值。服务器必须允许当前用户写入,值也必须能够转换为目标数据类型。</p>
52
53
  <h4>输入</h4>
@@ -62,6 +63,7 @@
62
63
  </section>
63
64
  <hr>
64
65
  <section lang="en">
66
+ <p>Output 2 emits status/error events with payload.level (info, warn, error). Connect it to Debug, file or logging nodes. Catch and existing logs remain supported. Repeats are limited to one per 30 seconds; the next matching event includes suppressed counts. Raw values, credentials and SDK error text are omitted.</p>
65
67
  <h3>Write</h3>
66
68
  <p>Writes values to one or more OPC UA Variables. The current user must have write access and each value must be convertible to the target data type.</p>
67
69
  <h4>Input</h4>
@@ -78,7 +80,7 @@
78
80
  </script>
79
81
  <script type="text/javascript">
80
82
  RED.nodes.registerType('tier0-opcua-write', {
81
- category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 1, icon: 'bridge.png',
83
+ category: 'OPC UA', color: '#c7e9f1', inputs: 1, outputs: 2, outputLabels: ['Data', 'Status / errors'], icon: 'bridge.png',
82
84
  defaults: {
83
85
  connection: { value: '', type: 'tier0-opcua-connection', required: true },
84
86
  nodeId: { value: '' }, dataType: { value: 'String' },
package/nodes/write.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { createDiagnostics } = require('../lib/diagnostics');
4
+
3
5
  const { messageWritePoints } = require('../lib/points');
4
6
 
5
7
  module.exports = function registerWriteNode(RED) {
@@ -7,12 +9,17 @@ module.exports = function registerWriteNode(RED) {
7
9
  RED.nodes.createNode(this, config);
8
10
  const node = this;
9
11
  node.connection = RED.nodes.getNode(config.connection);
12
+ const diagnostics = createDiagnostics(node, config, 'write');
10
13
  const consumer = node.connection?.registerConsumer?.('write', {
11
14
  nodeId: config.id, name: config.name,
12
15
  });
13
16
 
14
17
  node.on('input', async (msg, send, done) => {
15
- if (!node.connection) return done(new Error('OPC UA connection not configured'));
18
+ if (!node.connection) {
19
+ const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
20
+ diagnostics.failure(error, send);
21
+ return done(error);
22
+ }
16
23
 
17
24
  try {
18
25
  consumer?.markUsed('write');
@@ -23,6 +30,7 @@ module.exports = function registerWriteNode(RED) {
23
30
  Number(config.batchSize) || 500
24
31
  );
25
32
 
33
+ const failed = diagnostics.results(results, send, new Map(points.map((point) => [point.nodeId, point])));
26
34
  if ((config.outputMode || 'batch') === 'split') {
27
35
  results.forEach((result) => send({ ...msg, topic: result.nodeId, opcua: result }));
28
36
  } else {
@@ -38,10 +46,12 @@ module.exports = function registerWriteNode(RED) {
38
46
  send(msg);
39
47
  }
40
48
 
41
- node.status({ fill: 'green', shape: 'dot', text: `${results.length} written` });
49
+ node.status({ fill: failed ? 'yellow' : 'green', shape: failed ? 'ring' : 'dot',
50
+ text: `${results.length - failed}/${results.length} written` + (failed ? ` / ${failed} failed` : '') });
42
51
  done();
43
52
  } catch (error) {
44
53
  node.status({ fill: 'red', shape: 'ring', text: error.message });
54
+ diagnostics.failure(error, send);
45
55
  done(error);
46
56
  }
47
57
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tier0/opcua-client",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
4
4
  "description": "High-scale OPC UA client nodes for Node-RED powered by open62541",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/addon.cc CHANGED
@@ -49,6 +49,14 @@ std::string SecurityPolicyUri(const std::string& policy) {
49
49
  policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
50
50
  return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
51
51
  }
52
+ if (policy == "Aes128_Sha256_RsaOaep" ||
53
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep") {
54
+ return "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep";
55
+ }
56
+ if (policy == "Aes256_Sha256_RsaPss" ||
57
+ policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss") {
58
+ return "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss";
59
+ }
52
60
  return "";
53
61
  }
54
62