# 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.