feat: wire real LmxProxy gRPC client into Data Connection Layer

Replace stub ILmxProxyClient with production proto-generated gRPC client
(RealLmxProxyClient) that connects to LmxProxy servers with x-api-key
metadata header authentication. Includes pre-generated proto stubs for
ARM64 Docker compatibility, updated adapter with proper quality mapping
(Good/Uncertain/Bad), subscription via server-streaming RPC, and 20 unit
tests covering all operations. Updated Component-DataConnectionLayer.md
to reflect the actual implementation.
This commit is contained in:
Joseph Doherty
2026-03-18 11:57:18 -04:00
parent da683d4fe9
commit e837eae2cc
10 changed files with 7022 additions and 232 deletions

View File

@@ -1,120 +1,109 @@
namespace ScadaLink.DataConnectionLayer.Adapters;
/// <summary>
/// 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.
/// Quality enumeration mirroring the LmxProxy SDK's Quality type.
/// </summary>
public enum LmxQuality { Good, Uncertain, Bad }
/// <summary>
/// Value-Timestamp-Quality record mirroring the LmxProxy SDK's Vtq type.
/// </summary>
public readonly record struct LmxVtq(object? Value, DateTime TimestampUtc, LmxQuality Quality);
/// <summary>
/// Subscription handle returned by <see cref="ILmxProxyClient.SubscribeAsync"/>.
/// Disposing the subscription stops receiving updates.
/// </summary>
public interface ILmxSubscription : IAsyncDisposable { }
/// <summary>
/// Abstraction over the LmxProxy SDK client for testability.
/// Mirrors the real ScadaBridge LmxProxyClient API:
/// - Session-based connection with automatic 30s keep-alive
/// - gRPC streaming for subscriptions
/// - Throws on write/read failures
/// </summary>
public interface ILmxProxyClient : IAsyncDisposable
{
/// <summary>
/// Opens a session to the LmxProxy server. Returns a session ID.
/// </summary>
Task<string> OpenSessionAsync(string host, int port, CancellationToken cancellationToken = default);
/// <summary>
/// Closes the current session.
/// </summary>
Task CloseSessionAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Sends a keep-alive to maintain the session.
/// </summary>
Task SendKeepAliveAsync(CancellationToken cancellationToken = default);
bool IsConnected { get; }
string? SessionId { get; }
/// <summary>
/// Subscribes to tag value changes via gRPC streaming. Returns a subscription handle.
/// </summary>
Task<string> SubscribeTagAsync(
string tagPath,
Action<string, object?, DateTime, bool> onValueChanged,
Task ConnectAsync(CancellationToken cancellationToken = default);
Task DisconnectAsync();
Task<LmxVtq> ReadAsync(string address, CancellationToken cancellationToken = default);
Task<IDictionary<string, LmxVtq>> ReadBatchAsync(IEnumerable<string> addresses, CancellationToken cancellationToken = default);
Task WriteAsync(string address, object value, CancellationToken cancellationToken = default);
Task WriteBatchAsync(IDictionary<string, object> values, CancellationToken cancellationToken = default);
Task<ILmxSubscription> SubscribeAsync(
IEnumerable<string> addresses,
Action<string, LmxVtq> onUpdate,
CancellationToken cancellationToken = default);
Task UnsubscribeTagAsync(string subscriptionHandle, CancellationToken cancellationToken = default);
Task<(object? Value, DateTime Timestamp, bool IsGood)> ReadTagAsync(
string tagPath, CancellationToken cancellationToken = default);
Task<bool> WriteTagAsync(string tagPath, object? value, CancellationToken cancellationToken = default);
}
/// <summary>
/// Factory for creating ILmxProxyClient instances.
/// Factory for creating <see cref="ILmxProxyClient"/> instances configured
/// with host, port, and optional API key.
/// </summary>
public interface ILmxProxyClientFactory
{
ILmxProxyClient Create();
ILmxProxyClient Create(string host, int port, string? apiKey);
}
/// <summary>
/// Default factory that creates stub LmxProxy clients.
/// In production, this would create real LmxProxy SDK client instances.
/// Default factory that creates stub LmxProxy clients for development/testing.
/// </summary>
public class DefaultLmxProxyClientFactory : ILmxProxyClientFactory
{
public ILmxProxyClient Create() => new StubLmxProxyClient();
public ILmxProxyClient Create(string host, int port, string? apiKey) => new StubLmxProxyClient();
}
/// <summary>
/// Stub LmxProxy client for development/testing.
/// Stub LmxProxy client for development and unit testing.
/// </summary>
internal class StubLmxProxyClient : ILmxProxyClient
{
public bool IsConnected { get; private set; }
public string? SessionId { get; private set; }
public Task<string> OpenSessionAsync(string host, int port, CancellationToken cancellationToken = default)
public Task ConnectAsync(CancellationToken cancellationToken = default)
{
SessionId = Guid.NewGuid().ToString();
IsConnected = true;
return Task.FromResult(SessionId);
return Task.CompletedTask;
}
public Task CloseSessionAsync(CancellationToken cancellationToken = default)
public Task DisconnectAsync()
{
IsConnected = false;
SessionId = null;
return Task.CompletedTask;
}
public Task SendKeepAliveAsync(CancellationToken cancellationToken = default)
public Task<LmxVtq> ReadAsync(string address, CancellationToken cancellationToken = default)
=> Task.FromResult(new LmxVtq(null, DateTime.UtcNow, LmxQuality.Good));
public Task<IDictionary<string, LmxVtq>> ReadBatchAsync(IEnumerable<string> addresses, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
var results = addresses.ToDictionary(a => a, _ => new LmxVtq(null, DateTime.UtcNow, LmxQuality.Good));
return Task.FromResult<IDictionary<string, LmxVtq>>(results);
}
public Task<string> SubscribeTagAsync(
string tagPath, Action<string, object?, DateTime, bool> onValueChanged,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Guid.NewGuid().ToString());
}
public Task WriteAsync(string address, object value, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task UnsubscribeTagAsync(string subscriptionHandle, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task WriteBatchAsync(IDictionary<string, object> values, CancellationToken cancellationToken = default)
=> 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<bool> WriteTagAsync(string tagPath, object? value, CancellationToken cancellationToken = default)
{
return Task.FromResult(true);
}
public Task<ILmxSubscription> SubscribeAsync(IEnumerable<string> addresses, Action<string, LmxVtq> onUpdate, CancellationToken cancellationToken = default)
=> Task.FromResult<ILmxSubscription>(new StubLmxSubscription());
public ValueTask DisposeAsync()
{
IsConnected = false;
SessionId = null;
return ValueTask.CompletedTask;
}
}
internal class StubLmxSubscription : ILmxSubscription
{
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

View File

@@ -5,13 +5,13 @@ using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.DataConnectionLayer.Adapters;
/// <summary>
/// WP-8: LmxProxy adapter implementing IDataConnection.
/// Maps IDataConnection to LmxProxy SDK calls.
/// LmxProxy adapter implementing IDataConnection.
/// Maps IDataConnection operations to the LmxProxy SDK client.
///
/// LmxProxy-specific behavior:
/// - Session-based connection with 30s keep-alive
/// - gRPC streaming for subscriptions
/// - SessionId management (required for all operations)
/// - Session-based connection with automatic 30s keep-alive (managed by SDK)
/// - gRPC streaming for subscriptions via ILmxSubscription handles
/// - API key authentication via x-api-key gRPC metadata header
/// </summary>
public class LmxProxyDataConnection : IDataConnection
{
@@ -19,11 +19,10 @@ public class LmxProxyDataConnection : IDataConnection
private readonly ILogger<LmxProxyDataConnection> _logger;
private ILmxProxyClient? _client;
private string _host = "localhost";
private int _port = 5000;
private int _port = 50051;
private ConnectionHealth _status = ConnectionHealth.Disconnected;
private Timer? _keepAliveTimer;
private readonly Dictionary<string, string> _subscriptionHandles = new();
private readonly Dictionary<string, ILmxSubscription> _subscriptions = new();
public LmxProxyDataConnection(ILmxProxyClientFactory clientFactory, ILogger<LmxProxyDataConnection> logger)
{
@@ -38,82 +37,56 @@ public class LmxProxyDataConnection : IDataConnection
_host = connectionDetails.TryGetValue("Host", out var host) ? host : "localhost";
if (connectionDetails.TryGetValue("Port", out var portStr) && int.TryParse(portStr, out var port))
_port = port;
connectionDetails.TryGetValue("ApiKey", out var apiKey);
_status = ConnectionHealth.Connecting;
_client = _clientFactory.Create();
_client = _clientFactory.Create(_host, _port, apiKey);
var sessionId = await _client.OpenSessionAsync(_host, _port, cancellationToken);
await _client.ConnectAsync(cancellationToken);
_status = ConnectionHealth.Connected;
// Start 30s keep-alive timer per design spec
_keepAliveTimer = new Timer(
async _ => await SendKeepAliveAsync(),
null,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(30));
_logger.LogInformation("LmxProxy connected to {Host}:{Port}, sessionId={SessionId}", _host, _port, sessionId);
_logger.LogInformation("LmxProxy connected to {Host}:{Port}", _host, _port);
}
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
{
_keepAliveTimer?.Dispose();
_keepAliveTimer = null;
if (_client != null)
{
await _client.CloseSessionAsync(cancellationToken);
await _client.DisconnectAsync();
_status = ConnectionHealth.Disconnected;
_logger.LogInformation("LmxProxy disconnected from {Host}:{Port}", _host, _port);
}
}
public async Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default)
{
EnsureConnected();
var handle = await _client!.SubscribeTagAsync(
tagPath,
(path, value, timestamp, isGood) =>
{
var quality = isGood ? QualityCode.Good : QualityCode.Bad;
callback(path, new TagValue(value, quality, new DateTimeOffset(timestamp, TimeSpan.Zero)));
},
cancellationToken);
_subscriptionHandles[handle] = tagPath;
return handle;
}
public async Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default)
{
if (_client != null)
{
await _client.UnsubscribeTagAsync(subscriptionId, cancellationToken);
_subscriptionHandles.Remove(subscriptionId);
}
}
public async Task<ReadResult> ReadAsync(string tagPath, CancellationToken cancellationToken = default)
{
EnsureConnected();
var (value, timestamp, isGood) = await _client!.ReadTagAsync(tagPath, cancellationToken);
var quality = isGood ? QualityCode.Good : QualityCode.Bad;
var vtq = await _client!.ReadAsync(tagPath, cancellationToken);
var quality = MapQuality(vtq.Quality);
var tagValue = new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.TimestampUtc, TimeSpan.Zero));
if (!isGood)
return new ReadResult(false, null, "LmxProxy read returned bad quality");
return new ReadResult(true, new TagValue(value, quality, new DateTimeOffset(timestamp, TimeSpan.Zero)), null);
return vtq.Quality == LmxQuality.Bad
? new ReadResult(false, tagValue, "LmxProxy read returned bad quality")
: new ReadResult(true, tagValue, null);
}
public async Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(IEnumerable<string> tagPaths, CancellationToken cancellationToken = default)
{
EnsureConnected();
var vtqs = await _client!.ReadBatchAsync(tagPaths, cancellationToken);
var results = new Dictionary<string, ReadResult>();
foreach (var tagPath in tagPaths)
foreach (var (tag, vtq) in vtqs)
{
results[tagPath] = await ReadAsync(tagPath, cancellationToken);
var quality = MapQuality(vtq.Quality);
var tagValue = new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.TimestampUtc, TimeSpan.Zero));
results[tag] = vtq.Quality == LmxQuality.Bad
? new ReadResult(false, tagValue, "LmxProxy read returned bad quality")
: new ReadResult(true, tagValue, null);
}
return results;
}
@@ -121,20 +94,35 @@ public class LmxProxyDataConnection : IDataConnection
{
EnsureConnected();
var success = await _client!.WriteTagAsync(tagPath, value, cancellationToken);
return success
? new WriteResult(true, null)
: new WriteResult(false, "LmxProxy write failed");
try
{
await _client!.WriteAsync(tagPath, value!, cancellationToken);
return new WriteResult(true, null);
}
catch (Exception ex)
{
return new WriteResult(false, ex.Message);
}
}
public async Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(IDictionary<string, object?> values, CancellationToken cancellationToken = default)
{
var results = new Dictionary<string, WriteResult>();
foreach (var (tagPath, value) in values)
EnsureConnected();
try
{
results[tagPath] = await WriteAsync(tagPath, value, cancellationToken);
var nonNullValues = values.Where(kv => kv.Value != null)
.ToDictionary(kv => kv.Key, kv => kv.Value!);
await _client!.WriteBatchAsync(nonNullValues, cancellationToken);
return values.Keys.ToDictionary(k => k, _ => new WriteResult(true, null))
as IReadOnlyDictionary<string, WriteResult>;
}
catch (Exception ex)
{
return values.Keys.ToDictionary(k => k, _ => new WriteResult(false, ex.Message))
as IReadOnlyDictionary<string, WriteResult>;
}
return results;
}
public async Task<bool> WriteBatchAndWaitAsync(
@@ -162,10 +150,40 @@ public class LmxProxyDataConnection : IDataConnection
return false;
}
public async Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default)
{
EnsureConnected();
var subscription = await _client!.SubscribeAsync(
[tagPath],
(path, vtq) =>
{
var quality = MapQuality(vtq.Quality);
callback(path, new TagValue(vtq.Value, quality, new DateTimeOffset(vtq.TimestampUtc, TimeSpan.Zero)));
},
cancellationToken);
var subscriptionId = Guid.NewGuid().ToString("N");
_subscriptions[subscriptionId] = subscription;
return subscriptionId;
}
public async Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default)
{
if (_subscriptions.Remove(subscriptionId, out var subscription))
{
await subscription.DisposeAsync();
}
}
public async ValueTask DisposeAsync()
{
_keepAliveTimer?.Dispose();
_keepAliveTimer = null;
foreach (var subscription in _subscriptions.Values)
{
try { await subscription.DisposeAsync(); }
catch { /* best-effort cleanup */ }
}
_subscriptions.Clear();
if (_client != null)
{
@@ -181,16 +199,11 @@ public class LmxProxyDataConnection : IDataConnection
throw new InvalidOperationException("LmxProxy client is not connected.");
}
private async Task SendKeepAliveAsync()
private static QualityCode MapQuality(LmxQuality quality) => quality switch
{
try
{
if (_client?.IsConnected == true)
await _client.SendKeepAliveAsync();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "LmxProxy keep-alive failed for {Host}:{Port}", _host, _port);
}
}
LmxQuality.Good => QualityCode.Good,
LmxQuality.Uncertain => QualityCode.Uncertain,
LmxQuality.Bad => QualityCode.Bad,
_ => QualityCode.Bad
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,531 @@
// <auto-generated>
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: Adapters/Protos/scada.proto
// </auto-generated>
#pragma warning disable 0414, 1591, 8981, 0612
#region Designer generated code
using grpc = global::Grpc.Core;
namespace ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc {
/// <summary>
/// The SCADA service definition
/// </summary>
public static partial class ScadaService
{
static readonly string __ServiceName = "scada.ScadaService";
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (message is global::Google.Protobuf.IBufferMessage)
{
context.SetPayloadLength(message.CalculateSize());
global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());
context.Complete();
return;
}
#endif
context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static class __Helper_MessageCache<T>
{
public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T>
{
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
if (__Helper_MessageCache<T>.IsBufferMessage)
{
return parser.ParseFrom(context.PayloadAsReadOnlySequence());
}
#endif
return parser.ParseFrom(context.PayloadAsNewBuffer());
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest> __Marshaller_scada_ConnectRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse> __Marshaller_scada_ConnectResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest> __Marshaller_scada_DisconnectRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse> __Marshaller_scada_DisconnectResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest> __Marshaller_scada_GetConnectionStateRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse> __Marshaller_scada_GetConnectionStateResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest> __Marshaller_scada_ReadRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse> __Marshaller_scada_ReadResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest> __Marshaller_scada_ReadBatchRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse> __Marshaller_scada_ReadBatchResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest> __Marshaller_scada_WriteRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse> __Marshaller_scada_WriteResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest> __Marshaller_scada_WriteBatchRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse> __Marshaller_scada_WriteBatchResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest> __Marshaller_scada_WriteBatchAndWaitRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse> __Marshaller_scada_WriteBatchAndWaitResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest> __Marshaller_scada_SubscribeRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage> __Marshaller_scada_VtqMessage = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest> __Marshaller_scada_CheckApiKeyRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Marshaller<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse> __Marshaller_scada_CheckApiKeyResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse.Parser));
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse> __Method_Connect = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse>(
grpc::MethodType.Unary,
__ServiceName,
"Connect",
__Marshaller_scada_ConnectRequest,
__Marshaller_scada_ConnectResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse> __Method_Disconnect = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse>(
grpc::MethodType.Unary,
__ServiceName,
"Disconnect",
__Marshaller_scada_DisconnectRequest,
__Marshaller_scada_DisconnectResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse> __Method_GetConnectionState = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse>(
grpc::MethodType.Unary,
__ServiceName,
"GetConnectionState",
__Marshaller_scada_GetConnectionStateRequest,
__Marshaller_scada_GetConnectionStateResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse> __Method_Read = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse>(
grpc::MethodType.Unary,
__ServiceName,
"Read",
__Marshaller_scada_ReadRequest,
__Marshaller_scada_ReadResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse> __Method_ReadBatch = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse>(
grpc::MethodType.Unary,
__ServiceName,
"ReadBatch",
__Marshaller_scada_ReadBatchRequest,
__Marshaller_scada_ReadBatchResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse> __Method_Write = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse>(
grpc::MethodType.Unary,
__ServiceName,
"Write",
__Marshaller_scada_WriteRequest,
__Marshaller_scada_WriteResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse> __Method_WriteBatch = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse>(
grpc::MethodType.Unary,
__ServiceName,
"WriteBatch",
__Marshaller_scada_WriteBatchRequest,
__Marshaller_scada_WriteBatchResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse> __Method_WriteBatchAndWait = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse>(
grpc::MethodType.Unary,
__ServiceName,
"WriteBatchAndWait",
__Marshaller_scada_WriteBatchAndWaitRequest,
__Marshaller_scada_WriteBatchAndWaitResponse);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage> __Method_Subscribe = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage>(
grpc::MethodType.ServerStreaming,
__ServiceName,
"Subscribe",
__Marshaller_scada_SubscribeRequest,
__Marshaller_scada_VtqMessage);
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
static readonly grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse> __Method_CheckApiKey = new grpc::Method<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest, global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse>(
grpc::MethodType.Unary,
__ServiceName,
"CheckApiKey",
__Marshaller_scada_CheckApiKeyRequest,
__Marshaller_scada_CheckApiKeyResponse);
/// <summary>Service descriptor</summary>
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
{
get { return global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ScadaReflection.Descriptor.Services[0]; }
}
/// <summary>Client for ScadaService</summary>
public partial class ScadaServiceClient : grpc::ClientBase<ScadaServiceClient>
{
/// <summary>Creates a new client for ScadaService</summary>
/// <param name="channel">The channel to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public ScadaServiceClient(grpc::ChannelBase channel) : base(channel)
{
}
/// <summary>Creates a new client for ScadaService that uses a custom <c>CallInvoker</c>.</summary>
/// <param name="callInvoker">The callInvoker to use to make remote calls.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public ScadaServiceClient(grpc::CallInvoker callInvoker) : base(callInvoker)
{
}
/// <summary>Protected parameterless constructor to allow creation of test doubles.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected ScadaServiceClient() : base()
{
}
/// <summary>Protected constructor to allow creation of configured clients.</summary>
/// <param name="configuration">The client configuration.</param>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected ScadaServiceClient(ClientBaseConfiguration configuration) : base(configuration)
{
}
/// <summary>
/// Connection management
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse Connect(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Connect(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Connection management
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse Connect(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Connect, null, options, request);
}
/// <summary>
/// Connection management
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse> ConnectAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return ConnectAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Connection management
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectResponse> ConnectAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ConnectRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Connect, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse Disconnect(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Disconnect(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse Disconnect(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Disconnect, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse> DisconnectAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return DisconnectAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectResponse> DisconnectAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.DisconnectRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Disconnect, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse GetConnectionState(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return GetConnectionState(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse GetConnectionState(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_GetConnectionState, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse> GetConnectionStateAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return GetConnectionStateAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateResponse> GetConnectionStateAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.GetConnectionStateRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_GetConnectionState, null, options, request);
}
/// <summary>
/// Read operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse Read(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Read(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Read operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse Read(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Read, null, options, request);
}
/// <summary>
/// Read operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse> ReadAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return ReadAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Read operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadResponse> ReadAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Read, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse ReadBatch(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return ReadBatch(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse ReadBatch(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_ReadBatch, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse> ReadBatchAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return ReadBatchAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchResponse> ReadBatchAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.ReadBatchRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_ReadBatch, null, options, request);
}
/// <summary>
/// Write operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse Write(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Write(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Write operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse Write(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_Write, null, options, request);
}
/// <summary>
/// Write operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse> WriteAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WriteAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Write operations
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteResponse> WriteAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_Write, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse WriteBatch(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WriteBatch(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse WriteBatch(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_WriteBatch, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse> WriteBatchAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WriteBatchAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchResponse> WriteBatchAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_WriteBatch, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse WriteBatchAndWait(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WriteBatchAndWait(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse WriteBatchAndWait(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_WriteBatchAndWait, null, options, request);
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse> WriteBatchAndWaitAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return WriteBatchAndWaitAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitResponse> WriteBatchAndWaitAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.WriteBatchAndWaitRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_WriteBatchAndWait, null, options, request);
}
/// <summary>
/// Subscription operations (server streaming) - now streams VtqMessage directly
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage> Subscribe(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return Subscribe(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Subscription operations (server streaming) - now streams VtqMessage directly
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncServerStreamingCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.VtqMessage> Subscribe(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.SubscribeRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncServerStreamingCall(__Method_Subscribe, null, options, request);
}
/// <summary>
/// Authentication
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse CheckApiKey(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CheckApiKey(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Authentication
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The response received from the server.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse CheckApiKey(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest request, grpc::CallOptions options)
{
return CallInvoker.BlockingUnaryCall(__Method_CheckApiKey, null, options, request);
}
/// <summary>
/// Authentication
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="headers">The initial metadata to send with the call. This parameter is optional.</param>
/// <param name="deadline">An optional deadline for the call. The call will be cancelled if deadline is hit.</param>
/// <param name="cancellationToken">An optional token for canceling the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse> CheckApiKeyAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
{
return CheckApiKeyAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
}
/// <summary>
/// Authentication
/// </summary>
/// <param name="request">The request to send to the server.</param>
/// <param name="options">The options for the call.</param>
/// <returns>The call object.</returns>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
public virtual grpc::AsyncUnaryCall<global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyResponse> CheckApiKeyAsync(global::ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc.CheckApiKeyRequest request, grpc::CallOptions options)
{
return CallInvoker.AsyncUnaryCall(__Method_CheckApiKey, null, options, request);
}
/// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
protected override ScadaServiceClient NewInstance(ClientBaseConfiguration configuration)
{
return new ScadaServiceClient(configuration);
}
}
}
}
#endregion

View File

@@ -0,0 +1,166 @@
syntax = "proto3";
option csharp_namespace = "ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc";
package scada;
// The SCADA service definition
service ScadaService {
// Connection management
rpc Connect(ConnectRequest) returns (ConnectResponse);
rpc Disconnect(DisconnectRequest) returns (DisconnectResponse);
rpc GetConnectionState(GetConnectionStateRequest) returns (GetConnectionStateResponse);
// Read operations
rpc Read(ReadRequest) returns (ReadResponse);
rpc ReadBatch(ReadBatchRequest) returns (ReadBatchResponse);
// Write operations
rpc Write(WriteRequest) returns (WriteResponse);
rpc WriteBatch(WriteBatchRequest) returns (WriteBatchResponse);
rpc WriteBatchAndWait(WriteBatchAndWaitRequest) returns (WriteBatchAndWaitResponse);
// Subscription operations (server streaming) - now streams VtqMessage directly
rpc Subscribe(SubscribeRequest) returns (stream VtqMessage);
// Authentication
rpc CheckApiKey(CheckApiKeyRequest) returns (CheckApiKeyResponse);
}
// === CONNECTION MESSAGES ===
message ConnectRequest {
string client_id = 1;
string api_key = 2;
}
message ConnectResponse {
bool success = 1;
string message = 2;
string session_id = 3;
}
message DisconnectRequest {
string session_id = 1;
}
message DisconnectResponse {
bool success = 1;
string message = 2;
}
message GetConnectionStateRequest {
string session_id = 1;
}
message GetConnectionStateResponse {
bool is_connected = 1;
string client_id = 2;
int64 connected_since_utc_ticks = 3;
}
// === VTQ MESSAGE ===
message VtqMessage {
string tag = 1;
string value = 2;
int64 timestamp_utc_ticks = 3;
string quality = 4; // "Good", "Uncertain", "Bad"
}
// === READ MESSAGES ===
message ReadRequest {
string session_id = 1;
string tag = 2;
}
message ReadResponse {
bool success = 1;
string message = 2;
VtqMessage vtq = 3;
}
message ReadBatchRequest {
string session_id = 1;
repeated string tags = 2;
}
message ReadBatchResponse {
bool success = 1;
string message = 2;
repeated VtqMessage vtqs = 3;
}
// === WRITE MESSAGES ===
message WriteRequest {
string session_id = 1;
string tag = 2;
string value = 3;
}
message WriteResponse {
bool success = 1;
string message = 2;
}
message WriteItem {
string tag = 1;
string value = 2;
}
message WriteResult {
string tag = 1;
bool success = 2;
string message = 3;
}
message WriteBatchRequest {
string session_id = 1;
repeated WriteItem items = 2;
}
message WriteBatchResponse {
bool success = 1;
string message = 2;
repeated WriteResult results = 3;
}
message WriteBatchAndWaitRequest {
string session_id = 1;
repeated WriteItem items = 2;
string flag_tag = 3;
string flag_value = 4;
int32 timeout_ms = 5;
int32 poll_interval_ms = 6;
}
message WriteBatchAndWaitResponse {
bool success = 1;
string message = 2;
repeated WriteResult write_results = 3;
bool flag_reached = 4;
int32 elapsed_ms = 5;
}
// === SUBSCRIPTION MESSAGES ===
message SubscribeRequest {
string session_id = 1;
repeated string tags = 2;
int32 sampling_ms = 3;
}
// Note: Subscribe RPC now streams VtqMessage directly (defined above)
// === AUTHENTICATION MESSAGES ===
message CheckApiKeyRequest {
string api_key = 1;
}
message CheckApiKeyResponse {
bool is_valid = 1;
string message = 2;
}

View File

@@ -0,0 +1,196 @@
using System.Net.Http;
using Grpc.Core;
using Grpc.Net.Client;
using ScadaLink.DataConnectionLayer.Adapters.LmxProxy.Grpc;
namespace ScadaLink.DataConnectionLayer.Adapters;
/// <summary>
/// Production ILmxProxyClient that talks to the LmxProxy gRPC service
/// using proto-generated client stubs with x-api-key header injection.
/// </summary>
internal class RealLmxProxyClient : ILmxProxyClient
{
private readonly string _host;
private readonly int _port;
private readonly string? _apiKey;
private GrpcChannel? _channel;
private ScadaService.ScadaServiceClient? _client;
private string? _sessionId;
private Metadata? _headers;
public RealLmxProxyClient(string host, int port, string? apiKey)
{
_host = host;
_port = port;
_apiKey = apiKey;
}
public bool IsConnected => _client != null && !string.IsNullOrEmpty(_sessionId);
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
_channel = GrpcChannel.ForAddress($"http://{_host}:{_port}");
_client = new ScadaService.ScadaServiceClient(_channel);
_headers = new Metadata();
if (!string.IsNullOrEmpty(_apiKey))
_headers.Add("x-api-key", _apiKey);
var response = await _client.ConnectAsync(new ConnectRequest
{
ClientId = $"ScadaLink-{Guid.NewGuid():N}",
ApiKey = _apiKey ?? string.Empty
}, _headers, cancellationToken: cancellationToken);
if (!response.Success)
throw new InvalidOperationException($"LmxProxy connect failed: {response.Message}");
_sessionId = response.SessionId;
}
public async Task DisconnectAsync()
{
if (_client != null && !string.IsNullOrEmpty(_sessionId))
{
try { await _client.DisconnectAsync(new DisconnectRequest { SessionId = _sessionId }, _headers); }
catch { /* best-effort */ }
}
_client = null;
_sessionId = null;
}
public async Task<LmxVtq> ReadAsync(string address, CancellationToken cancellationToken = default)
{
EnsureConnected();
var response = await _client!.ReadAsync(
new ReadRequest { SessionId = _sessionId!, Tag = address },
_headers, cancellationToken: cancellationToken);
if (!response.Success)
throw new InvalidOperationException($"Read failed for '{address}': {response.Message}");
return ConvertVtq(response.Vtq);
}
public async Task<IDictionary<string, LmxVtq>> ReadBatchAsync(IEnumerable<string> addresses, CancellationToken cancellationToken = default)
{
EnsureConnected();
var request = new ReadBatchRequest { SessionId = _sessionId! };
request.Tags.AddRange(addresses);
var response = await _client!.ReadBatchAsync(request, _headers, cancellationToken: cancellationToken);
if (!response.Success)
throw new InvalidOperationException($"ReadBatch failed: {response.Message}");
return response.Vtqs.ToDictionary(v => v.Tag, v => ConvertVtq(v));
}
public async Task WriteAsync(string address, object value, CancellationToken cancellationToken = default)
{
EnsureConnected();
var response = await _client!.WriteAsync(new WriteRequest
{
SessionId = _sessionId!,
Tag = address,
Value = value?.ToString() ?? string.Empty
}, _headers, cancellationToken: cancellationToken);
if (!response.Success)
throw new InvalidOperationException($"Write failed for '{address}': {response.Message}");
}
public async Task WriteBatchAsync(IDictionary<string, object> values, CancellationToken cancellationToken = default)
{
EnsureConnected();
var request = new WriteBatchRequest { SessionId = _sessionId! };
request.Items.AddRange(values.Select(kv => new WriteItem
{
Tag = kv.Key,
Value = kv.Value?.ToString() ?? string.Empty
}));
var response = await _client!.WriteBatchAsync(request, _headers, cancellationToken: cancellationToken);
if (!response.Success)
throw new InvalidOperationException($"WriteBatch failed: {response.Message}");
}
public Task<ILmxSubscription> SubscribeAsync(IEnumerable<string> addresses, Action<string, LmxVtq> onUpdate, CancellationToken cancellationToken = default)
{
EnsureConnected();
var tags = addresses.ToList();
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var request = new SubscribeRequest { SessionId = _sessionId!, SamplingMs = 0 };
request.Tags.AddRange(tags);
var call = _client!.Subscribe(request, _headers, cancellationToken: cts.Token);
_ = Task.Run(async () =>
{
try
{
while (await call.ResponseStream.MoveNext(cts.Token))
{
var msg = call.ResponseStream.Current;
onUpdate(msg.Tag, ConvertVtq(msg));
}
}
catch (OperationCanceledException) { }
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled) { }
}, cts.Token);
return Task.FromResult<ILmxSubscription>(new CtsSubscription(cts));
}
public async ValueTask DisposeAsync()
{
await DisconnectAsync();
_channel?.Dispose();
_channel = null;
}
private void EnsureConnected()
{
if (_client == null || string.IsNullOrEmpty(_sessionId))
throw new InvalidOperationException("LmxProxy client is not connected.");
}
private static LmxVtq ConvertVtq(VtqMessage? msg)
{
if (msg == null)
return new LmxVtq(null, DateTime.UtcNow, LmxQuality.Bad);
object? value = msg.Value;
if (!string.IsNullOrEmpty(msg.Value))
{
if (double.TryParse(msg.Value, out var d)) value = d;
else if (bool.TryParse(msg.Value, out var b)) value = b;
else value = msg.Value;
}
var timestamp = new DateTime(msg.TimestampUtcTicks, DateTimeKind.Utc);
var quality = msg.Quality?.ToUpperInvariant() switch
{
"GOOD" => LmxQuality.Good,
"UNCERTAIN" => LmxQuality.Uncertain,
_ => LmxQuality.Bad
};
return new LmxVtq(value, timestamp, quality);
}
private sealed class CtsSubscription(CancellationTokenSource cts) : ILmxSubscription
{
public ValueTask DisposeAsync()
{
cts.Cancel();
cts.Dispose();
return ValueTask.CompletedTask;
}
}
}
/// <summary>
/// Production factory that creates real LmxProxy gRPC clients.
/// </summary>
public class RealLmxProxyClientFactory : ILmxProxyClientFactory
{
public ILmxProxyClient Create(string host, int port, string? apiKey)
=> new RealLmxProxyClient(host, port, apiKey);
}

View File

@@ -20,8 +20,8 @@ public class DataConnectionFactory : IDataConnectionFactory
// Register built-in protocols
RegisterAdapter("OpcUa", details => new OpcUaDataConnection(
new RealOpcUaClientFactory(), _loggerFactory.CreateLogger<OpcUaDataConnection>()));
RegisterAdapter("LmxProxy", details => new LmxProxyDataConnection(
new DefaultLmxProxyClientFactory(), _loggerFactory.CreateLogger<LmxProxyDataConnection>()));
RegisterAdapter("LmxProxy", _ => new LmxProxyDataConnection(
new RealLmxProxyClientFactory(), _loggerFactory.CreateLogger<LmxProxyDataConnection>()));
}
/// <summary>

View File

@@ -15,6 +15,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.5" />
<PackageReference Include="Google.Protobuf" Version="3.33.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.378.106" />
</ItemGroup>