6e356da092
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>
122 lines
5.5 KiB
Markdown
122 lines
5.5 KiB
Markdown
# 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.
|