Files
mxaccessgw/docs/AlarmClientDiscovery.md
T
Joseph Doherty 6e356da092 docs: AlarmClient public surface — managed-event premise wrong, WM_APP required
Reflection probe of the deployed aaAlarmManagedClient.dll
(v1.0.7368.41290) on 2026-05-01 confirmed the public AlarmClient class
exposes zero public events. The PR A.5 design that AlarmClientConsumer
is built on (managed-event surface, no message pump) does not hold
against this assembly.

The actual notification mechanism is WM_APP messaging:
RegisterConsumer(hWnd, ...) takes a window handle because AVEVA's alarm
provider WM_APP-pokes the registered window, then GetStatistics +
GetAlarmExtendedRec pull the change set on each poke.

Practical impact:

- AlarmClientConsumer.AlarmRecordReceived has no production caller.
  RaiseAlarmRecordReceived is invoked only from tests. Subscribe(...)
  returns OK from RegisterConsumer + Subscribe but no notifications
  reach the consumer at runtime because no window is attached.
- Until A.2 lands a hidden message-only window + WindowProc that routes
  WM_APP into MxAccessAlarmEventSink.EnqueueTransition, the gateway's
  MX_EVENT_FAMILY_ON_ALARM_TRANSITION family cannot carry events.
- AcknowledgeByGuid and SnapshotActiveAlarms are pull-style and remain
  correct as written.

Changes:

- docs/AlarmClientDiscovery.md (new) — reflection probe summary, full
  AlarmClient method list, open questions for A.2 implementation.
- AlarmClientConsumer.cs xmldoc — replaced the inaccurate "managed
  event surface" claim with the WM_APP finding; flagged
  AlarmRecordReceived as unreachable in production until the WM_APP
  pump lands.
- MxAccessAlarmEventSink.cs xmldoc — replaced the "verify on dev rig"
  hedge in the wiring plan with the resolved finding; expanded the
  open-questions list (WM_APP message ID, wParam/lParam semantics, STA
  affinity, subscription scope) so the next A.2 PR knows what the
  dev-rig probe needs to answer.

Code-only no-op for the worker; worker builds clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 06:50:57 -04:00

5.5 KiB

aaAlarmManagedClient discovery — public surface, 2026-05-01

Result of running MxGateway.Worker.Tests.AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface against the deployed AVEVA assembly:

  • File: C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll
  • Assembly identity: aaAlarmManagedClient, Version=1.0.7368.41290, Culture=neutral, PublicKeyToken=7ebd82b507d9e10c

Public types

  • aaAlarmManagedClient.AlarmClient (class)
  • aaAlarmManagedClient.PriorityData (class)

That's the entire exported surface — two types, no interfaces, no delegates.

AlarmClient events

None. The class has no public events at all. The reflection probe's GetEvents(BindingFlags.Public | Instance | Static) returned an empty list.

AlarmClient methods (relevant subset)

  • Lifecycle: RegisterConsumer(int hWnd, string szProductName, string szApplicationName, string szVersion, bool bRetainHiddenAlarms) → int, DeregisterConsumer() → int, InitializeConsumer(string szApplicationName) → int, UninitializeConsumer() → int, Dispose().
  • Subscription: Subscribe(string szSubscription, short wFromPri, short wToPri, eQueryType QueryType, eSortFlags SortFlags, eAlarmFilterState FilterMask, eAlarmFilterState FilterSpecification) → int.
  • Change enumeration (pull on poke): GetStatistics(out int lPercentQuery, out int lTotalAlarms, out int lActiveAlarms, out int lSuppressedAlarms, out int lSuppressedFilters, out int lNewAlarms, out int lChangesCount, out int[] ChangeCodes, out int[] ChangePos, out int[] hAlarm) → int.
  • Record fetch: GetAlarmExtendedRec(int lIndex, out AlarmRecord almRec) → int, GetAlarmExtendedRec2(...), GetHighPriAlarm(out AlarmRecord almRec) → int.
  • Selection model (used by ack-selected-* family): DeselectAll, SelectAlaramEntry(short select, int from, int to), SelectByGUID(Guid), SelectAlarmCount(int from, int to).
  • Acknowledge: AlarmAckByGUID(Guid alarmGuid, string ackComment, string ackOprName, string ackOprNode, string ackOprDomain, string ackOprFullName) → int is the per-alarm full-fidelity native ack. AlarmAckSelected(string ackComment, string ackOprName, string ackOprNode, string ackOprDomain, string ackOprFullName) → int acks whatever the selection model currently has selected. Several AckSelected*Group/Tag/Priority/All/Visible*Alarms_Ex(...) variants exist for bulk ack scoped to a group / tag / priority range.
  • Suppress / shelve: SupressSelected* and ShelveSelected* families plus DoAlarmShelveAction(...). Out of scope for the v1 alarm path.
  • Snapshot/filter (SF* prefix): SFSetSortA / SFSetFilterA / SFCreateSnapshot / SFGetListCount / SFDeleteSnapshot / SFRefreshAlarm / SFGetStatistics. Snapshot-style query API, distinct from the consumer-subscription path. Not currently used.

What this means

The architecture comment on src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs (PR A.5) is wrong against this deployed assembly:

"The AVEVA alarm-manager surface (IAlarmMgrDataProvider) exposes the events we need as plain .NET events — no Windows message pump required."

There is no managed event surface. AlarmClient.RegisterConsumer takes an hWnd because WM_APP messaging is the actual notification mechanism: AVEVA's alarm provider WM_APP-pokes the registered window, and the consumer is expected to call GetStatistics on each poke to pull ChangeCodes / ChangePos / hAlarm arrays, then GetAlarmExtendedRec(pos, …) per index to fetch each changed record.

AlarmClientConsumer.AlarmRecordReceived has no production callers as a result — RaiseAlarmRecordReceived is internal for tests and never gets invoked at runtime. Until A.2 lands a WM_APP pump, MX_EVENT_FAMILY_ON_ALARM_TRANSITION cannot carry events.

Implications for A.2

The original plan banner was right: A.2 needs a hidden message-only window inside the worker's STA whose hWnd is passed to RegisterConsumer, with a WindowProc that intercepts the AVEVA WM_APP message and routes change-enumeration into MxAccessAlarmEventSink.EnqueueTransition. Open questions before implementation:

  1. WM_APP message ID. Not in the public surface — needs either AVEVA's C++ Toolkit reference (canonical doc per gateway.md) or a runtime probe (subclass a window, log every WM arriving while a live alarm is fired, identify the AVEVA one). Worth doing once on the dev rig and checking the result in.
  2. wParam / lParam semantics. Probably none — the pattern is "got poked; pull state via GetStatistics." Confirm during the probe.
  3. Threading. AVEVA almost certainly delivers the WM on the thread that owns the window. The worker's STA is the natural home; the existing StaRuntime already runs a pump there. If AVEVA assumes a UI thread (MTA-incompatible call paths inside GetStatistics), the alarm path may need its own STA.
  4. Subscription scope. AlarmClient.Subscribe(szSubscription, …) takes an AVEVA-syntax string for the alarm provider (e.g. "\Galaxy!" for a whole Galaxy or "\Galaxy!Group.Tag" for a subset). The configured Galaxy name is already known to the worker via the existing data session — reuse it.

PR A.5's Subscribe / AcknowledgeByGuid / SnapshotActiveAlarms are correct — they're pull-style and don't depend on the missing event surface. The event-subscription wiring is what has to be replaced.