158 lines
5.6 KiB
C#
158 lines
5.6 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.Domain;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|
{
|
|
/// <summary>
|
|
/// Wraps the ArchestrA MXAccess COM API. All COM operations
|
|
/// execute via Task.Run (thread pool / MTA), relying on COM
|
|
/// marshaling to handle cross-apartment calls.
|
|
/// </summary>
|
|
public sealed partial class MxAccessClient : IScadaClient
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext<MxAccessClient>();
|
|
|
|
private readonly object _lock = new object();
|
|
private readonly int _maxConcurrentOperations;
|
|
private readonly int _readTimeoutMs;
|
|
private readonly int _writeTimeoutMs;
|
|
private readonly int _monitorIntervalMs;
|
|
private readonly bool _autoReconnect;
|
|
private readonly string? _nodeName;
|
|
private readonly string? _galaxyName;
|
|
|
|
private readonly SemaphoreSlim _readSemaphore;
|
|
private readonly SemaphoreSlim _writeSemaphore;
|
|
|
|
// COM objects
|
|
private LMXProxyServer? _lmxProxy;
|
|
private int _connectionHandle;
|
|
|
|
// State
|
|
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
|
private DateTime _connectedSince;
|
|
private bool _disposed;
|
|
|
|
// Reconnect
|
|
private CancellationTokenSource? _reconnectCts;
|
|
|
|
// Probe configuration
|
|
private readonly string? _probeTestTagAddress;
|
|
private readonly int _probeTimeoutMs;
|
|
private readonly int _maxConsecutiveTransportFailures;
|
|
private readonly int _degradedProbeIntervalMs;
|
|
|
|
// Probe state
|
|
private int _consecutiveTransportFailures;
|
|
private bool _isDegraded;
|
|
|
|
// Stored subscriptions for reconnect replay
|
|
private readonly Dictionary<string, Action<string, Vtq>> _storedSubscriptions
|
|
= new Dictionary<string, Action<string, Vtq>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Handle-to-address mapping for resolving COM callbacks
|
|
private readonly Dictionary<int, string> _handleToAddress = new Dictionary<int, string>();
|
|
|
|
// Address-to-handle mapping for unsubscribe by address
|
|
private readonly Dictionary<string, int> _addressToHandle
|
|
= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Pending write operations tracked by item handle
|
|
private readonly Dictionary<int, TaskCompletionSource<bool>> _pendingWrites
|
|
= new Dictionary<int, TaskCompletionSource<bool>>();
|
|
|
|
public MxAccessClient(
|
|
int maxConcurrentOperations = 10,
|
|
int readTimeoutSeconds = 5,
|
|
int writeTimeoutSeconds = 5,
|
|
int monitorIntervalSeconds = 5,
|
|
bool autoReconnect = true,
|
|
string? nodeName = null,
|
|
string? galaxyName = null,
|
|
string? probeTestTagAddress = null,
|
|
int probeTimeoutMs = 5000,
|
|
int maxConsecutiveTransportFailures = 3,
|
|
int degradedProbeIntervalMs = 30000)
|
|
{
|
|
_maxConcurrentOperations = maxConcurrentOperations;
|
|
_readTimeoutMs = readTimeoutSeconds * 1000;
|
|
_writeTimeoutMs = writeTimeoutSeconds * 1000;
|
|
_monitorIntervalMs = monitorIntervalSeconds * 1000;
|
|
_autoReconnect = autoReconnect;
|
|
_nodeName = nodeName;
|
|
_galaxyName = galaxyName;
|
|
_probeTestTagAddress = probeTestTagAddress;
|
|
_probeTimeoutMs = probeTimeoutMs;
|
|
_maxConsecutiveTransportFailures = maxConsecutiveTransportFailures;
|
|
_degradedProbeIntervalMs = degradedProbeIntervalMs;
|
|
|
|
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
|
_writeSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
|
}
|
|
|
|
public bool IsConnected
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _lmxProxy != null
|
|
&& _connectionState == ConnectionState.Connected
|
|
&& _connectionHandle > 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public ConnectionState ConnectionState
|
|
{
|
|
get { lock (_lock) { return _connectionState; } }
|
|
}
|
|
|
|
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
|
|
|
private void SetState(ConnectionState newState, string? message = null)
|
|
{
|
|
ConnectionState previousState;
|
|
lock (_lock)
|
|
{
|
|
previousState = _connectionState;
|
|
_connectionState = newState;
|
|
}
|
|
|
|
if (previousState != newState)
|
|
{
|
|
Log.Information("Connection state changed: {Previous} -> {Current} {Message}",
|
|
previousState, newState, message ?? "");
|
|
ConnectionStateChanged?.Invoke(this,
|
|
new ConnectionStateChangedEventArgs(previousState, newState, message));
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
_reconnectCts?.Cancel();
|
|
|
|
try
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error during disposal disconnect");
|
|
}
|
|
|
|
_readSemaphore.Dispose();
|
|
_writeSemaphore.Dispose();
|
|
_reconnectCts?.Dispose();
|
|
}
|
|
}
|
|
}
|