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,23 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-client subscription statistics.
|
||||
/// </summary>
|
||||
public class ClientStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the number of tags the client is subscribed to.
|
||||
/// </summary>
|
||||
public int SubscribedTags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of delivered messages.
|
||||
/// </summary>
|
||||
public long DeliveredMessages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of dropped messages.
|
||||
/// </summary>
|
||||
public long DroppedMessages { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the state of a SCADA client connection.
|
||||
/// </summary>
|
||||
public enum ConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// The client is disconnected.
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// The client is in the process of connecting.
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// The client is connected.
|
||||
/// </summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>
|
||||
/// The client is in the process of disconnecting.
|
||||
/// </summary>
|
||||
Disconnecting,
|
||||
|
||||
/// <summary>
|
||||
/// The client encountered an error.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The client is reconnecting after a connection loss.
|
||||
/// </summary>
|
||||
Reconnecting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Event arguments for SCADA client connection state changes.
|
||||
/// </summary>
|
||||
public class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionStateChangedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="previousState">The previous connection state.</param>
|
||||
/// <param name="currentState">The current connection state.</param>
|
||||
/// <param name="message">Optional message providing additional information about the state change.</param>
|
||||
public ConnectionStateChangedEventArgs(ConnectionState previousState, ConnectionState currentState,
|
||||
string? message = null)
|
||||
{
|
||||
PreviousState = previousState;
|
||||
CurrentState = currentState;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previous connection state.
|
||||
/// </summary>
|
||||
public ConnectionState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection state.
|
||||
/// </summary>
|
||||
public ConnectionState CurrentState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the state change occurred.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional information about the state change, such as error messages.
|
||||
/// </summary>
|
||||
public string? Message { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for SCADA system clients.
|
||||
/// </summary>
|
||||
public interface IScadaClient : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the connection status.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection state.
|
||||
/// </summary>
|
||||
ConnectionState ConnectionState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the connection state changes.
|
||||
/// </summary>
|
||||
event EventHandler<ConnectionStateChangedEventArgs> ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task ConnectAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task DisconnectAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single tag value from the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="address">The tag address.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The value, timestamp, and quality.</returns>
|
||||
Task<Vtq> ReadAsync(string address, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple tag values from the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="addresses">The tag addresses.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Dictionary of address to VTQ values.</returns>
|
||||
Task<IReadOnlyDictionary<string, Vtq>>
|
||||
ReadBatchAsync(IEnumerable<string> addresses, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single tag value to the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="address">The tag address.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task WriteAsync(string address, object value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes multiple tag values to the SCADA system.
|
||||
/// </summary>
|
||||
/// <param name="values">Dictionary of address to value.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task WriteBatchAsync(IReadOnlyDictionary<string, object> values, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a batch of tag values and a flag tag, then waits for a response tag to
|
||||
/// equal the expected value.
|
||||
/// </summary>
|
||||
/// <param name="values">The regular tag values to write.</param>
|
||||
/// <param name="flagAddress">The address of the flag tag to write.</param>
|
||||
/// <param name="flagValue">The value to write to the flag tag.</param>
|
||||
/// <param name="responseAddress">The address of the response tag to monitor.</param>
|
||||
/// <param name="responseValue">The expected value of the response tag.</param>
|
||||
/// <param name="ct">Cancellation token controlling the wait.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the response value was observed before cancellation;
|
||||
/// otherwise <c>false</c>.
|
||||
/// </returns>
|
||||
Task<bool> WriteBatchAndWaitAsync(
|
||||
IReadOnlyDictionary<string, object> values,
|
||||
string flagAddress,
|
||||
object flagValue,
|
||||
string responseAddress,
|
||||
object responseValue,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to value changes for specified addresses.
|
||||
/// </summary>
|
||||
/// <param name="addresses">The tag addresses to monitor.</param>
|
||||
/// <param name="callback">Callback for value changes.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Subscription handle for unsubscribing.</returns>
|
||||
Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
124
lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Domain/Quality.cs
Normal file
124
lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Domain/Quality.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// OPC quality codes mapped to domain-level values.
|
||||
/// The byte value matches the low-order byte of the OPC UA StatusCode,
|
||||
/// so it can be persisted or round-tripped without translation.
|
||||
/// </summary>
|
||||
public enum Quality : byte
|
||||
{
|
||||
// ─────────────── Bad family (0-31) ───────────────
|
||||
/// <summary>0x00 – Bad [Non-Specific]</summary>
|
||||
Bad = 0,
|
||||
|
||||
/// <summary>0x01 – Unknown quality value</summary>
|
||||
Unknown = 1,
|
||||
|
||||
/// <summary>0x04 – Bad [Configuration Error]</summary>
|
||||
Bad_ConfigError = 4,
|
||||
|
||||
/// <summary>0x08 – Bad [Not Connected]</summary>
|
||||
Bad_NotConnected = 8,
|
||||
|
||||
/// <summary>0x0C – Bad [Device Failure]</summary>
|
||||
Bad_DeviceFailure = 12,
|
||||
|
||||
/// <summary>0x10 – Bad [Sensor Failure]</summary>
|
||||
Bad_SensorFailure = 16,
|
||||
|
||||
/// <summary>0x14 – Bad [Last Known Value]</summary>
|
||||
Bad_LastKnownValue = 20,
|
||||
|
||||
/// <summary>0x18 – Bad [Communication Failure]</summary>
|
||||
Bad_CommFailure = 24,
|
||||
|
||||
/// <summary>0x1C – Bad [Out of Service]</summary>
|
||||
Bad_OutOfService = 28,
|
||||
|
||||
// ──────────── Uncertain family (64-95) ───────────
|
||||
/// <summary>0x40 – Uncertain [Non-Specific]</summary>
|
||||
Uncertain = 64,
|
||||
|
||||
/// <summary>0x41 – Uncertain [Non-Specific] (Low Limited)</summary>
|
||||
Uncertain_LowLimited = 65,
|
||||
|
||||
/// <summary>0x42 – Uncertain [Non-Specific] (High Limited)</summary>
|
||||
Uncertain_HighLimited = 66,
|
||||
|
||||
/// <summary>0x43 – Uncertain [Non-Specific] (Constant)</summary>
|
||||
Uncertain_Constant = 67,
|
||||
|
||||
/// <summary>0x44 – Uncertain [Last Usable]</summary>
|
||||
Uncertain_LastUsable = 68,
|
||||
|
||||
/// <summary>0x45 – Uncertain [Last Usable] (Low Limited)</summary>
|
||||
Uncertain_LastUsable_LL = 69,
|
||||
|
||||
/// <summary>0x46 – Uncertain [Last Usable] (High Limited)</summary>
|
||||
Uncertain_LastUsable_HL = 70,
|
||||
|
||||
/// <summary>0x47 – Uncertain [Last Usable] (Constant)</summary>
|
||||
Uncertain_LastUsable_Cnst = 71,
|
||||
|
||||
/// <summary>0x50 – Uncertain [Sensor Not Accurate]</summary>
|
||||
Uncertain_SensorNotAcc = 80,
|
||||
|
||||
/// <summary>0x51 – Uncertain [Sensor Not Accurate] (Low Limited)</summary>
|
||||
Uncertain_SensorNotAcc_LL = 81,
|
||||
|
||||
/// <summary>0x52 – Uncertain [Sensor Not Accurate] (High Limited)</summary>
|
||||
Uncertain_SensorNotAcc_HL = 82,
|
||||
|
||||
/// <summary>0x53 – Uncertain [Sensor Not Accurate] (Constant)</summary>
|
||||
Uncertain_SensorNotAcc_C = 83,
|
||||
|
||||
/// <summary>0x54 – Uncertain [EU Exceeded]</summary>
|
||||
Uncertain_EuExceeded = 84,
|
||||
|
||||
/// <summary>0x55 – Uncertain [EU Exceeded] (Low Limited)</summary>
|
||||
Uncertain_EuExceeded_LL = 85,
|
||||
|
||||
/// <summary>0x56 – Uncertain [EU Exceeded] (High Limited)</summary>
|
||||
Uncertain_EuExceeded_HL = 86,
|
||||
|
||||
/// <summary>0x57 – Uncertain [EU Exceeded] (Constant)</summary>
|
||||
Uncertain_EuExceeded_C = 87,
|
||||
|
||||
/// <summary>0x58 – Uncertain [Sub-Normal]</summary>
|
||||
Uncertain_SubNormal = 88,
|
||||
|
||||
/// <summary>0x59 – Uncertain [Sub-Normal] (Low Limited)</summary>
|
||||
Uncertain_SubNormal_LL = 89,
|
||||
|
||||
/// <summary>0x5A – Uncertain [Sub-Normal] (High Limited)</summary>
|
||||
Uncertain_SubNormal_HL = 90,
|
||||
|
||||
/// <summary>0x5B – Uncertain [Sub-Normal] (Constant)</summary>
|
||||
Uncertain_SubNormal_C = 91,
|
||||
|
||||
// ─────────────── Good family (192-219) ────────────
|
||||
/// <summary>0xC0 – Good [Non-Specific]</summary>
|
||||
Good = 192,
|
||||
|
||||
/// <summary>0xC1 – Good [Non-Specific] (Low Limited)</summary>
|
||||
Good_LowLimited = 193,
|
||||
|
||||
/// <summary>0xC2 – Good [Non-Specific] (High Limited)</summary>
|
||||
Good_HighLimited = 194,
|
||||
|
||||
/// <summary>0xC3 – Good [Non-Specific] (Constant)</summary>
|
||||
Good_Constant = 195,
|
||||
|
||||
/// <summary>0xD8 – Good [Local Override]</summary>
|
||||
Good_LocalOverride = 216,
|
||||
|
||||
/// <summary>0xD9 – Good [Local Override] (Low Limited)</summary>
|
||||
Good_LocalOverride_LL = 217,
|
||||
|
||||
/// <summary>0xDA – Good [Local Override] (High Limited)</summary>
|
||||
Good_LocalOverride_HL = 218,
|
||||
|
||||
/// <summary>0xDB – Good [Local Override] (Constant)</summary>
|
||||
Good_LocalOverride_C = 219
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscription statistics for all clients and tags.
|
||||
/// </summary>
|
||||
public class SubscriptionStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of clients.
|
||||
/// </summary>
|
||||
public int TotalClients { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of tags.
|
||||
/// </summary>
|
||||
public int TotalTags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mapping of tag addresses to client counts.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> TagClientCounts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mapping of client IDs to their statistics.
|
||||
/// </summary>
|
||||
public Dictionary<string, ClientStats> ClientStats { get; set; } = new();
|
||||
}
|
||||
}
|
||||
129
lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Domain/Vtq.cs
Normal file
129
lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Domain/Vtq.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Value, Timestamp, and Quality structure for SCADA data.
|
||||
/// </summary>
|
||||
public readonly struct Vtq : IEquatable<Vtq>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
public object? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the value was read.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quality of the value.
|
||||
/// </summary>
|
||||
public Quality Quality { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Vtq" /> struct.
|
||||
/// </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 Vtq(object? value, DateTime timestamp, Quality quality)
|
||||
{
|
||||
Value = value;
|
||||
Timestamp = timestamp;
|
||||
Quality = quality;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Vtq" /> instance with the specified value and quality, using the current UTC timestamp.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="quality">The quality of the value.</param>
|
||||
/// <returns>A new <see cref="Vtq" /> instance.</returns>
|
||||
public static Vtq New(object value, Quality quality) => new(value, DateTime.UtcNow, quality);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Vtq" /> instance with the specified value, timestamp, and quality.
|
||||
/// </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>
|
||||
/// <returns>A new <see cref="Vtq" /> instance.</returns>
|
||||
public static Vtq New(object value, DateTime timestamp, Quality quality) => new(value, timestamp, quality);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Vtq" /> instance with good quality and the current UTC timestamp.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>A new <see cref="Vtq" /> instance with good quality.</returns>
|
||||
public static Vtq Good(object value) => new(value, DateTime.UtcNow, Quality.Good);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Vtq" /> instance with bad quality and the current UTC timestamp.
|
||||
/// </summary>
|
||||
/// <param name="value">The value. Optional.</param>
|
||||
/// <returns>A new <see cref="Vtq" /> instance with bad quality.</returns>
|
||||
public static Vtq Bad(object? value = null) => new(value, DateTime.UtcNow, Quality.Bad);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Vtq" /> instance with uncertain quality and the current UTC timestamp.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>A new <see cref="Vtq" /> instance with uncertain quality.</returns>
|
||||
public static Vtq Uncertain(object value) => new(value, DateTime.UtcNow, Quality.Uncertain);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="Vtq" /> is equal to the current <see cref="Vtq" />.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="Vtq" /> to compare with the current <see cref="Vtq" />.</param>
|
||||
/// <returns>true if the specified <see cref="Vtq" /> is equal to the current <see cref="Vtq" />; otherwise, false.</returns>
|
||||
public bool Equals(Vtq other) =>
|
||||
Equals(Value, other.Value) && Timestamp.Equals(other.Timestamp) && Quality == other.Quality;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified object is equal to the current <see cref="Vtq" />.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare with the current <see cref="Vtq" />.</param>
|
||||
/// <returns>true if the specified object is equal to the current <see cref="Vtq" />; otherwise, false.</returns>
|
||||
public override bool Equals(object obj) => obj is Vtq other && Equals(other);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>A 32-bit signed integer hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hashCode = Value != null ? Value.GetHashCode() : 0;
|
||||
hashCode = (hashCode * 397) ^ Timestamp.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ (int)Quality;
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current object.
|
||||
/// </summary>
|
||||
/// <returns>A string that represents the current object.</returns>
|
||||
public override string ToString() =>
|
||||
$"{{Value={Value}, Timestamp={Timestamp:yyyy-MM-dd HH:mm:ss.fff}, Quality={Quality}}}";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two specified instances of <see cref="Vtq" /> are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="Vtq" /> to compare.</param>
|
||||
/// <param name="right">The second <see cref="Vtq" /> to compare.</param>
|
||||
/// <returns>true if left and right are equal; otherwise, false.</returns>
|
||||
public static bool operator ==(Vtq left, Vtq right) => left.Equals(right);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two specified instances of <see cref="Vtq" /> are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first <see cref="Vtq" /> to compare.</param>
|
||||
/// <param name="right">The second <see cref="Vtq" /> to compare.</param>
|
||||
/// <returns>true if left and right are not equal; otherwise, false.</returns>
|
||||
public static bool operator !=(Vtq left, Vtq right) => !left.Equals(right);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user