using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters; /// /// Configuration options for OPC UA connections, parsed from connection details JSON. /// All values have defaults matching the OPC Foundation SDK's typical settings. /// public record OpcUaConnectionOptions( int SessionTimeoutMs = 60000, int OperationTimeoutMs = 15000, int PublishingIntervalMs = 1000, int KeepAliveCount = 10, int LifetimeCount = 30, int MaxNotificationsPerPublish = 100, int SamplingIntervalMs = 1000, int QueueSize = 10, string SecurityMode = "None", // DataConnectionLayer-012: secure-by-default — untrusted server certificates are // rejected unless an operator explicitly opts in per connection. Accepting any // certificate defeats the Sign / SignAndEncrypt modes against a man-in-the-middle. bool AutoAcceptUntrustedCerts = false, bool DiscardOldest = true, byte SubscriptionPriority = 0, string SubscriptionDisplayName = "ScadaBridge", 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); /// /// WP-7: Abstraction over OPC UA client library for testability. /// The real implementation would wrap an OPC UA SDK (e.g., OPC Foundation .NET Standard Library). /// public interface IOpcUaClient : IAsyncDisposable { /// /// Connects to an OPC UA server at the specified endpoint URL. /// /// The OPC UA server endpoint URL. /// Connection options; if null, defaults are used. /// A cancellation token that can be used to cancel the operation. /// A task representing the asynchronous operation. Task ConnectAsync(string endpointUrl, OpcUaConnectionOptions? options = null, CancellationToken cancellationToken = default); /// /// Disconnects from the OPC UA server. /// /// A cancellation token that can be used to cancel the operation. /// A task representing the asynchronous operation. Task DisconnectAsync(CancellationToken cancellationToken = default); /// /// Gets a value indicating whether the client is currently connected to the server. /// bool IsConnected { get; } /// /// Creates a monitored item subscription for a node. Returns a subscription handle. /// /// The OPC UA node ID to monitor. /// Callback invoked when the monitored value changes, receiving nodeId, value, sourceTimestamp, and statusCode. /// A cancellation token that can be used to cancel the operation. /// A task that completes with a subscription handle string. Task CreateSubscriptionAsync( string nodeId, Action onValueChanged, CancellationToken cancellationToken = default); /// /// Removes a monitored item subscription by handle. /// /// The subscription handle returned by CreateSubscriptionAsync. /// A cancellation token that can be used to cancel the operation. /// A task representing the asynchronous operation. Task RemoveSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default); /// /// Subscribes to OPC UA Alarms & Conditions events under /// (or the Server object when null). On /// (re)subscribe the adapter issues a ConditionRefresh and replays the /// active conditions as Snapshot…SnapshotComplete transitions. Returns a /// handle for . /// /// OPC UA node ID to monitor; null subscribes to the Server object. /// Optional OPC UA condition type filter; null subscribes to all conditions. /// Callback invoked for each alarm transition received from the server. /// Token to observe for cancellation. /// A task that resolves to a subscription handle string for use with . Task CreateAlarmSubscriptionAsync( string? sourceNodeId, string? conditionFilter, Action onTransition, CancellationToken cancellationToken = default); /// Removes an alarm-event subscription by handle. /// The handle returned by . /// Token to observe for cancellation. /// A task representing the asynchronous operation. Task RemoveAlarmSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default); /// /// Reads the current value of a node. /// /// The OPC UA node ID to read. /// A cancellation token that can be used to cancel the operation. /// A task that completes with a tuple of (value, sourceTimestamp, statusCode). Task<(object? Value, DateTime SourceTimestamp, uint StatusCode)> ReadValueAsync( string nodeId, CancellationToken cancellationToken = default); /// /// Writes a value to a node. /// /// The OPC UA node ID to write to. /// The value to write. /// A cancellation token that can be used to cancel the operation. /// A task that completes with the OPC UA status code of the write operation. Task WriteValueAsync(string nodeId, object? value, CancellationToken cancellationToken = default); /// /// Raised when the OPC UA session detects a keep-alive failure or the server /// becomes unreachable. The adapter layer uses this to trigger reconnection. /// event Action? ConnectionLost; /// /// Enumerates the immediate children of /// (or the server's ObjectsFolder when null). Pageable via the OPC UA /// continuation-point mechanism: when a prior call returned a non-null /// , pass it back via /// to fetch the next page (BrowseNext); /// a null/empty token starts a fresh browse. Throws /// when the session is not /// currently up. /// /// Node id whose children to browse, or null for the server root. /// Opaque token from a prior to fetch the next page via BrowseNext; null/empty starts a fresh browse. /// A cancellation token that can be used to cancel the operation. /// A task that completes with the immediate children of the requested node and a continuation token for the next page (null when exhausted). Task BrowseChildrenAsync( string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default); } /// /// Factory for creating IOpcUaClient instances. /// public interface IOpcUaClientFactory { /// /// Creates a new IOpcUaClient instance. /// /// A new IOpcUaClient instance. IOpcUaClient Create(); } /// /// Default factory that creates stub OPC UA clients. /// In production, this would create real OPC UA SDK client instances. /// public class DefaultOpcUaClientFactory : IOpcUaClientFactory { /// public IOpcUaClient Create() => new StubOpcUaClient(); } /// /// Stub OPC UA client for development/testing. A real implementation would /// wrap the OPC Foundation .NET Standard Library. /// internal class StubOpcUaClient : IOpcUaClient { /// public bool IsConnected { get; private set; } #pragma warning disable CS0067 /// Raised when the OPC UA session detects a keep-alive failure or the server becomes unreachable. public event Action? ConnectionLost; #pragma warning restore CS0067 /// public Task ConnectAsync(string endpointUrl, OpcUaConnectionOptions? options = null, CancellationToken cancellationToken = default) { IsConnected = true; return Task.CompletedTask; } /// public Task DisconnectAsync(CancellationToken cancellationToken = default) { IsConnected = false; return Task.CompletedTask; } /// public Task CreateSubscriptionAsync( string nodeId, Action onValueChanged, CancellationToken cancellationToken = default) { return Task.FromResult(Guid.NewGuid().ToString()); } /// public Task RemoveSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default) { return Task.CompletedTask; } /// public Task CreateAlarmSubscriptionAsync( string? sourceNodeId, string? conditionFilter, Action onTransition, CancellationToken cancellationToken = default) { // Stub: no events. Real A&C subscription lives in RealOpcUaClient. return Task.FromResult(Guid.NewGuid().ToString()); } /// public Task RemoveAlarmSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default) => Task.CompletedTask; /// public Task<(object? Value, DateTime SourceTimestamp, uint StatusCode)> ReadValueAsync( string nodeId, CancellationToken cancellationToken = default) { return Task.FromResult<(object?, DateTime, uint)>((null, DateTime.UtcNow, 0)); } /// public Task WriteValueAsync(string nodeId, object? value, CancellationToken cancellationToken = default) { return Task.FromResult(0); // Good status } /// public Task BrowseChildrenAsync( string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default) { // Canned address-space tree so DCL browse/paging flows can be exercised // without a live OPC UA server (T15): // (root) // ├─ Folder1 (Object, HasChildren=true) // │ ├─ Tag1 (Variable) ← page 1 // │ └─ Tag2 (Variable) ← page 2 (continuation token "STUB_PAGE2") // └─ Folder2 (Object, HasChildren=false, leaf) // Folder1 fakes BrowseNext continuation paging so the continuation-token // round-trip is testable; everything else is a single, exhausted page. if (string.IsNullOrEmpty(parentNodeId)) { return Task.FromResult(new BrowseChildrenResult( new[] { new BrowseNode("Folder1", "Folder1", BrowseNodeClass.Object, HasChildren: true), new BrowseNode("Folder2", "Folder2", BrowseNodeClass.Object, HasChildren: false), }, Truncated: false)); } if (parentNodeId == "Folder1") { // Page 2: the caller passed back the continuation token from page 1. if (continuationToken == "STUB_PAGE2") { return Task.FromResult(new BrowseChildrenResult( new[] { new BrowseNode("Tag2", "Tag2", BrowseNodeClass.Variable, HasChildren: false) }, Truncated: false)); } // Page 1: return only Tag1 and a continuation token for the next page. return Task.FromResult(new BrowseChildrenResult( new[] { new BrowseNode("Tag1", "Tag1", BrowseNodeClass.Variable, HasChildren: false) }, Truncated: true, ContinuationToken: "STUB_PAGE2")); } // Leaves (Folder2, Variable nodes, anything unknown) have no children. return Task.FromResult(new BrowseChildrenResult(Array.Empty(), Truncated: false)); } /// Disposes this stub client and marks the connection as closed. /// A completed . public ValueTask DisposeAsync() { IsConnected = false; return ValueTask.CompletedTask; } }