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:
@@ -14,7 +14,22 @@ public record OpcUaConnectionOptions(
|
||||
int SamplingIntervalMs = 1000,
|
||||
int QueueSize = 10,
|
||||
string SecurityMode = "None",
|
||||
bool AutoAcceptUntrustedCerts = true);
|
||||
bool AutoAcceptUntrustedCerts = true,
|
||||
bool DiscardOldest = true,
|
||||
byte SubscriptionPriority = 0,
|
||||
string SubscriptionDisplayName = "ScadaLink",
|
||||
string TimestampsToReturn = "Source",
|
||||
OpcUaDeadbandOptions? Deadband = null,
|
||||
OpcUaUserIdentityOptions? UserIdentity = null);
|
||||
|
||||
public record OpcUaDeadbandOptions(string Type, double Value);
|
||||
|
||||
public record OpcUaUserIdentityOptions(
|
||||
string TokenType,
|
||||
string Username,
|
||||
string Password,
|
||||
string CertificatePath,
|
||||
string CertificatePassword);
|
||||
|
||||
/// <summary>
|
||||
/// WP-7: Abstraction over OPC UA client library for testability.
|
||||
|
||||
@@ -61,7 +61,19 @@ public class OpcUaDataConnection : IDataConnection
|
||||
SamplingIntervalMs: config.SamplingIntervalMs,
|
||||
QueueSize: config.QueueSize,
|
||||
SecurityMode: config.SecurityMode.ToString(),
|
||||
AutoAcceptUntrustedCerts: config.AutoAcceptUntrustedCerts);
|
||||
AutoAcceptUntrustedCerts: config.AutoAcceptUntrustedCerts,
|
||||
DiscardOldest: config.DiscardOldest,
|
||||
SubscriptionPriority: config.SubscriptionPriority,
|
||||
SubscriptionDisplayName: config.SubscriptionDisplayName,
|
||||
TimestampsToReturn: config.TimestampsToReturn.ToString(),
|
||||
Deadband: config.Deadband is { } db
|
||||
? new OpcUaDeadbandOptions(db.Type.ToString(), db.Value)
|
||||
: null,
|
||||
UserIdentity: config.UserIdentity is { } ui
|
||||
? new OpcUaUserIdentityOptions(
|
||||
ui.TokenType.ToString(), ui.Username, ui.Password,
|
||||
ui.CertificatePath, ui.CertificatePassword)
|
||||
: null);
|
||||
|
||||
_status = ConnectionHealth.Connecting;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user