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,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchestrA.MxAccess;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Implementation
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IScadaClient" /> using ArchestrA MxAccess.
|
||||
/// Provides connection management, read/write operations, and subscription support for SCADA tags.
|
||||
/// </summary>
|
||||
public sealed partial class MxAccessClient : IScadaClient
|
||||
{
|
||||
private const int DefaultMaxConcurrency = 10;
|
||||
private static readonly ILogger Logger = Log.ForContext<MxAccessClient>();
|
||||
private readonly ConnectionConfiguration _configuration;
|
||||
|
||||
private readonly object _lock = new();
|
||||
private readonly Dictionary<int, WriteOperation> _pendingWrites = new();
|
||||
|
||||
// Concurrency control for batch operations
|
||||
private readonly SemaphoreSlim _readSemaphore;
|
||||
|
||||
// Store subscription details for automatic recreation after reconnect
|
||||
private readonly List<StoredSubscription> _storedSubscriptions = new();
|
||||
private readonly Dictionary<string, SubscriptionInfo> _subscriptions = new();
|
||||
private readonly Dictionary<int, SubscriptionInfo> _subscriptionsByHandle = new();
|
||||
private readonly SemaphoreSlim _writeSemaphore;
|
||||
private int _connectionHandle;
|
||||
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
||||
private bool _disposed;
|
||||
private LMXProxyServer? _lmxProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MxAccessClient" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The connection configuration settings.</param>
|
||||
public MxAccessClient(ConnectionConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
|
||||
// Initialize semaphores with configurable concurrency limits
|
||||
int maxConcurrency = _configuration.MaxConcurrentOperations ?? DefaultMaxConcurrency;
|
||||
_readSemaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
||||
_writeSemaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lmxProxy != null && _connectionState == ConnectionState.Connected && _connectionHandle > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionState ConnectionState
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _connectionState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the connection state changes.
|
||||
/// </summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DisconnectAsync();
|
||||
_disposed = true;
|
||||
|
||||
// Dispose semaphores
|
||||
_readSemaphore?.Dispose();
|
||||
_writeSemaphore?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => DisposeAsync().GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the connection state and raises the <see cref="ConnectionStateChanged" /> event.
|
||||
/// </summary>
|
||||
/// <param name="newState">The new connection state.</param>
|
||||
/// <param name="message">Optional message describing the state change.</param>
|
||||
private void SetConnectionState(ConnectionState newState, string? message = null)
|
||||
{
|
||||
ConnectionState previousState = _connectionState;
|
||||
if (previousState == newState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_connectionState = newState;
|
||||
Logger.Information("Connection state changed from {Previous} to {Current}", previousState, newState);
|
||||
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(previousState, newState, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a stored subscription group by its ID.
|
||||
/// </summary>
|
||||
/// <param name="groupId">The group identifier to remove.</param>
|
||||
private void RemoveStoredSubscription(string groupId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_storedSubscriptions.RemoveAll(s => s.GroupId == groupId);
|
||||
Logger.Debug("Removed stored subscription group {GroupId}", groupId);
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0169 // Field is never used - reserved for future functionality
|
||||
private string? _currentNodeName;
|
||||
private string? _currentGalaxyName;
|
||||
#pragma warning restore CS0169
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user