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:
@@ -39,29 +39,29 @@ Additional protocols can be added by implementing this interface.
|
||||
|
||||
### Concrete Type Mappings
|
||||
|
||||
| IDataConnection | OPC UA SDK | LmxProxy SDK (`LmxProxyClient`) |
|
||||
| IDataConnection | OPC UA SDK | LmxProxy (`RealLmxProxyClient`) |
|
||||
|---|---|---|
|
||||
| `Connect()` | OPC UA session establishment | `ConnectAsync()` → gRPC `ConnectRequest`, server returns `SessionId` |
|
||||
| `Disconnect()` | Close OPC UA session | `DisconnectAsync()` → gRPC `DisconnectRequest` |
|
||||
| `Subscribe(tagPath, callback)` | OPC UA Monitored Items | `SubscribeAsync(addresses, onUpdate)` → server-streaming gRPC (`IAsyncEnumerable<VtqMessage>`) |
|
||||
| `Unsubscribe(id)` | Remove Monitored Item | `ISubscription.DisposeAsync()` (cancels streaming RPC) |
|
||||
| `Read(tagPath)` | OPC UA Read | `ReadAsync(address)` → `Vtq` |
|
||||
| `ReadBatch(tagPaths)` | OPC UA Read (multiple nodes) | `ReadBatchAsync(addresses)` → `IDictionary<string, Vtq>` |
|
||||
| `Write(tagPath, value)` | OPC UA Write | `WriteAsync(address, value)` |
|
||||
| `WriteBatch(values)` | OPC UA Write (multiple nodes) | `WriteBatchAsync(values)` |
|
||||
| `WriteBatchAndWait(...)` | OPC UA Write + poll for confirmation | `WriteBatchAndWaitAsync(values, flagAddress, flagValue, responseAddress, responseValue, timeout)` |
|
||||
| `Status` | OPC UA session state | `IsConnected` property + keep-alive heartbeat (30-second interval via `GetConnectionStateAsync`) |
|
||||
| `Connect()` | OPC UA session establishment | gRPC `Connect` RPC with `x-api-key` metadata header, server returns `SessionId` |
|
||||
| `Disconnect()` | Close OPC UA session | gRPC `Disconnect` RPC |
|
||||
| `Subscribe(tagPath, callback)` | OPC UA Monitored Items | gRPC `Subscribe` server-streaming RPC (`stream VtqMessage`), cancelled via `CancellationTokenSource` |
|
||||
| `Unsubscribe(id)` | Remove Monitored Item | Cancel the `CancellationTokenSource` for that subscription (stops streaming RPC) |
|
||||
| `Read(tagPath)` | OPC UA Read | gRPC `Read` RPC → `VtqMessage` → `LmxVtq` |
|
||||
| `ReadBatch(tagPaths)` | OPC UA Read (multiple nodes) | gRPC `ReadBatch` RPC → `repeated VtqMessage` → `IDictionary<string, LmxVtq>` |
|
||||
| `Write(tagPath, value)` | OPC UA Write | gRPC `Write` RPC (throws on failure) |
|
||||
| `WriteBatch(values)` | OPC UA Write (multiple nodes) | gRPC `WriteBatch` RPC (throws on failure) |
|
||||
| `WriteBatchAndWait(...)` | OPC UA Write + poll for confirmation | `WriteBatch` + poll `Read` at 100ms intervals until response value matches or timeout |
|
||||
| `Status` | OPC UA session state | `IsConnected` — true when `SessionId` is non-empty |
|
||||
|
||||
### Common Value Type
|
||||
|
||||
Both protocols produce the same value tuple consumed by Instance Actors. Before the first value update arrives from the DCL, data-sourced attributes are held at **uncertain** quality by the Instance Actor (see Site Runtime — Initialization):
|
||||
|
||||
| Concept | ScadaLink Design | LmxProxy SDK (`Vtq`) |
|
||||
|---|---|---|
|
||||
| Value container | `{value, quality, timestamp}` | `Vtq(Value, Timestamp, Quality)` — readonly record struct |
|
||||
| Quality | good / bad / uncertain | `Quality` enum (byte, OPC UA compatible: Good=0xC0, Bad=0x00, Uncertain=0x40) |
|
||||
| Timestamp | UTC | `DateTime` (UTC) |
|
||||
| Value type | object | `object?` (parsed: double, bool, string) |
|
||||
| Concept | ScadaLink Design | LmxProxy Wire Format | Local Type |
|
||||
|---|---|---|---|
|
||||
| Value container | `TagValue(Value, Quality, Timestamp)` | `VtqMessage { Tag, Value, TimestampUtcTicks, Quality }` | `LmxVtq(Value, TimestampUtc, Quality)` — readonly record struct |
|
||||
| Quality | `QualityCode` enum: Good / Bad / Uncertain | String: `"Good"` / `"Uncertain"` / `"Bad"` | `LmxQuality` enum: Good / Uncertain / Bad |
|
||||
| Timestamp | `DateTimeOffset` (UTC) | `int64` (DateTime.Ticks, UTC) | `DateTime` (UTC) |
|
||||
| Value type | `object?` | `string` (parsed by client to double, bool, or string) | `object?` |
|
||||
|
||||
## Supported Protocols
|
||||
|
||||
@@ -71,31 +71,31 @@ Both protocols produce the same value tuple consumed by Instance Actors. Before
|
||||
|
||||
### LmxProxy (Custom Protocol)
|
||||
|
||||
LmxProxy is a gRPC-based protocol for communicating with LMX data servers. An existing client SDK (`LmxProxyClient` NuGet package) provides a production-ready implementation.
|
||||
LmxProxy is a gRPC-based protocol for communicating with LMX data servers. The DCL includes its own proto-generated gRPC client (`RealLmxProxyClient`) — no external SDK dependency.
|
||||
|
||||
**Transport & Connection**:
|
||||
- gRPC over HTTP/2, using protobuf-net code-first contracts (service: `scada.ScadaService`).
|
||||
- Default port: **5050**.
|
||||
- Session-based: `ConnectAsync` returns a `SessionId` used for all subsequent operations.
|
||||
- Keep-alive: 30-second heartbeat via `GetConnectionStateAsync`. On failure, the client marks itself disconnected and disposes subscriptions.
|
||||
- gRPC over HTTP/2, using proto-generated client stubs from `scada.proto` (service: `scada.ScadaService`). Pre-generated C# files are checked into `Adapters/LmxProxyGrpc/` to avoid running `protoc` in Docker (ARM64 compatibility).
|
||||
- Default port: **50051**.
|
||||
- Session-based: `Connect` RPC returns a `SessionId` used for all subsequent operations.
|
||||
- Keep-alive: Managed by the LmxProxy server's session timeout. The DCL reconnect cycle handles session loss.
|
||||
|
||||
**Authentication & TLS**:
|
||||
- API key-based authentication (sent in `ConnectRequest`).
|
||||
- Full TLS support: TLS 1.2/1.3, mutual TLS (client cert + key in PEM), custom CA trust, self-signed cert allowance for dev.
|
||||
- API key-based authentication sent as `x-api-key` gRPC metadata header on every call. The server's `ApiKeyInterceptor` validates the header before the request reaches the service method. The API key is also included in the `ConnectRequest` body for session-level validation.
|
||||
- Plain HTTP/2 (no TLS) for current deployments. The server supports TLS when configured.
|
||||
|
||||
**Subscriptions**:
|
||||
- Server-streaming gRPC (`IAsyncEnumerable<VtqMessage>`).
|
||||
- Configurable sampling interval (default: 1000ms; 0 = on-change).
|
||||
- Wire format: `VtqMessage { Tag, Value (string), TimestampUtcTicks (long), Quality (string: "Good"/"Uncertain"/"Bad") }`.
|
||||
- Subscription disposed via `ISubscription.DisposeAsync()`.
|
||||
- Server-streaming gRPC (`Subscribe` RPC returns `stream VtqMessage`).
|
||||
- Configurable sampling interval (default: 0 = on-change).
|
||||
- Wire format: `VtqMessage { tag, value (string), timestamp_utc_ticks (int64), quality (string: "Good"/"Uncertain"/"Bad") }`.
|
||||
- Subscription lifetime managed by `CancellationTokenSource` — cancellation stops the streaming RPC.
|
||||
|
||||
**Additional Capabilities (beyond IDataConnection)**:
|
||||
- Built-in retry policy via Polly: exponential backoff (base delay × 2^attempt), configurable max attempts (default: 3), applied to reads. Transient errors: `Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`.
|
||||
- Operation metrics: count, errors, p95/p99 latency (ring buffer of last 1000 samples per operation).
|
||||
- Correlation ID propagation for distributed tracing (configurable header name).
|
||||
- DI integration: `AddLmxProxyClient(IConfiguration)` binds to `"LmxProxy"` config section in `appsettings.json`.
|
||||
**Client Implementation** (`RealLmxProxyClient`):
|
||||
- Uses `Google.Protobuf` + `Grpc.Net.Client` (standard proto-generated stubs, no protobuf-net runtime IL emit).
|
||||
- `ILmxProxyClientFactory` creates instances configured with host, port, and API key.
|
||||
- Value conversion: string values from `VtqMessage` are parsed to `double`, `bool`, or left as `string`.
|
||||
- Quality mapping: `"Good"` → `LmxQuality.Good`, `"Uncertain"` → `LmxQuality.Uncertain`, else `LmxQuality.Bad`.
|
||||
|
||||
**SDK Reference**: The client SDK source is at `LmxProxyClient` in the ScadaBridge repository. The DCL's LmxProxy adapter wraps this SDK behind the `IDataConnection` interface.
|
||||
**Proto Source**: The `.proto` file originates from the LmxProxy server repository (`lmx/Proxy/Grpc/Protos/scada.proto` in ScadaBridge). The C# stubs are pre-generated and stored at `Adapters/LmxProxyGrpc/`.
|
||||
|
||||
## Subscription Management
|
||||
|
||||
@@ -130,14 +130,14 @@ Each data connection is managed by a dedicated connection actor that uses the Ak
|
||||
|
||||
This pattern ensures no messages are lost during connection transitions and is the standard Akka.NET approach for actors with I/O lifecycle dependencies.
|
||||
|
||||
**LmxProxy-specific notes**: The LmxProxy connection actor holds the `SessionId` returned by `ConnectAsync` and passes it to all subsequent operations. On entering the **Connected** state, the actor starts the 30-second keep-alive timer. Subscriptions use server-streaming gRPC — the actor processes the `IAsyncEnumerable<VtqMessage>` stream and forwards updates to Instance Actors. On keep-alive failure, the actor transitions to **Reconnecting** and the client automatically disposes active subscriptions.
|
||||
**LmxProxy-specific notes**: The `RealLmxProxyClient` holds the `SessionId` returned by the `Connect` RPC and includes it in all subsequent operations. The `LmxProxyDataConnection` adapter has no keep-alive timer — session liveness is handled by the DCL's existing reconnect cycle. Subscriptions use server-streaming gRPC — a background task reads from the `ResponseStream` and invokes the callback for each `VtqMessage`. On connection failure, the DCL actor transitions to **Reconnecting**, disposes the client (which cancels active subscriptions), and retries at the fixed interval.
|
||||
|
||||
## Connection Lifecycle & Reconnection
|
||||
|
||||
The DCL manages connection lifecycle automatically:
|
||||
|
||||
1. **Connection drop detection**: When a connection to a data source is lost, the DCL immediately pushes a value update with quality `bad` for **every tag subscribed on that connection**. Instance Actors and their downstream consumers (alarms, scripts checking quality) see the staleness immediately.
|
||||
2. **Auto-reconnect with fixed interval**: The DCL retries the connection at a configurable fixed interval (e.g., every 5 seconds). The retry interval is defined **per data connection**. This is consistent with the fixed-interval retry philosophy used throughout the system. **Note on LmxProxy**: The LmxProxy SDK includes its own retry policy (exponential backoff via Polly) for individual operations (reads). The DCL's fixed-interval reconnect owns **connection-level** recovery (re-establishing the gRPC session after a keep-alive failure or disconnect). The SDK's retry policy handles **operation-level** transient failures within an active session. These are complementary — the DCL does not disable the SDK's retry policy.
|
||||
2. **Auto-reconnect with fixed interval**: The DCL retries the connection at a configurable fixed interval (e.g., every 5 seconds). The retry interval is defined **per data connection**. This is consistent with the fixed-interval retry philosophy used throughout the system. For LmxProxy, the DCL's reconnect cycle owns all recovery — re-establishing the gRPC channel and session after any connection failure. Individual gRPC operations (reads, writes) fail immediately to the caller on error; there is no operation-level retry within the adapter.
|
||||
3. **Connection state transitions**: The DCL tracks each connection's state as `connected`, `disconnected`, or `reconnecting`. All transitions are logged to Site Event Logging.
|
||||
4. **Transparent re-subscribe**: On successful reconnection, the DCL automatically re-establishes all previously active subscriptions for that connection. Instance Actors require no action — they simply see quality return to `good` as fresh values arrive from restored subscriptions.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
5739
src/ScadaLink.DataConnectionLayer/Adapters/LmxProxyGrpc/Scada.cs
Normal file
5739
src/ScadaLink.DataConnectionLayer/Adapters/LmxProxyGrpc/Scada.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
166
src/ScadaLink.DataConnectionLayer/Adapters/Protos/scada.proto
Normal file
166
src/ScadaLink.DataConnectionLayer/Adapters/Protos/scada.proto
Normal 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;
|
||||
}
|
||||
196
src/ScadaLink.DataConnectionLayer/Adapters/RealLmxProxyClient.cs
Normal file
196
src/ScadaLink.DataConnectionLayer/Adapters/RealLmxProxyClient.cs
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ScadaLink.Commons.Interfaces.Protocol;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
using ScadaLink.DataConnectionLayer.Adapters;
|
||||
|
||||
namespace ScadaLink.DataConnectionLayer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-8: Tests for LmxProxy adapter.
|
||||
/// </summary>
|
||||
public class LmxProxyDataConnectionTests
|
||||
{
|
||||
private readonly ILmxProxyClient _mockClient;
|
||||
@@ -18,15 +17,21 @@ public class LmxProxyDataConnectionTests
|
||||
{
|
||||
_mockClient = Substitute.For<ILmxProxyClient>();
|
||||
_mockFactory = Substitute.For<ILmxProxyClientFactory>();
|
||||
_mockFactory.Create().Returns(_mockClient);
|
||||
_mockFactory.Create(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string?>()).Returns(_mockClient);
|
||||
_adapter = new LmxProxyDataConnection(_mockFactory, NullLogger<LmxProxyDataConnection>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_OpensSessionWithHostAndPort()
|
||||
private async Task ConnectAdapter(Dictionary<string, string>? details = null)
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
await _adapter.ConnectAsync(details ?? new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
// --- Connection ---
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_SetsStatusToConnected()
|
||||
{
|
||||
_mockClient.OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
@@ -36,91 +41,240 @@ public class LmxProxyDataConnectionTests
|
||||
});
|
||||
|
||||
Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
|
||||
await _mockClient.Received(1).OpenSessionAsync("myhost", 5001, Arg.Any<CancellationToken>());
|
||||
_mockFactory.Received(1).Create("myhost", 5001, null);
|
||||
await _mockClient.Received(1).ConnectAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disconnect_ClosesSession()
|
||||
public async Task Connect_ExtractsApiKeyFromDetails()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>
|
||||
{
|
||||
["Host"] = "server",
|
||||
["Port"] = "50051",
|
||||
["ApiKey"] = "my-secret-key"
|
||||
});
|
||||
|
||||
_mockFactory.Received(1).Create("server", 50051, "my-secret-key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connect_DefaultsHostAndPort()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
|
||||
_mockFactory.Received(1).Create("localhost", 50051, null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Disconnect_SetsStatusToDisconnected()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
await _adapter.DisconnectAsync();
|
||||
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
await _mockClient.Received(1).CloseSessionAsync(Arg.Any<CancellationToken>());
|
||||
await _mockClient.Received(1).DisconnectAsync();
|
||||
}
|
||||
|
||||
// --- Read ---
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Good_ReturnsSuccessWithValue()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var now = DateTime.UtcNow;
|
||||
_mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns(new LmxVtq(42.5, now, LmxQuality.Good));
|
||||
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(42.5, result.Value!.Value);
|
||||
Assert.Equal(QualityCode.Good, result.Value.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Bad_ReturnsFailureWithValue()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
_mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns(new LmxVtq(null, DateTime.UtcNow, LmxQuality.Bad));
|
||||
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.NotNull(result.Value);
|
||||
Assert.Equal(QualityCode.Bad, result.Value!.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Uncertain_MapsQuality()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
_mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns(new LmxVtq("maybe", DateTime.UtcNow, LmxQuality.Uncertain));
|
||||
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(QualityCode.Uncertain, result.Value!.Quality);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadBatch_ReturnsMappedResults()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var now = DateTime.UtcNow;
|
||||
_mockClient.ReadBatchAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new Dictionary<string, LmxVtq>
|
||||
{
|
||||
["Tag1"] = new(10, now, LmxQuality.Good),
|
||||
["Tag2"] = new(null, now, LmxQuality.Bad)
|
||||
});
|
||||
|
||||
var results = await _adapter.ReadBatchAsync(["Tag1", "Tag2"]);
|
||||
|
||||
Assert.True(results["Tag1"].Success);
|
||||
Assert.Equal(10, results["Tag1"].Value!.Value);
|
||||
Assert.False(results["Tag2"].Success);
|
||||
}
|
||||
|
||||
// --- Write ---
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Success_ReturnsGoodResult()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns(true);
|
||||
await ConnectAdapter();
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("Tag1", 42);
|
||||
|
||||
Assert.True(result.Success);
|
||||
await _mockClient.Received(1).WriteAsync("Tag1", 42, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_Failure_ReturnsError()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.WriteTagAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Returns(false);
|
||||
await ConnectAdapter();
|
||||
_mockClient.WriteAsync("Tag1", 42, Arg.Any<CancellationToken>())
|
||||
.Throws(new InvalidOperationException("Write failed for tag"));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.WriteAsync("Tag1", 42);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Equal("LmxProxy write failed", result.ErrorMessage);
|
||||
Assert.Contains("Write failed for tag", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Good_ReturnsValue()
|
||||
public async Task WriteBatch_Success_ReturnsAllGood()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((42.5, DateTime.UtcNow, true));
|
||||
await ConnectAdapter();
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
var results = await _adapter.WriteBatchAsync(new Dictionary<string, object?> { ["T1"] = 1, ["T2"] = 2 });
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(42.5, result.Value!.Value);
|
||||
Assert.True(results["T1"].Success);
|
||||
Assert.True(results["T2"].Success);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_Bad_ReturnsFailure()
|
||||
public async Task WriteBatch_Failure_ReturnsAllErrors()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(true);
|
||||
_mockClient.OpenSessionAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns("session-123");
|
||||
_mockClient.ReadTagAsync("Tag1", Arg.Any<CancellationToken>())
|
||||
.Returns((null, DateTime.UtcNow, false));
|
||||
await ConnectAdapter();
|
||||
_mockClient.WriteBatchAsync(Arg.Any<IDictionary<string, object>>(), Arg.Any<CancellationToken>())
|
||||
.Throws(new InvalidOperationException("Batch write failed"));
|
||||
|
||||
await _adapter.ConnectAsync(new Dictionary<string, string>());
|
||||
var result = await _adapter.ReadAsync("Tag1");
|
||||
var results = await _adapter.WriteBatchAsync(new Dictionary<string, object?> { ["T1"] = 1, ["T2"] = 2 });
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(results["T1"].Success);
|
||||
Assert.False(results["T2"].Success);
|
||||
Assert.Contains("Batch write failed", results["T1"].ErrorMessage);
|
||||
}
|
||||
|
||||
// --- Subscribe ---
|
||||
|
||||
[Fact]
|
||||
public async Task Subscribe_CreatesSubscriptionAndReturnsId()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
|
||||
var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
|
||||
Assert.NotNull(subId);
|
||||
Assert.NotEmpty(subId);
|
||||
await _mockClient.Received(1).SubscribeAsync(
|
||||
Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnOperations()
|
||||
public async Task Unsubscribe_DisposesSubscription()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
|
||||
var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
await _adapter.UnsubscribeAsync(subId);
|
||||
|
||||
await mockSub.Received(1).DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unsubscribe_UnknownId_DoesNotThrow()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
await _adapter.UnsubscribeAsync("nonexistent-id");
|
||||
}
|
||||
|
||||
// --- Dispose ---
|
||||
|
||||
[Fact]
|
||||
public async Task Dispose_DisposesClientAndSubscriptions()
|
||||
{
|
||||
await ConnectAdapter();
|
||||
var mockSub = Substitute.For<ILmxSubscription>();
|
||||
_mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, LmxVtq>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(mockSub);
|
||||
await _adapter.SubscribeAsync("Tag1", (_, _) => { });
|
||||
|
||||
await _adapter.DisposeAsync();
|
||||
|
||||
await mockSub.Received(1).DisposeAsync();
|
||||
await _mockClient.Received(1).DisposeAsync();
|
||||
Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
|
||||
}
|
||||
|
||||
// --- Guard ---
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnRead()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _adapter.ReadAsync("tag1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnWrite()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _adapter.WriteAsync("tag1", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotConnected_ThrowsOnSubscribe()
|
||||
{
|
||||
_mockClient.IsConnected.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_adapter.ReadAsync("tag1"));
|
||||
_adapter.SubscribeAsync("tag1", (_, _) => { }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user