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,49 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the connection state of an LmxProxy client.
|
||||
/// </summary>
|
||||
public enum ConnectionState
|
||||
{
|
||||
/// <summary>Not connected to the server.</summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>Connection attempt in progress.</summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>Connected and ready for operations.</summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>Graceful disconnect in progress.</summary>
|
||||
Disconnecting,
|
||||
|
||||
/// <summary>Connection failed with an error.</summary>
|
||||
Error,
|
||||
|
||||
/// <summary>Attempting to re-establish a lost connection.</summary>
|
||||
Reconnecting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for connection state change notifications.
|
||||
/// </summary>
|
||||
public class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>The previous connection state.</summary>
|
||||
public ConnectionState OldState { get; }
|
||||
|
||||
/// <summary>The new connection state.</summary>
|
||||
public ConnectionState NewState { get; }
|
||||
|
||||
/// <summary>Optional message describing the state change (e.g., error details).</summary>
|
||||
public string? Message { get; }
|
||||
|
||||
public ConnectionStateChangedEventArgs(ConnectionState oldState, ConnectionState newState, string? message = null)
|
||||
{
|
||||
OldState = oldState;
|
||||
NewState = newState;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// OPC-style quality codes for SCADA data values.
|
||||
/// Based on OPC DA quality encoding as a single byte:
|
||||
/// bits 7–6 = major (00=Bad, 01=Uncertain, 11=Good),
|
||||
/// bits 5–2 = substatus, bits 1–0 = limit (00=None, 01=Low, 10=High, 11=Constant).
|
||||
/// </summary>
|
||||
public enum Quality : byte
|
||||
{
|
||||
/// <summary>Bad – non-specific.</summary>
|
||||
Bad = 0,
|
||||
|
||||
/// <summary>Bad – configuration error in the server.</summary>
|
||||
Bad_ConfigError = 4,
|
||||
|
||||
/// <summary>Bad – input source is not connected.</summary>
|
||||
Bad_NotConnected = 8,
|
||||
|
||||
/// <summary>Bad – device failure detected.</summary>
|
||||
Bad_DeviceFailure = 12,
|
||||
|
||||
/// <summary>Bad – sensor failure detected.</summary>
|
||||
Bad_SensorFailure = 16,
|
||||
|
||||
/// <summary>Bad – last known value (communication lost, value stale).</summary>
|
||||
Bad_LastKnownValue = 20,
|
||||
|
||||
/// <summary>Bad – communication failure.</summary>
|
||||
Bad_CommFailure = 24,
|
||||
|
||||
/// <summary>Bad – item is out of service.</summary>
|
||||
Bad_OutOfService = 28,
|
||||
|
||||
/// <summary>Uncertain – non-specific.</summary>
|
||||
Uncertain = 64,
|
||||
|
||||
/// <summary>Uncertain – non-specific, low limited.</summary>
|
||||
Uncertain_LowLimited = 65,
|
||||
|
||||
/// <summary>Uncertain – non-specific, high limited.</summary>
|
||||
Uncertain_HighLimited = 66,
|
||||
|
||||
/// <summary>Uncertain – non-specific, constant.</summary>
|
||||
Uncertain_Constant = 67,
|
||||
|
||||
/// <summary>Uncertain – last usable value.</summary>
|
||||
Uncertain_LastUsable = 68,
|
||||
|
||||
/// <summary>Uncertain – last usable value, low limited.</summary>
|
||||
Uncertain_LastUsable_LL = 69,
|
||||
|
||||
/// <summary>Uncertain – last usable value, high limited.</summary>
|
||||
Uncertain_LastUsable_HL = 70,
|
||||
|
||||
/// <summary>Uncertain – last usable value, constant.</summary>
|
||||
Uncertain_LastUsable_Cnst = 71,
|
||||
|
||||
/// <summary>Uncertain – sensor not accurate.</summary>
|
||||
Uncertain_SensorNotAcc = 80,
|
||||
|
||||
/// <summary>Uncertain – sensor not accurate, low limited.</summary>
|
||||
Uncertain_SensorNotAcc_LL = 81,
|
||||
|
||||
/// <summary>Uncertain – sensor not accurate, high limited.</summary>
|
||||
Uncertain_SensorNotAcc_HL = 82,
|
||||
|
||||
/// <summary>Uncertain – sensor not accurate, constant.</summary>
|
||||
Uncertain_SensorNotAcc_C = 83,
|
||||
|
||||
/// <summary>Uncertain – engineering units exceeded.</summary>
|
||||
Uncertain_EuExceeded = 84,
|
||||
|
||||
/// <summary>Uncertain – engineering units exceeded, low limited.</summary>
|
||||
Uncertain_EuExceeded_LL = 85,
|
||||
|
||||
/// <summary>Uncertain – engineering units exceeded, high limited.</summary>
|
||||
Uncertain_EuExceeded_HL = 86,
|
||||
|
||||
/// <summary>Uncertain – engineering units exceeded, constant.</summary>
|
||||
Uncertain_EuExceeded_C = 87,
|
||||
|
||||
/// <summary>Uncertain – sub-normal operating conditions.</summary>
|
||||
Uncertain_SubNormal = 88,
|
||||
|
||||
/// <summary>Uncertain – sub-normal, low limited.</summary>
|
||||
Uncertain_SubNormal_LL = 89,
|
||||
|
||||
/// <summary>Uncertain – sub-normal, high limited.</summary>
|
||||
Uncertain_SubNormal_HL = 90,
|
||||
|
||||
/// <summary>Uncertain – sub-normal, constant.</summary>
|
||||
Uncertain_SubNormal_C = 91,
|
||||
|
||||
/// <summary>Good – non-specific.</summary>
|
||||
Good = 192,
|
||||
|
||||
/// <summary>Good – low limited.</summary>
|
||||
Good_LowLimited = 193,
|
||||
|
||||
/// <summary>Good – high limited.</summary>
|
||||
Good_HighLimited = 194,
|
||||
|
||||
/// <summary>Good – constant.</summary>
|
||||
Good_Constant = 195,
|
||||
|
||||
/// <summary>Good – local override active.</summary>
|
||||
Good_LocalOverride = 216,
|
||||
|
||||
/// <summary>Good – local override active, low limited.</summary>
|
||||
Good_LocalOverride_LL = 217,
|
||||
|
||||
/// <summary>Good – local override active, high limited.</summary>
|
||||
Good_LocalOverride_HL = 218,
|
||||
|
||||
/// <summary>Good – local override active, constant.</summary>
|
||||
Good_LocalOverride_C = 219
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
public static class QualityExtensions
|
||||
{
|
||||
public static bool IsGood(this Quality q) => (byte)q >= 128;
|
||||
public static bool IsUncertain(this Quality q) => (byte)q is >= 64 and < 128;
|
||||
public static bool IsBad(this Quality q) => (byte)q < 64;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Value, Timestamp, and Quality structure for SCADA data.
|
||||
/// </summary>
|
||||
/// <param name="Value">The value.</param>
|
||||
/// <param name="Timestamp">The timestamp when the value was read.</param>
|
||||
/// <param name="Quality">The quality of the value.</param>
|
||||
public readonly record struct Vtq(object? Value, DateTime Timestamp, Quality Quality)
|
||||
{
|
||||
/// <summary>Creates a new VTQ with the specified value and quality, using the current UTC timestamp.</summary>
|
||||
public static Vtq New(object? value, Quality quality) => new(value, DateTime.UtcNow, quality);
|
||||
|
||||
/// <summary>Creates a new VTQ with the specified value, timestamp, and quality.</summary>
|
||||
public static Vtq New(object? value, DateTime timestamp, Quality quality) => new(value, timestamp, quality);
|
||||
|
||||
/// <summary>Creates a Good-quality VTQ with the current UTC time.</summary>
|
||||
public static Vtq Good(object? value) => new(value, DateTime.UtcNow, Quality.Good);
|
||||
|
||||
/// <summary>Creates a Bad-quality VTQ with the current UTC time.</summary>
|
||||
public static Vtq Bad(object? value = null) => new(value, DateTime.UtcNow, Quality.Bad);
|
||||
|
||||
/// <summary>Creates an Uncertain-quality VTQ with the current UTC time.</summary>
|
||||
public static Vtq Uncertain(object? value) => new(value, DateTime.UtcNow, Quality.Uncertain);
|
||||
}
|
||||
Reference in New Issue
Block a user