Apply code style formatting and restore partial modifiers on Avalonia views
Linter/formatter pass across the full codebase. Restores required partial keyword on AXAML code-behind classes that the formatter incorrectly removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production implementation that builds a real OPC UA ApplicationConfiguration.
|
||||
/// Production implementation that builds a real OPC UA ApplicationConfiguration.
|
||||
/// </summary>
|
||||
internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfigurationFactory
|
||||
{
|
||||
@@ -54,11 +54,9 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
await config.Validate(ApplicationType.Client);
|
||||
|
||||
if (settings.AutoAcceptCertificates)
|
||||
{
|
||||
config.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
|
||||
}
|
||||
|
||||
if (settings.SecurityMode != Models.SecurityMode.None)
|
||||
if (settings.SecurityMode != SecurityMode.None)
|
||||
{
|
||||
var app = new ApplicationInstance
|
||||
{
|
||||
@@ -72,4 +70,4 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
Logger.Debug("ApplicationConfiguration created for {EndpointUrl}", settings.EndpointUrl);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ using Serilog;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production endpoint discovery that queries the real server.
|
||||
/// Production endpoint discovery that queries the real server.
|
||||
/// </summary>
|
||||
internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();
|
||||
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode)
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode)
|
||||
{
|
||||
if (requestedMode == MessageSecurityMode.None)
|
||||
{
|
||||
@@ -54,9 +55,10 @@ internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
|
||||
{
|
||||
var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host };
|
||||
best.EndpointUrl = builder.ToString();
|
||||
Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host, requestedUri.Host);
|
||||
Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host,
|
||||
requestedUri.Host);
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Serilog;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production session adapter wrapping a real OPC UA Session.
|
||||
/// Production session adapter wrapping a real OPC UA Session.
|
||||
/// </summary>
|
||||
internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
{
|
||||
@@ -67,14 +67,14 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
true,
|
||||
nodeClassMask);
|
||||
|
||||
return (continuationPoint, references ?? new ReferenceDescriptionCollection());
|
||||
return (continuationPoint, references ?? []);
|
||||
}
|
||||
|
||||
public async Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct)
|
||||
{
|
||||
var (_, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, continuationPoint);
|
||||
return (nextCp, nextRefs ?? new ReferenceDescriptionCollection());
|
||||
return (nextCp, nextRefs ?? []);
|
||||
}
|
||||
|
||||
public async Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct)
|
||||
@@ -134,26 +134,24 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
break;
|
||||
|
||||
if (result.HistoryData is ExtensionObject ext && ext.Body is HistoryData historyData)
|
||||
{
|
||||
allValues.AddRange(historyData.DataValues);
|
||||
}
|
||||
|
||||
continuationPoint = result.ContinuationPoint;
|
||||
}
|
||||
while (continuationPoint != null && continuationPoint.Length > 0 && allValues.Count < maxValues);
|
||||
} while (continuationPoint != null && continuationPoint.Length > 0 && allValues.Count < maxValues);
|
||||
|
||||
return allValues;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct)
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var details = new ReadProcessedDetails
|
||||
{
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
ProcessingInterval = intervalMs,
|
||||
AggregateType = new NodeIdCollection { aggregateId }
|
||||
AggregateType = [aggregateId]
|
||||
};
|
||||
|
||||
var nodesToRead = new HistoryReadValueIdCollection
|
||||
@@ -178,9 +176,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
if (!StatusCode.IsBad(result.StatusCode) &&
|
||||
result.HistoryData is ExtensionObject ext &&
|
||||
ext.Body is HistoryData historyData)
|
||||
{
|
||||
allValues.AddRange(historyData.DataValues);
|
||||
}
|
||||
}
|
||||
|
||||
return allValues;
|
||||
@@ -204,10 +200,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_session.Connected)
|
||||
{
|
||||
_session.Close();
|
||||
}
|
||||
if (_session.Connected) _session.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -219,12 +212,12 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_session.Connected)
|
||||
{
|
||||
_session.Close();
|
||||
}
|
||||
if (_session.Connected) _session.Close();
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_session.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Serilog;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production session factory that creates real OPC UA sessions.
|
||||
/// Production session factory that creates real OPC UA sessions.
|
||||
/// </summary>
|
||||
internal sealed class DefaultSessionFactory : ISessionFactory
|
||||
{
|
||||
@@ -34,4 +34,4 @@ internal sealed class DefaultSessionFactory : ISessionFactory
|
||||
Logger.Information("Session created: {SessionName} -> {EndpointUrl}", sessionName, endpoint.EndpointUrl);
|
||||
return new DefaultSessionAdapter(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@ using Serilog;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production subscription adapter wrapping a real OPC UA Subscription.
|
||||
/// Production subscription adapter wrapping a real OPC UA Subscription.
|
||||
/// </summary>
|
||||
internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultSubscriptionAdapter>();
|
||||
private readonly Subscription _subscription;
|
||||
private readonly Dictionary<uint, MonitoredItem> _monitoredItems = new();
|
||||
private readonly Subscription _subscription;
|
||||
|
||||
public DefaultSubscriptionAdapter(Subscription subscription)
|
||||
{
|
||||
@@ -33,9 +33,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
item.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is MonitoredItemNotification notification)
|
||||
{
|
||||
onDataChange(nodeId.ToString(), notification.Value);
|
||||
}
|
||||
};
|
||||
|
||||
_subscription.AddItem(item);
|
||||
@@ -75,10 +73,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
|
||||
item.Notification += (_, e) =>
|
||||
{
|
||||
if (e.NotificationValue is EventFieldList eventFields)
|
||||
{
|
||||
onEvent(eventFields);
|
||||
}
|
||||
if (e.NotificationValue is EventFieldList eventFields) onEvent(eventFields);
|
||||
};
|
||||
|
||||
_subscription.AddItem(item);
|
||||
@@ -106,6 +101,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
Logger.Warning(ex, "Error deleting subscription");
|
||||
}
|
||||
|
||||
_monitoredItems.Clear();
|
||||
}
|
||||
|
||||
@@ -115,7 +111,10 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
_subscription.Delete(true);
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_monitoredItems.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and configures an OPC UA ApplicationConfiguration.
|
||||
/// Creates and configures an OPC UA ApplicationConfiguration.
|
||||
/// </summary>
|
||||
internal interface IApplicationConfigurationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a validated ApplicationConfiguration for the given connection settings.
|
||||
/// Creates a validated ApplicationConfiguration for the given connection settings.
|
||||
/// </summary>
|
||||
Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ using Opc.Ua;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts OPC UA endpoint discovery for testability.
|
||||
/// Abstracts OPC UA endpoint discovery for testability.
|
||||
/// </summary>
|
||||
internal interface IEndpointDiscovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovers endpoints at the given URL and returns the best match for the requested security mode.
|
||||
/// Also rewrites the endpoint URL hostname to match the requested URL when they differ.
|
||||
/// Discovers endpoints at the given URL and returns the best match for the requested security mode.
|
||||
/// Also rewrites the endpoint URL hostname to match the requested URL when they differ.
|
||||
/// </summary>
|
||||
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode);
|
||||
}
|
||||
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Opc.Ua;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts the OPC UA session for read, write, browse, history, and subscription operations.
|
||||
/// Abstracts the OPC UA session for read, write, browse, history, and subscription operations.
|
||||
/// </summary>
|
||||
internal interface ISessionAdapter : IDisposable
|
||||
{
|
||||
@@ -17,7 +17,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
NamespaceTable NamespaceUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a keep-alive callback. The callback receives true when the session is healthy, false on failure.
|
||||
/// Registers a keep-alive callback. The callback receives true when the session is healthy, false on failure.
|
||||
/// </summary>
|
||||
void RegisterKeepAliveHandler(Action<bool> callback);
|
||||
|
||||
@@ -25,37 +25,39 @@ internal interface ISessionAdapter : IDisposable
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Browses forward hierarchical references from the given node.
|
||||
/// Returns (continuationPoint, references).
|
||||
/// Browses forward hierarchical references from the given node.
|
||||
/// Returns (continuationPoint, references).
|
||||
/// </summary>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
|
||||
NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Continues a browse from a continuation point.
|
||||
/// Continues a browse from a continuation point.
|
||||
/// </summary>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a node has any forward hierarchical child references.
|
||||
/// Checks whether a node has any forward hierarchical child references.
|
||||
/// </summary>
|
||||
Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads raw historical data.
|
||||
/// Reads raw historical data.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads processed/aggregate historical data.
|
||||
/// Reads processed/aggregate historical data.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
NodeId aggregateId, double intervalMs, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subscription adapter for this session.
|
||||
/// Creates a subscription adapter for this session.
|
||||
/// </summary>
|
||||
Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default);
|
||||
|
||||
Task CloseAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ using Opc.Ua;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Creates OPC UA sessions from a configured endpoint.
|
||||
/// Creates OPC UA sessions from a configured endpoint.
|
||||
/// </summary>
|
||||
internal interface ISessionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a session to the given endpoint.
|
||||
/// Creates a session to the given endpoint.
|
||||
/// </summary>
|
||||
/// <param name="config">The application configuration.</param>
|
||||
/// <param name="endpoint">The configured endpoint.</param>
|
||||
@@ -24,4 +24,4 @@ internal interface ISessionFactory
|
||||
uint sessionTimeoutMs,
|
||||
UserIdentity identity,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,30 @@ using Opc.Ua;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts OPC UA subscription and monitored item management.
|
||||
/// Abstracts OPC UA subscription and monitored item management.
|
||||
/// </summary>
|
||||
internal interface ISubscriptionAdapter : IDisposable
|
||||
{
|
||||
uint SubscriptionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a data-change monitored item and returns its client handle for tracking.
|
||||
/// Adds a data-change monitored item and returns its client handle for tracking.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node to monitor.</param>
|
||||
/// <param name="samplingIntervalMs">The sampling interval in milliseconds.</param>
|
||||
/// <param name="onDataChange">Callback when data changes. Receives (nodeIdString, DataValue).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A client handle that can be used to remove the item.</returns>
|
||||
Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, Action<string, DataValue> onDataChange, CancellationToken ct = default);
|
||||
Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs,
|
||||
Action<string, DataValue> onDataChange, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added monitored item by its client handle.
|
||||
/// Removes a previously added monitored item by its client handle.
|
||||
/// </summary>
|
||||
Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an event monitored item with the given event filter.
|
||||
/// Adds an event monitored item with the given event filter.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node to monitor for events.</param>
|
||||
/// <param name="samplingIntervalMs">The sampling interval.</param>
|
||||
@@ -33,15 +34,16 @@ internal interface ISubscriptionAdapter : IDisposable
|
||||
/// <param name="onEvent">Callback when events arrive. Receives the event field list.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A client handle for the monitored item.</returns>
|
||||
Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter, Action<EventFieldList> onEvent, CancellationToken ct = default);
|
||||
Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter,
|
||||
Action<EventFieldList> onEvent, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Requests a condition refresh for this subscription.
|
||||
/// Requests a condition refresh for this subscription.
|
||||
/// </summary>
|
||||
Task ConditionRefreshAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all monitored items and deletes the subscription.
|
||||
/// Removes all monitored items and deletes the subscription.
|
||||
/// </summary>
|
||||
Task DeleteAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps the library's AggregateType enum to OPC UA aggregate function NodeIds.
|
||||
/// Maps the library's AggregateType enum to OPC UA aggregate function NodeIds.
|
||||
/// </summary>
|
||||
public static class AggregateTypeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the OPC UA NodeId for the specified aggregate type.
|
||||
/// Returns the OPC UA NodeId for the specified aggregate type.
|
||||
/// </summary>
|
||||
public static NodeId ToNodeId(AggregateType aggregate)
|
||||
{
|
||||
@@ -24,4 +24,4 @@ public static class AggregateTypeMapper
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(aggregate), aggregate, "Unknown AggregateType value.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses and normalizes failover URL sets for redundant OPC UA connections.
|
||||
/// Parses and normalizes failover URL sets for redundant OPC UA connections.
|
||||
/// </summary>
|
||||
public static class FailoverUrlParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a comma-separated failover URL string, prepending the primary URL.
|
||||
/// Trims whitespace and deduplicates.
|
||||
/// Parses a comma-separated failover URL string, prepending the primary URL.
|
||||
/// Trims whitespace and deduplicates.
|
||||
/// </summary>
|
||||
/// <param name="primaryUrl">The primary endpoint URL.</param>
|
||||
/// <param name="failoverCsv">Optional comma-separated failover URLs.</param>
|
||||
@@ -15,7 +15,7 @@ public static class FailoverUrlParser
|
||||
public static string[] Parse(string primaryUrl, string? failoverCsv)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(failoverCsv))
|
||||
return new[] { primaryUrl };
|
||||
return [primaryUrl];
|
||||
|
||||
var urls = new List<string> { primaryUrl };
|
||||
foreach (var url in failoverCsv.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
@@ -24,11 +24,12 @@ public static class FailoverUrlParser
|
||||
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
|
||||
urls.Add(trimmed);
|
||||
}
|
||||
|
||||
return urls.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a failover URL set from the primary URL and an optional array of failover URLs.
|
||||
/// Builds a failover URL set from the primary URL and an optional array of failover URLs.
|
||||
/// </summary>
|
||||
/// <param name="primaryUrl">The primary endpoint URL.</param>
|
||||
/// <param name="failoverUrls">Optional failover URLs.</param>
|
||||
@@ -36,7 +37,7 @@ public static class FailoverUrlParser
|
||||
public static string[] Parse(string primaryUrl, string[]? failoverUrls)
|
||||
{
|
||||
if (failoverUrls == null || failoverUrls.Length == 0)
|
||||
return new[] { primaryUrl };
|
||||
return [primaryUrl];
|
||||
|
||||
var urls = new List<string> { primaryUrl };
|
||||
foreach (var url in failoverUrls)
|
||||
@@ -45,6 +46,7 @@ public static class FailoverUrlParser
|
||||
if (!string.IsNullOrEmpty(trimmed) && !urls.Contains(trimmed, StringComparer.OrdinalIgnoreCase))
|
||||
urls.Add(trimmed);
|
||||
}
|
||||
|
||||
return urls.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps between the library's SecurityMode enum and OPC UA SDK MessageSecurityMode.
|
||||
/// Maps between the library's SecurityMode enum and OPC UA SDK MessageSecurityMode.
|
||||
/// </summary>
|
||||
public static class SecurityModeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="SecurityMode"/> to an OPC UA <see cref="MessageSecurityMode"/>.
|
||||
/// Converts a <see cref="SecurityMode" /> to an OPC UA <see cref="MessageSecurityMode" />.
|
||||
/// </summary>
|
||||
public static MessageSecurityMode ToMessageSecurityMode(SecurityMode mode)
|
||||
{
|
||||
@@ -23,7 +23,7 @@ public static class SecurityModeMapper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string to a <see cref="SecurityMode"/> value, case-insensitively.
|
||||
/// Parses a string to a <see cref="SecurityMode" /> value, case-insensitively.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to parse (e.g., "none", "sign", "encrypt", "signandencrypt").</param>
|
||||
/// <returns>The corresponding SecurityMode.</returns>
|
||||
@@ -39,4 +39,4 @@ public static class SecurityModeMapper
|
||||
$"Unknown security mode '{value}'. Valid values: none, sign, encrypt, signandencrypt")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Converts raw string values into typed values based on the current value's runtime type.
|
||||
/// Ported from the CLI tool's OpcUaHelper.ConvertValue.
|
||||
/// Converts raw string values into typed values based on the current value's runtime type.
|
||||
/// Ported from the CLI tool's OpcUaHelper.ConvertValue.
|
||||
/// </summary>
|
||||
public static class ValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a raw string value into the runtime type expected by the target node.
|
||||
/// Converts a raw string value into the runtime type expected by the target node.
|
||||
/// </summary>
|
||||
/// <param name="rawValue">The raw string supplied by the user.</param>
|
||||
/// <param name="currentValue">The current node value used to infer the target type. May be null.</param>
|
||||
@@ -29,4 +29,4 @@ public static class ValueConverter
|
||||
_ => rawValue
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
using Opc.Ua;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Shared OPC UA client service contract for CLI and UI consumers.
|
||||
/// Shared OPC UA client service contract for CLI and UI consumers.
|
||||
/// </summary>
|
||||
public interface IOpcUaClientService : IDisposable
|
||||
{
|
||||
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
Task DisconnectAsync(CancellationToken ct = default);
|
||||
bool IsConnected { get; }
|
||||
ConnectionInfo? CurrentConnectionInfo { get; }
|
||||
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
Task DisconnectAsync(CancellationToken ct = default);
|
||||
|
||||
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<Models.BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
|
||||
|
||||
Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default);
|
||||
Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
@@ -25,12 +26,15 @@ public interface IOpcUaClientService : IDisposable
|
||||
Task UnsubscribeAlarmsAsync(CancellationToken ct = default);
|
||||
Task RequestConditionRefreshAsync(CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues = 1000, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
|
||||
|
||||
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
|
||||
|
||||
event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="IOpcUaClientService"/> instances.
|
||||
/// Factory for creating <see cref="IOpcUaClientService" /> instances.
|
||||
/// </summary>
|
||||
public interface IOpcUaClientServiceFactory
|
||||
{
|
||||
IOpcUaClientService Create();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate functions for processed history reads.
|
||||
/// Aggregate functions for processed history reads.
|
||||
/// </summary>
|
||||
public enum AggregateType
|
||||
{
|
||||
@@ -22,4 +22,4 @@ public enum AggregateType
|
||||
|
||||
/// <summary>Last value in the interval.</summary>
|
||||
End
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,30 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Event data for an alarm or condition notification from the OPC UA server.
|
||||
/// Event data for an alarm or condition notification from the OPC UA server.
|
||||
/// </summary>
|
||||
public sealed class AlarmEventArgs : EventArgs
|
||||
{
|
||||
public AlarmEventArgs(
|
||||
string sourceName,
|
||||
string conditionName,
|
||||
ushort severity,
|
||||
string message,
|
||||
bool retain,
|
||||
bool activeState,
|
||||
bool ackedState,
|
||||
DateTime time)
|
||||
{
|
||||
SourceName = sourceName;
|
||||
ConditionName = conditionName;
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
Retain = retain;
|
||||
ActiveState = activeState;
|
||||
AckedState = ackedState;
|
||||
Time = time;
|
||||
}
|
||||
|
||||
/// <summary>The name of the source object that raised the alarm.</summary>
|
||||
public string SourceName { get; }
|
||||
|
||||
@@ -28,24 +48,4 @@ public sealed class AlarmEventArgs : EventArgs
|
||||
|
||||
/// <summary>The time the event occurred.</summary>
|
||||
public DateTime Time { get; }
|
||||
|
||||
public AlarmEventArgs(
|
||||
string sourceName,
|
||||
string conditionName,
|
||||
ushort severity,
|
||||
string message,
|
||||
bool retain,
|
||||
bool activeState,
|
||||
bool ackedState,
|
||||
DateTime time)
|
||||
{
|
||||
SourceName = sourceName;
|
||||
ConditionName = conditionName;
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
Retain = retain;
|
||||
ActiveState = activeState;
|
||||
AckedState = ackedState;
|
||||
Time = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,10 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single node in the browse result set.
|
||||
/// Represents a single node in the browse result set.
|
||||
/// </summary>
|
||||
public sealed class BrowseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The string representation of the node's NodeId.
|
||||
/// </summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name of the node.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The node class (e.g., "Object", "Variable", "Method").
|
||||
/// </summary>
|
||||
public string NodeClass { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the node has child references.
|
||||
/// </summary>
|
||||
public bool HasChildren { get; }
|
||||
|
||||
public BrowseResult(string nodeId, string displayName, string nodeClass, bool hasChildren)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
@@ -32,4 +12,24 @@ public sealed class BrowseResult
|
||||
NodeClass = nodeClass;
|
||||
HasChildren = hasChildren;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The string representation of the node's NodeId.
|
||||
/// </summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name of the node.
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The node class (e.g., "Object", "Variable", "Method").
|
||||
/// </summary>
|
||||
public string NodeClass { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the node has child references.
|
||||
/// </summary>
|
||||
public bool HasChildren { get; }
|
||||
}
|
||||
@@ -1,10 +1,26 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Information about the current OPC UA session.
|
||||
/// Information about the current OPC UA session.
|
||||
/// </summary>
|
||||
public sealed class ConnectionInfo
|
||||
{
|
||||
public ConnectionInfo(
|
||||
string endpointUrl,
|
||||
string serverName,
|
||||
string securityMode,
|
||||
string securityPolicyUri,
|
||||
string sessionId,
|
||||
string sessionName)
|
||||
{
|
||||
EndpointUrl = endpointUrl;
|
||||
ServerName = serverName;
|
||||
SecurityMode = securityMode;
|
||||
SecurityPolicyUri = securityPolicyUri;
|
||||
SessionId = sessionId;
|
||||
SessionName = sessionName;
|
||||
}
|
||||
|
||||
/// <summary>The endpoint URL of the connected server.</summary>
|
||||
public string EndpointUrl { get; }
|
||||
|
||||
@@ -22,20 +38,4 @@ public sealed class ConnectionInfo
|
||||
|
||||
/// <summary>The session name.</summary>
|
||||
public string SessionName { get; }
|
||||
|
||||
public ConnectionInfo(
|
||||
string endpointUrl,
|
||||
string serverName,
|
||||
string securityMode,
|
||||
string securityPolicyUri,
|
||||
string sessionId,
|
||||
string sessionName)
|
||||
{
|
||||
EndpointUrl = endpointUrl;
|
||||
ServerName = serverName;
|
||||
SecurityMode = securityMode;
|
||||
SecurityPolicyUri = securityPolicyUri;
|
||||
SessionId = sessionId;
|
||||
SessionName = sessionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,54 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for establishing an OPC UA client connection.
|
||||
/// Settings for establishing an OPC UA client connection.
|
||||
/// </summary>
|
||||
public sealed class ConnectionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary OPC UA endpoint URL.
|
||||
/// The primary OPC UA endpoint URL.
|
||||
/// </summary>
|
||||
public string EndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional failover endpoint URLs for redundancy.
|
||||
/// Optional failover endpoint URLs for redundancy.
|
||||
/// </summary>
|
||||
public string[]? FailoverUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional username for authentication.
|
||||
/// Optional username for authentication.
|
||||
/// </summary>
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional password for authentication.
|
||||
/// Optional password for authentication.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Transport security mode. Defaults to <see cref="Models.SecurityMode.None"/>.
|
||||
/// Transport security mode. Defaults to <see cref="Models.SecurityMode.None" />.
|
||||
/// </summary>
|
||||
public SecurityMode SecurityMode { get; set; } = SecurityMode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Session timeout in seconds. Defaults to 60.
|
||||
/// Session timeout in seconds. Defaults to 60.
|
||||
/// </summary>
|
||||
public int SessionTimeoutSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically accept untrusted server certificates. Defaults to true.
|
||||
/// Whether to automatically accept untrusted server certificates. Defaults to true.
|
||||
/// </summary>
|
||||
public bool AutoAcceptCertificates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
|
||||
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
|
||||
/// </summary>
|
||||
public string CertificateStorePath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LmxOpcUaClient", "pki");
|
||||
|
||||
/// <summary>
|
||||
/// Validates the settings and throws if any required values are missing or invalid.
|
||||
/// Validates the settings and throws if any required values are missing or invalid.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException">Thrown when settings are invalid.</exception>
|
||||
public void Validate()
|
||||
@@ -57,9 +57,10 @@ public sealed class ConnectionSettings
|
||||
throw new ArgumentException("EndpointUrl must not be null or empty.", nameof(EndpointUrl));
|
||||
|
||||
if (SessionTimeoutSeconds <= 0)
|
||||
throw new ArgumentException("SessionTimeoutSeconds must be greater than zero.", nameof(SessionTimeoutSeconds));
|
||||
throw new ArgumentException("SessionTimeoutSeconds must be greater than zero.",
|
||||
nameof(SessionTimeoutSeconds));
|
||||
|
||||
if (SessionTimeoutSeconds > 3600)
|
||||
throw new ArgumentException("SessionTimeoutSeconds must not exceed 3600.", nameof(SessionTimeoutSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current state of the OPC UA client connection.
|
||||
/// Represents the current state of the OPC UA client connection.
|
||||
/// </summary>
|
||||
public enum ConnectionState
|
||||
{
|
||||
@@ -16,4 +16,4 @@ public enum ConnectionState
|
||||
|
||||
/// <summary>Connection was lost and reconnection is in progress.</summary>
|
||||
Reconnecting
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Event data raised when the client connection state changes.
|
||||
/// Event data raised when the client connection state changes.
|
||||
/// </summary>
|
||||
public sealed class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string endpointUrl)
|
||||
{
|
||||
OldState = oldState;
|
||||
NewState = newState;
|
||||
EndpointUrl = endpointUrl;
|
||||
}
|
||||
|
||||
/// <summary>The previous connection state.</summary>
|
||||
public ConnectionState OldState { get; }
|
||||
|
||||
@@ -13,11 +20,4 @@ public sealed class ConnectionStateChangedEventArgs : EventArgs
|
||||
|
||||
/// <summary>The endpoint URL associated with the state change.</summary>
|
||||
public string EndpointUrl { get; }
|
||||
|
||||
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string endpointUrl)
|
||||
{
|
||||
OldState = oldState;
|
||||
NewState = newState;
|
||||
EndpointUrl = endpointUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,19 @@ using Opc.Ua;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Event data for a monitored data value change.
|
||||
/// Event data for a monitored data value change.
|
||||
/// </summary>
|
||||
public sealed class DataChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The string representation of the node that changed.</summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>The new data value from the server.</summary>
|
||||
public DataValue Value { get; }
|
||||
|
||||
public DataChangedEventArgs(string nodeId, DataValue value)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The string representation of the node that changed.</summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>The new data value from the server.</summary>
|
||||
public DataValue Value { get; }
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Redundancy information read from the server.
|
||||
/// Redundancy information read from the server.
|
||||
/// </summary>
|
||||
public sealed class RedundancyInfo
|
||||
{
|
||||
public RedundancyInfo(string mode, byte serviceLevel, string[] serverUris, string applicationUri)
|
||||
{
|
||||
Mode = mode;
|
||||
ServiceLevel = serviceLevel;
|
||||
ServerUris = serverUris;
|
||||
ApplicationUri = applicationUri;
|
||||
}
|
||||
|
||||
/// <summary>The redundancy mode (e.g., "None", "Cold", "Warm", "Hot").</summary>
|
||||
public string Mode { get; }
|
||||
|
||||
@@ -16,12 +24,4 @@ public sealed class RedundancyInfo
|
||||
|
||||
/// <summary>The application URI of the connected server.</summary>
|
||||
public string ApplicationUri { get; }
|
||||
|
||||
public RedundancyInfo(string mode, byte serviceLevel, string[] serverUris, string applicationUri)
|
||||
{
|
||||
Mode = mode;
|
||||
ServiceLevel = serviceLevel;
|
||||
ServerUris = serverUris;
|
||||
ApplicationUri = applicationUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Transport security mode for the OPC UA connection.
|
||||
/// Transport security mode for the OPC UA connection.
|
||||
/// </summary>
|
||||
public enum SecurityMode
|
||||
{
|
||||
@@ -13,4 +13,4 @@ public enum SecurityMode
|
||||
|
||||
/// <summary>Messages are signed and encrypted.</summary>
|
||||
SignAndEncrypt
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,42 @@
|
||||
using System.Text;
|
||||
using Opc.Ua;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Helpers;
|
||||
using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models;
|
||||
using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Full implementation of <see cref="IOpcUaClientService"/> using adapter abstractions for testability.
|
||||
/// Full implementation of <see cref="IOpcUaClientService" /> using adapter abstractions for testability.
|
||||
/// </summary>
|
||||
public sealed class OpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<OpcUaClientService>();
|
||||
|
||||
private readonly IApplicationConfigurationFactory _configFactory;
|
||||
private readonly IEndpointDiscovery _endpointDiscovery;
|
||||
private readonly ISessionFactory _sessionFactory;
|
||||
|
||||
private ISessionAdapter? _session;
|
||||
private ISubscriptionAdapter? _dataSubscription;
|
||||
private ISubscriptionAdapter? _alarmSubscription;
|
||||
private ConnectionState _state = ConnectionState.Disconnected;
|
||||
private ConnectionSettings? _settings;
|
||||
private string[]? _allEndpointUrls;
|
||||
private int _currentEndpointIndex;
|
||||
private bool _disposed;
|
||||
|
||||
// Track active data subscriptions for replay after failover
|
||||
private readonly Dictionary<string, (NodeId NodeId, int IntervalMs, uint Handle)> _activeDataSubscriptions = new();
|
||||
|
||||
private readonly IApplicationConfigurationFactory _configFactory;
|
||||
private readonly IEndpointDiscovery _endpointDiscovery;
|
||||
|
||||
private readonly ISessionFactory _sessionFactory;
|
||||
|
||||
// Track alarm subscription state for replay after failover
|
||||
private (NodeId? SourceNodeId, int IntervalMs)? _activeAlarmSubscription;
|
||||
private ISubscriptionAdapter? _alarmSubscription;
|
||||
private string[]? _allEndpointUrls;
|
||||
private int _currentEndpointIndex;
|
||||
private ISubscriptionAdapter? _dataSubscription;
|
||||
private bool _disposed;
|
||||
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
public bool IsConnected => _state == ConnectionState.Connected && _session?.Connected == true;
|
||||
public ConnectionInfo? CurrentConnectionInfo { get; private set; }
|
||||
private ISessionAdapter? _session;
|
||||
private ConnectionSettings? _settings;
|
||||
private ConnectionState _state = ConnectionState.Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new OpcUaClientService with the specified adapter dependencies.
|
||||
/// Creates a new OpcUaClientService with the specified adapter dependencies.
|
||||
/// </summary>
|
||||
internal OpcUaClientService(
|
||||
IApplicationConfigurationFactory configFactory,
|
||||
@@ -52,7 +49,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new OpcUaClientService with default production adapters.
|
||||
/// Creates a new OpcUaClientService with default production adapters.
|
||||
/// </summary>
|
||||
public OpcUaClientService()
|
||||
: this(
|
||||
@@ -62,6 +59,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
public bool IsConnected => _state == ConnectionState.Connected && _session?.Connected == true;
|
||||
public ConnectionInfo? CurrentConnectionInfo { get; private set; }
|
||||
|
||||
public async Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -80,10 +84,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
|
||||
session.RegisterKeepAliveHandler(isGood =>
|
||||
{
|
||||
if (!isGood)
|
||||
{
|
||||
_ = HandleKeepAliveFailureAsync();
|
||||
}
|
||||
if (!isGood) _ = HandleKeepAliveFailureAsync();
|
||||
});
|
||||
|
||||
CurrentConnectionInfo = BuildConnectionInfo(session);
|
||||
@@ -112,11 +113,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
await _dataSubscription.DeleteAsync(ct);
|
||||
_dataSubscription = null;
|
||||
}
|
||||
|
||||
if (_alarmSubscription != null)
|
||||
{
|
||||
await _alarmSubscription.DeleteAsync(ct);
|
||||
_alarmSubscription = null;
|
||||
}
|
||||
|
||||
if (_session != null)
|
||||
{
|
||||
await _session.CloseAsync(ct);
|
||||
@@ -150,7 +153,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
ThrowIfNotConnected();
|
||||
|
||||
// Read current value for type coercion when value is a string
|
||||
object typedValue = value;
|
||||
var typedValue = value;
|
||||
if (value is string rawString)
|
||||
{
|
||||
var currentDataValue = await _session!.ReadValueAsync(nodeId, ct);
|
||||
@@ -161,14 +164,15 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return await _session!.WriteValueAsync(nodeId, dataValue, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Models.BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotConnected();
|
||||
|
||||
var startNode = parentNodeId ?? ObjectIds.ObjectsFolder;
|
||||
var nodeClassMask = (uint)NodeClass.Object | (uint)NodeClass.Variable | (uint)NodeClass.Method;
|
||||
var results = new List<Models.BrowseResult>();
|
||||
var results = new List<BrowseResult>();
|
||||
|
||||
var (continuationPoint, references) = await _session!.BrowseAsync(startNode, nodeClassMask, ct);
|
||||
|
||||
@@ -180,7 +184,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
var hasChildren = reference.NodeClass == NodeClass.Object &&
|
||||
await _session.HasChildrenAsync(childNodeId, ct);
|
||||
|
||||
results.Add(new Models.BrowseResult(
|
||||
results.Add(new BrowseResult(
|
||||
reference.NodeId.ToString(),
|
||||
reference.DisplayName?.Text ?? string.Empty,
|
||||
reference.NodeClass.ToString(),
|
||||
@@ -188,13 +192,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
|
||||
if (continuationPoint != null && continuationPoint.Length > 0)
|
||||
{
|
||||
(continuationPoint, references) = await _session.BrowseNextAsync(continuationPoint, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -209,10 +209,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
if (_activeDataSubscriptions.ContainsKey(nodeIdStr))
|
||||
return; // Already subscribed
|
||||
|
||||
if (_dataSubscription == null)
|
||||
{
|
||||
_dataSubscription = await _session!.CreateSubscriptionAsync(intervalMs, ct);
|
||||
}
|
||||
if (_dataSubscription == null) _dataSubscription = await _session!.CreateSubscriptionAsync(intervalMs, ct);
|
||||
|
||||
var handle = await _dataSubscription.AddDataChangeMonitoredItemAsync(
|
||||
nodeId, intervalMs, OnDataChangeNotification, ct);
|
||||
@@ -229,16 +226,14 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
if (!_activeDataSubscriptions.TryGetValue(nodeIdStr, out var sub))
|
||||
return; // Not subscribed, safe to ignore
|
||||
|
||||
if (_dataSubscription != null)
|
||||
{
|
||||
await _dataSubscription.RemoveMonitoredItemAsync(sub.Handle, ct);
|
||||
}
|
||||
if (_dataSubscription != null) await _dataSubscription.RemoveMonitoredItemAsync(sub.Handle, ct);
|
||||
|
||||
_activeDataSubscriptions.Remove(nodeIdStr);
|
||||
Logger.Debug("Unsubscribed from data changes on {NodeId}", nodeId);
|
||||
}
|
||||
|
||||
public async Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
|
||||
public async Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotConnected();
|
||||
@@ -305,16 +300,18 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
ThrowIfDisposed();
|
||||
ThrowIfNotConnected();
|
||||
|
||||
var redundancySupportValue = await _session!.ReadValueAsync(VariableIds.Server_ServerRedundancy_RedundancySupport, ct);
|
||||
var redundancySupportValue =
|
||||
await _session!.ReadValueAsync(VariableIds.Server_ServerRedundancy_RedundancySupport, ct);
|
||||
var redundancyMode = ((RedundancySupport)(int)redundancySupportValue.Value).ToString();
|
||||
|
||||
var serviceLevelValue = await _session.ReadValueAsync(VariableIds.Server_ServiceLevel, ct);
|
||||
var serviceLevel = (byte)serviceLevelValue.Value;
|
||||
|
||||
string[] serverUris = Array.Empty<string>();
|
||||
string[] serverUris = [];
|
||||
try
|
||||
{
|
||||
var serverUriArrayValue = await _session.ReadValueAsync(VariableIds.Server_ServerRedundancy_ServerUriArray, ct);
|
||||
var serverUriArrayValue =
|
||||
await _session.ReadValueAsync(VariableIds.Server_ServerRedundancy_ServerUriArray, ct);
|
||||
if (serverUriArrayValue.Value is string[] uris)
|
||||
serverUris = uris;
|
||||
}
|
||||
@@ -323,7 +320,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
// ServerUriArray may not be present when RedundancySupport is None
|
||||
}
|
||||
|
||||
string applicationUri = string.Empty;
|
||||
var applicationUri = string.Empty;
|
||||
try
|
||||
{
|
||||
var serverArrayValue = await _session.ReadValueAsync(VariableIds.Server_ServerArray, ct);
|
||||
@@ -354,7 +351,8 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
private async Task<ISessionAdapter> ConnectToEndpointAsync(ConnectionSettings settings, string endpointUrl, CancellationToken ct)
|
||||
private async Task<ISessionAdapter> ConnectToEndpointAsync(ConnectionSettings settings, string endpointUrl,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Create a settings copy with the current endpoint URL
|
||||
var effectiveSettings = new ConnectionSettings
|
||||
@@ -372,12 +370,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
var requestedMode = SecurityModeMapper.ToMessageSecurityMode(settings.SecurityMode);
|
||||
var endpoint = _endpointDiscovery.SelectEndpoint(config, endpointUrl, requestedMode);
|
||||
|
||||
UserIdentity identity = settings.Username != null
|
||||
? new UserIdentity(settings.Username, System.Text.Encoding.UTF8.GetBytes(settings.Password ?? ""))
|
||||
var identity = settings.Username != null
|
||||
? new UserIdentity(settings.Username, Encoding.UTF8.GetBytes(settings.Password ?? ""))
|
||||
: new UserIdentity();
|
||||
|
||||
var sessionTimeoutMs = (uint)(settings.SessionTimeoutSeconds * 1000);
|
||||
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity, ct);
|
||||
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity,
|
||||
ct);
|
||||
}
|
||||
|
||||
private async Task HandleKeepAliveFailureAsync()
|
||||
@@ -392,9 +391,17 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
// Close old session
|
||||
if (_session != null)
|
||||
{
|
||||
try { _session.Dispose(); } catch { }
|
||||
try
|
||||
{
|
||||
_session.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_session = null;
|
||||
}
|
||||
|
||||
_dataSubscription = null;
|
||||
_alarmSubscription = null;
|
||||
|
||||
@@ -405,7 +412,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
|
||||
// Try each endpoint
|
||||
for (int attempt = 0; attempt < _allEndpointUrls.Length; attempt++)
|
||||
for (var attempt = 0; attempt < _allEndpointUrls.Length; attempt++)
|
||||
{
|
||||
_currentEndpointIndex = (_currentEndpointIndex + 1) % _allEndpointUrls.Length;
|
||||
var url = _allEndpointUrls[_currentEndpointIndex];
|
||||
@@ -418,7 +425,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
|
||||
session.RegisterKeepAliveHandler(isGood =>
|
||||
{
|
||||
if (!isGood) { _ = HandleKeepAliveFailureAsync(); }
|
||||
if (!isGood) _ = HandleKeepAliveFailureAsync();
|
||||
});
|
||||
|
||||
CurrentConnectionInfo = BuildConnectionInfo(session);
|
||||
@@ -448,7 +455,6 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
_activeDataSubscriptions.Clear();
|
||||
|
||||
foreach (var (nodeIdStr, (nodeId, intervalMs, _)) in subscriptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_dataSubscription == null)
|
||||
@@ -462,7 +468,6 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
Logger.Warning(ex, "Failed to replay data subscription for {NodeId}", nodeIdStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replay alarm subscription
|
||||
@@ -569,4 +574,4 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
if (_state != ConnectionState.Connected || _session == null)
|
||||
throw new InvalidOperationException("Not connected to an OPC UA server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Default factory that creates <see cref="OpcUaClientService"/> instances with production adapters.
|
||||
/// Default factory that creates <see cref="OpcUaClientService" /> instances with production adapters.
|
||||
/// </summary>
|
||||
public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
{
|
||||
@@ -9,4 +9,4 @@ public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||
{
|
||||
return new OpcUaClientService();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Client.Shared</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.378.106" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.378.106"/>
|
||||
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user