using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Serilog; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; using ZB.MOM.WW.LmxOpcUa.Host.Domain; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess { /// /// Core MXAccess client implementing IMxAccessClient via IMxProxy abstraction. /// Split across partial classes: Connection, Subscription, ReadWrite, EventHandlers, Monitor. /// (MXA-001 through MXA-009) /// public sealed partial class MxAccessClient : IMxAccessClient { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly ConcurrentDictionary _addressToHandle = new(StringComparer.OrdinalIgnoreCase); private readonly MxAccessConfiguration _config; // Handle mappings private readonly ConcurrentDictionary _handleToAddress = new(); private readonly PerformanceMetrics _metrics; private readonly SemaphoreSlim _operationSemaphore; private readonly ConcurrentDictionary>> _pendingReadsByAddress = new(StringComparer.OrdinalIgnoreCase); // Pending writes private readonly ConcurrentDictionary> _pendingWrites = new(); private readonly IMxProxy _proxy; private readonly StaComThread _staThread; // Subscription storage private readonly ConcurrentDictionary> _storedSubscriptions = new(StringComparer.OrdinalIgnoreCase); private int _connectionHandle; private DateTime _lastProbeValueTime = DateTime.UtcNow; private CancellationTokenSource? _monitorCts; // Probe private string? _probeTag; private bool _proxyEventsAttached; private int _reconnectCount; private volatile ConnectionState _state = ConnectionState.Disconnected; /// /// Initializes a new MXAccess client around the STA thread, COM proxy abstraction, and runtime throttling settings. /// /// The STA thread used to marshal COM interactions. /// The COM proxy abstraction used to talk to the runtime. /// The runtime timeout, throttling, and reconnect settings. /// The metrics collector used to time MXAccess operations. public MxAccessClient(StaComThread staThread, IMxProxy proxy, MxAccessConfiguration config, PerformanceMetrics metrics) { _staThread = staThread; _proxy = proxy; _config = config; _metrics = metrics; _operationSemaphore = new SemaphoreSlim(config.MaxConcurrentOperations, config.MaxConcurrentOperations); } /// /// Gets the current runtime connection state for the MXAccess client. /// public ConnectionState State => _state; /// /// Gets the number of active tag subscriptions currently maintained against the runtime. /// public int ActiveSubscriptionCount => _storedSubscriptions.Count; /// /// Gets the number of reconnect attempts performed since the client was created. /// public int ReconnectCount => _reconnectCount; /// /// Occurs when the MXAccess connection state changes. /// public event EventHandler? ConnectionStateChanged; /// /// Occurs when a subscribed runtime tag publishes a new value. /// public event Action? OnTagValueChanged; /// /// Cancels monitoring and disconnects the runtime session before releasing local resources. /// public void Dispose() { try { _monitorCts?.Cancel(); DisconnectAsync().GetAwaiter().GetResult(); } catch (Exception ex) { Log.Warning(ex, "Error during MxAccessClient dispose"); } finally { _operationSemaphore.Dispose(); _monitorCts?.Dispose(); } } private void SetState(ConnectionState newState, string message = "") { var previous = _state; if (previous == newState) return; _state = newState; Log.Information("MxAccess state: {Previous} → {Current} {Message}", previous, newState, message); ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(previous, newState, message)); } } }