diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
index f95beca..5bc4e4b 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
@@ -72,10 +72,10 @@ public sealed class FailoverAlarmConsumerTests
}
///
- public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 11;
+ public AlarmAckTarget ResolveAckTarget(Guid g) => AlarmAckTarget.For("primary.guid.AckMsg");
///
- public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 11;
+ public AlarmAckTarget ResolveAckTargetByName(string n, string p, string gr) => AlarmAckTarget.For("primary.name.AckMsg");
///
public IReadOnlyList SnapshotActiveAlarms(out bool truncated)
@@ -122,10 +122,10 @@ public sealed class FailoverAlarmConsumerTests
public void PollOnce() { }
///
- public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 22;
+ public AlarmAckTarget ResolveAckTarget(Guid g) => AlarmAckTarget.For("standby.guid.AckMsg");
///
- public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 22;
+ public AlarmAckTarget ResolveAckTargetByName(string n, string p, string gr) => AlarmAckTarget.For("standby.name.AckMsg");
///
public IReadOnlyList SnapshotActiveAlarms(out bool truncated)
@@ -305,9 +305,9 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(subscribeCountAfterSetup, primary.SubscribeCount);
}
- /// Proves that acknowledgment calls delegate to whichever child (primary or standby) is currently active.
+ /// Proves that ack-target resolution delegates to whichever child (primary or standby) is currently active.
[Fact]
- public void Acknowledge_DelegatesToActiveChild()
+ public void ResolveAckTarget_DelegatesToActiveChild()
{
FlakyPrimary primary = new FlakyPrimary { ThrowOnPoll = false };
StubStandby standby = new StubStandby();
@@ -316,18 +316,18 @@ public sealed class FailoverAlarmConsumerTests
sut.Subscribe(@"\\HOST\Galaxy!Area");
- // Active = Primary → primary's sentinel value (11).
- Assert.Equal(11, sut.AcknowledgeByGuid(Guid.NewGuid(), "c", "n", "node", "dom", "full"));
- Assert.Equal(11, sut.AcknowledgeByName("a", "p", "g", "c", "n", "node", "dom", "full"));
+ // Active = Primary → primary's sentinel targets.
+ Assert.Equal("primary.guid.AckMsg", sut.ResolveAckTarget(Guid.NewGuid()).ItemAddress);
+ Assert.Equal("primary.name.AckMsg", sut.ResolveAckTargetByName("a", "p", "g").ItemAddress);
// Force a failover by failing the primary past threshold.
primary.ThrowOnPoll = true;
sut.PollOnce(); // threshold=1 → switch to Standby
Assert.Equal(AlarmProviderMode.Subtag, sut.Mode);
- // Active = Standby → standby's sentinel value (22).
- Assert.Equal(22, sut.AcknowledgeByGuid(Guid.NewGuid(), "c", "n", "node", "dom", "full"));
- Assert.Equal(22, sut.AcknowledgeByName("a", "p", "g", "c", "n", "node", "dom", "full"));
+ // Active = Standby → standby's sentinel targets.
+ Assert.Equal("standby.guid.AckMsg", sut.ResolveAckTarget(Guid.NewGuid()).ItemAddress);
+ Assert.Equal("standby.name.AckMsg", sut.ResolveAckTargetByName("a", "p", "g").ItemAddress);
}
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
index 2b5b7fb..ab74bd9 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
@@ -94,76 +94,28 @@ public sealed class LmxSubtagAlarmSourceTests
}
///
- /// Verifies adds the item when
- /// it was not previously advised and writes with user id 0.
+ /// Verifies UnAdvises and
+ /// removes every advised handle and unregisters the server exactly once.
///
[Fact]
- public void Write_AddsItemWhenUnknownAndWrites()
- {
- var server = new RecordingMxAccessServer();
- using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
-
- source.Write("Tank1.Alarm.AckComment", "acknowledged");
-
- Assert.Contains("Tank1.Alarm.AckComment", server.AddedItems);
- Assert.Single(server.Writes);
- RecordingMxAccessServer.WriteRecord write = server.Writes[0];
- Assert.Equal(FakeServerHandle, write.ServerHandle);
- Assert.Equal("acknowledged", write.Value);
- Assert.Equal(0, write.UserId);
- }
-
- ///
- /// Verifies reuses an existing
- /// item handle (no duplicate AddItem) when the address was already
- /// advised.
- ///
- [Fact]
- public void Write_ReusesHandleForAdvisedAddress()
- {
- var server = new RecordingMxAccessServer();
- using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
-
- source.Advise(new[] { "Tank1.Alarm.Subtag" });
- int adviseAddCount = server.AddedItems.Count;
-
- source.Write("Tank1.Alarm.Subtag", true);
-
- // No second AddItem for the already-advised address.
- Assert.Equal(adviseAddCount, server.AddedItems.Count);
- Assert.Single(server.Writes);
- }
-
- ///
- /// Verifies UnAdvises only the
- /// handles that were actually advised — a write-only item (added by
- /// but never advised) is removed
- /// but not unadvised — and unregisters the server exactly once.
- ///
- [Fact]
- public void Dispose_UnAdvisesOnlyAdvisedHandles_RemovesAll_AndUnregistersOnce()
+ public void Dispose_UnAdvisesAndRemovesAdvisedHandles_AndUnregistersOnce()
{
var server = new RecordingMxAccessServer();
var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
source.Advise(new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" });
- // A write-only subtag: added by Write, never advised.
- source.Write("Tank1.Alarm.AckComment", "acknowledged");
int advised1 = server.LastItemHandleFor("Tank1.Alarm.Subtag");
int advised2 = server.LastItemHandleFor("Tank2.Alarm.Subtag");
- int writeOnly = server.LastItemHandleFor("Tank1.Alarm.AckComment");
source.Dispose();
- // Only the two advised handles are unadvised — never the write-only one.
Assert.Equal(new[] { advised1, advised2 }, server.UnAdvisedItemHandles);
- Assert.DoesNotContain(writeOnly, server.UnAdvisedItemHandles);
- // Every added item (advised + write-only) is removed.
Assert.Equal(
- new[] { advised1, advised2, writeOnly }.OrderBy(h => h),
+ new[] { advised1, advised2 }.OrderBy(h => h),
server.RemovedItemHandles.OrderBy(h => h));
Assert.Equal(1, server.UnregisterCount);
+ Assert.Empty(server.Writes);
}
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
index 093a3b9..76b1be2 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
///
/// Unit tests for : prove that the subtag
/// fallback advises the observable subtags, synthesizes degraded
-/// transitions with stable synthetic GUIDs, routes acknowledgments to the
+/// transitions with stable synthetic GUIDs, resolves acknowledgments to the
/// ack-comment subtag, and stamps snapshots. A
/// stands in for the live MXAccess subtag source so this needs no AVEVA
/// install.
@@ -40,9 +40,13 @@ public sealed class SubtagAlarmConsumerTests
return new SubtagAlarmConsumer(source, new[] { BuildTarget() });
}
- /// Verifies Subscribe advises the active, acked, priority, and ack-comment subtags (the ack-comment is advised so it is an active, writable MXAccess item).
+ ///
+ /// Verifies Subscribe advises the observable active, acked and priority
+ /// subtags but not the ack-comment subtag: the ack is written by
+ /// , which adds and advises it on its own server.
+ ///
[Fact]
- public void Subscribe_AdvisesAllSubtagsIncludingAckComment()
+ public void Subscribe_AdvisesObservableSubtagsButNotAckComment()
{
FakeSource source = new FakeSource();
using SubtagAlarmConsumer consumer = BuildConsumer(source);
@@ -52,7 +56,7 @@ public sealed class SubtagAlarmConsumerTests
Assert.Contains(ActiveSubtag, source.Advised);
Assert.Contains(AckedSubtag, source.Advised);
Assert.Contains(PrioritySubtag, source.Advised);
- Assert.Contains(AckCommentSubtag, source.Advised);
+ Assert.DoesNotContain(AckCommentSubtag, source.Advised);
}
/// Verifies an active=true value change raises a degraded, GUID-stamped UNACK_ALM transition.
@@ -74,50 +78,49 @@ public sealed class SubtagAlarmConsumerTests
Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
}
- /// Verifies AcknowledgeByName writes the comment to the ack-comment subtag and returns success.
+ /// Verifies ResolveAckTargetByName resolves to the target's ack-comment subtag.
[Fact]
- public void AcknowledgeByName_WritesCommentToAckCommentSubtag()
+ public void ResolveAckTargetByName_ReturnsAckCommentSubtag()
{
FakeSource source = new FakeSource();
using SubtagAlarmConsumer consumer = BuildConsumer(source);
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
- int rc = consumer.AcknowledgeByName(
- alarmName: "Tank01.Level.HiHi",
- providerName: "Galaxy",
- groupName: "TestArea",
- ackComment: "operator ack",
- ackOperatorName: "alice",
- ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP",
- ackOperatorFullName: "Alice Smith");
+ AlarmAckTarget target = consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "TestArea");
- Assert.Equal(0, rc);
- Assert.NotNull(source.LastWrite);
- Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
- Assert.Equal("operator ack", source.LastWrite!.Value.Value);
+ Assert.True(target.Found);
+ Assert.Equal(AckCommentSubtag, target.ItemAddress);
}
- /// Verifies AcknowledgeByName returns non-zero when no target matches the supplied name.
+ /// Verifies ResolveAckTargetByName reports AlarmNotFound when no target matches the supplied name.
[Fact]
- public void AcknowledgeByName_UnknownAlarm_ReturnsNonZero()
+ public void ResolveAckTargetByName_UnknownAlarm_IsNotFound()
{
FakeSource source = new FakeSource();
using SubtagAlarmConsumer consumer = BuildConsumer(source);
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
- int rc = consumer.AcknowledgeByName(
- alarmName: "DoesNotExist.NoSuchAlarm",
- providerName: "Galaxy",
- groupName: "TestArea",
- ackComment: "operator ack",
- ackOperatorName: "alice",
- ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP",
- ackOperatorFullName: "Alice Smith");
+ AlarmAckTarget target = consumer.ResolveAckTargetByName("DoesNotExist.NoSuchAlarm", "Galaxy", "TestArea");
- Assert.NotEqual(0, rc);
- Assert.Null(source.LastWrite);
+ Assert.False(target.Found);
+ Assert.Equal(AlarmAckOutcome.AlarmNotFound, target.Failure);
+ }
+
+ /// Verifies a target with no ack-comment subtag resolves as NotAckable, by name and by GUID.
+ [Fact]
+ public void ResolveAckTarget_EmptyAckCommentSubtag_IsNotAckable()
+ {
+ AlarmSubtagTarget noAck = BuildTarget();
+ noAck.AckCommentSubtag = string.Empty;
+ FakeSource source = new FakeSource();
+ using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, new[] { noAck });
+
+ Assert.Equal(
+ AlarmAckOutcome.NotAckable,
+ consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "TestArea").Failure);
+ Assert.Equal(
+ AlarmAckOutcome.NotAckable,
+ consumer.ResolveAckTarget(SyntheticAlarmGuid.ForReference(Reference)).Failure);
}
/// Verifies a snapshot of an active alarm stamps Degraded and a non-empty synthetic GUID.
@@ -160,11 +163,11 @@ public sealed class SubtagAlarmConsumerTests
///
/// Verifies that when two alarm targets share a prefix (e.g. Level.Hi vs Level.HiHi),
- /// AcknowledgeByName routes each ack to its own ack-comment subtag and never
+ /// ResolveAckTargetByName resolves each to its own ack-comment subtag and never
/// conflates the shorter name with the longer one.
///
[Fact]
- public void AcknowledgeByName_PrefixNameDoesNotFalseMatch()
+ public void ResolveAckTargetByName_PrefixNameDoesNotFalseMatch()
{
const string ReferenceHi = "Galaxy!Area.Tank01.Level.Hi";
const string ReferenceHiHi = "Galaxy!Area.Tank01.Level.HiHi";
@@ -195,42 +198,19 @@ public sealed class SubtagAlarmConsumerTests
source, new[] { targetHi, targetHiHi });
consumer.Subscribe(@"\\HOST\Galaxy!Area");
- // Ack the shorter name — must write to the shorter target's subtag only.
- int rcHi = consumer.AcknowledgeByName(
- alarmName: "Tank01.Level.Hi",
- providerName: "Galaxy",
- groupName: "Area",
- ackComment: "ack hi",
- ackOperatorName: "op", ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");
-
- Assert.Equal(0, rcHi);
- Assert.NotNull(source.LastWrite);
- Assert.Equal(AckCommentHi, source.LastWrite!.Value.Address);
- Assert.Equal("ack hi", source.LastWrite.Value.Value);
-
- // Ack the longer name — must write to the longer target's subtag.
- int rcHiHi = consumer.AcknowledgeByName(
- alarmName: "Tank01.Level.HiHi",
- providerName: "Galaxy",
- groupName: "Area",
- ackComment: "ack hihi",
- ackOperatorName: "op", ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");
-
- Assert.Equal(0, rcHiHi);
- Assert.NotNull(source.LastWrite);
- Assert.Equal(AckCommentHiHi, source.LastWrite!.Value.Address);
- Assert.Equal("ack hihi", source.LastWrite.Value.Value);
+ Assert.Equal(AckCommentHi, consumer.ResolveAckTargetByName("Tank01.Level.Hi", "Galaxy", "Area").ItemAddress);
+ Assert.Equal(AckCommentHiHi, consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "Area").ItemAddress);
+ Assert.Equal(
+ AlarmAckOutcome.AlarmNotFound,
+ consumer.ResolveAckTargetByName("Tank01.Level.H", "Galaxy", "Area").Failure);
}
///
- /// Verifies AcknowledgeByGuid resolves the synthetic GUID (computed from
- /// the alarm's full reference) to the correct target and writes the comment
- /// to that target's ack-comment subtag.
+ /// Verifies ResolveAckTarget resolves the synthetic GUID (computed from
+ /// the alarm's full reference) to that target's ack-comment subtag.
///
[Fact]
- public void AcknowledgeByGuid_WritesCommentToAckCommentSubtag()
+ public void ResolveAckTarget_SyntheticGuid_ReturnsAckCommentSubtag()
{
FakeSource source = new FakeSource();
using SubtagAlarmConsumer consumer = BuildConsumer(source);
@@ -246,41 +226,21 @@ public sealed class SubtagAlarmConsumerTests
Guid syntheticGuid = emitted!.Record.AlarmGuid;
Assert.NotEqual(Guid.Empty, syntheticGuid);
- int rc = consumer.AcknowledgeByGuid(
- alarmGuid: syntheticGuid,
- ackComment: "guid ack",
- ackOperatorName: "op",
- ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP",
- ackOperatorFullName: "Operator");
+ AlarmAckTarget target = consumer.ResolveAckTarget(syntheticGuid);
- Assert.Equal(0, rc);
- Assert.NotNull(source.LastWrite);
- Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
- Assert.Equal("guid ack", source.LastWrite.Value.Value);
+ Assert.True(target.Found);
+ Assert.Equal(AckCommentSubtag, target.ItemAddress);
}
- ///
- /// Verifies AcknowledgeByGuid returns non-zero and performs no write when
- /// the supplied GUID is not known to the consumer.
- ///
+ /// Verifies ResolveAckTarget reports AlarmNotFound for a GUID unknown to the consumer.
[Fact]
- public void AcknowledgeByGuid_UnknownGuid_ReturnsNonZero()
+ public void ResolveAckTarget_UnknownGuid_IsNotFound()
{
FakeSource source = new FakeSource();
using SubtagAlarmConsumer consumer = BuildConsumer(source);
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
- int rc = consumer.AcknowledgeByGuid(
- alarmGuid: Guid.NewGuid(),
- ackComment: "should not write",
- ackOperatorName: "op",
- ackOperatorNode: "WS01",
- ackOperatorDomain: "CORP",
- ackOperatorFullName: "Operator");
-
- Assert.NotEqual(0, rc);
- Assert.Null(source.LastWrite);
+ Assert.Equal(AlarmAckOutcome.AlarmNotFound, consumer.ResolveAckTarget(Guid.NewGuid()).Failure);
}
private sealed class FakeSource : ISubtagAlarmSource
@@ -291,21 +251,12 @@ public sealed class SubtagAlarmConsumerTests
/// Gets the subtag addresses passed to .
public List Advised { get; } = new List();
- /// Gets the most recent (address, value) pair passed to .
- public (string Address, object? Value)? LastWrite { get; private set; }
-
///
public void Advise(IReadOnlyCollection itemAddresses)
{
Advised.AddRange(itemAddresses);
}
- ///
- public void Write(string itemAddress, object? value)
- {
- LastWrite = (itemAddress, value);
- }
-
/// Raises a for the given subtag.
/// The subtag address whose value changed.
/// The new value.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
index 3941a21..1d51f0e 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
@@ -824,4 +824,58 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Equal("mgack-authwrite 180714", record.AckComment);
Assert.Equal("Test alarm #1", record.AlarmComment);
}
+
+ /// A Galaxy-provider record resolves to its <tag>.AckMsg item; an unknown GUID is not found.
+ [Fact]
+ public void ResolveAckTarget_GalaxyRecord_ReturnsAckMsgItem()
+ {
+ Guid guid = Guid.NewGuid();
+ using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot(new MxAlarmSnapshotRecord
+ {
+ AlarmGuid = guid, ProviderName = "Galaxy", Group = "TestArea", TagName = "TestMachine_001.TestAlarm001", State = MxAlarmStateKind.UnackAlm,
+ });
+
+ AlarmAckTarget target = consumer.ResolveAckTarget(guid);
+
+ Assert.True(target.Found);
+ Assert.Equal("TestMachine_001.TestAlarm001.AckMsg", target.ItemAddress);
+ Assert.Equal(AlarmAckOutcome.AlarmNotFound, consumer.ResolveAckTarget(Guid.NewGuid()).Failure);
+ }
+
+ /// A non-Galaxy provider has no MXAccess ack attribute, so it is not ackable.
+ [Fact]
+ public void ResolveAckTarget_NonGalaxyProvider_IsNotAckable()
+ {
+ Guid guid = Guid.NewGuid();
+ using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot(new MxAlarmSnapshotRecord
+ {
+ AlarmGuid = guid, ProviderName = "InTouch", Group = "G", TagName = "Tag1", State = MxAlarmStateKind.UnackAlm,
+ });
+
+ Assert.Equal(AlarmAckOutcome.NotAckable, consumer.ResolveAckTarget(guid).Failure);
+ }
+
+ /// By-name resolution needs no snapshot: a Galaxy name maps straight to its AckMsg item.
+ [Fact]
+ public void ResolveAckTargetByName_Galaxy_ReturnsAckMsgItem()
+ {
+ using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot();
+
+ Assert.Equal("TestMachine_001.TestAlarm001.AckMsg",
+ consumer.ResolveAckTargetByName("TestMachine_001.TestAlarm001", "Galaxy", "TestArea").ItemAddress);
+ Assert.Equal(AlarmAckOutcome.NotAckable, consumer.ResolveAckTargetByName("Tag1", "InTouch", "G").Failure);
+ }
+
+ private static WnWrapAlarmConsumer CreateConsumerWithSnapshot(params MxAlarmSnapshotRecord[] records)
+ {
+ WnWrapAlarmConsumer consumer = new WnWrapAlarmConsumer(1024);
+ Dictionary next = new Dictionary();
+ foreach (MxAlarmSnapshotRecord record in records)
+ {
+ next[record.AlarmGuid] = record;
+ }
+
+ consumer.FoldFetch(next, truncated: false, out _);
+ return consumer;
+ }
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
index 9fb9254..57ab552 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
@@ -2,7 +2,7 @@
//
// Validates the subtag-fallback pipeline against a real Galaxy + MXAccess install:
// LmxSubtagAlarmSource (own LMXProxyServerClass) ->
-// SubtagAlarmConsumer (state machine + AcknowledgeByName write) ->
+// SubtagAlarmConsumer (state machine + ResolveAckTargetByName) + AlarmAckWriter (AckMsg write) ->
// synthesized MxAlarmTransitionEvent (Raise / Clear, Degraded=true, SyntheticGuid)
//
// FIELD NAMES CONFIRMED: InAlarm/Acked/AckMsg/Priority are the confirmed AVEVA
@@ -49,8 +49,9 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
/// synthesize records from Galaxy
/// alarm subtags using the confirmed AVEVA AlarmExtension field
/// names (InAlarm/Acked/AckMsg/Priority), and that
-/// writes the
-/// ack-comment subtag (AckMsg) successfully.
+/// resolves the
+/// ack-comment subtag (AckMsg) that then
+/// writes successfully.
///
/// Skip-gated; flip Skip=null on the dev rig with an alarm being
/// driven (flip script or a manual operator/IDE toggle of the alarm
@@ -119,7 +120,7 @@ public sealed class AlarmSubtagLiveSmokeTests
/// real LMXProxyServerClass, collects a Raise then a Clear
/// transition synthesized by , confirms
/// the Degraded flag and synthetic GUID are stamped, then
- /// AcknowledgeByName and verifies the ack-comment write returns 0.
+ /// acknowledges through the resolved ack-comment subtag and verifies the write succeeds.
///
[Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + an alarm being driven (flip script or manual operator/IDE toggle of the alarm attribute). Subtag fallback path. Field names confirmed (InAlarm/Acked/AckMsg/Priority); live-validate runtime path resolves without intermediate alarm-condition segment.")]
public void SubtagFallback_FullPipelineRoundTrip_SynthesizesRaiseAndAcknowledges()
@@ -257,17 +258,9 @@ public sealed class AlarmSubtagLiveSmokeTests
Log("InAlarm subtag delivered a value: " + inAlarmResolved);
// Decisive write: AckMsg. Returns 0 only if the address resolves and is writable.
- Log("AcknowledgeByName (writes AckMsg subtag) ...");
- int rc = consumer.AcknowledgeByName(
- alarmName: AlarmTagName,
- providerName: Provider,
- groupName: Group,
- ackComment: "subtag-resolution-probe",
- ackOperatorName: Environment.UserName,
- ackOperatorNode: Environment.MachineName,
- ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
- ackOperatorFullName: Environment.UserName);
- Log(string.Format("AcknowledgeByName(AckMsg write) -> rc={0}", rc));
+ Log("Acknowledge (writes AckMsg subtag via AlarmAckWriter) ...");
+ int rc = AckViaWriter(consumer, "subtag-resolution-probe").NativeStatus;
+ Log(string.Format("Ack (AckMsg write) -> rc={0}", rc));
DateTime settle = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (DateTime.UtcNow < settle)
@@ -392,21 +385,13 @@ public sealed class AlarmSubtagLiveSmokeTests
Assert.True(foundInSnapshot,
string.Format("Active alarm snapshot must contain '{0}' after a Raise.", AlarmFullReference));
- // 3. AcknowledgeByName — writes the ack-comment subtag.
+ // 3. Resolve the ack-comment subtag and write it as the operator.
// The dispatcher derives (alarmName, provider, group) via the same
// TryParseReference logic as AlarmsLiveSmokeTests.
- Log("Calling AcknowledgeByName ...");
- int rc = consumer.AcknowledgeByName(
- alarmName: AlarmTagName,
- providerName: Provider,
- groupName: Group,
- ackComment: "subtag-fallback-smoke ack",
- ackOperatorName: Environment.UserName,
- ackOperatorNode: Environment.MachineName,
- ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
- ackOperatorFullName: Environment.UserName);
+ Log("Acknowledging via AlarmAckWriter ...");
+ int rc = AckViaWriter(consumer, "subtag-fallback-smoke ack").NativeStatus;
- Log(string.Format("AcknowledgeByName -> rc={0}", rc));
+ Log(string.Format("Ack -> rc={0}", rc));
Assert.Equal(0, rc);
// 4. Wait for a Clear or Acknowledge transition to confirm the state
@@ -501,6 +486,29 @@ public sealed class AlarmSubtagLiveSmokeTests
/// Mirrors the pump pattern documented in
/// docs/MxAccessWorkerInstanceDesign.md.
///
+ ///
+ /// Resolves the probe alarm's ack item through the consumer and writes it
+ /// with as the operator named by
+ /// MXGATEWAY_PROBE_ACK_USER/MXGATEWAY_PROBE_ACK_PASSWORD
+ /// (default: current user, empty password).
+ ///
+ private static AlarmAckResult AckViaWriter(SubtagAlarmConsumer consumer, string comment)
+ {
+ AlarmAckTarget target = consumer.ResolveAckTargetByName(AlarmTagName, Provider, Group);
+ if (!target.Found)
+ {
+ return AlarmAckResult.FromTarget(target);
+ }
+
+ using AlarmAckWriter writer = new AlarmAckWriter(
+ new MxAccessComObjectFactory(), PumpMessages, TimeSpan.FromSeconds(5), AlarmAckWriter.DefaultClientName);
+ return writer.Acknowledge(target.ItemAddress, comment, new AlarmAckOperator
+ {
+ UserName = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_USER") ?? Environment.UserName,
+ Password = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_PASSWORD") ?? string.Empty,
+ });
+ }
+
private static void PumpMessages()
{
NativeMethods.MSG msg;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
index 0b94d55..1b8f765 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
@@ -41,7 +41,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// Threading. Like its children, this type is driven
/// entirely on the worker's STA: ,
/// , , and the
-/// AcknowledgeBy* calls are all invoked from the apartment that
+/// ResolveAckTarget* calls are all invoked from the apartment that
/// owns the underlying COM objects. It owns no locks of its own and no
/// internal timer; the worker drives on a timer.
///
@@ -259,34 +259,17 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
}
///
- public int AcknowledgeByGuid(
- Guid alarmGuid,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTarget(Guid alarmGuid)
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
- return ActiveChild.AcknowledgeByGuid(
- alarmGuid, ackComment, ackOperatorName, ackOperatorNode, ackOperatorDomain, ackOperatorFullName);
+ return ActiveChild.ResolveAckTarget(alarmGuid);
}
///
- public int AcknowledgeByName(
- string alarmName,
- string providerName,
- string groupName,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTargetByName(string alarmName, string providerName, string groupName)
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
- return ActiveChild.AcknowledgeByName(
- alarmName, providerName, groupName, ackComment,
- ackOperatorName, ackOperatorNode, ackOperatorDomain, ackOperatorFullName);
+ return ActiveChild.ResolveAckTargetByName(alarmName, providerName, groupName);
}
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
index d5b2b14..703d164 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
@@ -49,51 +49,19 @@ public interface IMxAccessAlarmConsumer : IDisposable
void Subscribe(string subscription);
///
- /// Acknowledges a single alarm with full operator-identity fidelity.
- /// 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.
+ /// Resolves the MXAccess item whose write acknowledges the alarm with the
+ /// given GUID (e.g. Obj.Alarm.AckMsg). Does not perform the ack.
///
- /// The alarm GUID.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
- /// The AVEVA-native status code.
- int AcknowledgeByGuid(
- Guid alarmGuid,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName);
+ /// The alarm GUID as reported by this consumer.
+ /// The resolved target or the reason none exists.
+ AlarmAckTarget ResolveAckTarget(Guid alarmGuid);
- ///
- /// 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.
- ///
- /// The alarm name.
- /// The provider name.
- /// The group name.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
- /// The AVEVA-native status code.
- int AcknowledgeByName(
- string alarmName,
- string providerName,
- string groupName,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName);
+ /// Resolves the ack item for an alarm identified by name, provider and group.
+ /// The tag/alarm name, e.g. TestMachine_001.TestAlarm001.
+ /// The provider (literal Galaxy for ArchestrA).
+ /// The area/group.
+ /// The resolved target or the reason none exists.
+ AlarmAckTarget ResolveAckTargetByName(string alarmName, string providerName, string groupName);
///
/// Returns the consumer's most recently parsed snapshot of currently
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/ISubtagAlarmSource.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/ISubtagAlarmSource.cs
index 75b4873..ef160c2 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/ISubtagAlarmSource.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/ISubtagAlarmSource.cs
@@ -16,9 +16,4 @@ public interface ISubtagAlarmSource : IDisposable
/// Begins advising the supplied MXAccess subtag item addresses.
/// The subtag references to advise.
void Advise(IReadOnlyCollection itemAddresses);
-
- /// Writes a value to the named MXAccess subtag (e.g. an ack request).
- /// The subtag reference to write.
- /// The value to write.
- void Write(string itemAddress, object? value);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
index 9fa0cd8..64a3fa8 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
@@ -9,8 +9,9 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// Production backed by its own MXAccess
/// LMXProxyServerClass COM object. It advises a set of alarm subtag
/// item addresses, normalizes each OnDataChange COM callback into a
-/// , and supports writing a value (e.g. an
-/// ack-comment) to a subtag.
+/// . It is observe-only: alarm
+/// acknowledgement goes through , which
+/// authenticates the operator on its own server.
///
///
///
@@ -25,7 +26,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// Threading. The LMXProxyServerClass CLSID is
/// apartment-threaded and delivers OnDataChange via the STA
/// message pump. Every method on this type — the lazy
-/// creation, , and — must be
+/// creation and — must be
/// invoked on the worker's STA that created the source, exactly as
/// and
/// require. The OnDataChange COM event also arrives on that STA.
@@ -47,9 +48,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
new Dictionary();
// Handles that were actually Advise()d, tracked separately from the added
- // set so Dispose only UnAdvises advised items. Write() can AddItem a
- // write-only subtag (e.g. an ack-comment that was never advised); calling
- // UnAdvise on such a handle would be an unbalanced teardown.
+ // set so Dispose only UnAdvises items this source advised.
private readonly HashSet advisedItemHandles = new HashSet();
private object? mxAccessComObject;
@@ -85,7 +84,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
/// with a pre-registered server handle,
/// bypassing the live COM factory. Exposed via
/// InternalsVisibleTo("ZB.MOM.WW.MxGateway.Worker.Tests") so tests can
- /// exercise / and
+ /// exercise and
/// without a live LMXProxyServerClass.
/// No COM event source is wired in this mode — tests drive
/// directly to simulate an
@@ -140,29 +139,6 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
}
}
- ///
- public void Write(string itemAddress, object? value)
- {
- // Writes with MXAccess user id 0 — always an unsecured Write, never WriteSecured semantics.
- if (itemAddress is null)
- {
- throw new ArgumentNullException(nameof(itemAddress));
- }
-
- ThrowIfDisposed();
- EnsureRegistered();
-
- IMxAccessServer mxServer = server!;
- if (!itemHandlesByAddress.TryGetValue(itemAddress, out int itemHandle))
- {
- itemHandle = mxServer.AddItem(serverHandle, itemAddress);
- itemHandlesByAddress[itemAddress] = itemHandle;
- addressesByItemHandle[itemHandle] = itemAddress;
- }
-
- mxServer.Write(serverHandle, itemHandle, value, userId: 0);
- }
-
///
/// Normalizes an MXAccess OnDataChange callback into a
/// and raises
@@ -223,8 +199,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
{
foreach (KeyValuePair entry in addressesByItemHandle)
{
- // Only UnAdvise handles that were actually advised; a write-only
- // item (added by Write but never Advise'd) was never advised.
+ // Only UnAdvise handles that were actually advised.
if (advisedItemHandles.Contains(entry.Key))
{
try { mxServer.UnAdvise(serverHandle, entry.Key); } catch { /* swallow — best effort */ }
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
index cca921d..4d9257b 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
@@ -27,9 +27,11 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
///
/// The subtag path is event-driven, so is a
/// no-op — value changes arrive through the source's advise stream
-/// rather than an explicit poll. Acknowledgment in subtag mode writes
-/// the comment to the target's writable ack-comment subtag rather than
-/// calling a native AlarmAckByGUID / AlarmAckByName.
+/// rather than an explicit poll. Acknowledgment in subtag mode resolves
+/// to the target's writable ack-comment subtag
+/// ();
+/// performs the operator-authenticated write on its own MXAccess
+/// server, so this consumer neither advises nor writes that subtag.
///
///
public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
@@ -49,7 +51,7 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
/// Initializes the consumer over a subtag source and a watch list of
/// alarm targets.
///
- /// The subtag value source to advise and write through.
+ /// The subtag value source to advise.
/// The alarm subtag targets to observe.
///
/// Thrown when or
@@ -91,7 +93,6 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
AddIfNotEmpty(addresses, target.ActiveSubtag);
AddIfNotEmpty(addresses, target.AckedSubtag);
AddIfNotEmpty(addresses, target.PrioritySubtag);
- AddIfNotEmpty(addresses, target.AckCommentSubtag);
}
source.Advise(addresses);
@@ -104,13 +105,7 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
}
///
- public int AcknowledgeByGuid(
- Guid alarmGuid,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTarget(Guid alarmGuid)
{
if (disposed)
{
@@ -120,22 +115,14 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
if (!referencesBySyntheticGuid.TryGetValue(alarmGuid, out string reference) ||
!targetsByReference.TryGetValue(reference, out AlarmSubtagTarget target))
{
- return -1;
+ return AlarmAckTarget.NotFound($"Alarm {alarmGuid} is not on the subtag watch list.");
}
- return WriteAckComment(target, ackComment);
+ return ToAckTarget(target);
}
///
- public int AcknowledgeByName(
- string alarmName,
- string providerName,
- string groupName,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTargetByName(string alarmName, string providerName, string groupName)
{
if (disposed)
{
@@ -143,12 +130,9 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
}
AlarmSubtagTarget? target = ResolveTargetByName(alarmName, providerName, groupName);
- if (target is null)
- {
- return -1;
- }
-
- return WriteAckComment(target, ackComment);
+ return target is null
+ ? AlarmAckTarget.NotFound($"Alarm '{providerName}!{groupName}.{alarmName}' is not on the subtag watch list.")
+ : ToAckTarget(target);
}
///
@@ -260,16 +244,12 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return target;
}
- private int WriteAckComment(AlarmSubtagTarget target, string ackComment)
+ private static AlarmAckTarget ToAckTarget(AlarmSubtagTarget target)
{
- string ackCommentSubtag = target.AckCommentSubtag ?? string.Empty;
- if (string.IsNullOrEmpty(ackCommentSubtag))
- {
- return -1;
- }
-
- source.Write(ackCommentSubtag, ackComment);
- return 0;
+ string ackItem = target.AckCommentSubtag ?? string.Empty;
+ return ackItem.Length == 0
+ ? AlarmAckTarget.NotAckable($"Alarm '{target.AlarmFullReference}' has no ack-comment subtag configured.")
+ : AlarmAckTarget.For(ackItem);
}
private static void AddIfNotEmpty(List addresses, string? address)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
index f109dc3..85089ea 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
@@ -34,7 +34,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// and operated from an STA thread; the worker's
/// runs an STA pump that hosts it.
/// The consumer owns no internal timer: every COM call
-/// (Subscribe, PollOnce, AcknowledgeBy*) must
+/// (Subscribe, PollOnce) must
/// be invoked on the STA that created the consumer. Polling cadence
/// is driven externally by the worker's STA via
/// StaRuntime.InvokeAsync(() => consumer.PollOnce()), which
@@ -43,6 +43,15 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// off the owning STA and can deadlock on cross-apartment marshaling
/// when the STA is not pumping messages, so no such timer exists.
///
+///
+/// Acknowledgement. This consumer only receives
+/// alarms. The wnwrap AlarmAckBy* calls were accepted and then
+/// inert on the dev rig (see docs/AlarmProbeFindings.md), so
+/// acknowledgement no longer goes through wnwrap: the consumer resolves
+/// a Galaxy alarm to its <tag>.AckMsg item
+/// () and
+/// writes that item over MXAccess as the authenticated operator.
+///
///
public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
{
@@ -50,6 +59,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
private const string DefaultApplicationName = "OtOpcUa.ZB.MOM.WW.MxGateway.Worker";
private const string DefaultVersion = "1.0";
private const int DefaultMaxAlarmsPerFetch = 1024;
+ private const string GalaxyProviderName = "Galaxy";
///
/// Floor on the resolved per-fetch cap. Mirrors the gateway-side
@@ -93,7 +103,6 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
private long truncatedFetchCount;
private bool lastSnapshotTruncated;
private wwAlarmConsumerClass? client;
- private wwAlarmConsumerClass? ackClient;
private bool subscribed;
private bool disposed;
@@ -200,7 +209,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
// WIN-911-style code uses against the same wnwrap library.
// Note rc=0 means the call was
// accepted, not that an acknowledgement was applied — see
- // AcknowledgeByName below and docs/AlarmProbeFindings.md.
+ // docs/AlarmProbeFindings.md. Acks now go through AlarmAckWriter.
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
if (init != 0)
{
@@ -241,10 +250,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
// 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).
+ // E_FAIL (HRESULT 0x80004005) on the first poll.
//
// The wnwrap interop signature is `void SetXmlAlarmQuery(string)`
// — there is no integer return code to gate on like the other v1
@@ -268,116 +274,49 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
ex);
}
- // 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}.");
- }
-
subscribed = true;
}
}
///
- public int AcknowledgeByGuid(
- Guid alarmGuid,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTarget(Guid alarmGuid)
{
if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
+ MxAlarmSnapshotRecord? record;
+ lock (syncRoot)
+ {
+ latestSnapshot.TryGetValue(alarmGuid, out record);
+ }
- wwAlarmConsumerClass com = client
- ?? throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
+ if (record is null)
+ {
+ return AlarmAckTarget.NotFound($"Alarm {alarmGuid} is not in the current alarm snapshot.");
+ }
- // 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);
+ return ResolveGalaxyAckTarget(record.ProviderName, record.TagName);
}
///
- public int AcknowledgeByName(
- string alarmName,
- string providerName,
- string groupName,
- string ackComment,
- string ackOperatorName,
- string ackOperatorNode,
- string ackOperatorDomain,
- string ackOperatorFullName)
+ public AlarmAckTarget ResolveAckTargetByName(string alarmName, string providerName, string groupName)
{
if (disposed) throw new ObjectDisposedException(nameof(WnWrapAlarmConsumer));
+ return ResolveGalaxyAckTarget(providerName, alarmName);
+ }
- // 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.");
+ private static AlarmAckTarget ResolveGalaxyAckTarget(string? providerName, string? tagName)
+ {
+ if (!string.Equals(providerName, GalaxyProviderName, StringComparison.OrdinalIgnoreCase))
+ {
+ return AlarmAckTarget.NotAckable(
+ $"Alarm provider '{providerName}' is not a Galaxy provider; only Galaxy alarms can be acknowledged over MXAccess.");
+ }
- // The original observation (live dev-rig 2026-05-01) was narrower than
- // it read: the IwwAlarmConsumer2 8-arg AlarmAckByName returned -55
- // "and looked like a stub" while the legacy 6-arg
- // IwwAlarmConsumer.AlarmAckByName returned 0, which is how the 6-arg
- // overload came to be the one called here. The 2026-08-18 probe
- // corrected both halves. The -55 tracks the *consumer*, not the
- // overload: both overloads return -55 against a
- // SetXmlAlarmQuery-applied consumer and 0 against the ack-only one
- // used above. And a zero return is NOT evidence the acknowledgement
- // was applied — the probe acked a real, freshly raised alarm six ways
- // and watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty,
- // and the alarm extension's own .Acked attribute stay False for 16 s
- // after each call. On that rig the ack is accepted and then inert, so
- // whether any wnwrap ack reaches the alarm-history path is
- // unverified; see docs/AlarmProbeFindings.md and the 2026-08-18
- // correction in docs/AlarmClientDiscovery.md. The 6-arg call site
- // below therefore stays as-is for MXAccess parity — the choice is no
- // longer justified by the rc, and rc semantics are per the probe.
- //
- // 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);
+ if (string.IsNullOrWhiteSpace(tagName))
+ {
+ return AlarmAckTarget.NotFound("Alarm record has no tag name.");
+ }
+
+ return AlarmAckTarget.For(tagName + "." + AlarmAckWriter.AckCommentAttribute);
}
///
@@ -944,37 +883,18 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
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()
{
wwAlarmConsumerClass? clientToDispose;
- wwAlarmConsumerClass? ackClientToDispose;
lock (syncRoot)
{
if (disposed) return;
disposed = true;
clientToDispose = client;
client = null;
- ackClientToDispose = ackClient;
- ackClient = null;
}
ReleaseConsumerCom(clientToDispose);
- ReleaseConsumerCom(ackClientToDispose);
}
private static void ReleaseConsumerCom(wwAlarmConsumerClass? consumer)