diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md new file mode 100644 index 0000000..da565bb --- /dev/null +++ b/docs/AlarmClientDiscovery.md @@ -0,0 +1,792 @@ +# 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. + +## Live runtime probe — 2026-05-01 + +`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. + +**`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. + +## 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. + +## 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. + +## 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. + +## 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. + +## 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. + +**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 +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 +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 +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.** 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 + 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 +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. + +## 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/lib/Interop.WNWRAPCONSUMERLib.dll b/lib/Interop.WNWRAPCONSUMERLib.dll new file mode 100644 index 0000000..2fccd23 Binary files /dev/null and b/lib/Interop.WNWRAPCONSUMERLib.dll differ diff --git a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs index 05c543c..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", - "MS5NeENvbW1hbmQizw8KCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", + "MS5NeENvbW1hbmQi7xIKCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh", "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRLaW5kEjgKCHJlZ2lzdGVyGAog", "ASgLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3RlckNvbW1hbmRIABI8", "Cgp1bnJlZ2lzdGVyGAsgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnJl", @@ -83,312 +83,344 @@ 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", + "bW1hbmRIABJHChBzdWJzY3JpYmVfYWxhcm1zGCIgASgLMisubXhhY2Nlc3Nf", + "Z2F0ZXdheS52MS5TdWJzY3JpYmVBbGFybXNDb21tYW5kSAASSwoSdW5zdWJz", + "Y3JpYmVfYWxhcm1zGCMgASgLMi0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnN1", + "YnNjcmliZUFsYXJtc0NvbW1hbmRIABJRChlhY2tub3dsZWRnZV9hbGFybV9j", + "b21tYW5kGCQgASgLMiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRn", + "ZUFsYXJtQ29tbWFuZEgAElQKG3F1ZXJ5X2FjdGl2ZV9hbGFybXNfY29tbWFu", + "ZBglIAEoCzItLm14YWNjZXNzX2dhdGV3YXkudjEuUXVlcnlBY3RpdmVBbGFy", + "bXNDb21tYW5kSAASXwohYWNrbm93bGVkZ2VfYWxhcm1fYnlfbmFtZV9jb21t", + "YW5kGCYgASgLMjIubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFs", + "YXJtQnlOYW1lQ29tbWFuZEgAEjAKBHBpbmcYZCABKAsyIC5teGFjY2Vzc19n", + "YXRld2F5LnYxLlBpbmdDb21tYW5kSAASSAoRZ2V0X3Nlc3Npb25fc3RhdGUY", + "ZSABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkdldFNlc3Npb25TdGF0ZUNv", + "bW1hbmRIABJECg9nZXRfd29ya2VyX2luZm8YZiABKAsyKS5teGFjY2Vzc19n", + "YXRld2F5LnYxLkdldFdvcmtlckluZm9Db21tYW5kSAASPwoMZHJhaW5fZXZl", + "bnRzGGcgASgLMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5EcmFpbkV2ZW50c0Nv", + "bW1hbmRIABJFCg9zaHV0ZG93bl93b3JrZXIYaCABKAsyKi5teGFjY2Vzc19n", + "YXRld2F5LnYxLlNodXRkb3duV29ya2VyQ29tbWFuZEgAQgkKB3BheWxvYWQi", + "JgoPUmVnaXN0ZXJDb21tYW5kEhMKC2NsaWVudF9uYW1lGAEgASgJIioKEVVu", + "cmVnaXN0ZXJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUiQAoOQWRk", + "SXRlbUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIXCg9pdGVtX2Rl", + "ZmluaXRpb24YAiABKAkiVwoPQWRkSXRlbTJDb21tYW5kEhUKDXNlcnZlcl9o", + "YW5kbGUYASABKAUSFwoPaXRlbV9kZWZpbml0aW9uGAIgASgJEhQKDGl0ZW1f", + "Y29udGV4dBgDIAEoCSI/ChFSZW1vdmVJdGVtQ29tbWFuZBIVCg1zZXJ2ZXJf", + "aGFuZGxlGAEgASgFEhMKC2l0ZW1faGFuZGxlGAIgASgFIjsKDUFkdmlzZUNv", + "bW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgC", + "IAEoBSI9Cg9VbkFkdmlzZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", + "BRITCgtpdGVtX2hhbmRsZRgCIAEoBSJGChhBZHZpc2VTdXBlcnZpc29yeUNv", + "bW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgC", + "IAEoBSJeChZBZGRCdWZmZXJlZEl0ZW1Db21tYW5kEhUKDXNlcnZlcl9oYW5k", + "bGUYASABKAUSFwoPaXRlbV9kZWZpbml0aW9uGAIgASgJEhQKDGl0ZW1fY29u", + "dGV4dBgDIAEoCSJfCiBTZXRCdWZmZXJlZFVwZGF0ZUludGVydmFsQ29tbWFu", + "ZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEiQKHHVwZGF0ZV9pbnRlcnZhbF9t", + "aWxsaXNlY29uZHMYAiABKAUiPAoOU3VzcGVuZENvbW1hbmQSFQoNc2VydmVy", + "X2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSI9Cg9BY3RpdmF0", + "ZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRs", + "ZRgCIAEoBSJ4CgxXcml0ZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo", + "BRITCgtpdGVtX2hhbmRsZRgCIAEoBRIrCgV2YWx1ZRgDIAEoCzIcLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgd1c2VyX2lkGAQgASgFIrABCg1X", + "cml0ZTJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9o", "YW5kbGUYAiABKAUSKwoFdmFsdWUYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5", - "LnYxLk14VmFsdWUSDwoHdXNlcl9pZBgEIAEoBSKwAQoNV3JpdGUyQ29tbWFu", - "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==")); + "LnYxLk14VmFsdWUSNQoPdGltZXN0YW1wX3ZhbHVlGAQgASgLMhwubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5NeFZhbHVlEg8KB3VzZXJfaWQYBSABKAUioQEKE1dy", + "aXRlU2VjdXJlZENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtp", + "dGVtX2hhbmRsZRgCIAEoBRIXCg9jdXJyZW50X3VzZXJfaWQYAyABKAUSGAoQ", + "dmVyaWZpZXJfdXNlcl9pZBgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIcLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuTXhWYWx1ZSLZAQoUV3JpdGVTZWN1cmVkMkNvbW1h", + "bmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEo", + "BRIXCg9jdXJyZW50X3VzZXJfaWQYAyABKAUSGAoQdmVyaWZpZXJfdXNlcl9p", + "ZBgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEu", + "TXhWYWx1ZRI1Cg90aW1lc3RhbXBfdmFsdWUYBiABKAsyHC5teGFjY2Vzc19n", + "YXRld2F5LnYxLk14VmFsdWUiYwoXQXV0aGVudGljYXRlVXNlckNvbW1hbmQS", + "FQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgt2ZXJpZnlfdXNlchgCIAEoCRIc", + "ChR2ZXJpZnlfdXNlcl9wYXNzd29yZBgDIAEoCSJHChhBcmNoZXN0ckFVc2Vy", + "VG9JZENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgx1c2VyX2lk", + "X2d1aWQYAiABKAkiQgoSQWRkSXRlbUJ1bGtDb21tYW5kEhUKDXNlcnZlcl9o", + "YW5kbGUYASABKAUSFQoNdGFnX2FkZHJlc3NlcxgCIAMoCSJEChVBZHZpc2VJ", + "dGVtQnVsa0NvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIUCgxpdGVt", + "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[] { @@ -398,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", "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), @@ -422,13 +454,18 @@ 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.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), 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 +479,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 +535,11 @@ 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_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, @@ -2532,6 +2576,21 @@ 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.AcknowledgeAlarmByNameCommand: + AcknowledgeAlarmByNameCommand = other.AcknowledgeAlarmByNameCommand.Clone(); + break; case PayloadOneofCase.Ping: Ping = other.Ping.Clone(); break; @@ -2858,6 +2917,66 @@ 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 "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] @@ -2946,6 +3065,11 @@ namespace MxGateway.Contracts.Proto { UnAdviseItemBulk = 31, SubscribeBulk = 32, UnsubscribeBulk = 33, + SubscribeAlarms = 34, + UnsubscribeAlarms = 35, + AcknowledgeAlarmCommand = 36, + QueryActiveAlarmsCommand = 37, + AcknowledgeAlarmByNameCommand = 38, Ping = 100, GetSessionState = 101, GetWorkerInfo = 102, @@ -3006,6 +3130,11 @@ 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(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; @@ -3044,6 +3173,11 @@ 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.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(); @@ -3168,6 +3302,26 @@ 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.AcknowledgeAlarmByNameCommand) { + output.WriteRawTag(178, 2); + output.WriteMessage(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3298,6 +3452,26 @@ 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.AcknowledgeAlarmByNameCommand) { + output.WriteRawTag(178, 2); + output.WriteMessage(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { output.WriteRawTag(162, 6); output.WriteMessage(Ping); @@ -3403,6 +3577,21 @@ 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.AcknowledgeAlarmByNameCommand) { + size += 2 + pb::CodedOutputStream.ComputeMessageSize(AcknowledgeAlarmByNameCommand); + } if (payloadCase_ == PayloadOneofCase.Ping) { size += 2 + pb::CodedOutputStream.ComputeMessageSize(Ping); } @@ -3578,6 +3767,36 @@ 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.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(); @@ -3849,6 +4068,51 @@ 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 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) { @@ -4133,6 +4397,51 @@ 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 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) { @@ -10088,6 +10397,1447 @@ 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 + + } + + /// + /// 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 @@ -10103,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[30]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[35]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10329,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[31]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[36]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10527,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[32]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[37]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10688,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[33]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[38]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -10849,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[34]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[39]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11047,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[35]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[40]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11255,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[36]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[41]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -11327,6 +13077,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 +13386,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 +13464,8 @@ namespace MxGateway.Contracts.Proto { UnAdviseItemBulk = 31, SubscribeBulk = 32, UnsubscribeBulk = 33, + AcknowledgeAlarm = 34, + QueryActiveAlarms = 35, SessionState = 100, WorkerInfo = 101, DrainEvents = 102, @@ -11739,6 +13521,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 +13556,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 +13665,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 +13780,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 +13874,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 +14014,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 +14229,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 +14457,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 +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[37]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[42]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -12866,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[38]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[43]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13064,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[39]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[44]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13262,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[40]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[45]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13460,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[41]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[46]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13667,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[42]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[47]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -13874,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[43]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[48]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14072,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[44]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[49]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14270,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[45]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[50]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14616,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[46]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[51]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -14803,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[47]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[52]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15001,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[48]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[53]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15310,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[49]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[54]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -15482,6 +17338,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[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 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[56]; } + } + + [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 +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[50]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[57]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16473,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[51]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[58]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16634,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[52]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[59]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16795,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[53]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[60]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -16956,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[54]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[61]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -17289,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[55]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[62]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18023,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[56]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[63]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -18713,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[57]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[64]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19070,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[58]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[65]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19493,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[59]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[66]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -19769,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[60]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[67]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -20189,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[61]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[68]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21046,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[62]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[69]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -21834,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[63]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[70]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22023,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[64]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[71]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22212,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[65]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[72]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22401,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[66]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[73]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22590,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[67]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[74]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22779,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[68]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[75]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -22966,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[69]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[76]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23153,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[70]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[77]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -23340,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[71]; } + 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 b7ecf29..fa71b6b 100644 --- a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -88,6 +88,11 @@ 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; + AcknowledgeAlarmByNameCommand acknowledge_alarm_by_name_command = 38; PingCommand ping = 100; GetSessionStateCommand get_session_state = 101; GetWorkerInfoCommand get_worker_info = 102; @@ -122,6 +127,11 @@ 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_ACKNOWLEDGE_ALARM_BY_NAME = 29; MX_COMMAND_KIND_PING = 100; MX_COMMAND_KIND_GET_SESSION_STATE = 101; MX_COMMAND_KIND_GET_WORKER_INFO = 102; @@ -263,6 +273,63 @@ 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; +} + +// 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; @@ -314,6 +381,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 +448,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.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.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..81b1561 --- /dev/null +++ b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs @@ -0,0 +1,231 @@ +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; + } + + /// + /// 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, + 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.", + }; + } + + 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 + { + return new AcknowledgeAlarmReply + { + SessionId = request.SessionId, + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.InvalidRequest, + Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.", + }, + DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.", + }; + } + + 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/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; + } +} diff --git a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs new file mode 100644 index 0000000..dce9148 --- /dev/null +++ b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs @@ -0,0 +1,374 @@ +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_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); + } + + [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() + { + 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; + } +} diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs new file mode 100644 index 0000000..ad716de --- /dev/null +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -0,0 +1,779 @@ +using System; +using System.Collections.Concurrent; +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; + +/// +/// 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. + // 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 = + { + // 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); + 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( + 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 (AVEVA installed) to capture alarm-path behavior")] + 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(); + + // 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 + // 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", + szApplicationName: "AlarmProbe.Tests", + szVersion: "1.0", + bRetainHiddenAlarms: false); + Log($"RegisterConsumer -> {register}"); + + 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. + // 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-multi"); + + // 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 + // 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)) + { + LogIfInteresting(msg); + 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); + LogProviders(client, $"poll #{pollCount}"); + PollAllChannels(client, pollCount); + nextPoll = DateTime.UtcNow + PollInterval; + } + Thread.Sleep(10); + } + + 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}"); } + } + finally + { + try { client?.Dispose(); } catch { /* swallow */ } + if (probeWindow != IntPtr.Zero) + { + DestroyWindow(probeWindow); + probeWindow = IntPtr.Zero; + } + if (registeredClass != null) + { + UnregisterClassW(registeredClass, GetModuleHandle(null!)); + } + } + } + + private string lastStatsSummary = string.Empty; + 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 + /// 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 — 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; ok={getHighPriOk}, throw={getHighPriThrow})"); + lastHighPriSummary = summary; + } + } + catch (Exception ex) + { + string es = $"{ex.GetType().Name}"; + getHighPriThrow++; + if (es != lastHighPriSummary) + { + Log($"GetHighPriAlarm #{seq}: threw {es} (changed; ok={getHighPriOk}, throw={getHighPriThrow})"); + 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) + { + 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 + /// 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 + { + 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; + + // 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) + { + 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) + { + 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 + // 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/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.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs new file mode 100644 index 0000000..cd54467 --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs @@ -0,0 +1,452 @@ +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 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() + { + 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 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; + 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..decd4b8 --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs @@ -0,0 +1,244 @@ +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 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() + { + Disposed = true; + } + } +} diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs new file mode 100644 index 0000000..6b3e03d --- /dev/null +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs @@ -0,0 +1,326 @@ +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 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() + { + 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 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; + } + + public void Dispose() + { + Disposed = true; + } + } +} diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmRecordTransitionMapperTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmRecordTransitionMapperTests.cs index 75f235f..8e108e4 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/AlarmRecordTransitionMapperTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmRecordTransitionMapperTests.cs @@ -1,19 +1,19 @@ +using System; +using MxGateway.Contracts.Proto; using MxGateway.Worker.MxAccess; namespace MxGateway.Worker.Tests.MxAccess; /// -/// 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 47796e3..55b9608 100644 --- a/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj +++ b/src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj @@ -25,4 +25,28 @@ + + + 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 + false + + + C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\IAlarmMgrDataProvider.dll + 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 a78849e..0000000 --- a/src/MxGateway.Worker/MxAccess/AlarmClientConsumer.cs +++ /dev/null @@ -1,172 +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 . -/// -/// -/// -/// 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. -/// -/// -/// 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. -/// -/// -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: 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. - 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/AlarmCommandHandler.cs b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs new file mode 100644 index 0000000..7867de8 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/AlarmCommandHandler.cs @@ -0,0 +1,229 @@ +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 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) + { + 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); + + /// + /// 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. + /// + IReadOnlyList QueryActive(string? alarmFilterPrefix); +} diff --git a/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs new file mode 100644 index 0000000..cc4e617 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/AlarmDispatcher.cs @@ -0,0 +1,217 @@ +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); + } + + /// + /// 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 + /// 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 */ } + } +} 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..c2973bd 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,27 @@ 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. + /// 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. /// - System.Collections.Generic.IReadOnlyList SnapshotActiveAlarms(); + 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) + /// ConditionRefresh path — operator clients call this after reconnect + /// to seed local Part 9 state. + /// + IReadOnlyList SnapshotActiveAlarms(); } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs index 5b1f430..59bf61e 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs @@ -4,49 +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 (pinned 2026-04-30): 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 -/// 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. -/// -/// -/// Discovered API surface (see -/// AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface in -/// MxGateway.Worker.Tests — Skip-gated reflection probe): -/// -/// -/// 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. -/// -/// -/// Wiring plan (subsequent PRs): -/// -/// -/// 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. -/// -/// -/// 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. +/// 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/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index 0ddb00a..da49ffa 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,11 @@ 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.AcknowledgeAlarmByName => ExecuteAcknowledgeAlarmByName(command), + MxCommandKind.QueryActiveAlarms => ExecuteQueryActiveAlarms(command), _ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."), }; } @@ -280,6 +301,201 @@ 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 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) + { + 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 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..06dc39a --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -0,0 +1,548 @@ +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 wwAlarmConsumerClass? ackClient; + private string subscriptionExpression = string.Empty; + private Timer? pollTimer; + private bool subscribed; + private bool disposed; + + public WnWrapAlarmConsumer() + : this(new wwAlarmConsumerClass(), DefaultPollIntervalMilliseconds, DefaultMaxAlarmsPerFetch) + { + } + + /// + /// 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 + ? DefaultPollIntervalMilliseconds + : pollIntervalMilliseconds; + 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)); + + // 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( + $"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.IwwAlarmConsumer_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.IwwAlarmConsumer_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}."); + } + + // 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; + if (pollIntervalMs > 0) + { + 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 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)); + + // 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); + } + + /// + 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; + } + } + + /// + /// 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) + { + 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); + } + + /// + /// 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(); + // 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; + wwAlarmConsumerClass? ackClientToDispose; + lock (syncRoot) + { + if (disposed) return; + disposed = true; + timerToDispose = pollTimer; + pollTimer = null; + clientToDispose = client; + client = null; + ackClientToDispose = ackClient; + ackClient = null; + } + timerToDispose?.Dispose(); + 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 { Marshal.FinalReleaseComObject(consumer); } 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