137 lines
5.0 KiB
C#
137 lines
5.0 KiB
C#
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
|
|
}
|
|
}
|