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 { }
}
}