feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using System.ServiceModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Service contract
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Code-first gRPC service contract for SCADA operations.
|
||||
/// </summary>
|
||||
[ServiceContract(Name = "scada.ScadaService")]
|
||||
public interface IScadaService
|
||||
{
|
||||
/// <summary>Establishes a connection with the SCADA service.</summary>
|
||||
ValueTask<ConnectResponse> ConnectAsync(ConnectRequest request);
|
||||
|
||||
/// <summary>Terminates a SCADA service connection.</summary>
|
||||
ValueTask<DisconnectResponse> DisconnectAsync(DisconnectRequest request);
|
||||
|
||||
/// <summary>Retrieves the current state of a SCADA connection.</summary>
|
||||
ValueTask<GetConnectionStateResponse> GetConnectionStateAsync(GetConnectionStateRequest request);
|
||||
|
||||
/// <summary>Reads a single tag value from the SCADA system.</summary>
|
||||
ValueTask<ReadResponse> ReadAsync(ReadRequest request);
|
||||
|
||||
/// <summary>Reads multiple tag values from the SCADA system in a batch operation.</summary>
|
||||
ValueTask<ReadBatchResponse> ReadBatchAsync(ReadBatchRequest request);
|
||||
|
||||
/// <summary>Writes a single value to a tag in the SCADA system.</summary>
|
||||
ValueTask<WriteResponse> WriteAsync(WriteRequest request);
|
||||
|
||||
/// <summary>Writes multiple values to tags in the SCADA system in a batch operation.</summary>
|
||||
ValueTask<WriteBatchResponse> WriteBatchAsync(WriteBatchRequest request);
|
||||
|
||||
/// <summary>Writes multiple values and waits for a completion flag before returning.</summary>
|
||||
ValueTask<WriteBatchAndWaitResponse> WriteBatchAndWaitAsync(WriteBatchAndWaitRequest request);
|
||||
|
||||
/// <summary>Subscribes to real-time value changes from specified tags.</summary>
|
||||
IAsyncEnumerable<VtqMessage> SubscribeAsync(SubscribeRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Validates an API key for authentication.</summary>
|
||||
ValueTask<CheckApiKeyResponse> CheckApiKeyAsync(CheckApiKeyRequest request);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// VTQ message
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Value-Timestamp-Quality message transmitted over gRPC.
|
||||
/// All values are string-encoded; timestamps are UTC ticks.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class VtqMessage
|
||||
{
|
||||
/// <summary>Tag address.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Value encoded as a string.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>UTC timestamp as DateTime.Ticks (100ns intervals since 0001-01-01).</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public long TimestampUtcTicks { get; set; }
|
||||
|
||||
/// <summary>Quality string: "Good", "Uncertain", or "Bad".</summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string Quality { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Connect
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to establish a session with the proxy server.</summary>
|
||||
[DataContract]
|
||||
public class ConnectRequest
|
||||
{
|
||||
/// <summary>Client identifier (e.g., "ScadaLink-{guid}").</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>API key for authentication (empty if none required).</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response from a Connect call.</summary>
|
||||
[DataContract]
|
||||
public class ConnectResponse
|
||||
{
|
||||
/// <summary>Whether the connection was established successfully.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Status or error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Session ID (32-char hex GUID). Only valid when <see cref="Success"/> is <c>true</c>.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Disconnect
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to terminate a session.</summary>
|
||||
[DataContract]
|
||||
public class DisconnectRequest
|
||||
{
|
||||
/// <summary>Active session ID to disconnect.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response from a Disconnect call.</summary>
|
||||
[DataContract]
|
||||
public class DisconnectResponse
|
||||
{
|
||||
/// <summary>Whether the disconnect succeeded.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Status or error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// GetConnectionState
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to query connection state for a session.</summary>
|
||||
[DataContract]
|
||||
public class GetConnectionStateRequest
|
||||
{
|
||||
/// <summary>Session ID to query.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response with connection state information.</summary>
|
||||
[DataContract]
|
||||
public class GetConnectionStateResponse
|
||||
{
|
||||
/// <summary>Whether the session is currently connected.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
/// <summary>Client identifier for this session.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>UTC ticks when the connection was established.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public long ConnectedSinceUtcTicks { get; set; }
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Read
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to read a single tag.</summary>
|
||||
[DataContract]
|
||||
public class ReadRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag address to read.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response from a single-tag Read call.</summary>
|
||||
[DataContract]
|
||||
public class ReadResponse
|
||||
{
|
||||
/// <summary>Whether the read succeeded.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Error message if the read failed.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The value-timestamp-quality result.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public VtqMessage? Vtq { get; set; }
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// ReadBatch
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to read multiple tags in a single round-trip.</summary>
|
||||
[DataContract]
|
||||
public class ReadBatchRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag addresses to read.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<string> Tags { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Response from a batch Read call.</summary>
|
||||
[DataContract]
|
||||
public class ReadBatchResponse
|
||||
{
|
||||
/// <summary>False if any tag read failed.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>VTQ results in the same order as the request tags.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<VtqMessage> Vtqs { get; set; } = [];
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Write
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to write a single tag value.</summary>
|
||||
[DataContract]
|
||||
public class WriteRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag address to write.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Value as a string (parsed server-side).</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response from a single-tag Write call.</summary>
|
||||
[DataContract]
|
||||
public class WriteResponse
|
||||
{
|
||||
/// <summary>Whether the write succeeded.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Status or error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// WriteItem / WriteResult
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>A single tag-value pair for batch write operations.</summary>
|
||||
[DataContract]
|
||||
public class WriteItem
|
||||
{
|
||||
/// <summary>Tag address.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Value as a string.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Per-item result from a batch write operation.</summary>
|
||||
[DataContract]
|
||||
public class WriteResult
|
||||
{
|
||||
/// <summary>Tag address that was written.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether the individual write succeeded.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Error message for this item, if any.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// WriteBatch
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to write multiple tag values in a single round-trip.</summary>
|
||||
[DataContract]
|
||||
public class WriteBatchRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag-value pairs to write.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<WriteItem> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Response from a batch Write call.</summary>
|
||||
[DataContract]
|
||||
public class WriteBatchResponse
|
||||
{
|
||||
/// <summary>Overall success — false if any item failed.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Status or error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Per-item write results.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<WriteResult> Results { get; set; } = [];
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// WriteBatchAndWait
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Request to write multiple tag values then poll a flag tag
|
||||
/// until it matches an expected value or the timeout expires.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class WriteBatchAndWaitRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag-value pairs to write.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<WriteItem> Items { get; set; } = [];
|
||||
|
||||
/// <summary>Tag to poll after writes complete.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public string FlagTag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Expected value for the flag tag (string comparison).</summary>
|
||||
[DataMember(Order = 4)]
|
||||
public string FlagValue { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Timeout in milliseconds (default 5000 if <= 0).</summary>
|
||||
[DataMember(Order = 5)]
|
||||
public int TimeoutMs { get; set; }
|
||||
|
||||
/// <summary>Poll interval in milliseconds (default 100 if <= 0).</summary>
|
||||
[DataMember(Order = 6)]
|
||||
public int PollIntervalMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Response from a WriteBatchAndWait call.</summary>
|
||||
[DataContract]
|
||||
public class WriteBatchAndWaitResponse
|
||||
{
|
||||
/// <summary>Overall operation success.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>Status or error message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Per-item write results.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public List<WriteResult> WriteResults { get; set; } = [];
|
||||
|
||||
/// <summary>Whether the flag tag matched the expected value before timeout.</summary>
|
||||
[DataMember(Order = 4)]
|
||||
public bool FlagReached { get; set; }
|
||||
|
||||
/// <summary>Total elapsed time in milliseconds.</summary>
|
||||
[DataMember(Order = 5)]
|
||||
public int ElapsedMs { get; set; }
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Subscribe
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to subscribe to value change notifications on one or more tags.</summary>
|
||||
[DataContract]
|
||||
public class SubscribeRequest
|
||||
{
|
||||
/// <summary>Valid session ID.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string SessionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Tag addresses to monitor.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
/// <summary>Backend sampling interval in milliseconds.</summary>
|
||||
[DataMember(Order = 3)]
|
||||
public int SamplingMs { get; set; }
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// CheckApiKey
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Request to validate an API key without creating a session.</summary>
|
||||
[DataContract]
|
||||
public class CheckApiKeyRequest
|
||||
{
|
||||
/// <summary>API key to validate.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Response from an API key validation check.</summary>
|
||||
[DataContract]
|
||||
public class CheckApiKeyResponse
|
||||
{
|
||||
/// <summary>Whether the API key is valid.</summary>
|
||||
[DataMember(Order = 1)]
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>Validation message.</summary>
|
||||
[DataMember(Order = 2)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
Reference in New Issue
Block a user