using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Serilog; using ZB.MOM.WW.LmxProxy.Host.Domain; namespace ZB.MOM.WW.LmxProxy.Host.MxAccess { public sealed partial class MxAccessClient { /// /// Subscribes to value changes for the specified addresses. /// Stores subscription state for reconnect replay. /// COM calls dispatched on the dedicated STA thread. /// public async Task SubscribeAsync( IEnumerable addresses, Action callback, CancellationToken ct = default) { if (!IsConnected) throw new InvalidOperationException("Not connected to MxAccess"); var addressList = addresses.ToList(); await _staThread.RunAsync(() => { lock (_lock) { if (!IsConnected || _lmxProxy == null) throw new InvalidOperationException("Not connected to MxAccess"); foreach (var address in addressList) { SubscribeInternal(address); // Store for reconnect replay _storedSubscriptions[address] = callback; } } }); Log.Information("Subscribed to {Count} tags", addressList.Count); return new SubscriptionHandle(this, addressList, callback); } /// /// Unsubscribes specific addresses by address name. /// Removes from both COM state and stored subscriptions (no reconnect replay). /// public async Task UnsubscribeByAddressAsync(IEnumerable addresses) { await UnsubscribeAsync(addresses); } /// /// Unsubscribes specific addresses. /// internal async Task UnsubscribeAsync(IEnumerable addresses) { var addressList = addresses.ToList(); await _staThread.RunAsync(() => { lock (_lock) { foreach (var address in addressList) { UnsubscribeInternal(address); _storedSubscriptions.Remove(address); } } }); Log.Information("Unsubscribed from {Count} tags", addressList.Count); } /// /// Recreates all stored subscriptions after a reconnect. /// Does not re-store them (they're already stored). /// private async Task RecreateStoredSubscriptionsAsync() { Dictionary> subscriptions; lock (_lock) { if (_storedSubscriptions.Count == 0) return; subscriptions = new Dictionary>(_storedSubscriptions); } Log.Information("Recreating {Count} stored subscriptions after reconnect", subscriptions.Count); await _staThread.RunAsync(() => { lock (_lock) { foreach (var kvp in subscriptions) { try { SubscribeInternal(kvp.Key); } catch (Exception ex) { Log.Warning(ex, "Failed to recreate subscription for {Address}", kvp.Key); } } } }); } // ── Internal COM calls ────────── /// /// Registers a tag subscription with MxAccess COM API (AddItem + AdviseSupervisory). /// Must be called while holding _lock. /// private void SubscribeInternal(string address) { if (_lmxProxy == null || _connectionHandle <= 0) throw new InvalidOperationException("Not connected to MxAccess"); // If already subscribed to this address, skip if (_addressToHandle.ContainsKey(address)) { Log.Debug("Already subscribed to {Address}, skipping", address); return; } // Add the item to MxAccess int itemHandle = _lmxProxy.AddItem(_connectionHandle, address); // Track handle-to-address and address-to-handle mappings _handleToAddress[itemHandle] = address; _addressToHandle[address] = itemHandle; // Advise (subscribe) for data change events _lmxProxy.AdviseSupervisory(_connectionHandle, itemHandle); Log.Debug("Subscribed to {Address} with handle {Handle}", address, itemHandle); } /// /// Unregisters a tag subscription from MxAccess COM API (UnAdvise + RemoveItem). /// Must be called while holding _lock. /// private void UnsubscribeInternal(string address) { if (!_addressToHandle.TryGetValue(address, out int itemHandle)) { Log.Debug("No active subscription for {Address}, skipping unsubscribe", address); return; } try { if (_lmxProxy != null && _connectionHandle > 0) { _lmxProxy.UnAdvise(_connectionHandle, itemHandle); _lmxProxy.RemoveItem(_connectionHandle, itemHandle); } } catch (Exception ex) { Log.Warning(ex, "Error unsubscribing from {Address} (handle {Handle})", address, itemHandle); } finally { _handleToAddress.Remove(itemHandle); _addressToHandle.Remove(address); } Log.Debug("Unsubscribed from {Address} (handle {Handle})", address, itemHandle); } /// /// Disposable subscription handle that unsubscribes on disposal. /// private sealed class SubscriptionHandle : IAsyncDisposable { private readonly MxAccessClient _client; private readonly List _addresses; private readonly Action _callback; private bool _disposed; public SubscriptionHandle(MxAccessClient client, List addresses, Action callback) { _client = client; _addresses = addresses; _callback = callback; } public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; await _client.UnsubscribeAsync(_addresses); } } } }