feat(dcl): Layer C runtime wires new OPC UA settings through to OPC SDK

OpcUaConnectionOptions record gains DiscardOldest, SubscriptionPriority,
SubscriptionDisplayName, TimestampsToReturn, plus OpcUaDeadbandOptions and
OpcUaUserIdentityOptions nullable sub-records.

OpcUaDataConnection.ConnectAsync copies all new fields from the typed
OpcUaEndpointConfig (including the Deadband and UserIdentity sub-objects)
into the OpcUaConnectionOptions record.

RealOpcUaClient:
- BuildUserIdentity translates TokenType into Opc.Ua.UserIdentity:
  Anonymous → null, UsernamePassword → new UserIdentity(name, utf8(pass)),
  X509Certificate → new UserIdentity(X509CertificateLoader.LoadPkcs12FromFile(...)).
- Subscription uses opts.SubscriptionDisplayName and opts.SubscriptionPriority.
- MonitoredItem.DiscardOldest is opts.DiscardOldest (was hardcoded true).
- BuildDataChangeFilter materializes a DataChangeFilter when Deadband is set.
- ReadAsync uses MapTimestampsToReturn for opts.TimestampsToReturn (was hardcoded Source).

X509CertificateLoader replaces obsolete X509Certificate2(string,string) ctor
(SYSLIB0057 on .NET 10). UserIdentity(string,byte[]) ctor used because the
(string,string) overload was removed in OPC Foundation 1.5.378.106.
This commit is contained in:
Joseph Doherty
2026-05-12 02:26:15 -04:00
parent b60a8ef409
commit e6a5b558f3
3 changed files with 76 additions and 6 deletions
@@ -1,3 +1,4 @@
using System.Security.Cryptography.X509Certificates;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
@@ -77,9 +78,10 @@ public class RealOpcUaClient : IOpcUaClient
#pragma warning disable CS0618 // Allow obsolete DefaultSessionFactory constructor for compatibility
var sessionFactory = new DefaultSessionFactory();
#pragma warning restore CS0618
var userIdentity = BuildUserIdentity(opts.UserIdentity);
_session = await sessionFactory.CreateAsync(
appConfig, configuredEndpoint, false,
"ScadaLink-DCL-Session", (uint)opts.SessionTimeoutMs, null, null, cancellationToken);
"ScadaLink-DCL-Session", (uint)opts.SessionTimeoutMs, userIdentity, null, cancellationToken);
// Detect server going offline via keep-alive failures
_connectionLostFired = false;
@@ -91,7 +93,8 @@ public class RealOpcUaClient : IOpcUaClient
// Create a default subscription for all monitored items
_subscription = new Subscription(_session.DefaultSubscription)
{
DisplayName = "ScadaLink",
DisplayName = opts.SubscriptionDisplayName,
Priority = opts.SubscriptionPriority,
PublishingEnabled = true,
PublishingInterval = opts.PublishingIntervalMs,
KeepAliveCount = (uint)opts.KeepAliveCount,
@@ -135,7 +138,8 @@ public class RealOpcUaClient : IOpcUaClient
AttributeId = Attributes.Value,
SamplingInterval = _options.SamplingIntervalMs,
QueueSize = (uint)_options.QueueSize,
DiscardOldest = true
DiscardOldest = _options.DiscardOldest,
Filter = BuildDataChangeFilter(_options.Deadband)
};
_callbacks[handle] = onValueChanged;
@@ -185,7 +189,7 @@ public class RealOpcUaClient : IOpcUaClient
};
var response = await _session.ReadAsync(
null, 0, TimestampsToReturn.Source,
null, 0, MapTimestampsToReturn(_options.TimestampsToReturn),
new ReadValueIdCollection { readValue }, cancellationToken);
var result = response.Results[0];
@@ -227,6 +231,45 @@ public class RealOpcUaClient : IOpcUaClient
{
await DisconnectAsync();
}
private static UserIdentity? BuildUserIdentity(OpcUaUserIdentityOptions? options)
{
if (options is null) return null;
return options.TokenType.ToUpperInvariant() switch
{
"USERNAMEPASSWORD" => new UserIdentity(
options.Username,
System.Text.Encoding.UTF8.GetBytes(options.Password ?? "")),
"X509CERTIFICATE" => new UserIdentity(
X509CertificateLoader.LoadPkcs12FromFile(
options.CertificatePath, options.CertificatePassword)),
_ => null
};
}
private static MonitoringFilter? BuildDataChangeFilter(OpcUaDeadbandOptions? deadband)
{
if (deadband is null) return null;
var deadbandType = deadband.Type.ToUpperInvariant() switch
{
"PERCENT" => DeadbandType.Percent,
_ => DeadbandType.Absolute
};
return new DataChangeFilter
{
Trigger = DataChangeTrigger.StatusValue,
DeadbandType = (uint)deadbandType,
DeadbandValue = deadband.Value
};
}
private static TimestampsToReturn MapTimestampsToReturn(string mode) =>
mode.ToUpperInvariant() switch
{
"SERVER" => TimestampsToReturn.Server,
"BOTH" => TimestampsToReturn.Both,
_ => TimestampsToReturn.Source
};
}
/// <summary>