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;
}
}