A.2: replace AlarmClientConsumer with wnwrap-based polling consumer
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>
This commit is contained in:
@@ -4,70 +4,21 @@ using MxGateway.Contracts.Proto;
|
||||
namespace MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// PR A.2 sink for native MxAccess alarm transitions. Bridges the
|
||||
/// <c>aaAlarmManagedClient.AlarmClient</c> consumer to the worker's
|
||||
/// event queue, producing <see cref="OnAlarmTransitionEvent"/> messages
|
||||
/// via <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/>.
|
||||
/// Sink for native MxAccess alarm transitions. Bridges
|
||||
/// <see cref="WnWrapAlarmConsumer"/> to the worker's event queue,
|
||||
/// producing <see cref="OnAlarmTransitionEvent"/> messages via
|
||||
/// <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <strong>Architecture (revised 2026-05-01 — see
|
||||
/// <c>docs/AlarmClientDiscovery.md</c>):</strong> the worker hosts
|
||||
/// <c>aaAlarmManagedClient.AlarmClient</c> alongside the existing
|
||||
/// <c>ArchestrA.MxAccess</c> COM consumer. Both are x86 .NET
|
||||
/// Framework 4.8. The MxAccess COM Toolkit at
|
||||
/// <c>C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll</c>
|
||||
/// exposes no alarm events; the alarm provider lives in a separate
|
||||
/// AVEVA service that <c>aaAlarmManagedClient</c> subscribes to.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Notification mechanism: WM_APP pump.</strong> A reflection
|
||||
/// probe of <c>aaAlarmManagedClient.dll</c> (v1.0.7368.41290) on
|
||||
/// 2026-05-01 confirmed the public <c>AlarmClient</c> class has zero
|
||||
/// public events. The original PR A.5 design (managed-event surface,
|
||||
/// no message pump) is incorrect against this assembly. AVEVA's
|
||||
/// alarm provider WM_APP-pokes a window registered through
|
||||
/// <c>RegisterConsumer(hWnd, …)</c>; the consumer pulls the change
|
||||
/// set via <c>GetStatistics</c> + <c>GetAlarmExtendedRec</c> on each
|
||||
/// poke. PR A.5's <see cref="AlarmClientConsumer"/> still owns the
|
||||
/// <see cref="AlarmClient"/> handle and the
|
||||
/// <see cref="AlarmClient.Subscribe"/> /
|
||||
/// <see cref="AlarmClient.AlarmAckByGUID"/> pull-style calls; only
|
||||
/// the receive path is wrong.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Discovered API surface</strong> (see
|
||||
/// <c>AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface</c> in
|
||||
/// <c>MxGateway.Worker.Tests</c> — Skip-gated reflection probe; full
|
||||
/// output captured in <c>docs/AlarmClientDiscovery.md</c>):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>RegisterConsumer(hWnd, productName, applicationName, version, retainHidden)</c> — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change.</description></item>
|
||||
/// <item><description><c>Subscribe(provider, fromPri, toPri, queryType, sortFlags, filterMask, filterSpec)</c> — subscribes to a Galaxy alarm provider with priority + filter scoping.</description></item>
|
||||
/// <item><description><c>GetStatistics(out percentQuery, totalAlarms, activeAlarms, …, out int[] changeCodes, out int[] changePos, out int[] hAlarm)</c> — called on each WM_APP poke; enumerates which alarms changed.</description></item>
|
||||
/// <item><description><c>GetAlarmExtendedRec(index, out AlarmRecord)</c> — pulls the full alarm record (operator, comment, original raise, category, severity).</description></item>
|
||||
/// <item><description><c>AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName)</c> — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <strong>Open questions before A.2 implementation</strong>
|
||||
/// (see <c>docs/AlarmClientDiscovery.md</c> "Implications for A.2"):
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><description>WM_APP message ID — not in the public surface, needs AVEVA C++ Toolkit reference or a runtime probe.</description></item>
|
||||
/// <item><description><c>wParam</c> / <c>lParam</c> semantics — likely none (the pattern is "got poked → pull state via <c>GetStatistics</c>"), but confirm during the probe.</description></item>
|
||||
/// <item><description>STA / threading affinity for the message-only window — likely the worker's existing STA, but if AVEVA assumes UI-thread inside <c>GetStatistics</c> the alarm path may need its own STA.</description></item>
|
||||
/// <item><description>Subscription scope — reuse the configured Galaxy name from the data session.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Until A.2 lands a hidden message-only window + WindowProc that
|
||||
/// routes WM_APP into <see cref="EnqueueTransition"/>,
|
||||
/// <see cref="Attach"/> is a no-op. The worker continues to function
|
||||
/// for data subscriptions, and the gateway's
|
||||
/// <see cref="MxEventFamily.OnAlarmTransition"/> family is reserved
|
||||
/// on the wire but never emitted. lmxopcua-side
|
||||
/// <c>AlarmConditionService</c> keeps the sub-attribute synthesis
|
||||
/// active and continues to surface alarms to OPC UA Part 9 clients
|
||||
/// in the meantime.
|
||||
/// The dispatcher subscribes the consumer's
|
||||
/// <see cref="IMxAccessAlarmConsumer.AlarmTransitionEmitted"/> event
|
||||
/// to <see cref="EnqueueTransition"/> at session attach time. The
|
||||
/// <see cref="Attach"/> override here is a stub kept for the data-
|
||||
/// session shape; the actual wire-up between consumer and sink
|
||||
/// lives in the A.3 dispatcher (one step up the stack). Captured
|
||||
/// payload schema and consumer threading discipline are described in
|
||||
/// <c>docs/AlarmClientDiscovery.md</c> "Option A — captured".
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
|
||||
|
||||
Reference in New Issue
Block a user