Switch the worker's alarm-consumer surface from `aaAlarmManagedClient.AlarmClient`
to `WNWRAPCONSUMERLib.wwAlarmConsumerClass` (CLSID 7AB52E5F-…) hosted by
`wnwrapConsumer.dll`. The new path returns alarm records as a BSTR XML
payload via `GetXmlCurrentAlarms2`, bypassing the FILETIME→DateTime
auto-marshaling that crashed `GetHighPriAlarm` with
ArgumentOutOfRangeException on every poll. Live captured 60/60 polls
clean against `\DESKTOP-6JL3KKO\Galaxy!DEV` while a System Platform
script flipped TestMachine_001.TestAlarm001 every 10s; the GUID,
priority, state (UNACK_ALM ↔ UNACK_RTN), and ASCII-formatted timestamps
arrived end-to-end.
Implementation:
- `Interop.WNWRAPCONSUMERLib.dll` generated via tlbimp, checked in under
`lib/` so dev boxes don't need the SDK to build.
- New `WnWrapAlarmConsumer` (replaces `AlarmClientConsumer`): owns a
500ms polling timer, parses `GetXmlCurrentAlarms2` output, diffs the
snapshot keyed by alarm GUID, and raises one
`MxAlarmTransitionEvent` per state change. Includes the
Initialize→Register-before-Subscribe ordering fix found during
Discovery probe runs.
- New library-agnostic types `MxAlarmSnapshotRecord` /
`MxAlarmStateKind` / `MxAlarmTransitionEvent` so the proto-build
path is testable without an AVEVA install.
- `AlarmRecordTransitionMapper` retired the COM-coupled
`MapTransitionKind(eAlmTransitions)`; new pure helpers
`ParseStateKind`, `MapTransition(prev, curr)`, and
`ParseTransitionTimestampUtc` cover XML decode + state-delta logic.
- `IMxAccessAlarmConsumer` event surface changed from
`EventHandler<AlarmRecord>` to `EventHandler<MxAlarmTransitionEvent>`
and `SnapshotActiveAlarms()` returns `MxAlarmSnapshotRecord` —
decoupling the interface from any specific COM library.
- Worker csproj drops `aaAlarmManagedClient` / `IAlarmMgrDataProvider`
refs; adds `Interop.WNWRAPCONSUMERLib`.
Tests:
- 36 new unit tests (state-string mapping, prev/current → proto kind
decision table, timestamp UTC reassembly, XML payload parser, 32-char
hex GUID round-trip) covering everything that doesn't touch the live
COM surface — all passing.
- Skip-gated `WnWrapConsumerProbeTests.ProbeWnWrapConsumer` archives
the live capture flow for regression / future probes.
Docs:
- `docs/AlarmClientDiscovery.md` "Option A — captured" section records
sample XML payloads, the mangled `SetXmlAlarmQuery` round-trip
(prefer `Subscribe` for filtering), the `GetStatistics`
AccessViolationException quirk, and the worker-integration outline.
Pre-existing failure noted (separate):
`MxAccessInteropReference_ExistsOnlyInWorkerProject` was already
failing on HEAD — the test project still references `ArchestrA.MxAccess`
for the Skip-gated discovery probes. Not regressed by this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Wires the worker-side consumer for AVEVA alarm transitions over the
aaAlarmManagedClient API discovered in the prior foundation PR.
- IAlarmMgrDataProvider.dll referenced — exposes AlarmRecord +
eAlmTransitions / eQueryType / eSortFlags / eAlarmFilterState.
Both DLLs (aaAlarmManagedClient + IAlarmMgrDataProvider) load in
the worker's existing net48 x86 process; no new bitness boundary.
- IMxAccessAlarmConsumer abstraction — Subscribe / AcknowledgeByGuid
/ SnapshotActiveAlarms / AlarmRecordReceived event. Test seam.
- AlarmClientConsumer production wrapper — RegisterConsumer +
Subscribe + AlarmAckByGUID + GetStatistics-based active-alarm
walk, all delegated to AlarmClient. Uses AVEVA's managed event
surface (GetAlarmChangesCompleted on IAlarmMgrDataProvider) so
no Windows message pump is required — plain .NET events arrive
on the alarm-client's internal callback thread.
- AlarmRecordTransitionMapper — pure-function helpers:
MapTransitionKind(eAlmTransitions): ALM→Raise, ACK→Acknowledge,
RTN→Clear, others (SUB/ENB/DIS/SUP/REL/REMOVE)→Unspecified
so EventPump's decoding-failure counter records them.
ComposeFullReference(provider, group, name): Provider!Group.Name
format matching AVEVA's standard alarm-reference syntax.
Pinned during dev-rig validation (subsequent commits):
1. Confirm RegisterConsumer accepts hWnd=0 — if it requires a real
hwnd, the worker creates a hidden message-only window and
passes that handle. The managed event surface should make
this irrelevant but the AVEVA API is older than its managed
wrapper.
2. Wire AlarmClientConsumer.AlarmRecordReceived: the AVEVA
IAlarmMgrDataProvider.GetAlarmChangesCompleted event needs to
be hooked from inside the AlarmClient — find the proper
accessor (likely a property exposing the inner provider).
3. AlarmRecord field-by-field translation into the proto event
uses MxAccessAlarmEventSink.EnqueueTransition (existing
plumbing). The AlarmRecord field names (ar_OrigTime,
AlarmName, AckOperatorFullName, AckComment, etc.) are
pinned in the discovery dump preserved in
AlarmClientDiscoveryTests.
Tests: 127 pass (4 new ComposeFullReference cases + 1 Skip-gated
discovery probe). Transition-kind enum mapping is dev-rig-validated
rather than unit-tested because the AVEVA assembly is Private=false
on the reference and isn't copied to the test bin directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovers the surface of aaAlarmManagedClient.dll and stages the worker
csproj reference so subsequent PRs can wire native MxAccess alarm
subscription. Replaces the speculative "operator decision needed
between path 1 and path 2" framing in MxAccessAlarmEventSink with the
validated architecture.
Key findings from the discovery probe:
1. aaAlarmManagedClient.dll is x86 + .NET Framework (mixed-mode
C++/CLI; PE Machine = i386, NativeEntryPoint flag set). The
"x64-only" framing in the prior follow-up was wrong — confused
by the file path under Wonderware\Historian\x64\.
The assembly is bitness- and runtime-compatible with the
worker (net48 x86), so it loads in the existing process. No
sub-process needed.
2. AlarmClient is the public class. Its model mirrors MxAccess:
RegisterConsumer takes a Windows hWnd and the AVEVA alarm
service WM_APP-pokes that hwnd when alarms change. The worker's
existing STA + WM_APP pump can drive both the data-change COM
subscriber and the alarm-client consumer.
3. AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode,
oprDomain, oprFullName) — the native ack carries the operator's
full identity atomically with the comment. Closes the v1
operator-comment fidelity gap completely.
This PR:
- Adds the aaAlarmManagedClient.dll reference to MxGateway.Worker.
csproj. Worker still builds clean.
- Adds AlarmClientDiscoveryTests as a Skip-gated reflection probe;
flip the Skip parameter to dump the public type surface for
reference. Captured the dump into MxAccessAlarmEventSink
documentation so it doesn't have to be re-run.
- Replaces MxAccessAlarmEventSink's "two paths forward" doc with
the actual wiring plan against AlarmClient's RegisterConsumer +
Subscribe + AlarmAckByGUID surface.
Subsequent PRs (gated on STA + WM_APP integration testing on the
dev rig):
- Wire RegisterConsumer + Subscribe at session-startup; route
WM_APP messages through GetStatistics + GetAlarmExtendedRec into
EnqueueTransition.
- Translate gateway-side AcknowledgeAlarm RPC to a worker command
that calls AlarmAckByGUID with the OPC UA operator's identity;
replaces the worker-pending diagnostic from PR A.3.
- Translate gateway-side QueryActiveAlarms to a worker command
that walks GetStatistics's reported handles via GetAlarmExtendedRec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR A.2 ship-pin discovery: the MXAccess COM Toolkit installed at
C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll
does not expose any alarm-event family. Reflection enumeration of
the assembly confirms ILMXProxyServerEvents and
ILMXProxyServerEvents2 only carry OnDataChange, OnWriteComplete,
OperationComplete, and OnBufferedDataChange — no IAlarmEventSink,
no Alarms collection, no OnAlarmTransition.
AVEVA's separate alarm-subscription managed assemblies
(aaAlarmManagedClient.dll under InTouch\ViewAppFramework\Content\MA\
and ArchestrAAlarmsAndEvents.SDK.Common.dll under
Wonderware\Historian\x64\) exist on this box but are x64-only —
incompatible with the worker's x86 bitness, which is the bitness
constraint the mxaccessgw architecture exists to isolate in the
first place.
This commit replaces the speculative "TBD pin during dev-rig
validation" comment in MxAccessAlarmEventSink with the actual
finding plus the two operator-facing paths forward:
1. Stay on the value-driven sub-attribute path (current production
behaviour). lmxopcua's AlarmConditionService already synthesizes
Part 9 transitions from the four MXAccess sub-attributes.
Operator-comment fidelity is the only v1 regression.
2. Add an x64 alarm-helper sub-process alongside the worker that
loads aaAlarmManagedClient and forwards transitions to the
worker over a small named-pipe IPC. Recovers full v1 fidelity
but adds operational complexity.
Until that decision resolves, the sink's Attach is a no-op, the
worker continues to function for data subscriptions, and
lmxopcua-side AlarmConditionService keeps the sub-attribute
synthesis active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eighteenth PR of the alarms-over-gateway epic
(docs/plans/alarms-over-gateway.md). Lands the proto-build path that
the worker uses to create OnAlarmTransition events. The COM-side
subscription that registers an alarm event sink against the MXAccess
Toolkit is pinned during dev-rig validation — the exact API differs
across AVEVA versions and needs hardware to verify.
Lands today (unit-testable, no hardware needed):
- MxAccessEventMapper.CreateOnAlarmTransition — mechanical proto
builder. Takes decoded alarm fields (full reference, source
object, alarm type, transition kind, severity, timestamps,
operator user/comment, category, description) and produces an
MxEvent with the OnAlarmTransition body populated. Mirrors the
pattern of CreateOnDataChange / CreateOnWriteComplete / etc.
- MxAccessAlarmEventSink — scaffolded class with documented
Attach / Detach + an internal EnqueueTransition entry point.
When dev-rig validation pins the MXAccess Toolkit alarm
subscription API, the only edit needed is to wire the COM
delegate inside Attach to call EnqueueTransition. The mapper
bridge is already done.
Pending dev-rig validation:
- Pin the MXAccess Toolkit alarm event source COM API (likely one
of IAlarmEventSink, IAlarmEventSubscription, or a method on
LMXProxyServerClass — verify against the worker host's installed
version).
- Add cancellation/cleanup tests once the COM hook is wired.
- Integration test against the parity rig that fires a real Galaxy
alarm and asserts the gateway emits OnAlarmTransition.
Tests:
- 2 new mapper tests pin the full-payload Acknowledge case and
the bare-bones Raise case.
- Full Worker.Tests suite green: 123 passed (was 121; 2 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>