using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ZB.MOM.WW.LmxProxy.Host.Domain; namespace ZB.MOM.WW.LmxProxy.Host.Implementation { /// /// Subscription management for MxAccessClient to handle SCADA tag updates. /// public sealed partial class MxAccessClient { /// /// Subscribes to a set of addresses and registers a callback for value changes. /// /// The collection of addresses to subscribe to. /// /// The callback to invoke when a value changes. /// The callback receives the address and the new value. /// /// An optional to cancel the operation. /// /// A that completes with a handle to the subscription. /// Disposing the handle will unsubscribe from all addresses. /// /// Thrown if not connected to MxAccess. /// Thrown if subscription fails for any address. public Task SubscribeAsync(IEnumerable addresses, Action callback, CancellationToken ct = default) => SubscribeInternalAsync(addresses, callback, true, ct); /// /// Internal subscription method that allows control over whether to store the subscription for recreation. /// private Task SubscribeInternalAsync(IEnumerable addresses, Action callback, bool storeForRecreation, CancellationToken ct = default) { return Task.Run(() => { lock (_lock) { if (!IsConnected || _lmxProxy == null) { throw new InvalidOperationException("Not connected to MxAccess"); } var subscriptionIds = new List(); try { var addressList = addresses.ToList(); foreach (string? address in addressList) { // Add the item var itemHandle = _lmxProxy.AddItem(_connectionHandle, address); // Create subscription info string subscriptionId = Guid.NewGuid().ToString(); var subscription = new SubscriptionInfo { Address = address, ItemHandle = itemHandle, Callback = callback, SubscriptionId = subscriptionId }; // Store subscription _subscriptions[subscriptionId] = subscription; _subscriptionsByHandle[itemHandle] = subscription; subscriptionIds.Add(subscriptionId); // Advise the item _lmxProxy.AdviseSupervisory(_connectionHandle, itemHandle); Logger.Debug("Subscribed to {Address} with handle {Handle}", address, itemHandle); } // Store subscription group for automatic recreation after reconnect string groupId = Guid.NewGuid().ToString(); if (storeForRecreation) { _storedSubscriptions.Add(new StoredSubscription { Addresses = addressList, Callback = callback, GroupId = groupId }); Logger.Debug( "Stored subscription group {GroupId} with {Count} addresses for automatic recreation", groupId, addressList.Count); } return new SubscriptionHandle(this, subscriptionIds, groupId); } catch (Exception ex) { // Clean up any subscriptions that were created foreach (string? id in subscriptionIds) { UnsubscribeInternalAsync(id).Wait(); } Logger.Error(ex, "Failed to subscribe to addresses"); throw; } } }, ct); } /// /// Unsubscribes from a subscription by its ID. /// /// The subscription identifier. /// /// A representing the asynchronous operation. /// private Task UnsubscribeInternalAsync(string subscriptionId) { return Task.Run(() => { lock (_lock) { if (!_subscriptions.TryGetValue(subscriptionId, out SubscriptionInfo? subscription)) { return; } try { if (_lmxProxy != null && _connectionHandle > 0) { _lmxProxy.UnAdvise(_connectionHandle, subscription.ItemHandle); _lmxProxy.RemoveItem(_connectionHandle, subscription.ItemHandle); } _subscriptions.Remove(subscriptionId); _subscriptionsByHandle.Remove(subscription.ItemHandle); Logger.Debug("Unsubscribed from {Address}", subscription.Address); } catch (Exception ex) { Logger.Warning(ex, "Error unsubscribing from {Address}", subscription.Address); } } }); } } }