namespace ScadaLink.DataConnectionLayer.Adapters; /// /// WP-8: Abstraction over the LmxProxy SDK client for testability. /// The actual LmxProxyClient SDK lives in a separate repo; this interface /// defines the contract the adapter depends on. /// /// LmxProxy uses gRPC streaming for subscriptions and a session-based model /// with keep-alive for connection management. /// public interface ILmxProxyClient : IAsyncDisposable { /// /// Opens a session to the LmxProxy server. Returns a session ID. /// Task OpenSessionAsync(string host, int port, CancellationToken cancellationToken = default); /// /// Closes the current session. /// Task CloseSessionAsync(CancellationToken cancellationToken = default); /// /// Sends a keep-alive to maintain the session. /// Task SendKeepAliveAsync(CancellationToken cancellationToken = default); bool IsConnected { get; } string? SessionId { get; } /// /// Subscribes to tag value changes via gRPC streaming. Returns a subscription handle. /// Task SubscribeTagAsync( string tagPath, Action onValueChanged, CancellationToken cancellationToken = default); Task UnsubscribeTagAsync(string subscriptionHandle, CancellationToken cancellationToken = default); Task<(object? Value, DateTime Timestamp, bool IsGood)> ReadTagAsync( string tagPath, CancellationToken cancellationToken = default); Task WriteTagAsync(string tagPath, object? value, CancellationToken cancellationToken = default); } /// /// Factory for creating ILmxProxyClient instances. /// public interface ILmxProxyClientFactory { ILmxProxyClient Create(); } /// /// Default factory that creates stub LmxProxy clients. /// In production, this would create real LmxProxy SDK client instances. /// public class DefaultLmxProxyClientFactory : ILmxProxyClientFactory { public ILmxProxyClient Create() => new StubLmxProxyClient(); } /// /// Stub LmxProxy client for development/testing. /// internal class StubLmxProxyClient : ILmxProxyClient { public bool IsConnected { get; private set; } public string? SessionId { get; private set; } public Task OpenSessionAsync(string host, int port, CancellationToken cancellationToken = default) { SessionId = Guid.NewGuid().ToString(); IsConnected = true; return Task.FromResult(SessionId); } public Task CloseSessionAsync(CancellationToken cancellationToken = default) { IsConnected = false; SessionId = null; return Task.CompletedTask; } public Task SendKeepAliveAsync(CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task SubscribeTagAsync( string tagPath, Action onValueChanged, CancellationToken cancellationToken = default) { return Task.FromResult(Guid.NewGuid().ToString()); } public Task UnsubscribeTagAsync(string subscriptionHandle, CancellationToken cancellationToken = default) { return Task.CompletedTask; } public Task<(object? Value, DateTime Timestamp, bool IsGood)> ReadTagAsync( string tagPath, CancellationToken cancellationToken = default) { return Task.FromResult<(object?, DateTime, bool)>((null, DateTime.UtcNow, true)); } public Task WriteTagAsync(string tagPath, object? value, CancellationToken cancellationToken = default) { return Task.FromResult(true); } public ValueTask DisposeAsync() { IsConnected = false; SessionId = null; return ValueTask.CompletedTask; } }