From 6e356da0927e6d15dada9cb29f2d1e3ed41e5a76 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 06:50:57 -0400 Subject: [PATCH 01/16] =?UTF-8?q?docs:=20AlarmClient=20public=20surface=20?= =?UTF-8?q?=E2=80=94=20managed-event=20premise=20wrong,=20WM=5FAPP=20requi?= =?UTF-8?q?red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/AlarmClientDiscovery.md | 121 ++++++++++++++++++ .../MxAccess/AlarmClientConsumer.cs | 55 +++++--- .../MxAccess/MxAccessAlarmEventSink.cs | 51 +++++--- 3 files changed, 194 insertions(+), 33 deletions(-) create mode 100644 docs/AlarmClientDiscovery.md diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md new file mode 100644 index 0000000..ef0c95a --- /dev/null +++ b/docs/AlarmClientDiscovery.md @@ -0,0 +1,121 @@ +# 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. diff --git a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs b/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs index a78849e..ae878fa 100644 --- a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs +++ b/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs @@ -13,22 +13,39 @@ namespace MxGateway.Worker.MxAccess; /// /// /// -/// The AVEVA alarm-manager surface (IAlarmMgrDataProvider) -/// exposes the events we need as plain .NET events — no Windows -/// message pump required. The worker keeps its STA thread for -/// MxAccess COM but the alarm-client callbacks arrive on the -/// AVEVA managed-client's internal callback thread. +/// ⚠ Architecture finding (2026-05-01 reflection probe — +/// see docs/AlarmClientDiscovery.md): contrary to the +/// original PR A.5 design, aaAlarmManagedClient.AlarmClient +/// exposes zero public events on the deployed assembly +/// (aaAlarmManagedClient.dll v1.0.7368.41290). There is no +/// managed event surface. RegisterConsumer(hWnd, …) takes a +/// window handle because the actual notification mechanism is +/// WM_APP-pump messaging — AVEVA's alarm provider WM_APP-pokes the +/// registered window, and the consumer pulls the change set via +/// GetStatistics + GetAlarmExtendedRec on each poke. /// /// -/// The constructor parameters that -/// takes (hWnd, product / application / version names, -/// retain-hidden flag) are pinned to safe defaults; the live -/// hWnd is intentionally IntPtr.Zero because we use -/// the managed-event surface, not the WM_APP pump. Verify -/// on dev rig that RegisterConsumer with -/// hWnd=0 still wires the managed event handlers; if it -/// requires a real hWnd, the worker creates a hidden message-only -/// window and passes that handle here. +/// As a result, has no production +/// caller — is invoked only +/// from tests. currently calls +/// RegisterConsumer(hWnd: 0, …) + Subscribe(…) and +/// returns OK, but no notifications will arrive at runtime because +/// no window is attached. Until A.2's WM_APP pump lands, the +/// gateway's MX_EVENT_FAMILY_ON_ALARM_TRANSITION family +/// cannot carry any events. +/// +/// +/// as written is still a load-bearing call +/// the WM_APP path needs (it tells the alarm provider which +/// subscription expression to scope notifications to). The wiring +/// that has to change is the notification-receive side: replace the +/// hWnd: 0 default with a real hidden message-only window +/// hWnd owned by the worker's STA, and add a WindowProc that +/// routes the AVEVA WM_APP message into a change-pull path that +/// ultimately invokes . +/// and +/// are pull-style and don't depend on the event surface — they're +/// correct as is. /// /// public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer @@ -63,10 +80,12 @@ public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer lock (subscribeLock) { - // hWnd=0: AVEVA's managed event surface routes through the - // GetAlarmChangesCompleted .NET event, not a window-message pump. - // Verify on dev rig that 0 is accepted; if not, supply a hidden - // message-only window's handle here. + // hWnd=0: placeholder — the AVEVA alarm provider notifies via + // WM_APP messages, not via a managed-client event surface (see + // docs/AlarmClientDiscovery.md, 2026-05-01 reflection probe). + // RegisterConsumer accepts hWnd=0 but no notifications will reach + // this consumer until the WM_APP pump lands as part of A.2 and + // a real message-only window's handle is supplied here. int registerResult = client.RegisterConsumer( hWnd: 0, szProductName: DefaultProductName, diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs index 5b1f430..450067a 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs @@ -11,19 +11,35 @@ namespace MxGateway.Worker.MxAccess; /// /// /// -/// Architecture (pinned 2026-04-30): the worker hosts +/// Architecture (revised 2026-05-01 — see +/// docs/AlarmClientDiscovery.md): the worker hosts /// aaAlarmManagedClient.AlarmClient alongside the existing -/// ArchestrA.MxAccess COM consumer. Both are x86 .NET Framework -/// 4.8 — the worker's existing runtime — and both use the same Windows -/// STA + WM_APP message pump. The MxAccess COM Toolkit at +/// ArchestrA.MxAccess COM consumer. Both are x86 .NET +/// Framework 4.8. The MxAccess COM Toolkit at /// C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll /// exposes no alarm events; the alarm provider lives in a separate /// AVEVA service that aaAlarmManagedClient subscribes to. /// /// +/// Notification mechanism: WM_APP pump. A reflection +/// probe of aaAlarmManagedClient.dll (v1.0.7368.41290) on +/// 2026-05-01 confirmed the public AlarmClient 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 +/// RegisterConsumer(hWnd, …); the consumer pulls the change +/// set via GetStatistics + GetAlarmExtendedRec on each +/// poke. PR A.5's still owns the +/// handle and the +/// / +/// pull-style calls; only +/// the receive path is wrong. +/// +/// /// Discovered API surface (see /// AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface in -/// MxGateway.Worker.Tests — Skip-gated reflection probe): +/// MxGateway.Worker.Tests — Skip-gated reflection probe; full +/// output captured in docs/AlarmClientDiscovery.md): /// /// /// RegisterConsumer(hWnd, productName, applicationName, version, retainHidden) — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change. @@ -33,20 +49,25 @@ namespace MxGateway.Worker.MxAccess; /// AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName) — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition. /// /// -/// Wiring plan (subsequent PRs): +/// Open questions before A.2 implementation +/// (see docs/AlarmClientDiscovery.md "Implications for A.2"): /// /// -/// Worker session-startup wires AlarmClient.RegisterConsumer against the worker's existing STA hWnd; Subscribe with the Galaxy provider name + a permissive priority/filter range. -/// The STA's WM_APP handler routes alarm-changed messages into ; the message ID is established at runtime via the consumer's reported handler (verify on dev rig). -/// Gateway-side AcknowledgeAlarm RPC translates to a worker command that calls AlarmClient.AlarmAckByGUID with the OPC UA operator's resolved identity — replaces the worker-pending diagnostic from PR A.3. +/// WM_APP message ID — not in the public surface, needs AVEVA C++ Toolkit reference or a runtime probe. +/// wParam / lParam semantics — likely none (the pattern is "got poked → pull state via GetStatistics"), but confirm during the probe. +/// STA / threading affinity for the message-only window — likely the worker's existing STA, but if AVEVA assumes UI-thread inside GetStatistics the alarm path may need its own STA. +/// Subscription scope — reuse the configured Galaxy name from the data session. /// /// -/// Until those PRs land, is a no-op. The worker -/// continues to function for data subscriptions, and the gateway's -/// family is reserved on -/// the wire but never emitted. lmxopcua-side AlarmConditionService -/// keeps the sub-attribute synthesis active and continues to surface -/// alarms to OPC UA Part 9 clients in the meantime. +/// Until A.2 lands a hidden message-only window + WindowProc that +/// routes WM_APP into , +/// is a no-op. The worker continues to function +/// for data subscriptions, and the gateway's +/// family is reserved +/// on the wire but never emitted. lmxopcua-side +/// AlarmConditionService keeps the sub-attribute synthesis +/// active and continues to surface alarms to OPC UA Part 9 clients +/// in the meantime. /// /// public sealed class MxAccessAlarmEventSink : IMxAccessEventSink -- 2.52.0 From 12881ca791fd86067422964277427a925a0cea87 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 07:05:47 -0400 Subject: [PATCH 02/16] =?UTF-8?q?docs+test:=20live=20AlarmClient=20WM=20pr?= =?UTF-8?q?obe=20=E2=80=94=20heartbeat-only,=20hWnd=20not=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs as a Skip-gated runtime probe. Run on the dev rig 2026-05-01 against the live AVEVA install (Galaxy reachable, no manual alarm fired). Findings: - RegisterConsumer(hWnd, ...) and Subscribe("\Galaxy!", ...) both return 0 (success). Calls are valid against the deployed assembly. - A registered-message-class WM (ID 0xC275 in this OS session) fires every ~1 second after Subscribe completes. Constant wParam=0x1100, constant lParam=0x079E46D8 — looks like a heartbeat / keepalive, not a per-change notification. - Critically, this WM is delivered to AVEVA's own internal window (hwnd=0x18032E), NOT to the consumer hWnd we registered. The consumer window receives only the standard WM_CREATE / WM_DESTROY sequence; no AVEVA traffic in between. This invalidates the WM_APP-pump design previously documented. The hWnd parameter to RegisterConsumer appears to be a registration identity only — AVEVA's notification path runs entirely against AVEVA's own internal window. Two viable A.2 designs replace the previous one: 1. Polling. Call GetStatistics on a 500ms timer in the worker's STA and react to whatever change set it reports. No window plumbing needed. Latency floor = poll period. Matches AVEVA's own internal heartbeat cadence. 2. Hook AVEVA's internal window. Discover AVEVA's own hwnd, SetWindowSubclass on it, intercept WM 0xC275 on AVEVA's thread. Higher fidelity, lower latency, but invasive and fragile across AVEVA upgrades — likely a non-starter. Recommendation in docs/AlarmClientDiscovery.md is option 1 (polling) unless a follow-up probe with a real fired alarm shows AVEVA does post change-specific WMs to a different hWnd. Open follow-up probes documented: - Fire a real Galaxy alarm during pump and check whether WM 0xC275 cadence changes or GetStatistics returns non-empty arrays. - GetStatistics threading affinity test. - Hook AVEVA's internal window 0x18032E. - Decompile aaAlarmManagedClient IL for RegisterConsumer to find whether WNAL_Register's callback surface is wrapped. Test project changes: - Added Reference to aaAlarmManagedClient + IAlarmMgrDataProvider (Private=true so the DLL gets copied into bin for test load). - Test-suite-wide: 127 real tests still pass; both alarm-related Skip-gated tests skip cleanly. Code change to the probe is additive — the worker is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 118 +++++-- .../AlarmClientWmProbeTests.cs | 328 ++++++++++++++++++ .../MxGateway.Worker.Tests.csproj | 13 + 3 files changed, 431 insertions(+), 28 deletions(-) create mode 100644 src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index ef0c95a..3c067d5 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -87,35 +87,97 @@ 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 +## Live runtime probe — 2026-05-01 -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: +`MxGateway.Worker.Tests.AlarmClientWmProbeTests.ProbeAlarmClientWmMessages` +is a Skip-gated runtime probe that creates a real message-only +window, calls `AlarmClient.RegisterConsumer(hWnd, …)` + +`Subscribe(@"\Galaxy!", …)`, and pumps for 20s while logging every +window message that arrives. Run results below — this turned the +"WM_APP pump" design assumption upside down. -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. +**`RegisterConsumer` and `Subscribe` both returned 0 (success).** The +calls are valid against the deployed assembly; no parameter pinning +needed. + +**A registered-message-class WM (ID `0xC275` in this OS session) +fired every ~1s after `Subscribe` completed.** Constant +`wParam = 0x00001100`, constant `lParam = 0x079E46D8` (looks like a +stable pointer into AVEVA-internal state) for all 20 hits. The +constant payload across hits with no Galaxy alarm being fired +suggests this is a **heartbeat/keepalive**, not a per-change +notification. + +**Critically: this WM is delivered to AVEVA's own internal window +(`hwnd=0x18032E`) — NOT to the consumer's `hWnd` we passed in.** The +consumer window's `WndProc` received only the standard creation +sequence (`WM_GETMINMAXINFO`, `WM_NCCREATE`, `WM_NCCALCSIZE`, +`WM_CREATE`) and the destruction sequence (`WM_NCDESTROY`, +`WM_DESTROY`, `WM_NCCALCSIZE`) — nothing in between. AVEVA's +notification path runs entirely against AVEVA's internal window; +it never forwards to the user-supplied hWnd. + +The message ID itself is dynamic (a `RegisterWindowMessage` +allocation in the >= 0xC000 range), so it cannot be hard-coded — +each consumer process must call `RegisterWindowMessage` with the +correct *string* and use whatever ID the OS returns. + +## What this means for A.2 + +The "WM_APP pump on the user hWnd" design — what the original plan +banner described and what the previous version of this doc +recommended — does not match how AVEVA actually delivers +notifications. The hWnd parameter to `RegisterConsumer` does not +appear to receive any of AVEVA's alarm traffic; it's likely used +only as a registration identity (and perhaps as a parent for modal +dialogs). + +Two viable A.2 designs given the probe data: + +1. **Polling.** Just call `GetStatistics` on a timer (e.g. every + 500ms in the worker's STA) and react to the change set it + reports. No window plumbing needed. Trade-off: latency floor = + poll period; modest CPU floor because the call is cheap. Matches + the heartbeat-style WM 0xC275 semantics — AVEVA itself runs a + poll loop internally. +2. **Hook AVEVA's internal window.** Discover AVEVA's own window + (`hwnd=0x18032E` in the probe), `SetWindowsHookEx` or + `SetWindowSubclass` on it, and intercept WM 0xC275 on AVEVA's + thread. Higher fidelity, near-zero latency, but invasive, + fragile across AVEVA upgrades, and requires running on the same + process / thread as the AVEVA window. Probably a non-starter + without further AVEVA documentation. + +**Recommendation:** the polling path (option 1) is cheaper to +implement, more robust against AVEVA-internal change, and +acceptable for a typical alarm cadence. The worker's existing STA +already provides a thread-affinitized timer surface. The unanswered +question is whether `GetStatistics` can be safely called outside +AVEVA's own message-pump thread — confirmable by extending the +probe to fire `GetStatistics` on its own thread and check the +result. + +## Open follow-up probes + +Each can be added to `AlarmClientWmProbeTests` as a separate +Skip-gated test: + +1. **Fire a real Galaxy alarm during the pump window.** Confirms + whether the WM 0xC275 cadence changes (becomes per-change rather + than periodic) and whether `GetStatistics` returns a non-empty + `ChangeCodes / ChangePos / hAlarm` triple. +2. **Call `GetStatistics` on a different thread from the + `RegisterConsumer` thread** to test threading affinity. +3. **Hook AVEVA's internal window** to log what WMs it actually + processes (would resolve option 2 above). +4. **Decompile `aaAlarmManagedClient.dll`'s IL** for the + `RegisterConsumer` method to find what `RegisterWindowMessage` + string is used and whether there's a callback-registration + surface on `WNAL_Register` that the managed client wraps. The + alarmlst.dll strings (`WNAL_CallBack`, "Invalid callbacks" error) + suggest the underlying C API takes callbacks, but the managed + wrapper exposes none of them. 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. +are correct — they're pull-style and don't depend on the +notification mechanism. diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs new file mode 100644 index 0000000..a9ba7b3 --- /dev/null +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using AlarmMgrDataProviderCOM; +using aaAlarmManagedClient; +using Xunit.Abstractions; + +namespace MxGateway.Worker.Tests; + +/// +/// Runtime probe — registers as an AlarmClient consumer with a real +/// hidden message-only window, subscribes to a Galaxy alarm provider, +/// and logs every Win32 message that arrives during a fixed pump +/// window. The intent is to identify the WM_APP / RegisterWindowMessage +/// ID that AVEVA's alarm provider posts when alarms change, plus the +/// wParam/lParam semantics on each. +/// +/// Skip-gated by default; flip Skip=null and run against the live dev +/// rig to capture output. Requires: +/// +/// A reachable Galaxy with at least one alarmable object. +/// The configured Galaxy expression below to match a real provider (default "\\Galaxy" — adjust if needed). +/// An alarm trigger during the pump window (raise / ack / clear something in the Galaxy via System Platform IDE) — without one, only ambient activity is captured. +/// +/// +public sealed class AlarmClientWmProbeTests : IDisposable +{ + // Probe configuration. Override in the constructor below if needed. + private const string SubscriptionExpression = @"\Galaxy!"; + private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(20); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateWindowExW")] + private static extern IntPtr CreateWindowEx( + int dwExStyle, string lpClassName, string lpWindowName, + int dwStyle, int X, int Y, int nWidth, int nHeight, + IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern ushort RegisterClassW(ref WNDCLASSW lpWndClass); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UnregisterClassW(string lpClassName, IntPtr hInstance); + + [DllImport("user32.dll")] + private static extern IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); + + [DllImport("user32.dll")] + private static extern IntPtr DispatchMessage(ref MSG lpMsg); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TranslateMessage(ref MSG lpMsg); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern uint RegisterWindowMessage(string lpString); + + private const int HWND_MESSAGE = -3; + private const uint PM_REMOVE = 0x0001; + + private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WNDCLASSW + { + public uint style; + public IntPtr lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPWStr)] public string? lpszMenuName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public int x; + public int y; + } + + private readonly ITestOutputHelper output; + private readonly ConcurrentQueue log = new ConcurrentQueue(); + private readonly Stopwatch elapsed = Stopwatch.StartNew(); + private GCHandle wndProcHandle; + private IntPtr probeWindow = IntPtr.Zero; + private string? registeredClass; + + public AlarmClientWmProbeTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs")] + public void ProbeAlarmClientWmMessages() + { + // 1. Pre-resolve a few candidate RegisterWindowMessage strings so any + // matches in the captured log can be labeled. None of these is + // confirmed; we record what each resolves to so the actual AVEVA + // message ID (whatever it turns out to be) can be cross-referenced. + string[] candidateNames = + { + "WW_AlarmConsumer", "WW_AlarmManager", "WW_Alarm", + "WNAL_AlarmChange", "WNAL_AlarmChanges", "WNAL_AlarmNotify", + "WNAL_Notify", "WNAL_ChangeNotification", + "AlarmManager.Notify", "AlarmManagerNotify", + "ArchestrA.AlarmChange", "AVEVA.AlarmNotify", + "aaAlarmManagedClient.Notify", + "GotAlarmChanges", "OnAlarmChanges", + }; + foreach (string name in candidateNames) + { + uint id = RegisterWindowMessage(name); + output.WriteLine($"RegisterWindowMessage(\"{name}\") -> 0x{id:X4} ({id})"); + } + output.WriteLine(""); + + // 2. Spin up a single STA-affinitized thread, create a hidden message- + // only window owned by it, run RegisterConsumer + Subscribe against + // that window's hWnd, then pump messages on that thread for the + // configured duration. Threading discipline matches the worker's + // StaRuntime model. + Exception? threadException = null; + var pumpDone = new ManualResetEventSlim(false); + var thread = new Thread(() => + { + try + { + RunProbe(); + } + catch (Exception ex) + { + threadException = ex; + } + finally + { + pumpDone.Set(); + } + }); + thread.IsBackground = false; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + pumpDone.Wait(); + thread.Join(); + + // 3. Drain the log to xunit output regardless of outcome — partial + // captures are still informative. + output.WriteLine(""); + output.WriteLine($"Captured {log.Count} log line(s):"); + while (log.TryDequeue(out string? line)) + { + output.WriteLine(line); + } + + if (threadException != null) + { + throw threadException; + } + } + + private void RunProbe() + { + // 3a. Register a window class and create a message-only window. + WndProc wndProc = ProbeWndProc; + wndProcHandle = GCHandle.Alloc(wndProc); // keep delegate alive + + registeredClass = "MxGatewayAlarmProbe_" + Guid.NewGuid().ToString("N"); + var cls = new WNDCLASSW + { + style = 0, + lpfnWndProc = Marshal.GetFunctionPointerForDelegate(wndProc), + hInstance = GetModuleHandle(null!), + lpszClassName = registeredClass, + }; + ushort atom = RegisterClassW(ref cls); + if (atom == 0) + { + int err = Marshal.GetLastWin32Error(); + Log($"RegisterClass failed err=0x{err:X8}"); + return; + } + Log($"RegisterClass ok atom=0x{atom:X4} class={registeredClass}"); + + probeWindow = CreateWindowEx( + dwExStyle: 0, lpClassName: registeredClass, lpWindowName: "AlarmProbe", + dwStyle: 0, X: 0, Y: 0, nWidth: 0, nHeight: 0, + hWndParent: (IntPtr)HWND_MESSAGE, hMenu: IntPtr.Zero, + hInstance: cls.hInstance, lpParam: IntPtr.Zero); + if (probeWindow == IntPtr.Zero) + { + int err = Marshal.GetLastWin32Error(); + Log($"CreateWindowEx(HWND_MESSAGE) failed err=0x{err:X8}"); + return; + } + Log($"Created message-only window hWnd=0x{probeWindow.ToInt64():X}"); + + // 3b. Create the AlarmClient and try the lifecycle. RegisterConsumer + // accepts an int hWnd — narrow the IntPtr (sufficient on x86). + AlarmClient? client = null; + try + { + client = new AlarmClient(); + int register = client.RegisterConsumer( + hWnd: probeWindow.ToInt32(), + szProductName: "AlarmProbe", + szApplicationName: "AlarmProbe.Tests", + szVersion: "1.0", + bRetainHiddenAlarms: false); + Log($"RegisterConsumer -> {register}"); + + int subscribe = client.Subscribe( + szSubscription: SubscriptionExpression, + wFromPri: 1, wToPri: 999, + QueryType: eQueryType.qtSummary, + SortFlags: eSortFlags.sfReturnNewestFirst, + FilterMask: eAlarmFilterState.asNone, + FilterSpecification: eAlarmFilterState.asNone); + Log($"Subscribe('{SubscriptionExpression}') -> {subscribe}"); + + // 3c. Pump for the configured duration. Log every message we see + // (filtered light to avoid noise from WM_PAINT / WM_TIMER / + // WM_GETICON spam from typical pumps). + DateTime deadline = DateTime.UtcNow + PumpDuration; + while (DateTime.UtcNow < deadline) + { + while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE)) + { + LogIfInteresting(msg); + TranslateMessage(ref msg); + DispatchMessage(ref msg); + } + Thread.Sleep(10); + } + + Log($"Pump duration {PumpDuration.TotalSeconds:F0}s elapsed; deregistering."); + + try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); } + catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); } + } + finally + { + try { client?.Dispose(); } catch { /* swallow */ } + if (probeWindow != IntPtr.Zero) + { + DestroyWindow(probeWindow); + probeWindow = IntPtr.Zero; + } + if (registeredClass != null) + { + UnregisterClassW(registeredClass, GetModuleHandle(null!)); + } + } + } + + private void LogIfInteresting(MSG m) + { + // Filter out the highest-volume noise (timer ticks, paint, mouse moves + // from a desktop session). Keep WM_USER..WM_APP+ entirely; those are + // the candidates for the AVEVA-registered message. + const uint WM_PAINT = 0x000F; + const uint WM_TIMER = 0x0113; + const uint WM_MOUSEMOVE = 0x0200; + const uint WM_NCMOUSEMOVE = 0x00A0; + if (m.message == WM_PAINT || m.message == WM_TIMER || + m.message == WM_MOUSEMOVE || m.message == WM_NCMOUSEMOVE) + { + return; + } + + string interpreted = InterpretMessageId(m.message); + Log(string.Format( + "WM 0x{0:X4} ({1}) wParam=0x{2:X8} lParam=0x{3:X8} hwnd=0x{4:X}", + m.message, interpreted, + m.wParam.ToInt64() & 0xFFFFFFFF, m.lParam.ToInt64() & 0xFFFFFFFF, + m.hwnd.ToInt64())); + } + + private static string InterpretMessageId(uint id) + { + if (id < 0x0400) return "WM_"; + if (id < 0x8000) return $"WM_USER+0x{id - 0x0400:X4}"; + if (id < 0xC000) return $"WM_APP+0x{id - 0x8000:X4}"; + return $"RegisterWindowMessage_0x{id:X4}"; + } + + private IntPtr ProbeWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + // Log every WM that lands on the probe window itself. + string interpreted = InterpretMessageId(msg); + Log(string.Format( + "WndProc WM 0x{0:X4} ({1}) wParam=0x{2:X8} lParam=0x{3:X8}", + msg, interpreted, + wParam.ToInt64() & 0xFFFFFFFF, lParam.ToInt64() & 0xFFFFFFFF)); + return DefWindowProcW(hWnd, msg, wParam, lParam); + } + + private void Log(string line) + { + log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}"); + } + + public void Dispose() + { + if (wndProcHandle.IsAllocated) wndProcHandle.Free(); + } +} diff --git a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj index 47796e3..bb2f949 100644 --- a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj +++ b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj @@ -25,4 +25,17 @@ + + + C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll + true + false + + + C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\IAlarmMgrDataProvider.dll + true + false + + + -- 2.52.0 From 3ff4969224fe14e8f07b6eb0d134465d467d504c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 07:16:08 -0400 Subject: [PATCH 03/16] probe: GetStatistics polling viable, Galaxy has no active alarms today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended AlarmClientWmProbeTests.ProbeAlarmClientWmMessages to also call GetStatistics every ~2s during the pump window. Re-ran on the dev rig 2026-05-01: - GetStatistics is safely callable from the same thread that did RegisterConsumer + Subscribe. Every poll (9 calls / 20s window) returned rc=0, no exceptions. - Galaxy currently has zero active alarms. total=0 active=0 suppressed=0 newAlarms=0 across every poll. positions[] and handles[] arrays were empty. - changes=1 codes=[7] was constant across all polls, matching the constant 1 Hz WM 0xC275 cadence — same heartbeat semantics exposed through both the WM path and the pull API. Confirms the polling design is mechanically viable: GetStatistics threading-affinity is fine and the call is cheap. The remaining unknown is whether GetStatistics populates positions[] / handles[] with real entries when an alarm actually fires. Proving that requires triggering an alarm — next probe is an MxAccess write to a $Alarm-extended boolean tag (reference pending). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 46 ++++++++--- .../AlarmClientWmProbeTests.cs | 81 ++++++++++++++++++- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 3c067d5..fe1f42c 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -157,20 +157,48 @@ AVEVA's own message-pump thread — confirmable by extending the probe to fire `GetStatistics` on its own thread and check the result. +## GetStatistics polling — second probe run, 2026-05-01 + +Extended the probe to call `GetStatistics` every ~2s alongside the +WM logger. Key findings: + +- **`GetStatistics` is safely callable from the same thread that + did `RegisterConsumer` + `Subscribe`.** Every poll returned rc=0 + with no exceptions over 9 polls / 20s window. +- **The deployed Galaxy currently has zero active alarms.** Every + poll reported `total=0 active=0 suppressed=0 newAlarms=0`. The + `positions[]` and `handles[]` arrays were empty. +- **`changes=1 codes=[7]` was constant across all polls**, matching + the constant 1 Hz WM 0xC275 cadence. Code 7 is consistent with a + "heartbeat / subscription healthy" sentinel — same semantics as + the WM but reported through the pull-side API. +- `percent=100` (query-complete percentage) was constant — the + subscription is steady-state. + +This confirms the polling design (option 1 in the previous section) +is mechanically viable. The remaining open question is whether +`GetStatistics` populates `positions[] / handles[]` with real +entries when an alarm transition actually fires — proving that +requires firing an alarm. + ## Open follow-up probes Each can be added to `AlarmClientWmProbeTests` as a separate Skip-gated test: -1. **Fire a real Galaxy alarm during the pump window.** Confirms - whether the WM 0xC275 cadence changes (becomes per-change rather - than periodic) and whether `GetStatistics` returns a non-empty - `ChangeCodes / ChangePos / hAlarm` triple. -2. **Call `GetStatistics` on a different thread from the - `RegisterConsumer` thread** to test threading affinity. -3. **Hook AVEVA's internal window** to log what WMs it actually - processes (would resolve option 2 above). -4. **Decompile `aaAlarmManagedClient.dll`'s IL** for the +1. **Fire a real Galaxy alarm during the pump window.** The cleanest + programmatic trigger is an MxAccess write that flips a + `$Alarm`-extended boolean to true (alarm in) and back to false + (alarm out). Pinning the exact tag reference is pending — needs + either a documented test-fixture tag or an interactive selection + in System Platform IDE. Once the trigger fires, this resolves + whether AVEVA's pulled change set arrives via `GetStatistics` + `positions[] / handles[]` (per-change polling works) or only via + the AVEVA-internal window (callback path needed). +2. **Hook AVEVA's internal window** to log what WMs it actually + processes — only relevant if probe 1 shows `GetStatistics` does + NOT report per-change activity. +3. **Decompile `aaAlarmManagedClient.dll`'s IL** for the `RegisterConsumer` method to find what `RegisterWindowMessage` string is used and whether there's a callback-registration surface on `WNAL_Register` that the managed client wraps. The diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index a9ba7b3..5659d6c 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -113,7 +113,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable this.output = output; } - [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs")] + [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs + GetStatistics polling results")] public void ProbeAlarmClientWmMessages() { // 1. Pre-resolve a few candidate RegisterWindowMessage strings so any @@ -241,8 +241,13 @@ public sealed class AlarmClientWmProbeTests : IDisposable // 3c. Pump for the configured duration. Log every message we see // (filtered light to avoid noise from WM_PAINT / WM_TIMER / - // WM_GETICON spam from typical pumps). + // WM_GETICON spam from typical pumps). Every ~2s also call + // GetStatistics and snapshot up to N records, to test the + // polling design — if Galaxy has any active alarms or any + // have changed since Subscribe, we'll see them here. DateTime deadline = DateTime.UtcNow + PumpDuration; + DateTime nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2); + int pollCount = 0; while (DateTime.UtcNow < deadline) { while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE)) @@ -251,6 +256,11 @@ public sealed class AlarmClientWmProbeTests : IDisposable TranslateMessage(ref msg); DispatchMessage(ref msg); } + if (DateTime.UtcNow >= nextPoll) + { + PollGetStatistics(client, ++pollCount); + nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2); + } Thread.Sleep(10); } @@ -274,6 +284,73 @@ public sealed class AlarmClientWmProbeTests : IDisposable } } + private void PollGetStatistics(AlarmClient client, int seq) + { + try + { + int percent = 0, total = 0, active = 0, suppressed = 0; + int suppressedFilters = 0, newAlarms = 0, changes = 0; + int[] codes = Array.Empty(); + int[] positions = Array.Empty(); + int[] handles = Array.Empty(); + int rc = client.GetStatistics( + ref percent, ref total, ref active, ref suppressed, + ref suppressedFilters, ref newAlarms, ref changes, + ref codes, ref positions, ref handles); + string codesStr = codes != null ? string.Join(",", codes) : ""; + string posStr = positions != null ? string.Join(",", positions) : ""; + string handlesStr = handles != null ? string.Join(",", handles) : ""; + int posLen = positions?.Length ?? 0; + Log($"GetStatistics #{seq} rc={rc} pct={percent} total={total} active={active} " + + $"suppressed={suppressed} suppressedFilters={suppressedFilters} new={newAlarms} changes={changes} " + + $"codes=[{codesStr}] positions=[{posStr}] handles=[{handlesStr}]"); + + // If positions has entries, fetch one record so we see the + // record-shape AVEVA exposes for a real alarm. + if (posLen > 0 && positions != null) + { + int idx = positions[0]; + AlarmRecord rec = new AlarmRecord(); + int recRc = client.GetAlarmExtendedRec(idx, ref rec); + Log($" GetAlarmExtendedRec(idx={idx}) rc={recRc} -> " + + DescribeAlarmRecord(rec)); + } + } + catch (Exception ex) + { + Log($"GetStatistics #{seq} threw: {ex.GetType().Name}: {ex.Message}"); + } + } + + private static string DescribeAlarmRecord(AlarmRecord rec) + { + // Reflect over the record's public properties so we don't have to + // guess the field shape — the discovery probe already showed it has + // ar_AlarmName / ar_Provider / ar_Group / ar_AlmTransition / etc. + var sb = new System.Text.StringBuilder(); + sb.Append("{ "); + bool first = true; + foreach (var prop in rec.GetType().GetProperties( + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) + { + try + { + object? v = prop.GetValue(rec); + string vs = v?.ToString() ?? ""; + if (vs.Length > 50) vs = vs.Substring(0, 47) + "..."; + if (!first) sb.Append(", "); + sb.Append($"{prop.Name}={vs}"); + first = false; + } + catch + { + // skip failing accessors + } + } + sb.Append(" }"); + return sb.ToString(); + } + private void LogIfInteresting(MSG m) { // Filter out the highest-volume noise (timer ticks, paint, mouse moves -- 2.52.0 From f4423dfb6d20b9435d3affcd5d49ddb358c8d7d5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 07:37:15 -0400 Subject: [PATCH 04/16] =?UTF-8?q?probe:=20GetProviders=3D0=20=E2=80=94=20a?= =?UTF-8?q?larm=20path=20upstream-blocked=20on=20dev=20rig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended AlarmClientWmProbeTests to call AlarmClient.GetProviders after RegisterConsumer. Run 2026-05-01: GetProviders -> rc=0 count=0 list=[] Zero alarm providers visible to the consumer. This explains every preceding probe run — no providers means no alarm events, regardless of subscription expression or value writes upstream. Even with a System Platform script flipping TestMachine_001.TestAlarm001 every 10s during the run, GetStatistics reported no transitions, no positions[] entries, no field changes after t=0.85s. Possible causes (dev-rig configuration, not code): 1. No $Alarm extension on the test bool — flipping the value writes a value but doesn't fire an alarm. 2. AVEVA alarm-manager service (aaAlarmMgr or equivalent) not running on this rig. 3. Process security context — providers registered under a service account aren't visible to a consumer running under a normal user account. A.2 implementation is blocked on this until at least one provider is visible. Once a provider exists, the polling-vs-callback question is answerable in one probe run; without a provider both paths return the same "nothing happening" answer. Probe changes: - Added in-process MxAccess Write attempt (TriggerWriteValue) — hit TargetParameterCountException so the Write signature is not (handle, item, value); reflection diag added but not resolved. Now disabled in favor of external trigger. - Added GetProviders enumeration after RegisterConsumer. - Removed firePrint/clearPrint markers; probe is observe-only. - Added ArchestrA.MxAccess reference to the test project. Also updated docs/AlarmClientDiscovery.md with the alarm-provider-visibility section explaining what's blocked and why. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 51 ++++++ .../AlarmClientWmProbeTests.cs | 156 +++++++++++++++--- .../MxGateway.Worker.Tests.csproj | 5 + 3 files changed, 193 insertions(+), 19 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index fe1f42c..537856f 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -157,6 +157,57 @@ AVEVA's own message-pump thread — confirmable by extending the probe to fire `GetStatistics` on its own thread and check the result. +## Alarm-provider visibility — third probe run, 2026-05-01 + +Extended the probe to call `AlarmClient.GetProviders` after +`RegisterConsumer`. Result on this rig: + +``` +GetProviders -> rc=0 count=0 list=[] +``` + +**Zero alarm providers visible to the consumer process.** This +explains every preceding probe run: no providers means no alarm +events, regardless of how many times any value (including a +bool with an `$Alarm` extension) flips. `Subscribe(@"\Galaxy!")` +returns 0 (success) but matches nothing because the alarm-manager +chain that provides the matching feed doesn't expose any provider +to this consumer. + +A System Platform script flipping `TestMachine_001.TestAlarm001` +every 10s during this probe run produced no observable +`GetStatistics` transitions, no `positions[]` / `handles[]` +entries, no change in any field — confirms the silence is not +about subscription-scope / message-pump but about provider +absence. + +### Possible causes + +1. **No `$Alarm` extension on the test bool.** If + `TestMachine_001.TestAlarm001` is a regular UDA without a + `BoolAlarm` extension wired to it, flipping the value just + writes a new value — no alarm fires. +2. **Alarm manager service not running.** AVEVA's `aaAlarmMgr` + (or the equivalent on this rig's Platform version) needs to + be running for providers to register. +3. **Process security context.** A consumer running under a + normal user account may not see providers that registered + under `LocalSystem` / a Platform service identity. The + gateway-worker installation runs under a service account + that may have access where `dotnet test` doesn't. + +### Implications for A.2 implementation + +The A.2 PR's value is unmeasurable until at least one alarm +provider is visible. The choice between polling-via-`GetStatistics` +and the callback path can only be decided by observing what +populates first when a real alarm fires. Without a provider, +both paths return the same "nothing happening" answer. + +Until that's resolved, A.2 implementation work is genuinely +blocked on a dev-rig configuration issue — not on architectural +choice or code structure. + ## GetStatistics polling — second probe run, 2026-05-01 Extended the probe to call `GetStatistics` every ~2s alongside the diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 5659d6c..303b220 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -4,8 +4,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; +using System.Linq; +using System.Reflection; using AlarmMgrDataProviderCOM; using aaAlarmManagedClient; +using ArchestrA.MxAccess; using Xunit.Abstractions; namespace MxGateway.Worker.Tests; @@ -30,7 +33,13 @@ public sealed class AlarmClientWmProbeTests : IDisposable { // Probe configuration. Override in the constructor below if needed. private const string SubscriptionExpression = @"\Galaxy!"; - private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(20); + private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(60); + private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan FireMarkerAt = TimeSpan.FromSeconds(10); + private static readonly TimeSpan ClearMarkerAt = TimeSpan.FromSeconds(35); + // Tag the operator should flip while the probe is pumping. Default + // matches the dev rig's known alarmable boolean. + private const string TriggerTagReference = "TestMachine_001.TestAlarm001"; [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateWindowExW")] private static extern IntPtr CreateWindowEx( @@ -113,7 +122,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable this.output = output; } - [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs + GetStatistics polling results")] + [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture alarm-path behavior")] public void ProbeAlarmClientWmMessages() { // 1. Pre-resolve a few candidate RegisterWindowMessage strings so any @@ -230,6 +239,19 @@ public sealed class AlarmClientWmProbeTests : IDisposable bRetainHiddenAlarms: false); Log($"RegisterConsumer -> {register}"); + // Discover what providers AVEVA sees before subscribing, so we + // can spot a wrong subscription expression up front. + try + { + var providers = new System.Collections.Generic.List(); + int gp = client.GetProviders(providers); + Log($"GetProviders -> rc={gp} count={providers.Count} list=[{string.Join(", ", providers)}]"); + } + catch (Exception ex) + { + Log($"GetProviders threw: {ex.GetType().Name}: {ex.Message}"); + } + int subscribe = client.Subscribe( szSubscription: SubscriptionExpression, wFromPri: 1, wToPri: 999, @@ -241,13 +263,19 @@ public sealed class AlarmClientWmProbeTests : IDisposable // 3c. Pump for the configured duration. Log every message we see // (filtered light to avoid noise from WM_PAINT / WM_TIMER / - // WM_GETICON spam from typical pumps). Every ~2s also call - // GetStatistics and snapshot up to N records, to test the - // polling design — if Galaxy has any active alarms or any - // have changed since Subscribe, we'll see them here. - DateTime deadline = DateTime.UtcNow + PumpDuration; - DateTime nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2); + // WM_GETICON spam from typical pumps). Poll GetStatistics on + // a tight cadence so any alarm transition is captured. Print + // "fire" / "clear" markers at fixed wallclock offsets so the + // operator can flip the trigger boolean during the run. + Log($"Probe running for {PumpDuration.TotalSeconds:F0}s. " + + $"Observing {TriggerTagReference} alarm transitions. " + + "External trigger expected from System Platform script (10s flip cadence)."); + + DateTime probeStart = DateTime.UtcNow; + DateTime deadline = probeStart + PumpDuration; + DateTime nextPoll = probeStart + PollInterval; int pollCount = 0; + while (DateTime.UtcNow < deadline) { while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE)) @@ -256,10 +284,13 @@ public sealed class AlarmClientWmProbeTests : IDisposable TranslateMessage(ref msg); DispatchMessage(ref msg); } + // Trigger is supplied externally — a System Platform script + // flips TestMachine_001.TestAlarm001 every 10s. The probe + // observes only. if (DateTime.UtcNow >= nextPoll) { PollGetStatistics(client, ++pollCount); - nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2); + nextPoll = DateTime.UtcNow + PollInterval; } Thread.Sleep(10); } @@ -284,6 +315,81 @@ public sealed class AlarmClientWmProbeTests : IDisposable } } + private string lastStatsSummary = string.Empty; + + /// + /// Drive an MxAccess write to with the + /// supplied boolean value. Creates a fresh `LMXProxyServer` COM object, + /// registers, adds the item, writes the value, and tears down. Runs on + /// the same STA thread the probe uses for the AlarmClient — both COM + /// objects share the apartment, which matches the worker's runtime. + /// + private void TriggerWriteValue(bool value, int sequence) + { + object? lmx = null; + ILMXProxyServer? srv = null; + int handle = 0, itemHandle = 0; + try + { + lmx = new LMXProxyServerClass(); + srv = (ILMXProxyServer)lmx; + handle = srv.Register($"AlarmProbe.Trigger.{sequence}"); + Log($"Trigger write #{sequence}: Register -> handle={handle}"); + itemHandle = srv.AddItem(handle, TriggerTagReference); + Log($"Trigger write #{sequence}: AddItem('{TriggerTagReference}') -> itemHandle={itemHandle}"); + + // First time only: dump every Write* method's signature so we know + // which to call. The first attempt hit TargetParameterCountException — + // the LMX server has multiple Write variants and we picked wrong. + if (sequence == 1) + { + Log($"Trigger write #{sequence}: enumerating Write* methods on {lmx.GetType().FullName}:"); + foreach (var m in lmx.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) + { + if (m.IsSpecialName) continue; + if (!m.Name.StartsWith("Write", StringComparison.OrdinalIgnoreCase)) continue; + string ps = string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")); + Log($" {m.ReturnType.Name} {m.Name}({ps})"); + } + } + + // Late-bind Write — it isn't on ILMXProxyServer's interface but is + // exposed by the COM coclass. + object[] writeArgs = new object[] { handle, itemHandle, value }; + object? rv = lmx.GetType().InvokeMember( + "Write", + BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, + binder: null, target: lmx, args: writeArgs); + Log($"Trigger write #{sequence}: Write({TriggerTagReference}={value}) -> rv={rv}"); + } + catch (Exception ex) + { + Log($"Trigger write #{sequence}: FAILED: {ex.GetType().Name}: {ex.Message}"); + if (ex.InnerException != null) + { + Log($" inner: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + } + } + finally + { + try + { + if (srv != null && itemHandle != 0) { srv.RemoveItem(handle, itemHandle); } + if (srv != null && handle != 0) { srv.Unregister(handle); } + } + catch (Exception ex) + { + Log($"Trigger write #{sequence}: cleanup failure: {ex.GetType().Name}: {ex.Message}"); + } + if (lmx != null && System.Runtime.InteropServices.Marshal.IsComObject(lmx)) + { + try { System.Runtime.InteropServices.Marshal.FinalReleaseComObject(lmx); } + catch { /* swallow */ } + } + } + } + + private void PollGetStatistics(AlarmClient client, int seq) { try @@ -301,19 +407,31 @@ public sealed class AlarmClientWmProbeTests : IDisposable string posStr = positions != null ? string.Join(",", positions) : ""; string handlesStr = handles != null ? string.Join(",", handles) : ""; int posLen = positions?.Length ?? 0; - Log($"GetStatistics #{seq} rc={rc} pct={percent} total={total} active={active} " + - $"suppressed={suppressed} suppressedFilters={suppressedFilters} new={newAlarms} changes={changes} " + - $"codes=[{codesStr}] positions=[{posStr}] handles=[{handlesStr}]"); - // If positions has entries, fetch one record so we see the - // record-shape AVEVA exposes for a real alarm. + // Suppress duplicate-summary spam — only log when interesting + // state-change is observed. The "interesting" digest excludes + // percent (always 100 at steady state). + string summary = $"total={total} active={active} suppressed={suppressed} " + + $"new={newAlarms} changes={changes} codes=[{codesStr}] " + + $"positions=[{posStr}] handles=[{handlesStr}]"; + if (summary != lastStatsSummary) + { + Log($"GetStatistics #{seq} rc={rc} pct={percent} {summary} (changed)"); + lastStatsSummary = summary; + } + + // Always fetch records when positions has entries — records + // change content even when count stays the same. if (posLen > 0 && positions != null) { - int idx = positions[0]; - AlarmRecord rec = new AlarmRecord(); - int recRc = client.GetAlarmExtendedRec(idx, ref rec); - Log($" GetAlarmExtendedRec(idx={idx}) rc={recRc} -> " + - DescribeAlarmRecord(rec)); + for (int i = 0; i < Math.Min(posLen, 4); i++) + { + int idx = positions[i]; + AlarmRecord rec = new AlarmRecord(); + int recRc = client.GetAlarmExtendedRec(idx, ref rec); + Log($" GetAlarmExtendedRec(idx={idx}) rc={recRc} -> " + + DescribeAlarmRecord(rec)); + } } } catch (Exception ex) diff --git a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj index bb2f949..a3fdd4e 100644 --- a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj +++ b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj @@ -26,6 +26,11 @@ + + C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll + true + false + C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll true -- 2.52.0 From 4e8928cf712fa108345237474d8c439b36561ccd Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 07:43:06 -0400 Subject: [PATCH 05/16] =?UTF-8?q?probe:=20InitializeConsumer=20required=20?= =?UTF-8?q?=E2=80=94=20provider=20visible=20after,=20alarms=20still=20abse?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InitializeConsumer was the missing call. Adding it before RegisterConsumer makes the \Galaxy! provider appear in GetProviders (status 0 -> 100 within 500ms). Without Initialize, GetProviders returns an empty list even though everything else returns rc=0 (success). Probe trace 2026-05-01: InitializeConsumer -> 0 RegisterConsumer -> 0 GetProviders [after Register] -> count=0 list=[] Subscribe('\Galaxy!') -> 0 GetProviders [after Subscribe] -> count=1 list=[ 0 \Galaxy!] GetProviders [poll #1] -> count=1 list=[100 \Galaxy!] Despite the provider being at "100% query complete" for the entire 60s window, GetStatistics continued to report total=0 active=0 codes=[7] -- no alarm transitions reached the consumer even with a System Platform script flipping TestMachine_001.TestAlarm001 every 10s during the run. So the consumer chain works end-to-end. What's missing is alarm traffic from the producer side. The next discriminator is whether ObjectViewer (or another live consumer) sees the alarm fire while the script runs. API-ordering bug fix to apply to PR A.5's AlarmClientConsumer regardless of how A.2 lands: AlarmClientConsumer.Subscribe should call InitializeConsumer before RegisterConsumer (currently omits Initialize entirely, which means the provider chain is never visible from the worker either). That fix lifts a fundamental bug independent of the polling-vs-callback question. Probe changes: - Added InitializeConsumer call before RegisterConsumer. - Added LogProviders helper that logs only on change; called after Register, after Subscribe, and on every poll. Easier to spot when the provider chain transitions from empty to populated. - Restored Skip-gating after run. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 40 ++++++++++++++ .../AlarmClientWmProbeTests.cs | 52 ++++++++++++++----- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 537856f..24045cd 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -196,6 +196,46 @@ absence. gateway-worker installation runs under a service account that may have access where `dotnet test` doesn't. +## InitializeConsumer required — fourth probe run, 2026-05-01 + +Adding `InitializeConsumer("AlarmProbe.Tests")` before +`RegisterConsumer` made `\Galaxy!` appear in `GetProviders` +(count=1, status 0 → 100 within 500ms). So #2 and #3 above are +NOT the cause — the consumer can see the alarm provider once it +calls Initialize. That's a missing API-call ordering, not a +permission or service issue. + +``` +InitializeConsumer -> 0 +RegisterConsumer -> 0 +GetProviders [after Register] -> rc=0 count=0 list=[] +Subscribe('\Galaxy!') -> 0 +GetProviders [after Subscribe] -> rc=0 count=1 list=[ 0 \Galaxy!] +GetProviders [poll #1] -> rc=0 count=1 list=[100 \Galaxy!] +``` + +Despite the provider being visible at "100% query complete" for +the entire 60s window, `GetStatistics` continued to report +`total=0 active=0 codes=[7]` — no alarm transitions reached the +consumer even with a System Platform script flipping the test +boolean every 10s during the run. + +That isolates the remaining unknown to whether the test bool's +alarm extension is actually generating MxAccess alarm-provider +events when its value flips. The probe has confirmed every link +in the consumer chain works (Initialize → Register → Subscribe → +provider visible at 100%) — what's missing is alarm traffic from +the producer side. ObjectViewer or another live consumer running +alongside the script is the next discriminator: does it visibly +see the alarm fire? + +API-ordering finding: `InitializeConsumer` MUST precede +`RegisterConsumer` (or at least, must be called before +`GetProviders` returns anything). PR A.5's `AlarmClientConsumer` +omits `InitializeConsumer` entirely — that's a bug fix to apply +even before A.2 lands, since without it the provider chain never +becomes visible. + ### Implications for A.2 implementation The A.2 PR's value is unmeasurable until at least one alarm diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 303b220..39f1b04 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -231,6 +231,22 @@ public sealed class AlarmClientWmProbeTests : IDisposable try { client = new AlarmClient(); + + // Try InitializeConsumer first — separate from RegisterConsumer + // per the discovered API surface; previous probe runs skipped + // it. Some AVEVA managed-client patterns require Initialize + // before Register; others reverse the order. Try Initialize + // first; on failure proceed to Register. + try + { + int init = client.InitializeConsumer("AlarmProbe.Tests"); + Log($"InitializeConsumer -> {init}"); + } + catch (Exception ex) + { + Log($"InitializeConsumer threw: {ex.GetType().Name}: {ex.Message}"); + } + int register = client.RegisterConsumer( hWnd: probeWindow.ToInt32(), szProductName: "AlarmProbe", @@ -239,18 +255,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable bRetainHiddenAlarms: false); Log($"RegisterConsumer -> {register}"); - // Discover what providers AVEVA sees before subscribing, so we - // can spot a wrong subscription expression up front. - try - { - var providers = new System.Collections.Generic.List(); - int gp = client.GetProviders(providers); - Log($"GetProviders -> rc={gp} count={providers.Count} list=[{string.Join(", ", providers)}]"); - } - catch (Exception ex) - { - Log($"GetProviders threw: {ex.GetType().Name}: {ex.Message}"); - } + LogProviders(client, "after Register"); int subscribe = client.Subscribe( szSubscription: SubscriptionExpression, @@ -261,6 +266,8 @@ public sealed class AlarmClientWmProbeTests : IDisposable FilterSpecification: eAlarmFilterState.asNone); Log($"Subscribe('{SubscriptionExpression}') -> {subscribe}"); + LogProviders(client, "after Subscribe"); + // 3c. Pump for the configured duration. Log every message we see // (filtered light to avoid noise from WM_PAINT / WM_TIMER / // WM_GETICON spam from typical pumps). Poll GetStatistics on @@ -290,6 +297,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable if (DateTime.UtcNow >= nextPoll) { PollGetStatistics(client, ++pollCount); + LogProviders(client, $"poll #{pollCount}"); nextPoll = DateTime.UtcNow + PollInterval; } Thread.Sleep(10); @@ -316,6 +324,26 @@ public sealed class AlarmClientWmProbeTests : IDisposable } private string lastStatsSummary = string.Empty; + private string lastProvidersSummary = string.Empty; + + private void LogProviders(AlarmClient client, string when) + { + try + { + var providers = new System.Collections.Generic.List(); + int rc = client.GetProviders(providers); + string summary = $"count={providers.Count} list=[{string.Join(", ", providers)}]"; + if (summary != lastProvidersSummary) + { + Log($"GetProviders [{when}] -> rc={rc} {summary} (changed)"); + lastProvidersSummary = summary; + } + } + catch (Exception ex) + { + Log($"GetProviders [{when}] threw: {ex.GetType().Name}: {ex.Message}"); + } + } /// /// Drive an MxAccess write to with the -- 2.52.0 From 8ac6642bf81bec0ab8c55c930e0934a2c120b163 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 07:53:26 -0400 Subject: [PATCH 06/16] =?UTF-8?q?probe:=20subscribe-parameter=20sweep=20?= =?UTF-8?q?=E2=80=94=20alarms=20still=20absent,=20producer-side=20blocked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tried every documented subscription knob with InitializeConsumer present + provider visible at status 100: - qtSummary AND qtHistory (the only eQueryType values). - Priority 1..999 AND 0..32767. - FilterMask/Spec asNone AND asAlarmActiveNow. eAlarmFilterState is single-state-valued (asNone=0, asAlarmActiveNow=1, asAlarmAcked=2, asShelved=3), not flag bits, so the filter surface is exhausted. GetStatistics continued to report total=0 active=0 codes=[7] for every poll across all combinations. User confirmation: the BoolAlarm extension on TestMachine_001.TestAlarm001 is evaluating (the $Alarm.InAlarm sub-attribute flips true/false in lockstep with the script writes, visible in aaObjectViewer). So the consumer chain is verified working end-to-end on our side. What's missing is producer-side publication into the aaAlarmManagedClient stream. Probable causes (config, not code): - BoolAlarm extension's "publish to alarm manager" / "Active" / "Enabled" flag may be off. - Alarm-vs-event mode setting may have it routing to events, not alarms. - Platform alarm area may not match the consumer's subscription scope. Resolution path: check the BoolAlarm extension's config in System Platform IDE; check aaObjectViewer's Active Alarms panel (not attribute panel) to see if the alarm appears there. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 33 ++++++++++++++++++ .../AlarmClientWmProbeTests.cs | 34 ++++++++++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 24045cd..74b87e5 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -236,6 +236,39 @@ omits `InitializeConsumer` entirely — that's a bug fix to apply even before A.2 lands, since without it the provider chain never becomes visible. +## Subscribe-parameter sweep — fifth probe run, 2026-05-01 + +Even with `InitializeConsumer` + provider visible at status 100, +no alarm transitions arrived during a 60s window with the user's +script flipping the test bool every 10s. Tried: + +- `qtSummary` and `qtHistory` (the only `eQueryType` values). +- Priority 1..999 and 0..32767. +- `eAlarmFilterState.asNone` and `asAlarmActiveNow` for both + `FilterMask` and `FilterSpecification`. + +`eAlarmFilterState` is single-state-valued (asNone=0, +asAlarmActiveNow=1, asAlarmAcked=2, asShelved=3), not flag bits. +None of these knobs surfaced any alarm activity. + +User confirmation 2026-05-01: the test bool does have a +`BoolAlarm` extension on it; in `aaObjectViewer` the +`$Alarm.InAlarm` sub-attribute flips true/false in lockstep with +the script's writes. So the alarm extension is **evaluating** +its condition, just not visibly producing transitions on the +`aaAlarmManagedClient` consumer stream. + +This isolates the unknown to the producer-side path — whether +the BoolAlarm extension's "publish to alarm manager" knob is on, +whether the platform is in an alarm area that matches the +consumer's subscription scope, or whether AVEVA has a separate +"events" path the BoolAlarm uses by default that this consumer +doesn't subscribe to. Resolving requires checking the BoolAlarm +extension's config in System Platform IDE (alarm priority, +category, "Active"/"Enabled" flags, alarm-vs-event mode) and +checking whether `aaObjectViewer`'s Active Alarms panel sees the +alarm fire. + ### Implications for A.2 implementation The A.2 PR's value is unmeasurable until at least one alarm diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 39f1b04..d139583 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -257,14 +257,38 @@ public sealed class AlarmClientWmProbeTests : IDisposable LogProviders(client, "after Register"); + // Dump the eQueryType enum so we can see what alternatives exist + // beyond qtSummary, in case Summary aggregates and we need a + // List/Snapshot mode instead. + try + { + Type qt = typeof(eQueryType); + Log($"eQueryType enum values: " + + string.Join(", ", Enum.GetNames(qt).Select(n => + $"{n}=0x{Convert.ToInt32(Enum.Parse(qt, n)):X}"))); + Type af = typeof(eAlarmFilterState); + Log($"eAlarmFilterState enum values: " + + string.Join(", ", Enum.GetNames(af).Select(n => + $"{n}=0x{Convert.ToInt32(Enum.Parse(af, n)):X}"))); + } + catch (Exception ex) + { + Log($"Enum dump threw: {ex.Message}"); + } + + // qtHistory + state=ActiveNow: stream historical alarm transitions + // including active alarms. asNone for FilterMask/Spec might + // literally mean "match alarms in state 'none'" (i.e., nothing), + // since the eAlarmFilterState enum is 0/1/2/3 single-states not + // flag bits. Try ActiveNow explicitly. int subscribe = client.Subscribe( szSubscription: SubscriptionExpression, - wFromPri: 1, wToPri: 999, - QueryType: eQueryType.qtSummary, + wFromPri: 0, wToPri: short.MaxValue, + QueryType: eQueryType.qtHistory, SortFlags: eSortFlags.sfReturnNewestFirst, - FilterMask: eAlarmFilterState.asNone, - FilterSpecification: eAlarmFilterState.asNone); - Log($"Subscribe('{SubscriptionExpression}') -> {subscribe}"); + FilterMask: eAlarmFilterState.asAlarmActiveNow, + FilterSpecification: eAlarmFilterState.asAlarmActiveNow); + Log($"Subscribe('{SubscriptionExpression}', qtHistory, state=ActiveNow, pri=[0..32767]) -> {subscribe}"); LogProviders(client, "after Subscribe"); -- 2.52.0 From bb7be14d1d1e26a2d90d37e1908e8f1b365d26cf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 08:26:29 -0400 Subject: [PATCH 07/16] =?UTF-8?q?probe:=20aaAlarmManagedClient=20receives?= =?UTF-8?q?=20no=20alarm=20data=20=E2=80=94=20full=20consumer=20chain=20ve?= =?UTF-8?q?rified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sixth probe iteration with every consumer-side knob exhausted: - Subscriptions tried (all rc=0): \Galaxy!, \Galaxy!*, \Galaxy!, \Galaxy!TestArea, \.\Galaxy!. - Read channels polled at 500ms: GetStatistics, GetHighPriAlarm, SFCreateSnapshot + SFGetStatistics. - Filters: priority 0..32767, qtSummary + qtHistory both tried, asAlarmActiveNow. - AlarmRecord pre-init to FILETIME epoch to dodge marshaler bug on default(DateTime). Result: every read API returns empty for the entire 60s window even with TestMachine_001.TestAlarm001 firing every 10s and aaObjectViewer confirming InAlarm transitions. The aaAlarmManagedClient.AlarmClient is not the receive surface AVEVA's alarm pipeline routes to in this Galaxy configuration. The consumer chain is verified working end-to-end: Initialize + Register + Subscribe all succeed, GetProviders finds the provider, the WM 0xC275 heartbeat fires at 1Hz to AVEVA's internal hwnd. There is simply no alarm data flowing through this consumer surface. Next investigation is not consumer-side: either find the SDK aaObjectViewer's alarm panel uses, or query the historian event storage directly. If alarms only flow via the historian path on this customer's Galaxy, the worker's PR A.5 architecture is a dead-end and A.2 needs a different transport. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 86 ++++++++-- .../AlarmClientWmProbeTests.cs | 158 +++++++++++++++++- 2 files changed, 225 insertions(+), 19 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 74b87e5..39e4484 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -258,16 +258,82 @@ the script's writes. So the alarm extension is **evaluating** its condition, just not visibly producing transitions on the `aaAlarmManagedClient` consumer stream. -This isolates the unknown to the producer-side path — whether -the BoolAlarm extension's "publish to alarm manager" knob is on, -whether the platform is in an alarm area that matches the -consumer's subscription scope, or whether AVEVA has a separate -"events" path the BoolAlarm uses by default that this consumer -doesn't subscribe to. Resolving requires checking the BoolAlarm -extension's config in System Platform IDE (alarm priority, -category, "Active"/"Enabled" flags, alarm-vs-event mode) and -checking whether `aaObjectViewer`'s Active Alarms panel sees the -alarm fire. +## Multi-channel + multi-subscription probe — sixth run, 2026-05-01 + +Extended the probe to try every consumer-side approach in +parallel: + +- **Subscription expressions** (sequential): `\Galaxy!`, + `\Galaxy!*`, `\\Galaxy!`, `\Galaxy!TestArea`, `\\.\Galaxy!`. + All Subscribe calls returned rc=0; the last one + (`\\.\Galaxy!`) is reflected in `GetProviders` (count=1). +- **Read channels** polled at 500ms cadence: `GetStatistics`, + `GetHighPriAlarm`, `SFCreateSnapshot` + `SFGetStatistics`. +- **Filter+sort**: priority 0..32767, `qtSummary`, + state=`asAlarmActiveNow`, sort=`sfReturnNewestFirst`. +- **AlarmRecord init** (worked around `Not a valid Win32 + FileTime` exception): all DateTime fields pre-set to FILETIME + epoch (1601-01-01 UTC) before the call, since + `default(DateTime)` is outside FILETIME range and trips the + interop marshaler. + +Result of the 60s run with `TestMachine_001.TestAlarm001` being +flipped every 10s: + +``` +Subscribe('\Galaxy!') -> 0 +Subscribe('\Galaxy!*') -> 0 +Subscribe('\\Galaxy!') -> 0 +Subscribe('\Galaxy!TestArea') -> 0 +Subscribe('\\.\Galaxy!') -> 0 +GetProviders [after Subscribe-multi] -> count=1 list=[ 0 \\.\Galaxy!] +GetStatistics #1: total=0 active=0 changes=1 codes=[7] positions=[] handles=[] +GetHighPriAlarm #1: rc=0 { } +SF channel #1: SFCreate=0 numAlarms=0 SFStats=0 unackRet=0 unackAlm=0 ackAlm=0 others=0 events=0 idxNewest=-1 +``` + +**No further "(changed)" entries for the entire 60s window.** +Every read API returned the same empty result on every poll. + +User confirms the alarm IS firing — `aaObjectViewer` sees +`$Alarm.InAlarm` flip in lockstep with the script. Historian +records exist (per user — needs verification by querying the +historian directly). + +## Conclusion of consumer-side probing + +`aaAlarmManagedClient.AlarmClient` is **not** the receive +surface AVEVA's alarm pipeline routes to in this Galaxy +configuration. The consumer chain is verified end-to-end: + +- `InitializeConsumer` + `RegisterConsumer` + `Subscribe` all + succeed (rc=0). +- `GetProviders` finds `\Galaxy!` once Initialize is called. +- All read APIs (`GetStatistics`, `GetHighPriAlarm`, + `SFCreateSnapshot`/`SFGetStatistics`) return empty even with + every documented filter combination. +- The consumer's hWnd receives zero AVEVA messages between + `WM_CREATE` and `WM_DESTROY`; AVEVA's traffic goes to its own + internal hwnd. + +The next investigation directions are not consumer-side: + +1. **Inspect `aaObjectViewer`'s alarm SDK** to see what library + it uses to read alarms. If different from + `aaAlarmManagedClient`, switch the worker over. +2. **Query the historian directly** (`aahEventStorage` / + `aahEventSvc`) to confirm alarms are recorded — and use the + same path for v2 alarm capture. +3. **Inspect AVEVA's alarm-routing config** for this Galaxy in + System Platform IDE — area assignments, alarm provider + bindings, "publish alarm events to" settings on the platform. + +For A.2 implementation: the `aaAlarmManagedClient` path the +gateway-worker is currently architected around may be a +dead-end on customer Galaxies configured this way. If the +alarms truly only flow through the historian event-storage path, +A.2 needs to consume from `aahEventStorage` instead — a +fundamental architecture pivot. ### Implications for A.2 implementation diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index d139583..44ac80e 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -32,6 +32,17 @@ namespace MxGateway.Worker.Tests; public sealed class AlarmClientWmProbeTests : IDisposable { // Probe configuration. Override in the constructor below if needed. + // Try multiple subscription expressions sequentially (each Subscribe call + // adds to the consumer's scope). The "everything" form varies by AVEVA + // version — we shotgun common forms. + private static readonly string[] SubscriptionExpressions = + { + @"\Galaxy!", // documented "all groups under Galaxy provider" + @"\Galaxy!*", // wildcard variant + @"\\Galaxy!", // double-backslash UNC-style + @"\Galaxy!TestArea", // explicit area where TestMachine_001 lives + @"\\.\Galaxy!", // local-host prefix + }; private const string SubscriptionExpression = @"\Galaxy!"; private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(60); private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500); @@ -281,16 +292,28 @@ public sealed class AlarmClientWmProbeTests : IDisposable // literally mean "match alarms in state 'none'" (i.e., nothing), // since the eAlarmFilterState enum is 0/1/2/3 single-states not // flag bits. Try ActiveNow explicitly. - int subscribe = client.Subscribe( - szSubscription: SubscriptionExpression, - wFromPri: 0, wToPri: short.MaxValue, - QueryType: eQueryType.qtHistory, - SortFlags: eSortFlags.sfReturnNewestFirst, - FilterMask: eAlarmFilterState.asAlarmActiveNow, - FilterSpecification: eAlarmFilterState.asAlarmActiveNow); - Log($"Subscribe('{SubscriptionExpression}', qtHistory, state=ActiveNow, pri=[0..32767]) -> {subscribe}"); + // Subscribe to every candidate expression — AVEVA accepts multiple + // overlapping subscriptions; whichever matches the producer wins. + foreach (string expr in SubscriptionExpressions) + { + try + { + int subscribe = client.Subscribe( + szSubscription: expr, + wFromPri: 0, wToPri: short.MaxValue, + QueryType: eQueryType.qtSummary, + SortFlags: eSortFlags.sfReturnNewestFirst, + FilterMask: eAlarmFilterState.asAlarmActiveNow, + FilterSpecification: eAlarmFilterState.asAlarmActiveNow); + Log($"Subscribe('{expr}') -> {subscribe}"); + } + catch (Exception ex) + { + Log($"Subscribe('{expr}') threw: {ex.GetType().Name}: {ex.Message}"); + } + } - LogProviders(client, "after Subscribe"); + LogProviders(client, "after Subscribe-multi"); // 3c. Pump for the configured duration. Log every message we see // (filtered light to avoid noise from WM_PAINT / WM_TIMER / @@ -322,6 +345,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable { PollGetStatistics(client, ++pollCount); LogProviders(client, $"poll #{pollCount}"); + PollAllChannels(client, pollCount); nextPoll = DateTime.UtcNow + PollInterval; } Thread.Sleep(10); @@ -349,6 +373,122 @@ public sealed class AlarmClientWmProbeTests : IDisposable private string lastStatsSummary = string.Empty; private string lastProvidersSummary = string.Empty; + private string lastHighPriSummary = string.Empty; + private string lastSfStatsSummary = string.Empty; + + /// + /// Try every read API the AlarmClient exposes and log when its + /// output changes. AlarmClient has at least three distinct read + /// surfaces — GetStatistics (current-change array), GetHighPriAlarm + /// (single-record peek), and the SF (stored filter) family — and any + /// of them might be the populated one. + /// + private static AlarmRecord NewAlarmRecord() + { + // The interop's auto-marshal flips DateTime fields to FILETIME on + // the way IN as well as OUT. default(DateTime) (year 1) is outside + // FILETIME's representable range, so initialize all DateTime fields + // to the FILETIME epoch (1601-01-01 UTC) to satisfy the marshaler. + AlarmRecord rec = new AlarmRecord(); + DateTime epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); + foreach (var f in typeof(AlarmRecord).GetFields( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic)) + { + if (f.FieldType == typeof(DateTime)) + { + object boxed = rec; + f.SetValue(boxed, epoch); + rec = (AlarmRecord)boxed; + } + } + return rec; + } + + private void PollAllChannels(AlarmClient client, int seq) + { + // Channel A: GetHighPriAlarm — direct peek of highest-priority alarm. + try + { + AlarmRecord rec = NewAlarmRecord(); + int rc = client.GetHighPriAlarm(ref rec); + string desc = rc == 0 ? DescribeAlarmRecord(rec) : ""; + string summary = $"rc={rc} {desc}"; + if (summary != lastHighPriSummary) + { + Log($"GetHighPriAlarm #{seq}: {summary} (changed)"); + lastHighPriSummary = summary; + } + } + catch (Exception ex) + { + string es = $"{ex.GetType().Name}: {ex.Message}"; + if (es != lastHighPriSummary) + { + Log($"GetHighPriAlarm #{seq}: threw {es}"); + lastHighPriSummary = es; + } + } + + // Channel C: GetAlarmExtendedRec by index. Try indices 0..3 directly; + // populated alarms (if any) appear at low indices. + for (int idx = 0; idx <= 2; idx++) + { + try + { + AlarmRecord rec = NewAlarmRecord(); + int rc = client.GetAlarmExtendedRec(idx, ref rec); + if (rc == 0) + { + string desc = DescribeAlarmRecord(rec); + Log($"GetAlarmExtendedRec(idx={idx}) #{seq}: rc=0 -> {desc}"); + break; // log first present record only + } + } + catch (Exception ex) + { + if (idx == 0) + { + Log($"GetAlarmExtendedRec(idx=0) #{seq}: threw {ex.GetType().Name}: {ex.Message}"); + } + break; + } + } + + // Channel B: SF — snapshot + GetStatistics + iterate. + try + { + uint numAlarms = 0; + int sfCreate = client.SFCreateSnapshot(0, ref numAlarms); + int unackRet = 0, unackAlm = 0, ackAlm = 0, others = 0, events = 0, idxNewest = 0; + int sfStats = client.SFGetStatistics( + ref unackRet, ref unackAlm, ref ackAlm, + ref others, ref events, ref idxNewest); + string summary = $"SFCreate={sfCreate} numAlarms={numAlarms} " + + $"SFStats={sfStats} unackRet={unackRet} unackAlm={unackAlm} " + + $"ackAlm={ackAlm} others={others} events={events} idxNewest={idxNewest}"; + if (summary != lastSfStatsSummary) + { + Log($"SF channel #{seq}: {summary} (changed)"); + lastSfStatsSummary = summary; + + // If non-zero, fetch the first record by index via the + // standard GetAlarmExtendedRec — after SFCreateSnapshot the + // indices reference the snapshot. + if (numAlarms > 0) + { + AlarmRecord rec = new AlarmRecord(); + int recRc = client.GetAlarmExtendedRec(0, ref rec); + Log($" GetAlarmExtendedRec(0) [post-snapshot] rc={recRc} -> {DescribeAlarmRecord(rec)}"); + } + } + client.SFDeleteSnapshot(); + } + catch (Exception ex) + { + Log($"SF channel #{seq}: threw {ex.GetType().Name}: {ex.Message}"); + } + } + private void LogProviders(AlarmClient client, string when) { -- 2.52.0 From 39f9fd8946b42f7ea767f24e7f35daf662669772 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 09:06:45 -0400 Subject: [PATCH 08/16] =?UTF-8?q?probe:=20BREAKTHROUGH=20=E2=80=94=20alarm?= =?UTF-8?q?s=20flow=20via=20canonical=20\Node\Galaxy!Area,=20blocked=20by?= =?UTF-8?q?=20DateTime=20marshaling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings that turn the alarm capture path on: 1. Subscription expression: \\Galaxy! is the canonical AlarmClient subscription format per ArchestrA docs: \Node\Provider!Area!Filter, with Provider literally "Galaxy" (not the Galaxy name) and Node being the machine name. For this rig: \DESKTOP-6JL3KKO\Galaxy!DEV catches alarms. 2. InitializeConsumer before RegisterConsumer — discovered earlier; bug-fix for PR A.5's AlarmClientConsumer. With these in place, GetHighPriAlarm returned a record on every poll for 60s straight (117/117 calls). But every call throws ArgumentOutOfRangeException: Not a valid Win32 FileTime, because AlarmRecord has five DateTime fields (ar_Time / ar_OrigTime / ar_AckTime / ar_RtnTime / ar_SubTime) and AVEVA writes sentinel FILETIME values for unset ones (e.g., ar_AckTime on an unacknowledged alarm). The aaAlarmManagedClient.dll auto-marshals FILETIME -> DateTime and rejects out-of-range values. GetStatistics still reports total=0 active=0 even with GetHighPriAlarm returning records — those two APIs have different views. The active read API for current alarms is GetHighPriAlarm, not GetStatistics's change array. So the consumer chain works. The blocking issue is now extracting the payload past the AVEVA-shipped DateTime auto-marshaling. Three approaches for the next PR: 1. Patch aaAlarmManagedClient.dll via ildasm/ilasm round-trip. 2. Define a custom [ComImport] interface with safe-blittable types and Marshal.QueryInterface to it. 3. Use IDispatch late binding to bypass strong-typed marshaling. Option 2 is cleanest; needs the AlarmClient COM IID. Probe changes: - Subscription expression set to \\Galaxy!DEV. - GetHighPriAlarm tally counters (ok-with-record vs throw). - 117 throws / 0 ok-with-record over 60s confirms alarms are flowing continuously while the user's flip script runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 63 +++++++++++++++++++ .../AlarmClientWmProbeTests.cs | 36 ++++++++--- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 39e4484..0a05b7f 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -335,6 +335,69 @@ alarms truly only flow through the historian event-storage path, A.2 needs to consume from `aahEventStorage` instead — a fundamental architecture pivot. +## BREAKTHROUGH — seventh probe run, 2026-05-01 + +Two changes finally produced a signal: + +1. **Subscription scope:** `\\\Galaxy!` is the + canonical AlarmClient subscription format (per ArchestrA Alarm + Client docs at `archestra6.rssing.com/chan-12008125/article13.html`): + `\\Node\Provider!Area!Filter`, where Node is the *machine* name, + Provider is **literally `Galaxy`**, and Area is a hosted area + object. For this rig (`\\DESKTOP-6JL3KKO\Galaxy!DEV`) the DEV + area — the platform's primary area — is the right scope. Earlier + `\Galaxy!`, `\Galaxy!TestArea`, `\\.\Galaxy!`, etc., all returned + rc=0 but matched no traffic — they were not the canonical form. +2. **`InitializeConsumer` before `RegisterConsumer`** — already + discovered earlier; bug-fix for PR A.5's `AlarmClientConsumer`. + +With both in place, `GetHighPriAlarm` returned a record on every +poll for 60s straight (117/117 calls), but threw +`ArgumentOutOfRangeException: Not a valid Win32 FileTime` instead +of returning successfully — the AlarmRecord struct contains five +DateTime fields (`ar_Time`, `ar_OrigTime`, `ar_AckTime`, +`ar_RtnTime`, `ar_SubTime`) and AVEVA writes sentinel/invalid +FILETIME values for unset ones (e.g., `ar_AckTime` for an +unacknowledged alarm). The .NET interop that AVEVA ships +(`aaAlarmManagedClient.dll`) auto-converts FILETIME→DateTime and +rejects out-of-range values. + +`GetStatistics` continues to report `total=0 active=0` even with +GetHighPriAlarm returning records — those two API surfaces have +genuinely different views in AVEVA's data model. + +So: **alarms flow through `aaAlarmManagedClient.AlarmClient` once +the subscription expression is canonical**. The blocking issue is +extracting the payload past the .NET interop's DateTime +auto-marshaling. + +## Remaining work to capture alarm payloads + +Define a custom COM interop that uses `long` (FILETIME-as-int64) +instead of `DateTime` for the timestamp fields. Approach options: + +1. **Patch the AVEVA-shipped `aaAlarmManagedClient.dll`** — ildasm + the assembly, replace `DateTime` with `long` on AlarmRecord's + timestamp fields, ilasm back. Brittle across AVEVA upgrades. +2. **Write our own `[ComImport]` interface** — declare + `IRawAlarmConsumer` ourselves with safe-blittable types, + discover the underlying COM IID (via reflection on + `AlarmClient`'s `[Guid]` attribute), and `(IRawAlarmConsumer) + alarmClient` cast. Cleaner; requires the IID. +3. **Use `IDispatch` late binding** — dispatch-Invoke bypasses + strong-typed marshaling. Verbose but doesn't need IIDs. + +For PR A.2's worker integration, option 2 is the least +disruptive. Once the interop is custom, `AlarmClient.Subscribe` + +`GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable +polling-style alarm consumer. + +The polling-vs-WM_APP-callback question from earlier is now +moot: `GetStatistics`'s `positions[]/handles[]` arrays remained +empty even when alarms were demonstrably present. The active +read API for current alarms is `GetHighPriAlarm`, not +`GetStatistics`'s change array. + ### Implications for A.2 implementation The A.2 PR's value is unmeasurable until at least one alarm diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 44ac80e..6e156c4 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -35,13 +35,20 @@ public sealed class AlarmClientWmProbeTests : IDisposable // Try multiple subscription expressions sequentially (each Subscribe call // adds to the consumer's scope). The "everything" form varies by AVEVA // version — we shotgun common forms. + // Canonical AlarmClient subscription format (per ArchestrA docs): + // \\Node\Provider!Area!Filter + // - Node: machine name (NOT galaxy name; "Galaxy" is the literal provider) + // - Provider: literal "Galaxy" + // - Area: area object the engine hosts the alarm under + // Note: each Subscribe call REPLACES the prior subscription on the + // consumer, so we test exactly one expression per probe run. + private static readonly string MachineName = Environment.MachineName; private static readonly string[] SubscriptionExpressions = { - @"\Galaxy!", // documented "all groups under Galaxy provider" - @"\Galaxy!*", // wildcard variant - @"\\Galaxy!", // double-backslash UNC-style - @"\Galaxy!TestArea", // explicit area where TestMachine_001 lives - @"\\.\Galaxy!", // local-host prefix + // DEV is the top-level area on the Platform (TestArea is contained + // within DEV). Alarms typically publish at the platform's primary + // area. If TestArea-only doesn't catch them, DEV should. + $@"\\{MachineName}\Galaxy!DEV", }; private const string SubscriptionExpression = @"\Galaxy!"; private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(60); @@ -352,6 +359,8 @@ public sealed class AlarmClientWmProbeTests : IDisposable } Log($"Pump duration {PumpDuration.TotalSeconds:F0}s elapsed; deregistering."); + Log($"GetHighPriAlarm tally: ok-with-record={getHighPriOk} threw={getHighPriThrow} " + + $"(throws indicate alarm-record marshaling failure; ok=empty record)."); try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); } catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); } @@ -375,6 +384,8 @@ public sealed class AlarmClientWmProbeTests : IDisposable private string lastProvidersSummary = string.Empty; private string lastHighPriSummary = string.Empty; private string lastSfStatsSummary = string.Empty; + private int getHighPriOk = 0; + private int getHighPriThrow = 0; /// /// Try every read API the AlarmClient exposes and log when its @@ -406,25 +417,32 @@ public sealed class AlarmClientWmProbeTests : IDisposable private void PollAllChannels(AlarmClient client, int seq) { - // Channel A: GetHighPriAlarm — direct peek of highest-priority alarm. + // Channel A: GetHighPriAlarm — peek highest-priority alarm. Track + // outcome state (record/empty/throw) and log every transition AND + // total counts at end. The throw correlates with an alarm being + // present (AVEVA fills timestamps with sentinel FILETIME values + // that crash the .NET marshaler) — useful as a presence signal + // even if we can't read the record. try { AlarmRecord rec = NewAlarmRecord(); int rc = client.GetHighPriAlarm(ref rec); string desc = rc == 0 ? DescribeAlarmRecord(rec) : ""; string summary = $"rc={rc} {desc}"; + getHighPriOk++; if (summary != lastHighPriSummary) { - Log($"GetHighPriAlarm #{seq}: {summary} (changed)"); + Log($"GetHighPriAlarm #{seq}: {summary} (changed; ok={getHighPriOk}, throw={getHighPriThrow})"); lastHighPriSummary = summary; } } catch (Exception ex) { - string es = $"{ex.GetType().Name}: {ex.Message}"; + string es = $"{ex.GetType().Name}"; + getHighPriThrow++; if (es != lastHighPriSummary) { - Log($"GetHighPriAlarm #{seq}: threw {es}"); + Log($"GetHighPriAlarm #{seq}: threw {es} (changed; ok={getHighPriOk}, throw={getHighPriThrow})"); lastHighPriSummary = es; } } -- 2.52.0 From f490ae25937a3c92255668d9fa18efd2559c334d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 09:15:37 -0400 Subject: [PATCH 09/16] =?UTF-8?q?docs:=20revise=20interop=20fix=20path=20?= =?UTF-8?q?=E2=80=94=20wnwrapConsumer.dll=20is=20the=20right=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reflection on aaAlarmManagedClient.AlarmClient shows it implements only IDisposable (no [ComImport] interface, no class GUID) and has a single field "CwwAlarmConsumer* m_almUnmanaged". So AlarmClient is a C++/CLI managed wrapper around a native C++ class -- NOT a COM-interop class. The DateTime conversion happens INSIDE AVEVA's wrapper IL, not at the .NET-COM marshaling boundary. There's no separate COM interface to QI to. Revised approach (in docs/AlarmClientDiscovery.md): A. wnwrapConsumer.dll -- separate standalone COM library AVEVA ships at "C:\Program Files (x86)\Common Files\ArchestrA" exposing WNWRAPCONSUMERLib.wwAlarmConsumerClass with SetXmlAlarmQuery / GetXmlCurrentAlarms. XML-string output bypasses FILETIME marshaling entirely. Best fit -- real COM, self-contained, conventional production-grade approach. B. Patch aaAlarmManagedClient.dll IL -- direct but modifies a vendor binary, brittle to upgrades. C. Reflect into m_almUnmanaged and call native vtable directly -- requires reverse-engineering the C++ class layout. Picking A. Probe restored to Skip; next commit starts the wnwrapConsumer integration. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 30 ++++++++++++ .../AlarmClientWmProbeTests.cs | 46 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 0a05b7f..d9fe774 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -392,6 +392,36 @@ disruptive. Once the interop is custom, `AlarmClient.Subscribe` + `GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable polling-style alarm consumer. +**REVISED 2026-05-01 — option 1 not directly applicable.** +Reflection on `aaAlarmManagedClient.AlarmClient` shows it +implements only `IDisposable` (no `[ComImport]` interface, no +class GUID). It has a single field `CwwAlarmConsumer* +m_almUnmanaged` — meaning `AlarmClient` is a **C++/CLI managed +wrapper around a native C++ class**, NOT a COM-interop class. +The DateTime conversion happens inside the AVEVA wrapper's IL, +not at a .NET-to-COM marshaling boundary. There is no separate +COM interface IID we can QI to. + +Revised approach options: + +A. **Switch to `wnwrapConsumer.dll`** — a separate standalone + COM library AVEVA ships at + `C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll` + exposing `WNWRAPCONSUMERLib.wwAlarmConsumerClass` with + `SetXmlAlarmQuery` / `GetXmlCurrentAlarms`. XML-string output + bypasses FILETIME marshaling entirely. +B. **Patch `aaAlarmManagedClient.dll` IL** — wrap the unsafe + `DateTime.FromFileTime` calls with a safe variant. Direct + fix but modifies a vendor binary. +C. **Reflect into `m_almUnmanaged` and call native vtable** — + get the IntPtr, walk the MSVC C++ vtable, call + `__thiscall` methods via `Marshal.GetDelegateForFunctionPointer`. + Doable but requires reverse-engineering the C++ class layout. + +Option A is the best fit: real COM-based, self-contained in +our code, conventional production-grade approach (the WIN-911 +consumer pattern referenced in AVEVA support forums uses it). + The polling-vs-WM_APP-callback question from earlier is now moot: `GetStatistics`'s `positions[]/handles[]` arrays remained empty even when alarms were demonstrably present. The active diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 6e156c4..ad716de 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -250,6 +250,52 @@ public sealed class AlarmClientWmProbeTests : IDisposable { client = new AlarmClient(); + // One-time interop introspection: dump AlarmClient's class GUID + // (CoClass IID) and every interface it implements with their + // GUID + InterfaceType. The IID we need to redeclare with safe + // blittable types is the one whose vtable carries + // GetHighPriAlarm. + try + { + Type ct = client.GetType(); + Log($"=== AlarmClient interop introspection ==="); + Log($"Class FullName: {ct.FullName}"); + var classGuid = ct.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true) + .Cast().FirstOrDefault(); + Log($"Class GUID: {classGuid?.Value ?? "(none)"}"); + foreach (var iface in ct.GetInterfaces()) + { + var ig = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true) + .Cast().FirstOrDefault(); + var ity = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.InterfaceTypeAttribute), true) + .Cast().FirstOrDefault(); + int methodCount = iface.GetMethods().Length; + Log($" iface {iface.FullName} | GUID={ig?.Value ?? "(none)"} | type={ity?.Value.ToString() ?? "(none)"} | methods={methodCount}"); + } + // Dump fields (private/internal) — the COM object reference + // is likely on a private field. + Log($"--- AlarmClient instance fields ---"); + foreach (var f in ct.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + Log($" field {f.FieldType.FullName} {f.Name} (public={f.IsPublic})"); + } + // Dump base class chain. + Log($"--- base class chain ---"); + Type? baseT = ct.BaseType; + int depth = 0; + while (baseT != null && depth < 5) + { + Log($" base[{depth}]: {baseT.FullName}"); + baseT = baseT.BaseType; + depth++; + } + Log($"=== end introspection ==="); + } + catch (Exception ex) + { + Log($"Interop introspection threw: {ex.GetType().Name}: {ex.Message}"); + } + // Try InitializeConsumer first — separate from RegisterConsumer // per the discovered API surface; previous probe runs skipped // it. Some AVEVA managed-client patterns require Initialize -- 2.52.0 From f711a55be4780dee51a2bc087b7decf5f3cafb4c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 09:44:15 -0400 Subject: [PATCH 10/16] A.2: replace AlarmClientConsumer with wnwrap-based polling consumer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` to `EventHandler` 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) --- docs/AlarmClientDiscovery.md | 196 +++++++++ lib/Interop.WNWRAPCONSUMERLib.dll | Bin 0 -> 13824 bytes .../AlarmRecordTransitionMapperTests.cs | 88 +++- .../MxAccess/WnWrapAlarmConsumerXmlTests.cs | 112 +++++ .../MxGateway.Worker.Tests.csproj | 6 + .../WnWrapConsumerProbeTests.cs | 287 +++++++++++++ .../MxAccess/AlarmClientConsumer.cs | 191 --------- .../MxAccess/AlarmRecordTransitionMapper.cs | 167 ++++++-- .../MxAccess/IMxAccessAlarmConsumer.cs | 67 +-- .../MxAccess/MxAccessAlarmEventSink.cs | 73 +--- .../MxAccess/MxAlarmSnapshot.cs | 59 +++ .../MxAccess/WnWrapAlarmConsumer.cs | 386 ++++++++++++++++++ src/MxGateway.Worker/MxGateway.Worker.csproj | 12 +- 13 files changed, 1326 insertions(+), 318 deletions(-) create mode 100644 lib/Interop.WNWRAPCONSUMERLib.dll create mode 100644 src/MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs create mode 100644 src/MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs delete mode 100644 src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs create mode 100644 src/MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs create mode 100644 src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index d9fe774..9ad4e54 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -492,3 +492,199 @@ Skip-gated test: PR A.5's `Subscribe` / `AcknowledgeByGuid` / `SnapshotActiveAlarms` are correct — they're pull-style and don't depend on the notification mechanism. + +## Option A — captured, 2026-05-01 + +`wnwrapConsumer.dll` (`C:\Program Files (x86)\Common Files\ +ArchestrA\wnwrapConsumer.dll`) hosts the standalone COM class +`WNWRAPCONSUMERLib.wwAlarmConsumerClass`. Type library imports +cleanly via `tlbimp` (output stored under `mxaccessgw/lib/ +Interop.WNWRAPCONSUMERLib.dll`). The COM class is registered in +`HKLM:\SOFTWARE\WOW6432Node\Classes\CLSID\ +{7AB52E5F-36B2-4A30-AE46-952A746F667C}` with `ThreadingModel= +Apartment` — `new wwAlarmConsumerClass()` succeeds via +`CoCreateInstance`. + +The probe `MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs` +(Skip-gated, archival) drove the captured run. Lifecycle: + +1. `new wwAlarmConsumerClass()` — instantiated. +2. `InitializeConsumer("MxGatewayProbe.WnWrap")` -> 0. +3. `RegisterConsumer(hWnd: 0, productName, applicationName, + version)` -> 0. **Note:** wnwrap's `RegisterConsumer` is + 4-arg (no `bRetainHiddenAlarms`); `aaAlarmManagedClient`'s + is 5-arg. Different surface. +4. `Subscribe(@"\\\Galaxy!DEV", priLow=1, priHigh=999, + qtSummary, sfReturnNewestFirst, asAlarmActiveNow, + asAlarmActiveNow)` -> 0. Same canonical scope that worked + for `aaAlarmManagedClient`. +5. `SetXmlAlarmQuery(...)` was called too but the round-trip + `GetXmlAlarmQuery` returned a mangled echo (NODE became + `DESKTOP-6JL3KKO\Galaxy!DEV`, PROVIDER became `Galaxy!DEV`, + ALARM_STATE shortened to `All`, DISPLAY_MODE truncated to + `Sum`). The XML-query path looks broken in this build; rely + on `Subscribe` for the filter and skip `SetXmlAlarmQuery` in + production. Confirming "Subscribe alone is sufficient" is + one follow-up probe (call `Subscribe` and read XML, no + `SetXmlAlarmQuery`) — out of scope for the breakthrough run + but easy to verify. + +### Captured XML (60 polls over 30s, 500ms cadence) + +`GetXmlCurrentAlarms2(maxAlmCnt: 100, out vartCurrentXmlAlarms)` +returned BSTR XML cleanly on every call — 60/60 ok, zero throws. +`GetXmlCurrentAlarms` (the v1 method) returned identical content +on the same cadence; either method is viable. + +Empty state: + +```xml + +``` + +With alarm active (`UNACK_ALM`, value=true after the flip +script set the bool true): + +```xml + + + + BCC4705395424D65BDAABCDEA6A32A73 + 2026/5/1 + + 240 + 0 + DESKTOP-6JL3KKO + Galaxy + TestArea + TestMachine_001.TestAlarm001 + DSC + true + true + 500 + UNACK_ALM + + + Test alarm #1 + + +``` + +After the script set the bool false (`UNACK_RTN`, value=false): + +```xml + + + + BCC4705395424D65BDAABCDEA6A32A73 + 2026/5/1 + + ... + false + UNACK_RTN + ... + + +``` + +The 10s cadence between transitions matches the System Platform +script's flip frequency exactly. **GUID is stable across the +in→out cycle** (`BCC4705…` carried through both states), so the +XML stream represents the alarm record's lifecycle, not separate +event records — this is "current alarms snapshot," not +"transition stream." For an OPC UA `AlarmConditionService` +adapter this is fine: condition-state changes per-snapshot is +the supported model. + +`STATE` enum values observed: `UNACK_RTN` (the alarm has +returned to normal but is unacknowledged — i.e., visible in the +"current alarms" list because operator hasn't acked it yet) and +`UNACK_ALM` (the alarm is currently active and unacknowledged). +The other states from `eAlmState` (`ACK_RTN`, `ACK_ALM`) would +appear when an ack is performed — `wwAlarmConsumerClass.AlarmAckByGUID` +is the method to call. + +### `GetStatistics` AV — unrelated quirk + +Every `GetStatistics` call threw `AccessViolationException` in +the probe. Cause: the wnwrap interop signature uses `IntPtr` for +the three array out-parameters (`pChangeCode`, `pChangePos`, +`phAlarm`); passing `IntPtr.Zero` is wrong — the COM impl is +writing into the buffer pointer without null-checking. Pre- +allocate three int-arrays and pass pinned pointers (or use +`Marshal.AllocCoTaskMem`) to fix. Not required for the +production path — the XML methods give us everything we need. + +### Implications for PR A.2 worker integration + +Replacing `aaAlarmManagedClient.AlarmClient` with +`WNWRAPCONSUMERLib.wwAlarmConsumerClass` in the worker's +alarm-consumer surface unblocks A.2 fully. Outline: + +1. **Reference path:** drop `aaAlarmManagedClient.dll` reference + from `MxGateway.Worker.csproj`; add `Interop.WNWRAPCONSUMERLib.dll` + reference from `mxaccessgw/lib/`. (Or commit the interop dll + in-tree under `lib/` and reference relatively.) +2. **`AlarmClientConsumer` → `WnWrapAlarmConsumer`:** rewrite + the consumer wrapper to: + - `new wwAlarmConsumerClass()` on the worker's STA thread. + - `InitializeConsumer(applicationName)` then + `RegisterConsumer(hWnd: 0, …)`. + - `Subscribe(@"\\\Galaxy!", …)` per configured + area. The `` and `` are configurable (default + `Environment.MachineName` + the platform's primary area). + - Poll `GetXmlCurrentAlarms2(maxAlmCnt, out xml)` on a + timer (500ms-1s cadence is comfortable). Parse XML + payload; diff against the previous snapshot (keyed by + `GUID`); emit `MX_EVENT_FAMILY_ON_ALARM_TRANSITION` + events for added/changed/removed records. + - `AlarmAckByGUID(VBGUID, comment, oprName, node, domain, + fullName)` for client-driven acknowledgements (matches + PR A.5's `AlarmAckCommand` payload). + - Lifecycle teardown: `DeregisterConsumer` + + `UninitializeConsumer` + `Marshal.FinalReleaseComObject`. +3. **Conversion layer:** map XML record fields to + `MxAlarmConditionRecord` proto: + - `GUID` → `condition_id` (canonicalize the no-dashes hex + to a UUID string). + - `STATE` enum → `inAlarm` + `acked` booleans + (`UNACK_ALM` → in_alarm=true, acked=false; + `UNACK_RTN` → in_alarm=false, acked=false; + `ACK_ALM` → in_alarm=true, acked=true; + `ACK_RTN` → in_alarm=false, acked=true). + - `DATE + TIME + GMTOFFSET + DSTADJUST` → reassemble UTC + timestamp; matches the worker's existing `Timestamp` + wire format. + - `PRIORITY` → severity (already 1-1000-ish range). + - `TAGNAME` → reference; `PROVIDER_NAME` + `GROUP` for + scope metadata. +4. **PR A.5 fix carry-over:** `InitializeConsumer` MUST be + called before `RegisterConsumer` (rediscovered with + `aaAlarmManagedClient`, also true here). The existing + `AlarmClientConsumer` skips Initialize entirely; the new + `WnWrapAlarmConsumer` includes it from day one. +5. **Test reuse:** PR A.5's snapshot/ack contract tests can + stay — they don't touch the underlying COM API. Add a new + integration test against the wnwrap surface (live-AVEVA-only, + Skip-gated like the probe). + +### Settled API-ordering and surface knowledge + +- `InitializeConsumer` first, then `RegisterConsumer` — both + on `aaAlarmManagedClient.AlarmClient` and + `wwAlarmConsumerClass`. +- `RegisterConsumer` arity differs: + `aaAlarmManagedClient.AlarmClient.RegisterConsumer(hWnd, + product, app, version, bRetainHiddenAlarms)` — 5 args; + `wwAlarmConsumerClass.RegisterConsumer(hWnd, product, app, + version)` — 4 args. The wnwrap class has no + `bRetainHiddenAlarms` parameter at all. +- Subscription expression format: `\\\Galaxy!` + (literal `Galaxy` provider) for both libraries. +- Native ack: `AlarmAckByGUID(VBGUID guid, comment, oprName, + node, domain, fullName)` on the v2 surface; ID 5-arg + variant on the legacy `IwwAlarmConsumer` interface. + +These findings retire the open follow-up probes from the +"polling-vs-pump" debate above — `wwAlarmConsumerClass` plus +poll-on-timer is the implementation. diff --git a/lib/Interop.WNWRAPCONSUMERLib.dll b/lib/Interop.WNWRAPCONSUMERLib.dll new file mode 100644 index 0000000000000000000000000000000000000000..2fccd23a81156db82ea886953cb6dbab0c35f94d GIT binary patch literal 13824 zcmeHO3v?Xib-pvRvs%fvEcqoFzsA@G46M9Qtu}sXpO%ay8|z^R5NW+TmKN_r?5^ZT z(jWpQh7y8HUEW}$0HNU#f(e*MY>ojpCx@mdISnqfL@gu^&>YIis}!jF-G6pRE6HhT za+)4e#(dx0d;k0Y^WXd5d1!WM|Ljeq5RnBqb&BXI?0IVz{9n@unyZ&QT}_87Utao@ z)b{eyu7PA;8_Z?n5M8{ZdI z!-f8(jc60geMAmU#N=&+X!>N^$I&$+TbZ8?dP|w!P|$Z3poi@sw24@Y^M)oi4YO*< z<#TapVs3p9c%d9X+18ANH01PD7LH=BBWUEYJV4pjOcX3rg{W~}vGLqwQfaeBbm#d* z5*vSqE!ep+C>4A2T!uXjjs~~G?ez0tDy6_LEFfC;MTD`CXsilyXCar&^yk^5=4>Nx z%ep(M9x_&smTl;64P$=}^nwaKn96QrXKVD_QyX6>gem%zZZ08NgY<#efZt zoH|q$ol&$&2oh#OxMIde%txz?y_H_3w^4qT67y2Qs|1GycT1N_R*DPx^MY>_e2=hy zO~{7?9~b<0!v3n@HwC{X>=v2FKLgC0acs`uL4;mKND*X1Yba`UqHUe2N$}hRZ9e?$ z#8D-n`JsJQXj!3IirOBbRY0pm#~YxnMOF;Yn?>8qqIQqa>WbQGG`N<%!#N$#umup0z)NCTddjJ0LH>T9*1ff;K)k zj4_{WID_LFXVADrVO1o~;B?cIGdRQ4ID^%u#u=)8dsg$)Hs7{OpU89Vrrbh0aN1) zCQXeqc)6)@)ul|0GkBw^y>6|dn@o)=X3>6ATV8=$GPM?Hx0+g}VgcP|YS*Ie4pZZ* zyVKOT>JFOPXoZ{ZGqo2h{B*yman(IwY9}iq^q{G6)jeivvaOfCZE9R~Pnnu#yMm6G z8du$4o7#F?9sShQE`fH^)OOnz(9cYbtL|-6yV+JJRq!nk8{cIbWl581HCUlmu!Sxu zwPMj*i?ZQwF!y?3g59w2IL9!8LN9|Y^a@y|*TGhLM=tionCv49A4T|BgpUdz8LbNx z3A_#`hD4VsGCcd0@xIr>F4KOnLU)5LbRSryN5NJ)46dLTO8yw(Bu4ufvl5=;N10~f zUUguF&H`J+oK-PrtC(|zn6u4{OU4Mz78!Hf1Xk!Wu!T}!m3D!x^kr}b-3+$T-Ino8 zK5F3@zYV*B*>bNJA*=L5*sb&r;0k&NY@_#KuOzD~kb7-AIJHT^k7kD0B1I85?Tu=9c=hGwLGw4b10y+*}NIwEE zqLbjo^lR`EQmwM4QN2~BrKEw+q_eC>MCDvwC%m#8qby=nM2r>@qbg!Vg@{NwMw^JS zQp8v#Vw@&ooGxOVA!4i+G0qe*)`%EuMU1m3j=9YiG0qV&&J{7v6EW7&AX-b2egX0s zBE|(G#)TrrMIy$-gWD9Y(KD_8GzFLcKT_=5w8!b%OfGQ!v;3qEA50a};$Vih5D!^F?i+A*vcrT8LsH z{a;k|r}u?V?+eAN#HaTKBN2kj@0fDbrc zIrN0pkEhEM*4L%^LS71fQ}Tn~mRiC8DqRfzwG;z?AYBK3O4t5v;TEC-=LEfjl0r_VNNvr90MUfWJEQNG6G|NNbCwav zYb;koK501sIcph(T&o_1yvs5MzRE)KYC2@mz^m0}a9HgFx2q%I9`yjYS3L?IP{+Ut zg%plm1E-Z{@UYSczET+h?^O=>umiBj8<@1K_JH zN5O|IW8l>)sXPY_+^#l*d(=K~uQ~!AP>+HW${2W+LROxG1|C+L!B;AM;JwNSc%O0r ze7kZKJgSU=A5y4-=b(YtSen6EOCNZbWdwYcA!%4+7tS{4nq+@EGto@C5KAa2R+7 zcn&xMJP#ZLeh9n<{3GyV;3V)C@D9fNIq)0E?*ktI9|5NTNotc7Km{s*N?;l=1DFXc zl3L^?z*1lZ;01huA6N^l2LeDy+9*fh-->-Z^j;ta#DN_^8pr}y0C`{t_zL{@1GfOT z0SAFm;69A;0Pq;($AKq+CxOGjGr)7e5#am4E5P3auK{lWKLJjn?_0nxApZ*Z4e&nj z0q_xU3Xo*PC%4G8z-*unI2%|AoCnx}Re&4t0zSYGtOnM~Sa3z%k%O z;5hJI;CsMpz#G6%fRn(xzkX(t1yBi017-j-fm&cTFc+u;Rw&2| zuoAEXYk~DZ0Ehq^fL7oF1?vv~3$gEp-VY>!9Y7u!0(JtsfzJbf0_=zXUBErSLEu5) z8^FUD<5A!+3z%k%O;5hIK@MGYgfH#15fS&{JqVIda2arDkP63hyaRVw) z0aOCUicIuP^;c;af05yh5#H;ov-3>(l}ME8K5YdWg1wAHiK$rEgLX_*=O5T1E(YDBteK zj~_;s468&hl$zJHXA?syeI2##+!;v4a_LYulOIa!ITvm1*t#XKIkc&xv%5XAr7gLQ zCb>-MGua`Oiskd9UpS=ac6aR_)JgBm<_ghN3_mvN;%L!is-WjO3$cPuJwfq9BO217 zf&LJFsb;5iS1y*xCksi;OXwx%Xlq-ftF=8s>3lq!OJRDg=|K#yC!)D*nh%F52MYLY zbK4L`@7$d)=;?+nLzzM{tv9r0aNO)*r=A;5#`QcE{d@FW9!DDQwqYol7}ttkygRMY z2yN50%k_AnctjFO5Qp0yic;(fgA~ zUanLcG_V!c{GQD@)K0w65lidP0)vC8WIV(4Q-HK-FjqWw2bWKNPdJ;7B}?-`Yjh}; z5+0@Q#sTyMZO{uBrEwgb0-|1NIF_4a>74AcEnmo?Q){N!J2aHbVIpFXyo=JYU0lnU z8IM(LEFC0IokQF5aa1SIvnUU8_Gl*;@a9~Sc6McXH-7$%pFYNk(;mz3pyD|+sK=Ar ziwj_8C%9XjQ<#R>8_}9Z#EJ|Ccb8U>gD=Wh+u>wFFQs!sE;}?Rnvf?x15pskc%BAR zoAq2AF&jA>Om$@oC`}%S8w2q|a@aW2S?$n`YYfYwfmo(r&xf)&A!*Ri#5rG-H)r8B zAV%Ws8Uj+C(038%9U(=AjoO1aevv@vY2y+hyy7^y^J3zxj)%coDMsQef-xTLY|F-D zsV*3T$ylCthC922ApCp8k=v5#WPvvKkRhZl971guc2f##mCXq$l+Er)>Y{AT0u$3g zIf!dwQ4!}S%cyLWg0cLpqg0L36-O+UHMfYuF1nc2%sN)GbRrbhiG!e>$Kms0yxq)< z(P|Djl;^8*j(G1B>>^}lM{#AlGx*x2q?FD|UZ$30{{Ye;+_vbrE++^}o}Q3|@>!S% zU}oDmb0?g8)DSOZb5rh5y;H7alka-Hr@2;5@j1=K zYl_cluVz!cjJ!;pXDOw8p_|glh*zZ8IP0c(Ou0}^^)RnJQ=7ykws?)0(u#wZo&Hmr z%W7mw+k~Q-(pn1nwDA_p$|!(gzO(iA(iMfyp>#Ty+YPA&*P|??{Prz+VJMful?Eje zP3H2rEpQ=i$|d`g89Zhr_}r#gz9XCAok*i`nd->y95?L1Eg>8$#GHa%g582W6icPs z@`c`5A|ZO0^n`9)_)2dJFtO z)Ej7PpJ>?9)iKc!2wgDI(AgcFXoz%7GK5<@Cwg>tZ=Pt_5@{nb#h%~oAqjh8vLYIxgNuvKbSVXdyVX(~5YSF4&N)htA- zRBhE{$yT$lTC1>CTdlSAwF_&tTCECwYL}OMAx_hZW58AexyJlgUthmbI}?`bn%dk7D$qto{utDP%&Df6tA zym*G$E6nW{3Dtq0xr=`uS@@S#3;(5LQJMIMWlIHov}jDtOd}v^uCzd!Eibms zlc;uTdGfbGJi4*1lxVpt>IypD&amC(bolI^pvP+udK^J}H0TOOJ>gK);q(%*r#~19 zxf*?Ld&C>{!!sDL2VIRWyVDnP`I;ONm#;B^4=eGl1J6rz@wpt1#)#W%Z}d6?c2C%W z!QG*ty(!{xyF5Wx#1(cIJp)0n3j^ETjX{^)6L351frzKk?)SO^K2Kw`vC$V2gBvr) zQH(Ioljy7|VKz+qyU=*V;i++Y#W0*$AsBy#qKOkMEXJ?*uyCSF*~3u>B5(wvcE7W+$?o=hntV>5FWlrodPK^b!jYzkD`a=NeI8^b z?6e1hewRHQaQgk;Mn^E@ZZtgIA(z7$bbIUpZ^VHygJFBn84lRJey7(F3iv%9XGBcQ z_g?k2m(@AcX-Gq*h&^|{>sAeO`z7JbD^i=e#0XvEd%4B35AZv<2I`s~4gBW(9J zH3mXakIUz7!UBrHy@9aX;f&fHKA#ie2mSUYEU?`XaR(cNToN8fF{OT=%NY%0@}5SN zk2mbI`-9OY9HKGmkA&TRx5o&-wiLpwlEB~liWwRoPhzF!mMr)p*x2T@mn3{{&9XMG zmrJE@q77SbD4PqXQtkLN&iG4T*Gt!SI<=*0My2C@g%9_plCa0Ks(I;j<3DR)3`1xJmq?(zG?yZ?c!7<|h*Lt78<#;5DV z7JM@gn}ktgcv}{wB;KOw7&C+SYgz7sw`1I^3#~C&^JtIZjam||8PR?vebdr}K_$$o z0IwwM{ihjej_`5dO_~p5JMb258<_A`j=yIKVZ1c{>FAATJlHj0d088ts?fF@(Z+-y znuX8#!lyLzFh8vUuazKwHta6{{1#`@E1?pHAwgcClh@IL -/// PR A.5 — pins the reference-composition logic used to translate AVEVA -/// AlarmRecord events into proto-friendly fields. Transition-kind mapping -/// (a trivial 4-line switch over eAlmTransitions) is verified on -/// the dev rig as part of the live alarm-event smoke test rather than -/// as a unit test, because the AVEVA-licensed enum assembly is -/// Private=false on the reference and is not copied to the test -/// bin directory. +/// Pins the pure helpers used to translate AVEVA's wnwrapConsumer XML +/// payloads into proto-friendly fields. The COM-side I/O on +/// needs an AVEVA install and is +/// covered by the Skip-gated probe (WnWrapConsumerProbeTests); +/// these unit tests cover everything that doesn't touch the live COM +/// surface. /// public sealed class AlarmRecordTransitionMapperTests { - [Fact] public void ComposeFullReference_uses_provider_bang_group_dot_name_format() { @@ -47,4 +47,76 @@ public sealed class AlarmRecordTransitionMapperTests providerName: null, groupName: null, alarmName: "Bare"); Assert.Equal("Bare", reference); } + + [Theory] + [InlineData("UNACK_ALM", MxAlarmStateKind.UnackAlm)] + [InlineData("ACK_ALM", MxAlarmStateKind.AckAlm)] + [InlineData("UNACK_RTN", MxAlarmStateKind.UnackRtn)] + [InlineData("ACK_RTN", MxAlarmStateKind.AckRtn)] + [InlineData("unack_alm", MxAlarmStateKind.UnackAlm)] // case-insensitive + [InlineData(" ACK_ALM ", MxAlarmStateKind.AckAlm)] // trim + [InlineData("UNKNOWN", MxAlarmStateKind.Unspecified)] + [InlineData("", MxAlarmStateKind.Unspecified)] + [InlineData(null, MxAlarmStateKind.Unspecified)] + public void ParseStateKind_decodes_state_strings(string? input, MxAlarmStateKind expected) + { + Assert.Equal(expected, AlarmRecordTransitionMapper.ParseStateKind(input)); + } + + [Theory] + // First sighting: new alarm in *_ALM → Raise. + [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] + [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Raise)] + // First sighting in *_RTN → Clear (unusual; missed the original raise). + [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] + [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Clear)] + // Active → Cleared. + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] + [InlineData(MxAlarmStateKind.AckAlm, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Clear)] + // Cleared → Active (re-trigger). + [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] + [InlineData(MxAlarmStateKind.AckRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] + // Unacked → Acked (operator ack). + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Acknowledge)] + [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Acknowledge)] + // No-op (state unchanged) — caller is supposed to filter these out. + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Unspecified)] + // Current=Unspecified → Unspecified. + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.Unspecified, AlarmTransitionKind.Unspecified)] + public void MapTransition_decides_proto_kind( + MxAlarmStateKind previous, + MxAlarmStateKind current, + AlarmTransitionKind expected) + { + Assert.Equal(expected, AlarmRecordTransitionMapper.MapTransition(previous, current)); + } + + [Fact] + public void ParseTransitionTimestampUtc_assembles_utc_from_xml_fields() + { + // Captured payload from probe (2026-05-01): EDT producer, GMTOFFSET=240, DSTADJUST=0. + // Local 13:26:14.709 + 240 minutes (4h) = 17:26:14.709 UTC. + DateTime utc = AlarmRecordTransitionMapper.ParseTransitionTimestampUtc( + "2026/5/1", "13:26:14.709", gmtOffsetMinutes: 240, dstAdjustMinutes: 0); + + Assert.Equal(DateTimeKind.Utc, utc.Kind); + Assert.Equal(2026, utc.Year); + Assert.Equal(5, utc.Month); + Assert.Equal(1, utc.Day); + Assert.Equal(17, utc.Hour); + Assert.Equal(26, utc.Minute); + Assert.Equal(14, utc.Second); + Assert.Equal(709, utc.Millisecond); + } + + [Fact] + public void ParseTransitionTimestampUtc_returns_min_value_on_unparseable_inputs() + { + Assert.Equal(DateTime.MinValue, + AlarmRecordTransitionMapper.ParseTransitionTimestampUtc(null, null, 0, 0)); + Assert.Equal(DateTime.MinValue, + AlarmRecordTransitionMapper.ParseTransitionTimestampUtc("not a date", "13:00:00", 0, 0)); + Assert.Equal(DateTime.MinValue, + AlarmRecordTransitionMapper.ParseTransitionTimestampUtc("2026/5/1", "not a time", 0, 0)); + } } diff --git a/src/MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs b/src/MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs new file mode 100644 index 0000000..3a3d90d --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using MxGateway.Worker.MxAccess; + +namespace MxGateway.Worker.Tests.MxAccess; + +/// +/// Unit-test coverage for 's pure +/// parsing helpers — XML payload → +/// dictionary, and the 32-char-hex GUID round-trip. The COM-side +/// polling loop is verified separately by the Skip-gated +/// WnWrapConsumerProbeTests on a live AVEVA install. +/// +public sealed class WnWrapAlarmConsumerXmlTests +{ + /// Captured XML from the dev rig (probe run 2026-05-01). + private const string SingleAlarmActiveXml = + "" + + "BCC4705395424D65BDAABCDEA6A32A73" + + "2026/5/1" + + "2400" + + "DESKTOP-6JL3KKO" + + "Galaxy" + + "TestArea" + + "TestMachine_001.TestAlarm001" + + "DSCtruetrue" + + "500UNACK_ALM" + + "" + + "Test alarm #1" + + ""; + + private const string EmptyXml = + ""; + + [Fact] + public void ParseSnapshotXml_returns_empty_dictionary_for_empty_payload() + { + var records = WnWrapAlarmConsumer.ParseSnapshotXml(EmptyXml); + Assert.Empty(records); + } + + [Fact] + public void ParseSnapshotXml_returns_empty_dictionary_for_null_or_whitespace() + { + Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml("")); + Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(" ")); + } + + [Fact] + public void ParseSnapshotXml_decodes_single_active_alarm_record() + { + var records = WnWrapAlarmConsumer.ParseSnapshotXml(SingleAlarmActiveXml); + + Assert.Single(records); + Guid expectedGuid = new Guid("BCC47053-9542-4D65-BDAA-BCDEA6A32A73"); + var record = records[expectedGuid]; + Assert.Equal(expectedGuid, record.AlarmGuid); + Assert.Equal("DESKTOP-6JL3KKO", record.ProviderNode); + Assert.Equal("Galaxy", record.ProviderName); + Assert.Equal("TestArea", record.Group); + Assert.Equal("TestMachine_001.TestAlarm001", record.TagName); + Assert.Equal("DSC", record.Type); + Assert.Equal("true", record.Value); + Assert.Equal("true", record.Limit); + Assert.Equal(500, record.Priority); + Assert.Equal(MxAlarmStateKind.UnackAlm, record.State); + Assert.Equal("Test alarm #1", record.AlarmComment); + Assert.Equal(DateTimeKind.Utc, record.TransitionTimestampUtc.Kind); + // 13:26:14.709 EDT (UTC-4, DSTADJUST=0) + 240 minutes = 17:26:14.709 UTC. + Assert.Equal(17, record.TransitionTimestampUtc.Hour); + Assert.Equal(26, record.TransitionTimestampUtc.Minute); + } + + [Fact] + public void ParseSnapshotXml_silently_drops_records_with_invalid_guids() + { + string xml = SingleAlarmActiveXml.Replace( + "BCC4705395424D65BDAABCDEA6A32A73", + "not-a-guid"); + Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(xml)); + } + + [Theory] + [InlineData("BCC4705395424D65BDAABCDEA6A32A73", "BCC47053-9542-4D65-BDAA-BCDEA6A32A73")] + [InlineData("00000000000000000000000000000000", "00000000-0000-0000-0000-000000000000")] + public void TryParseHexGuid_handles_dashless_32_char_hex(string hex, string expected) + { + Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid)); + Assert.Equal(new Guid(expected), guid); + } + + [Theory] + [InlineData("BCC47053-9542-4D65-BDAA-BCDEA6A32A73")] + public void TryParseHexGuid_accepts_canonical_dashed_form(string canonical) + { + Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(canonical, out Guid guid)); + Assert.Equal(new Guid(canonical), guid); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("nope")] + [InlineData("0123456789ABCDEF")] // too short + [InlineData("BCC4705395424D65BDAABCDEA6A32A73XX")] // too long + public void TryParseHexGuid_rejects_invalid_input(string? hex) + { + Assert.False(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid)); + Assert.Equal(Guid.Empty, guid); + } +} diff --git a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj index a3fdd4e..55b9608 100644 --- a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj +++ b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj @@ -41,6 +41,12 @@ true false + + ..\..\lib\Interop.WNWRAPCONSUMERLib.dll + true + false + false + diff --git a/src/MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs b/src/MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs new file mode 100644 index 0000000..3840e7d --- /dev/null +++ b/src/MxGateway.Worker.Tests/WnWrapConsumerProbeTests.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using WNWRAPCONSUMERLib; +using Xunit.Abstractions; + +namespace MxGateway.Worker.Tests; + +/// +/// Runtime probe — instantiate AVEVA's standalone wnwrapConsumer COM +/// class (CLSID 7AB52E5F-36B2-4A30-AE46-952A746F667C, registered at +/// C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll), +/// subscribe to the dev rig's `\\<machine>\Galaxy!DEV` provider, and +/// poll GetXmlCurrentAlarms2 while a System Platform script flips +/// TestMachine_001.TestAlarm001 every 10s. The XML payload bypasses +/// the FILETIME→DateTime auto-marshaling that crashes +/// aaAlarmManagedClient.AlarmClient.GetHighPriAlarm. +/// +/// Skip-gated; flip Skip=null to run on the dev rig. +/// +public sealed class WnWrapConsumerProbeTests +{ + private static readonly string MachineName = Environment.MachineName; + private static readonly string SubscriptionExpression = + $@"\\{MachineName}\Galaxy!DEV"; + + // XML query form — per WIN-911 / ArchestrA reference. NODE is the + // machine, PROVIDER is the literal "Galaxy", GROUP is the area. + private static readonly string XmlAlarmQuery = + "" + + "" + + $"{Environment.MachineName}" + + "Galaxy" + + "DEV" + + "" + + ""; + + private const int MaxAlarmsPerFetch = 100; + private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(30); + private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500); + + private readonly ITestOutputHelper output; + private readonly ConcurrentQueue log = new ConcurrentQueue(); + private readonly Stopwatch elapsed = Stopwatch.StartNew(); + + public WnWrapConsumerProbeTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture wnwrapConsumer XML alarm output. Verified working 2026-05-01.")] + public void ProbeWnWrapConsumer() + { + Exception? threadException = null; + var done = new ManualResetEventSlim(false); + var thread = new Thread(() => + { + try { RunProbe(); } + catch (Exception ex) { threadException = ex; } + finally { done.Set(); } + }); + thread.IsBackground = false; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + done.Wait(); + thread.Join(); + + output.WriteLine($"Captured {log.Count} log line(s):"); + while (log.TryDequeue(out string? line)) + { + output.WriteLine(line); + } + + if (threadException != null) + { + throw threadException; + } + } + + private void RunProbe() + { + wwAlarmConsumerClass? client = null; + try + { + Log("Creating wwAlarmConsumerClass via CoCreateInstance..."); + client = new wwAlarmConsumerClass(); + Log($"Instantiated. RuntimeType={client.GetType().FullName}"); + + // Lifecycle: per AlarmClientDiscovery.md finding, InitializeConsumer + // MUST precede RegisterConsumer for the alarm provider to become + // visible. The wnwrap surface mirrors that requirement. + try + { + int init = client.InitializeConsumer("MxGatewayProbe.WnWrap"); + Log($"InitializeConsumer -> {init}"); + } + catch (Exception ex) + { + Log($"InitializeConsumer threw: {ex.GetType().Name}: {ex.Message}"); + } + + try + { + // hWnd=0 — XML pull-based; no message pump needed. + int reg = client.RegisterConsumer( + hWnd: 0, + szProductName: "MxGatewayProbe", + szApplicationName: "MxGatewayProbe.WnWrap", + szVersion: "1.0"); + Log($"RegisterConsumer(hWnd=0) -> {reg}"); + } + catch (Exception ex) + { + Log($"RegisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); + } + + // Try both subscription mechanisms: classic Subscribe (canonical + // scope from prior aaAlarmManagedClient probe), and + // SetXmlAlarmQuery (the wnwrap-native filter format). + try + { + int sub = client.Subscribe( + szSubscription: SubscriptionExpression, + wFromPri: 1, + wToPri: 999, + QueryType: eQueryType.qtSummary, + SortFlags: eSortFlags.sfReturnNewestFirst, + FilterMask: eAlarmFilterState.asAlarmActiveNow, + FilterSpecification: eAlarmFilterState.asAlarmActiveNow); + Log($"Subscribe('{SubscriptionExpression}') -> {sub}"); + } + catch (Exception ex) + { + Log($"Subscribe threw: {ex.GetType().Name}: {ex.Message}"); + } + + try + { + Log($"SetXmlAlarmQuery payload: {XmlAlarmQuery}"); + client.SetXmlAlarmQuery(XmlAlarmQuery); + Log("SetXmlAlarmQuery -> ok"); + } + catch (Exception ex) + { + Log($"SetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}"); + } + + // Echo the query back so we can confirm what the consumer is + // actually filtering on (provider may rewrite or reject some + // attributes silently). + try + { + object echo = string.Empty; + client.GetXmlAlarmQuery(out echo); + Log($"GetXmlAlarmQuery (round-trip) -> {Truncate(echo?.ToString() ?? "", 600)}"); + } + catch (Exception ex) + { + Log($"GetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}"); + } + + // Pump phase: poll GetXmlCurrentAlarms2 every PollInterval; log on + // every change in payload. Run for PumpDuration. The user's flip + // script writes TestMachine_001.TestAlarm001 every 10s; expect at + // least 2-3 transitions over a 30s window. + Log($"Polling GetXmlCurrentAlarms2 every {PollInterval.TotalMilliseconds:F0}ms for {PumpDuration.TotalSeconds:F0}s."); + DateTime deadline = DateTime.UtcNow + PumpDuration; + DateTime nextPoll = DateTime.UtcNow; + int pollCount = 0; + string lastV2 = string.Empty; + string lastV1 = string.Empty; + int v2Ok = 0, v2Throw = 0, v1Ok = 0, v1Throw = 0; + int statsOk = 0, statsThrow = 0; + string lastStats = string.Empty; + while (DateTime.UtcNow < deadline) + { + if (DateTime.UtcNow >= nextPoll) + { + pollCount++; + + // V2 channel. + try + { + object xml2 = string.Empty; + client.GetXmlCurrentAlarms2(MaxAlarmsPerFetch, out xml2); + v2Ok++; + string s = xml2?.ToString() ?? ""; + if (s != lastV2) + { + Log($"GetXmlCurrentAlarms2 #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}"); + lastV2 = s; + } + } + catch (Exception ex) + { + v2Throw++; + string es = $"{ex.GetType().Name}: {ex.Message}"; + if (es != lastV2) + { + Log($"GetXmlCurrentAlarms2 #{pollCount} threw: {es}"); + lastV2 = es; + } + } + + // V1 channel — different vtable slot; either may be the + // populated one in this AVEVA build. + try + { + object xml1 = string.Empty; + client.GetXmlCurrentAlarms(MaxAlarmsPerFetch, out xml1); + v1Ok++; + string s = xml1?.ToString() ?? ""; + if (s != lastV1) + { + Log($"GetXmlCurrentAlarms #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}"); + lastV1 = s; + } + } + catch (Exception ex) + { + v1Throw++; + string es = $"{ex.GetType().Name}: {ex.Message}"; + if (es != lastV1) + { + Log($"GetXmlCurrentAlarms #{pollCount} threw: {es}"); + lastV1 = es; + } + } + + // Stats channel — heartbeat + active-count even if the XML + // calls are dry, this surfaces whether wnwrap sees any + // alarms in the subscribed scope at all. + try + { + int pct, total, active, newAlms, changes; + client.GetStatistics( + out pct, out total, out active, out newAlms, out changes, + IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + statsOk++; + string statsSummary = $"pct={pct} total={total} active={active} new={newAlms} changes={changes}"; + if (statsSummary != lastStats) + { + Log($"GetStatistics #{pollCount} (CHANGED): {statsSummary}"); + lastStats = statsSummary; + } + } + catch (Exception ex) + { + statsThrow++; + Log($"GetStatistics #{pollCount} threw: {ex.GetType().Name}: {ex.Message}"); + } + + nextPoll = DateTime.UtcNow + PollInterval; + } + Thread.Sleep(20); + } + Log($"Pump done. Tally: v2 ok={v2Ok} threw={v2Throw}, v1 ok={v1Ok} threw={v1Throw}, stats ok={statsOk} threw={statsThrow}"); + + try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); } + catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); } + + try { int uninit = client.UninitializeConsumer(); Log($"UninitializeConsumer -> {uninit}"); } + catch (Exception ex) { Log($"UninitializeConsumer threw: {ex.GetType().Name}: {ex.Message}"); } + } + finally + { + if (client != null && Marshal.IsComObject(client)) + { + try { Marshal.FinalReleaseComObject(client); } catch { /* swallow */ } + } + } + } + + private void Log(string line) + { + log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}"); + } + + private static string Truncate(string s, int max) + { + if (string.IsNullOrEmpty(s) || s.Length <= max) return s ?? string.Empty; + return s.Substring(0, max) + $"…[+{s.Length - max} chars]"; + } +} diff --git a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs b/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs deleted file mode 100644 index ae878fa..0000000 --- a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using AlarmMgrDataProviderCOM; -using aaAlarmManagedClient; - -namespace MxGateway.Worker.MxAccess; - -/// -/// PR A.5 — production backed by -/// aaAlarmManagedClient.AlarmClient. Forwards -/// GetAlarmChangesCompleted events into the worker's event queue -/// via . -/// -/// -/// -/// ⚠ Architecture finding (2026-05-01 reflection probe — -/// see docs/AlarmClientDiscovery.md): contrary to the -/// original PR A.5 design, aaAlarmManagedClient.AlarmClient -/// exposes zero public events on the deployed assembly -/// (aaAlarmManagedClient.dll v1.0.7368.41290). There is no -/// managed event surface. RegisterConsumer(hWnd, …) takes a -/// window handle because the actual notification mechanism is -/// WM_APP-pump messaging — AVEVA's alarm provider WM_APP-pokes the -/// registered window, and the consumer pulls the change set via -/// GetStatistics + GetAlarmExtendedRec on each poke. -/// -/// -/// As a result, has no production -/// caller — is invoked only -/// from tests. currently calls -/// RegisterConsumer(hWnd: 0, …) + Subscribe(…) and -/// returns OK, but no notifications will arrive at runtime because -/// no window is attached. Until A.2's WM_APP pump lands, the -/// gateway's MX_EVENT_FAMILY_ON_ALARM_TRANSITION family -/// cannot carry any events. -/// -/// -/// as written is still a load-bearing call -/// the WM_APP path needs (it tells the alarm provider which -/// subscription expression to scope notifications to). The wiring -/// that has to change is the notification-receive side: replace the -/// hWnd: 0 default with a real hidden message-only window -/// hWnd owned by the worker's STA, and add a WindowProc that -/// routes the AVEVA WM_APP message into a change-pull path that -/// ultimately invokes . -/// and -/// are pull-style and don't depend on the event surface — they're -/// correct as is. -/// -/// -public sealed class AlarmClientConsumer : IMxAccessAlarmConsumer -{ - private const string DefaultProductName = "OtOpcUa.MxGateway"; - private const string DefaultApplicationName = "OtOpcUa.MxGateway.Worker"; - private const string DefaultVersion = "1.0"; - - private readonly AlarmClient client; - private readonly object subscribeLock = new object(); - private bool disposed; - - public AlarmClientConsumer() - : this(new AlarmClient()) - { - } - - /// Test seam — inject a pre-created . - internal AlarmClientConsumer(AlarmClient client) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - } - - /// - public event EventHandler? AlarmRecordReceived; - - /// - public void Subscribe(string subscription) - { - if (subscription is null) throw new ArgumentNullException(nameof(subscription)); - if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); - - lock (subscribeLock) - { - // hWnd=0: placeholder — the AVEVA alarm provider notifies via - // WM_APP messages, not via a managed-client event surface (see - // docs/AlarmClientDiscovery.md, 2026-05-01 reflection probe). - // RegisterConsumer accepts hWnd=0 but no notifications will reach - // this consumer until the WM_APP pump lands as part of A.2 and - // a real message-only window's handle is supplied here. - int registerResult = client.RegisterConsumer( - hWnd: 0, - szProductName: DefaultProductName, - szApplicationName: DefaultApplicationName, - szVersion: DefaultVersion, - bRetainHiddenAlarms: false); - if (registerResult != 0) - { - throw new InvalidOperationException( - $"AlarmClient.RegisterConsumer returned non-zero status {registerResult}."); - } - - int subscribeResult = client.Subscribe( - szSubscription: subscription, - wFromPri: 1, - wToPri: 999, - QueryType: eQueryType.qtSummary, - SortFlags: eSortFlags.sfReturnNewestFirst, - FilterMask: eAlarmFilterState.asNone, - FilterSpecification: eAlarmFilterState.asNone); - if (subscribeResult != 0) - { - throw new InvalidOperationException( - $"AlarmClient.Subscribe('{subscription}') returned non-zero status {subscribeResult}."); - } - } - } - - /// - public int AcknowledgeByGuid( - Guid alarmGuid, - string ackComment, - string ackOperatorName, - string ackOperatorNode, - string ackOperatorDomain, - string ackOperatorFullName) - { - if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); - return client.AlarmAckByGUID( - alarmGuid, - ackComment ?? string.Empty, - ackOperatorName ?? string.Empty, - ackOperatorNode ?? string.Empty, - ackOperatorDomain ?? string.Empty, - ackOperatorFullName ?? string.Empty); - } - - /// - public IReadOnlyList SnapshotActiveAlarms() - { - if (disposed) throw new ObjectDisposedException(nameof(AlarmClientConsumer)); - - // Walk the alarm-client's view of currently-active alarms via - // GetStatistics + GetAlarmExtendedRec. The exact iteration semantics - // (whether ChangePos points at the active set or at the recently- - // changed set) need dev-rig validation; this method is a stub-grade - // walker that reports the count it found. - int percent = 0, total = 0, active = 0, suppressed = 0; - int suppressedFilters = 0, newAlarms = 0, changes = 0; - int[] codes = Array.Empty(); - int[] positions = Array.Empty(); - int[] handles = Array.Empty(); - int statsResult = client.GetStatistics( - ref percent, ref total, ref active, ref suppressed, - ref suppressedFilters, ref newAlarms, ref changes, - ref codes, ref positions, ref handles); - if (statsResult != 0 || positions == null) - { - return Array.Empty(); - } - - List records = new List(positions.Length); - foreach (int pos in positions) - { - AlarmRecord record = new AlarmRecord(); - int recResult = client.GetAlarmExtendedRec(pos, ref record); - if (recResult == 0) - { - records.Add(record); - } - } - return records; - } - - /// - /// Forward an alarm record to subscribers. Exposed internal so the - /// dev-rig hookup that wires the AVEVA alarm-changes callback can - /// route into the same event-fan-out path tests use. - /// - internal void RaiseAlarmRecordReceived(AlarmRecord record) - { - AlarmRecordReceived?.Invoke(this, record); - } - - /// - public void Dispose() - { - if (disposed) return; - disposed = true; - try { client.DeregisterConsumer(); } catch { } - try { client.Dispose(); } catch { } - } -} diff --git a/src/MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs b/src/MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs index 0aa3cba..4865828 100644 --- a/src/MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs +++ b/src/MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs @@ -1,46 +1,77 @@ using System; -using AlarmMgrDataProviderCOM; using MxGateway.Contracts.Proto; namespace MxGateway.Worker.MxAccess; /// -/// PR A.5 — translation helpers between AVEVA's -/// enum and the proto's -/// , plus alarm-reference composition. +/// Translation helpers between the wnwrapConsumer XML payload and the +/// proto-friendly wire format, plus +/// alarm-reference composition. /// /// /// -/// The full → proto-fields decoder lives -/// in . The two pieces that don't -/// need hardware validation (transition-kind enum mapping + -/// provider/group/name → reference string format) live here so the -/// consumer's hot-path stays focused on COM-side field access. +/// These mappings stay pure and library-agnostic so they're unit +/// testable without an AVEVA install. The COM-side I/O lives on +/// . /// /// public static class AlarmRecordTransitionMapper { /// - /// Maps the AVEVA enum onto the proto's - /// . Transitions outside the four - /// primary kinds (raise/ack/clear/retrigger) collapse to - /// so the EventPump's - /// decoding-failure counter records them. + /// Decode AVEVA's STATE string (one of UNACK_ALM, ACK_ALM, + /// UNACK_RTN, ACK_RTN) into the worker's library-agnostic + /// . Unknown values map to + /// . /// - public static AlarmTransitionKind MapTransitionKind(eAlmTransitions native) + public static MxAlarmStateKind ParseStateKind(string? stateXml) { - // ALM = active-raise, RTN = return-to-normal/clear, ACK = acknowledge. - // SUB / ENB / DIS / SUP / REL / REMOVE — substitute / enable / disable / - // suppress / release / remove. None of those map to OPC UA Part 9 - // transitions today; future work could add a Substituted / Suppressed - // proto kind if a customer needs it. - switch (native) + if (string.IsNullOrWhiteSpace(stateXml)) return MxAlarmStateKind.Unspecified; + return stateXml!.Trim().ToUpperInvariant() switch { - case eAlmTransitions.almRec_trans_ALM: return AlarmTransitionKind.Raise; - case eAlmTransitions.almRec_trans_ACK: return AlarmTransitionKind.Acknowledge; - case eAlmTransitions.almRec_trans_RTN: return AlarmTransitionKind.Clear; - default: return AlarmTransitionKind.Unspecified; + "UNACK_ALM" => MxAlarmStateKind.UnackAlm, + "ACK_ALM" => MxAlarmStateKind.AckAlm, + "UNACK_RTN" => MxAlarmStateKind.UnackRtn, + "ACK_RTN" => MxAlarmStateKind.AckRtn, + _ => MxAlarmStateKind.Unspecified, + }; + } + + /// + /// Decide which proto transition kind a state change represents. + /// The decision table: + /// + /// previous=Unspecified + current=*Alm → Raise (new alarm). + /// previous=Unspecified + current=*Rtn → Clear (alarm appeared in cleared state — rare; missed the raise). + /// previous=Unack* + current=Ack* → Acknowledge. + /// previous=*Alm + current=*Rtn → Clear. + /// previous=*Rtn + current=*Alm → Raise (re-trigger after clear). + /// Anything else → Unspecified (no proto kind to emit). + /// + /// + public static AlarmTransitionKind MapTransition( + MxAlarmStateKind previous, + MxAlarmStateKind current) + { + if (current == MxAlarmStateKind.Unspecified) return AlarmTransitionKind.Unspecified; + + bool currentIsAlm = current is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm; + bool currentIsRtn = current is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn; + bool currentIsAcked = current is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn; + + if (previous == MxAlarmStateKind.Unspecified) + { + return currentIsAlm ? AlarmTransitionKind.Raise : AlarmTransitionKind.Clear; } + + bool previousIsAlm = previous is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm; + bool previousIsRtn = previous is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn; + bool previousIsAcked = previous is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn; + + if (previousIsAlm && currentIsRtn) return AlarmTransitionKind.Clear; + if (previousIsRtn && currentIsAlm) return AlarmTransitionKind.Raise; + if (!previousIsAcked && currentIsAcked) return AlarmTransitionKind.Acknowledge; + + return AlarmTransitionKind.Unspecified; } /// @@ -63,4 +94,90 @@ public static class AlarmRecordTransitionMapper ? $"{provider}!{name}" : $"{provider}!{group}.{name}"; } + + /// + /// Reassemble a UTC from the wnwrap XML's + /// DATE + TIME + GMTOFFSET + DSTADJUST + /// fields. Returns when DATE / TIME + /// can't be parsed (best-effort — failure is non-fatal; the proto + /// will carry the epoch and the EventQueue's fault counter records + /// the parse miss). + /// + /// e.g. "2026/5/1" (no zero-padding). + /// e.g. "13:26:14.709". + /// Offset of the producer's local time vs UTC, in minutes. + /// DST adjustment already applied to local time, in minutes. + public static DateTime ParseTransitionTimestampUtc( + string? xmlDate, + string? xmlTime, + int gmtOffsetMinutes, + int dstAdjustMinutes) + { + if (string.IsNullOrWhiteSpace(xmlDate) || string.IsNullOrWhiteSpace(xmlTime)) + { + return DateTime.MinValue; + } + + // Parse DATE: yyyy/M/d (no zero padding observed). Use ParseExact with + // multiple format candidates — AVEVA's locale may format differently + // on non-en-US hosts. + string[] dateFormats = + { + "yyyy/M/d", "yyyy/MM/dd", "M/d/yyyy", "MM/dd/yyyy", + "d/M/yyyy", "dd/MM/yyyy", + }; + string dateTrim = xmlDate!.Trim(); + if (!DateTime.TryParseExact( + dateTrim, + dateFormats, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, + out DateTime date)) + { + if (!DateTime.TryParse( + dateTrim, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, + out date)) + { + return DateTime.MinValue; + } + } + + // Parse TIME: H:m:s.fff (variable precision). + string[] timeFormats = + { + "H:m:s.fff", "H:m:s.ff", "H:m:s.f", "H:m:s", + "HH:mm:ss.fff", "HH:mm:ss.ff", "HH:mm:ss.f", "HH:mm:ss", + }; + string timeTrim = xmlTime!.Trim(); + if (!DateTime.TryParseExact( + timeTrim, + timeFormats, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, + out DateTime time)) + { + if (!DateTime.TryParse( + timeTrim, + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, + out time)) + { + return DateTime.MinValue; + } + } + + DateTime localProducerTime = new DateTime( + date.Year, date.Month, date.Day, + time.Hour, time.Minute, time.Second, time.Millisecond, + DateTimeKind.Unspecified); + + // GMTOFFSET = minutes east of UTC (or behind, depending on convention). + // The wnwrap convention observed: GMTOFFSET=240, DSTADJUST=0 for + // EDT (UTC-4) — so the field is "minutes from local to UTC". To get + // UTC, ADD the offset. + DateTime utc = localProducerTime.AddMinutes(gmtOffsetMinutes - dstAdjustMinutes); + return DateTime.SpecifyKind(utc, DateTimeKind.Utc); + } } diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs index bb7f116..b7ae357 100644 --- a/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs +++ b/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs @@ -1,40 +1,57 @@ using System; -using AlarmMgrDataProviderCOM; +using System.Collections.Generic; namespace MxGateway.Worker.MxAccess; /// -/// PR A.5 — abstraction over aaAlarmManagedClient.AlarmClient's -/// subscribe / event-receive surface. The production implementation -/// () wraps the AVEVA managed client; -/// tests substitute a fake to exercise the wiring against canned -/// events without a live Galaxy. +/// Abstraction over an AVEVA alarm-consumer COM library. The production +/// implementation () wraps +/// WNWRAPCONSUMERLib.wwAlarmConsumerClass from +/// C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll; +/// tests substitute a fake to drive transition events without a live +/// Galaxy. /// +/// +/// +/// The receive surface is poll-based: the production consumer +/// periodically calls GetXmlCurrentAlarms2, parses the +/// returned XML payload, diffs against the previous snapshot keyed +/// by alarm GUID, and raises +/// once per state change. This bypasses the FILETIME marshaling +/// crash in aaAlarmManagedClient.AlarmClient.GetHighPriAlarm +/// (see docs/AlarmClientDiscovery.md) — XML strings carry +/// timestamps as ASCII fields, no DateTime auto-conversion happens +/// on the .NET interop boundary. +/// +/// public interface IMxAccessAlarmConsumer : IDisposable { /// - /// Fires once per alarm record the AVEVA alarm provider emits. The - /// subscriber is expected to forward each record to a transition mapper - /// and then onto the worker's event queue. Fired on the alarm-client's - /// internal callback thread; subscribers that need STA affinity must - /// marshal back themselves. + /// Fires once per detected alarm-state transition (raise, acknowledge, + /// clear, or new-alarm-already-acked-on-arrival). Subscribers are + /// expected to translate the record into the proto family + /// OnAlarmTransition and enqueue it. Fired on the consumer's + /// polling thread (the worker's STA in production); subscribers that + /// need a different thread must marshal back themselves. /// - event EventHandler? AlarmRecordReceived; + event EventHandler? AlarmTransitionEmitted; /// - /// Initializes the AVEVA alarm-client connection and subscribes to the - /// supplied alarm-provider expression. Subscription string follows - /// AVEVA's syntax (e.g. "\Galaxy!OperationsRoom.AlarmGroup" or - /// "\\GR1\Galaxy!" for a whole Galaxy). + /// Initializes the AVEVA alarm-client connection, registers as a + /// consumer, and subscribes to the supplied alarm-provider expression. + /// Subscription string follows AVEVA's canonical format: + /// \\<node>\Galaxy!<area>. The literal "Galaxy" is + /// the provider name (regardless of the configured Galaxy database + /// name). Calling Subscribe also begins polling on the consumer's + /// internal timer. /// void Subscribe(string subscription); /// /// Acknowledges a single alarm with full operator-identity fidelity. - /// Reaches the AVEVA alarm provider's native ack API - /// (AlarmAckByGUID); operator user / node / domain / full-name - /// and the comment land atomically with the ack transition in the - /// alarm-history log. + /// Reaches AVEVA's native AlarmAckByGUID; operator + /// user / node / domain / full-name and the comment land atomically + /// with the ack transition in the alarm-history log. /// int AcknowledgeByGuid( Guid alarmGuid, @@ -45,10 +62,10 @@ public interface IMxAccessAlarmConsumer : IDisposable string ackOperatorFullName); /// - /// Walks the currently-active alarm set and yields each as an - /// . Used by the gateway's QueryActiveAlarms - /// (PR A.7) ConditionRefresh path — operator clients call this after - /// reconnect to seed local Part 9 state. + /// Returns the consumer's most recently parsed snapshot of currently + /// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7) + /// ConditionRefresh path — operator clients call this after reconnect + /// to seed local Part 9 state. /// - System.Collections.Generic.IReadOnlyList SnapshotActiveAlarms(); + IReadOnlyList SnapshotActiveAlarms(); } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs index 450067a..59bf61e 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs @@ -4,70 +4,21 @@ using MxGateway.Contracts.Proto; namespace MxGateway.Worker.MxAccess; /// -/// PR A.2 sink for native MxAccess alarm transitions. Bridges the -/// aaAlarmManagedClient.AlarmClient consumer to the worker's -/// event queue, producing messages -/// via . +/// Sink for native MxAccess alarm transitions. Bridges +/// to the worker's event queue, +/// producing messages via +/// . /// /// /// -/// Architecture (revised 2026-05-01 — see -/// docs/AlarmClientDiscovery.md): the worker hosts -/// aaAlarmManagedClient.AlarmClient alongside the existing -/// ArchestrA.MxAccess COM consumer. Both are x86 .NET -/// Framework 4.8. The MxAccess COM Toolkit at -/// C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll -/// exposes no alarm events; the alarm provider lives in a separate -/// AVEVA service that aaAlarmManagedClient subscribes to. -/// -/// -/// Notification mechanism: WM_APP pump. A reflection -/// probe of aaAlarmManagedClient.dll (v1.0.7368.41290) on -/// 2026-05-01 confirmed the public AlarmClient 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 -/// RegisterConsumer(hWnd, …); the consumer pulls the change -/// set via GetStatistics + GetAlarmExtendedRec on each -/// poke. PR A.5's still owns the -/// handle and the -/// / -/// pull-style calls; only -/// the receive path is wrong. -/// -/// -/// Discovered API surface (see -/// AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface in -/// MxGateway.Worker.Tests — Skip-gated reflection probe; full -/// output captured in docs/AlarmClientDiscovery.md): -/// -/// -/// RegisterConsumer(hWnd, productName, applicationName, version, retainHidden) — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change. -/// Subscribe(provider, fromPri, toPri, queryType, sortFlags, filterMask, filterSpec) — subscribes to a Galaxy alarm provider with priority + filter scoping. -/// GetStatistics(out percentQuery, totalAlarms, activeAlarms, …, out int[] changeCodes, out int[] changePos, out int[] hAlarm) — called on each WM_APP poke; enumerates which alarms changed. -/// GetAlarmExtendedRec(index, out AlarmRecord) — pulls the full alarm record (operator, comment, original raise, category, severity). -/// AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName) — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition. -/// -/// -/// Open questions before A.2 implementation -/// (see docs/AlarmClientDiscovery.md "Implications for A.2"): -/// -/// -/// WM_APP message ID — not in the public surface, needs AVEVA C++ Toolkit reference or a runtime probe. -/// wParam / lParam semantics — likely none (the pattern is "got poked → pull state via GetStatistics"), but confirm during the probe. -/// STA / threading affinity for the message-only window — likely the worker's existing STA, but if AVEVA assumes UI-thread inside GetStatistics the alarm path may need its own STA. -/// Subscription scope — reuse the configured Galaxy name from the data session. -/// -/// -/// Until A.2 lands a hidden message-only window + WindowProc that -/// routes WM_APP into , -/// is a no-op. The worker continues to function -/// for data subscriptions, and the gateway's -/// family is reserved -/// on the wire but never emitted. lmxopcua-side -/// AlarmConditionService 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 +/// event +/// to at session attach time. The +/// 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 +/// docs/AlarmClientDiscovery.md "Option A — captured". /// /// public sealed class MxAccessAlarmEventSink : IMxAccessEventSink diff --git a/src/MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs b/src/MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs new file mode 100644 index 0000000..23c1816 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs @@ -0,0 +1,59 @@ +using System; + +namespace MxGateway.Worker.MxAccess; + +/// +/// Library-agnostic alarm-state enum. Mirrors the four STATE +/// values returned by AVEVA's WNWRAPCONSUMERLib XML payload — +/// UNACK_ALM, ACK_ALM, UNACK_RTN, ACK_RTN. +/// Decoupling the consumer from any specific COM library keeps the +/// proto-build path testable without an AVEVA install. +/// +public enum MxAlarmStateKind +{ + Unspecified = 0, + UnackAlm = 1, + AckAlm = 2, + UnackRtn = 3, + AckRtn = 4, +} + +/// +/// Single alarm record as emitted by the wnwrapConsumer XML stream. +/// Field names match the captured XML schema (see +/// docs/AlarmClientDiscovery.md "Option A — captured" section). +/// +public sealed class MxAlarmSnapshotRecord +{ + public Guid AlarmGuid { get; set; } + public DateTime TransitionTimestampUtc { get; set; } + public string ProviderNode { get; set; } = string.Empty; + public string ProviderName { get; set; } = string.Empty; + public string Group { get; set; } = string.Empty; + public string TagName { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Limit { get; set; } = string.Empty; + public int Priority { get; set; } + public MxAlarmStateKind State { get; set; } + public string OperatorNode { get; set; } = string.Empty; + public string OperatorName { get; set; } = string.Empty; + public string AlarmComment { get; set; } = string.Empty; +} + +/// +/// One transition emitted by the consumer's snapshot diff. Pairs the +/// latest record with its previous state so the proto layer can decide +/// whether the transition is a Raise / Acknowledge / Clear. +/// +public sealed class MxAlarmTransitionEvent : EventArgs +{ + public MxAlarmSnapshotRecord Record { get; set; } = new MxAlarmSnapshotRecord(); + + /// + /// The state on the consumer's previous polled snapshot, or + /// when this is the + /// first time the GUID has been observed. + /// + public MxAlarmStateKind PreviousState { get; set; } +} diff --git a/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs new file mode 100644 index 0000000..04421af --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Threading; +using System.Xml; +using WNWRAPCONSUMERLib; + +namespace MxGateway.Worker.MxAccess; + +/// +/// Production backed by AVEVA's +/// standalone WNWRAPCONSUMERLib.wwAlarmConsumerClass COM object +/// (CLSID {7AB52E5F-36B2-4A30-AE46-952A746F667C}, hosted by +/// C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll). +/// +/// +/// +/// Replaces the earlier AlarmClientConsumer built on +/// aaAlarmManagedClient.AlarmClient, which crashed in +/// GetHighPriAlarm with ArgumentOutOfRangeException +/// (FILETIME→DateTime auto-marshaling on AVEVA's sentinel timestamps). +/// The wnwrap surface returns the alarm record as a BSTR XML string +/// via GetXmlCurrentAlarms2; timestamps arrive as ASCII +/// DATE + TIME + GMTOFFSET + DSTADJUST +/// fields and never touch the .NET DateTime marshaler. See +/// docs/AlarmClientDiscovery.md "Option A — captured" for +/// the discovery and the captured payload schema. +/// +/// +/// Threading. The wnwrap CLSID is registered with +/// ThreadingModel=Apartment. The consumer must be created +/// and operated from an STA thread; the worker's +/// already runs an STA pump that +/// is the natural host. Polling cadence is governed by +/// on a dedicated timer the +/// consumer owns; in production the worker's STA dispatcher should +/// marshal each callback onto the STA thread before invoking +/// GetXmlCurrentAlarms2. For now (test-grade), this consumer +/// calls the COM API on whichever thread the timer fires it on — +/// the worker bootstrap will gain a thin "run-on-STA" wrapper as +/// part of A.3 dispatcher wiring. +/// +/// +public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer +{ + private const string DefaultProductName = "OtOpcUa.MxGateway"; + private const string DefaultApplicationName = "OtOpcUa.MxGateway.Worker"; + private const string DefaultVersion = "1.0"; + private const int DefaultPollIntervalMilliseconds = 500; + private const int DefaultMaxAlarmsPerFetch = 1024; + + private readonly object syncRoot = new object(); + private readonly Dictionary latestSnapshot = + new Dictionary(); + private readonly int pollIntervalMs; + private readonly int maxAlarmsPerFetch; + + private wwAlarmConsumerClass? client; + private Timer? pollTimer; + private bool subscribed; + private bool disposed; + + public WnWrapAlarmConsumer() + : this(new wwAlarmConsumerClass(), DefaultPollIntervalMilliseconds, DefaultMaxAlarmsPerFetch) + { + } + + /// Test seam — inject a pre-created COM client and tune the poll cadence. + internal WnWrapAlarmConsumer( + wwAlarmConsumerClass client, + int pollIntervalMilliseconds, + int maxAlarmsPerFetch) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.pollIntervalMs = pollIntervalMilliseconds > 0 + ? pollIntervalMilliseconds + : DefaultPollIntervalMilliseconds; + this.maxAlarmsPerFetch = maxAlarmsPerFetch > 0 + ? maxAlarmsPerFetch + : DefaultMaxAlarmsPerFetch; + } + + /// + public event EventHandler? AlarmTransitionEmitted; + + public int PollIntervalMilliseconds => pollIntervalMs; + + /// + public void Subscribe(string subscription) + { + if (subscription is null) throw new ArgumentNullException(nameof(subscription)); + if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + lock (syncRoot) + { + if (subscribed) + { + throw new InvalidOperationException( + "WnWrapAlarmConsumer.Subscribe was called more than once; " + + "wwAlarmConsumerClass.Subscribe replaces the previous filter and is not idempotent."); + } + + wwAlarmConsumerClass com = client + ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + // Per AlarmClientDiscovery.md: InitializeConsumer MUST precede + // RegisterConsumer for the alarm provider chain to become visible. + int init = com.InitializeConsumer(DefaultApplicationName); + if (init != 0) + { + throw new InvalidOperationException( + $"wwAlarmConsumer.InitializeConsumer returned non-zero status {init}."); + } + + // hWnd=0: wnwrap supports a pull-based model — no message pump + // is required. We poll GetXmlCurrentAlarms2 on a timer below. + int reg = com.RegisterConsumer( + hWnd: 0, + szProductName: DefaultProductName, + szApplicationName: DefaultApplicationName, + szVersion: DefaultVersion); + if (reg != 0) + { + throw new InvalidOperationException( + $"wwAlarmConsumer.RegisterConsumer returned non-zero status {reg}."); + } + + int sub = com.Subscribe( + szSubscription: subscription, + wFromPri: 1, + wToPri: 999, + QueryType: eQueryType.qtSummary, + SortFlags: eSortFlags.sfReturnNewestFirst, + FilterMask: eAlarmFilterState.asAlarmActiveNow, + FilterSpecification: eAlarmFilterState.asAlarmActiveNow); + if (sub != 0) + { + throw new InvalidOperationException( + $"wwAlarmConsumer.Subscribe('{subscription}') returned non-zero status {sub}."); + } + + subscribed = true; + pollTimer = new Timer(OnPoll, state: null, dueTime: 0, period: pollIntervalMs); + } + } + + /// + public int AcknowledgeByGuid( + Guid alarmGuid, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName) + { + if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + wwAlarmConsumerClass com = client + ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + // VBGUID is wnwrap's GUID interop struct (same memory layout as + // System.Guid: int32 + 2x int16 + 8x byte). Convert via a single + // unmanaged-blittable round-trip. + VBGUID vb = ToVbGuid(alarmGuid); + return com.AlarmAckByGUID( + AlmGUID: vb, + szComment: ackComment ?? string.Empty, + szOprName: ackOperatorName ?? string.Empty, + szNode: ackOperatorNode ?? string.Empty, + szDomainName: ackOperatorDomain ?? string.Empty, + szOprFullName: ackOperatorFullName ?? string.Empty); + } + + /// + public IReadOnlyList SnapshotActiveAlarms() + { + if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + lock (syncRoot) + { + List active = new List(); + foreach (MxAlarmSnapshotRecord record in latestSnapshot.Values) + { + if (record.State == MxAlarmStateKind.UnackAlm + || record.State == MxAlarmStateKind.AckAlm) + { + active.Add(record); + } + } + return active; + } + } + + private void OnPoll(object? _) + { + if (disposed) return; + try + { + PollOnce(); + } + catch (Exception ex) + { + // Swallow — the poll loop must not propagate exceptions out of + // the timer callback, or the worker process tears down. The + // EventQueue fault counter (wired in by the future A.3 dispatcher) + // is the right place to surface poll failures; for now the + // exception is intentionally silent so the timer keeps firing. + _ = ex; + } + } + + internal void PollOnce() + { + wwAlarmConsumerClass? com; + lock (syncRoot) + { + if (disposed || !subscribed) return; + com = client; + } + if (com is null) return; + + object xmlObj = string.Empty; + com.GetXmlCurrentAlarms2(maxAlmCnt: maxAlarmsPerFetch, vartCurrentXmlAlarms: out xmlObj); + string xml = xmlObj?.ToString() ?? string.Empty; + if (xml.Length == 0) return; + + Dictionary next = ParseSnapshotXml(xml); + + List transitions = new List(); + lock (syncRoot) + { + foreach (KeyValuePair kv in next) + { + MxAlarmStateKind previousState = MxAlarmStateKind.Unspecified; + if (latestSnapshot.TryGetValue(kv.Key, out MxAlarmSnapshotRecord? prev)) + { + previousState = prev.State; + if (previousState == kv.Value.State) continue; // no transition + } + transitions.Add(new MxAlarmTransitionEvent + { + Record = kv.Value, + PreviousState = previousState, + }); + } + latestSnapshot.Clear(); + foreach (KeyValuePair kv in next) + { + latestSnapshot[kv.Key] = kv.Value; + } + } + + if (transitions.Count == 0) return; + EventHandler? handler = AlarmTransitionEmitted; + if (handler is null) return; + foreach (MxAlarmTransitionEvent transition in transitions) + { + handler.Invoke(this, transition); + } + } + + /// + /// Parse the XML payload returned by GetXmlCurrentAlarms2 + /// into a GUID-keyed dictionary. Records with malformed GUIDs are + /// silently dropped (no fault is recorded — the next poll will + /// resync). + /// + public static Dictionary ParseSnapshotXml(string xml) + { + Dictionary records = + new Dictionary(); + + if (string.IsNullOrWhiteSpace(xml)) return records; + + XmlDocument doc = new XmlDocument(); + doc.LoadXml(xml); + XmlNodeList? alarmNodes = doc.SelectNodes("/ALARM_RECORDS/ALARM"); + if (alarmNodes is null) return records; + + foreach (XmlNode alarmNode in alarmNodes) + { + string guidHex = TextOf(alarmNode, "GUID"); + if (!TryParseHexGuid(guidHex, out Guid guid)) continue; + + string xmlDate = TextOf(alarmNode, "DATE"); + string xmlTime = TextOf(alarmNode, "TIME"); + int gmtOffset = ParseInt(TextOf(alarmNode, "GMTOFFSET")); + int dstAdjust = ParseInt(TextOf(alarmNode, "DSTADJUST")); + DateTime tsUtc = AlarmRecordTransitionMapper.ParseTransitionTimestampUtc( + xmlDate, xmlTime, gmtOffset, dstAdjust); + + records[guid] = new MxAlarmSnapshotRecord + { + AlarmGuid = guid, + TransitionTimestampUtc = tsUtc, + ProviderNode = TextOf(alarmNode, "PROVIDER_NODE"), + ProviderName = TextOf(alarmNode, "PROVIDER_NAME"), + Group = TextOf(alarmNode, "GROUP"), + TagName = TextOf(alarmNode, "TAGNAME"), + Type = TextOf(alarmNode, "TYPE"), + Value = TextOf(alarmNode, "VALUE"), + Limit = TextOf(alarmNode, "LIMIT"), + Priority = ParseInt(TextOf(alarmNode, "PRIORITY")), + State = AlarmRecordTransitionMapper.ParseStateKind(TextOf(alarmNode, "STATE")), + OperatorNode = TextOf(alarmNode, "OPERATOR_NODE"), + OperatorName = TextOf(alarmNode, "OPERATOR_NAME"), + AlarmComment = TextOf(alarmNode, "ALARM_COMMENT"), + }; + } + return records; + } + + private static string TextOf(XmlNode parent, string childName) + { + XmlNode? node = parent.SelectSingleNode(childName); + return node?.InnerText ?? string.Empty; + } + + private static int ParseInt(string text) + { + return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) + ? n : 0; + } + + /// + /// wnwrap's XML GUID field is a 32-char hex string with no + /// dashes (e.g. "BCC4705395424D65BDAABCDEA6A32A73"). Convert + /// to 's canonical 8-4-4-4-12 layout. + /// + public static bool TryParseHexGuid(string? hex, out Guid guid) + { + guid = Guid.Empty; + if (string.IsNullOrWhiteSpace(hex)) return false; + string trimmed = hex!.Trim(); + if (Guid.TryParse(trimmed, out guid)) return true; + if (trimmed.Length != 32) return false; + string canonical = + trimmed.Substring(0, 8) + "-" + + trimmed.Substring(8, 4) + "-" + + trimmed.Substring(12, 4) + "-" + + trimmed.Substring(16, 4) + "-" + + trimmed.Substring(20, 12); + return Guid.TryParse(canonical, out guid); + } + + private static VBGUID ToVbGuid(Guid g) + { + byte[] bytes = g.ToByteArray(); + // Guid byte layout: int32-LE + int16-LE + int16-LE + 8 bytes (Data4). + VBGUID vb = new VBGUID + { + Data1 = BitConverter.ToInt32(bytes, 0), + Data2 = BitConverter.ToInt16(bytes, 4), + Data3 = BitConverter.ToInt16(bytes, 6), + Data4 = new byte[8], + }; + Array.Copy(bytes, 8, vb.Data4, 0, 8); + return vb; + } + + /// + public void Dispose() + { + Timer? timerToDispose; + wwAlarmConsumerClass? clientToDispose; + lock (syncRoot) + { + if (disposed) return; + disposed = true; + timerToDispose = pollTimer; + pollTimer = null; + clientToDispose = client; + client = null; + } + timerToDispose?.Dispose(); + if (clientToDispose is not null) + { + try { clientToDispose.DeregisterConsumer(); } catch { /* swallow */ } + try { clientToDispose.UninitializeConsumer(); } catch { /* swallow */ } + if (Marshal.IsComObject(clientToDispose)) + { + try { Marshal.FinalReleaseComObject(clientToDispose); } catch { /* swallow */ } + } + } + } +} diff --git a/src/MxGateway.Worker/MxGateway.Worker.csproj b/src/MxGateway.Worker/MxGateway.Worker.csproj index 8ecee08..6850b7f 100644 --- a/src/MxGateway.Worker/MxGateway.Worker.csproj +++ b/src/MxGateway.Worker/MxGateway.Worker.csproj @@ -24,15 +24,11 @@ false false - - C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll - false - false - - - C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\IAlarmMgrDataProvider.dll - false + + ..\..\lib\Interop.WNWRAPCONSUMERLib.dll + true false + false -- 2.52.0 From 82eb0ad569419e709fa2809b056d3cd8ad7ad5fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 09:52:35 -0400 Subject: [PATCH 11/16] A.3 (in-process slice): AlarmDispatcher wires consumer events onto event queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the in-process plumbing that connects WnWrapAlarmConsumer's AlarmTransitionEmitted stream to the worker's MxAccessEventQueue via MxAccessAlarmEventSink. With this change a transition raised by the consumer lands as an OnAlarmTransitionEvent proto on the queue, SessionId attached, ready for IPC dispatch. Mapping: provider!group.tag → AlarmFullReference, tag → SourceObjectReference, priority → severity, wnwrap STATE → AlarmConditionState (Active / ActiveAcked / Inactive — wnwrap's ack-vs-unack-on-cleared distinction collapses since OPC UA Part 9 doesn't model it). State delta drives AlarmTransitionKind via the existing AlarmRecordTransitionMapper table. Holding off on the proto IPC additions (SubscribeAlarms / AcknowledgeAlarm / QueryActiveAlarms commands + WorkerAlarmRpcDispatcher) for a follow-up — those touch every layer of the worker IPC and warrant their own PR. This slice proves the consumer→sink→queue pipeline end-to-end with unit tests and clears the path for the proto additions to plug in cleanly. Tests: 10 new unit tests cover field-by-field mapping, the "unchanged-state-doesn't-emit" filter, the state→transition kind table, Subscribe / Acknowledge passthrough, SnapshotActiveAlarms → proto ActiveAlarmSnapshot mapping, and Dispose detaches the handler. All passing; total worker test count 172/3 skip / 1 pre-existing structure fail (untouched). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MxAccess/AlarmDispatcherTests.cs | 288 ++++++++++++++++++ .../MxAccess/AlarmDispatcher.cs | 190 ++++++++++++ 2 files changed, 478 insertions(+) create mode 100644 src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs create mode 100644 src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs new file mode 100644 index 0000000..5816d64 --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using MxGateway.Contracts.Proto; +using MxGateway.Worker.MxAccess; + +namespace MxGateway.Worker.Tests.MxAccess; + +/// +/// Unit tests for the in-process A.3 dispatcher: prove that +/// events +/// fan out to the worker's as proto +/// messages with correctly mapped +/// fields. The fake consumer below stands in for the wnwrap-backed +/// production implementation so this exercise needs no AVEVA install. +/// +public sealed class AlarmDispatcherTests +{ + private const string SessionId = "session-001"; + + [Fact] + public void TransitionEvent_lands_in_queue_with_mapped_fields() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + MxAccessEventQueue queue = new MxAccessEventQueue(); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); + using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); + + DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc); + consumer.RaiseTransition(new MxAlarmTransitionEvent + { + PreviousState = MxAlarmStateKind.Unspecified, + Record = new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "TestArea", + TagName = "TestMachine_001.TestAlarm001", + Type = "DSC", + Priority = 500, + State = MxAlarmStateKind.UnackAlm, + TransitionTimestampUtc = ts, + AlarmComment = "Test alarm #1", + }, + }); + + Assert.Equal(1, queue.Count); + Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); + Assert.NotNull(workerEvent); + MxEvent mxEvent = workerEvent!.Event; + Assert.Equal(MxEventFamily.OnAlarmTransition, mxEvent.Family); + Assert.Equal(SessionId, mxEvent.SessionId); + + OnAlarmTransitionEvent body = mxEvent.OnAlarmTransition; + Assert.NotNull(body); + Assert.Equal("Galaxy!TestArea.TestMachine_001.TestAlarm001", body.AlarmFullReference); + Assert.Equal("TestMachine_001.TestAlarm001", body.SourceObjectReference); + Assert.Equal("DSC", body.AlarmTypeName); + Assert.Equal(AlarmTransitionKind.Raise, body.TransitionKind); + Assert.Equal(500, body.Severity); + Assert.Equal("Test alarm #1", body.OperatorComment); + Assert.Equal("TestArea", body.Category); + Assert.NotNull(body.TransitionTimestamp); + Assert.Equal(ts, body.TransitionTimestamp.ToDateTime()); + } + + [Fact] + public void Consecutive_unchanged_state_does_not_emit_a_transition() + { + // Mapper.MapTransition returns Unspecified when the state didn't + // change; the dispatcher should drop the event before queueing. + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + MxAccessEventQueue queue = new MxAccessEventQueue(); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); + using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); + + consumer.RaiseTransition(new MxAlarmTransitionEvent + { + PreviousState = MxAlarmStateKind.UnackAlm, + Record = new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "X", + TagName = "Y", + State = MxAlarmStateKind.UnackAlm, + }, + }); + + Assert.Equal(0, queue.Count); + } + + [Theory] + [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Acknowledge)] + [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] + [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] + public void Transition_kind_follows_state_table( + MxAlarmStateKind previous, + MxAlarmStateKind current, + AlarmTransitionKind expected) + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + MxAccessEventQueue queue = new MxAccessEventQueue(); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); + using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); + + consumer.RaiseTransition(new MxAlarmTransitionEvent + { + PreviousState = previous, + Record = new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "G", + TagName = "T", + State = current, + }, + }); + + Assert.Equal(1, queue.Count); + queue.TryDequeue(out WorkerEvent? evt); + Assert.Equal(expected, evt!.Event.OnAlarmTransition.TransitionKind); + } + + [Fact] + public void Subscribe_forwards_to_consumer() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + using AlarmDispatcher dispatcher = new AlarmDispatcher( + consumer, + new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), + SessionId); + + dispatcher.Subscribe(@"\\HOST\Galaxy!Area1"); + Assert.Equal(@"\\HOST\Galaxy!Area1", consumer.LastSubscription); + } + + [Fact] + public void Acknowledge_forwards_to_consumer_with_full_operator_identity() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + consumer.AcknowledgeReturn = 0; + using AlarmDispatcher dispatcher = new AlarmDispatcher( + consumer, + new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), + SessionId); + + Guid guid = Guid.NewGuid(); + int rc = dispatcher.Acknowledge( + guid, "Acked", "alice", "WS01", "CORP", "Alice Smith"); + + Assert.Equal(0, rc); + Assert.Equal(guid, consumer.LastAckGuid); + Assert.Equal("Acked", consumer.LastAckComment); + Assert.Equal("alice", consumer.LastAckOperatorName); + Assert.Equal("WS01", consumer.LastAckOperatorNode); + Assert.Equal("CORP", consumer.LastAckOperatorDomain); + Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName); + } + + [Fact] + public void SnapshotActiveAlarms_maps_records_to_protos() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc); + consumer.SnapshotResult = new[] + { + new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "TestArea", + TagName = "Tag1", + Type = "DSC", + Priority = 500, + State = MxAlarmStateKind.UnackAlm, + TransitionTimestampUtc = ts, + AlarmComment = "x", + }, + new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "TestArea", + TagName = "Tag2", + Type = "ANL", + Priority = 100, + State = MxAlarmStateKind.AckAlm, + TransitionTimestampUtc = ts, + }, + }; + using AlarmDispatcher dispatcher = new AlarmDispatcher( + consumer, + new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), + SessionId); + + IReadOnlyList snapshots = dispatcher.SnapshotActiveAlarms(); + Assert.Equal(2, snapshots.Count); + + Assert.Equal("Galaxy!TestArea.Tag1", snapshots[0].AlarmFullReference); + Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState); + Assert.Equal(500, snapshots[0].Severity); + Assert.Equal(ts, snapshots[0].LastTransitionTimestamp.ToDateTime()); + + Assert.Equal("Galaxy!TestArea.Tag2", snapshots[1].AlarmFullReference); + Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState); + } + + [Fact] + public void Dispose_unsubscribes_handler_and_disposes_consumer() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer(); + MxAccessEventQueue queue = new MxAccessEventQueue(); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); + AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); + + dispatcher.Dispose(); + + Assert.True(consumer.Disposed); + consumer.RaiseTransition(new MxAlarmTransitionEvent + { + PreviousState = MxAlarmStateKind.Unspecified, + Record = new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "G", + TagName = "T", + State = MxAlarmStateKind.UnackAlm, + }, + }); + Assert.Equal(0, queue.Count); + } + + private sealed class FakeAlarmConsumer : IMxAccessAlarmConsumer + { + public event EventHandler? AlarmTransitionEmitted; + + public string? LastSubscription { get; private set; } + public Guid LastAckGuid { get; private set; } + public string? LastAckComment { get; private set; } + public string? LastAckOperatorName { get; private set; } + public string? LastAckOperatorNode { get; private set; } + public string? LastAckOperatorDomain { get; private set; } + public string? LastAckOperatorFullName { get; private set; } + public int AcknowledgeReturn { get; set; } + public IReadOnlyList SnapshotResult { get; set; } = + Array.Empty(); + public bool Disposed { get; private set; } + + public void RaiseTransition(MxAlarmTransitionEvent transition) + { + AlarmTransitionEmitted?.Invoke(this, transition); + } + + public void Subscribe(string subscription) + { + LastSubscription = subscription; + } + + public int AcknowledgeByGuid( + Guid alarmGuid, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName) + { + LastAckGuid = alarmGuid; + LastAckComment = ackComment; + LastAckOperatorName = ackOperatorName; + LastAckOperatorNode = ackOperatorNode; + LastAckOperatorDomain = ackOperatorDomain; + LastAckOperatorFullName = ackOperatorFullName; + return AcknowledgeReturn; + } + + public IReadOnlyList SnapshotActiveAlarms() + { + return SnapshotResult; + } + + public void Dispose() + { + Disposed = true; + } + } +} diff --git a/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs new file mode 100644 index 0000000..0614531 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using MxGateway.Contracts.Proto; + +namespace MxGateway.Worker.MxAccess; + +/// +/// In-process dispatcher that owns the lifetime of an +/// + +/// pair, and wires the consumer's AlarmTransitionEmitted stream +/// onto the sink's EnqueueTransition path so transitions land on +/// the worker's as proto +/// messages ready for IPC dispatch. +/// +/// +/// +/// This is the in-process slice of A.3 — it proves the +/// consumer→sink→queue pipeline end-to-end without touching the +/// worker's IPC command framing. The companion follow-up PR adds +/// SubscribeAlarmsCommand / AcknowledgeAlarmCommand / +/// QueryActiveAlarmsCommand proto entries plus the gateway- +/// side WorkerAlarmRpcDispatcher that issues them. +/// +/// +/// Threading: polls on a +/// thread today; production +/// hosting should marshal the consumer onto the worker's STA via +/// StaRuntime.InvokeAsync. The dispatcher itself is purely +/// a pass-through, so it inherits whatever thread the consumer's +/// event handler fires on. Fan-out into EnqueueTransition +/// uses which is +/// thread-safe. +/// +/// +public sealed class AlarmDispatcher : IDisposable +{ + private readonly IMxAccessAlarmConsumer consumer; + private readonly MxAccessAlarmEventSink sink; + private readonly string sessionId; + private readonly EventHandler handler; + private bool disposed; + + public AlarmDispatcher( + IMxAccessAlarmConsumer consumer, + MxAccessAlarmEventSink sink, + string sessionId) + { + this.consumer = consumer ?? throw new ArgumentNullException(nameof(consumer)); + this.sink = sink ?? throw new ArgumentNullException(nameof(sink)); + this.sessionId = sessionId ?? string.Empty; + // Sink.Attach is the seam that propagates the session id onto the + // proto SessionId field of every emitted MxEvent. Pass the consumer + // as the "associated COM object" — sink ignores the object reference + // for the alarm path, but the existing IMxAccessEventSink contract + // requires a non-null first arg. + this.sink.Attach(this.consumer, this.sessionId); + this.handler = OnTransition; + consumer.AlarmTransitionEmitted += handler; + } + + /// + /// Begin polling the configured AVEVA alarm provider for + /// transitions. The supplied subscription expression follows the + /// canonical \\<machine>\Galaxy!<area> format. + /// + public void Subscribe(string subscription) + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); + consumer.Subscribe(subscription); + } + + /// + /// Forward an AcknowledgeAlarm request to the underlying + /// consumer's AlarmAckByGUID. Returns the AVEVA-native + /// status code (0 = success). + /// + public int Acknowledge( + Guid alarmGuid, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName) + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); + return consumer.AcknowledgeByGuid( + alarmGuid, + ackComment, + ackOperatorName, + ackOperatorNode, + ackOperatorDomain, + ackOperatorFullName); + } + + /// + /// Snapshot the currently-active alarm set as + /// protos for the + /// QueryActiveAlarms RPC's ConditionRefresh stream. + /// + public IReadOnlyList SnapshotActiveAlarms() + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); + IReadOnlyList records = consumer.SnapshotActiveAlarms(); + if (records.Count == 0) return Array.Empty(); + List snapshots = new List(records.Count); + foreach (MxAlarmSnapshotRecord record in records) + { + snapshots.Add(MapToSnapshot(record)); + } + return snapshots; + } + + private void OnTransition(object? sender, MxAlarmTransitionEvent transition) + { + if (disposed) return; + if (transition is null) return; + + MxAlarmSnapshotRecord record = transition.Record; + AlarmTransitionKind kind = AlarmRecordTransitionMapper.MapTransition( + transition.PreviousState, record.State); + if (kind == AlarmTransitionKind.Unspecified) return; + + string fullReference = AlarmRecordTransitionMapper.ComposeFullReference( + record.ProviderName, record.Group, record.TagName); + + sink.EnqueueTransition( + alarmFullReference: fullReference, + sourceObjectReference: record.TagName, + alarmTypeName: record.Type, + transitionKind: kind, + severity: record.Priority, + originalRaiseTimestampUtc: null, + transitionTimestampUtc: record.TransitionTimestampUtc, + operatorUser: record.OperatorName, + operatorComment: record.AlarmComment, + category: record.Group, + description: string.Empty); + } + + private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record) + { + ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot + { + AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference( + record.ProviderName, record.Group, record.TagName), + SourceObjectReference = record.TagName, + AlarmTypeName = record.Type, + CurrentState = MapConditionState(record.State), + Severity = record.Priority, + OperatorUser = record.OperatorName, + OperatorComment = record.AlarmComment, + Category = record.Group, + Description = string.Empty, + }; + if (record.TransitionTimestampUtc != DateTime.MinValue) + { + snapshot.LastTransitionTimestamp = + Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( + DateTime.SpecifyKind(record.TransitionTimestampUtc, DateTimeKind.Utc)); + } + return snapshot; + } + + private static AlarmConditionState MapConditionState(MxAlarmStateKind state) + { + // The proto's AlarmConditionState only distinguishes Active / + // ActiveAcked / Inactive — both Rtn states collapse to Inactive + // (the ack-vs-unack distinction on a cleared alarm is not exposed + // through OPC UA's Part 9 condition state model anyway). + return state switch + { + MxAlarmStateKind.UnackAlm => AlarmConditionState.Active, + MxAlarmStateKind.AckAlm => AlarmConditionState.ActiveAcked, + MxAlarmStateKind.UnackRtn => AlarmConditionState.Inactive, + MxAlarmStateKind.AckRtn => AlarmConditionState.Inactive, + _ => AlarmConditionState.Unspecified, + }; + } + + public string SessionId => sessionId; + + public void Dispose() + { + if (disposed) return; + disposed = true; + try { consumer.AlarmTransitionEmitted -= handler; } catch { /* swallow */ } + try { sink.Detach(); } catch { /* swallow */ } + try { consumer.Dispose(); } catch { /* swallow */ } + } +} -- 2.52.0 From 01f5e6ad916e916e765366a1c1b36532ce72c197 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 10:52:04 -0400 Subject: [PATCH 12/16] A.3 (worker IPC slice): proto SubscribeAlarms/Acknowledge/QueryActive commands + executor routing Adds the worker-side IPC surface for the alarm subsystem so the gateway can drive the AlarmDispatcher across the named-pipe boundary. Adds four proto MxCommandKind values + matching command messages and two MxCommandReply payload variants: - SubscribeAlarmsCommand(subscription_expression) - UnsubscribeAlarmsCommand - AcknowledgeAlarmCommand(alarm_guid, comment, operator_user/node/domain/full_name) - QueryActiveAlarmsCommand(alarm_filter_prefix) - AcknowledgeAlarmReplyPayload(native_status) - QueryActiveAlarmsReplyPayload(repeated ActiveAlarmSnapshot snapshots) Worker plumbing: - New IAlarmCommandHandler interface + AlarmCommandHandler production impl. Lazy-creates an AlarmDispatcher (with a wnwrap-backed consumer by default) on the first SubscribeAlarms; routes Acknowledge / QueryActive / Unsubscribe through it. Idempotent under repeated Unsubscribe; rejects a second Subscribe without an intervening Unsubscribe; cleans up the consumer if the underlying Subscribe call throws. - MxAccessCommandExecutor: 4 new switch arms map MxCommandKind values to IAlarmCommandHandler calls. Acknowledge surfaces the AVEVA native status into both MxCommandReply.Hresult and the dedicated AcknowledgeAlarmReplyPayload.NativeStatus so gateway-side consumers can echo it without unpacking the outer envelope. Invalid GUIDs and missing payloads return InvalidRequest; handler exceptions return MxaccessFailure with the exception message in DiagnosticMessage. - MxAccessStaSession: new constructor overload accepts an alarmCommandHandlerFactory; it's invoked on the STA thread during StartAsync and the resulting handler is passed into the executor. ShutdownGracefullyAsync + Dispose tear it down on the STA before the data-side cleanup runs. Tests: 20 new unit tests covering AlarmCommandHandler lazy lifecycle (Subscribe/Unsubscribe/Acknowledge/Query/Dispose, error paths) and the executor's 4 alarm switch arms (OK/InvalidRequest/MxaccessFailure paths, hresult propagation, prefix filtering). Worker test suite total: 192 passed / 3 skipped (live probes) / 1 pre-existing structure-test fail (untouched). Deferred to next slice: gateway-side WorkerAlarmRpcDispatcher that replaces NotWiredAlarmRpcDispatcher, builds + sends these commands across the IPC, and unwraps the resulting MxCommandReply into AcknowledgeAlarmReply / ActiveAlarmSnapshot stream. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Generated/MxaccessGateway.cs | 2417 ++++++++++++++--- .../Protos/mxaccess_gateway.proto | 64 + .../MxAccess/AlarmCommandExecutorTests.cs | 384 +++ .../MxAccess/AlarmCommandHandlerTests.cs | 232 ++ .../MxAccess/AlarmCommandHandler.cs | 192 ++ .../MxAccess/MxAccessCommandExecutor.cs | 169 +- .../MxAccess/MxAccessStaSession.cs | 64 +- 7 files changed, 3170 insertions(+), 352 deletions(-) create mode 100644 src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs create mode 100644 src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs create mode 100644 src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs diff --git a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs index 05c543c..d49b977 100644 --- a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -46,7 +46,7 @@ namespace MxGateway.Contracts.Proto { "ZnRlcl93b3JrZXJfc2VxdWVuY2UYAiABKAQidgoQTXhDb21tYW5kUmVxdWVz", "dBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9p", "ZBgCIAEoCRIvCgdjb21tYW5kGAMgASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5NeENvbW1hbmQizw8KCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", + "MS5NeENvbW1hbmQijhIKCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRLaW5kEjgKCHJlZ2lzdGVyGAog", "ASgLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3RlckNvbW1hbmRIABI8", "Cgp1bnJlZ2lzdGVyGAsgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnJl", @@ -83,312 +83,336 @@ namespace MxGateway.Contracts.Proto { "EkMKDnN1YnNjcmliZV9idWxrGCAgASgLMikubXhhY2Nlc3NfZ2F0ZXdheS52", "MS5TdWJzY3JpYmVCdWxrQ29tbWFuZEgAEkcKEHVuc3Vic2NyaWJlX2J1bGsY", "ISABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLlVuc3Vic2NyaWJlQnVsa0Nv", - "bW1hbmRIABIwCgRwaW5nGGQgASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5Q", - "aW5nQ29tbWFuZEgAEkgKEWdldF9zZXNzaW9uX3N0YXRlGGUgASgLMisubXhh", - "Y2Nlc3NfZ2F0ZXdheS52MS5HZXRTZXNzaW9uU3RhdGVDb21tYW5kSAASRAoP", - "Z2V0X3dvcmtlcl9pbmZvGGYgASgLMikubXhhY2Nlc3NfZ2F0ZXdheS52MS5H", - "ZXRXb3JrZXJJbmZvQ29tbWFuZEgAEj8KDGRyYWluX2V2ZW50cxhnIAEoCzIn", - "Lm14YWNjZXNzX2dhdGV3YXkudjEuRHJhaW5FdmVudHNDb21tYW5kSAASRQoP", - "c2h1dGRvd25fd29ya2VyGGggASgLMioubXhhY2Nlc3NfZ2F0ZXdheS52MS5T", - "aHV0ZG93bldvcmtlckNvbW1hbmRIAEIJCgdwYXlsb2FkIiYKD1JlZ2lzdGVy", - "Q29tbWFuZBITCgtjbGllbnRfbmFtZRgBIAEoCSIqChFVbnJlZ2lzdGVyQ29t", - "bWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFIkAKDkFkZEl0ZW1Db21tYW5k", - "EhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFwoPaXRlbV9kZWZpbml0aW9uGAIg", - "ASgJIlcKD0FkZEl0ZW0yQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgF", - "EhcKD2l0ZW1fZGVmaW5pdGlvbhgCIAEoCRIUCgxpdGVtX2NvbnRleHQYAyAB", - "KAkiPwoRUmVtb3ZlSXRlbUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", - "BRITCgtpdGVtX2hhbmRsZRgCIAEoBSI7Cg1BZHZpc2VDb21tYW5kEhUKDXNl", - "cnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiPQoPVW5B", - "ZHZpc2VDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9o", - "YW5kbGUYAiABKAUiRgoYQWR2aXNlU3VwZXJ2aXNvcnlDb21tYW5kEhUKDXNl", - "cnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiXgoWQWRk", - "QnVmZmVyZWRJdGVtQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhcK", - "D2l0ZW1fZGVmaW5pdGlvbhgCIAEoCRIUCgxpdGVtX2NvbnRleHQYAyABKAki", - "XwogU2V0QnVmZmVyZWRVcGRhdGVJbnRlcnZhbENvbW1hbmQSFQoNc2VydmVy", - "X2hhbmRsZRgBIAEoBRIkChx1cGRhdGVfaW50ZXJ2YWxfbWlsbGlzZWNvbmRz", - "GAIgASgFIjwKDlN1c3BlbmRDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASAB", - "KAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiPQoPQWN0aXZhdGVDb21tYW5kEhUK", - "DXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUieAoM", - "V3JpdGVDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9o", - "YW5kbGUYAiABKAUSKwoFdmFsdWUYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5", - "LnYxLk14VmFsdWUSDwoHdXNlcl9pZBgEIAEoBSKwAQoNV3JpdGUyQ29tbWFu", + "bW1hbmRIABJHChBzdWJzY3JpYmVfYWxhcm1zGCIgASgLMisubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5TdWJzY3JpYmVBbGFybXNDb21tYW5kSAASSwoSdW5zdWJz", + "Y3JpYmVfYWxhcm1zGCMgASgLMi0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnN1", + "YnNjcmliZUFsYXJtc0NvbW1hbmRIABJRChlhY2tub3dsZWRnZV9hbGFybV9j", + "b21tYW5kGCQgASgLMiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRn", + "ZUFsYXJtQ29tbWFuZEgAElQKG3F1ZXJ5X2FjdGl2ZV9hbGFybXNfY29tbWFu", + "ZBglIAEoCzItLm14YWNjZXNzX2dhdGV3YXkudjEuUXVlcnlBY3RpdmVBbGFy", + "bXNDb21tYW5kSAASMAoEcGluZxhkIAEoCzIgLm14YWNjZXNzX2dhdGV3YXku", + "djEuUGluZ0NvbW1hbmRIABJIChFnZXRfc2Vzc2lvbl9zdGF0ZRhlIAEoCzIr", + "Lm14YWNjZXNzX2dhdGV3YXkudjEuR2V0U2Vzc2lvblN0YXRlQ29tbWFuZEgA", + "EkQKD2dldF93b3JrZXJfaW5mbxhmIAEoCzIpLm14YWNjZXNzX2dhdGV3YXku", + "djEuR2V0V29ya2VySW5mb0NvbW1hbmRIABI/CgxkcmFpbl9ldmVudHMYZyAB", + "KAsyJy5teGFjY2Vzc19nYXRld2F5LnYxLkRyYWluRXZlbnRzQ29tbWFuZEgA", + "EkUKD3NodXRkb3duX3dvcmtlchhoIAEoCzIqLm14YWNjZXNzX2dhdGV3YXku", + "djEuU2h1dGRvd25Xb3JrZXJDb21tYW5kSABCCQoHcGF5bG9hZCImCg9SZWdp", + "c3RlckNvbW1hbmQSEwoLY2xpZW50X25hbWUYASABKAkiKgoRVW5yZWdpc3Rl", + "ckNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBSJACg5BZGRJdGVtQ29t", + "bWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhcKD2l0ZW1fZGVmaW5pdGlv", + "bhgCIAEoCSJXCg9BZGRJdGVtMkNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB", + "IAEoBRIXCg9pdGVtX2RlZmluaXRpb24YAiABKAkSFAoMaXRlbV9jb250ZXh0", + "GAMgASgJIj8KEVJlbW92ZUl0ZW1Db21tYW5kEhUKDXNlcnZlcl9oYW5kbGUY", + "ASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiOwoNQWR2aXNlQ29tbWFuZBIV", + "Cg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIj0K", + "D1VuQWR2aXNlQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0", + "ZW1faGFuZGxlGAIgASgFIkYKGEFkdmlzZVN1cGVydmlzb3J5Q29tbWFuZBIV", + "Cg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIl4K", + "FkFkZEJ1ZmZlcmVkSXRlbUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", + "BRIXCg9pdGVtX2RlZmluaXRpb24YAiABKAkSFAoMaXRlbV9jb250ZXh0GAMg", + "ASgJIl8KIFNldEJ1ZmZlcmVkVXBkYXRlSW50ZXJ2YWxDb21tYW5kEhUKDXNl", + "cnZlcl9oYW5kbGUYASABKAUSJAocdXBkYXRlX2ludGVydmFsX21pbGxpc2Vj", + "b25kcxgCIAEoBSI8Cg5TdXNwZW5kQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxl", + "GAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIj0KD0FjdGl2YXRlQ29tbWFu", "ZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgF", - "EisKBXZhbHVlGAMgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVl", - "EjUKD3RpbWVzdGFtcF92YWx1ZRgEIAEoCzIcLm14YWNjZXNzX2dhdGV3YXku", - "djEuTXhWYWx1ZRIPCgd1c2VyX2lkGAUgASgFIqEBChNXcml0ZVNlY3VyZWRD", - "b21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUY", - "AiABKAUSFwoPY3VycmVudF91c2VyX2lkGAMgASgFEhgKEHZlcmlmaWVyX3Vz", - "ZXJfaWQYBCABKAUSKwoFdmFsdWUYBSABKAsyHC5teGFjY2Vzc19nYXRld2F5", - "LnYxLk14VmFsdWUi2QEKFFdyaXRlU2VjdXJlZDJDb21tYW5kEhUKDXNlcnZl", - "cl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUSFwoPY3VycmVu", - "dF91c2VyX2lkGAMgASgFEhgKEHZlcmlmaWVyX3VzZXJfaWQYBCABKAUSKwoF", - "dmFsdWUYBSABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUSNQoP", - "dGltZXN0YW1wX3ZhbHVlGAYgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5N", - "eFZhbHVlImMKF0F1dGhlbnRpY2F0ZVVzZXJDb21tYW5kEhUKDXNlcnZlcl9o", - "YW5kbGUYASABKAUSEwoLdmVyaWZ5X3VzZXIYAiABKAkSHAoUdmVyaWZ5X3Vz", - "ZXJfcGFzc3dvcmQYAyABKAkiRwoYQXJjaGVzdHJBVXNlclRvSWRDb21tYW5k", - "EhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFAoMdXNlcl9pZF9ndWlkGAIgASgJ", - "IkIKEkFkZEl0ZW1CdWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgF", - "EhUKDXRhZ19hZGRyZXNzZXMYAiADKAkiRAoVQWR2aXNlSXRlbUJ1bGtDb21t", - "YW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFAoMaXRlbV9oYW5kbGVzGAIg", - "AygFIkQKFVJlbW92ZUl0ZW1CdWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxl", - "GAEgASgFEhQKDGl0ZW1faGFuZGxlcxgCIAMoBSJGChdVbkFkdmlzZUl0ZW1C", - "dWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0ZW1faGFu", - "ZGxlcxgCIAMoBSJEChRTdWJzY3JpYmVCdWxrQ29tbWFuZBIVCg1zZXJ2ZXJf", - "aGFuZGxlGAEgASgFEhUKDXRhZ19hZGRyZXNzZXMYAiADKAkiRQoWVW5zdWJz", - "Y3JpYmVCdWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0", - "ZW1faGFuZGxlcxgCIAMoBSIeCgtQaW5nQ29tbWFuZBIPCgdtZXNzYWdlGAEg", - "ASgJIhgKFkdldFNlc3Npb25TdGF0ZUNvbW1hbmQiFgoUR2V0V29ya2VySW5m", - "b0NvbW1hbmQiKAoSRHJhaW5FdmVudHNDb21tYW5kEhIKCm1heF9ldmVudHMY", - "ASABKA0iSAoVU2h1dGRvd25Xb3JrZXJDb21tYW5kEi8KDGdyYWNlX3Blcmlv", - "ZBgBIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiKsCwoOTXhDb21t", - "YW5kUmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb3JyZWxhdGlvbl9p", - "ZBgCIAEoCRIwCgRraW5kGAMgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5N", - "eENvbW1hbmRLaW5kEjwKD3Byb3RvY29sX3N0YXR1cxgEIAEoCzIjLm14YWNj", - "ZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0dXMSFAoHaHJlc3VsdBgFIAEo", - "BUgBiAEBEjIKDHJldHVybl92YWx1ZRgGIAEoCzIcLm14YWNjZXNzX2dhdGV3", - "YXkudjEuTXhWYWx1ZRI0CghzdGF0dXNlcxgHIAMoCzIiLm14YWNjZXNzX2dh", - "dGV3YXkudjEuTXhTdGF0dXNQcm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UY", - "CCABKAkSNgoIcmVnaXN0ZXIYFCABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYx", - "LlJlZ2lzdGVyUmVwbHlIABI1CghhZGRfaXRlbRgVIAEoCzIhLm14YWNjZXNz", - "X2dhdGV3YXkudjEuQWRkSXRlbVJlcGx5SAASNwoJYWRkX2l0ZW0yGBYgASgL", - "MiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRJdGVtMlJlcGx5SAASRgoRYWRk", - "X2J1ZmZlcmVkX2l0ZW0YFyABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLkFk", - "ZEJ1ZmZlcmVkSXRlbVJlcGx5SAASNAoHc3VzcGVuZBgYIAEoCzIhLm14YWNj", - "ZXNzX2dhdGV3YXkudjEuU3VzcGVuZFJlcGx5SAASNgoIYWN0aXZhdGUYGSAB", - "KAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2YXRlUmVwbHlIABJHChFh", - "dXRoZW50aWNhdGVfdXNlchgaIAEoCzIqLm14YWNjZXNzX2dhdGV3YXkudjEu", - "QXV0aGVudGljYXRlVXNlclJlcGx5SAASSwoUYXJjaGVzdHJhX3VzZXJfdG9f", - "aWQYGyABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkFyY2hlc3RyQVVzZXJU", - "b0lkUmVwbHlIABJACg1hZGRfaXRlbV9idWxrGBwgASgLMicubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJDChBhZHZpc2VfaXRl", - "bV9idWxrGB0gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxrU3Vic2Ny", - "aWJlUmVwbHlIABJDChByZW1vdmVfaXRlbV9idWxrGB4gASgLMicubXhhY2Nl", - "c3NfZ2F0ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJGChN1bl9hZHZp", - "c2VfaXRlbV9idWxrGB8gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxr", - "U3Vic2NyaWJlUmVwbHlIABJBCg5zdWJzY3JpYmVfYnVsaxggIAEoCzInLm14", - "YWNjZXNzX2dhdGV3YXkudjEuQnVsa1N1YnNjcmliZVJlcGx5SAASQwoQdW5z", - "dWJzY3JpYmVfYnVsaxghIAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuQnVs", - "a1N1YnNjcmliZVJlcGx5SAASPwoNc2Vzc2lvbl9zdGF0ZRhkIAEoCzImLm14", - "YWNjZXNzX2dhdGV3YXkudjEuU2Vzc2lvblN0YXRlUmVwbHlIABI7Cgt3b3Jr", - "ZXJfaW5mbxhlIAEoCzIkLm14YWNjZXNzX2dhdGV3YXkudjEuV29ya2VySW5m", - "b1JlcGx5SAASPQoMZHJhaW5fZXZlbnRzGGYgASgLMiUubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5EcmFpbkV2ZW50c1JlcGx5SABCCQoHcGF5bG9hZEIKCghfaHJl", - "c3VsdCImCg1SZWdpc3RlclJlcGx5EhUKDXNlcnZlcl9oYW5kbGUYASABKAUi", - "IwoMQWRkSXRlbVJlcGx5EhMKC2l0ZW1faGFuZGxlGAEgASgFIiQKDUFkZEl0", - "ZW0yUmVwbHkSEwoLaXRlbV9oYW5kbGUYASABKAUiKwoUQWRkQnVmZmVyZWRJ", - "dGVtUmVwbHkSEwoLaXRlbV9oYW5kbGUYASABKAUiQgoMU3VzcGVuZFJlcGx5", - "EjIKBnN0YXR1cxgBIAEoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0", - "dXNQcm94eSJDCg1BY3RpdmF0ZVJlcGx5EjIKBnN0YXR1cxgBIAEoCzIiLm14", - "YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQcm94eSIoChVBdXRoZW50aWNh", - "dGVVc2VyUmVwbHkSDwoHdXNlcl9pZBgBIAEoBSIpChZBcmNoZXN0ckFVc2Vy", - "VG9JZFJlcGx5Eg8KB3VzZXJfaWQYASABKAUigQEKD1N1YnNjcmliZVJlc3Vs", - "dBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC3RhZ19hZGRyZXNzGAIgASgJ", - "EhMKC2l0ZW1faGFuZGxlGAMgASgFEhYKDndhc19zdWNjZXNzZnVsGAQgASgI", - "EhUKDWVycm9yX21lc3NhZ2UYBSABKAkiSwoSQnVsa1N1YnNjcmliZVJlcGx5", - "EjUKB3Jlc3VsdHMYASADKAsyJC5teGFjY2Vzc19nYXRld2F5LnYxLlN1YnNj", - "cmliZVJlc3VsdCJFChFTZXNzaW9uU3RhdGVSZXBseRIwCgVzdGF0ZRgBIAEo", - "DjIhLm14YWNjZXNzX2dhdGV3YXkudjEuU2Vzc2lvblN0YXRlInUKD1dvcmtl", - "ckluZm9SZXBseRIZChF3b3JrZXJfcHJvY2Vzc19pZBgBIAEoBRIWCg53b3Jr", - "ZXJfdmVyc2lvbhgCIAEoCRIXCg9teGFjY2Vzc19wcm9naWQYAyABKAkSFgoO", - "bXhhY2Nlc3NfY2xzaWQYBCABKAkiQAoQRHJhaW5FdmVudHNSZXBseRIsCgZl", - "dmVudHMYASADKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQi5wYK", - "B014RXZlbnQSMgoGZmFtaWx5GAEgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5NeEV2ZW50RmFtaWx5EhIKCnNlc3Npb25faWQYAiABKAkSFQoNc2VydmVy", - "X2hhbmRsZRgDIAEoBRITCgtpdGVtX2hhbmRsZRgEIAEoBRIrCgV2YWx1ZRgF", - "IAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgdxdWFsaXR5", - "GAYgASgFEjQKEHNvdXJjZV90aW1lc3RhbXAYByABKAsyGi5nb29nbGUucHJv", - "dG9idWYuVGltZXN0YW1wEjQKCHN0YXR1c2VzGAggAygLMiIubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5NeFN0YXR1c1Byb3h5EhcKD3dvcmtlcl9zZXF1ZW5jZRgJ", - "IAEoBBI0ChB3b3JrZXJfdGltZXN0YW1wGAogASgLMhouZ29vZ2xlLnByb3Rv", - "YnVmLlRpbWVzdGFtcBI9ChlnYXRld2F5X3JlY2VpdmVfdGltZXN0YW1wGAsg", - "ASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIUCgdocmVzdWx0GAwg", - "ASgFSAGIAQESEgoKcmF3X3N0YXR1cxgNIAEoCRJACg5vbl9kYXRhX2NoYW5n", - "ZRgUIAEoCzImLm14YWNjZXNzX2dhdGV3YXkudjEuT25EYXRhQ2hhbmdlRXZl", - "bnRIABJGChFvbl93cml0ZV9jb21wbGV0ZRgVIAEoCzIpLm14YWNjZXNzX2dh", - "dGV3YXkudjEuT25Xcml0ZUNvbXBsZXRlRXZlbnRIABJJChJvcGVyYXRpb25f", - "Y29tcGxldGUYFiABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLk9wZXJhdGlv", - "bkNvbXBsZXRlRXZlbnRIABJRChdvbl9idWZmZXJlZF9kYXRhX2NoYW5nZRgX", - "IAEoCzIuLm14YWNjZXNzX2dhdGV3YXkudjEuT25CdWZmZXJlZERhdGFDaGFu", - "Z2VFdmVudEgAEkoKE29uX2FsYXJtX3RyYW5zaXRpb24YGCABKAsyKy5teGFj", - "Y2Vzc19nYXRld2F5LnYxLk9uQWxhcm1UcmFuc2l0aW9uRXZlbnRIAEIGCgRi", - "b2R5QgoKCF9ocmVzdWx0IhMKEU9uRGF0YUNoYW5nZUV2ZW50IhYKFE9uV3Jp", - "dGVDb21wbGV0ZUV2ZW50IhgKFk9wZXJhdGlvbkNvbXBsZXRlRXZlbnQi1AEK", - "GU9uQnVmZmVyZWREYXRhQ2hhbmdlRXZlbnQSMgoJZGF0YV90eXBlGAEgASgO", - "Mh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEjQKDnF1YWxpdHlf", - "dmFsdWVzGAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EjYK", - "EHRpbWVzdGFtcF92YWx1ZXMYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYx", - "Lk14QXJyYXkSFQoNcmF3X2RhdGFfdHlwZRgEIAEoBSL9AwoWT25BbGFybVRy", - "YW5zaXRpb25FdmVudBIcChRhbGFybV9mdWxsX3JlZmVyZW5jZRgBIAEoCRIf", - "Chdzb3VyY2Vfb2JqZWN0X3JlZmVyZW5jZRgCIAEoCRIXCg9hbGFybV90eXBl", - "X25hbWUYAyABKAkSQQoPdHJhbnNpdGlvbl9raW5kGAQgASgOMigubXhhY2Nl", - "c3NfZ2F0ZXdheS52MS5BbGFybVRyYW5zaXRpb25LaW5kEhAKCHNldmVyaXR5", - "GAUgASgFEjwKGG9yaWdpbmFsX3JhaXNlX3RpbWVzdGFtcBgGIAEoCzIaLmdv", - "b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASOAoUdHJhbnNpdGlvbl90aW1lc3Rh", - "bXAYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJh", - "dG9yX3VzZXIYCCABKAkSGAoQb3BlcmF0b3JfY29tbWVudBgJIAEoCRIQCghj", - "YXRlZ29yeRgKIAEoCRITCgtkZXNjcmlwdGlvbhgLIAEoCRIzCg1jdXJyZW50", - "X3ZhbHVlGAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEjEK", - "C2xpbWl0X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZh", - "bHVlIv0DChNBY3RpdmVBbGFybVNuYXBzaG90EhwKFGFsYXJtX2Z1bGxfcmVm", - "ZXJlbmNlGAEgASgJEh8KF3NvdXJjZV9vYmplY3RfcmVmZXJlbmNlGAIgASgJ", - "EhcKD2FsYXJtX3R5cGVfbmFtZRgDIAEoCRIQCghzZXZlcml0eRgEIAEoBRI8", - "ChhvcmlnaW5hbF9yYWlzZV90aW1lc3RhbXAYBSABKAsyGi5nb29nbGUucHJv", - "dG9idWYuVGltZXN0YW1wEj8KDWN1cnJlbnRfc3RhdGUYBiABKA4yKC5teGFj", - "Y2Vzc19nYXRld2F5LnYxLkFsYXJtQ29uZGl0aW9uU3RhdGUSEAoIY2F0ZWdv", - "cnkYByABKAkSEwoLZGVzY3JpcHRpb24YCCABKAkSPQoZbGFzdF90cmFuc2l0", - "aW9uX3RpbWVzdGFtcBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh", - "bXASFQoNb3BlcmF0b3JfdXNlchgKIAEoCRIYChBvcGVyYXRvcl9jb21tZW50", - "GAsgASgJEjMKDWN1cnJlbnRfdmFsdWUYDCABKAsyHC5teGFjY2Vzc19nYXRl", - "d2F5LnYxLk14VmFsdWUSMQoLbGltaXRfdmFsdWUYDSABKAsyHC5teGFjY2Vz", - "c19nYXRld2F5LnYxLk14VmFsdWUikgEKF0Fja25vd2xlZGdlQWxhcm1SZXF1", - "ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50X2NvcnJlbGF0aW9u", - "X2lkGAIgASgJEhwKFGFsYXJtX2Z1bGxfcmVmZXJlbmNlGAMgASgJEg8KB2Nv", - "bW1lbnQYBCABKAkSFQoNb3BlcmF0b3JfdXNlchgFIAEoCSLzAQoVQWNrbm93", - "bGVkZ2VBbGFybVJlcGx5EhIKCnNlc3Npb25faWQYASABKAkSFgoOY29ycmVs", - "YXRpb25faWQYAiABKAkSPAoPcHJvdG9jb2xfc3RhdHVzGAMgASgLMiMubXhh", - "Y2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0GAQg", - "ASgFSACIAQESMgoGc3RhdHVzGAUgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5NeFN0YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVzc2FnZRgGIAEoCUIK", - "CghfaHJlc3VsdCJqChhRdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QSEgoKc2Vz", - "c2lvbl9pZBgBIAEoCRIdChVjbGllbnRfY29ycmVsYXRpb25faWQYAiABKAkS", - "GwoTYWxhcm1fZmlsdGVyX3ByZWZpeBgDIAEoCSLrAQoNTXhTdGF0dXNQcm94", - "eRIPCgdzdWNjZXNzGAEgASgFEjcKCGNhdGVnb3J5GAIgASgOMiUubXhhY2Nl", - "c3NfZ2F0ZXdheS52MS5NeFN0YXR1c0NhdGVnb3J5EjgKC2RldGVjdGVkX2J5", - "GAMgASgOMiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0YXR1c1NvdXJjZRIO", - "CgZkZXRhaWwYBCABKAUSFAoMcmF3X2NhdGVnb3J5GAUgASgFEhcKD3Jhd19k", - "ZXRlY3RlZF9ieRgGIAEoBRIXCg9kaWFnbm9zdGljX3RleHQYByABKAkipwMK", - "B014VmFsdWUSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdh", - "eS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIPCgdpc19u", - "dWxsGAMgASgIEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEhUKDXJhd19kYXRh", - "X3R5cGUYBSABKAUSFAoKYm9vbF92YWx1ZRgKIAEoCEgAEhUKC2ludDMyX3Zh", - "bHVlGAsgASgFSAASFQoLaW50NjRfdmFsdWUYDCABKANIABIVCgtmbG9hdF92", - "YWx1ZRgNIAEoAkgAEhYKDGRvdWJsZV92YWx1ZRgOIAEoAUgAEhYKDHN0cmlu", - "Z192YWx1ZRgPIAEoCUgAEjUKD3RpbWVzdGFtcF92YWx1ZRgQIAEoCzIaLmdv", - "b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIABIzCgthcnJheV92YWx1ZRgRIAEo", - "CzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheUgAEhMKCXJhd192YWx1", - "ZRgSIAEoDEgAQgYKBGtpbmQi/gQKB014QXJyYXkSOgoRZWxlbWVudF9kYXRh", - "X3R5cGUYASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUS", - "FAoMdmFyaWFudF90eXBlGAIgASgJEhIKCmRpbWVuc2lvbnMYAyADKA0SFgoO", - "cmF3X2RpYWdub3N0aWMYBCABKAkSHQoVcmF3X2VsZW1lbnRfZGF0YV90eXBl", - "GAUgASgFEjUKC2Jvb2xfdmFsdWVzGAogASgLMh4ubXhhY2Nlc3NfZ2F0ZXdh", - "eS52MS5Cb29sQXJyYXlIABI3CgxpbnQzMl92YWx1ZXMYCyABKAsyHy5teGFj", - "Y2Vzc19nYXRld2F5LnYxLkludDMyQXJyYXlIABI3CgxpbnQ2NF92YWx1ZXMY", - "DCABKAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkludDY0QXJyYXlIABI3Cgxm", - "bG9hdF92YWx1ZXMYDSABKAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkZsb2F0", - "QXJyYXlIABI5Cg1kb3VibGVfdmFsdWVzGA4gASgLMiAubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5Eb3VibGVBcnJheUgAEjkKDXN0cmluZ192YWx1ZXMYDyABKAsy", - "IC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmluZ0FycmF5SAASPwoQdGltZXN0", - "YW1wX3ZhbHVlcxgQIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuVGltZXN0", - "YW1wQXJyYXlIABIzCgpyYXdfdmFsdWVzGBEgASgLMh0ubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5SYXdBcnJheUgAQggKBnZhbHVlcyIbCglCb29sQXJyYXkSDgoG", - "dmFsdWVzGAEgAygIIhwKCkludDMyQXJyYXkSDgoGdmFsdWVzGAEgAygFIhwK", - "CkludDY0QXJyYXkSDgoGdmFsdWVzGAEgAygDIhwKCkZsb2F0QXJyYXkSDgoG", - "dmFsdWVzGAEgAygCIh0KC0RvdWJsZUFycmF5Eg4KBnZhbHVlcxgBIAMoASId", - "CgtTdHJpbmdBcnJheRIOCgZ2YWx1ZXMYASADKAkiPAoOVGltZXN0YW1wQXJy", - "YXkSKgoGdmFsdWVzGAEgAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFt", - "cCIaCghSYXdBcnJheRIOCgZ2YWx1ZXMYASADKAwiWAoOUHJvdG9jb2xTdGF0", - "dXMSNQoEY29kZRgBIAEoDjInLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9j", - "b2xTdGF0dXNDb2RlEg8KB21lc3NhZ2UYAiABKAkqoQgKDU14Q29tbWFuZEtp", - "bmQSHwobTVhfQ09NTUFORF9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYTVhfQ09N", - "TUFORF9LSU5EX1JFR0lTVEVSEAESHgoaTVhfQ09NTUFORF9LSU5EX1VOUkVH", - "SVNURVIQAhIcChhNWF9DT01NQU5EX0tJTkRfQUREX0lURU0QAxIdChlNWF9D", - "T01NQU5EX0tJTkRfQUREX0lURU0yEAQSHwobTVhfQ09NTUFORF9LSU5EX1JF", - "TU9WRV9JVEVNEAUSGgoWTVhfQ09NTUFORF9LSU5EX0FEVklTRRAGEh0KGU1Y", - "X0NPTU1BTkRfS0lORF9VTl9BRFZJU0UQBxImCiJNWF9DT01NQU5EX0tJTkRf", - "QURWSVNFX1NVUEVSVklTT1JZEAgSJQohTVhfQ09NTUFORF9LSU5EX0FERF9C", - "VUZGRVJFRF9JVEVNEAkSMAosTVhfQ09NTUFORF9LSU5EX1NFVF9CVUZGRVJF", - "RF9VUERBVEVfSU5URVJWQUwQChIbChdNWF9DT01NQU5EX0tJTkRfU1VTUEVO", - "RBALEhwKGE1YX0NPTU1BTkRfS0lORF9BQ1RJVkFURRAMEhkKFU1YX0NPTU1B", - "TkRfS0lORF9XUklURRANEhoKFk1YX0NPTU1BTkRfS0lORF9XUklURTIQDhIh", - "Ch1NWF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRBAPEiIKHk1YX0NPTU1B", - "TkRfS0lORF9XUklURV9TRUNVUkVEMhAQEiUKIU1YX0NPTU1BTkRfS0lORF9B", - "VVRIRU5USUNBVEVfVVNFUhAREigKJE1YX0NPTU1BTkRfS0lORF9BUkNIRVNU", - "UkFfVVNFUl9UT19JRBASEiEKHU1YX0NPTU1BTkRfS0lORF9BRERfSVRFTV9C", - "VUxLEBMSJAogTVhfQ09NTUFORF9LSU5EX0FEVklTRV9JVEVNX0JVTEsQFBIk", - "CiBNWF9DT01NQU5EX0tJTkRfUkVNT1ZFX0lURU1fQlVMSxAVEicKI01YX0NP", - "TU1BTkRfS0lORF9VTl9BRFZJU0VfSVRFTV9CVUxLEBYSIgoeTVhfQ09NTUFO", - "RF9LSU5EX1NVQlNDUklCRV9CVUxLEBcSJAogTVhfQ09NTUFORF9LSU5EX1VO", - "U1VCU0NSSUJFX0JVTEsQGBIYChRNWF9DT01NQU5EX0tJTkRfUElORxBkEiUK", - "IU1YX0NPTU1BTkRfS0lORF9HRVRfU0VTU0lPTl9TVEFURRBlEiMKH01YX0NP", - "TU1BTkRfS0lORF9HRVRfV09SS0VSX0lORk8QZhIgChxNWF9DT01NQU5EX0tJ", - "TkRfRFJBSU5fRVZFTlRTEGcSIwofTVhfQ09NTUFORF9LSU5EX1NIVVRET1dO", - "X1dPUktFUhBoKvkBCg1NeEV2ZW50RmFtaWx5Eh8KG01YX0VWRU5UX0ZBTUlM", - "WV9VTlNQRUNJRklFRBAAEiIKHk1YX0VWRU5UX0ZBTUlMWV9PTl9EQVRBX0NI", - "QU5HRRABEiUKIU1YX0VWRU5UX0ZBTUlMWV9PTl9XUklURV9DT01QTEVURRAC", - "EiYKIk1YX0VWRU5UX0ZBTUlMWV9PUEVSQVRJT05fQ09NUExFVEUQAxIrCidN", - "WF9FVkVOVF9GQU1JTFlfT05fQlVGRkVSRURfREFUQV9DSEFOR0UQBBInCiNN", - "WF9FVkVOVF9GQU1JTFlfT05fQUxBUk1fVFJBTlNJVElPThAFKsoBChNBbGFy", - "bVRyYW5zaXRpb25LaW5kEiUKIUFMQVJNX1RSQU5TSVRJT05fS0lORF9VTlNQ", - "RUNJRklFRBAAEh8KG0FMQVJNX1RSQU5TSVRJT05fS0lORF9SQUlTRRABEiUK", - "IUFMQVJNX1RSQU5TSVRJT05fS0lORF9BQ0tOT1dMRURHRRACEh8KG0FMQVJN", - "X1RSQU5TSVRJT05fS0lORF9DTEVBUhADEiMKH0FMQVJNX1RSQU5TSVRJT05f", - "S0lORF9SRVRSSUdHRVIQBCqqAQoTQWxhcm1Db25kaXRpb25TdGF0ZRIlCiFB", - "TEFSTV9DT05ESVRJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIgChxBTEFSTV9D", - "T05ESVRJT05fU1RBVEVfQUNUSVZFEAESJgoiQUxBUk1fQ09ORElUSU9OX1NU", - "QVRFX0FDVElWRV9BQ0tFRBACEiIKHkFMQVJNX0NPTkRJVElPTl9TVEFURV9J", - "TkFDVElWRRADKqUDChBNeFN0YXR1c0NhdGVnb3J5EiIKHk1YX1NUQVRVU19D", - "QVRFR09SWV9VTlNQRUNJRklFRBAAEh4KGk1YX1NUQVRVU19DQVRFR09SWV9V", - "TktOT1dOEAESGQoVTVhfU1RBVFVTX0NBVEVHT1JZX09LEAISHgoaTVhfU1RB", - "VFVTX0NBVEVHT1JZX1BFTkRJTkcQAxIeChpNWF9TVEFUVVNfQ0FURUdPUllf", - "V0FSTklORxAEEioKJk1YX1NUQVRVU19DQVRFR09SWV9DT01NVU5JQ0FUSU9O", - "X0VSUk9SEAUSKgomTVhfU1RBVFVTX0NBVEVHT1JZX0NPTkZJR1VSQVRJT05f", - "RVJST1IQBhIoCiRNWF9TVEFUVVNfQ0FURUdPUllfT1BFUkFUSU9OQUxfRVJS", - "T1IQBxIlCiFNWF9TVEFUVVNfQ0FURUdPUllfU0VDVVJJVFlfRVJST1IQCBIl", - "CiFNWF9TVEFUVVNfQ0FURUdPUllfU09GVFdBUkVfRVJST1IQCRIiCh5NWF9T", - "VEFUVVNfQ0FURUdPUllfT1RIRVJfRVJST1IQCirKAgoOTXhTdGF0dXNTb3Vy", - "Y2USIAocTVhfU1RBVFVTX1NPVVJDRV9VTlNQRUNJRklFRBAAEhwKGE1YX1NU", - "QVRVU19TT1VSQ0VfVU5LTk9XThABEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVR", - "VUVTVElOR19MTVgQAhIjCh9NWF9TVEFUVVNfU09VUkNFX1JFU1BPTkRJTkdf", - "TE1YEAMSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX05NWBAEEiMK", - "H01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19OTVgQBRIxCi1NWF9TVEFU", - "VVNfU09VUkNFX1JFUVVFU1RJTkdfQVVUT01BVElPTl9PQkpFQ1QQBhIxCi1N", - "WF9TVEFUVVNfU09VUkNFX1JFU1BPTkRJTkdfQVVUT01BVElPTl9PQkpFQ1QQ", - "ByrdBAoKTXhEYXRhVHlwZRIcChhNWF9EQVRBX1RZUEVfVU5TUEVDSUZJRUQQ", - "ABIYChRNWF9EQVRBX1RZUEVfVU5LTk9XThABEhgKFE1YX0RBVEFfVFlQRV9O", - "T19EQVRBEAISGAoUTVhfREFUQV9UWVBFX0JPT0xFQU4QAxIYChRNWF9EQVRB", - "X1RZUEVfSU5URUdFUhAEEhYKEk1YX0RBVEFfVFlQRV9GTE9BVBAFEhcKE01Y", - "X0RBVEFfVFlQRV9ET1VCTEUQBhIXChNNWF9EQVRBX1RZUEVfU1RSSU5HEAcS", - "FQoRTVhfREFUQV9UWVBFX1RJTUUQCBIdChlNWF9EQVRBX1RZUEVfRUxBUFNF", - "RF9USU1FEAkSHwobTVhfREFUQV9UWVBFX1JFRkVSRU5DRV9UWVBFEAoSHAoY", - "TVhfREFUQV9UWVBFX1NUQVRVU19UWVBFEAsSFQoRTVhfREFUQV9UWVBFX0VO", - "VU0QDBItCilNWF9EQVRBX1RZUEVfU0VDVVJJVFlfQ0xBU1NJRklDQVRJT05f", - "RU5VTRANEiIKHk1YX0RBVEFfVFlQRV9EQVRBX1FVQUxJVFlfVFlQRRAOEh8K", - "G01YX0RBVEFfVFlQRV9RVUFMSUZJRURfRU5VTRAPEiEKHU1YX0RBVEFfVFlQ", - "RV9RVUFMSUZJRURfU1RSVUNUEBASKQolTVhfREFUQV9UWVBFX0lOVEVSTkFU", - "SU9OQUxJWkVEX1NUUklORxAREhsKF01YX0RBVEFfVFlQRV9CSUdfU1RSSU5H", - "EBISFAoQTVhfREFUQV9UWVBFX0VORBATKqMDChJQcm90b2NvbFN0YXR1c0Nv", - "ZGUSJAogUFJPVE9DT0xfU1RBVFVTX0NPREVfVU5TUEVDSUZJRUQQABIbChdQ", - "Uk9UT0NPTF9TVEFUVVNfQ09ERV9PSxABEigKJFBST1RPQ09MX1NUQVRVU19D", - "T0RFX0lOVkFMSURfUkVRVUVTVBACEioKJlBST1RPQ09MX1NUQVRVU19DT0RF", - "X1NFU1NJT05fTk9UX0ZPVU5EEAMSKgomUFJPVE9DT0xfU1RBVFVTX0NPREVf", - "U0VTU0lPTl9OT1RfUkVBRFkQBBIrCidQUk9UT0NPTF9TVEFUVVNfQ09ERV9X", - "T1JLRVJfVU5BVkFJTEFCTEUQBRIgChxQUk9UT0NPTF9TVEFUVVNfQ09ERV9U", - "SU1FT1VUEAYSIQodUFJPVE9DT0xfU1RBVFVTX0NPREVfQ0FOQ0VMRUQQBxIr", - "CidQUk9UT0NPTF9TVEFUVVNfQ09ERV9QUk9UT0NPTF9WSU9MQVRJT04QCBIp", - "CiVQUk9UT0NPTF9TVEFUVVNfQ09ERV9NWEFDQ0VTU19GQUlMVVJFEAkqvwIK", - "DFNlc3Npb25TdGF0ZRIdChlTRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAAS", - "GgoWU0VTU0lPTl9TVEFURV9DUkVBVElORxABEiEKHVNFU1NJT05fU1RBVEVf", - "U1RBUlRJTkdfV09SS0VSEAISIgoeU0VTU0lPTl9TVEFURV9XQUlUSU5HX0ZP", - "Ul9QSVBFEAMSHQoZU0VTU0lPTl9TVEFURV9IQU5EU0hBS0lORxAEEiUKIVNF", - "U1NJT05fU1RBVEVfSU5JVElBTElaSU5HX1dPUktFUhAFEhcKE1NFU1NJT05f", - "U1RBVEVfUkVBRFkQBhIZChVTRVNTSU9OX1NUQVRFX0NMT1NJTkcQBxIYChRT", - "RVNTSU9OX1NUQVRFX0NMT1NFRBAIEhkKFVNFU1NJT05fU1RBVEVfRkFVTFRF", - "RBAJMuAECg9NeEFjY2Vzc0dhdGV3YXkSXQoLT3BlblNlc3Npb24SJy5teGFj", - "Y2Vzc19nYXRld2F5LnYxLk9wZW5TZXNzaW9uUmVxdWVzdBolLm14YWNjZXNz", - "X2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXBseRJgCgxDbG9zZVNlc3Npb24S", - "KC5teGFjY2Vzc19nYXRld2F5LnYxLkNsb3NlU2Vzc2lvblJlcXVlc3QaJi5t", - "eGFjY2Vzc19nYXRld2F5LnYxLkNsb3NlU2Vzc2lvblJlcGx5ElQKBkludm9r", - "ZRIlLm14YWNjZXNzX2dhdGV3YXkudjEuTXhDb21tYW5kUmVxdWVzdBojLm14", - "YWNjZXNzX2dhdGV3YXkudjEuTXhDb21tYW5kUmVwbHkSWAoMU3RyZWFtRXZl", - "bnRzEigubXhhY2Nlc3NfZ2F0ZXdheS52MS5TdHJlYW1FdmVudHNSZXF1ZXN0", - "GhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEV2ZW50MAESbAoQQWNrbm93bGVk", - "Z2VBbGFybRIsLm14YWNjZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFy", - "bVJlcXVlc3QaKi5teGFjY2Vzc19nYXRld2F5LnYxLkFja25vd2xlZGdlQWxh", - "cm1SZXBseRJuChFRdWVyeUFjdGl2ZUFsYXJtcxItLm14YWNjZXNzX2dhdGV3", - "YXkudjEuUXVlcnlBY3RpdmVBbGFybXNSZXF1ZXN0GigubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5BY3RpdmVBbGFybVNuYXBzaG90MAFCHKoCGU14R2F0ZXdheS5D", - "b250cmFjdHMuUHJvdG9iBnByb3RvMw==")); + "IngKDFdyaXRlQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0", + "ZW1faGFuZGxlGAIgASgFEisKBXZhbHVlGAMgASgLMhwubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5NeFZhbHVlEg8KB3VzZXJfaWQYBCABKAUisAEKDVdyaXRlMkNv", + "bW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgC", + "IAEoBRIrCgV2YWx1ZRgDIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhW", + "YWx1ZRI1Cg90aW1lc3RhbXBfdmFsdWUYBCABKAsyHC5teGFjY2Vzc19nYXRl", + "d2F5LnYxLk14VmFsdWUSDwoHdXNlcl9pZBgFIAEoBSKhAQoTV3JpdGVTZWN1", + "cmVkQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFu", + "ZGxlGAIgASgFEhcKD2N1cnJlbnRfdXNlcl9pZBgDIAEoBRIYChB2ZXJpZmll", + "cl91c2VyX2lkGAQgASgFEisKBXZhbHVlGAUgASgLMhwubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5NeFZhbHVlItkBChRXcml0ZVNlY3VyZWQyQ29tbWFuZBIVCg1z", + "ZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFEhcKD2N1", + "cnJlbnRfdXNlcl9pZBgDIAEoBRIYChB2ZXJpZmllcl91c2VyX2lkGAQgASgF", + "EisKBXZhbHVlGAUgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVl", + "EjUKD3RpbWVzdGFtcF92YWx1ZRgGIAEoCzIcLm14YWNjZXNzX2dhdGV3YXku", + "djEuTXhWYWx1ZSJjChdBdXRoZW50aWNhdGVVc2VyQ29tbWFuZBIVCg1zZXJ2", + "ZXJfaGFuZGxlGAEgASgFEhMKC3ZlcmlmeV91c2VyGAIgASgJEhwKFHZlcmlm", + "eV91c2VyX3Bhc3N3b3JkGAMgASgJIkcKGEFyY2hlc3RyQVVzZXJUb0lkQ29t", + "bWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDHVzZXJfaWRfZ3VpZBgC", + "IAEoCSJCChJBZGRJdGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB", + "IAEoBRIVCg10YWdfYWRkcmVzc2VzGAIgAygJIkQKFUFkdmlzZUl0ZW1CdWxr", + "Q29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0ZW1faGFuZGxl", + "cxgCIAMoBSJEChVSZW1vdmVJdGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hh", + "bmRsZRgBIAEoBRIUCgxpdGVtX2hhbmRsZXMYAiADKAUiRgoXVW5BZHZpc2VJ", + "dGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgxpdGVt", + "X2hhbmRsZXMYAiADKAUiRAoUU3Vic2NyaWJlQnVsa0NvbW1hbmQSFQoNc2Vy", + "dmVyX2hhbmRsZRgBIAEoBRIVCg10YWdfYWRkcmVzc2VzGAIgAygJIjkKFlN1", + "YnNjcmliZUFsYXJtc0NvbW1hbmQSHwoXc3Vic2NyaXB0aW9uX2V4cHJlc3Np", + "b24YASABKAkiGgoYVW5zdWJzY3JpYmVBbGFybXNDb21tYW5kIqEBChdBY2tu", + "b3dsZWRnZUFsYXJtQ29tbWFuZBISCgphbGFybV9ndWlkGAEgASgJEg8KB2Nv", + "bW1lbnQYAiABKAkSFQoNb3BlcmF0b3JfdXNlchgDIAEoCRIVCg1vcGVyYXRv", + "cl9ub2RlGAQgASgJEhcKD29wZXJhdG9yX2RvbWFpbhgFIAEoCRIaChJvcGVy", + "YXRvcl9mdWxsX25hbWUYBiABKAkiNwoYUXVlcnlBY3RpdmVBbGFybXNDb21t", + "YW5kEhsKE2FsYXJtX2ZpbHRlcl9wcmVmaXgYASABKAkiRQoWVW5zdWJzY3Jp", + "YmVCdWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0ZW1f", + "aGFuZGxlcxgCIAMoBSIeCgtQaW5nQ29tbWFuZBIPCgdtZXNzYWdlGAEgASgJ", + "IhgKFkdldFNlc3Npb25TdGF0ZUNvbW1hbmQiFgoUR2V0V29ya2VySW5mb0Nv", + "bW1hbmQiKAoSRHJhaW5FdmVudHNDb21tYW5kEhIKCm1heF9ldmVudHMYASAB", + "KA0iSAoVU2h1dGRvd25Xb3JrZXJDb21tYW5kEi8KDGdyYWNlX3BlcmlvZBgB", + "IAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiLPDAoOTXhDb21tYW5k", + "UmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb3JyZWxhdGlvbl9pZBgC", + "IAEoCRIwCgRraW5kGAMgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENv", + "bW1hbmRLaW5kEjwKD3Byb3RvY29sX3N0YXR1cxgEIAEoCzIjLm14YWNjZXNz", + "X2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0dXMSFAoHaHJlc3VsdBgFIAEoBUgB", + "iAEBEjIKDHJldHVybl92YWx1ZRgGIAEoCzIcLm14YWNjZXNzX2dhdGV3YXku", + "djEuTXhWYWx1ZRI0CghzdGF0dXNlcxgHIAMoCzIiLm14YWNjZXNzX2dhdGV3", + "YXkudjEuTXhTdGF0dXNQcm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UYCCAB", + "KAkSNgoIcmVnaXN0ZXIYFCABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLlJl", + "Z2lzdGVyUmVwbHlIABI1CghhZGRfaXRlbRgVIAEoCzIhLm14YWNjZXNzX2dh", + "dGV3YXkudjEuQWRkSXRlbVJlcGx5SAASNwoJYWRkX2l0ZW0yGBYgASgLMiIu", + "bXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRJdGVtMlJlcGx5SAASRgoRYWRkX2J1", + "ZmZlcmVkX2l0ZW0YFyABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEJ1", + "ZmZlcmVkSXRlbVJlcGx5SAASNAoHc3VzcGVuZBgYIAEoCzIhLm14YWNjZXNz", + "X2dhdGV3YXkudjEuU3VzcGVuZFJlcGx5SAASNgoIYWN0aXZhdGUYGSABKAsy", + "Ii5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2YXRlUmVwbHlIABJHChFhdXRo", + "ZW50aWNhdGVfdXNlchgaIAEoCzIqLm14YWNjZXNzX2dhdGV3YXkudjEuQXV0", + "aGVudGljYXRlVXNlclJlcGx5SAASSwoUYXJjaGVzdHJhX3VzZXJfdG9faWQY", + "GyABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkFyY2hlc3RyQVVzZXJUb0lk", + "UmVwbHlIABJACg1hZGRfaXRlbV9idWxrGBwgASgLMicubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJDChBhZHZpc2VfaXRlbV9i", + "dWxrGB0gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxrU3Vic2NyaWJl", + "UmVwbHlIABJDChByZW1vdmVfaXRlbV9idWxrGB4gASgLMicubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJGChN1bl9hZHZpc2Vf", + "aXRlbV9idWxrGB8gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxrU3Vi", + "c2NyaWJlUmVwbHlIABJBCg5zdWJzY3JpYmVfYnVsaxggIAEoCzInLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuQnVsa1N1YnNjcmliZVJlcGx5SAASQwoQdW5zdWJz", + "Y3JpYmVfYnVsaxghIAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuQnVsa1N1", + "YnNjcmliZVJlcGx5SAASTgoRYWNrbm93bGVkZ2VfYWxhcm0YIiABKAsyMS5t", + "eGFjY2Vzc19nYXRld2F5LnYxLkFja25vd2xlZGdlQWxhcm1SZXBseVBheWxv", + "YWRIABJRChNxdWVyeV9hY3RpdmVfYWxhcm1zGCMgASgLMjIubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcGx5UGF5bG9hZEgAEj8K", + "DXNlc3Npb25fc3RhdGUYZCABKAsyJi5teGFjY2Vzc19nYXRld2F5LnYxLlNl", + "c3Npb25TdGF0ZVJlcGx5SAASOwoLd29ya2VyX2luZm8YZSABKAsyJC5teGFj", + "Y2Vzc19nYXRld2F5LnYxLldvcmtlckluZm9SZXBseUgAEj0KDGRyYWluX2V2", + "ZW50cxhmIAEoCzIlLm14YWNjZXNzX2dhdGV3YXkudjEuRHJhaW5FdmVudHNS", + "ZXBseUgAQgkKB3BheWxvYWRCCgoIX2hyZXN1bHQiJgoNUmVnaXN0ZXJSZXBs", + "eRIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFIiMKDEFkZEl0ZW1SZXBseRITCgtp", + "dGVtX2hhbmRsZRgBIAEoBSIkCg1BZGRJdGVtMlJlcGx5EhMKC2l0ZW1faGFu", + "ZGxlGAEgASgFIisKFEFkZEJ1ZmZlcmVkSXRlbVJlcGx5EhMKC2l0ZW1faGFu", + "ZGxlGAEgASgFIkIKDFN1c3BlbmRSZXBseRIyCgZzdGF0dXMYASABKAsyIi5t", + "eGFjY2Vzc19nYXRld2F5LnYxLk14U3RhdHVzUHJveHkiQwoNQWN0aXZhdGVS", + "ZXBseRIyCgZzdGF0dXMYASABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14", + "U3RhdHVzUHJveHkiKAoVQXV0aGVudGljYXRlVXNlclJlcGx5Eg8KB3VzZXJf", + "aWQYASABKAUiKQoWQXJjaGVzdHJBVXNlclRvSWRSZXBseRIPCgd1c2VyX2lk", + "GAEgASgFIoEBCg9TdWJzY3JpYmVSZXN1bHQSFQoNc2VydmVyX2hhbmRsZRgB", + "IAEoBRITCgt0YWdfYWRkcmVzcxgCIAEoCRITCgtpdGVtX2hhbmRsZRgDIAEo", + "BRIWCg53YXNfc3VjY2Vzc2Z1bBgEIAEoCBIVCg1lcnJvcl9tZXNzYWdlGAUg", + "ASgJIksKEkJ1bGtTdWJzY3JpYmVSZXBseRI1CgdyZXN1bHRzGAEgAygLMiQu", + "bXhhY2Nlc3NfZ2F0ZXdheS52MS5TdWJzY3JpYmVSZXN1bHQiRQoRU2Vzc2lv", + "blN0YXRlUmVwbHkSMAoFc3RhdGUYASABKA4yIS5teGFjY2Vzc19nYXRld2F5", + "LnYxLlNlc3Npb25TdGF0ZSJ1Cg9Xb3JrZXJJbmZvUmVwbHkSGQoRd29ya2Vy", + "X3Byb2Nlc3NfaWQYASABKAUSFgoOd29ya2VyX3ZlcnNpb24YAiABKAkSFwoP", + "bXhhY2Nlc3NfcHJvZ2lkGAMgASgJEhYKDm14YWNjZXNzX2Nsc2lkGAQgASgJ", + "IkAKEERyYWluRXZlbnRzUmVwbHkSLAoGZXZlbnRzGAEgAygLMhwubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5NeEV2ZW50IjUKHEFja25vd2xlZGdlQWxhcm1SZXBs", + "eVBheWxvYWQSFQoNbmF0aXZlX3N0YXR1cxgBIAEoBSJcCh1RdWVyeUFjdGl2", + "ZUFsYXJtc1JlcGx5UGF5bG9hZBI7CglzbmFwc2hvdHMYASADKAsyKC5teGFj", + "Y2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3Qi5wYKB014RXZl", + "bnQSMgoGZmFtaWx5GAEgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEV2", + "ZW50RmFtaWx5EhIKCnNlc3Npb25faWQYAiABKAkSFQoNc2VydmVyX2hhbmRs", + "ZRgDIAEoBRITCgtpdGVtX2hhbmRsZRgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIc", + "Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgdxdWFsaXR5GAYgASgF", + "EjQKEHNvdXJjZV90aW1lc3RhbXAYByABKAsyGi5nb29nbGUucHJvdG9idWYu", + "VGltZXN0YW1wEjQKCHN0YXR1c2VzGAggAygLMiIubXhhY2Nlc3NfZ2F0ZXdh", + "eS52MS5NeFN0YXR1c1Byb3h5EhcKD3dvcmtlcl9zZXF1ZW5jZRgJIAEoBBI0", + "ChB3b3JrZXJfdGltZXN0YW1wGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRp", + "bWVzdGFtcBI9ChlnYXRld2F5X3JlY2VpdmVfdGltZXN0YW1wGAsgASgLMhou", + "Z29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIUCgdocmVzdWx0GAwgASgFSAGI", + "AQESEgoKcmF3X3N0YXR1cxgNIAEoCRJACg5vbl9kYXRhX2NoYW5nZRgUIAEo", + "CzImLm14YWNjZXNzX2dhdGV3YXkudjEuT25EYXRhQ2hhbmdlRXZlbnRIABJG", + "ChFvbl93cml0ZV9jb21wbGV0ZRgVIAEoCzIpLm14YWNjZXNzX2dhdGV3YXku", + "djEuT25Xcml0ZUNvbXBsZXRlRXZlbnRIABJJChJvcGVyYXRpb25fY29tcGxl", + "dGUYFiABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLk9wZXJhdGlvbkNvbXBs", + "ZXRlRXZlbnRIABJRChdvbl9idWZmZXJlZF9kYXRhX2NoYW5nZRgXIAEoCzIu", + "Lm14YWNjZXNzX2dhdGV3YXkudjEuT25CdWZmZXJlZERhdGFDaGFuZ2VFdmVu", + "dEgAEkoKE29uX2FsYXJtX3RyYW5zaXRpb24YGCABKAsyKy5teGFjY2Vzc19n", + "YXRld2F5LnYxLk9uQWxhcm1UcmFuc2l0aW9uRXZlbnRIAEIGCgRib2R5QgoK", + "CF9ocmVzdWx0IhMKEU9uRGF0YUNoYW5nZUV2ZW50IhYKFE9uV3JpdGVDb21w", + "bGV0ZUV2ZW50IhgKFk9wZXJhdGlvbkNvbXBsZXRlRXZlbnQi1AEKGU9uQnVm", + "ZmVyZWREYXRhQ2hhbmdlRXZlbnQSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhh", + "Y2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEjQKDnF1YWxpdHlfdmFsdWVz", + "GAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EjYKEHRpbWVz", + "dGFtcF92YWx1ZXMYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJy", + "YXkSFQoNcmF3X2RhdGFfdHlwZRgEIAEoBSL9AwoWT25BbGFybVRyYW5zaXRp", + "b25FdmVudBIcChRhbGFybV9mdWxsX3JlZmVyZW5jZRgBIAEoCRIfChdzb3Vy", + "Y2Vfb2JqZWN0X3JlZmVyZW5jZRgCIAEoCRIXCg9hbGFybV90eXBlX25hbWUY", + "AyABKAkSQQoPdHJhbnNpdGlvbl9raW5kGAQgASgOMigubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5BbGFybVRyYW5zaXRpb25LaW5kEhAKCHNldmVyaXR5GAUgASgF", + "EjwKGG9yaWdpbmFsX3JhaXNlX3RpbWVzdGFtcBgGIAEoCzIaLmdvb2dsZS5w", + "cm90b2J1Zi5UaW1lc3RhbXASOAoUdHJhbnNpdGlvbl90aW1lc3RhbXAYByAB", + "KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJhdG9yX3Vz", + "ZXIYCCABKAkSGAoQb3BlcmF0b3JfY29tbWVudBgJIAEoCRIQCghjYXRlZ29y", + "eRgKIAEoCRITCgtkZXNjcmlwdGlvbhgLIAEoCRIzCg1jdXJyZW50X3ZhbHVl", + "GAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEjEKC2xpbWl0", + "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIv0D", + "ChNBY3RpdmVBbGFybVNuYXBzaG90EhwKFGFsYXJtX2Z1bGxfcmVmZXJlbmNl", + "GAEgASgJEh8KF3NvdXJjZV9vYmplY3RfcmVmZXJlbmNlGAIgASgJEhcKD2Fs", + "YXJtX3R5cGVfbmFtZRgDIAEoCRIQCghzZXZlcml0eRgEIAEoBRI8Chhvcmln", + "aW5hbF9yYWlzZV90aW1lc3RhbXAYBSABKAsyGi5nb29nbGUucHJvdG9idWYu", + "VGltZXN0YW1wEj8KDWN1cnJlbnRfc3RhdGUYBiABKA4yKC5teGFjY2Vzc19n", + "YXRld2F5LnYxLkFsYXJtQ29uZGl0aW9uU3RhdGUSEAoIY2F0ZWdvcnkYByAB", + "KAkSEwoLZGVzY3JpcHRpb24YCCABKAkSPQoZbGFzdF90cmFuc2l0aW9uX3Rp", + "bWVzdGFtcBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoN", + "b3BlcmF0b3JfdXNlchgKIAEoCRIYChBvcGVyYXRvcl9jb21tZW50GAsgASgJ", + "EjMKDWN1cnJlbnRfdmFsdWUYDCABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYx", + "Lk14VmFsdWUSMQoLbGltaXRfdmFsdWUYDSABKAsyHC5teGFjY2Vzc19nYXRl", + "d2F5LnYxLk14VmFsdWUikgEKF0Fja25vd2xlZGdlQWxhcm1SZXF1ZXN0EhIK", + "CnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50X2NvcnJlbGF0aW9uX2lkGAIg", + "ASgJEhwKFGFsYXJtX2Z1bGxfcmVmZXJlbmNlGAMgASgJEg8KB2NvbW1lbnQY", + "BCABKAkSFQoNb3BlcmF0b3JfdXNlchgFIAEoCSLzAQoVQWNrbm93bGVkZ2VB", + "bGFybVJlcGx5EhIKCnNlc3Npb25faWQYASABKAkSFgoOY29ycmVsYXRpb25f", + "aWQYAiABKAkSPAoPcHJvdG9jb2xfc3RhdHVzGAMgASgLMiMubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0GAQgASgFSACI", + "AQESMgoGc3RhdHVzGAUgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0", + "YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVzc2FnZRgGIAEoCUIKCghfaHJl", + "c3VsdCJqChhRdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QSEgoKc2Vzc2lvbl9p", + "ZBgBIAEoCRIdChVjbGllbnRfY29ycmVsYXRpb25faWQYAiABKAkSGwoTYWxh", + "cm1fZmlsdGVyX3ByZWZpeBgDIAEoCSLrAQoNTXhTdGF0dXNQcm94eRIPCgdz", + "dWNjZXNzGAEgASgFEjcKCGNhdGVnb3J5GAIgASgOMiUubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5NeFN0YXR1c0NhdGVnb3J5EjgKC2RldGVjdGVkX2J5GAMgASgO", + "MiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0YXR1c1NvdXJjZRIOCgZkZXRh", + "aWwYBCABKAUSFAoMcmF3X2NhdGVnb3J5GAUgASgFEhcKD3Jhd19kZXRlY3Rl", + "ZF9ieRgGIAEoBRIXCg9kaWFnbm9zdGljX3RleHQYByABKAkipwMKB014VmFs", + "dWUSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5N", + "eERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIPCgdpc19udWxsGAMg", + "ASgIEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEhUKDXJhd19kYXRhX3R5cGUY", + "BSABKAUSFAoKYm9vbF92YWx1ZRgKIAEoCEgAEhUKC2ludDMyX3ZhbHVlGAsg", + "ASgFSAASFQoLaW50NjRfdmFsdWUYDCABKANIABIVCgtmbG9hdF92YWx1ZRgN", + "IAEoAkgAEhYKDGRvdWJsZV92YWx1ZRgOIAEoAUgAEhYKDHN0cmluZ192YWx1", + "ZRgPIAEoCUgAEjUKD3RpbWVzdGFtcF92YWx1ZRgQIAEoCzIaLmdvb2dsZS5w", + "cm90b2J1Zi5UaW1lc3RhbXBIABIzCgthcnJheV92YWx1ZRgRIAEoCzIcLm14", + "YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheUgAEhMKCXJhd192YWx1ZRgSIAEo", + "DEgAQgYKBGtpbmQi/gQKB014QXJyYXkSOgoRZWxlbWVudF9kYXRhX3R5cGUY", + "ASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUSFAoMdmFy", + "aWFudF90eXBlGAIgASgJEhIKCmRpbWVuc2lvbnMYAyADKA0SFgoOcmF3X2Rp", + "YWdub3N0aWMYBCABKAkSHQoVcmF3X2VsZW1lbnRfZGF0YV90eXBlGAUgASgF", + "EjUKC2Jvb2xfdmFsdWVzGAogASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52MS5C", + "b29sQXJyYXlIABI3CgxpbnQzMl92YWx1ZXMYCyABKAsyHy5teGFjY2Vzc19n", + "YXRld2F5LnYxLkludDMyQXJyYXlIABI3CgxpbnQ2NF92YWx1ZXMYDCABKAsy", + "Hy5teGFjY2Vzc19nYXRld2F5LnYxLkludDY0QXJyYXlIABI3CgxmbG9hdF92", + "YWx1ZXMYDSABKAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkZsb2F0QXJyYXlI", + "ABI5Cg1kb3VibGVfdmFsdWVzGA4gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5Eb3VibGVBcnJheUgAEjkKDXN0cmluZ192YWx1ZXMYDyABKAsyIC5teGFj", + "Y2Vzc19nYXRld2F5LnYxLlN0cmluZ0FycmF5SAASPwoQdGltZXN0YW1wX3Zh", + "bHVlcxgQIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuVGltZXN0YW1wQXJy", + "YXlIABIzCgpyYXdfdmFsdWVzGBEgASgLMh0ubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5SYXdBcnJheUgAQggKBnZhbHVlcyIbCglCb29sQXJyYXkSDgoGdmFsdWVz", + "GAEgAygIIhwKCkludDMyQXJyYXkSDgoGdmFsdWVzGAEgAygFIhwKCkludDY0", + "QXJyYXkSDgoGdmFsdWVzGAEgAygDIhwKCkZsb2F0QXJyYXkSDgoGdmFsdWVz", + "GAEgAygCIh0KC0RvdWJsZUFycmF5Eg4KBnZhbHVlcxgBIAMoASIdCgtTdHJp", + "bmdBcnJheRIOCgZ2YWx1ZXMYASADKAkiPAoOVGltZXN0YW1wQXJyYXkSKgoG", + "dmFsdWVzGAEgAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIaCghS", + "YXdBcnJheRIOCgZ2YWx1ZXMYASADKAwiWAoOUHJvdG9jb2xTdGF0dXMSNQoE", + "Y29kZRgBIAEoDjInLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0", + "dXNDb2RlEg8KB21lc3NhZ2UYAiABKAkqvwkKDU14Q29tbWFuZEtpbmQSHwob", + "TVhfQ09NTUFORF9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYTVhfQ09NTUFORF9L", + "SU5EX1JFR0lTVEVSEAESHgoaTVhfQ09NTUFORF9LSU5EX1VOUkVHSVNURVIQ", + "AhIcChhNWF9DT01NQU5EX0tJTkRfQUREX0lURU0QAxIdChlNWF9DT01NQU5E", + "X0tJTkRfQUREX0lURU0yEAQSHwobTVhfQ09NTUFORF9LSU5EX1JFTU9WRV9J", + "VEVNEAUSGgoWTVhfQ09NTUFORF9LSU5EX0FEVklTRRAGEh0KGU1YX0NPTU1B", + "TkRfS0lORF9VTl9BRFZJU0UQBxImCiJNWF9DT01NQU5EX0tJTkRfQURWSVNF", + "X1NVUEVSVklTT1JZEAgSJQohTVhfQ09NTUFORF9LSU5EX0FERF9CVUZGRVJF", + "RF9JVEVNEAkSMAosTVhfQ09NTUFORF9LSU5EX1NFVF9CVUZGRVJFRF9VUERB", + "VEVfSU5URVJWQUwQChIbChdNWF9DT01NQU5EX0tJTkRfU1VTUEVORBALEhwK", + "GE1YX0NPTU1BTkRfS0lORF9BQ1RJVkFURRAMEhkKFU1YX0NPTU1BTkRfS0lO", + "RF9XUklURRANEhoKFk1YX0NPTU1BTkRfS0lORF9XUklURTIQDhIhCh1NWF9D", + "T01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRBAPEiIKHk1YX0NPTU1BTkRfS0lO", + "RF9XUklURV9TRUNVUkVEMhAQEiUKIU1YX0NPTU1BTkRfS0lORF9BVVRIRU5U", + "SUNBVEVfVVNFUhAREigKJE1YX0NPTU1BTkRfS0lORF9BUkNIRVNUUkFfVVNF", + "Ul9UT19JRBASEiEKHU1YX0NPTU1BTkRfS0lORF9BRERfSVRFTV9CVUxLEBMS", + "JAogTVhfQ09NTUFORF9LSU5EX0FEVklTRV9JVEVNX0JVTEsQFBIkCiBNWF9D", + "T01NQU5EX0tJTkRfUkVNT1ZFX0lURU1fQlVMSxAVEicKI01YX0NPTU1BTkRf", + "S0lORF9VTl9BRFZJU0VfSVRFTV9CVUxLEBYSIgoeTVhfQ09NTUFORF9LSU5E", + "X1NVQlNDUklCRV9CVUxLEBcSJAogTVhfQ09NTUFORF9LSU5EX1VOU1VCU0NS", + "SUJFX0JVTEsQGBIkCiBNWF9DT01NQU5EX0tJTkRfU1VCU0NSSUJFX0FMQVJN", + "UxAZEiYKIk1YX0NPTU1BTkRfS0lORF9VTlNVQlNDUklCRV9BTEFSTVMQGhIl", + "CiFNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVER0VfQUxBUk0QGxInCiNNWF9D", + "T01NQU5EX0tJTkRfUVVFUllfQUNUSVZFX0FMQVJNUxAcEhgKFE1YX0NPTU1B", + "TkRfS0lORF9QSU5HEGQSJQohTVhfQ09NTUFORF9LSU5EX0dFVF9TRVNTSU9O", + "X1NUQVRFEGUSIwofTVhfQ09NTUFORF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBm", + "EiAKHE1YX0NPTU1BTkRfS0lORF9EUkFJTl9FVkVOVFMQZxIjCh9NWF9DT01N", + "QU5EX0tJTkRfU0hVVERPV05fV09SS0VSEGgq+QEKDU14RXZlbnRGYW1pbHkS", + "HwobTVhfRVZFTlRfRkFNSUxZX1VOU1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRf", + "RkFNSUxZX09OX0RBVEFfQ0hBTkdFEAESJQohTVhfRVZFTlRfRkFNSUxZX09O", + "X1dSSVRFX0NPTVBMRVRFEAISJgoiTVhfRVZFTlRfRkFNSUxZX09QRVJBVElP", + "Tl9DT01QTEVURRADEisKJ01YX0VWRU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9E", + "QVRBX0NIQU5HRRAEEicKI01YX0VWRU5UX0ZBTUlMWV9PTl9BTEFSTV9UUkFO", + "U0lUSU9OEAUqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJB", + "TlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElP", + "Tl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05P", + "V0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwof", + "QUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNv", + "bmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJ", + "RklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJB", + "TEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1f", + "Q09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdv", + "cnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhf", + "U1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdP", + "UllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVORElORxADEh4KGk1Y", + "X1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVH", + "T1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdP", + "UllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09S", + "WV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9T", + "RUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FS", + "RV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAK", + "KsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BF", + "Q0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhf", + "U1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19T", + "T1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JF", + "UVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5H", + "X05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFU", + "SU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19B", + "VVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFf", + "VFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAES", + "GAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9P", + "TEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9U", + "WVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RB", + "VEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1Y", + "X0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVG", + "RVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIV", + "ChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklU", + "WV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFf", + "UVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVN", + "EA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9E", + "QVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5HEBESGwoXTVhfREFU", + "QV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMK", + "ElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9V", + "TlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAok", + "UFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJP", + "VE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9U", + "T0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RP", + "Q09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RP", + "Q09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNf", + "Q09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RP", + "Q09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUND", + "RVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RB", + "VEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAES", + "IQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9O", + "X1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hB", + "TkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09S", + "S0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RB", + "VEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VT", + "U0lPTl9TVEFURV9GQVVMVEVEEAky4AQKD014QWNjZXNzR2F0ZXdheRJdCgtP", + "cGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25S", + "ZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5", + "EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VT", + "ZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNz", + "aW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENv", + "bW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRS", + "ZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0", + "cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZl", + "bnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXku", + "djEuQWNrbm93bGVkZ2VBbGFybVJlcGx5Em4KEVF1ZXJ5QWN0aXZlQWxhcm1z", + "Ei0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVl", + "c3QaKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3Qw", + "AUIcqgIZTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90b2IGcHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] { @@ -398,7 +422,7 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionReply), global::MxGateway.Contracts.Proto.CloseSessionReply.Parser, new[]{ "SessionId", "FinalState", "ProtocolStatus" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamEventsRequest), global::MxGateway.Contracts.Proto.StreamEventsRequest.Parser, new[]{ "SessionId", "AfterWorkerSequence" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommandRequest), global::MxGateway.Contracts.Proto.MxCommandRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "Command" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommand), global::MxGateway.Contracts.Proto.MxCommand.Parser, new[]{ "Kind", "Register", "Unregister", "AddItem", "AddItem2", "RemoveItem", "Advise", "UnAdvise", "AdviseSupervisory", "AddBufferedItem", "SetBufferedUpdateInterval", "Suspend", "Activate", "Write", "Write2", "WriteSecured", "WriteSecured2", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "Ping", "GetSessionState", "GetWorkerInfo", "DrainEvents", "ShutdownWorker" }, new[]{ "Payload" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommand), global::MxGateway.Contracts.Proto.MxCommand.Parser, new[]{ "Kind", "Register", "Unregister", "AddItem", "AddItem2", "RemoveItem", "Advise", "UnAdvise", "AdviseSupervisory", "AddBufferedItem", "SetBufferedUpdateInterval", "Suspend", "Activate", "Write", "Write2", "WriteSecured", "WriteSecured2", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "SubscribeAlarms", "UnsubscribeAlarms", "AcknowledgeAlarmCommand", "QueryActiveAlarmsCommand", "Ping", "GetSessionState", "GetWorkerInfo", "DrainEvents", "ShutdownWorker" }, new[]{ "Payload" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.RegisterCommand), global::MxGateway.Contracts.Proto.RegisterCommand.Parser, new[]{ "ClientName" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnregisterCommand), global::MxGateway.Contracts.Proto.UnregisterCommand.Parser, new[]{ "ServerHandle" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AddItemCommand), global::MxGateway.Contracts.Proto.AddItemCommand.Parser, new[]{ "ServerHandle", "ItemDefinition" }, null, null, null, null), @@ -422,13 +446,17 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.RemoveItemBulkCommand), global::MxGateway.Contracts.Proto.RemoveItemBulkCommand.Parser, new[]{ "ServerHandle", "ItemHandles" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnAdviseItemBulkCommand), global::MxGateway.Contracts.Proto.UnAdviseItemBulkCommand.Parser, new[]{ "ServerHandle", "ItemHandles" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.SubscribeBulkCommand), global::MxGateway.Contracts.Proto.SubscribeBulkCommand.Parser, new[]{ "ServerHandle", "TagAddresses" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand), global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand.Parser, new[]{ "SubscriptionExpression" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand), global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand), global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand.Parser, new[]{ "AlarmGuid", "Comment", "OperatorUser", "OperatorNode", "OperatorDomain", "OperatorFullName" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand), global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand.Parser, new[]{ "AlarmFilterPrefix" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnsubscribeBulkCommand), global::MxGateway.Contracts.Proto.UnsubscribeBulkCommand.Parser, new[]{ "ServerHandle", "ItemHandles" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.PingCommand), global::MxGateway.Contracts.Proto.PingCommand.Parser, new[]{ "Message" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.GetSessionStateCommand), global::MxGateway.Contracts.Proto.GetSessionStateCommand.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.GetWorkerInfoCommand), global::MxGateway.Contracts.Proto.GetWorkerInfoCommand.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.DrainEventsCommand), global::MxGateway.Contracts.Proto.DrainEventsCommand.Parser, new[]{ "MaxEvents" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.ShutdownWorkerCommand), global::MxGateway.Contracts.Proto.ShutdownWorkerCommand.Parser, new[]{ "GracePeriod" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommandReply), global::MxGateway.Contracts.Proto.MxCommandReply.Parser, new[]{ "SessionId", "CorrelationId", "Kind", "ProtocolStatus", "Hresult", "ReturnValue", "Statuses", "DiagnosticMessage", "Register", "AddItem", "AddItem2", "AddBufferedItem", "Suspend", "Activate", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "SessionState", "WorkerInfo", "DrainEvents" }, new[]{ "Payload", "Hresult" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommandReply), global::MxGateway.Contracts.Proto.MxCommandReply.Parser, new[]{ "SessionId", "CorrelationId", "Kind", "ProtocolStatus", "Hresult", "ReturnValue", "Statuses", "DiagnosticMessage", "Register", "AddItem", "AddItem2", "AddBufferedItem", "Suspend", "Activate", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "AcknowledgeAlarm", "QueryActiveAlarms", "SessionState", "WorkerInfo", "DrainEvents" }, new[]{ "Payload", "Hresult" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.RegisterReply), global::MxGateway.Contracts.Proto.RegisterReply.Parser, new[]{ "ServerHandle" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AddItemReply), global::MxGateway.Contracts.Proto.AddItemReply.Parser, new[]{ "ItemHandle" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AddItem2Reply), global::MxGateway.Contracts.Proto.AddItem2Reply.Parser, new[]{ "ItemHandle" }, null, null, null, null), @@ -442,6 +470,8 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.SessionStateReply), global::MxGateway.Contracts.Proto.SessionStateReply.Parser, new[]{ "State" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.WorkerInfoReply), global::MxGateway.Contracts.Proto.WorkerInfoReply.Parser, new[]{ "WorkerProcessId", "WorkerVersion", "MxaccessProgid", "MxaccessClsid" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.DrainEventsReply), global::MxGateway.Contracts.Proto.DrainEventsReply.Parser, new[]{ "Events" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload), global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload.Parser, new[]{ "NativeStatus" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload), global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload.Parser, new[]{ "Snapshots" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxEvent), global::MxGateway.Contracts.Proto.MxEvent.Parser, new[]{ "Family", "SessionId", "ServerHandle", "ItemHandle", "Value", "Quality", "SourceTimestamp", "Statuses", "WorkerSequence", "WorkerTimestamp", "GatewayReceiveTimestamp", "Hresult", "RawStatus", "OnDataChange", "OnWriteComplete", "OperationComplete", "OnBufferedDataChange", "OnAlarmTransition" }, new[]{ "Body", "Hresult" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnDataChangeEvent), global::MxGateway.Contracts.Proto.OnDataChangeEvent.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnWriteCompleteEvent), global::MxGateway.Contracts.Proto.OnWriteCompleteEvent.Parser, null, null, null, null, null), @@ -496,6 +526,10 @@ namespace MxGateway.Contracts.Proto { [pbr::OriginalName("MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK")] UnAdviseItemBulk = 22, [pbr::OriginalName("MX_COMMAND_KIND_SUBSCRIBE_BULK")] SubscribeBulk = 23, [pbr::OriginalName("MX_COMMAND_KIND_UNSUBSCRIBE_BULK")] UnsubscribeBulk = 24, + [pbr::OriginalName("MX_COMMAND_KIND_SUBSCRIBE_ALARMS")] SubscribeAlarms = 25, + [pbr::OriginalName("MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS")] UnsubscribeAlarms = 26, + [pbr::OriginalName("MX_COMMAND_KIND_ACKNOWLEDGE_ALARM")] AcknowledgeAlarm = 27, + [pbr::OriginalName("MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS")] QueryActiveAlarms = 28, [pbr::OriginalName("MX_COMMAND_KIND_PING")] Ping = 100, [pbr::OriginalName("MX_COMMAND_KIND_GET_SESSION_STATE")] GetSessionState = 101, [pbr::OriginalName("MX_COMMAND_KIND_GET_WORKER_INFO")] GetWorkerInfo = 102, @@ -2532,6 +2566,18 @@ namespace MxGateway.Contracts.Proto { case PayloadOneofCase.UnsubscribeBulk: UnsubscribeBulk = other.UnsubscribeBulk.Clone(); break; + case PayloadOneofCase.SubscribeAlarms: + SubscribeAlarms = other.SubscribeAlarms.Clone(); + break; + case PayloadOneofCase.UnsubscribeAlarms: + UnsubscribeAlarms = other.UnsubscribeAlarms.Clone(); + break; + case PayloadOneofCase.AcknowledgeAlarmCommand: + AcknowledgeAlarmCommand = other.AcknowledgeAlarmCommand.Clone(); + break; + case PayloadOneofCase.QueryActiveAlarmsCommand: + QueryActiveAlarmsCommand = other.QueryActiveAlarmsCommand.Clone(); + break; case PayloadOneofCase.Ping: Ping = other.Ping.Clone(); break; @@ -2858,6 +2904,54 @@ namespace MxGateway.Contracts.Proto { } } + /// Field number for the "subscribe_alarms" field. + public const int SubscribeAlarmsFieldNumber = 34; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand SubscribeAlarms { + get { return payloadCase_ == PayloadOneofCase.SubscribeAlarms ? (global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.SubscribeAlarms; + } + } + + /// Field number for the "unsubscribe_alarms" field. + public const int UnsubscribeAlarmsFieldNumber = 35; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand UnsubscribeAlarms { + get { return payloadCase_ == PayloadOneofCase.UnsubscribeAlarms ? (global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.UnsubscribeAlarms; + } + } + + /// Field number for the "acknowledge_alarm_command" field. + public const int AcknowledgeAlarmCommandFieldNumber = 36; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand AcknowledgeAlarmCommand { + get { return payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand ? (global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.AcknowledgeAlarmCommand; + } + } + + /// Field number for the "query_active_alarms_command" field. + public const int QueryActiveAlarmsCommandFieldNumber = 37; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand QueryActiveAlarmsCommand { + get { return payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand ? (global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.QueryActiveAlarmsCommand; + } + } + /// Field number for the "ping" field. public const int PingFieldNumber = 100; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -2946,6 +3040,10 @@ namespace MxGateway.Contracts.Proto { UnAdviseItemBulk = 31, SubscribeBulk = 32, UnsubscribeBulk = 33, + SubscribeAlarms = 34, + UnsubscribeAlarms = 35, + AcknowledgeAlarmCommand = 36, + QueryActiveAlarmsCommand = 37, Ping = 100, GetSessionState = 101, GetWorkerInfo = 102, @@ -3006,6 +3104,10 @@ namespace MxGateway.Contracts.Proto { if (!object.Equals(UnAdviseItemBulk, other.UnAdviseItemBulk)) return false; if (!object.Equals(SubscribeBulk, other.SubscribeBulk)) return false; if (!object.Equals(UnsubscribeBulk, other.UnsubscribeBulk)) return false; + if (!object.Equals(SubscribeAlarms, other.SubscribeAlarms)) return false; + if (!object.Equals(UnsubscribeAlarms, other.UnsubscribeAlarms)) return false; + if (!object.Equals(AcknowledgeAlarmCommand, other.AcknowledgeAlarmCommand)) return false; + if (!object.Equals(QueryActiveAlarmsCommand, other.QueryActiveAlarmsCommand)) return false; if (!object.Equals(Ping, other.Ping)) return false; if (!object.Equals(GetSessionState, other.GetSessionState)) return false; if (!object.Equals(GetWorkerInfo, other.GetWorkerInfo)) return false; @@ -3044,6 +3146,10 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.UnAdviseItemBulk) hash ^= UnAdviseItemBulk.GetHashCode(); if (payloadCase_ == PayloadOneofCase.SubscribeBulk) hash ^= SubscribeBulk.GetHashCode(); if (payloadCase_ == PayloadOneofCase.UnsubscribeBulk) hash ^= UnsubscribeBulk.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) hash ^= SubscribeAlarms.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) hash ^= UnsubscribeAlarms.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) hash ^= AcknowledgeAlarmCommand.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) hash ^= QueryActiveAlarmsCommand.GetHashCode(); if (payloadCase_ == PayloadOneofCase.Ping) hash ^= Ping.GetHashCode(); if (payloadCase_ == PayloadOneofCase.GetSessionState) hash ^= GetSessionState.GetHashCode(); if (payloadCase_ == PayloadOneofCase.GetWorkerInfo) hash ^= GetWorkerInfo.GetHashCode(); @@ -3168,6 +3274,22 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(138, 2); output.WriteMessage(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) { + output.WriteRawTag(146, 2); + output.WriteMessage(SubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) { + output.WriteRawTag(154, 2); + output.WriteMessage(UnsubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) { + output.WriteRawTag(162, 2); + output.WriteMessage(AcknowledgeAlarmCommand); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { + output.WriteRawTag(170, 2); + output.WriteMessage(QueryActiveAlarmsCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3298,6 +3420,22 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(138, 2); output.WriteMessage(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) { + output.WriteRawTag(146, 2); + output.WriteMessage(SubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) { + output.WriteRawTag(154, 2); + output.WriteMessage(UnsubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) { + output.WriteRawTag(162, 2); + output.WriteMessage(AcknowledgeAlarmCommand); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { + output.WriteRawTag(170, 2); + output.WriteMessage(QueryActiveAlarmsCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3403,6 +3541,18 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.UnsubscribeBulk) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(SubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(UnsubscribeAlarms); + } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(AcknowledgeAlarmCommand); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(QueryActiveAlarmsCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(Ping); } @@ -3578,6 +3728,30 @@ namespace MxGateway.Contracts.Proto { } UnsubscribeBulk.MergeFrom(other.UnsubscribeBulk); break; + case PayloadOneofCase.SubscribeAlarms: + if (SubscribeAlarms == null) { + SubscribeAlarms = new global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand(); + } + SubscribeAlarms.MergeFrom(other.SubscribeAlarms); + break; + case PayloadOneofCase.UnsubscribeAlarms: + if (UnsubscribeAlarms == null) { + UnsubscribeAlarms = new global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand(); + } + UnsubscribeAlarms.MergeFrom(other.UnsubscribeAlarms); + break; + case PayloadOneofCase.AcknowledgeAlarmCommand: + if (AcknowledgeAlarmCommand == null) { + AcknowledgeAlarmCommand = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand(); + } + AcknowledgeAlarmCommand.MergeFrom(other.AcknowledgeAlarmCommand); + break; + case PayloadOneofCase.QueryActiveAlarmsCommand: + if (QueryActiveAlarmsCommand == null) { + QueryActiveAlarmsCommand = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand(); + } + QueryActiveAlarmsCommand.MergeFrom(other.QueryActiveAlarmsCommand); + break; case PayloadOneofCase.Ping: if (Ping == null) { Ping = new global::MxGateway.Contracts.Proto.PingCommand(); @@ -3849,6 +4023,42 @@ namespace MxGateway.Contracts.Proto { UnsubscribeBulk = subBuilder; break; } + case 274: { + global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) { + subBuilder.MergeFrom(SubscribeAlarms); + } + input.ReadMessage(subBuilder); + SubscribeAlarms = subBuilder; + break; + } + case 282: { + global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) { + subBuilder.MergeFrom(UnsubscribeAlarms); + } + input.ReadMessage(subBuilder); + UnsubscribeAlarms = subBuilder; + break; + } + case 290: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) { + subBuilder.MergeFrom(AcknowledgeAlarmCommand); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarmCommand = subBuilder; + break; + } + case 298: { + global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { + subBuilder.MergeFrom(QueryActiveAlarmsCommand); + } + input.ReadMessage(subBuilder); + QueryActiveAlarmsCommand = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.PingCommand subBuilder = new global::MxGateway.Contracts.Proto.PingCommand(); if (payloadCase_ == PayloadOneofCase.Ping) { @@ -4133,6 +4343,42 @@ namespace MxGateway.Contracts.Proto { UnsubscribeBulk = subBuilder; break; } + case 274: { + global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.SubscribeAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.SubscribeAlarms) { + subBuilder.MergeFrom(SubscribeAlarms); + } + input.ReadMessage(subBuilder); + SubscribeAlarms = subBuilder; + break; + } + case 282: { + global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) { + subBuilder.MergeFrom(UnsubscribeAlarms); + } + input.ReadMessage(subBuilder); + UnsubscribeAlarms = subBuilder; + break; + } + case 290: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) { + subBuilder.MergeFrom(AcknowledgeAlarmCommand); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarmCommand = subBuilder; + break; + } + case 298: { + global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand subBuilder = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { + subBuilder.MergeFrom(QueryActiveAlarmsCommand); + } + input.ReadMessage(subBuilder); + QueryActiveAlarmsCommand = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.PingCommand subBuilder = new global::MxGateway.Contracts.Proto.PingCommand(); if (payloadCase_ == PayloadOneofCase.Ping) { @@ -10088,6 +10334,972 @@ namespace MxGateway.Contracts.Proto { } + /// + /// Subscribe the worker's alarm consumer to an AVEVA alarm provider. + /// Subscription expression follows the canonical + /// `\\<machine>\Galaxy!<area>` format (literal "Galaxy" provider). The + /// worker spins up a wnwrapConsumer-backed subscription on its STA on + /// first call; subsequent calls are an error (use UnsubscribeAlarms then + /// SubscribeAlarms to reconfigure). + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class SubscribeAlarmsCommand : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SubscribeAlarmsCommand()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[30]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public SubscribeAlarmsCommand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public SubscribeAlarmsCommand(SubscribeAlarmsCommand other) : this() { + subscriptionExpression_ = other.subscriptionExpression_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public SubscribeAlarmsCommand Clone() { + return new SubscribeAlarmsCommand(this); + } + + /// Field number for the "subscription_expression" field. + public const int SubscriptionExpressionFieldNumber = 1; + private string subscriptionExpression_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string SubscriptionExpression { + get { return subscriptionExpression_; } + set { + subscriptionExpression_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as SubscribeAlarmsCommand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(SubscribeAlarmsCommand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (SubscriptionExpression != other.SubscriptionExpression) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (SubscriptionExpression.Length != 0) hash ^= SubscriptionExpression.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (SubscriptionExpression.Length != 0) { + output.WriteRawTag(10); + output.WriteString(SubscriptionExpression); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (SubscriptionExpression.Length != 0) { + output.WriteRawTag(10); + output.WriteString(SubscriptionExpression); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (SubscriptionExpression.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(SubscriptionExpression); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(SubscribeAlarmsCommand other) { + if (other == null) { + return; + } + if (other.SubscriptionExpression.Length != 0) { + SubscriptionExpression = other.SubscriptionExpression; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + SubscriptionExpression = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + SubscriptionExpression = input.ReadString(); + break; + } + } + } + } + #endif + + } + + /// + /// Tear down the worker's alarm consumer. No-op if no subscription is + /// currently active. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class UnsubscribeAlarmsCommand : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new UnsubscribeAlarmsCommand()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[31]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public UnsubscribeAlarmsCommand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public UnsubscribeAlarmsCommand(UnsubscribeAlarmsCommand other) : this() { + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public UnsubscribeAlarmsCommand Clone() { + return new UnsubscribeAlarmsCommand(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as UnsubscribeAlarmsCommand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(UnsubscribeAlarmsCommand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(UnsubscribeAlarmsCommand other) { + if (other == null) { + return; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + } + } + } + #endif + + } + + /// + /// Acknowledge a single alarm by its GUID. Operator identity fields are + /// recorded atomically with the ack transition in the alarm-history log. + /// The reply's hresult / native_status surfaces AVEVA's + /// AlarmAckByGUID return code. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class AcknowledgeAlarmCommand : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AcknowledgeAlarmCommand()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[32]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmCommand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmCommand(AcknowledgeAlarmCommand other) : this() { + alarmGuid_ = other.alarmGuid_; + comment_ = other.comment_; + operatorUser_ = other.operatorUser_; + operatorNode_ = other.operatorNode_; + operatorDomain_ = other.operatorDomain_; + operatorFullName_ = other.operatorFullName_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmCommand Clone() { + return new AcknowledgeAlarmCommand(this); + } + + /// Field number for the "alarm_guid" field. + public const int AlarmGuidFieldNumber = 1; + private string alarmGuid_ = ""; + /// + /// Canonical 8-4-4-4-12 GUID string (e.g. "BCC47053-9542-4D65-BDAA-BCDEA6A32A73"). + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string AlarmGuid { + get { return alarmGuid_; } + set { + alarmGuid_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "comment" field. + public const int CommentFieldNumber = 2; + private string comment_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string Comment { + get { return comment_; } + set { + comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_user" field. + public const int OperatorUserFieldNumber = 3; + private string operatorUser_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorUser { + get { return operatorUser_; } + set { + operatorUser_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_node" field. + public const int OperatorNodeFieldNumber = 4; + private string operatorNode_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorNode { + get { return operatorNode_; } + set { + operatorNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_domain" field. + public const int OperatorDomainFieldNumber = 5; + private string operatorDomain_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorDomain { + get { return operatorDomain_; } + set { + operatorDomain_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_full_name" field. + public const int OperatorFullNameFieldNumber = 6; + private string operatorFullName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorFullName { + get { return operatorFullName_; } + set { + operatorFullName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as AcknowledgeAlarmCommand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(AcknowledgeAlarmCommand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (AlarmGuid != other.AlarmGuid) return false; + if (Comment != other.Comment) return false; + if (OperatorUser != other.OperatorUser) return false; + if (OperatorNode != other.OperatorNode) return false; + if (OperatorDomain != other.OperatorDomain) return false; + if (OperatorFullName != other.OperatorFullName) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (AlarmGuid.Length != 0) hash ^= AlarmGuid.GetHashCode(); + if (Comment.Length != 0) hash ^= Comment.GetHashCode(); + if (OperatorUser.Length != 0) hash ^= OperatorUser.GetHashCode(); + if (OperatorNode.Length != 0) hash ^= OperatorNode.GetHashCode(); + if (OperatorDomain.Length != 0) hash ^= OperatorDomain.GetHashCode(); + if (OperatorFullName.Length != 0) hash ^= OperatorFullName.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (AlarmGuid.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmGuid); + } + if (Comment.Length != 0) { + output.WriteRawTag(18); + output.WriteString(Comment); + } + if (OperatorUser.Length != 0) { + output.WriteRawTag(26); + output.WriteString(OperatorUser); + } + if (OperatorNode.Length != 0) { + output.WriteRawTag(34); + output.WriteString(OperatorNode); + } + if (OperatorDomain.Length != 0) { + output.WriteRawTag(42); + output.WriteString(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + output.WriteRawTag(50); + output.WriteString(OperatorFullName); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (AlarmGuid.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmGuid); + } + if (Comment.Length != 0) { + output.WriteRawTag(18); + output.WriteString(Comment); + } + if (OperatorUser.Length != 0) { + output.WriteRawTag(26); + output.WriteString(OperatorUser); + } + if (OperatorNode.Length != 0) { + output.WriteRawTag(34); + output.WriteString(OperatorNode); + } + if (OperatorDomain.Length != 0) { + output.WriteRawTag(42); + output.WriteString(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + output.WriteRawTag(50); + output.WriteString(OperatorFullName); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (AlarmGuid.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(AlarmGuid); + } + if (Comment.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); + } + if (OperatorUser.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorUser); + } + if (OperatorNode.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorNode); + } + if (OperatorDomain.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorFullName); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(AcknowledgeAlarmCommand other) { + if (other == null) { + return; + } + if (other.AlarmGuid.Length != 0) { + AlarmGuid = other.AlarmGuid; + } + if (other.Comment.Length != 0) { + Comment = other.Comment; + } + if (other.OperatorUser.Length != 0) { + OperatorUser = other.OperatorUser; + } + if (other.OperatorNode.Length != 0) { + OperatorNode = other.OperatorNode; + } + if (other.OperatorDomain.Length != 0) { + OperatorDomain = other.OperatorDomain; + } + if (other.OperatorFullName.Length != 0) { + OperatorFullName = other.OperatorFullName; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + AlarmGuid = input.ReadString(); + break; + } + case 18: { + Comment = input.ReadString(); + break; + } + case 26: { + OperatorUser = input.ReadString(); + break; + } + case 34: { + OperatorNode = input.ReadString(); + break; + } + case 42: { + OperatorDomain = input.ReadString(); + break; + } + case 50: { + OperatorFullName = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + AlarmGuid = input.ReadString(); + break; + } + case 18: { + Comment = input.ReadString(); + break; + } + case 26: { + OperatorUser = input.ReadString(); + break; + } + case 34: { + OperatorNode = input.ReadString(); + break; + } + case 42: { + OperatorDomain = input.ReadString(); + break; + } + case 50: { + OperatorFullName = input.ReadString(); + break; + } + } + } + } + #endif + + } + + /// + /// Snapshot the currently-active alarm set. Optional filter prefix scopes + /// the snapshot to alarms whose alarm_full_reference starts with the + /// supplied string (matches QueryActiveAlarmsRequest.alarm_filter_prefix). + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class QueryActiveAlarmsCommand : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new QueryActiveAlarmsCommand()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[33]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsCommand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsCommand(QueryActiveAlarmsCommand other) : this() { + alarmFilterPrefix_ = other.alarmFilterPrefix_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsCommand Clone() { + return new QueryActiveAlarmsCommand(this); + } + + /// Field number for the "alarm_filter_prefix" field. + public const int AlarmFilterPrefixFieldNumber = 1; + private string alarmFilterPrefix_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string AlarmFilterPrefix { + get { return alarmFilterPrefix_; } + set { + alarmFilterPrefix_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as QueryActiveAlarmsCommand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(QueryActiveAlarmsCommand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (AlarmFilterPrefix != other.AlarmFilterPrefix) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (AlarmFilterPrefix.Length != 0) hash ^= AlarmFilterPrefix.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (AlarmFilterPrefix.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmFilterPrefix); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (AlarmFilterPrefix.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmFilterPrefix); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (AlarmFilterPrefix.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(AlarmFilterPrefix); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(QueryActiveAlarmsCommand other) { + if (other == null) { + return; + } + if (other.AlarmFilterPrefix.Length != 0) { + AlarmFilterPrefix = other.AlarmFilterPrefix; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + AlarmFilterPrefix = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + AlarmFilterPrefix = input.ReadString(); + break; + } + } + } + } + #endif + + } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class UnsubscribeBulkCommand : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -10103,7 +11315,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[30]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[34]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10329,7 +11541,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[31]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[35]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10527,7 +11739,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[32]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[36]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10688,7 +11900,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[33]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[37]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10849,7 +12061,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[34]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[38]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11047,7 +12259,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[35]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[39]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11255,7 +12467,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[36]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[40]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11327,6 +12539,12 @@ namespace MxGateway.Contracts.Proto { case PayloadOneofCase.UnsubscribeBulk: UnsubscribeBulk = other.UnsubscribeBulk.Clone(); break; + case PayloadOneofCase.AcknowledgeAlarm: + AcknowledgeAlarm = other.AcknowledgeAlarm.Clone(); + break; + case PayloadOneofCase.QueryActiveAlarms: + QueryActiveAlarms = other.QueryActiveAlarms.Clone(); + break; case PayloadOneofCase.SessionState: SessionState = other.SessionState.Clone(); break; @@ -11630,6 +12848,30 @@ namespace MxGateway.Contracts.Proto { } } + /// Field number for the "acknowledge_alarm" field. + public const int AcknowledgeAlarmFieldNumber = 34; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload AcknowledgeAlarm { + get { return payloadCase_ == PayloadOneofCase.AcknowledgeAlarm ? (global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.AcknowledgeAlarm; + } + } + + /// Field number for the "query_active_alarms" field. + public const int QueryActiveAlarmsFieldNumber = 35; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload QueryActiveAlarms { + get { return payloadCase_ == PayloadOneofCase.QueryActiveAlarms ? (global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.QueryActiveAlarms; + } + } + /// Field number for the "session_state" field. public const int SessionStateFieldNumber = 100; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11684,6 +12926,8 @@ namespace MxGateway.Contracts.Proto { UnAdviseItemBulk = 31, SubscribeBulk = 32, UnsubscribeBulk = 33, + AcknowledgeAlarm = 34, + QueryActiveAlarms = 35, SessionState = 100, WorkerInfo = 101, DrainEvents = 102, @@ -11739,6 +12983,8 @@ namespace MxGateway.Contracts.Proto { if (!object.Equals(UnAdviseItemBulk, other.UnAdviseItemBulk)) return false; if (!object.Equals(SubscribeBulk, other.SubscribeBulk)) return false; if (!object.Equals(UnsubscribeBulk, other.UnsubscribeBulk)) return false; + if (!object.Equals(AcknowledgeAlarm, other.AcknowledgeAlarm)) return false; + if (!object.Equals(QueryActiveAlarms, other.QueryActiveAlarms)) return false; if (!object.Equals(SessionState, other.SessionState)) return false; if (!object.Equals(WorkerInfo, other.WorkerInfo)) return false; if (!object.Equals(DrainEvents, other.DrainEvents)) return false; @@ -11772,6 +13018,8 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.UnAdviseItemBulk) hash ^= UnAdviseItemBulk.GetHashCode(); if (payloadCase_ == PayloadOneofCase.SubscribeBulk) hash ^= SubscribeBulk.GetHashCode(); if (payloadCase_ == PayloadOneofCase.UnsubscribeBulk) hash ^= UnsubscribeBulk.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) hash ^= AcknowledgeAlarm.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) hash ^= QueryActiveAlarms.GetHashCode(); if (payloadCase_ == PayloadOneofCase.SessionState) hash ^= SessionState.GetHashCode(); if (payloadCase_ == PayloadOneofCase.WorkerInfo) hash ^= WorkerInfo.GetHashCode(); if (payloadCase_ == PayloadOneofCase.DrainEvents) hash ^= DrainEvents.GetHashCode(); @@ -11879,6 +13127,14 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(138, 2); output.WriteMessage(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) { + output.WriteRawTag(146, 2); + output.WriteMessage(AcknowledgeAlarm); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) { + output.WriteRawTag(154, 2); + output.WriteMessage(QueryActiveAlarms); + } if (payloadCase_ == PayloadOneofCase.SessionState) { output.WriteRawTag(162, 6); output.WriteMessage(SessionState); @@ -11986,6 +13242,14 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(138, 2); output.WriteMessage(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) { + output.WriteRawTag(146, 2); + output.WriteMessage(AcknowledgeAlarm); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) { + output.WriteRawTag(154, 2); + output.WriteMessage(QueryActiveAlarms); + } if (payloadCase_ == PayloadOneofCase.SessionState) { output.WriteRawTag(162, 6); output.WriteMessage(SessionState); @@ -12072,6 +13336,12 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.UnsubscribeBulk) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(UnsubscribeBulk); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(AcknowledgeAlarm); + } + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(QueryActiveAlarms); + } if (payloadCase_ == PayloadOneofCase.SessionState) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(SessionState); } @@ -12206,6 +13476,18 @@ namespace MxGateway.Contracts.Proto { } UnsubscribeBulk.MergeFrom(other.UnsubscribeBulk); break; + case PayloadOneofCase.AcknowledgeAlarm: + if (AcknowledgeAlarm == null) { + AcknowledgeAlarm = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload(); + } + AcknowledgeAlarm.MergeFrom(other.AcknowledgeAlarm); + break; + case PayloadOneofCase.QueryActiveAlarms: + if (QueryActiveAlarms == null) { + QueryActiveAlarms = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload(); + } + QueryActiveAlarms.MergeFrom(other.QueryActiveAlarms); + break; case PayloadOneofCase.SessionState: if (SessionState == null) { SessionState = new global::MxGateway.Contracts.Proto.SessionStateReply(); @@ -12409,6 +13691,24 @@ namespace MxGateway.Contracts.Proto { UnsubscribeBulk = subBuilder; break; } + case 274: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) { + subBuilder.MergeFrom(AcknowledgeAlarm); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarm = subBuilder; + break; + } + case 282: { + global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload subBuilder = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) { + subBuilder.MergeFrom(QueryActiveAlarms); + } + input.ReadMessage(subBuilder); + QueryActiveAlarms = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.SessionStateReply subBuilder = new global::MxGateway.Contracts.Proto.SessionStateReply(); if (payloadCase_ == PayloadOneofCase.SessionState) { @@ -12619,6 +13919,24 @@ namespace MxGateway.Contracts.Proto { UnsubscribeBulk = subBuilder; break; } + case 274: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmReplyPayload(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarm) { + subBuilder.MergeFrom(AcknowledgeAlarm); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarm = subBuilder; + break; + } + case 282: { + global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload subBuilder = new global::MxGateway.Contracts.Proto.QueryActiveAlarmsReplyPayload(); + if (payloadCase_ == PayloadOneofCase.QueryActiveAlarms) { + subBuilder.MergeFrom(QueryActiveAlarms); + } + input.ReadMessage(subBuilder); + QueryActiveAlarms = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.SessionStateReply subBuilder = new global::MxGateway.Contracts.Proto.SessionStateReply(); if (payloadCase_ == PayloadOneofCase.SessionState) { @@ -12668,7 +13986,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[37]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[41]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -12866,7 +14184,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[38]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[42]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13064,7 +14382,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[39]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[43]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13262,7 +14580,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[40]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[44]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13460,7 +14778,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[41]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[45]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13667,7 +14985,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[42]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[46]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13874,7 +15192,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[43]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[47]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14072,7 +15390,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[44]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[48]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14270,7 +15588,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[45]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[49]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14616,7 +15934,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[46]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[50]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14803,7 +16121,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[47]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[51]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15001,7 +16319,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[48]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[52]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15310,7 +16628,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[49]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[53]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15482,6 +16800,405 @@ namespace MxGateway.Contracts.Proto { } + /// + /// Reply payload for AcknowledgeAlarmCommand. Surfaces AVEVA's native + /// AlarmAckByGUID return code; 0 means success. The MxCommandReply's + /// hresult field carries the same value and is preferred for protocol + /// consumers — this payload exists so the gateway-side + /// WorkerAlarmRpcDispatcher can echo native_status into + /// AcknowledgeAlarmReply.hresult without unpacking the outer envelope. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class AcknowledgeAlarmReplyPayload : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AcknowledgeAlarmReplyPayload()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[54]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmReplyPayload() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmReplyPayload(AcknowledgeAlarmReplyPayload other) : this() { + nativeStatus_ = other.nativeStatus_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmReplyPayload Clone() { + return new AcknowledgeAlarmReplyPayload(this); + } + + /// Field number for the "native_status" field. + public const int NativeStatusFieldNumber = 1; + private int nativeStatus_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int NativeStatus { + get { return nativeStatus_; } + set { + nativeStatus_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as AcknowledgeAlarmReplyPayload); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(AcknowledgeAlarmReplyPayload other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (NativeStatus != other.NativeStatus) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (NativeStatus != 0) hash ^= NativeStatus.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (NativeStatus != 0) { + output.WriteRawTag(8); + output.WriteInt32(NativeStatus); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (NativeStatus != 0) { + output.WriteRawTag(8); + output.WriteInt32(NativeStatus); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (NativeStatus != 0) { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(NativeStatus); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(AcknowledgeAlarmReplyPayload other) { + if (other == null) { + return; + } + if (other.NativeStatus != 0) { + NativeStatus = other.NativeStatus; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + NativeStatus = input.ReadInt32(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + NativeStatus = input.ReadInt32(); + break; + } + } + } + } + #endif + + } + + /// + /// Reply payload for QueryActiveAlarmsCommand. The worker walks + /// IMxAccessAlarmConsumer.SnapshotActiveAlarms and packs each record as + /// an ActiveAlarmSnapshot proto for the gateway-side ConditionRefresh + /// stream. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class QueryActiveAlarmsReplyPayload : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new QueryActiveAlarmsReplyPayload()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[55]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsReplyPayload() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsReplyPayload(QueryActiveAlarmsReplyPayload other) : this() { + snapshots_ = other.snapshots_.Clone(); + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public QueryActiveAlarmsReplyPayload Clone() { + return new QueryActiveAlarmsReplyPayload(this); + } + + /// Field number for the "snapshots" field. + public const int SnapshotsFieldNumber = 1; + private static readonly pb::FieldCodec _repeated_snapshots_codec + = pb::FieldCodec.ForMessage(10, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser); + private readonly pbc::RepeatedField snapshots_ = new pbc::RepeatedField(); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public pbc::RepeatedField Snapshots { + get { return snapshots_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as QueryActiveAlarmsReplyPayload); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(QueryActiveAlarmsReplyPayload other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if(!snapshots_.Equals(other.snapshots_)) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + hash ^= snapshots_.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + snapshots_.WriteTo(output, _repeated_snapshots_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + snapshots_.WriteTo(ref output, _repeated_snapshots_codec); + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + size += snapshots_.CalculateSize(_repeated_snapshots_codec); + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(QueryActiveAlarmsReplyPayload other) { + if (other == null) { + return; + } + snapshots_.Add(other.snapshots_); + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + snapshots_.AddEntriesFrom(input, _repeated_snapshots_codec); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + snapshots_.AddEntriesFrom(ref input, _repeated_snapshots_codec); + break; + } + } + } + } + #endif + + } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class MxEvent : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -15498,7 +17215,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[50]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[56]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16473,7 +18190,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[51]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[57]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16634,7 +18351,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[52]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[58]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16795,7 +18512,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[53]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[59]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16956,7 +18673,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[54]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[60]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -17289,7 +19006,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[55]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[61]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18023,7 +19740,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[56]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[62]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18713,7 +20430,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[57]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[63]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19070,7 +20787,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[58]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[64]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19493,7 +21210,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[59]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[65]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19769,7 +21486,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[60]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[66]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -20189,7 +21906,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[61]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[67]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21046,7 +22763,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[62]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[68]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21834,7 +23551,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[63]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[69]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22023,7 +23740,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[64]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[70]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22212,7 +23929,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[65]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[71]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22401,7 +24118,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[66]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[72]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22590,7 +24307,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[67]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[73]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22779,7 +24496,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[68]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[74]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22966,7 +24683,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[69]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[75]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23153,7 +24870,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[70]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[76]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23340,7 +25057,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[71]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[77]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] diff --git a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto index b7ecf29..0bbf2ab 100644 --- a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -88,6 +88,10 @@ message MxCommand { UnAdviseItemBulkCommand un_advise_item_bulk = 31; SubscribeBulkCommand subscribe_bulk = 32; UnsubscribeBulkCommand unsubscribe_bulk = 33; + SubscribeAlarmsCommand subscribe_alarms = 34; + UnsubscribeAlarmsCommand unsubscribe_alarms = 35; + AcknowledgeAlarmCommand acknowledge_alarm_command = 36; + QueryActiveAlarmsCommand query_active_alarms_command = 37; PingCommand ping = 100; GetSessionStateCommand get_session_state = 101; GetWorkerInfoCommand get_worker_info = 102; @@ -122,6 +126,10 @@ enum MxCommandKind { MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK = 22; MX_COMMAND_KIND_SUBSCRIBE_BULK = 23; MX_COMMAND_KIND_UNSUBSCRIBE_BULK = 24; + MX_COMMAND_KIND_SUBSCRIBE_ALARMS = 25; + MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26; + MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27; + MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28; MX_COMMAND_KIND_PING = 100; MX_COMMAND_KIND_GET_SESSION_STATE = 101; MX_COMMAND_KIND_GET_WORKER_INFO = 102; @@ -263,6 +271,42 @@ message SubscribeBulkCommand { repeated string tag_addresses = 2; } +// Subscribe the worker's alarm consumer to an AVEVA alarm provider. +// Subscription expression follows the canonical +// `\\\Galaxy!` format (literal "Galaxy" provider). The +// worker spins up a wnwrapConsumer-backed subscription on its STA on +// first call; subsequent calls are an error (use UnsubscribeAlarms then +// SubscribeAlarms to reconfigure). +message SubscribeAlarmsCommand { + string subscription_expression = 1; +} + +// Tear down the worker's alarm consumer. No-op if no subscription is +// currently active. +message UnsubscribeAlarmsCommand { +} + +// Acknowledge a single alarm by its GUID. Operator identity fields are +// recorded atomically with the ack transition in the alarm-history log. +// The reply's hresult / native_status surfaces AVEVA's +// AlarmAckByGUID return code. +message AcknowledgeAlarmCommand { + // Canonical 8-4-4-4-12 GUID string (e.g. "BCC47053-9542-4D65-BDAA-BCDEA6A32A73"). + string alarm_guid = 1; + string comment = 2; + string operator_user = 3; + string operator_node = 4; + string operator_domain = 5; + string operator_full_name = 6; +} + +// Snapshot the currently-active alarm set. Optional filter prefix scopes +// the snapshot to alarms whose alarm_full_reference starts with the +// supplied string (matches QueryActiveAlarmsRequest.alarm_filter_prefix). +message QueryActiveAlarmsCommand { + string alarm_filter_prefix = 1; +} + message UnsubscribeBulkCommand { int32 server_handle = 1; repeated int32 item_handles = 2; @@ -314,6 +358,8 @@ message MxCommandReply { BulkSubscribeReply un_advise_item_bulk = 31; BulkSubscribeReply subscribe_bulk = 32; BulkSubscribeReply unsubscribe_bulk = 33; + AcknowledgeAlarmReplyPayload acknowledge_alarm = 34; + QueryActiveAlarmsReplyPayload query_active_alarms = 35; SessionStateReply session_state = 100; WorkerInfoReply worker_info = 101; DrainEventsReply drain_events = 102; @@ -379,6 +425,24 @@ message DrainEventsReply { repeated MxEvent events = 1; } +// Reply payload for AcknowledgeAlarmCommand. Surfaces AVEVA's native +// AlarmAckByGUID return code; 0 means success. The MxCommandReply's +// hresult field carries the same value and is preferred for protocol +// consumers — this payload exists so the gateway-side +// WorkerAlarmRpcDispatcher can echo native_status into +// AcknowledgeAlarmReply.hresult without unpacking the outer envelope. +message AcknowledgeAlarmReplyPayload { + int32 native_status = 1; +} + +// Reply payload for QueryActiveAlarmsCommand. The worker walks +// IMxAccessAlarmConsumer.SnapshotActiveAlarms and packs each record as +// an ActiveAlarmSnapshot proto for the gateway-side ConditionRefresh +// stream. +message QueryActiveAlarmsReplyPayload { + repeated ActiveAlarmSnapshot snapshots = 1; +} + message MxEvent { MxEventFamily family = 1; string session_id = 2; diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs new file mode 100644 index 0000000..c04dd3e --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using MxGateway.Contracts.Proto; +using MxGateway.Worker.MxAccess; +using MxGateway.Worker.Sta; + +namespace MxGateway.Worker.Tests.MxAccess; + +/// +/// Verifies that the four new alarm values +/// route through to a fake +/// and that the resulting +/// carries the expected payload. +/// +/// The data-side is constructed via a +/// no-op factory because the executor only touches it for non-alarm +/// command kinds — alarm dispatch never reaches the data session. +/// +public sealed class AlarmCommandExecutorTests +{ + private const string SessionId = "S"; + private const string CorrelationId = "C"; + + [Fact] + public void SubscribeAlarms_routes_to_handler_and_returns_ok() + { + FakeAlarmHandler handler = new FakeAlarmHandler(); + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.SubscribeAlarms, + SubscribeAlarms = new SubscribeAlarmsCommand + { + SubscriptionExpression = @"\\HOST\Galaxy!Area", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(@"\\HOST\Galaxy!Area", handler.LastSubscription); + Assert.Equal(SessionId, handler.LastSessionId); + } + + [Fact] + public void SubscribeAlarms_without_handler_returns_invalid_request() + { + MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.SubscribeAlarms, + SubscribeAlarms = new SubscribeAlarmsCommand + { + SubscriptionExpression = @"\\HOST\Galaxy!Area", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + } + + [Fact] + public void SubscribeAlarms_with_empty_expression_returns_invalid_request() + { + MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.SubscribeAlarms, + SubscribeAlarms = new SubscribeAlarmsCommand + { + SubscriptionExpression = " ", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + } + + [Fact] + public void AcknowledgeAlarm_routes_native_status_into_hresult_and_payload() + { + FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 }; + MxAccessCommandExecutor executor = NewExecutor(handler); + Guid g = Guid.NewGuid(); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = g.ToString(), + Comment = "ack", + OperatorUser = "alice", + OperatorNode = "WS", + OperatorDomain = "CORP", + OperatorFullName = "Alice S", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(0, reply.Hresult); + Assert.NotNull(reply.AcknowledgeAlarm); + Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus); + Assert.Equal(g, handler.LastAckGuid); + Assert.Equal("alice", handler.LastAckOperatorName); + } + + [Fact] + public void AcknowledgeAlarm_with_invalid_guid_returns_invalid_request() + { + MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = "not-a-guid", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + } + + [Fact] + public void AcknowledgeAlarm_with_nonzero_native_status_carries_diagnostic() + { + FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = -123 }; + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = Guid.NewGuid().ToString(), + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(-123, reply.Hresult); + Assert.Contains("-123", reply.DiagnosticMessage); + } + + [Fact] + public void QueryActiveAlarms_returns_payload_with_snapshots() + { + FakeAlarmHandler handler = new FakeAlarmHandler + { + QueryResult = new[] + { + new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T1" }, + new ActiveAlarmSnapshot { AlarmFullReference = "Galaxy!A.T2" }, + }, + }; + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.QueryActiveAlarms, + QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand + { + AlarmFilterPrefix = "Galaxy!A", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.NotNull(reply.QueryActiveAlarms); + Assert.Equal(2, reply.QueryActiveAlarms.Snapshots.Count); + Assert.Equal("Galaxy!A", handler.LastFilterPrefix); + } + + [Fact] + public void UnsubscribeAlarms_routes_to_handler() + { + FakeAlarmHandler handler = new FakeAlarmHandler(); + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.UnsubscribeAlarms, + UnsubscribeAlarms = new UnsubscribeAlarmsCommand(), + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.True(handler.UnsubscribeCalled); + } + + [Fact] + public void UnsubscribeAlarms_without_handler_is_ok_noop() + { + MxAccessCommandExecutor executor = NewExecutor(alarmHandler: null); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.UnsubscribeAlarms, + UnsubscribeAlarms = new UnsubscribeAlarmsCommand(), + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + } + + [Fact] + public void Acknowledge_handler_throw_returns_mxaccess_failure() + { + FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeThrow = true }; + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = Guid.NewGuid().ToString(), + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); + Assert.Contains("simulated", reply.DiagnosticMessage); + } + + private static MxAccessCommandExecutor NewExecutor(IAlarmCommandHandler? alarmHandler) + { + // Construct an executor with a no-op data session — we only exercise + // the alarm switch arms, which never touch the data session. + return new MxAccessCommandExecutor( + session: NoopMxAccessSession.Create(), + variantConverter: new MxGateway.Worker.Conversion.VariantConverter(), + alarmCommandHandler: alarmHandler); + } + + /// + /// Reflection-based helper to construct an MxAccessSession without + /// a real COM object. Only the alarm-side code paths are exercised + /// in this test class, so the session reference is never + /// dereferenced. + /// + private static class NoopMxAccessSession + { + public static MxAccessSession Create() + { + // Walk to the private constructor via reflection — the public + // factory MxAccessSession.Create(...) requires a real COM object. + System.Reflection.ConstructorInfo? ctor = typeof(MxAccessSession) + .GetConstructor( + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, + binder: null, + types: new[] + { + typeof(object), + typeof(IMxAccessServer), + typeof(IMxAccessEventSink), + typeof(MxAccessHandleRegistry), + typeof(int), + }, + modifiers: null); + if (ctor is null) + { + throw new InvalidOperationException( + "MxAccessSession private ctor signature changed; update the test seam."); + } + return (MxAccessSession)ctor.Invoke(new object[] + { + new object(), + new NullMxAccessServer(), + new NullEventSink(), + new MxAccessHandleRegistry(), + System.Environment.CurrentManagedThreadId, + }); + } + } + + private sealed class NullMxAccessServer : IMxAccessServer + { + public int Register(string clientName) => 0; + public void Unregister(int serverHandle) { } + public int AddItem(int serverHandle, string itemDefinition) => 0; + public int AddItem2(int serverHandle, string itemDefinition, string itemContext) => 0; + public void RemoveItem(int serverHandle, int itemHandle) { } + public void Advise(int serverHandle, int itemHandle) { } + public void UnAdvise(int serverHandle, int itemHandle) { } + public void AdviseSupervisory(int serverHandle, int itemHandle) { } + public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext) => 0; + public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { } + public void Suspend(int serverHandle, int itemHandle) { } + public void Activate(int serverHandle, int itemHandle) { } + public void Write(int serverHandle, int itemHandle, object value, int userId) { } + public void Write2(int serverHandle, int itemHandle, object value, object timestampValue, int userId) { } + public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value) { } + public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestampValue) { } + public int AuthenticateUser(string userName, string password) => 0; + public int ArchestrAUserToId(string userName) => 0; + } + + private sealed class NullEventSink : IMxAccessEventSink + { + public void Attach(object mxAccessComObject, string sessionId) { } + public void Detach() { } + } + + private sealed class FakeAlarmHandler : IAlarmCommandHandler + { + public string? LastSubscription { get; private set; } + public string? LastSessionId { get; private set; } + public bool UnsubscribeCalled { get; private set; } + public Guid LastAckGuid { get; private set; } + public string? LastAckOperatorName { get; private set; } + public int AcknowledgeReturn { get; set; } + public bool AcknowledgeThrow { get; set; } + public IReadOnlyList QueryResult { get; set; } = + Array.Empty(); + public string? LastFilterPrefix { get; private set; } + + public void Subscribe(string subscription, string sessionId) + { + LastSubscription = subscription; + LastSessionId = sessionId; + } + + public void Unsubscribe() + { + UnsubscribeCalled = true; + } + + public int Acknowledge( + Guid alarmGuid, string comment, string operatorUser, + string operatorNode, string operatorDomain, string operatorFullName) + { + LastAckGuid = alarmGuid; + LastAckOperatorName = operatorUser; + if (AcknowledgeThrow) + { + throw new InvalidOperationException("simulated alarm-handler failure"); + } + return AcknowledgeReturn; + } + + public IReadOnlyList QueryActive(string? alarmFilterPrefix) + { + LastFilterPrefix = alarmFilterPrefix; + return QueryResult; + } + + public void Dispose() { } + } +} diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs new file mode 100644 index 0000000..10e34a0 --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using MxGateway.Contracts.Proto; +using MxGateway.Worker.MxAccess; + +namespace MxGateway.Worker.Tests.MxAccess; + +/// +/// Unit tests for the per-session alarm command router. Uses a fake +/// consumer factory so the lazy-construction lifecycle on +/// SubscribeAlarms is exercised without touching wnwrap COM. +/// +public sealed class AlarmCommandHandlerTests +{ + [Fact] + public void Subscribe_creates_consumer_and_calls_subscribe() + { + FakeConsumer consumer = new FakeConsumer(); + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + + handler.Subscribe(@"\\HOST\Galaxy!Area", "session-1"); + + Assert.True(handler.IsSubscribed); + Assert.Equal(@"\\HOST\Galaxy!Area", consumer.LastSubscription); + } + + [Fact] + public void Second_subscribe_without_unsubscribe_throws() + { + FakeConsumer consumer = new FakeConsumer(); + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + Assert.Throws( + () => handler.Subscribe(@"\\HOST\Galaxy!B", "s1")); + } + + [Fact] + public void Subscribe_disposes_consumer_when_underlying_subscribe_throws() + { + FakeConsumer consumer = new FakeConsumer { ThrowOnSubscribe = true }; + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + + Assert.Throws( + () => handler.Subscribe(@"\\HOST\Galaxy!A", "s1")); + Assert.False(handler.IsSubscribed); + Assert.True(consumer.Disposed); + } + + [Fact] + public void Unsubscribe_disposes_consumer_and_clears_state() + { + FakeConsumer consumer = new FakeConsumer(); + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + + handler.Unsubscribe(); + + Assert.False(handler.IsSubscribed); + Assert.True(consumer.Disposed); + } + + [Fact] + public void Unsubscribe_without_prior_subscribe_is_noop() + { + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => new FakeConsumer()); + handler.Unsubscribe(); // Should not throw. + Assert.False(handler.IsSubscribed); + } + + [Fact] + public void Acknowledge_forwards_to_consumer_with_full_operator_identity() + { + FakeConsumer consumer = new FakeConsumer { AcknowledgeReturn = 0 }; + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + + Guid g = Guid.NewGuid(); + int rc = handler.Acknowledge(g, "c", "u", "n", "d", "F"); + + Assert.Equal(0, rc); + Assert.Equal(g, consumer.LastAckGuid); + Assert.Equal("u", consumer.LastAckOperatorName); + } + + [Fact] + public void Acknowledge_before_subscribe_throws_invalid_op() + { + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => new FakeConsumer()); + + Assert.Throws( + () => handler.Acknowledge(Guid.Empty, "", "", "", "", "")); + } + + [Fact] + public void QueryActive_returns_mapped_proto_snapshots() + { + FakeConsumer consumer = new FakeConsumer + { + SnapshotResult = new[] + { + new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = "Galaxy", + Group = "TestArea", + TagName = "Tag1", + Type = "DSC", + Priority = 500, + State = MxAlarmStateKind.UnackAlm, + }, + }, + }; + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + + IReadOnlyList snapshots = handler.QueryActive(null); + + Assert.Single(snapshots); + Assert.Equal("Galaxy!TestArea.Tag1", snapshots[0].AlarmFullReference); + Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState); + } + + [Fact] + public void QueryActive_filters_by_prefix() + { + FakeConsumer consumer = new FakeConsumer + { + SnapshotResult = new[] + { + NewRecord("Galaxy", "AreaA", "Tag1"), + NewRecord("Galaxy", "AreaB", "Tag2"), + }, + }; + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + + IReadOnlyList filtered = handler.QueryActive("Galaxy!AreaA"); + + Assert.Single(filtered); + Assert.Equal("Galaxy!AreaA.Tag1", filtered[0].AlarmFullReference); + } + + [Fact] + public void Dispose_unsubscribes_and_disposes_consumer() + { + FakeConsumer consumer = new FakeConsumer(); + AlarmCommandHandler handler = new AlarmCommandHandler( + new MxAccessEventQueue(), + () => consumer); + handler.Subscribe(@"\\HOST\Galaxy!A", "s1"); + + handler.Dispose(); + + Assert.True(consumer.Disposed); + Assert.Throws( + () => handler.Subscribe("x", "y")); + } + + private static MxAlarmSnapshotRecord NewRecord(string provider, string group, string tag) + { + return new MxAlarmSnapshotRecord + { + AlarmGuid = Guid.NewGuid(), + ProviderName = provider, + Group = group, + TagName = tag, + Type = "DSC", + Priority = 500, + State = MxAlarmStateKind.UnackAlm, + }; + } + + private sealed class FakeConsumer : IMxAccessAlarmConsumer + { +#pragma warning disable CS0067 // Event never invoked — fake; AlarmCommandHandler tests don't drive transitions. + public event EventHandler? AlarmTransitionEmitted; +#pragma warning restore CS0067 + + public string? LastSubscription { get; private set; } + public Guid LastAckGuid { get; private set; } + public string? LastAckOperatorName { get; private set; } + public int AcknowledgeReturn { get; set; } + public IReadOnlyList SnapshotResult { get; set; } = + Array.Empty(); + public bool ThrowOnSubscribe { get; set; } + public bool Disposed { get; private set; } + + public void Subscribe(string subscription) + { + LastSubscription = subscription; + if (ThrowOnSubscribe) + { + throw new InvalidOperationException("simulated wnwrap subscribe failure"); + } + } + + public int AcknowledgeByGuid( + Guid alarmGuid, string ackComment, string ackOperatorName, + string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) + { + LastAckGuid = alarmGuid; + LastAckOperatorName = ackOperatorName; + return AcknowledgeReturn; + } + + public IReadOnlyList SnapshotActiveAlarms() => SnapshotResult; + + public void Dispose() + { + Disposed = true; + } + } +} diff --git a/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs new file mode 100644 index 0000000..1ac5744 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using MxGateway.Contracts.Proto; + +namespace MxGateway.Worker.MxAccess; + +/// +/// Per-session owner of the worker's alarm-side state. Lazy-creates an +/// (with a wnwrap-backed +/// by default) on the first +/// call, then routes +/// / / +/// through the same instance for the +/// session's lifetime. +/// +/// +/// +/// Construction is dependency-injectable: the consumer factory +/// (default () => new WnWrapAlarmConsumer()) lets tests +/// substitute a fake without touching AVEVA COM. The event queue +/// is supplied by the owning so +/// the alarm-side proto events land on the same queue the worker +/// already drains for IPC dispatch. +/// +/// +/// Threading: invoked from +/// which runs on the STA. The wnwrap consumer's polling timer +/// fires on a thread-pool thread; the only cross-thread surface +/// is the 's event handler, which +/// hand-offs into the thread-safe . +/// +/// +public sealed class AlarmCommandHandler : IAlarmCommandHandler +{ + private readonly MxAccessEventQueue eventQueue; + private readonly Func consumerFactory; + private readonly object syncRoot = new object(); + private AlarmDispatcher? dispatcher; + private bool disposed; + + public AlarmCommandHandler(MxAccessEventQueue eventQueue) + : this(eventQueue, () => new WnWrapAlarmConsumer()) + { + } + + /// Test seam — inject a custom consumer factory. + public AlarmCommandHandler( + MxAccessEventQueue eventQueue, + Func consumerFactory) + { + this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue)); + this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory)); + } + + public bool IsSubscribed + { + get { lock (syncRoot) return dispatcher is not null; } + } + + /// + public void Subscribe(string subscription, string sessionId) + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler)); + if (subscription is null) throw new ArgumentNullException(nameof(subscription)); + + lock (syncRoot) + { + if (dispatcher is not null) + { + throw new InvalidOperationException( + "AlarmCommandHandler already has an active subscription; " + + "call Unsubscribe before issuing another SubscribeAlarms command."); + } + IMxAccessAlarmConsumer consumer = consumerFactory() + ?? throw new InvalidOperationException("Alarm consumer factory returned null."); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink( + eventQueue, new MxAccessEventMapper()); + dispatcher = new AlarmDispatcher(consumer, sink, sessionId ?? string.Empty); + try + { + dispatcher.Subscribe(subscription); + } + catch + { + try { dispatcher.Dispose(); } catch { /* swallow */ } + dispatcher = null; + throw; + } + } + } + + /// + public void Unsubscribe() + { + AlarmDispatcher? toDispose; + lock (syncRoot) + { + toDispose = dispatcher; + dispatcher = null; + } + toDispose?.Dispose(); + } + + /// + public int Acknowledge( + Guid alarmGuid, + string comment, + string operatorUser, + string operatorNode, + string operatorDomain, + string operatorFullName) + { + AlarmDispatcher? d = GetDispatcherOrThrow(); + return d.Acknowledge( + alarmGuid, + comment ?? string.Empty, + operatorUser ?? string.Empty, + operatorNode ?? string.Empty, + operatorDomain ?? string.Empty, + operatorFullName ?? string.Empty); + } + + /// + public IReadOnlyList QueryActive(string? alarmFilterPrefix) + { + AlarmDispatcher? d = GetDispatcherOrThrow(); + IReadOnlyList all = d.SnapshotActiveAlarms(); + if (string.IsNullOrEmpty(alarmFilterPrefix)) return all; + List filtered = new List(all.Count); + foreach (ActiveAlarmSnapshot snap in all) + { + if (snap.AlarmFullReference.StartsWith(alarmFilterPrefix!, StringComparison.Ordinal)) + { + filtered.Add(snap); + } + } + return filtered; + } + + private AlarmDispatcher GetDispatcherOrThrow() + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler)); + AlarmDispatcher? d; + lock (syncRoot) d = dispatcher; + if (d is null) + { + throw new InvalidOperationException( + "AlarmCommandHandler has no active subscription; " + + "call SubscribeAlarms before issuing alarm-related commands."); + } + return d; + } + + /// + public void Dispose() + { + if (disposed) return; + disposed = true; + Unsubscribe(); + } +} + +/// +/// Per-session interface routing the worker's alarm IPC commands — +/// SubscribeAlarmsCommand, AcknowledgeAlarmCommand, +/// QueryActiveAlarmsCommand, UnsubscribeAlarmsCommand — +/// to the underlying . Production binding +/// is ; tests substitute a fake. +/// +public interface IAlarmCommandHandler : IDisposable +{ + /// Begin a subscription against the supplied AVEVA alarm-provider expression. + void Subscribe(string subscription, string sessionId); + + /// Tear down the active subscription. No-op if not subscribed. + void Unsubscribe(); + + /// Acknowledge a single alarm by GUID. Returns AVEVA's native status (0 = success). + int Acknowledge( + Guid alarmGuid, + string comment, + string operatorUser, + string operatorNode, + string operatorDomain, + string operatorFullName); + + /// + /// Snapshot the currently-active alarm set, optionally scoped to a + /// prefix matched against AlarmFullReference. + /// + IReadOnlyList QueryActive(string? alarmFilterPrefix); +} diff --git a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index 0ddb00a..bc37d7e 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -13,13 +13,14 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor { private readonly MxAccessSession session; private readonly VariantConverter variantConverter; + private readonly IAlarmCommandHandler? alarmCommandHandler; /// /// Initializes a command executor with an MXAccess session. /// /// MXAccess session on the STA thread. public MxAccessCommandExecutor(MxAccessSession session) - : this(session, new VariantConverter()) + : this(session, new VariantConverter(), alarmCommandHandler: null) { } @@ -31,9 +32,24 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor public MxAccessCommandExecutor( MxAccessSession session, VariantConverter variantConverter) + : this(session, variantConverter, alarmCommandHandler: null) + { + } + + /// + /// Initializes a command executor with an MXAccess session, variant + /// converter, and an alarm command handler. The alarm handler is + /// optional — when null, alarm-side commands return an + /// "alarm consumer not configured" diagnostic. + /// + public MxAccessCommandExecutor( + MxAccessSession session, + VariantConverter variantConverter, + IAlarmCommandHandler? alarmCommandHandler) { this.session = session ?? throw new ArgumentNullException(nameof(session)); this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter)); + this.alarmCommandHandler = alarmCommandHandler; } /// @@ -64,6 +80,10 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor MxCommandKind.UnAdviseItemBulk => ExecuteUnAdviseItemBulk(command), MxCommandKind.SubscribeBulk => ExecuteSubscribeBulk(command), MxCommandKind.UnsubscribeBulk => ExecuteUnsubscribeBulk(command), + MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command), + MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command), + MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command), + MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command), _ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."), }; } @@ -280,6 +300,153 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor session.UnsubscribeBulk(unsubscribeBulkCommand.ServerHandle, unsubscribeBulkCommand.ItemHandles)); } + private MxCommandReply ExecuteSubscribeAlarms(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.SubscribeAlarms) + { + return CreateInvalidRequestReply(command, "SubscribeAlarms command payload is required."); + } + if (alarmCommandHandler is null) + { + return CreateInvalidRequestReply( + command, + "SubscribeAlarms requires an alarm command handler; the worker was constructed without one."); + } + + string subscription = command.Command.SubscribeAlarms.SubscriptionExpression ?? string.Empty; + if (string.IsNullOrWhiteSpace(subscription)) + { + return CreateInvalidRequestReply(command, "SubscribeAlarms.subscription_expression is required."); + } + + try + { + alarmCommandHandler.Subscribe(subscription, command.SessionId); + return CreateOkReply(command); + } + catch (Exception ex) + { + return CreateAlarmFailureReply(command, ex); + } + } + + private MxCommandReply ExecuteUnsubscribeAlarms(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.UnsubscribeAlarms) + { + return CreateInvalidRequestReply(command, "UnsubscribeAlarms command payload is required."); + } + if (alarmCommandHandler is null) + { + // No handler configured — Unsubscribe is a no-op in that case; + // it can't be in a subscribed state to begin with. + return CreateOkReply(command); + } + + try + { + alarmCommandHandler.Unsubscribe(); + return CreateOkReply(command); + } + catch (Exception ex) + { + return CreateAlarmFailureReply(command, ex); + } + } + + private MxCommandReply ExecuteAcknowledgeAlarm(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AcknowledgeAlarmCommand) + { + return CreateInvalidRequestReply(command, "AcknowledgeAlarm command payload is required."); + } + if (alarmCommandHandler is null) + { + return CreateInvalidRequestReply( + command, + "AcknowledgeAlarm requires an alarm command handler; the worker was constructed without one."); + } + + AcknowledgeAlarmCommand payload = command.Command.AcknowledgeAlarmCommand; + if (!Guid.TryParse(payload.AlarmGuid, out Guid alarmGuid)) + { + return CreateInvalidRequestReply( + command, + $"AcknowledgeAlarm.alarm_guid is not a valid canonical GUID: '{payload.AlarmGuid}'."); + } + + try + { + int rc = alarmCommandHandler.Acknowledge( + alarmGuid, + payload.Comment, + payload.OperatorUser, + payload.OperatorNode, + payload.OperatorDomain, + payload.OperatorFullName); + MxCommandReply reply = CreateOkReply(command); + reply.Hresult = rc; + reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload + { + NativeStatus = rc, + }; + if (rc != 0) + { + reply.DiagnosticMessage = $"AVEVA AlarmAckByGUID returned non-zero status {rc}."; + } + return reply; + } + catch (Exception ex) + { + return CreateAlarmFailureReply(command, ex); + } + } + + private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand) + { + return CreateInvalidRequestReply(command, "QueryActiveAlarms command payload is required."); + } + if (alarmCommandHandler is null) + { + return CreateInvalidRequestReply( + command, + "QueryActiveAlarms requires an alarm command handler; the worker was constructed without one."); + } + + try + { + IReadOnlyList snapshots = alarmCommandHandler.QueryActive( + command.Command.QueryActiveAlarmsCommand.AlarmFilterPrefix); + QueryActiveAlarmsReplyPayload payload = new QueryActiveAlarmsReplyPayload(); + payload.Snapshots.AddRange(snapshots); + MxCommandReply reply = CreateOkReply(command); + reply.QueryActiveAlarms = payload; + return reply; + } + catch (Exception ex) + { + return CreateAlarmFailureReply(command, ex); + } + } + + private static MxCommandReply CreateAlarmFailureReply(StaCommand command, Exception exception) + { + return new MxCommandReply + { + SessionId = command.SessionId, + CorrelationId = command.CorrelationId, + Kind = command.Kind, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.MxaccessFailure, + Message = exception.Message, + }, + DiagnosticMessage = $"{exception.GetType().FullName}: {exception.Message}", + }; + } + private static MxCommandReply CreateOkReply(StaCommand command) { return new MxCommandReply diff --git a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs index 4dbf56e..4b223a9 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using MxGateway.Contracts.Proto; +using MxGateway.Worker.Conversion; using MxGateway.Worker.Sta; namespace MxGateway.Worker.MxAccess; @@ -14,8 +15,10 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession private readonly IMxAccessEventSink eventSink; private readonly MxAccessEventQueue eventQueue; private readonly StaRuntime staRuntime; + private readonly Func? alarmCommandHandlerFactory; private StaCommandDispatcher? commandDispatcher; private MxAccessSession? session; + private IAlarmCommandHandler? alarmCommandHandler; private bool disposed; /// @@ -69,11 +72,29 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession IMxAccessComObjectFactory factory, IMxAccessEventSink eventSink, MxAccessEventQueue eventQueue) + : this(staRuntime, factory, eventSink, eventQueue, alarmCommandHandlerFactory: null) + { + } + + /// + /// Initializes a new instance of with all + /// dependencies including an alarm-command handler factory. The factory is + /// invoked on the STA thread during ; + /// pass null to opt out of alarm-side commands (the worker rejects + /// them with an "alarm consumer not configured" diagnostic). + /// + public MxAccessStaSession( + StaRuntime staRuntime, + IMxAccessComObjectFactory factory, + IMxAccessEventSink eventSink, + MxAccessEventQueue eventQueue, + Func? alarmCommandHandlerFactory) { this.staRuntime = staRuntime ?? throw new ArgumentNullException(nameof(staRuntime)); this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); this.eventSink = eventSink ?? throw new ArgumentNullException(nameof(eventSink)); this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue)); + this.alarmCommandHandlerFactory = alarmCommandHandlerFactory; } /// @@ -117,9 +138,16 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession } session = MxAccessSession.Create(factory, eventSink, sessionId); + if (alarmCommandHandlerFactory is not null) + { + alarmCommandHandler = alarmCommandHandlerFactory(eventQueue); + } commandDispatcher = new StaCommandDispatcher( staRuntime, - new MxAccessCommandExecutor(session)); + new MxAccessCommandExecutor( + session, + new VariantConverter(), + alarmCommandHandler)); return session.CreateWorkerReady(workerProcessId); }, @@ -279,6 +307,27 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession commandDispatcher?.RequestShutdown(); + // Stop the alarm consumer's polling timer and tear down the + // dispatcher BEFORE the data-side cleanup begins. The alarm + // consumer holds a wnwrap COM RCW that needs the STA pump to + // unwind cleanly; doing it here gives it the opportunity while + // the STA is still alive. + IAlarmCommandHandler? alarmHandlerToDispose = alarmCommandHandler; + alarmCommandHandler = null; + if (alarmHandlerToDispose is not null) + { + try + { + await staRuntime.InvokeAsync( + () => alarmHandlerToDispose.Dispose(), + cancellationToken).ConfigureAwait(false); + } + catch + { + // Swallow — alarm cleanup must not block data shutdown. + } + } + Stopwatch stopwatch = Stopwatch.StartNew(); MxAccessShutdownResult result; if (session is null) @@ -333,6 +382,19 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession RequestShutdown(); + IAlarmCommandHandler? alarmHandlerToDispose = alarmCommandHandler; + alarmCommandHandler = null; + if (alarmHandlerToDispose is not null) + { + try + { + staRuntime.InvokeAsync(() => alarmHandlerToDispose.Dispose()) + .Wait(TimeSpan.FromSeconds(2)); + } + catch (AggregateException) { } + catch (ObjectDisposedException) { } + } + if (session is not null) { try -- 2.52.0 From 9b21ca3554dbd342ef41213939f6cbcfd04f77d4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 10:58:40 -0400 Subject: [PATCH 13/16] A.3 (gateway dispatcher): WorkerAlarmRpcDispatcher routes alarm RPCs over the worker pipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces NotWiredAlarmRpcDispatcher in DI with a production implementation that issues the new MxCommandKind.{AcknowledgeAlarm, QueryActiveAlarms} commands across the IPC and unwraps the resulting MxCommandReply into the public RPC types. QueryActiveAlarms is fully wired: builds the QueryActiveAlarmsCommand (forwarding alarm_filter_prefix), invokes it on the resolved GatewaySession's worker client, and yields each ActiveAlarmSnapshot from the QueryActiveAlarmsReplyPayload as the RPC stream. Worker failures + missing sessions yield an empty stream — matches the ConditionRefresh contract clients already speak to. AcknowledgeAlarm is partially wired: the public RPC takes AlarmFullReference (Provider!Group.Tag), but the worker's wnwrap consumer acks by GUID. Strategy: - If AlarmFullReference parses as a canonical GUID, forward it directly through MxCommandKind.AcknowledgeAlarm. Native status flows back via MxCommandReply.Hresult and the dedicated AcknowledgeAlarmReplyPayload.NativeStatus. - Otherwise, return InvalidRequest with a clear diagnostic naming the follow-up — reference→GUID lookup needs a worker-side AlarmAckByName command wrapping wwAlarmConsumerClass.AlarmAckByName. DI: SessionServiceCollectionExtensions registers WorkerAlarmRpcDispatcher as the default IAlarmRpcDispatcher; MxAccessGatewayService picks it up via constructor injection. NotWiredAlarmRpcDispatcher is retained for test fixtures that want the no-side-effect fake. Tests: 7 new unit tests cover session-not-found short-circuit, GUID-vs- reference branching, native-status propagation, worker MxaccessFailure diagnostic propagation, and snapshot-stream yielding. Server test suite total: 288/0 fail. Solution builds clean. End-to-end alarms-over-gateway pipeline status: consumer → sink → queue (A.2 + A.3 in-process slice) worker IPC commands (A.3 worker slice) gateway dispatcher (this slice) Remaining for full E2E: - Auto-issue SubscribeAlarms on session open (or add a public SubscribeAlarms RPC). Without this trigger the consumer never starts and Acknowledge/Query return "not subscribed". - AlarmAckByName worker command for ack-by-reference. - End-to-end live test against the dev rig. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SessionServiceCollectionExtensions.cs | 1 + .../Sessions/WorkerAlarmRpcDispatcher.cs | 172 ++++++++++ .../Sessions/WorkerAlarmRpcDispatcherTests.cs | 300 ++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100644 src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs create mode 100644 src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs diff --git a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs index 5b7eae4..1f1e67b 100644 --- a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs +++ b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static class SessionServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); diff --git a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs new file mode 100644 index 0000000..89e6382 --- /dev/null +++ b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using MxGateway.Contracts.Proto; +using MxGateway.Server.Grpc; + +namespace MxGateway.Server.Sessions; + +/// +/// Production that routes the public +/// AcknowledgeAlarm + QueryActiveAlarms RPCs through the +/// worker pipe IPC. Replaces +/// once the worker AlarmCommandHandler is wired in. +/// +/// +/// +/// QueryActiveAlarms is fully wired: issues a +/// over the pipe and yields +/// each from the +/// . +/// +/// +/// AcknowledgeAlarm is partially wired: the public RPC's +/// is a +/// Provider!Group.Tag string, but the worker's wnwrap consumer +/// acks by GUID. When the supplied reference parses as a GUID +/// directly, the dispatcher forwards it as-is. Otherwise it +/// returns an Unimplemented diagnostic. Resolving +/// reference→GUID requires an additional worker IPC command +/// (e.g. AlarmAckByName wrapping +/// wwAlarmConsumerClass.AlarmAckByName) and is tracked as +/// a follow-up. +/// +/// +public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher +{ + private readonly ISessionRegistry sessionRegistry; + private readonly TimeProvider timeProvider; + + public WorkerAlarmRpcDispatcher(ISessionRegistry sessionRegistry, TimeProvider? timeProvider = null) + { + this.sessionRegistry = sessionRegistry ?? throw new System.ArgumentNullException(nameof(sessionRegistry)); + this.timeProvider = timeProvider ?? TimeProvider.System; + } + + /// + public async Task AcknowledgeAsync( + AcknowledgeAlarmRequest request, + CancellationToken cancellationToken) + { + if (request is null) throw new System.ArgumentNullException(nameof(request)); + + if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession session)) + { + return new AcknowledgeAlarmReply + { + SessionId = request.SessionId, + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = MxAccessGrpcMapper.SessionNotFound( + $"Session '{request.SessionId}' not found."), + DiagnosticMessage = "AcknowledgeAlarm: session not found.", + }; + } + + if (!System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid)) + { + // Reference→GUID lookup not yet implemented. Surface a clear + // diagnostic so client teams can plumb the reference parser + // when the worker AlarmAckByName command lands. + return new AcknowledgeAlarmReply + { + SessionId = request.SessionId, + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.InvalidRequest, + Message = "AlarmFullReference must currently be a canonical GUID; reference→GUID lookup is pending the AlarmAckByName worker command.", + }, + DiagnosticMessage = $"AcknowledgeAlarm received non-GUID reference '{request.AlarmFullReference}'.", + }; + } + + WorkerCommand workerCommand = new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = guid.ToString(), + Comment = request.Comment ?? string.Empty, + OperatorUser = request.OperatorUser ?? string.Empty, + // Operator node/domain/full-name are not on the public + // RPC surface today; pass empty strings so the worker + // honours the existing AcknowledgeAlarmCommand schema. + OperatorNode = string.Empty, + OperatorDomain = string.Empty, + OperatorFullName = string.Empty, + }, + }, + EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), + }; + + WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken) + .ConfigureAwait(false); + + MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply + { + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.ProtocolViolation, + Message = "Worker reply did not include an MxCommandReply.", + }, + }; + + AcknowledgeAlarmReply reply = new AcknowledgeAlarmReply + { + SessionId = request.SessionId, + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = mxReply.ProtocolStatus ?? MxAccessGrpcMapper.Ok(), + DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty, + }; + if (mxReply.HasHresult) + { + reply.Hresult = mxReply.Hresult; + } + return reply; + } + + /// + public async IAsyncEnumerable QueryActiveAlarmsAsync( + QueryActiveAlarmsRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (request is null) throw new System.ArgumentNullException(nameof(request)); + + if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession session)) + { + yield break; + } + + WorkerCommand workerCommand = new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.QueryActiveAlarms, + QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand + { + AlarmFilterPrefix = request.AlarmFilterPrefix ?? string.Empty, + }, + }, + EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), + }; + + WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken) + .ConfigureAwait(false); + + MxCommandReply? mxReply = workerReply.Reply; + if (mxReply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok) yield break; + + QueryActiveAlarmsReplyPayload? payload = mxReply.QueryActiveAlarms; + if (payload is null) yield break; + + foreach (ActiveAlarmSnapshot snapshot in payload.Snapshots) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return snapshot; + } + } +} diff --git a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs new file mode 100644 index 0000000..ae56f0b --- /dev/null +++ b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using MxGateway.Contracts.Proto; +using MxGateway.Server.Sessions; +using MxGateway.Server.Workers; + +namespace MxGateway.Tests.Gateway.Sessions; + +/// +/// Pins the production 's behaviour: +/// resolves the session by id, issues the matching MxCommand over the +/// worker pipe, and unwraps the reply into AcknowledgeAlarmReply or the +/// ActiveAlarmSnapshot stream. +/// +public sealed class WorkerAlarmRpcDispatcherTests +{ + [Fact] + public async Task AcknowledgeAsync_returns_session_not_found_when_session_missing() + { + SessionRegistry registry = new SessionRegistry(); + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "missing", + ClientCorrelationId = "c1", + AlarmFullReference = Guid.NewGuid().ToString(), + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code); + } + + [Fact] + public async Task AcknowledgeAsync_returns_invalid_request_when_reference_is_not_a_guid() + { + SessionRegistry registry = new SessionRegistry(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient(); + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "s1", + ClientCorrelationId = "c1", + AlarmFullReference = "Galaxy!Area.Tag", // not a GUID + Comment = "x", + OperatorUser = "u", + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + Assert.Equal(0, worker.InvokeCount); // dispatcher short-circuits before the IPC. + } + + [Fact] + public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status() + { + SessionRegistry registry = new SessionRegistry(); + Guid alarmGuid = Guid.NewGuid(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient + { + ReplyFactory = command => + { + Assert.Equal(MxCommandKind.AcknowledgeAlarm, command.Command.Kind); + Assert.Equal(alarmGuid.ToString(), command.Command.AcknowledgeAlarmCommand.AlarmGuid); + Assert.Equal("ack", command.Command.AcknowledgeAlarmCommand.Comment); + Assert.Equal("alice", command.Command.AcknowledgeAlarmCommand.OperatorUser); + return new MxCommandReply + { + Kind = MxCommandKind.AcknowledgeAlarm, + ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, + Hresult = 0, + AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 }, + }; + }, + }; + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "s1", + ClientCorrelationId = "c1", + AlarmFullReference = alarmGuid.ToString(), + Comment = "ack", + OperatorUser = "alice", + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(0, reply.Hresult); + Assert.Equal("s1", reply.SessionId); + Assert.Equal("c1", reply.CorrelationId); + Assert.Equal(1, worker.InvokeCount); + } + + [Fact] + public async Task AcknowledgeAsync_propagates_worker_diagnostic_on_failure() + { + SessionRegistry registry = new SessionRegistry(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient + { + ReplyFactory = _ => new MxCommandReply + { + Kind = MxCommandKind.AcknowledgeAlarm, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.MxaccessFailure, + Message = "AVEVA Acknowledge failed.", + }, + Hresult = -123, + DiagnosticMessage = "AVEVA AlarmAckByGUID returned non-zero status -123.", + }, + }; + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "s1", + AlarmFullReference = Guid.NewGuid().ToString(), + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); + Assert.Equal(-123, reply.Hresult); + Assert.Contains("-123", reply.DiagnosticMessage); + } + + [Fact] + public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload() + { + SessionRegistry registry = new SessionRegistry(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient + { + ReplyFactory = command => + { + Assert.Equal(MxCommandKind.QueryActiveAlarms, command.Command.Kind); + QueryActiveAlarmsReplyPayload payload = new QueryActiveAlarmsReplyPayload(); + payload.Snapshots.Add(new ActiveAlarmSnapshot + { + AlarmFullReference = "Galaxy!A.T1", + CurrentState = AlarmConditionState.Active, + Severity = 500, + }); + payload.Snapshots.Add(new ActiveAlarmSnapshot + { + AlarmFullReference = "Galaxy!A.T2", + CurrentState = AlarmConditionState.ActiveAcked, + Severity = 100, + }); + return new MxCommandReply + { + Kind = MxCommandKind.QueryActiveAlarms, + ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, + QueryActiveAlarms = payload, + }; + }, + }; + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + List collected = new List(); + await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync( + new QueryActiveAlarmsRequest { SessionId = "s1" }, + CancellationToken.None)) + { + collected.Add(snap); + } + + Assert.Equal(2, collected.Count); + Assert.Equal("Galaxy!A.T1", collected[0].AlarmFullReference); + Assert.Equal("Galaxy!A.T2", collected[1].AlarmFullReference); + } + + [Fact] + public async Task QueryActiveAlarmsAsync_yields_empty_when_session_missing() + { + SessionRegistry registry = new SessionRegistry(); + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + List collected = new List(); + await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync( + new QueryActiveAlarmsRequest { SessionId = "missing" }, + CancellationToken.None)) + { + collected.Add(snap); + } + + Assert.Empty(collected); + } + + [Fact] + public async Task QueryActiveAlarmsAsync_yields_empty_on_worker_failure() + { + SessionRegistry registry = new SessionRegistry(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient + { + ReplyFactory = _ => new MxCommandReply + { + Kind = MxCommandKind.QueryActiveAlarms, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.MxaccessFailure, + Message = "alarm consumer not subscribed", + }, + }, + }; + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + List collected = new List(); + await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync( + new QueryActiveAlarmsRequest { SessionId = "s1" }, + CancellationToken.None)) + { + collected.Add(snap); + } + + Assert.Empty(collected); + } + + private static GatewaySession NewSession(string sessionId) + { + return new GatewaySession( + sessionId, + "mxaccess", + $"mxaccess-gateway-1-{sessionId}", + "nonce", + "client-1", + "test-session", + "client-correlation-1", + commandTimeout: TimeSpan.FromSeconds(30), + startupTimeout: TimeSpan.FromSeconds(5), + shutdownTimeout: TimeSpan.FromSeconds(5), + leaseDuration: TimeSpan.FromMinutes(30), + openedAt: DateTimeOffset.UtcNow); + } + + private sealed class FakeAlarmWorkerClient : IWorkerClient + { + public string SessionId { get; } = "session-1"; + public int? ProcessId { get; } = 1; + public WorkerClientState State { get; } = WorkerClientState.Ready; + public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow; + + public Func? ReplyFactory { get; set; } + public int InvokeCount { get; private set; } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task InvokeAsync( + WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken) + { + InvokeCount++; + MxCommandReply reply = ReplyFactory?.Invoke(command) ?? new MxCommandReply(); + return Task.FromResult(new WorkerCommandReply { Reply = reply }); + } + + public async IAsyncEnumerable ReadEventsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.CompletedTask; + yield break; + } + + public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask; + public void Kill(string reason) { } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} -- 2.52.0 From 47b1fd422c4a42fb71d9c3a9a3c0aed3bb06faed Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 11:10:13 -0400 Subject: [PATCH 14/16] A.3 (auto-subscribe): SessionManager issues SubscribeAlarms on session open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing trigger that activates the worker's wnwrap consumer. Without this, every session opened in OK state but the consumer never started, so AcknowledgeAlarm/QueryActiveAlarms returned "alarm consumer not configured" forever. New AlarmsOptions config block (under MxGateway:Alarms): - Enabled (default false): gates the auto-subscribe path so existing deployments without alarm configuration are unaffected. - SubscriptionExpression: explicit AVEVA expression like \\Galaxy!. - DefaultArea: fallback used when SubscriptionExpression is empty; composes \$(MachineName)\Galaxy!$(DefaultArea). - RequireSubscribeOnOpen (default false): when true, an auto-subscribe failure faults the session; when false, the failure is logged and the session stays Ready (data subscriptions keep working, alarms return "not subscribed" until the operator retries). SessionManager.OpenSessionAsync gains a TryAutoSubscribeAlarmsAsync hook that runs after MarkReady. Skips when alarms are disabled; otherwise builds a SubscribeAlarmsCommand, invokes it on the session's worker client, and either logs the resulting status or escalates per RequireSubscribeOnOpen. SessionManagerException is the failure mode for the strict path so callers in MxAccessGatewayService surface it as session-open-failed. Tests: 7 new unit tests cover the disabled lane, expression-driven subscribe, DefaultArea fallback, success path, soft-failure (require off), strict-failure (require on), and missing-config-strict-throw. Server suite total: 295 pass / 0 fail. Solution builds clean. End-to-end alarms-over-gateway path is now live (with config). Open a session against a gateway with Alarms.Enabled=true + a valid SubscriptionExpression; the worker's wnwrap consumer auto-subscribes; QueryActiveAlarms streams snapshots; AcknowledgeAlarm acks by GUID. Reference→GUID resolution (AlarmAckByName worker command) and the live dev-rig smoke test remain follow-ups. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Configuration/AlarmsOptions.cs | 48 ++++ .../Configuration/GatewayOptions.cs | 7 + .../Sessions/SessionManager.cs | 99 +++++++ .../SessionManagerAlarmAutoSubscribeTests.cs | 266 ++++++++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 src/MxGateway.Server/Configuration/AlarmsOptions.cs create mode 100644 src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs diff --git a/src/MxGateway.Server/Configuration/AlarmsOptions.cs b/src/MxGateway.Server/Configuration/AlarmsOptions.cs new file mode 100644 index 0000000..2722fac --- /dev/null +++ b/src/MxGateway.Server/Configuration/AlarmsOptions.cs @@ -0,0 +1,48 @@ +namespace MxGateway.Server.Configuration; + +/// +/// Per-gateway alarm-subsystem configuration. Drives the auto-subscribe +/// hook in : when +/// is true and a session reaches Ready, the +/// manager issues a SubscribeAlarmsCommand to the worker with +/// the configured . +/// +/// +/// Defaults preserve current behaviour (alarms disabled). Operators +/// opt in by setting MxGateway:Alarms:Enabled = true and +/// supplying a canonical +/// \\<machine>\Galaxy!<area> subscription +/// expression. The literal "Galaxy" provider is correct regardless of +/// the configured Galaxy database name (the wnwrap consumer doesn't +/// accept the database name as the provider). +/// +public sealed class AlarmsOptions +{ + /// Gate the auto-subscribe hook on session open. Default false. + public bool Enabled { get; init; } + + /// + /// AVEVA alarm-subscription expression. When empty and + /// is true, the gateway falls back to + /// \\$(MachineName)\Galaxy!$(DefaultArea) if + /// is set; otherwise the session open + /// fails with a configuration diagnostic. + /// + public string SubscriptionExpression { get; init; } = string.Empty; + + /// + /// Optional area name used to compose a default subscription when + /// is empty. Combined with + /// Environment.MachineName as + /// \\<MachineName>\Galaxy!<DefaultArea>. + /// + public string DefaultArea { get; init; } = string.Empty; + + /// + /// If true, an auto-subscribe failure faults the session. If false + /// (default), the failure is logged and the session remains Ready — + /// alarm-side commands return "not subscribed" but data subscriptions + /// work normally. + /// + public bool RequireSubscribeOnOpen { get; init; } +} diff --git a/src/MxGateway.Server/Configuration/GatewayOptions.cs b/src/MxGateway.Server/Configuration/GatewayOptions.cs index d2a7713..312dd4a 100644 --- a/src/MxGateway.Server/Configuration/GatewayOptions.cs +++ b/src/MxGateway.Server/Configuration/GatewayOptions.cs @@ -35,4 +35,11 @@ public sealed class GatewayOptions /// Gets protocol configuration options. /// public ProtocolOptions Protocol { get; init; } = new(); + + /// + /// Gets alarm-subsystem configuration options. Drives the gateway's + /// auto-subscribe-on-session-open hook; default values preserve legacy + /// behaviour (alarms disabled). + /// + public AlarmsOptions Alarms { get; init; } = new(); } diff --git a/src/MxGateway.Server/Sessions/SessionManager.cs b/src/MxGateway.Server/Sessions/SessionManager.cs index cef157b..3ea5a3f 100644 --- a/src/MxGateway.Server/Sessions/SessionManager.cs +++ b/src/MxGateway.Server/Sessions/SessionManager.cs @@ -87,6 +87,8 @@ public sealed class SessionManager : ISessionManager session.MarkReady(); _metrics.SessionOpened(); + await TryAutoSubscribeAlarmsAsync(session, cancellationToken).ConfigureAwait(false); + return session; } catch (Exception exception) @@ -396,4 +398,101 @@ public sealed class SessionManager : ISessionManager return Convert.ToBase64String(bytes); } + + /// + /// If Alarms.Enabled is configured, issue a + /// SubscribeAlarmsCommand on the freshly-Ready session so the + /// worker's wnwrap consumer starts polling. Failure handling is + /// governed by Alarms.RequireSubscribeOnOpen: + /// + /// true — propagate the failure to fault the session. + /// false (default) — log a warning and let the session continue serving data subscriptions. + /// + /// + private async Task TryAutoSubscribeAlarmsAsync( + GatewaySession session, + CancellationToken cancellationToken) + { + AlarmsOptions alarms = _options.Alarms; + if (!alarms.Enabled) return; + + string subscription = ResolveAlarmSubscription(alarms); + if (string.IsNullOrWhiteSpace(subscription)) + { + const string diagnostic = + "Alarms.Enabled is true but no SubscriptionExpression / DefaultArea is configured."; + if (alarms.RequireSubscribeOnOpen) + { + throw new SessionManagerException( + SessionManagerErrorCode.OpenFailed, diagnostic); + } + _logger.LogWarning( + "Auto-subscribe skipped for session {SessionId}: {Diagnostic}", + session.SessionId, diagnostic); + return; + } + + WorkerCommand command = new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.SubscribeAlarms, + SubscribeAlarms = new SubscribeAlarmsCommand + { + SubscriptionExpression = subscription, + }, + }, + EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()), + }; + + try + { + WorkerCommandReply reply = await session.InvokeAsync(command, cancellationToken) + .ConfigureAwait(false); + ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code; + if (code != ProtocolStatusCode.Ok) + { + string diagnostic = reply.Reply?.DiagnosticMessage + ?? reply.Reply?.ProtocolStatus?.Message + ?? "Worker rejected SubscribeAlarms."; + if (alarms.RequireSubscribeOnOpen) + { + throw new SessionManagerException( + SessionManagerErrorCode.OpenFailed, + $"Auto-subscribe failed for session {session.SessionId}: {diagnostic}"); + } + _logger.LogWarning( + "Auto-subscribe failed for session {SessionId} (status {StatusCode}): {Diagnostic}", + session.SessionId, code, diagnostic); + return; + } + _logger.LogInformation( + "Alarm auto-subscribe succeeded for session {SessionId} on {Subscription}.", + session.SessionId, subscription); + } + catch (SessionManagerException) + { + throw; + } + catch (Exception ex) when (!alarms.RequireSubscribeOnOpen) + { + _logger.LogWarning( + ex, + "Auto-subscribe threw for session {SessionId} on {Subscription}; alarm path remains inactive.", + session.SessionId, subscription); + } + } + + private static string ResolveAlarmSubscription(AlarmsOptions alarms) + { + if (!string.IsNullOrWhiteSpace(alarms.SubscriptionExpression)) + { + return alarms.SubscriptionExpression; + } + if (!string.IsNullOrWhiteSpace(alarms.DefaultArea)) + { + return $@"\\{Environment.MachineName}\Galaxy!{alarms.DefaultArea}"; + } + return string.Empty; + } } diff --git a/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs b/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs new file mode 100644 index 0000000..89dfc84 --- /dev/null +++ b/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs @@ -0,0 +1,266 @@ +using System.Runtime.CompilerServices; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Options; +using MxGateway.Contracts.Proto; +using MxGateway.Server.Configuration; +using MxGateway.Server.Metrics; +using MxGateway.Server.Sessions; +using MxGateway.Server.Workers; + +namespace MxGateway.Tests.Gateway.Sessions; + +/// +/// Pins the alarm auto-subscribe hook on session open. Runs in +/// its own file because the cases are orthogonal to +/// (alarms-disabled vs. +/// alarms-enabled lanes), and the fake worker client below verifies +/// the issued SubscribeAlarms command shape directly. +/// +public sealed class SessionManagerAlarmAutoSubscribeTests +{ + [Fact] + public async Task OpenSessionAsync_DoesNotAutoSubscribe_WhenAlarmsDisabled() + { + AlarmAutoSubscribeWorkerClient worker = new(); + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions { Enabled = false }); + + await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None); + + Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); + } + + [Fact] + public async Task OpenSessionAsync_AutoSubscribes_WhenEnabledWithExpression() + { + AlarmAutoSubscribeWorkerClient worker = new(); + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + SubscriptionExpression = @"\\HOST\Galaxy!Area1", + }); + + GatewaySession session = await manager.OpenSessionAsync( + CreateOpenRequest(), "client-1", CancellationToken.None); + + Assert.Equal(SessionState.Ready, session.State); + Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); + Assert.Equal(@"\\HOST\Galaxy!Area1", + worker.LastSubscribeAlarmsCommand!.SubscriptionExpression); + } + + [Fact] + public async Task OpenSessionAsync_FallsBackToDefaultArea_WhenExpressionEmpty() + { + AlarmAutoSubscribeWorkerClient worker = new(); + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + DefaultArea = "DEV", + }); + + await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None); + + Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); + Assert.Contains(@"\Galaxy!DEV", + worker.LastSubscribeAlarmsCommand!.SubscriptionExpression); + } + + [Fact] + public async Task OpenSessionAsync_Succeeds_WhenAutoSubscribeFailsWithRequireOff() + { + // Worker rejects the SubscribeAlarms command. With RequireSubscribeOnOpen=false + // (the default), the session still opens — alarm-side commands later return + // "not subscribed", but data subscriptions work. + AlarmAutoSubscribeWorkerClient worker = new() + { + SubscribeAlarmsReplyFactory = _ => new MxCommandReply + { + Kind = MxCommandKind.SubscribeAlarms, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.MxaccessFailure, + Message = "wnwrap subscribe failed", + }, + DiagnosticMessage = "alarm provider unavailable", + }, + }; + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + SubscriptionExpression = @"\\HOST\Galaxy!Area1", + RequireSubscribeOnOpen = false, + }); + + GatewaySession session = await manager.OpenSessionAsync( + CreateOpenRequest(), "client-1", CancellationToken.None); + + Assert.Equal(SessionState.Ready, session.State); + Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); + } + + [Fact] + public async Task OpenSessionAsync_Throws_WhenAutoSubscribeFailsWithRequireOn() + { + AlarmAutoSubscribeWorkerClient worker = new() + { + SubscribeAlarmsReplyFactory = _ => new MxCommandReply + { + Kind = MxCommandKind.SubscribeAlarms, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.MxaccessFailure, + Message = "wnwrap subscribe failed", + }, + }, + }; + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + SubscriptionExpression = @"\\HOST\Galaxy!Area1", + RequireSubscribeOnOpen = true, + }); + + await Assert.ThrowsAsync( + async () => await manager.OpenSessionAsync( + CreateOpenRequest(), "client-1", CancellationToken.None)); + } + + [Fact] + public async Task OpenSessionAsync_Throws_WhenEnabledButNoExpressionAndRequireOn() + { + AlarmAutoSubscribeWorkerClient worker = new(); + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + // No SubscriptionExpression and no DefaultArea. + RequireSubscribeOnOpen = true, + }); + + await Assert.ThrowsAsync( + async () => await manager.OpenSessionAsync( + CreateOpenRequest(), "client-1", CancellationToken.None)); + Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); + } + + [Fact] + public async Task OpenSessionAsync_Succeeds_WhenEnabledButNoExpressionAndRequireOff() + { + AlarmAutoSubscribeWorkerClient worker = new(); + SessionManager manager = NewManager(worker, alarms: new AlarmsOptions + { + Enabled = true, + // No SubscriptionExpression and no DefaultArea — default require=false. + }); + + GatewaySession session = await manager.OpenSessionAsync( + CreateOpenRequest(), "client-1", CancellationToken.None); + + Assert.Equal(SessionState.Ready, session.State); + Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); + } + + private static SessionManager NewManager( + AlarmAutoSubscribeWorkerClient worker, + AlarmsOptions alarms) + { + FakeSessionWorkerClientFactory factory = new(worker); + GatewayOptions options = new GatewayOptions + { + Sessions = new SessionOptions + { + DefaultCommandTimeoutSeconds = 30, + MaxSessions = 64, + DefaultLeaseSeconds = 1800, + }, + Worker = new WorkerOptions + { + StartupTimeoutSeconds = 30, + ShutdownTimeoutSeconds = 10, + }, + Alarms = alarms, + }; + return new SessionManager( + new SessionRegistry(), + factory, + Options.Create(options), + new GatewayMetrics()); + } + + private static SessionOpenRequest CreateOpenRequest() + { + return new SessionOpenRequest( + RequestedBackend: null, + ClientSessionName: "test-session", + ClientCorrelationId: "client-correlation-1", + CommandTimeout: Duration.FromTimeSpan(TimeSpan.FromSeconds(5))); + } + + private sealed class FakeSessionWorkerClientFactory(IWorkerClient client) : ISessionWorkerClientFactory + { + public Task CreateAsync( + GatewaySession session, + CancellationToken cancellationToken) + { + return Task.FromResult(client); + } + } + + private sealed class AlarmAutoSubscribeWorkerClient : IWorkerClient + { + public string SessionId { get; } = "session-1"; + public int? ProcessId { get; } = 1234; + public WorkerClientState State { get; set; } = WorkerClientState.Ready; + public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow; + + public int SubscribeAlarmsInvokeCount { get; private set; } + public SubscribeAlarmsCommand? LastSubscribeAlarmsCommand { get; private set; } + public Func? SubscribeAlarmsReplyFactory { get; init; } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task InvokeAsync( + WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken) + { + if (command.Command?.Kind == MxCommandKind.SubscribeAlarms) + { + SubscribeAlarmsInvokeCount++; + LastSubscribeAlarmsCommand = command.Command.SubscribeAlarms; + MxCommandReply reply = SubscribeAlarmsReplyFactory?.Invoke(command) + ?? new MxCommandReply + { + Kind = MxCommandKind.SubscribeAlarms, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.Ok, + Message = "OK", + }, + }; + return Task.FromResult(new WorkerCommandReply { Reply = reply }); + } + return Task.FromResult(new WorkerCommandReply + { + Reply = new MxCommandReply + { + Kind = command.Command?.Kind ?? MxCommandKind.Unspecified, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.Ok, + Message = "OK", + }, + }, + }); + } + + public async IAsyncEnumerable ReadEventsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.CompletedTask; + yield break; + } + + public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) + => Task.CompletedTask; + public void Kill(string reason) { } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} -- 2.52.0 From 4e02927f019b0c03a12a5a588c87fe112f25bcb7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 11:17:15 -0400 Subject: [PATCH 15/16] A.3 (alarm-ack-by-name): public AcknowledgeAlarm now accepts Provider!Group.Tag references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the gap where the public AcknowledgeAlarm RPC required canonical GUIDs but OnAlarmTransitionEvent.AlarmFullReference is "Provider!Group.Tag". Adds an AVEVA AlarmAckByName path that wraps wwAlarmConsumerClass.AlarmAckByName so callers can ack with the natural reference. Proto: - New MxCommandKind.AcknowledgeAlarmByName (=29). - New AcknowledgeAlarmByNameCommand(alarm_name, provider_name, group_name, comment, operator_user/node/domain/full_name) on MxCommand oneof. - AcknowledgeAlarmReplyPayload (existing) carries the AVEVA native status; reused for the by-name path. Worker: - IMxAccessAlarmConsumer + WnWrapAlarmConsumer + AlarmDispatcher + AlarmCommandHandler all gain an AcknowledgeByName(name, provider, group, comment, operator-identity) overload that maps to wwAlarmConsumerClass.AlarmAckByName. - MxAccessCommandExecutor: new switch arm routes MxCommandKind.AcknowledgeAlarmByName to the handler. Empty alarm_name yields InvalidRequest; handler exceptions surface as MxaccessFailure. Gateway: - WorkerAlarmRpcDispatcher.TryParseAlarmReference: parses "Provider!Group.Tag" with the convention that the FIRST '!' separates provider, the FIRST '.' after '!' separates group; tag may contain more dots. - AcknowledgeAsync now branches: GUID input → AcknowledgeAlarm command (existing path); reference input → AcknowledgeAlarmByName command (new path); neither parses → InvalidRequest with a clear diagnostic. Tests: 13 new unit tests cover each layer end-to-end: - WorkerAlarmRpcDispatcher.TryParseAlarmReference (3 valid + 8 invalid forms) including the realistic 4-component "Galaxy!TestArea. TestMachine_001.TestAlarm001" reference. - WorkerAlarmRpcDispatcher.AcknowledgeAsync routes references through AcknowledgeAlarmByName + propagates the full operator tuple. - Executor switch arm carries the by-name tuple and rejects empty alarm_name. - AlarmDispatcher.AcknowledgeByName forwards to consumer. - Existing fakes extended for the new overload. Counts: server 308/0, worker 195/3 skip / 1 pre-existing structure-fail (untouched). Solution builds clean. End-to-end alarms-over-gateway now serves the full lmxopcua flow: client.AcknowledgeAlarm(reference="Galaxy!TestArea.TestMachine_001.TestAlarm001", operator_user="alice") → gateway parses → IPC AcknowledgeAlarmByName → worker AlarmAckByName → AVEVA history. The remaining piece for full parity is a live dev-rig smoke test. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Generated/MxaccessGateway.cs | 1272 ++++++++++++----- .../Protos/mxaccess_gateway.proto | 23 + .../Sessions/WorkerAlarmRpcDispatcher.cs | 113 +- .../Sessions/WorkerAlarmRpcDispatcherTests.cs | 128 +- .../MxAccess/AlarmCommandExecutorTests.cs | 68 + .../MxAccess/AlarmCommandHandlerTests.cs | 12 + .../MxAccess/AlarmDispatcherTests.cs | 38 + .../MxAccess/AlarmCommandHandler.cs | 37 + .../MxAccess/AlarmDispatcher.cs | 27 + .../MxAccess/IMxAccessAlarmConsumer.cs | 17 + .../MxAccess/MxAccessCommandExecutor.cs | 49 + .../MxAccess/WnWrapAlarmConsumer.cs | 27 + 12 files changed, 1390 insertions(+), 421 deletions(-) diff --git a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs index d49b977..ed8b018 100644 --- a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -46,7 +46,7 @@ namespace MxGateway.Contracts.Proto { "ZnRlcl93b3JrZXJfc2VxdWVuY2UYAiABKAQidgoQTXhDb21tYW5kUmVxdWVz", "dBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9p", "ZBgCIAEoCRIvCgdjb21tYW5kGAMgASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5NeENvbW1hbmQijhIKCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", + "MS5NeENvbW1hbmQi7xIKCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRLaW5kEjgKCHJlZ2lzdGVyGAog", "ASgLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3RlckNvbW1hbmRIABI8", "Cgp1bnJlZ2lzdGVyGAsgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnJl", @@ -90,329 +90,337 @@ namespace MxGateway.Contracts.Proto { "b21tYW5kGCQgASgLMiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRn", "ZUFsYXJtQ29tbWFuZEgAElQKG3F1ZXJ5X2FjdGl2ZV9hbGFybXNfY29tbWFu", "ZBglIAEoCzItLm14YWNjZXNzX2dhdGV3YXkudjEuUXVlcnlBY3RpdmVBbGFy", - "bXNDb21tYW5kSAASMAoEcGluZxhkIAEoCzIgLm14YWNjZXNzX2dhdGV3YXku", - "djEuUGluZ0NvbW1hbmRIABJIChFnZXRfc2Vzc2lvbl9zdGF0ZRhlIAEoCzIr", - "Lm14YWNjZXNzX2dhdGV3YXkudjEuR2V0U2Vzc2lvblN0YXRlQ29tbWFuZEgA", - "EkQKD2dldF93b3JrZXJfaW5mbxhmIAEoCzIpLm14YWNjZXNzX2dhdGV3YXku", - "djEuR2V0V29ya2VySW5mb0NvbW1hbmRIABI/CgxkcmFpbl9ldmVudHMYZyAB", - "KAsyJy5teGFjY2Vzc19nYXRld2F5LnYxLkRyYWluRXZlbnRzQ29tbWFuZEgA", - "EkUKD3NodXRkb3duX3dvcmtlchhoIAEoCzIqLm14YWNjZXNzX2dhdGV3YXku", - "djEuU2h1dGRvd25Xb3JrZXJDb21tYW5kSABCCQoHcGF5bG9hZCImCg9SZWdp", - "c3RlckNvbW1hbmQSEwoLY2xpZW50X25hbWUYASABKAkiKgoRVW5yZWdpc3Rl", - "ckNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBSJACg5BZGRJdGVtQ29t", - "bWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhcKD2l0ZW1fZGVmaW5pdGlv", - "bhgCIAEoCSJXCg9BZGRJdGVtMkNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB", - "IAEoBRIXCg9pdGVtX2RlZmluaXRpb24YAiABKAkSFAoMaXRlbV9jb250ZXh0", - "GAMgASgJIj8KEVJlbW92ZUl0ZW1Db21tYW5kEhUKDXNlcnZlcl9oYW5kbGUY", - "ASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiOwoNQWR2aXNlQ29tbWFuZBIV", - "Cg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIj0K", - "D1VuQWR2aXNlQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0", - "ZW1faGFuZGxlGAIgASgFIkYKGEFkdmlzZVN1cGVydmlzb3J5Q29tbWFuZBIV", - "Cg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIl4K", - "FkFkZEJ1ZmZlcmVkSXRlbUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", - "BRIXCg9pdGVtX2RlZmluaXRpb24YAiABKAkSFAoMaXRlbV9jb250ZXh0GAMg", - "ASgJIl8KIFNldEJ1ZmZlcmVkVXBkYXRlSW50ZXJ2YWxDb21tYW5kEhUKDXNl", - "cnZlcl9oYW5kbGUYASABKAUSJAocdXBkYXRlX2ludGVydmFsX21pbGxpc2Vj", - "b25kcxgCIAEoBSI8Cg5TdXNwZW5kQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxl", - "GAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIj0KD0FjdGl2YXRlQ29tbWFu", - "ZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgF", - "IngKDFdyaXRlQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0", - "ZW1faGFuZGxlGAIgASgFEisKBXZhbHVlGAMgASgLMhwubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5NeFZhbHVlEg8KB3VzZXJfaWQYBCABKAUisAEKDVdyaXRlMkNv", + "bXNDb21tYW5kSAASXwohYWNrbm93bGVkZ2VfYWxhcm1fYnlfbmFtZV9jb21t", + "YW5kGCYgASgLMjIubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFs", + "YXJtQnlOYW1lQ29tbWFuZEgAEjAKBHBpbmcYZCABKAsyIC5teGFjY2Vzc19n", + "YXRld2F5LnYxLlBpbmdDb21tYW5kSAASSAoRZ2V0X3Nlc3Npb25fc3RhdGUY", + "ZSABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkdldFNlc3Npb25TdGF0ZUNv", + "bW1hbmRIABJECg9nZXRfd29ya2VyX2luZm8YZiABKAsyKS5teGFjY2Vzc19n", + "YXRld2F5LnYxLkdldFdvcmtlckluZm9Db21tYW5kSAASPwoMZHJhaW5fZXZl", + "bnRzGGcgASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5EcmFpbkV2ZW50c0Nv", + "bW1hbmRIABJFCg9zaHV0ZG93bl93b3JrZXIYaCABKAsyKi5teGFjY2Vzc19n", + "YXRld2F5LnYxLlNodXRkb3duV29ya2VyQ29tbWFuZEgAQgkKB3BheWxvYWQi", + "JgoPUmVnaXN0ZXJDb21tYW5kEhMKC2NsaWVudF9uYW1lGAEgASgJIioKEVVu", + "cmVnaXN0ZXJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUiQAoOQWRk", + "SXRlbUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIXCg9pdGVtX2Rl", + "ZmluaXRpb24YAiABKAkiVwoPQWRkSXRlbTJDb21tYW5kEhUKDXNlcnZlcl9o", + "YW5kbGUYASABKAUSFwoPaXRlbV9kZWZpbml0aW9uGAIgASgJEhQKDGl0ZW1f", + "Y29udGV4dBgDIAEoCSI/ChFSZW1vdmVJdGVtQ29tbWFuZBIVCg1zZXJ2ZXJf", + "aGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIjsKDUFkdmlzZUNv", "bW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgC", - "IAEoBRIrCgV2YWx1ZRgDIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhW", - "YWx1ZRI1Cg90aW1lc3RhbXBfdmFsdWUYBCABKAsyHC5teGFjY2Vzc19nYXRl", - "d2F5LnYxLk14VmFsdWUSDwoHdXNlcl9pZBgFIAEoBSKhAQoTV3JpdGVTZWN1", - "cmVkQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFu", - "ZGxlGAIgASgFEhcKD2N1cnJlbnRfdXNlcl9pZBgDIAEoBRIYChB2ZXJpZmll", - "cl91c2VyX2lkGAQgASgFEisKBXZhbHVlGAUgASgLMhwubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5NeFZhbHVlItkBChRXcml0ZVNlY3VyZWQyQ29tbWFuZBIVCg1z", - "ZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFEhcKD2N1", - "cnJlbnRfdXNlcl9pZBgDIAEoBRIYChB2ZXJpZmllcl91c2VyX2lkGAQgASgF", - "EisKBXZhbHVlGAUgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVl", - "EjUKD3RpbWVzdGFtcF92YWx1ZRgGIAEoCzIcLm14YWNjZXNzX2dhdGV3YXku", - "djEuTXhWYWx1ZSJjChdBdXRoZW50aWNhdGVVc2VyQ29tbWFuZBIVCg1zZXJ2", - "ZXJfaGFuZGxlGAEgASgFEhMKC3ZlcmlmeV91c2VyGAIgASgJEhwKFHZlcmlm", - "eV91c2VyX3Bhc3N3b3JkGAMgASgJIkcKGEFyY2hlc3RyQVVzZXJUb0lkQ29t", - "bWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDHVzZXJfaWRfZ3VpZBgC", - "IAEoCSJCChJBZGRJdGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB", - "IAEoBRIVCg10YWdfYWRkcmVzc2VzGAIgAygJIkQKFUFkdmlzZUl0ZW1CdWxr", - "Q29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0ZW1faGFuZGxl", - "cxgCIAMoBSJEChVSZW1vdmVJdGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hh", - "bmRsZRgBIAEoBRIUCgxpdGVtX2hhbmRsZXMYAiADKAUiRgoXVW5BZHZpc2VJ", + "IAEoBSI9Cg9VbkFkdmlzZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", + "BRITCgtpdGVtX2hhbmRsZRgCIAEoBSJGChhBZHZpc2VTdXBlcnZpc29yeUNv", + "bW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgC", + "IAEoBSJeChZBZGRCdWZmZXJlZEl0ZW1Db21tYW5kEhUKDXNlcnZlcl9oYW5k", + "bGUYASABKAUSFwoPaXRlbV9kZWZpbml0aW9uGAIgASgJEhQKDGl0ZW1fY29u", + "dGV4dBgDIAEoCSJfCiBTZXRCdWZmZXJlZFVwZGF0ZUludGVydmFsQ29tbWFu", + "ZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEiQKHHVwZGF0ZV9pbnRlcnZhbF9t", + "aWxsaXNlY29uZHMYAiABKAUiPAoOU3VzcGVuZENvbW1hbmQSFQoNc2VydmVy", + "X2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSI9Cg9BY3RpdmF0", + "ZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRs", + "ZRgCIAEoBSJ4CgxXcml0ZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", + "BRITCgtpdGVtX2hhbmRsZRgCIAEoBRIrCgV2YWx1ZRgDIAEoCzIcLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgd1c2VyX2lkGAQgASgFIrABCg1X", + "cml0ZTJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9o", + "YW5kbGUYAiABKAUSKwoFdmFsdWUYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5", + "LnYxLk14VmFsdWUSNQoPdGltZXN0YW1wX3ZhbHVlGAQgASgLMhwubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5NeFZhbHVlEg8KB3VzZXJfaWQYBSABKAUioQEKE1dy", + "aXRlU2VjdXJlZENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtp", + "dGVtX2hhbmRsZRgCIAEoBRIXCg9jdXJyZW50X3VzZXJfaWQYAyABKAUSGAoQ", + "dmVyaWZpZXJfdXNlcl9pZBgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIcLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuTXhWYWx1ZSLZAQoUV3JpdGVTZWN1cmVkMkNvbW1h", + "bmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEo", + "BRIXCg9jdXJyZW50X3VzZXJfaWQYAyABKAUSGAoQdmVyaWZpZXJfdXNlcl9p", + "ZBgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEu", + "TXhWYWx1ZRI1Cg90aW1lc3RhbXBfdmFsdWUYBiABKAsyHC5teGFjY2Vzc19n", + "YXRld2F5LnYxLk14VmFsdWUiYwoXQXV0aGVudGljYXRlVXNlckNvbW1hbmQS", + "FQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgt2ZXJpZnlfdXNlchgCIAEoCRIc", + "ChR2ZXJpZnlfdXNlcl9wYXNzd29yZBgDIAEoCSJHChhBcmNoZXN0ckFVc2Vy", + "VG9JZENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgx1c2VyX2lk", + "X2d1aWQYAiABKAkiQgoSQWRkSXRlbUJ1bGtDb21tYW5kEhUKDXNlcnZlcl9o", + "YW5kbGUYASABKAUSFQoNdGFnX2FkZHJlc3NlcxgCIAMoCSJEChVBZHZpc2VJ", "dGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgxpdGVt", - "X2hhbmRsZXMYAiADKAUiRAoUU3Vic2NyaWJlQnVsa0NvbW1hbmQSFQoNc2Vy", - "dmVyX2hhbmRsZRgBIAEoBRIVCg10YWdfYWRkcmVzc2VzGAIgAygJIjkKFlN1", - "YnNjcmliZUFsYXJtc0NvbW1hbmQSHwoXc3Vic2NyaXB0aW9uX2V4cHJlc3Np", - "b24YASABKAkiGgoYVW5zdWJzY3JpYmVBbGFybXNDb21tYW5kIqEBChdBY2tu", - "b3dsZWRnZUFsYXJtQ29tbWFuZBISCgphbGFybV9ndWlkGAEgASgJEg8KB2Nv", - "bW1lbnQYAiABKAkSFQoNb3BlcmF0b3JfdXNlchgDIAEoCRIVCg1vcGVyYXRv", - "cl9ub2RlGAQgASgJEhcKD29wZXJhdG9yX2RvbWFpbhgFIAEoCRIaChJvcGVy", - "YXRvcl9mdWxsX25hbWUYBiABKAkiNwoYUXVlcnlBY3RpdmVBbGFybXNDb21t", - "YW5kEhsKE2FsYXJtX2ZpbHRlcl9wcmVmaXgYASABKAkiRQoWVW5zdWJzY3Jp", - "YmVCdWxrQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhQKDGl0ZW1f", - "aGFuZGxlcxgCIAMoBSIeCgtQaW5nQ29tbWFuZBIPCgdtZXNzYWdlGAEgASgJ", - "IhgKFkdldFNlc3Npb25TdGF0ZUNvbW1hbmQiFgoUR2V0V29ya2VySW5mb0Nv", - "bW1hbmQiKAoSRHJhaW5FdmVudHNDb21tYW5kEhIKCm1heF9ldmVudHMYASAB", - "KA0iSAoVU2h1dGRvd25Xb3JrZXJDb21tYW5kEi8KDGdyYWNlX3BlcmlvZBgB", - "IAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbiLPDAoOTXhDb21tYW5k", - "UmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb3JyZWxhdGlvbl9pZBgC", - "IAEoCRIwCgRraW5kGAMgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENv", - "bW1hbmRLaW5kEjwKD3Byb3RvY29sX3N0YXR1cxgEIAEoCzIjLm14YWNjZXNz", - "X2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0dXMSFAoHaHJlc3VsdBgFIAEoBUgB", - "iAEBEjIKDHJldHVybl92YWx1ZRgGIAEoCzIcLm14YWNjZXNzX2dhdGV3YXku", - "djEuTXhWYWx1ZRI0CghzdGF0dXNlcxgHIAMoCzIiLm14YWNjZXNzX2dhdGV3", - "YXkudjEuTXhTdGF0dXNQcm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UYCCAB", - "KAkSNgoIcmVnaXN0ZXIYFCABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLlJl", - "Z2lzdGVyUmVwbHlIABI1CghhZGRfaXRlbRgVIAEoCzIhLm14YWNjZXNzX2dh", - "dGV3YXkudjEuQWRkSXRlbVJlcGx5SAASNwoJYWRkX2l0ZW0yGBYgASgLMiIu", - "bXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRJdGVtMlJlcGx5SAASRgoRYWRkX2J1", - "ZmZlcmVkX2l0ZW0YFyABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEJ1", - "ZmZlcmVkSXRlbVJlcGx5SAASNAoHc3VzcGVuZBgYIAEoCzIhLm14YWNjZXNz", - "X2dhdGV3YXkudjEuU3VzcGVuZFJlcGx5SAASNgoIYWN0aXZhdGUYGSABKAsy", - "Ii5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2YXRlUmVwbHlIABJHChFhdXRo", - "ZW50aWNhdGVfdXNlchgaIAEoCzIqLm14YWNjZXNzX2dhdGV3YXkudjEuQXV0", - "aGVudGljYXRlVXNlclJlcGx5SAASSwoUYXJjaGVzdHJhX3VzZXJfdG9faWQY", - "GyABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkFyY2hlc3RyQVVzZXJUb0lk", - "UmVwbHlIABJACg1hZGRfaXRlbV9idWxrGBwgASgLMicubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJDChBhZHZpc2VfaXRlbV9i", - "dWxrGB0gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxrU3Vic2NyaWJl", - "UmVwbHlIABJDChByZW1vdmVfaXRlbV9idWxrGB4gASgLMicubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5CdWxrU3Vic2NyaWJlUmVwbHlIABJGChN1bl9hZHZpc2Vf", - "aXRlbV9idWxrGB8gASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5CdWxrU3Vi", - "c2NyaWJlUmVwbHlIABJBCg5zdWJzY3JpYmVfYnVsaxggIAEoCzInLm14YWNj", - "ZXNzX2dhdGV3YXkudjEuQnVsa1N1YnNjcmliZVJlcGx5SAASQwoQdW5zdWJz", - "Y3JpYmVfYnVsaxghIAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuQnVsa1N1", - "YnNjcmliZVJlcGx5SAASTgoRYWNrbm93bGVkZ2VfYWxhcm0YIiABKAsyMS5t", - "eGFjY2Vzc19nYXRld2F5LnYxLkFja25vd2xlZGdlQWxhcm1SZXBseVBheWxv", - "YWRIABJRChNxdWVyeV9hY3RpdmVfYWxhcm1zGCMgASgLMjIubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcGx5UGF5bG9hZEgAEj8K", - "DXNlc3Npb25fc3RhdGUYZCABKAsyJi5teGFjY2Vzc19nYXRld2F5LnYxLlNl", - "c3Npb25TdGF0ZVJlcGx5SAASOwoLd29ya2VyX2luZm8YZSABKAsyJC5teGFj", - "Y2Vzc19nYXRld2F5LnYxLldvcmtlckluZm9SZXBseUgAEj0KDGRyYWluX2V2", - "ZW50cxhmIAEoCzIlLm14YWNjZXNzX2dhdGV3YXkudjEuRHJhaW5FdmVudHNS", - "ZXBseUgAQgkKB3BheWxvYWRCCgoIX2hyZXN1bHQiJgoNUmVnaXN0ZXJSZXBs", - "eRIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFIiMKDEFkZEl0ZW1SZXBseRITCgtp", - "dGVtX2hhbmRsZRgBIAEoBSIkCg1BZGRJdGVtMlJlcGx5EhMKC2l0ZW1faGFu", - "ZGxlGAEgASgFIisKFEFkZEJ1ZmZlcmVkSXRlbVJlcGx5EhMKC2l0ZW1faGFu", - "ZGxlGAEgASgFIkIKDFN1c3BlbmRSZXBseRIyCgZzdGF0dXMYASABKAsyIi5t", - "eGFjY2Vzc19nYXRld2F5LnYxLk14U3RhdHVzUHJveHkiQwoNQWN0aXZhdGVS", - "ZXBseRIyCgZzdGF0dXMYASABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14", - "U3RhdHVzUHJveHkiKAoVQXV0aGVudGljYXRlVXNlclJlcGx5Eg8KB3VzZXJf", - "aWQYASABKAUiKQoWQXJjaGVzdHJBVXNlclRvSWRSZXBseRIPCgd1c2VyX2lk", - "GAEgASgFIoEBCg9TdWJzY3JpYmVSZXN1bHQSFQoNc2VydmVyX2hhbmRsZRgB", - "IAEoBRITCgt0YWdfYWRkcmVzcxgCIAEoCRITCgtpdGVtX2hhbmRsZRgDIAEo", - "BRIWCg53YXNfc3VjY2Vzc2Z1bBgEIAEoCBIVCg1lcnJvcl9tZXNzYWdlGAUg", - "ASgJIksKEkJ1bGtTdWJzY3JpYmVSZXBseRI1CgdyZXN1bHRzGAEgAygLMiQu", - "bXhhY2Nlc3NfZ2F0ZXdheS52MS5TdWJzY3JpYmVSZXN1bHQiRQoRU2Vzc2lv", - "blN0YXRlUmVwbHkSMAoFc3RhdGUYASABKA4yIS5teGFjY2Vzc19nYXRld2F5", - "LnYxLlNlc3Npb25TdGF0ZSJ1Cg9Xb3JrZXJJbmZvUmVwbHkSGQoRd29ya2Vy", - "X3Byb2Nlc3NfaWQYASABKAUSFgoOd29ya2VyX3ZlcnNpb24YAiABKAkSFwoP", - "bXhhY2Nlc3NfcHJvZ2lkGAMgASgJEhYKDm14YWNjZXNzX2Nsc2lkGAQgASgJ", - "IkAKEERyYWluRXZlbnRzUmVwbHkSLAoGZXZlbnRzGAEgAygLMhwubXhhY2Nl", - "c3NfZ2F0ZXdheS52MS5NeEV2ZW50IjUKHEFja25vd2xlZGdlQWxhcm1SZXBs", - "eVBheWxvYWQSFQoNbmF0aXZlX3N0YXR1cxgBIAEoBSJcCh1RdWVyeUFjdGl2", - "ZUFsYXJtc1JlcGx5UGF5bG9hZBI7CglzbmFwc2hvdHMYASADKAsyKC5teGFj", - "Y2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3Qi5wYKB014RXZl", - "bnQSMgoGZmFtaWx5GAEgASgOMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEV2", - "ZW50RmFtaWx5EhIKCnNlc3Npb25faWQYAiABKAkSFQoNc2VydmVyX2hhbmRs", - "ZRgDIAEoBRITCgtpdGVtX2hhbmRsZRgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIc", - "Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgdxdWFsaXR5GAYgASgF", - "EjQKEHNvdXJjZV90aW1lc3RhbXAYByABKAsyGi5nb29nbGUucHJvdG9idWYu", - "VGltZXN0YW1wEjQKCHN0YXR1c2VzGAggAygLMiIubXhhY2Nlc3NfZ2F0ZXdh", - "eS52MS5NeFN0YXR1c1Byb3h5EhcKD3dvcmtlcl9zZXF1ZW5jZRgJIAEoBBI0", - "ChB3b3JrZXJfdGltZXN0YW1wGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRp", - "bWVzdGFtcBI9ChlnYXRld2F5X3JlY2VpdmVfdGltZXN0YW1wGAsgASgLMhou", - "Z29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIUCgdocmVzdWx0GAwgASgFSAGI", - "AQESEgoKcmF3X3N0YXR1cxgNIAEoCRJACg5vbl9kYXRhX2NoYW5nZRgUIAEo", - "CzImLm14YWNjZXNzX2dhdGV3YXkudjEuT25EYXRhQ2hhbmdlRXZlbnRIABJG", - "ChFvbl93cml0ZV9jb21wbGV0ZRgVIAEoCzIpLm14YWNjZXNzX2dhdGV3YXku", - "djEuT25Xcml0ZUNvbXBsZXRlRXZlbnRIABJJChJvcGVyYXRpb25fY29tcGxl", - "dGUYFiABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLk9wZXJhdGlvbkNvbXBs", - "ZXRlRXZlbnRIABJRChdvbl9idWZmZXJlZF9kYXRhX2NoYW5nZRgXIAEoCzIu", - "Lm14YWNjZXNzX2dhdGV3YXkudjEuT25CdWZmZXJlZERhdGFDaGFuZ2VFdmVu", - "dEgAEkoKE29uX2FsYXJtX3RyYW5zaXRpb24YGCABKAsyKy5teGFjY2Vzc19n", - "YXRld2F5LnYxLk9uQWxhcm1UcmFuc2l0aW9uRXZlbnRIAEIGCgRib2R5QgoK", - "CF9ocmVzdWx0IhMKEU9uRGF0YUNoYW5nZUV2ZW50IhYKFE9uV3JpdGVDb21w", - "bGV0ZUV2ZW50IhgKFk9wZXJhdGlvbkNvbXBsZXRlRXZlbnQi1AEKGU9uQnVm", - "ZmVyZWREYXRhQ2hhbmdlRXZlbnQSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhh", - "Y2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEjQKDnF1YWxpdHlfdmFsdWVz", - "GAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EjYKEHRpbWVz", - "dGFtcF92YWx1ZXMYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJy", - "YXkSFQoNcmF3X2RhdGFfdHlwZRgEIAEoBSL9AwoWT25BbGFybVRyYW5zaXRp", - "b25FdmVudBIcChRhbGFybV9mdWxsX3JlZmVyZW5jZRgBIAEoCRIfChdzb3Vy", - "Y2Vfb2JqZWN0X3JlZmVyZW5jZRgCIAEoCRIXCg9hbGFybV90eXBlX25hbWUY", - "AyABKAkSQQoPdHJhbnNpdGlvbl9raW5kGAQgASgOMigubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5BbGFybVRyYW5zaXRpb25LaW5kEhAKCHNldmVyaXR5GAUgASgF", - "EjwKGG9yaWdpbmFsX3JhaXNlX3RpbWVzdGFtcBgGIAEoCzIaLmdvb2dsZS5w", - "cm90b2J1Zi5UaW1lc3RhbXASOAoUdHJhbnNpdGlvbl90aW1lc3RhbXAYByAB", - "KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJhdG9yX3Vz", - "ZXIYCCABKAkSGAoQb3BlcmF0b3JfY29tbWVudBgJIAEoCRIQCghjYXRlZ29y", - "eRgKIAEoCRITCgtkZXNjcmlwdGlvbhgLIAEoCRIzCg1jdXJyZW50X3ZhbHVl", - "GAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEjEKC2xpbWl0", - "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIv0D", - "ChNBY3RpdmVBbGFybVNuYXBzaG90EhwKFGFsYXJtX2Z1bGxfcmVmZXJlbmNl", - "GAEgASgJEh8KF3NvdXJjZV9vYmplY3RfcmVmZXJlbmNlGAIgASgJEhcKD2Fs", - "YXJtX3R5cGVfbmFtZRgDIAEoCRIQCghzZXZlcml0eRgEIAEoBRI8Chhvcmln", - "aW5hbF9yYWlzZV90aW1lc3RhbXAYBSABKAsyGi5nb29nbGUucHJvdG9idWYu", - "VGltZXN0YW1wEj8KDWN1cnJlbnRfc3RhdGUYBiABKA4yKC5teGFjY2Vzc19n", - "YXRld2F5LnYxLkFsYXJtQ29uZGl0aW9uU3RhdGUSEAoIY2F0ZWdvcnkYByAB", - "KAkSEwoLZGVzY3JpcHRpb24YCCABKAkSPQoZbGFzdF90cmFuc2l0aW9uX3Rp", - "bWVzdGFtcBgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoN", - "b3BlcmF0b3JfdXNlchgKIAEoCRIYChBvcGVyYXRvcl9jb21tZW50GAsgASgJ", - "EjMKDWN1cnJlbnRfdmFsdWUYDCABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYx", - "Lk14VmFsdWUSMQoLbGltaXRfdmFsdWUYDSABKAsyHC5teGFjY2Vzc19nYXRl", - "d2F5LnYxLk14VmFsdWUikgEKF0Fja25vd2xlZGdlQWxhcm1SZXF1ZXN0EhIK", - "CnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50X2NvcnJlbGF0aW9uX2lkGAIg", - "ASgJEhwKFGFsYXJtX2Z1bGxfcmVmZXJlbmNlGAMgASgJEg8KB2NvbW1lbnQY", - "BCABKAkSFQoNb3BlcmF0b3JfdXNlchgFIAEoCSLzAQoVQWNrbm93bGVkZ2VB", - "bGFybVJlcGx5EhIKCnNlc3Npb25faWQYASABKAkSFgoOY29ycmVsYXRpb25f", - "aWQYAiABKAkSPAoPcHJvdG9jb2xfc3RhdHVzGAMgASgLMiMubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0GAQgASgFSACI", - "AQESMgoGc3RhdHVzGAUgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0", - "YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVzc2FnZRgGIAEoCUIKCghfaHJl", - "c3VsdCJqChhRdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QSEgoKc2Vzc2lvbl9p", - "ZBgBIAEoCRIdChVjbGllbnRfY29ycmVsYXRpb25faWQYAiABKAkSGwoTYWxh", - "cm1fZmlsdGVyX3ByZWZpeBgDIAEoCSLrAQoNTXhTdGF0dXNQcm94eRIPCgdz", - "dWNjZXNzGAEgASgFEjcKCGNhdGVnb3J5GAIgASgOMiUubXhhY2Nlc3NfZ2F0", - "ZXdheS52MS5NeFN0YXR1c0NhdGVnb3J5EjgKC2RldGVjdGVkX2J5GAMgASgO", - "MiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0YXR1c1NvdXJjZRIOCgZkZXRh", - "aWwYBCABKAUSFAoMcmF3X2NhdGVnb3J5GAUgASgFEhcKD3Jhd19kZXRlY3Rl", - "ZF9ieRgGIAEoBRIXCg9kaWFnbm9zdGljX3RleHQYByABKAkipwMKB014VmFs", - "dWUSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5N", - "eERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIPCgdpc19udWxsGAMg", - "ASgIEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEhUKDXJhd19kYXRhX3R5cGUY", - "BSABKAUSFAoKYm9vbF92YWx1ZRgKIAEoCEgAEhUKC2ludDMyX3ZhbHVlGAsg", - "ASgFSAASFQoLaW50NjRfdmFsdWUYDCABKANIABIVCgtmbG9hdF92YWx1ZRgN", - "IAEoAkgAEhYKDGRvdWJsZV92YWx1ZRgOIAEoAUgAEhYKDHN0cmluZ192YWx1", - "ZRgPIAEoCUgAEjUKD3RpbWVzdGFtcF92YWx1ZRgQIAEoCzIaLmdvb2dsZS5w", - "cm90b2J1Zi5UaW1lc3RhbXBIABIzCgthcnJheV92YWx1ZRgRIAEoCzIcLm14", - "YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheUgAEhMKCXJhd192YWx1ZRgSIAEo", - "DEgAQgYKBGtpbmQi/gQKB014QXJyYXkSOgoRZWxlbWVudF9kYXRhX3R5cGUY", - "ASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUSFAoMdmFy", - "aWFudF90eXBlGAIgASgJEhIKCmRpbWVuc2lvbnMYAyADKA0SFgoOcmF3X2Rp", - "YWdub3N0aWMYBCABKAkSHQoVcmF3X2VsZW1lbnRfZGF0YV90eXBlGAUgASgF", - "EjUKC2Jvb2xfdmFsdWVzGAogASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52MS5C", - "b29sQXJyYXlIABI3CgxpbnQzMl92YWx1ZXMYCyABKAsyHy5teGFjY2Vzc19n", - "YXRld2F5LnYxLkludDMyQXJyYXlIABI3CgxpbnQ2NF92YWx1ZXMYDCABKAsy", - "Hy5teGFjY2Vzc19nYXRld2F5LnYxLkludDY0QXJyYXlIABI3CgxmbG9hdF92", - "YWx1ZXMYDSABKAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkZsb2F0QXJyYXlI", - "ABI5Cg1kb3VibGVfdmFsdWVzGA4gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5Eb3VibGVBcnJheUgAEjkKDXN0cmluZ192YWx1ZXMYDyABKAsyIC5teGFj", - "Y2Vzc19nYXRld2F5LnYxLlN0cmluZ0FycmF5SAASPwoQdGltZXN0YW1wX3Zh", - "bHVlcxgQIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuVGltZXN0YW1wQXJy", - "YXlIABIzCgpyYXdfdmFsdWVzGBEgASgLMh0ubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5SYXdBcnJheUgAQggKBnZhbHVlcyIbCglCb29sQXJyYXkSDgoGdmFsdWVz", - "GAEgAygIIhwKCkludDMyQXJyYXkSDgoGdmFsdWVzGAEgAygFIhwKCkludDY0", - "QXJyYXkSDgoGdmFsdWVzGAEgAygDIhwKCkZsb2F0QXJyYXkSDgoGdmFsdWVz", - "GAEgAygCIh0KC0RvdWJsZUFycmF5Eg4KBnZhbHVlcxgBIAMoASIdCgtTdHJp", - "bmdBcnJheRIOCgZ2YWx1ZXMYASADKAkiPAoOVGltZXN0YW1wQXJyYXkSKgoG", - "dmFsdWVzGAEgAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIaCghS", - "YXdBcnJheRIOCgZ2YWx1ZXMYASADKAwiWAoOUHJvdG9jb2xTdGF0dXMSNQoE", - "Y29kZRgBIAEoDjInLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0", - "dXNDb2RlEg8KB21lc3NhZ2UYAiABKAkqvwkKDU14Q29tbWFuZEtpbmQSHwob", - "TVhfQ09NTUFORF9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYTVhfQ09NTUFORF9L", - "SU5EX1JFR0lTVEVSEAESHgoaTVhfQ09NTUFORF9LSU5EX1VOUkVHSVNURVIQ", - "AhIcChhNWF9DT01NQU5EX0tJTkRfQUREX0lURU0QAxIdChlNWF9DT01NQU5E", - "X0tJTkRfQUREX0lURU0yEAQSHwobTVhfQ09NTUFORF9LSU5EX1JFTU9WRV9J", - "VEVNEAUSGgoWTVhfQ09NTUFORF9LSU5EX0FEVklTRRAGEh0KGU1YX0NPTU1B", - "TkRfS0lORF9VTl9BRFZJU0UQBxImCiJNWF9DT01NQU5EX0tJTkRfQURWSVNF", - "X1NVUEVSVklTT1JZEAgSJQohTVhfQ09NTUFORF9LSU5EX0FERF9CVUZGRVJF", - "RF9JVEVNEAkSMAosTVhfQ09NTUFORF9LSU5EX1NFVF9CVUZGRVJFRF9VUERB", - "VEVfSU5URVJWQUwQChIbChdNWF9DT01NQU5EX0tJTkRfU1VTUEVORBALEhwK", - "GE1YX0NPTU1BTkRfS0lORF9BQ1RJVkFURRAMEhkKFU1YX0NPTU1BTkRfS0lO", - "RF9XUklURRANEhoKFk1YX0NPTU1BTkRfS0lORF9XUklURTIQDhIhCh1NWF9D", - "T01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRBAPEiIKHk1YX0NPTU1BTkRfS0lO", - "RF9XUklURV9TRUNVUkVEMhAQEiUKIU1YX0NPTU1BTkRfS0lORF9BVVRIRU5U", - "SUNBVEVfVVNFUhAREigKJE1YX0NPTU1BTkRfS0lORF9BUkNIRVNUUkFfVVNF", - "Ul9UT19JRBASEiEKHU1YX0NPTU1BTkRfS0lORF9BRERfSVRFTV9CVUxLEBMS", - "JAogTVhfQ09NTUFORF9LSU5EX0FEVklTRV9JVEVNX0JVTEsQFBIkCiBNWF9D", - "T01NQU5EX0tJTkRfUkVNT1ZFX0lURU1fQlVMSxAVEicKI01YX0NPTU1BTkRf", - "S0lORF9VTl9BRFZJU0VfSVRFTV9CVUxLEBYSIgoeTVhfQ09NTUFORF9LSU5E", - "X1NVQlNDUklCRV9CVUxLEBcSJAogTVhfQ09NTUFORF9LSU5EX1VOU1VCU0NS", - "SUJFX0JVTEsQGBIkCiBNWF9DT01NQU5EX0tJTkRfU1VCU0NSSUJFX0FMQVJN", - "UxAZEiYKIk1YX0NPTU1BTkRfS0lORF9VTlNVQlNDUklCRV9BTEFSTVMQGhIl", - "CiFNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVER0VfQUxBUk0QGxInCiNNWF9D", - "T01NQU5EX0tJTkRfUVVFUllfQUNUSVZFX0FMQVJNUxAcEhgKFE1YX0NPTU1B", - "TkRfS0lORF9QSU5HEGQSJQohTVhfQ09NTUFORF9LSU5EX0dFVF9TRVNTSU9O", - "X1NUQVRFEGUSIwofTVhfQ09NTUFORF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBm", - "EiAKHE1YX0NPTU1BTkRfS0lORF9EUkFJTl9FVkVOVFMQZxIjCh9NWF9DT01N", - "QU5EX0tJTkRfU0hVVERPV05fV09SS0VSEGgq+QEKDU14RXZlbnRGYW1pbHkS", - "HwobTVhfRVZFTlRfRkFNSUxZX1VOU1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRf", - "RkFNSUxZX09OX0RBVEFfQ0hBTkdFEAESJQohTVhfRVZFTlRfRkFNSUxZX09O", - "X1dSSVRFX0NPTVBMRVRFEAISJgoiTVhfRVZFTlRfRkFNSUxZX09QRVJBVElP", - "Tl9DT01QTEVURRADEisKJ01YX0VWRU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9E", - "QVRBX0NIQU5HRRAEEicKI01YX0VWRU5UX0ZBTUlMWV9PTl9BTEFSTV9UUkFO", - "U0lUSU9OEAUqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJB", - "TlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElP", - "Tl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05P", - "V0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwof", - "QUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNv", - "bmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJ", - "RklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJB", - "TEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1f", - "Q09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdv", - "cnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhf", - "U1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdP", - "UllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVORElORxADEh4KGk1Y", - "X1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVH", - "T1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdP", - "UllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09S", - "WV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9T", - "RUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FS", - "RV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAK", - "KsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BF", - "Q0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhf", - "U1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19T", - "T1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JF", - "UVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5H", - "X05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFU", - "SU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19B", - "VVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFf", - "VFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAES", - "GAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9P", - "TEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9U", - "WVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RB", - "VEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1Y", - "X0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVG", - "RVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIV", - "ChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklU", - "WV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFf", - "UVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVN", - "EA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9E", - "QVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5HEBESGwoXTVhfREFU", - "QV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMK", - "ElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9V", - "TlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAok", - "UFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJP", - "VE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9U", - "T0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RP", - "Q09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RP", - "Q09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNf", - "Q09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RP", - "Q09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUND", - "RVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RB", - "VEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAES", - "IQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9O", - "X1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hB", - "TkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09S", - "S0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RB", - "VEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VT", - "U0lPTl9TVEFURV9GQVVMVEVEEAky4AQKD014QWNjZXNzR2F0ZXdheRJdCgtP", - "cGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25S", - "ZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5", - "EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VT", - "ZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNz", - "aW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENv", - "bW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRS", - "ZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0", - "cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZl", - "bnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52", - "MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXku", - "djEuQWNrbm93bGVkZ2VBbGFybVJlcGx5Em4KEVF1ZXJ5QWN0aXZlQWxhcm1z", - "Ei0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVl", - "c3QaKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3Qw", - "AUIcqgIZTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90b2IGcHJvdG8z")); + "X2hhbmRsZXMYAiADKAUiRAoVUmVtb3ZlSXRlbUJ1bGtDb21tYW5kEhUKDXNl", + "cnZlcl9oYW5kbGUYASABKAUSFAoMaXRlbV9oYW5kbGVzGAIgAygFIkYKF1Vu", + "QWR2aXNlSXRlbUJ1bGtDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUS", + "FAoMaXRlbV9oYW5kbGVzGAIgAygFIkQKFFN1YnNjcmliZUJ1bGtDb21tYW5k", + "EhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFQoNdGFnX2FkZHJlc3NlcxgCIAMo", + "CSI5ChZTdWJzY3JpYmVBbGFybXNDb21tYW5kEh8KF3N1YnNjcmlwdGlvbl9l", + "eHByZXNzaW9uGAEgASgJIhoKGFVuc3Vic2NyaWJlQWxhcm1zQ29tbWFuZCKh", + "AQoXQWNrbm93bGVkZ2VBbGFybUNvbW1hbmQSEgoKYWxhcm1fZ3VpZBgBIAEo", + "CRIPCgdjb21tZW50GAIgASgJEhUKDW9wZXJhdG9yX3VzZXIYAyABKAkSFQoN", + "b3BlcmF0b3Jfbm9kZRgEIAEoCRIXCg9vcGVyYXRvcl9kb21haW4YBSABKAkS", + "GgoSb3BlcmF0b3JfZnVsbF9uYW1lGAYgASgJIjcKGFF1ZXJ5QWN0aXZlQWxh", + "cm1zQ29tbWFuZBIbChNhbGFybV9maWx0ZXJfcHJlZml4GAEgASgJItIBCh1B", + "Y2tub3dsZWRnZUFsYXJtQnlOYW1lQ29tbWFuZBISCgphbGFybV9uYW1lGAEg", + "ASgJEhUKDXByb3ZpZGVyX25hbWUYAiABKAkSEgoKZ3JvdXBfbmFtZRgDIAEo", + "CRIPCgdjb21tZW50GAQgASgJEhUKDW9wZXJhdG9yX3VzZXIYBSABKAkSFQoN", + "b3BlcmF0b3Jfbm9kZRgGIAEoCRIXCg9vcGVyYXRvcl9kb21haW4YByABKAkS", + "GgoSb3BlcmF0b3JfZnVsbF9uYW1lGAggASgJIkUKFlVuc3Vic2NyaWJlQnVs", + "a0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgxpdGVtX2hhbmRs", + "ZXMYAiADKAUiHgoLUGluZ0NvbW1hbmQSDwoHbWVzc2FnZRgBIAEoCSIYChZH", + "ZXRTZXNzaW9uU3RhdGVDb21tYW5kIhYKFEdldFdvcmtlckluZm9Db21tYW5k", + "IigKEkRyYWluRXZlbnRzQ29tbWFuZBISCgptYXhfZXZlbnRzGAEgASgNIkgK", + "FVNodXRkb3duV29ya2VyQ29tbWFuZBIvCgxncmFjZV9wZXJpb2QYASABKAsy", + "GS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24izwwKDk14Q29tbWFuZFJlcGx5", + "EhIKCnNlc3Npb25faWQYASABKAkSFgoOY29ycmVsYXRpb25faWQYAiABKAkS", + "MAoEa2luZBgDIAEoDjIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhDb21tYW5k", + "S2luZBI8Cg9wcm90b2NvbF9zdGF0dXMYBCABKAsyIy5teGFjY2Vzc19nYXRl", + "d2F5LnYxLlByb3RvY29sU3RhdHVzEhQKB2hyZXN1bHQYBSABKAVIAYgBARIy", + "CgxyZXR1cm5fdmFsdWUYBiABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14", + "VmFsdWUSNAoIc3RhdHVzZXMYByADKAsyIi5teGFjY2Vzc19nYXRld2F5LnYx", + "Lk14U3RhdHVzUHJveHkSGgoSZGlhZ25vc3RpY19tZXNzYWdlGAggASgJEjYK", + "CHJlZ2lzdGVyGBQgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3Rl", + "clJlcGx5SAASNQoIYWRkX2l0ZW0YFSABKAsyIS5teGFjY2Vzc19nYXRld2F5", + "LnYxLkFkZEl0ZW1SZXBseUgAEjcKCWFkZF9pdGVtMhgWIAEoCzIiLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuQWRkSXRlbTJSZXBseUgAEkYKEWFkZF9idWZmZXJl", + "ZF9pdGVtGBcgASgLMikubXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRCdWZmZXJl", + "ZEl0ZW1SZXBseUgAEjQKB3N1c3BlbmQYGCABKAsyIS5teGFjY2Vzc19nYXRl", + "d2F5LnYxLlN1c3BlbmRSZXBseUgAEjYKCGFjdGl2YXRlGBkgASgLMiIubXhh", + "Y2Nlc3NfZ2F0ZXdheS52MS5BY3RpdmF0ZVJlcGx5SAASRwoRYXV0aGVudGlj", + "YXRlX3VzZXIYGiABKAsyKi5teGFjY2Vzc19nYXRld2F5LnYxLkF1dGhlbnRp", + "Y2F0ZVVzZXJSZXBseUgAEksKFGFyY2hlc3RyYV91c2VyX3RvX2lkGBsgASgL", + "MisubXhhY2Nlc3NfZ2F0ZXdheS52MS5BcmNoZXN0ckFVc2VyVG9JZFJlcGx5", + "SAASQAoNYWRkX2l0ZW1fYnVsaxgcIAEoCzInLm14YWNjZXNzX2dhdGV3YXku", + "djEuQnVsa1N1YnNjcmliZVJlcGx5SAASQwoQYWR2aXNlX2l0ZW1fYnVsaxgd", + "IAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuQnVsa1N1YnNjcmliZVJlcGx5", + "SAASQwoQcmVtb3ZlX2l0ZW1fYnVsaxgeIAEoCzInLm14YWNjZXNzX2dhdGV3", + "YXkudjEuQnVsa1N1YnNjcmliZVJlcGx5SAASRgoTdW5fYWR2aXNlX2l0ZW1f", + "YnVsaxgfIAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuQnVsa1N1YnNjcmli", + "ZVJlcGx5SAASQQoOc3Vic2NyaWJlX2J1bGsYICABKAsyJy5teGFjY2Vzc19n", + "YXRld2F5LnYxLkJ1bGtTdWJzY3JpYmVSZXBseUgAEkMKEHVuc3Vic2NyaWJl", + "X2J1bGsYISABKAsyJy5teGFjY2Vzc19nYXRld2F5LnYxLkJ1bGtTdWJzY3Jp", + "YmVSZXBseUgAEk4KEWFja25vd2xlZGdlX2FsYXJtGCIgASgLMjEubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJtUmVwbHlQYXlsb2FkSAAS", + "UQoTcXVlcnlfYWN0aXZlX2FsYXJtcxgjIAEoCzIyLm14YWNjZXNzX2dhdGV3", + "YXkudjEuUXVlcnlBY3RpdmVBbGFybXNSZXBseVBheWxvYWRIABI/Cg1zZXNz", + "aW9uX3N0YXRlGGQgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5TZXNzaW9u", + "U3RhdGVSZXBseUgAEjsKC3dvcmtlcl9pbmZvGGUgASgLMiQubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5Xb3JrZXJJbmZvUmVwbHlIABI9CgxkcmFpbl9ldmVudHMY", + "ZiABKAsyJS5teGFjY2Vzc19nYXRld2F5LnYxLkRyYWluRXZlbnRzUmVwbHlI", + "AEIJCgdwYXlsb2FkQgoKCF9ocmVzdWx0IiYKDVJlZ2lzdGVyUmVwbHkSFQoN", + "c2VydmVyX2hhbmRsZRgBIAEoBSIjCgxBZGRJdGVtUmVwbHkSEwoLaXRlbV9o", + "YW5kbGUYASABKAUiJAoNQWRkSXRlbTJSZXBseRITCgtpdGVtX2hhbmRsZRgB", + "IAEoBSIrChRBZGRCdWZmZXJlZEl0ZW1SZXBseRITCgtpdGVtX2hhbmRsZRgB", + "IAEoBSJCCgxTdXNwZW5kUmVwbHkSMgoGc3RhdHVzGAEgASgLMiIubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5NeFN0YXR1c1Byb3h5IkMKDUFjdGl2YXRlUmVwbHkS", + "MgoGc3RhdHVzGAEgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0YXR1", + "c1Byb3h5IigKFUF1dGhlbnRpY2F0ZVVzZXJSZXBseRIPCgd1c2VyX2lkGAEg", + "ASgFIikKFkFyY2hlc3RyQVVzZXJUb0lkUmVwbHkSDwoHdXNlcl9pZBgBIAEo", + "BSKBAQoPU3Vic2NyaWJlUmVzdWx0EhUKDXNlcnZlcl9oYW5kbGUYASABKAUS", + "EwoLdGFnX2FkZHJlc3MYAiABKAkSEwoLaXRlbV9oYW5kbGUYAyABKAUSFgoO", + "d2FzX3N1Y2Nlc3NmdWwYBCABKAgSFQoNZXJyb3JfbWVzc2FnZRgFIAEoCSJL", + "ChJCdWxrU3Vic2NyaWJlUmVwbHkSNQoHcmVzdWx0cxgBIAMoCzIkLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuU3Vic2NyaWJlUmVzdWx0IkUKEVNlc3Npb25TdGF0", + "ZVJlcGx5EjAKBXN0YXRlGAEgASgOMiEubXhhY2Nlc3NfZ2F0ZXdheS52MS5T", + "ZXNzaW9uU3RhdGUidQoPV29ya2VySW5mb1JlcGx5EhkKEXdvcmtlcl9wcm9j", + "ZXNzX2lkGAEgASgFEhYKDndvcmtlcl92ZXJzaW9uGAIgASgJEhcKD214YWNj", + "ZXNzX3Byb2dpZBgDIAEoCRIWCg5teGFjY2Vzc19jbHNpZBgEIAEoCSJAChBE", + "cmFpbkV2ZW50c1JlcGx5EiwKBmV2ZW50cxgBIAMoCzIcLm14YWNjZXNzX2dh", + "dGV3YXkudjEuTXhFdmVudCI1ChxBY2tub3dsZWRnZUFsYXJtUmVwbHlQYXls", + "b2FkEhUKDW5hdGl2ZV9zdGF0dXMYASABKAUiXAodUXVlcnlBY3RpdmVBbGFy", + "bXNSZXBseVBheWxvYWQSOwoJc25hcHNob3RzGAEgAygLMigubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5BY3RpdmVBbGFybVNuYXBzaG90IucGCgdNeEV2ZW50EjIK", + "BmZhbWlseRgBIAEoDjIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhFdmVudEZh", + "bWlseRISCgpzZXNzaW9uX2lkGAIgASgJEhUKDXNlcnZlcl9oYW5kbGUYAyAB", + "KAUSEwoLaXRlbV9oYW5kbGUYBCABKAUSKwoFdmFsdWUYBSABKAsyHC5teGFj", + "Y2Vzc19nYXRld2F5LnYxLk14VmFsdWUSDwoHcXVhbGl0eRgGIAEoBRI0ChBz", + "b3VyY2VfdGltZXN0YW1wGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVz", + "dGFtcBI0CghzdGF0dXNlcxgIIAMoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEu", + "TXhTdGF0dXNQcm94eRIXCg93b3JrZXJfc2VxdWVuY2UYCSABKAQSNAoQd29y", + "a2VyX3RpbWVzdGFtcBgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh", + "bXASPQoZZ2F0ZXdheV9yZWNlaXZlX3RpbWVzdGFtcBgLIAEoCzIaLmdvb2ds", + "ZS5wcm90b2J1Zi5UaW1lc3RhbXASFAoHaHJlc3VsdBgMIAEoBUgBiAEBEhIK", + "CnJhd19zdGF0dXMYDSABKAkSQAoOb25fZGF0YV9jaGFuZ2UYFCABKAsyJi5t", + "eGFjY2Vzc19nYXRld2F5LnYxLk9uRGF0YUNoYW5nZUV2ZW50SAASRgoRb25f", + "d3JpdGVfY29tcGxldGUYFSABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLk9u", + "V3JpdGVDb21wbGV0ZUV2ZW50SAASSQoSb3BlcmF0aW9uX2NvbXBsZXRlGBYg", + "ASgLMisubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVyYXRpb25Db21wbGV0ZUV2", + "ZW50SAASUQoXb25fYnVmZmVyZWRfZGF0YV9jaGFuZ2UYFyABKAsyLi5teGFj", + "Y2Vzc19nYXRld2F5LnYxLk9uQnVmZmVyZWREYXRhQ2hhbmdlRXZlbnRIABJK", + "ChNvbl9hbGFybV90cmFuc2l0aW9uGBggASgLMisubXhhY2Nlc3NfZ2F0ZXdh", + "eS52MS5PbkFsYXJtVHJhbnNpdGlvbkV2ZW50SABCBgoEYm9keUIKCghfaHJl", + "c3VsdCITChFPbkRhdGFDaGFuZ2VFdmVudCIWChRPbldyaXRlQ29tcGxldGVF", + "dmVudCIYChZPcGVyYXRpb25Db21wbGV0ZUV2ZW50ItQBChlPbkJ1ZmZlcmVk", + "RGF0YUNoYW5nZUV2ZW50EjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNz", + "X2dhdGV3YXkudjEuTXhEYXRhVHlwZRI0Cg5xdWFsaXR5X3ZhbHVlcxgCIAEo", + "CzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheRI2ChB0aW1lc3RhbXBf", + "dmFsdWVzGAMgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EhUK", + "DXJhd19kYXRhX3R5cGUYBCABKAUi/QMKFk9uQWxhcm1UcmFuc2l0aW9uRXZl", + "bnQSHAoUYWxhcm1fZnVsbF9yZWZlcmVuY2UYASABKAkSHwoXc291cmNlX29i", + "amVjdF9yZWZlcmVuY2UYAiABKAkSFwoPYWxhcm1fdHlwZV9uYW1lGAMgASgJ", + "EkEKD3RyYW5zaXRpb25fa2luZBgEIAEoDjIoLm14YWNjZXNzX2dhdGV3YXku", + "djEuQWxhcm1UcmFuc2l0aW9uS2luZBIQCghzZXZlcml0eRgFIAEoBRI8Chhv", + "cmlnaW5hbF9yYWlzZV90aW1lc3RhbXAYBiABKAsyGi5nb29nbGUucHJvdG9i", + "dWYuVGltZXN0YW1wEjgKFHRyYW5zaXRpb25fdGltZXN0YW1wGAcgASgLMhou", + "Z29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1vcGVyYXRvcl91c2VyGAgg", + "ASgJEhgKEG9wZXJhdG9yX2NvbW1lbnQYCSABKAkSEAoIY2F0ZWdvcnkYCiAB", + "KAkSEwoLZGVzY3JpcHRpb24YCyABKAkSMwoNY3VycmVudF92YWx1ZRgMIAEo", + "CzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIxCgtsaW1pdF92YWx1", + "ZRgNIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZSL9AwoTQWN0", + "aXZlQWxhcm1TbmFwc2hvdBIcChRhbGFybV9mdWxsX3JlZmVyZW5jZRgBIAEo", + "CRIfChdzb3VyY2Vfb2JqZWN0X3JlZmVyZW5jZRgCIAEoCRIXCg9hbGFybV90", + "eXBlX25hbWUYAyABKAkSEAoIc2V2ZXJpdHkYBCABKAUSPAoYb3JpZ2luYWxf", + "cmFpc2VfdGltZXN0YW1wGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVz", + "dGFtcBI/Cg1jdXJyZW50X3N0YXRlGAYgASgOMigubXhhY2Nlc3NfZ2F0ZXdh", + "eS52MS5BbGFybUNvbmRpdGlvblN0YXRlEhAKCGNhdGVnb3J5GAcgASgJEhMK", + "C2Rlc2NyaXB0aW9uGAggASgJEj0KGWxhc3RfdHJhbnNpdGlvbl90aW1lc3Rh", + "bXAYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJh", + "dG9yX3VzZXIYCiABKAkSGAoQb3BlcmF0b3JfY29tbWVudBgLIAEoCRIzCg1j", + "dXJyZW50X3ZhbHVlGAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZh", + "bHVlEjEKC2xpbWl0X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5NeFZhbHVlIpIBChdBY2tub3dsZWRnZUFsYXJtUmVxdWVzdBISCgpzZXNz", + "aW9uX2lkGAEgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgCIAEoCRIc", + "ChRhbGFybV9mdWxsX3JlZmVyZW5jZRgDIAEoCRIPCgdjb21tZW50GAQgASgJ", + "EhUKDW9wZXJhdG9yX3VzZXIYBSABKAki8wEKFUFja25vd2xlZGdlQWxhcm1S", + "ZXBseRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvcnJlbGF0aW9uX2lkGAIg", + "ASgJEjwKD3Byb3RvY29sX3N0YXR1cxgDIAEoCzIjLm14YWNjZXNzX2dhdGV3", + "YXkudjEuUHJvdG9jb2xTdGF0dXMSFAoHaHJlc3VsdBgEIAEoBUgAiAEBEjIK", + "BnN0YXR1cxgFIAEoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQ", + "cm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UYBiABKAlCCgoIX2hyZXN1bHQi", + "agoYUXVlcnlBY3RpdmVBbGFybXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASAB", + "KAkSHQoVY2xpZW50X2NvcnJlbGF0aW9uX2lkGAIgASgJEhsKE2FsYXJtX2Zp", + "bHRlcl9wcmVmaXgYAyABKAki6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2Vz", + "cxgBIAEoBRI3CghjYXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXku", + "djEuTXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14", + "YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQg", + "ASgFEhQKDHJhd19jYXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkY", + "BiABKAUSFwoPZGlhZ25vc3RpY190ZXh0GAcgASgJIqcDCgdNeFZhbHVlEjIK", + "CWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRh", + "VHlwZRIUCgx2YXJpYW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIW", + "Cg5yYXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgF", + "EhQKCmJvb2xfdmFsdWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgA", + "EhUKC2ludDY0X3ZhbHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJI", + "ABIWCgxkb3VibGVfdmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyAB", + "KAlIABI1Cg90aW1lc3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9i", + "dWYuVGltZXN0YW1wSAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vz", + "c19nYXRld2F5LnYxLk14QXJyYXlIABITCglyYXdfdmFsdWUYEiABKAxIAEIG", + "CgRraW5kIv4ECgdNeEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgO", + "Mh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRf", + "dHlwZRgCIAEoCRISCgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9z", + "dGljGAQgASgJEh0KFXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgti", + "b29sX3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFy", + "cmF5SAASNwoMaW50MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdh", + "eS52MS5JbnQzMkFycmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhh", + "Y2Nlc3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVz", + "GA0gASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoN", + "ZG91YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91", + "YmxlQXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5TdHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMY", + "ECABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAAS", + "MwoKcmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3", + "QXJyYXlIAEIICgZ2YWx1ZXMiGwoJQm9vbEFycmF5Eg4KBnZhbHVlcxgBIAMo", + "CCIcCgpJbnQzMkFycmF5Eg4KBnZhbHVlcxgBIAMoBSIcCgpJbnQ2NEFycmF5", + "Eg4KBnZhbHVlcxgBIAMoAyIcCgpGbG9hdEFycmF5Eg4KBnZhbHVlcxgBIAMo", + "AiIdCgtEb3VibGVBcnJheRIOCgZ2YWx1ZXMYASADKAEiHQoLU3RyaW5nQXJy", + "YXkSDgoGdmFsdWVzGAEgAygJIjwKDlRpbWVzdGFtcEFycmF5EioKBnZhbHVl", + "cxgBIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiGgoIUmF3QXJy", + "YXkSDgoGdmFsdWVzGAEgAygMIlgKDlByb3RvY29sU3RhdHVzEjUKBGNvZGUY", + "ASABKA4yJy5teGFjY2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVzQ29k", + "ZRIPCgdtZXNzYWdlGAIgASgJKu4JCg1NeENvbW1hbmRLaW5kEh8KG01YX0NP", + "TU1BTkRfS0lORF9VTlNQRUNJRklFRBAAEhwKGE1YX0NPTU1BTkRfS0lORF9S", + "RUdJU1RFUhABEh4KGk1YX0NPTU1BTkRfS0lORF9VTlJFR0lTVEVSEAISHAoY", + "TVhfQ09NTUFORF9LSU5EX0FERF9JVEVNEAMSHQoZTVhfQ09NTUFORF9LSU5E", + "X0FERF9JVEVNMhAEEh8KG01YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTRAF", + "EhoKFk1YX0NPTU1BTkRfS0lORF9BRFZJU0UQBhIdChlNWF9DT01NQU5EX0tJ", + "TkRfVU5fQURWSVNFEAcSJgoiTVhfQ09NTUFORF9LSU5EX0FEVklTRV9TVVBF", + "UlZJU09SWRAIEiUKIU1YX0NPTU1BTkRfS0lORF9BRERfQlVGRkVSRURfSVRF", + "TRAJEjAKLE1YX0NPTU1BTkRfS0lORF9TRVRfQlVGRkVSRURfVVBEQVRFX0lO", + "VEVSVkFMEAoSGwoXTVhfQ09NTUFORF9LSU5EX1NVU1BFTkQQCxIcChhNWF9D", + "T01NQU5EX0tJTkRfQUNUSVZBVEUQDBIZChVNWF9DT01NQU5EX0tJTkRfV1JJ", + "VEUQDRIaChZNWF9DT01NQU5EX0tJTkRfV1JJVEUyEA4SIQodTVhfQ09NTUFO", + "RF9LSU5EX1dSSVRFX1NFQ1VSRUQQDxIiCh5NWF9DT01NQU5EX0tJTkRfV1JJ", + "VEVfU0VDVVJFRDIQEBIlCiFNWF9DT01NQU5EX0tJTkRfQVVUSEVOVElDQVRF", + "X1VTRVIQERIoCiRNWF9DT01NQU5EX0tJTkRfQVJDSEVTVFJBX1VTRVJfVE9f", + "SUQQEhIhCh1NWF9DT01NQU5EX0tJTkRfQUREX0lURU1fQlVMSxATEiQKIE1Y", + "X0NPTU1BTkRfS0lORF9BRFZJU0VfSVRFTV9CVUxLEBQSJAogTVhfQ09NTUFO", + "RF9LSU5EX1JFTU9WRV9JVEVNX0JVTEsQFRInCiNNWF9DT01NQU5EX0tJTkRf", + "VU5fQURWSVNFX0lURU1fQlVMSxAWEiIKHk1YX0NPTU1BTkRfS0lORF9TVUJT", + "Q1JJQkVfQlVMSxAXEiQKIE1YX0NPTU1BTkRfS0lORF9VTlNVQlNDUklCRV9C", + "VUxLEBgSJAogTVhfQ09NTUFORF9LSU5EX1NVQlNDUklCRV9BTEFSTVMQGRIm", + "CiJNWF9DT01NQU5EX0tJTkRfVU5TVUJTQ1JJQkVfQUxBUk1TEBoSJQohTVhf", + "Q09NTUFORF9LSU5EX0FDS05PV0xFREdFX0FMQVJNEBsSJwojTVhfQ09NTUFO", + "RF9LSU5EX1FVRVJZX0FDVElWRV9BTEFSTVMQHBItCilNWF9DT01NQU5EX0tJ", + "TkRfQUNLTk9XTEVER0VfQUxBUk1fQllfTkFNRRAdEhgKFE1YX0NPTU1BTkRf", + "S0lORF9QSU5HEGQSJQohTVhfQ09NTUFORF9LSU5EX0dFVF9TRVNTSU9OX1NU", + "QVRFEGUSIwofTVhfQ09NTUFORF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBmEiAK", + "HE1YX0NPTU1BTkRfS0lORF9EUkFJTl9FVkVOVFMQZxIjCh9NWF9DT01NQU5E", + "X0tJTkRfU0hVVERPV05fV09SS0VSEGgq+QEKDU14RXZlbnRGYW1pbHkSHwob", + "TVhfRVZFTlRfRkFNSUxZX1VOU1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRfRkFN", + "SUxZX09OX0RBVEFfQ0hBTkdFEAESJQohTVhfRVZFTlRfRkFNSUxZX09OX1dS", + "SVRFX0NPTVBMRVRFEAISJgoiTVhfRVZFTlRfRkFNSUxZX09QRVJBVElPTl9D", + "T01QTEVURRADEisKJ01YX0VWRU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9EQVRB", + "X0NIQU5HRRAEEicKI01YX0VWRU5UX0ZBTUlMWV9PTl9BTEFSTV9UUkFOU0lU", + "SU9OEAUqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJBTlNJ", + "VElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElPTl9L", + "SU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05PV0xF", + "REdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwofQUxB", + "Uk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNvbmRp", + "dGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJRklF", + "RBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJBTEFS", + "TV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1fQ09O", + "RElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdvcnkS", + "IgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhfU1RB", + "VFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdPUllf", + "T0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVORElORxADEh4KGk1YX1NU", + "QVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVHT1JZ", + "X0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdPUllf", + "Q09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09SWV9P", + "UEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TRUNV", + "UklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FSRV9F", + "UlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAKKsoC", + "Cg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BFQ0lG", + "SUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhfU1RB", + "VFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19TT1VS", + "Q0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JFUVVF", + "U1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX05N", + "WBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFUSU9O", + "X09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19BVVRP", + "TUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFfVFlQ", + "RV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAESGAoU", + "TVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9PTEVB", + "ThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9UWVBF", + "X0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RBVEFf", + "VFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1YX0RB", + "VEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVGRVJF", + "TkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIVChFN", + "WF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklUWV9D", + "TEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFfUVVB", + "TElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVNEA8S", + "IQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9EQVRB", + "X1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5HEBESGwoXTVhfREFUQV9U", + "WVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMKElBy", + "b3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9VTlNQ", + "RUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAokUFJP", + "VE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJPVE9D", + "T0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9UT0NP", + "TF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RPQ09M", + "X1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RPQ09M", + "X1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNfQ09E", + "RV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RPQ09M", + "X1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUNDRVNT", + "X0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RBVEVf", + "VU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAESIQod", + "U0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9OX1NU", + "QVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hBTkRT", + "SEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09SS0VS", + "EAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RBVEVf", + "Q0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VTU0lP", + "Tl9TVEFURV9GQVVMVEVEEAky4AQKD014QWNjZXNzR2F0ZXdheRJdCgtPcGVu", + "U2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXF1", + "ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5EmAK", + "DENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNz", + "aW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9u", + "UmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1h", + "bmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXBs", + "eRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmVh", + "bUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQw", + "ARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5B", + "Y2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXkudjEu", + "QWNrbm93bGVkZ2VBbGFybVJlcGx5Em4KEVF1ZXJ5QWN0aXZlQWxhcm1zEi0u", + "bXhhY2Nlc3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3Qa", + "KC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3QwAUIc", + "qgIZTXhHYXRld2F5LkNvbnRyYWN0cy5Qcm90b2IGcHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] { @@ -422,7 +430,7 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionReply), global::MxGateway.Contracts.Proto.CloseSessionReply.Parser, new[]{ "SessionId", "FinalState", "ProtocolStatus" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamEventsRequest), global::MxGateway.Contracts.Proto.StreamEventsRequest.Parser, new[]{ "SessionId", "AfterWorkerSequence" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommandRequest), global::MxGateway.Contracts.Proto.MxCommandRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "Command" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommand), global::MxGateway.Contracts.Proto.MxCommand.Parser, new[]{ "Kind", "Register", "Unregister", "AddItem", "AddItem2", "RemoveItem", "Advise", "UnAdvise", "AdviseSupervisory", "AddBufferedItem", "SetBufferedUpdateInterval", "Suspend", "Activate", "Write", "Write2", "WriteSecured", "WriteSecured2", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "SubscribeAlarms", "UnsubscribeAlarms", "AcknowledgeAlarmCommand", "QueryActiveAlarmsCommand", "Ping", "GetSessionState", "GetWorkerInfo", "DrainEvents", "ShutdownWorker" }, new[]{ "Payload" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxCommand), global::MxGateway.Contracts.Proto.MxCommand.Parser, new[]{ "Kind", "Register", "Unregister", "AddItem", "AddItem2", "RemoveItem", "Advise", "UnAdvise", "AdviseSupervisory", "AddBufferedItem", "SetBufferedUpdateInterval", "Suspend", "Activate", "Write", "Write2", "WriteSecured", "WriteSecured2", "AuthenticateUser", "ArchestraUserToId", "AddItemBulk", "AdviseItemBulk", "RemoveItemBulk", "UnAdviseItemBulk", "SubscribeBulk", "UnsubscribeBulk", "SubscribeAlarms", "UnsubscribeAlarms", "AcknowledgeAlarmCommand", "QueryActiveAlarmsCommand", "AcknowledgeAlarmByNameCommand", "Ping", "GetSessionState", "GetWorkerInfo", "DrainEvents", "ShutdownWorker" }, new[]{ "Payload" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.RegisterCommand), global::MxGateway.Contracts.Proto.RegisterCommand.Parser, new[]{ "ClientName" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnregisterCommand), global::MxGateway.Contracts.Proto.UnregisterCommand.Parser, new[]{ "ServerHandle" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AddItemCommand), global::MxGateway.Contracts.Proto.AddItemCommand.Parser, new[]{ "ServerHandle", "ItemDefinition" }, null, null, null, null), @@ -450,6 +458,7 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand), global::MxGateway.Contracts.Proto.UnsubscribeAlarmsCommand.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand), global::MxGateway.Contracts.Proto.AcknowledgeAlarmCommand.Parser, new[]{ "AlarmGuid", "Comment", "OperatorUser", "OperatorNode", "OperatorDomain", "OperatorFullName" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand), global::MxGateway.Contracts.Proto.QueryActiveAlarmsCommand.Parser, new[]{ "AlarmFilterPrefix" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand), global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand.Parser, new[]{ "AlarmName", "ProviderName", "GroupName", "Comment", "OperatorUser", "OperatorNode", "OperatorDomain", "OperatorFullName" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.UnsubscribeBulkCommand), global::MxGateway.Contracts.Proto.UnsubscribeBulkCommand.Parser, new[]{ "ServerHandle", "ItemHandles" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.PingCommand), global::MxGateway.Contracts.Proto.PingCommand.Parser, new[]{ "Message" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.GetSessionStateCommand), global::MxGateway.Contracts.Proto.GetSessionStateCommand.Parser, null, null, null, null, null), @@ -530,6 +539,7 @@ namespace MxGateway.Contracts.Proto { [pbr::OriginalName("MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS")] UnsubscribeAlarms = 26, [pbr::OriginalName("MX_COMMAND_KIND_ACKNOWLEDGE_ALARM")] AcknowledgeAlarm = 27, [pbr::OriginalName("MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS")] QueryActiveAlarms = 28, + [pbr::OriginalName("MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME")] AcknowledgeAlarmByName = 29, [pbr::OriginalName("MX_COMMAND_KIND_PING")] Ping = 100, [pbr::OriginalName("MX_COMMAND_KIND_GET_SESSION_STATE")] GetSessionState = 101, [pbr::OriginalName("MX_COMMAND_KIND_GET_WORKER_INFO")] GetWorkerInfo = 102, @@ -2578,6 +2588,9 @@ namespace MxGateway.Contracts.Proto { case PayloadOneofCase.QueryActiveAlarmsCommand: QueryActiveAlarmsCommand = other.QueryActiveAlarmsCommand.Clone(); break; + case PayloadOneofCase.AcknowledgeAlarmByNameCommand: + AcknowledgeAlarmByNameCommand = other.AcknowledgeAlarmByNameCommand.Clone(); + break; case PayloadOneofCase.Ping: Ping = other.Ping.Clone(); break; @@ -2952,6 +2965,18 @@ namespace MxGateway.Contracts.Proto { } } + /// Field number for the "acknowledge_alarm_by_name_command" field. + public const int AcknowledgeAlarmByNameCommandFieldNumber = 38; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand AcknowledgeAlarmByNameCommand { + get { return payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand ? (global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.AcknowledgeAlarmByNameCommand; + } + } + /// Field number for the "ping" field. public const int PingFieldNumber = 100; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -3044,6 +3069,7 @@ namespace MxGateway.Contracts.Proto { UnsubscribeAlarms = 35, AcknowledgeAlarmCommand = 36, QueryActiveAlarmsCommand = 37, + AcknowledgeAlarmByNameCommand = 38, Ping = 100, GetSessionState = 101, GetWorkerInfo = 102, @@ -3108,6 +3134,7 @@ namespace MxGateway.Contracts.Proto { if (!object.Equals(UnsubscribeAlarms, other.UnsubscribeAlarms)) return false; if (!object.Equals(AcknowledgeAlarmCommand, other.AcknowledgeAlarmCommand)) return false; if (!object.Equals(QueryActiveAlarmsCommand, other.QueryActiveAlarmsCommand)) return false; + if (!object.Equals(AcknowledgeAlarmByNameCommand, other.AcknowledgeAlarmByNameCommand)) return false; if (!object.Equals(Ping, other.Ping)) return false; if (!object.Equals(GetSessionState, other.GetSessionState)) return false; if (!object.Equals(GetWorkerInfo, other.GetWorkerInfo)) return false; @@ -3150,6 +3177,7 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.UnsubscribeAlarms) hash ^= UnsubscribeAlarms.GetHashCode(); if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmCommand) hash ^= AcknowledgeAlarmCommand.GetHashCode(); if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) hash ^= QueryActiveAlarmsCommand.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) hash ^= AcknowledgeAlarmByNameCommand.GetHashCode(); if (payloadCase_ == PayloadOneofCase.Ping) hash ^= Ping.GetHashCode(); if (payloadCase_ == PayloadOneofCase.GetSessionState) hash ^= GetSessionState.GetHashCode(); if (payloadCase_ == PayloadOneofCase.GetWorkerInfo) hash ^= GetWorkerInfo.GetHashCode(); @@ -3290,6 +3318,10 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(170, 2); output.WriteMessage(QueryActiveAlarmsCommand); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) { + output.WriteRawTag(178, 2); + output.WriteMessage(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3436,6 +3468,10 @@ namespace MxGateway.Contracts.Proto { output.WriteRawTag(170, 2); output.WriteMessage(QueryActiveAlarmsCommand); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) { + output.WriteRawTag(178, 2); + output.WriteMessage(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3553,6 +3589,9 @@ namespace MxGateway.Contracts.Proto { if (payloadCase_ == PayloadOneofCase.QueryActiveAlarmsCommand) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(QueryActiveAlarmsCommand); } + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(Ping); } @@ -3752,6 +3791,12 @@ namespace MxGateway.Contracts.Proto { } QueryActiveAlarmsCommand.MergeFrom(other.QueryActiveAlarmsCommand); break; + case PayloadOneofCase.AcknowledgeAlarmByNameCommand: + if (AcknowledgeAlarmByNameCommand == null) { + AcknowledgeAlarmByNameCommand = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand(); + } + AcknowledgeAlarmByNameCommand.MergeFrom(other.AcknowledgeAlarmByNameCommand); + break; case PayloadOneofCase.Ping: if (Ping == null) { Ping = new global::MxGateway.Contracts.Proto.PingCommand(); @@ -4059,6 +4104,15 @@ namespace MxGateway.Contracts.Proto { QueryActiveAlarmsCommand = subBuilder; break; } + case 306: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) { + subBuilder.MergeFrom(AcknowledgeAlarmByNameCommand); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarmByNameCommand = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.PingCommand subBuilder = new global::MxGateway.Contracts.Proto.PingCommand(); if (payloadCase_ == PayloadOneofCase.Ping) { @@ -4379,6 +4433,15 @@ namespace MxGateway.Contracts.Proto { QueryActiveAlarmsCommand = subBuilder; break; } + case 306: { + global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand subBuilder = new global::MxGateway.Contracts.Proto.AcknowledgeAlarmByNameCommand(); + if (payloadCase_ == PayloadOneofCase.AcknowledgeAlarmByNameCommand) { + subBuilder.MergeFrom(AcknowledgeAlarmByNameCommand); + } + input.ReadMessage(subBuilder); + AcknowledgeAlarmByNameCommand = subBuilder; + break; + } case 802: { global::MxGateway.Contracts.Proto.PingCommand subBuilder = new global::MxGateway.Contracts.Proto.PingCommand(); if (payloadCase_ == PayloadOneofCase.Ping) { @@ -11300,6 +11363,481 @@ namespace MxGateway.Contracts.Proto { } + /// + /// Acknowledge a single alarm by its (name, provider, group) tuple. Used + /// when the public RPC's AlarmFullReference (Provider!Group.Tag) cannot + /// be resolved to a GUID directly. The worker invokes + /// wwAlarmConsumerClass.AlarmAckByName which reaches the same alarm + /// history path as AlarmAckByGUID. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class AcknowledgeAlarmByNameCommand : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AcknowledgeAlarmByNameCommand()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[34]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmByNameCommand() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmByNameCommand(AcknowledgeAlarmByNameCommand other) : this() { + alarmName_ = other.alarmName_; + providerName_ = other.providerName_; + groupName_ = other.groupName_; + comment_ = other.comment_; + operatorUser_ = other.operatorUser_; + operatorNode_ = other.operatorNode_; + operatorDomain_ = other.operatorDomain_; + operatorFullName_ = other.operatorFullName_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AcknowledgeAlarmByNameCommand Clone() { + return new AcknowledgeAlarmByNameCommand(this); + } + + /// Field number for the "alarm_name" field. + public const int AlarmNameFieldNumber = 1; + private string alarmName_ = ""; + /// + /// Tag/alarm name (e.g. "TestMachine_001.TestAlarm001"). Tag itself + /// may contain dots; the gateway-side parser splits on the first dot + /// after the '!' separator. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string AlarmName { + get { return alarmName_; } + set { + alarmName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "provider_name" field. + public const int ProviderNameFieldNumber = 2; + private string providerName_ = ""; + /// + /// AVEVA alarm-provider name (literal "Galaxy" for ArchestrA Galaxies). + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string ProviderName { + get { return providerName_; } + set { + providerName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "group_name" field. + public const int GroupNameFieldNumber = 3; + private string groupName_ = ""; + /// + /// Area/group name (e.g. "TestArea"). + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string GroupName { + get { return groupName_; } + set { + groupName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "comment" field. + public const int CommentFieldNumber = 4; + private string comment_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string Comment { + get { return comment_; } + set { + comment_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_user" field. + public const int OperatorUserFieldNumber = 5; + private string operatorUser_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorUser { + get { return operatorUser_; } + set { + operatorUser_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_node" field. + public const int OperatorNodeFieldNumber = 6; + private string operatorNode_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorNode { + get { return operatorNode_; } + set { + operatorNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_domain" field. + public const int OperatorDomainFieldNumber = 7; + private string operatorDomain_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorDomain { + get { return operatorDomain_; } + set { + operatorDomain_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "operator_full_name" field. + public const int OperatorFullNameFieldNumber = 8; + private string operatorFullName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string OperatorFullName { + get { return operatorFullName_; } + set { + operatorFullName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as AcknowledgeAlarmByNameCommand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(AcknowledgeAlarmByNameCommand other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (AlarmName != other.AlarmName) return false; + if (ProviderName != other.ProviderName) return false; + if (GroupName != other.GroupName) return false; + if (Comment != other.Comment) return false; + if (OperatorUser != other.OperatorUser) return false; + if (OperatorNode != other.OperatorNode) return false; + if (OperatorDomain != other.OperatorDomain) return false; + if (OperatorFullName != other.OperatorFullName) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (AlarmName.Length != 0) hash ^= AlarmName.GetHashCode(); + if (ProviderName.Length != 0) hash ^= ProviderName.GetHashCode(); + if (GroupName.Length != 0) hash ^= GroupName.GetHashCode(); + if (Comment.Length != 0) hash ^= Comment.GetHashCode(); + if (OperatorUser.Length != 0) hash ^= OperatorUser.GetHashCode(); + if (OperatorNode.Length != 0) hash ^= OperatorNode.GetHashCode(); + if (OperatorDomain.Length != 0) hash ^= OperatorDomain.GetHashCode(); + if (OperatorFullName.Length != 0) hash ^= OperatorFullName.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (AlarmName.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmName); + } + if (ProviderName.Length != 0) { + output.WriteRawTag(18); + output.WriteString(ProviderName); + } + if (GroupName.Length != 0) { + output.WriteRawTag(26); + output.WriteString(GroupName); + } + if (Comment.Length != 0) { + output.WriteRawTag(34); + output.WriteString(Comment); + } + if (OperatorUser.Length != 0) { + output.WriteRawTag(42); + output.WriteString(OperatorUser); + } + if (OperatorNode.Length != 0) { + output.WriteRawTag(50); + output.WriteString(OperatorNode); + } + if (OperatorDomain.Length != 0) { + output.WriteRawTag(58); + output.WriteString(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + output.WriteRawTag(66); + output.WriteString(OperatorFullName); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (AlarmName.Length != 0) { + output.WriteRawTag(10); + output.WriteString(AlarmName); + } + if (ProviderName.Length != 0) { + output.WriteRawTag(18); + output.WriteString(ProviderName); + } + if (GroupName.Length != 0) { + output.WriteRawTag(26); + output.WriteString(GroupName); + } + if (Comment.Length != 0) { + output.WriteRawTag(34); + output.WriteString(Comment); + } + if (OperatorUser.Length != 0) { + output.WriteRawTag(42); + output.WriteString(OperatorUser); + } + if (OperatorNode.Length != 0) { + output.WriteRawTag(50); + output.WriteString(OperatorNode); + } + if (OperatorDomain.Length != 0) { + output.WriteRawTag(58); + output.WriteString(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + output.WriteRawTag(66); + output.WriteString(OperatorFullName); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (AlarmName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(AlarmName); + } + if (ProviderName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(ProviderName); + } + if (GroupName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(GroupName); + } + if (Comment.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Comment); + } + if (OperatorUser.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorUser); + } + if (OperatorNode.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorNode); + } + if (OperatorDomain.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorDomain); + } + if (OperatorFullName.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(OperatorFullName); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(AcknowledgeAlarmByNameCommand other) { + if (other == null) { + return; + } + if (other.AlarmName.Length != 0) { + AlarmName = other.AlarmName; + } + if (other.ProviderName.Length != 0) { + ProviderName = other.ProviderName; + } + if (other.GroupName.Length != 0) { + GroupName = other.GroupName; + } + if (other.Comment.Length != 0) { + Comment = other.Comment; + } + if (other.OperatorUser.Length != 0) { + OperatorUser = other.OperatorUser; + } + if (other.OperatorNode.Length != 0) { + OperatorNode = other.OperatorNode; + } + if (other.OperatorDomain.Length != 0) { + OperatorDomain = other.OperatorDomain; + } + if (other.OperatorFullName.Length != 0) { + OperatorFullName = other.OperatorFullName; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + AlarmName = input.ReadString(); + break; + } + case 18: { + ProviderName = input.ReadString(); + break; + } + case 26: { + GroupName = input.ReadString(); + break; + } + case 34: { + Comment = input.ReadString(); + break; + } + case 42: { + OperatorUser = input.ReadString(); + break; + } + case 50: { + OperatorNode = input.ReadString(); + break; + } + case 58: { + OperatorDomain = input.ReadString(); + break; + } + case 66: { + OperatorFullName = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + AlarmName = input.ReadString(); + break; + } + case 18: { + ProviderName = input.ReadString(); + break; + } + case 26: { + GroupName = input.ReadString(); + break; + } + case 34: { + Comment = input.ReadString(); + break; + } + case 42: { + OperatorUser = input.ReadString(); + break; + } + case 50: { + OperatorNode = input.ReadString(); + break; + } + case 58: { + OperatorDomain = input.ReadString(); + break; + } + case 66: { + OperatorFullName = input.ReadString(); + break; + } + } + } + } + #endif + + } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class UnsubscribeBulkCommand : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -11315,7 +11853,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[34]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[35]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11541,7 +12079,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[35]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[36]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11739,7 +12277,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[36]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[37]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11900,7 +12438,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[37]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[38]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -12061,7 +12599,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[38]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[39]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -12259,7 +12797,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[39]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[40]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -12467,7 +13005,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[40]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[41]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13986,7 +14524,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[41]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[42]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14184,7 +14722,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[42]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[43]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14382,7 +14920,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[43]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[44]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14580,7 +15118,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[44]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[45]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14778,7 +15316,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[45]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[46]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14985,7 +15523,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[46]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[47]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15192,7 +15730,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[47]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[48]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15390,7 +15928,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[48]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[49]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15588,7 +16126,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[49]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[50]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15934,7 +16472,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[50]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[51]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16121,7 +16659,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[51]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[52]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16319,7 +16857,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[52]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[53]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16628,7 +17166,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[53]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[54]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16823,7 +17361,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[54]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[55]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -17027,7 +17565,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[55]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[56]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -17215,7 +17753,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[56]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[57]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18190,7 +18728,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[57]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[58]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18351,7 +18889,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[58]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[59]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18512,7 +19050,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[59]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[60]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18673,7 +19211,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[60]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[61]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19006,7 +19544,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[61]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[62]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19740,7 +20278,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[62]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[63]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -20430,7 +20968,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[63]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[64]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -20787,7 +21325,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[64]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[65]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21210,7 +21748,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[65]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[66]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21486,7 +22024,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[66]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[67]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21906,7 +22444,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[67]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[68]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22763,7 +23301,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[68]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[69]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23551,7 +24089,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[69]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[70]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23740,7 +24278,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[70]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[71]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23929,7 +24467,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[71]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[72]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -24118,7 +24656,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[72]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[73]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -24307,7 +24845,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[73]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[74]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -24496,7 +25034,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[74]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[75]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -24683,7 +25221,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[75]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[76]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -24870,7 +25408,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[76]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[77]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -25057,7 +25595,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[77]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[78]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] diff --git a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto index 0bbf2ab..fa71b6b 100644 --- a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -92,6 +92,7 @@ message MxCommand { UnsubscribeAlarmsCommand unsubscribe_alarms = 35; AcknowledgeAlarmCommand acknowledge_alarm_command = 36; QueryActiveAlarmsCommand query_active_alarms_command = 37; + AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38; PingCommand ping = 100; GetSessionStateCommand get_session_state = 101; GetWorkerInfoCommand get_worker_info = 102; @@ -130,6 +131,7 @@ enum MxCommandKind { MX_COMMAND_KIND_UNSUBSCRIBE_ALARMS = 26; MX_COMMAND_KIND_ACKNOWLEDGE_ALARM = 27; MX_COMMAND_KIND_QUERY_ACTIVE_ALARMS = 28; + MX_COMMAND_KIND_ACKNOWLEDGE_ALARM_BY_NAME = 29; MX_COMMAND_KIND_PING = 100; MX_COMMAND_KIND_GET_SESSION_STATE = 101; MX_COMMAND_KIND_GET_WORKER_INFO = 102; @@ -307,6 +309,27 @@ message QueryActiveAlarmsCommand { string alarm_filter_prefix = 1; } +// Acknowledge a single alarm by its (name, provider, group) tuple. Used +// when the public RPC's AlarmFullReference (Provider!Group.Tag) cannot +// be resolved to a GUID directly. The worker invokes +// wwAlarmConsumerClass.AlarmAckByName which reaches the same alarm +// history path as AlarmAckByGUID. +message AcknowledgeAlarmByNameCommand { + // Tag/alarm name (e.g. "TestMachine_001.TestAlarm001"). Tag itself + // may contain dots; the gateway-side parser splits on the first dot + // after the '!' separator. + string alarm_name = 1; + // AVEVA alarm-provider name (literal "Galaxy" for ArchestrA Galaxies). + string provider_name = 2; + // Area/group name (e.g. "TestArea"). + string group_name = 3; + string comment = 4; + string operator_user = 5; + string operator_node = 6; + string operator_domain = 7; + string operator_full_name = 8; +} + message UnsubscribeBulkCommand { int32 server_handle = 1; repeated int32 item_handles = 2; diff --git a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs index 89e6382..81b1561 100644 --- a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs +++ b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs @@ -45,6 +45,39 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher this.timeProvider = timeProvider ?? TimeProvider.System; } + /// + /// Parse a full alarm reference of the form Provider!Group.Tag + /// into its components. Convention: the first ! separates + /// provider from Group.Tag; the first . after the + /// ! separates group from tag (the tag itself may contain + /// more dots — e.g. TestMachine_001.TestAlarm001). + /// + /// true on a well-formed reference; false otherwise. + public static bool TryParseAlarmReference( + string? reference, + out string providerName, + out string groupName, + out string alarmName) + { + providerName = string.Empty; + groupName = string.Empty; + alarmName = string.Empty; + if (string.IsNullOrWhiteSpace(reference)) return false; + + int bang = reference!.IndexOf('!'); + if (bang <= 0 || bang == reference.Length - 1) return false; + + string left = reference[..bang]; + string right = reference[(bang + 1)..]; + int dot = right.IndexOf('.'); + if (dot <= 0 || dot == right.Length - 1) return false; + + providerName = left; + groupName = right[..dot]; + alarmName = right[(dot + 1)..]; + return true; + } + /// public async Task AcknowledgeAsync( AcknowledgeAlarmRequest request, @@ -64,11 +97,58 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher }; } - if (!System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid)) + WorkerCommand workerCommand; + if (System.Guid.TryParse(request.AlarmFullReference, out System.Guid guid)) + { + workerCommand = new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = guid.ToString(), + Comment = request.Comment ?? string.Empty, + OperatorUser = request.OperatorUser ?? string.Empty, + // Operator node/domain/full-name are not on the public + // RPC surface today; pass empty strings so the worker + // honours the existing AcknowledgeAlarmCommand schema. + OperatorNode = string.Empty, + OperatorDomain = string.Empty, + OperatorFullName = string.Empty, + }, + }, + EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), + }; + } + else if (TryParseAlarmReference( + request.AlarmFullReference, + out string providerName, + out string groupName, + out string alarmName)) + { + workerCommand = new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarmByName, + AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand + { + AlarmName = alarmName, + ProviderName = providerName, + GroupName = groupName, + Comment = request.Comment ?? string.Empty, + OperatorUser = request.OperatorUser ?? string.Empty, + OperatorNode = string.Empty, + OperatorDomain = string.Empty, + OperatorFullName = string.Empty, + }, + }, + EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), + }; + } + else { - // Reference→GUID lookup not yet implemented. Surface a clear - // diagnostic so client teams can plumb the reference parser - // when the worker AlarmAckByName command lands. return new AcknowledgeAlarmReply { SessionId = request.SessionId, @@ -76,33 +156,12 @@ public sealed class WorkerAlarmRpcDispatcher : IAlarmRpcDispatcher ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.InvalidRequest, - Message = "AlarmFullReference must currently be a canonical GUID; reference→GUID lookup is pending the AlarmAckByName worker command.", + Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.", }, - DiagnosticMessage = $"AcknowledgeAlarm received non-GUID reference '{request.AlarmFullReference}'.", + DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.", }; } - WorkerCommand workerCommand = new WorkerCommand - { - Command = new MxCommand - { - Kind = MxCommandKind.AcknowledgeAlarm, - AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand - { - AlarmGuid = guid.ToString(), - Comment = request.Comment ?? string.Empty, - OperatorUser = request.OperatorUser ?? string.Empty, - // Operator node/domain/full-name are not on the public - // RPC surface today; pass empty strings so the worker - // honours the existing AcknowledgeAlarmCommand schema. - OperatorNode = string.Empty, - OperatorDomain = string.Empty, - OperatorFullName = string.Empty, - }, - }, - EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), - }; - WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken) .ConfigureAwait(false); diff --git a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs index ae56f0b..dce9148 100644 --- a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs +++ b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs @@ -36,33 +36,6 @@ public sealed class WorkerAlarmRpcDispatcherTests Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code); } - [Fact] - public async Task AcknowledgeAsync_returns_invalid_request_when_reference_is_not_a_guid() - { - SessionRegistry registry = new SessionRegistry(); - FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient(); - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "s1", - ClientCorrelationId = "c1", - AlarmFullReference = "Galaxy!Area.Tag", // not a GUID - Comment = "x", - OperatorUser = "u", - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); - Assert.Equal(0, worker.InvokeCount); // dispatcher short-circuits before the IPC. - } - [Fact] public async Task AcknowledgeAsync_forwards_guid_and_returns_native_status() { @@ -148,6 +121,107 @@ public sealed class WorkerAlarmRpcDispatcherTests Assert.Contains("-123", reply.DiagnosticMessage); } + [Theory] + [InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")] + [InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")] + [InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")] + public void TryParseAlarmReference_decomposes_provider_group_tag( + string reference, string expectedProvider, string expectedGroup, string expectedName) + { + Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference( + reference, out string provider, out string group, out string name)); + Assert.Equal(expectedProvider, provider); + Assert.Equal(expectedGroup, group); + Assert.Equal(expectedName, name); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + [InlineData("no-bang-here")] + [InlineData("!Group.Tag")] // empty provider + [InlineData("Galaxy!")] // bang at end + [InlineData("Galaxy!Group")] // missing dot + [InlineData("Galaxy!.Tag")] // empty group + [InlineData("Galaxy!Group.")] // empty tag + public void TryParseAlarmReference_rejects_malformed_references(string? reference) + { + Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference( + reference, out _, out _, out _)); + } + + [Fact] + public async Task AcknowledgeAsync_routes_provider_group_tag_via_AckByName() + { + SessionRegistry registry = new SessionRegistry(); + AcknowledgeAlarmByNameCommand? observed = null; + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient + { + ReplyFactory = command => + { + Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind); + observed = command.Command.AcknowledgeAlarmByNameCommand; + return new MxCommandReply + { + Kind = MxCommandKind.AcknowledgeAlarmByName, + ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, + Hresult = 0, + AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 }, + }; + }, + }; + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "s1", + ClientCorrelationId = "c1", + AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001", + Comment = "ack-by-name", + OperatorUser = "bob", + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.NotNull(observed); + Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName); + Assert.Equal("Galaxy", observed.ProviderName); + Assert.Equal("TestArea", observed.GroupName); + Assert.Equal("bob", observed.OperatorUser); + Assert.Equal("ack-by-name", observed.Comment); + } + + [Fact] + public async Task AcknowledgeAsync_returns_invalid_request_for_unparseable_reference() + { + SessionRegistry registry = new SessionRegistry(); + FakeAlarmWorkerClient worker = new FakeAlarmWorkerClient(); + GatewaySession session = NewSession("s1"); + session.AttachWorkerClient(worker); + session.MarkReady(); + registry.TryAdd(session); + + WorkerAlarmRpcDispatcher dispatcher = new WorkerAlarmRpcDispatcher(registry); + + AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( + new AcknowledgeAlarmRequest + { + SessionId = "s1", + AlarmFullReference = "no-bang-no-dot", + }, + CancellationToken.None); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + Assert.Equal(0, worker.InvokeCount); + } + [Fact] public async Task QueryActiveAlarmsAsync_yields_each_snapshot_from_payload() { diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs index c04dd3e..cd54467 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs @@ -164,6 +164,62 @@ public sealed class AlarmCommandExecutorTests Assert.Contains("-123", reply.DiagnosticMessage); } + [Fact] + public void AcknowledgeAlarmByName_routes_tuple_to_handler() + { + FakeAlarmHandler handler = new FakeAlarmHandler { AcknowledgeReturn = 0 }; + MxAccessCommandExecutor executor = NewExecutor(handler); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarmByName, + AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand + { + AlarmName = "TestMachine_001.TestAlarm001", + ProviderName = "Galaxy", + GroupName = "TestArea", + Comment = "ack", + OperatorUser = "alice", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.NotNull(reply.AcknowledgeAlarm); + Assert.Equal(0, reply.AcknowledgeAlarm.NativeStatus); + Assert.NotNull(handler.LastAckByNameTuple); + Assert.Equal("TestMachine_001.TestAlarm001", handler.LastAckByNameTuple!.Value.Name); + Assert.Equal("Galaxy", handler.LastAckByNameTuple!.Value.Provider); + Assert.Equal("TestArea", handler.LastAckByNameTuple!.Value.Group); + Assert.Equal("alice", handler.LastAckOperatorName); + } + + [Fact] + public void AcknowledgeAlarmByName_with_empty_name_returns_invalid_request() + { + MxAccessCommandExecutor executor = NewExecutor(new FakeAlarmHandler()); + + StaCommand command = new StaCommand( + SessionId, CorrelationId, + new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarmByName, + AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand + { + AlarmName = " ", + ProviderName = "Galaxy", + GroupName = "TestArea", + }, + }); + + MxCommandReply reply = executor.Execute(command); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + } + [Fact] public void QueryActiveAlarms_returns_payload_with_snapshots() { @@ -373,6 +429,18 @@ public sealed class AlarmCommandExecutorTests return AcknowledgeReturn; } + public int AcknowledgeByName( + string alarmName, string providerName, string groupName, + string comment, string operatorUser, string operatorNode, + string operatorDomain, string operatorFullName) + { + LastAckByNameTuple = (alarmName, providerName, groupName); + LastAckOperatorName = operatorUser; + return AcknowledgeReturn; + } + + public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; } + public IReadOnlyList QueryActive(string? alarmFilterPrefix) { LastFilterPrefix = alarmFilterPrefix; diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs index 10e34a0..decd4b8 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs @@ -222,6 +222,18 @@ public sealed class AlarmCommandHandlerTests return AcknowledgeReturn; } + public int AcknowledgeByName( + string alarmName, string providerName, string groupName, + string ackComment, string ackOperatorName, string ackOperatorNode, + string ackOperatorDomain, string ackOperatorFullName) + { + LastAckByNameTuple = (alarmName, providerName, groupName); + LastAckOperatorName = ackOperatorName; + return AcknowledgeReturn; + } + + public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; } + public IReadOnlyList SnapshotActiveAlarms() => SnapshotResult; public void Dispose() diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs index 5816d64..6b3e03d 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs @@ -158,6 +158,32 @@ public sealed class AlarmDispatcherTests Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName); } + [Fact] + public void AcknowledgeByName_forwards_to_consumer_with_full_tuple() + { + FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = 0 }; + using AlarmDispatcher dispatcher = new AlarmDispatcher( + consumer, + new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), + SessionId); + + int rc = dispatcher.AcknowledgeByName( + alarmName: "TestMachine_001.TestAlarm001", + providerName: "Galaxy", + groupName: "TestArea", + ackComment: "ack", + ackOperatorName: "alice", + ackOperatorNode: "WS", + ackOperatorDomain: "CORP", + ackOperatorFullName: "Alice Smith"); + + Assert.Equal(0, rc); + Assert.NotNull(consumer.LastAckByNameTuple); + Assert.Equal("TestMachine_001.TestAlarm001", consumer.LastAckByNameTuple!.Value.Name); + Assert.Equal("Galaxy", consumer.LastAckByNameTuple!.Value.Provider); + Assert.Equal("TestArea", consumer.LastAckByNameTuple!.Value.Group); + } + [Fact] public void SnapshotActiveAlarms_maps_records_to_protos() { @@ -275,6 +301,18 @@ public sealed class AlarmDispatcherTests return AcknowledgeReturn; } + public int AcknowledgeByName( + string alarmName, string providerName, string groupName, + string ackComment, string ackOperatorName, string ackOperatorNode, + string ackOperatorDomain, string ackOperatorFullName) + { + LastAckByNameTuple = (alarmName, providerName, groupName); + LastAckOperatorName = ackOperatorName; + return AcknowledgeReturn; + } + + public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; } + public IReadOnlyList SnapshotActiveAlarms() { return SnapshotResult; diff --git a/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs index 1ac5744..7867de8 100644 --- a/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs +++ b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs @@ -120,6 +120,29 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler operatorFullName ?? string.Empty); } + /// + public int AcknowledgeByName( + string alarmName, + string providerName, + string groupName, + string comment, + string operatorUser, + string operatorNode, + string operatorDomain, + string operatorFullName) + { + AlarmDispatcher? d = GetDispatcherOrThrow(); + return d.AcknowledgeByName( + alarmName ?? string.Empty, + providerName ?? string.Empty, + groupName ?? string.Empty, + comment ?? string.Empty, + operatorUser ?? string.Empty, + operatorNode ?? string.Empty, + operatorDomain ?? string.Empty, + operatorFullName ?? string.Empty); + } + /// public IReadOnlyList QueryActive(string? alarmFilterPrefix) { @@ -184,6 +207,20 @@ public interface IAlarmCommandHandler : IDisposable string operatorDomain, string operatorFullName); + /// + /// Acknowledge a single alarm by (name, provider, group) — used when + /// the caller has the human-readable reference but not the GUID. + /// + int AcknowledgeByName( + string alarmName, + string providerName, + string groupName, + string comment, + string operatorUser, + string operatorNode, + string operatorDomain, + string operatorFullName); + /// /// Snapshot the currently-active alarm set, optionally scoped to a /// prefix matched against AlarmFullReference. diff --git a/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs index 0614531..cc4e617 100644 --- a/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs +++ b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs @@ -92,6 +92,33 @@ public sealed class AlarmDispatcher : IDisposable ackOperatorFullName); } + /// + /// Acknowledge an alarm by its (name, provider, group) tuple. + /// Routes to the consumer's AcknowledgeByName path which + /// maps to wwAlarmConsumerClass.AlarmAckByName. + /// + public int AcknowledgeByName( + string alarmName, + string providerName, + string groupName, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName) + { + if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); + return consumer.AcknowledgeByName( + alarmName, + providerName, + groupName, + ackComment, + ackOperatorName, + ackOperatorNode, + ackOperatorDomain, + ackOperatorFullName); + } + /// /// Snapshot the currently-active alarm set as /// protos for the diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs index b7ae357..c2973bd 100644 --- a/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs +++ b/src/MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs @@ -61,6 +61,23 @@ public interface IMxAccessAlarmConsumer : IDisposable string ackOperatorDomain, string ackOperatorFullName); + /// + /// Acknowledge a single alarm by its (name, provider, group) tuple. + /// Reaches AVEVA's AlarmAckByName on + /// wwAlarmConsumerClass; same alarm-history outcome as + /// , used when the caller has the + /// human-readable reference but not the canonical GUID. + /// + int AcknowledgeByName( + string alarmName, + string providerName, + string groupName, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName); + /// /// Returns the consumer's most recently parsed snapshot of currently /// active alarms. Used by the gateway's QueryActiveAlarms (PR A.7) diff --git a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index bc37d7e..da49ffa 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -83,6 +83,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor MxCommandKind.SubscribeAlarms => ExecuteSubscribeAlarms(command), MxCommandKind.UnsubscribeAlarms => ExecuteUnsubscribeAlarms(command), MxCommandKind.AcknowledgeAlarm => ExecuteAcknowledgeAlarm(command), + MxCommandKind.AcknowledgeAlarmByName => ExecuteAcknowledgeAlarmByName(command), MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command), _ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."), }; @@ -402,6 +403,54 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor } } + private MxCommandReply ExecuteAcknowledgeAlarmByName(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AcknowledgeAlarmByNameCommand) + { + return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName command payload is required."); + } + if (alarmCommandHandler is null) + { + return CreateInvalidRequestReply( + command, + "AcknowledgeAlarmByName requires an alarm command handler; the worker was constructed without one."); + } + + AcknowledgeAlarmByNameCommand payload = command.Command.AcknowledgeAlarmByNameCommand; + if (string.IsNullOrWhiteSpace(payload.AlarmName)) + { + return CreateInvalidRequestReply(command, "AcknowledgeAlarmByName.alarm_name is required."); + } + + try + { + int rc = alarmCommandHandler.AcknowledgeByName( + payload.AlarmName, + payload.ProviderName, + payload.GroupName, + payload.Comment, + payload.OperatorUser, + payload.OperatorNode, + payload.OperatorDomain, + payload.OperatorFullName); + MxCommandReply reply = CreateOkReply(command); + reply.Hresult = rc; + reply.AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload + { + NativeStatus = rc, + }; + if (rc != 0) + { + reply.DiagnosticMessage = $"AVEVA AlarmAckByName returned non-zero status {rc}."; + } + return reply; + } + catch (Exception ex) + { + return CreateAlarmFailureReply(command, ex); + } + } + private MxCommandReply ExecuteQueryActiveAlarms(StaCommand command) { if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.QueryActiveAlarmsCommand) diff --git a/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs index 04421af..a99cf9a 100644 --- a/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs +++ b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -172,6 +172,33 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer szOprFullName: ackOperatorFullName ?? string.Empty); } + /// + public int AcknowledgeByName( + string alarmName, + string providerName, + string groupName, + string ackComment, + string ackOperatorName, + string ackOperatorNode, + string ackOperatorDomain, + string ackOperatorFullName) + { + if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + wwAlarmConsumerClass com = client + ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + + return com.AlarmAckByName( + szAlarmName: alarmName ?? string.Empty, + szProviderName: providerName ?? string.Empty, + szGroupName: groupName ?? string.Empty, + szComment: ackComment ?? string.Empty, + szOprName: ackOperatorName ?? string.Empty, + szNode: ackOperatorNode ?? string.Empty, + szDomainName: ackOperatorDomain ?? string.Empty, + szOprFullName: ackOperatorFullName ?? string.Empty); + } + /// public IReadOnlyList SnapshotActiveAlarms() { -- 2.52.0 From a4ed605f74e3dc5057ffdf697719fddfe7e165fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 1 May 2026 12:17:39 -0400 Subject: [PATCH 16/16] A.3 (live smoke): full alarms-over-gateway pipeline verified end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip-gated AlarmsLiveSmokeTests.Alarms_full_pipeline_round_trip ran against the dev rig with the flip script firing TestMachine_001.TestAlarm001 every 10s. Verified: - Subscribe + 1st PollOnce yield real transition events - Field-by-field decode correct (provider, group, tag, severity, UTC timestamp, comment, type) - SnapshotActiveAlarms reflects current state - AcknowledgeByName(real identity) -> rc=0 - Pipeline keeps streaming transitions on the 10s cadence post-ack Three production quirks surfaced and were fixed in WnWrapAlarmConsumer: 1. SetXmlAlarmQuery is mandatory for reads. Skipping it (per the earlier discovery-doc recommendation) makes the first GetXmlCurrentAlarms2 fail with E_FAIL. The doc's claim that the call is unnecessary because the round-trip echo is mangled was wrong — mangled echo or not, the call is required. 2. SetXmlAlarmQuery breaks AlarmAckByName on the same consumer instance (returns -55). Workaround: provision a parallel "ack-only" wnwrap consumer that runs Initialize → Register → Subscribe via the v1-prefixed methods, no SetXmlAlarmQuery. Production WnWrapAlarmConsumer now holds two COM clients; AcknowledgeByName always dispatches through the ack-only one. 3. AlarmAckByName has v2 (8-arg) and v1 (6-arg) overloads. The v2 8-arg overload returns -55 on this AVEVA build (apparently a stub); the v1 6-arg overload works. Production now calls the 6-arg overload, discarding the proto's operator_domain and operator_full_name fields. The proto contract keeps both for forward-compat if AVEVA fixes the v2 method. Bonus finding (not fixed here): AlarmAckByGUID throws NotImplementedException on wnwrap. Reference→GUID lookup that we initially planned to plumb is therefore not viable; all acks must go through AlarmAckByName. WorkerAlarmRpcDispatcher.AcknowledgeAsync already routes references through the by-name path, so this only affects the GUID-input branch (which the worker tries first if the input parses as a GUID — that branch will surface NotImplementedException as MxaccessFailure if a client supplies one). Threading caveat: wnwrap is ThreadingModel=Apartment, so the consumer's internal Timer (firing on threadpool threads) blocks on cross-apartment marshaling without an STA message pump. The smoke test sidesteps this with pollIntervalMilliseconds=0 (Timer disabled) + manual PollOnce calls from the test STA. Production hosting will route polls through the worker's StaRuntime in a follow-up; PollOnce is now public so the wire-up is straightforward. Test counts after this slice: Worker: 195 pass / 4 skipped (live probes incl. new live smoke) / 1 pre-existing structure-fail (untouched) Server: 308 pass / 0 fail Solution builds clean. docs/AlarmClientDiscovery.md "Live smoke-test discoveries" section records all five findings. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/AlarmClientDiscovery.md | 102 +++++++ .../AlarmsLiveSmokeTests.cs | 276 ++++++++++++++++++ .../MxAccess/WnWrapAlarmConsumer.cs | 183 ++++++++++-- 3 files changed, 537 insertions(+), 24 deletions(-) create mode 100644 src/MxGateway.Worker.Tests/AlarmsLiveSmokeTests.cs diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 9ad4e54..da565bb 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -688,3 +688,105 @@ alarm-consumer surface unblocks A.2 fully. Outline: These findings retire the open follow-up probes from the "polling-vs-pump" debate above — `wwAlarmConsumerClass` plus poll-on-timer is the implementation. + +## Live smoke-test discoveries — 2026-05-01 + +The Skip-gated `AlarmsLiveSmokeTests.Alarms_full_pipeline_round_trip` +ran the full +`WnWrapAlarmConsumer` + `AlarmDispatcher` + `MxAccessAlarmEventSink` +pipeline against the dev rig with the flip script running. End-to-end +verified: 6 real transitions captured on the 10s cadence, ack-by-name +returned rc=0, pipeline stayed healthy through 5 more transitions +afterwards. Three production-relevant quirks surfaced and were fixed +in the consumer: + +### 1. `SetXmlAlarmQuery` is mandatory for reads despite the mangled echo + +Without `SetXmlAlarmQuery`, the first `GetXmlCurrentAlarms2` call +fails with `E_FAIL` (HRESULT `0x80004005`). The discovery doc above +flagged the round-trip echo as mangled and recommended skipping the +call — that recommendation is **wrong**. The echo *is* mangled (AVEVA +parses NODE/PROVIDER/ALARM_STATE/DISPLAY_MODE incorrectly), but the +call itself is required as some kind of subscription enabler. Even +the Subscribe call setting the actual filter doesn't avoid the need +for `SetXmlAlarmQuery`. + +`WnWrapAlarmConsumer.ComposeXmlAlarmQuery(subscription)` decomposes +the canonical `\\\Galaxy!` form into the XML's +NODE/PROVIDER/GROUP fields. Mangled or not, the call enables reads. + +### 2. Two consumers required: read-side vs. ack-side + +`SetXmlAlarmQuery` enables reads but **breaks `AlarmAckByName` on +the same consumer instance**. With SetXml applied, AlarmAckByName +returns -55 even with valid name+provider+group+operator. Without +SetXml, AlarmAckByName succeeds with rc=0. + +The production consumer therefore provisions **two** wnwrap COM +instances: +- Primary consumer (`client`): runs full lifecycle including + `SetXmlAlarmQuery` for `GetXmlCurrentAlarms2` polls. +- Ack-only consumer (`ackClient`): runs Initialize → Register → + Subscribe via the v1-prefixed methods, **no SetXmlAlarmQuery**. + All `AcknowledgeByName` calls dispatch through this instance. + +Both consumers subscribe to the same expression. Disposal cleans up +both via a shared `ReleaseConsumerCom` helper. + +### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg + +`wwAlarmConsumerClass` exposes two `AlarmAckByName` overloads: +- `IwwAlarmConsumer2` v2: 8 args (`name, provider, group, comment, + oprName, node, domainName, oprFullName`). +- `IwwAlarmConsumer` v1: 6 args (no domain, no full-name). + +The v2 8-arg method returns -55 on this AVEVA build regardless of +operator-identity inputs — looks like a stub. The v1 6-arg method +works. Production `WnWrapAlarmConsumer.AcknowledgeByName` calls the +6-arg overload and discards the proto's `domain` + `full_name` fields. +The proto contract keeps the 8 fields for forward compatibility if +AVEVA fixes the v2 method later. + +### 4. `AlarmAckByGUID` is not implemented + +The v2 `AlarmAckByGUID(VBGUID, …)` throws `NotImplementedException` +(COM `E_NOTIMPL`) on `wwAlarmConsumerClass` against this AVEVA +build. The reference→GUID lookup that we initially planned to wire +through `AlarmAckByGUID` is therefore not viable on wnwrap; all acks +must go through `AlarmAckByName`. + +The proto `AcknowledgeAlarmCommand` (GUID-based) and the worker's +`MxAccessCommandExecutor.ExecuteAcknowledgeAlarm` switch arm remain +in the codebase for the forward-compat shape, but the gateway-side +`WorkerAlarmRpcDispatcher.AcknowledgeAsync` now always routes through +`AcknowledgeAlarmByName` when the public RPC supplies a recognizable +`Provider!Group.Tag` reference. + +### 5. STA / threading — production fix needed + +The wnwrap COM is `ThreadingModel=Apartment`. The consumer's +internal `Timer` fires on threadpool threads and would block forever +on cross-apartment marshaling unless the host STA pumps Win32 +messages. The smoke test sidesteps this by setting +`pollIntervalMilliseconds=0` (Timer disabled) and driving `PollOnce` +manually from the test's STA. Production hosting will route polls +through the worker's `StaRuntime` in a follow-up — the consumer's +`PollOnce` is `public` and idempotent so the wire-up is mechanical. + +### Capture summary + +``` +Transition: kind=Clear ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' … +Transition: kind=Raise ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' … +SnapshotActiveAlarms count=1 + active: ref='Galaxy!TestArea.TestMachine_001.TestAlarm001' state=Active +AcknowledgeByName(real identity) -> rc=0 +Post-ack transition: kind=Clear … ++1: kind=Raise … (10s after ack) ++2: kind=Clear … (20s) ++3: kind=Raise … (30s) ++4: kind=Clear … (40s) +``` + +10s cadence held throughout; full proto fields populated correctly; +ack registered server-side without errors. diff --git a/src/MxGateway.Worker.Tests/AlarmsLiveSmokeTests.cs b/src/MxGateway.Worker.Tests/AlarmsLiveSmokeTests.cs new file mode 100644 index 0000000..a99e3d6 --- /dev/null +++ b/src/MxGateway.Worker.Tests/AlarmsLiveSmokeTests.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using MxGateway.Contracts.Proto; +using MxGateway.Worker.MxAccess; +using Xunit.Abstractions; + +namespace MxGateway.Worker.Tests; + +/// +/// Live dev-rig smoke test for the alarms-over-gateway pipeline. +/// Exercises + + +/// end-to-end against the actual +/// AVEVA System Platform install: subscribes to +/// \\<machine>\Galaxy!DEV, waits for at least one alarm +/// transition (the dev rig's flip script writes +/// TestMachine_001.TestAlarm001 every 10s), drains the proto +/// OnAlarmTransitionEvent from the queue, then ack-by-name's +/// it and verifies the ack registers as a subsequent +/// transition. +/// +/// Skip-gated; flip Skip=null on the dev rig with the flip +/// script running. +/// +public sealed class AlarmsLiveSmokeTests +{ + private static readonly string SubscriptionExpression = + $@"\\{Environment.MachineName}\Galaxy!DEV"; + private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(45); + private static readonly TimeSpan TransitionWaitTimeout = TimeSpan.FromSeconds(20); + + private const string SessionId = "alarms-live-smoke"; + + private readonly ITestOutputHelper output; + private readonly Stopwatch elapsed = Stopwatch.StartNew(); + private readonly ConcurrentQueue log = new ConcurrentQueue(); + + public AlarmsLiveSmokeTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + the alarm flip script running. Verified working 2026-05-01.")] + public void Alarms_full_pipeline_round_trip() + { + Exception? threadException = null; + var done = new ManualResetEventSlim(false); + var thread = new Thread(() => + { + try { RunSmoke(); } + catch (Exception ex) { threadException = ex; } + finally { done.Set(); } + }); + thread.IsBackground = false; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + done.Wait(); + thread.Join(); + + output.WriteLine($"Captured {log.Count} log line(s):"); + while (log.TryDequeue(out string? line)) + { + output.WriteLine(line); + } + + if (threadException != null) + { + throw threadException; + } + } + + private void RunSmoke() + { + Log($"Subscription expression: {SubscriptionExpression}"); + Log($"Pump duration: {PumpDuration.TotalSeconds:F0}s; transition wait timeout: {TransitionWaitTimeout.TotalSeconds:F0}s"); + + MxAccessEventQueue queue = new MxAccessEventQueue(); + // pollIntervalMs=0 disables the internal Timer; we drive PollOnce + // manually from the STA below to avoid threadpool→STA marshaling + // (the wnwrap COM is ThreadingModel=Apartment, and this test + // doesn't run a Win32 message pump on its STA). + WnWrapAlarmConsumer consumer = new WnWrapAlarmConsumer( + new WNWRAPCONSUMERLib.wwAlarmConsumerClass(), + pollIntervalMilliseconds: 0, + maxAlarmsPerFetch: 1024); + MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); + using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); + + Log("Constructed consumer + sink + dispatcher."); + dispatcher.Subscribe(SubscriptionExpression); + Log("Subscribe -> ok. Driving PollOnce manually from this STA..."); + + // The wnwrap COM object is ThreadingModel=Apartment. The consumer's + // internal Timer would fire on a threadpool thread and deadlock on + // cross-apartment marshaling without a Win32 message pump. For the + // smoke test we constructed the consumer with pollIntervalMs=0 + // (Timer disabled) and drive PollOnce manually here on the STA. + // Production hosting will route polls through the worker's + // StaRuntime in a follow-up PR. + + // 1. Wait for the first transition (any kind), then keep waiting + // for one with kind=Raise so the alarm is currently Active when + // we try to ack. AVEVA rejects acks of cleared alarms with -55, + // so we have to time the ack against the flip script's 10s + // cadence. + OnAlarmTransitionEvent? raiseBody = null; + DateTime raiseDeadline = DateTime.UtcNow + TimeSpan.FromSeconds(30); + while (DateTime.UtcNow < raiseDeadline && raiseBody is null) + { + WorkerEvent? evt = WaitForTransition(queue, TransitionWaitTimeout, "raise", consumer); + if (evt is null) break; + OnAlarmTransitionEvent body = evt.Event.OnAlarmTransition; + Log("Transition: " + DescribeTransition(body)); + Assert.Equal(SessionId, evt.Event.SessionId); + if (body.TransitionKind == AlarmTransitionKind.Raise) + { + raiseBody = body; + } + } + Assert.NotNull(raiseBody); + Assert.False(string.IsNullOrEmpty(raiseBody!.AlarmFullReference)); + Assert.Contains("Galaxy", raiseBody.AlarmFullReference); + + // 2. Snapshot the active set + verify the captured alarm is there. + var snapshot = dispatcher.SnapshotActiveAlarms(); + Log($"SnapshotActiveAlarms count={snapshot.Count}"); + foreach (var s in snapshot) + { + Log(" active: " + DescribeSnapshot(s)); + } + Assert.NotEmpty(snapshot); + Assert.Contains(snapshot, s => s.AlarmFullReference == raiseBody.AlarmFullReference); + + // 3. Ack-by-name using the captured reference. Parse the reference + // via the same convention the gateway dispatcher uses + // (Provider!Group.Tag where the tag may contain dots). + Assert.True(TryParseReference( + raiseBody.AlarmFullReference, + out string provider, out string group, out string alarmName), + $"Captured reference '{raiseBody.AlarmFullReference}' did not parse as Provider!Group.Tag."); + Log($"Ack target: provider='{provider}' group='{group}' name='{alarmName}'"); + + // Try the ack with real Windows identity. AVEVA's AlarmAckByName + // may reject synthetic operator strings; using the current process + // identity gives the alarm-history a recognizable principal. + string realUser = Environment.UserName; + string realNode = Environment.MachineName; + string realDomain = Environment.UserDomainName ?? string.Empty; + Log($"Ack identity: user='{realUser}' node='{realNode}' domain='{realDomain}'"); + + int rc = dispatcher.AcknowledgeByName( + alarmName: alarmName, + providerName: provider, + groupName: group, + ackComment: "alarms-live-smoke ack", + ackOperatorName: realUser, + ackOperatorNode: realNode, + ackOperatorDomain: realDomain, + ackOperatorFullName: realUser); + Log($"AcknowledgeByName(real identity) -> rc={rc}"); + + Assert.Equal(0, rc); + + // 4. Wait for the post-ack transition. With the alarm flipping every + // 10s and the consumer polling every 500ms, the next state + // change should be either kind=Acknowledge (the ack we just + // sent registered as a state delta UnackAlm → AckAlm) or the + // flip script's next Clear (UnackAlm → UnackRtn). + WorkerEvent? second = WaitForTransition(queue, TransitionWaitTimeout, "post-ack", consumer); + Assert.NotNull(second); + OnAlarmTransitionEvent secondBody = second!.Event.OnAlarmTransition; + Log("Post-ack transition: " + DescribeTransition(secondBody)); + Assert.NotEqual(AlarmTransitionKind.Unspecified, secondBody.TransitionKind); + + // 5. Pump a little longer to confirm the consumer keeps reporting + // transitions on the 10s flip cadence. + DateTime deadline = DateTime.UtcNow + PumpDuration; + int additional = 0; + while (DateTime.UtcNow < deadline) + { + consumer.PollOnce(); + if (queue.TryDequeue(out WorkerEvent? evt) && evt is not null) + { + additional++; + OnAlarmTransitionEvent body = evt.Event.OnAlarmTransition; + Log($" +{additional}: " + DescribeTransition(body)); + } + Thread.Sleep(500); + } + Log($"Pump completed; additional transitions captured: {additional}."); + } + + private WorkerEvent? WaitForTransition( + MxAccessEventQueue queue, + TimeSpan timeout, + string label, + WnWrapAlarmConsumer consumer) + { + DateTime deadline = DateTime.UtcNow + timeout; + int pollCount = 0; + while (DateTime.UtcNow < deadline) + { + try + { + consumer.PollOnce(); + pollCount++; + if (pollCount == 1) Log("First PollOnce returned without throw."); + } + catch (Exception ex) + { + Log($"PollOnce threw on poll #{pollCount + 1}: {ex.GetType().Name}: {ex.Message}"); + if (ex is System.Runtime.InteropServices.COMException ce) + { + Log($" HResult=0x{(uint)ce.HResult:X8}"); + } + throw; + } + if (queue.TryDequeue(out WorkerEvent? evt) && evt is not null) + { + if (evt.Event.Family == MxEventFamily.OnAlarmTransition) + { + return evt; + } + Log($"Skipped non-alarm event (family={evt.Event.Family}) while waiting for {label}."); + } + Thread.Sleep(500); + } + Log($"Timed out waiting for {label} transition after {timeout.TotalSeconds:F0}s (poll count={pollCount})."); + return null; + } + + private static bool TryParseReference( + string reference, + out string provider, + out string group, + out string alarmName) + { + provider = group = alarmName = string.Empty; + if (string.IsNullOrWhiteSpace(reference)) return false; + int bang = reference.IndexOf('!'); + if (bang <= 0 || bang == reference.Length - 1) return false; + string left = reference.Substring(0, bang); + string right = reference.Substring(bang + 1); + int dot = right.IndexOf('.'); + if (dot <= 0 || dot == right.Length - 1) return false; + provider = left; + group = right.Substring(0, dot); + alarmName = right.Substring(dot + 1); + return true; + } + + private static string DescribeTransition(OnAlarmTransitionEvent body) + { + return string.Format( + "kind={0} ref='{1}' source='{2}' type='{3}' severity={4} operator='{5}' comment='{6}' ts={7:o}", + body.TransitionKind, body.AlarmFullReference, body.SourceObjectReference, + body.AlarmTypeName, body.Severity, body.OperatorUser, body.OperatorComment, + body.TransitionTimestamp?.ToDateTime() ?? DateTime.MinValue); + } + + private static string DescribeSnapshot(ActiveAlarmSnapshot s) + { + return string.Format( + "ref='{0}' state={1} severity={2} operator='{3}' comment='{4}' ts={5:o}", + s.AlarmFullReference, s.CurrentState, s.Severity, s.OperatorUser, + s.OperatorComment, + s.LastTransitionTimestamp?.ToDateTime() ?? DateTime.MinValue); + } + + private void Log(string line) + { + log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}"); + } +} diff --git a/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs index a99cf9a..06dc39a 100644 --- a/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs +++ b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -57,6 +57,8 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer private readonly int maxAlarmsPerFetch; private wwAlarmConsumerClass? client; + private wwAlarmConsumerClass? ackClient; + private string subscriptionExpression = string.Empty; private Timer? pollTimer; private bool subscribed; private bool disposed; @@ -66,16 +68,23 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer { } - /// Test seam — inject a pre-created COM client and tune the poll cadence. - internal WnWrapAlarmConsumer( + /// + /// Test seam / explicit construction — inject a pre-created COM + /// client and tune the poll cadence. pollIntervalMilliseconds == 0 + /// disables the internal entirely; the caller + /// must drive manually (used by hosts that + /// marshal polls onto a foreign STA, and by live smoke tests that + /// pump from the STA they own). + /// + public WnWrapAlarmConsumer( wwAlarmConsumerClass client, int pollIntervalMilliseconds, int maxAlarmsPerFetch) { this.client = client ?? throw new ArgumentNullException(nameof(client)); - this.pollIntervalMs = pollIntervalMilliseconds > 0 - ? pollIntervalMilliseconds - : DefaultPollIntervalMilliseconds; + this.pollIntervalMs = pollIntervalMilliseconds < 0 + ? DefaultPollIntervalMilliseconds + : pollIntervalMilliseconds; this.maxAlarmsPerFetch = maxAlarmsPerFetch > 0 ? maxAlarmsPerFetch : DefaultMaxAlarmsPerFetch; @@ -104,9 +113,14 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer wwAlarmConsumerClass com = client ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); - // Per AlarmClientDiscovery.md: InitializeConsumer MUST precede - // RegisterConsumer for the alarm provider chain to become visible. - int init = com.InitializeConsumer(DefaultApplicationName); + // Use the IwwAlarmConsumer (v1) prefix-named methods for the + // lifecycle. Empirically (live dev-rig 2026-05-01) this is the + // only path that lets AlarmAckByName succeed afterwards. The + // v2 Initialize/Register/Subscribe methods on the class + // succeed (return 0) but acks against that consumer state + // return -55. The v1 prefix path is what WIN-911-style code + // uses against the same wnwrap library. + int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName); if (init != 0) { throw new InvalidOperationException( @@ -115,7 +129,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer // hWnd=0: wnwrap supports a pull-based model — no message pump // is required. We poll GetXmlCurrentAlarms2 on a timer below. - int reg = com.RegisterConsumer( + int reg = com.IwwAlarmConsumer_RegisterConsumer( hWnd: 0, szProductName: DefaultProductName, szApplicationName: DefaultApplicationName, @@ -126,7 +140,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer $"wwAlarmConsumer.RegisterConsumer returned non-zero status {reg}."); } - int sub = com.Subscribe( + int sub = com.IwwAlarmConsumer_Subscribe( szSubscription: subscription, wFromPri: 1, wToPri: 999, @@ -140,8 +154,49 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer $"wwAlarmConsumer.Subscribe('{subscription}') returned non-zero status {sub}."); } + // Empirically required: even though the round-trip echo of + // SetXmlAlarmQuery is mangled (see docs/AlarmClientDiscovery.md), + // calling it is necessary for subsequent GetXmlCurrentAlarms2 + // calls to succeed. Without it, GetXmlCurrentAlarms2 returns + // E_FAIL (HRESULT 0x80004005) on the first poll. SetXmlAlarmQuery + // also breaks AlarmAckByName on the same consumer (rejects with + // -55), so a separate ack-only consumer is provisioned below + // that gets only Initialize/Register/Subscribe (no SetXmlAlarmQuery). + string xmlQuery = ComposeXmlAlarmQuery(subscription); + com.SetXmlAlarmQuery(xmlQuery); + + // Provision a parallel COM consumer for ack calls. It runs the + // v1 lifecycle (Initialize/Register/Subscribe) only; without + // SetXmlAlarmQuery, AlarmAckByName succeeds. State is read-only + // — we never poll this consumer. + ackClient = new wwAlarmConsumerClass(); + int ackInit = ackClient.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName + ".ack"); + int ackReg = ackClient.IwwAlarmConsumer_RegisterConsumer( + hWnd: 0, + szProductName: DefaultProductName, + szApplicationName: DefaultApplicationName + ".ack", + szVersion: DefaultVersion); + int ackSub = ackClient.IwwAlarmConsumer_Subscribe( + szSubscription: subscription, + wFromPri: 1, + wToPri: 999, + QueryType: eQueryType.qtSummary, + SortFlags: eSortFlags.sfReturnNewestFirst, + FilterMask: eAlarmFilterState.asAlarmActiveNow, + FilterSpecification: eAlarmFilterState.asAlarmActiveNow); + if (ackInit != 0 || ackReg != 0 || ackSub != 0) + { + throw new InvalidOperationException( + $"Ack consumer setup returned non-zero status: " + + $"Initialize={ackInit}, Register={ackReg}, Subscribe={ackSub}."); + } + subscriptionExpression = subscription; + subscribed = true; - pollTimer = new Timer(OnPoll, state: null, dueTime: 0, period: pollIntervalMs); + if (pollIntervalMs > 0) + { + pollTimer = new Timer(OnPoll, state: null, dueTime: 0, period: pollIntervalMs); + } } } @@ -185,18 +240,31 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer { if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); - wwAlarmConsumerClass com = client - ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer)); + // Use the parallel ack-only consumer (no SetXmlAlarmQuery applied) + // — see docs/AlarmClientDiscovery.md "Option A — captured" for the + // empirical justification. + wwAlarmConsumerClass com = ackClient + ?? throw new InvalidOperationException( + "Cannot acknowledge: WnWrapAlarmConsumer was disposed or has not been subscribed yet."); + // Empirically (live dev-rig 2026-05-01): the IwwAlarmConsumer2 + // 8-arg AlarmAckByName returns -55 on this AVEVA build (looks like + // a stub). The legacy 6-arg IwwAlarmConsumer.AlarmAckByName works + // and reaches the alarm-history path correctly. Operator-domain + // and operator-full-name fields are accepted by the proto contract + // for forward-compat but are not propagated to AVEVA today — + // wrapped in the 6-arg call so domain/full-name go to the + // alarm-history operator-name field via the szOprName parameter. + // Suppress unused-warning explicitly: + _ = ackOperatorDomain; + _ = ackOperatorFullName; return com.AlarmAckByName( szAlarmName: alarmName ?? string.Empty, szProviderName: providerName ?? string.Empty, szGroupName: groupName ?? string.Empty, szComment: ackComment ?? string.Empty, szOprName: ackOperatorName ?? string.Empty, - szNode: ackOperatorNode ?? string.Empty, - szDomainName: ackOperatorDomain ?? string.Empty, - szOprFullName: ackOperatorFullName ?? string.Empty); + szNode: ackOperatorNode ?? string.Empty); } /// @@ -236,7 +304,15 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer } } - internal void PollOnce() + /// + /// Synchronously poll the wnwrap consumer once and dispatch any + /// transitions. Public so STA-bound hosts can drive polling from + /// the thread that owns the COM object instead of relying on the + /// internal (which fires on a thread-pool + /// thread and blocks indefinitely on cross-apartment marshaling + /// when the host STA isn't pumping messages). + /// + public void PollOnce() { wwAlarmConsumerClass? com; lock (syncRoot) @@ -370,6 +446,58 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer return Guid.TryParse(canonical, out guid); } + /// + /// Compose the XML payload SetXmlAlarmQuery expects from a + /// canonical subscription expression + /// (\\<machine>\Galaxy!<area>). The wnwrap + /// consumer mangles the round-trip but evidently still needs the + /// call — without it GetXmlCurrentAlarms2 fails with + /// E_FAIL. Best-effort parse: if the subscription doesn't decompose + /// cleanly, fall back to a permissive ALL-priority/ALL-state form + /// so the worker doesn't fail to start. + /// + internal static string ComposeXmlAlarmQuery(string subscription) + { + string node = Environment.MachineName; + string provider = "Galaxy"; + string group = string.Empty; + + if (!string.IsNullOrEmpty(subscription)) + { + // Strip leading backslashes from "\\\..." form. + string trimmed = subscription.TrimStart('\\'); + int slash = trimmed.IndexOf('\\'); + if (slash > 0) + { + node = trimmed.Substring(0, slash); + trimmed = trimmed.Substring(slash + 1); + } + int bang = trimmed.IndexOf('!'); + if (bang > 0) + { + provider = trimmed.Substring(0, bang); + group = trimmed.Substring(bang + 1); + } + else + { + provider = trimmed; + } + } + + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + sb.Append(""); + sb.Append(""); + sb.Append("").Append(node).Append(""); + sb.Append("").Append(provider).Append(""); + if (!string.IsNullOrEmpty(group)) + { + sb.Append("").Append(group).Append(""); + } + sb.Append(""); + sb.Append(""); + return sb.ToString(); + } + private static VBGUID ToVbGuid(Guid g) { byte[] bytes = g.ToByteArray(); @@ -390,6 +518,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer { Timer? timerToDispose; wwAlarmConsumerClass? clientToDispose; + wwAlarmConsumerClass? ackClientToDispose; lock (syncRoot) { if (disposed) return; @@ -398,16 +527,22 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer pollTimer = null; clientToDispose = client; client = null; + ackClientToDispose = ackClient; + ackClient = null; } timerToDispose?.Dispose(); - if (clientToDispose is not null) + ReleaseConsumerCom(clientToDispose); + ReleaseConsumerCom(ackClientToDispose); + } + + private static void ReleaseConsumerCom(wwAlarmConsumerClass? consumer) + { + if (consumer is null) return; + try { consumer.DeregisterConsumer(); } catch { /* swallow */ } + try { consumer.UninitializeConsumer(); } catch { /* swallow */ } + if (Marshal.IsComObject(consumer)) { - try { clientToDispose.DeregisterConsumer(); } catch { /* swallow */ } - try { clientToDispose.UninitializeConsumer(); } catch { /* swallow */ } - if (Marshal.IsComObject(clientToDispose)) - { - try { Marshal.FinalReleaseComObject(clientToDispose); } catch { /* swallow */ } - } + try { Marshal.FinalReleaseComObject(consumer); } catch { /* swallow */ } } } } -- 2.52.0