using System; using System.Collections.Generic; using AlarmMgrDataProviderCOM; using aaAlarmManagedClient; namespace MxGateway.Worker.MxAccess; /// /// PR A.5 — production backed by /// aaAlarmManagedClient.AlarmClient. Forwards /// GetAlarmChangesCompleted events into the worker's event queue /// via . /// /// /// /// The AVEVA alarm-manager surface (IAlarmMgrDataProvider) /// exposes the events we need as plain .NET events — no Windows /// message pump required. The worker keeps its STA thread for /// MxAccess COM but the alarm-client callbacks arrive on the /// AVEVA managed-client's internal callback thread. /// /// /// The constructor parameters that /// takes (hWnd, product / application / version names, /// retain-hidden flag) are pinned to safe defaults; the live /// hWnd is intentionally IntPtr.Zero because we use /// the managed-event surface, not the WM_APP pump. Verify /// on dev rig that RegisterConsumer with /// hWnd=0 still wires the managed event handlers; if it /// requires a real hWnd, the worker creates a hidden message-only /// window and passes that handle here. /// /// public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer { private const string DefaultProductName = "OtOpcUa.MxGateway"; private const string DefaultApplicationName = "OtOpcUa.MxGateway.Worker"; private const string DefaultVersion = "1.0"; private readonly AlarmClient client; private readonly object subscribeLock = new object(); private bool disposed; public AlarmClientConsumer() : this(new AlarmClient()) { } /// Test seam — inject a pre-created . internal AlarmClientConsumer(AlarmClient client) { this.client = client ?? throw new ArgumentNullException(nameof(client)); } /// public event EventHandler? AlarmRecordReceived; /// public void Subscribe(string subscription) { if (subscription is null) throw new ArgumentNullException(nameof(subscription)); if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); lock (subscribeLock) { // hWnd=0: AVEVA's managed event surface routes through the // GetAlarmChangesCompleted .NET event, not a window-message pump. // Verify on dev rig that 0 is accepted; if not, supply a hidden // message-only window's handle here. int registerResult = client.RegisterConsumer( hWnd: 0, szProductName: DefaultProductName, szApplicationName: DefaultApplicationName, szVersion: DefaultVersion, bRetainHiddenAlarms: false); if (registerResult != 0) { throw new InvalidOperationException( $"AlarmClient.RegisterConsumer returned non-zero status {registerResult}."); } int subscribeResult = client.Subscribe( szSubscription: subscription, wFromPri: 1, wToPri: 999, QueryType: eQueryType.qtSummary, SortFlags: eSortFlags.sfReturnNewestFirst, FilterMask: eAlarmFilterState.asNone, FilterSpecification: eAlarmFilterState.asNone); if (subscribeResult != 0) { throw new InvalidOperationException( $"AlarmClient.Subscribe('{subscription}') returned non-zero status {subscribeResult}."); } } } /// public int AcknowledgeByGuid( Guid alarmGuid, string ackComment, string ackOperatorName, string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) { if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); return client.AlarmAckByGUID( alarmGuid, ackComment ?? string.Empty, ackOperatorName ?? string.Empty, ackOperatorNode ?? string.Empty, ackOperatorDomain ?? string.Empty, ackOperatorFullName ?? string.Empty); } /// public IReadOnlyList SnapshotActiveAlarms() { if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); // Walk the alarm-client's view of currently-active alarms via // GetStatistics + GetAlarmExtendedRec. The exact iteration semantics // (whether ChangePos points at the active set or at the recently- // changed set) need dev-rig validation; this method is a stub-grade // walker that reports the count it found. int percent = 0, total = 0, active = 0, suppressed = 0; int suppressedFilters = 0, newAlarms = 0, changes = 0; int[] codes = Array.Empty(); int[] positions = Array.Empty(); int[] handles = Array.Empty(); int statsResult = client.GetStatistics( ref percent, ref total, ref active, ref suppressed, ref suppressedFilters, ref newAlarms, ref changes, ref codes, ref positions, ref handles); if (statsResult != 0 || positions == null) { return Array.Empty(); } List records = new List(positions.Length); foreach (int pos in positions) { AlarmRecord record = new AlarmRecord(); int recResult = client.GetAlarmExtendedRec(pos, ref record); if (recResult == 0) { records.Add(record); } } return records; } /// /// Forward an alarm record to subscribers. Exposed internal so the /// dev-rig hookup that wires the AVEVA alarm-changes callback can /// route into the same event-fan-out path tests use. /// internal void RaiseAlarmRecordReceived(AlarmRecord record) { AlarmRecordReceived?.Invoke(this, record); } /// public void Dispose() { if (disposed) return; disposed = true; try { client.DeregisterConsumer(); } catch { } try { client.Dispose(); } catch { } } }