refactor(worker): alarm consumers resolve the AckMsg item instead of acking
IMxAccessAlarmConsumer.ResolveAckTarget/ResolveAckTargetByName replace AcknowledgeByGuid/AcknowledgeByName. wnwrap maps Galaxy-provider alarms to <tag>.AckMsg and drops its inert parallel ack consumer; subtag resolves to the target's AckCommentSubtag and no longer advises or writes it; ISubtagAlarmSource.Write is removed. Does not build alone: AlarmDispatcher moves to the new API in the next commit. Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
@@ -41,7 +41,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
/// <strong>Threading.</strong> Like its children, this type is driven
|
||||
/// entirely on the worker's STA: <see cref="Subscribe"/>,
|
||||
/// <see cref="PollOnce"/>, <see cref="ProbeOnce"/>, and the
|
||||
/// <c>AcknowledgeBy*</c> calls are all invoked from the apartment that
|
||||
/// <c>ResolveAckTarget*</c> 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 <see cref="PollOnce"/> on a timer.
|
||||
/// </para>
|
||||
@@ -259,34 +259,17 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -49,51 +49,19 @@ public interface IMxAccessAlarmConsumer : IDisposable
|
||||
void Subscribe(string subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges a single alarm with full operator-identity fidelity.
|
||||
/// Reaches AVEVA's native <c>AlarmAckByGUID</c>; 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. <c>Obj.Alarm.AckMsg</c>). Does not perform the ack.
|
||||
/// </summary>
|
||||
/// <param name="alarmGuid">The alarm GUID.</param>
|
||||
/// <param name="ackComment">The acknowledgment comment.</param>
|
||||
/// <param name="ackOperatorName">The operator name.</param>
|
||||
/// <param name="ackOperatorNode">The operator node.</param>
|
||||
/// <param name="ackOperatorDomain">The operator domain.</param>
|
||||
/// <param name="ackOperatorFullName">The operator full name.</param>
|
||||
/// <returns>The AVEVA-native status code.</returns>
|
||||
int AcknowledgeByGuid(
|
||||
Guid alarmGuid,
|
||||
string ackComment,
|
||||
string ackOperatorName,
|
||||
string ackOperatorNode,
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName);
|
||||
/// <param name="alarmGuid">The alarm GUID as reported by this consumer.</param>
|
||||
/// <returns>The resolved target or the reason none exists.</returns>
|
||||
AlarmAckTarget ResolveAckTarget(Guid alarmGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledge a single alarm by its (name, provider, group) tuple.
|
||||
/// Reaches AVEVA's <c>AlarmAckByName</c> on
|
||||
/// <c>wwAlarmConsumerClass</c>; same alarm-history outcome as
|
||||
/// <see cref="AcknowledgeByGuid"/>, used when the caller has the
|
||||
/// human-readable reference but not the canonical GUID.
|
||||
/// </summary>
|
||||
/// <param name="alarmName">The alarm name.</param>
|
||||
/// <param name="providerName">The provider name.</param>
|
||||
/// <param name="groupName">The group name.</param>
|
||||
/// <param name="ackComment">The acknowledgment comment.</param>
|
||||
/// <param name="ackOperatorName">The operator name.</param>
|
||||
/// <param name="ackOperatorNode">The operator node.</param>
|
||||
/// <param name="ackOperatorDomain">The operator domain.</param>
|
||||
/// <param name="ackOperatorFullName">The operator full name.</param>
|
||||
/// <returns>The AVEVA-native status code.</returns>
|
||||
int AcknowledgeByName(
|
||||
string alarmName,
|
||||
string providerName,
|
||||
string groupName,
|
||||
string ackComment,
|
||||
string ackOperatorName,
|
||||
string ackOperatorNode,
|
||||
string ackOperatorDomain,
|
||||
string ackOperatorFullName);
|
||||
/// <summary>Resolves the ack item for an alarm identified by name, provider and group.</summary>
|
||||
/// <param name="alarmName">The tag/alarm name, e.g. <c>TestMachine_001.TestAlarm001</c>.</param>
|
||||
/// <param name="providerName">The provider (literal <c>Galaxy</c> for ArchestrA).</param>
|
||||
/// <param name="groupName">The area/group.</param>
|
||||
/// <returns>The resolved target or the reason none exists.</returns>
|
||||
AlarmAckTarget ResolveAckTargetByName(string alarmName, string providerName, string groupName);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the consumer's most recently parsed snapshot of currently
|
||||
|
||||
@@ -16,9 +16,4 @@ public interface ISubtagAlarmSource : IDisposable
|
||||
/// <summary>Begins advising the supplied MXAccess subtag item addresses.</summary>
|
||||
/// <param name="itemAddresses">The subtag references to advise.</param>
|
||||
void Advise(IReadOnlyCollection<string> itemAddresses);
|
||||
|
||||
/// <summary>Writes a value to the named MXAccess subtag (e.g. an ack request).</summary>
|
||||
/// <param name="itemAddress">The subtag reference to write.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
void Write(string itemAddress, object? value);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
/// Production <see cref="ISubtagAlarmSource"/> backed by its own MXAccess
|
||||
/// <c>LMXProxyServerClass</c> COM object. It advises a set of alarm subtag
|
||||
/// item addresses, normalizes each <c>OnDataChange</c> COM callback into a
|
||||
/// <see cref="SubtagValueChange"/>, and supports writing a value (e.g. an
|
||||
/// ack-comment) to a subtag.
|
||||
/// <see cref="SubtagValueChange"/>. It is observe-only: alarm
|
||||
/// acknowledgement goes through <see cref="AlarmAckWriter"/>, which
|
||||
/// authenticates the operator on its own server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -25,7 +26,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
/// <strong>Threading.</strong> The <c>LMXProxyServerClass</c> CLSID is
|
||||
/// apartment-threaded and delivers <c>OnDataChange</c> via the STA
|
||||
/// message pump. Every method on this type — the lazy <see cref="Advise"/>
|
||||
/// creation, <see cref="Write"/>, and <see cref="Dispose"/> — must be
|
||||
/// creation and <see cref="Dispose"/> — must be
|
||||
/// invoked on the worker's STA that created the source, exactly as
|
||||
/// <see cref="MxAccessSession"/> and <see cref="WnWrapAlarmConsumer"/>
|
||||
/// require. The <c>OnDataChange</c> COM event also arrives on that STA.
|
||||
@@ -47,9 +48,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
|
||||
new Dictionary<int, string>();
|
||||
|
||||
// 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<int> advisedItemHandles = new HashSet<int>();
|
||||
|
||||
private object? mxAccessComObject;
|
||||
@@ -85,7 +84,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
|
||||
/// <see cref="IMxAccessServer"/> with a pre-registered server handle,
|
||||
/// bypassing the live COM factory. Exposed via
|
||||
/// <c>InternalsVisibleTo("ZB.MOM.WW.MxGateway.Worker.Tests")</c> so tests can
|
||||
/// exercise <see cref="Advise"/>/<see cref="Write"/> and
|
||||
/// exercise <see cref="Advise"/> and
|
||||
/// <see cref="HandleDataChange"/> without a live <c>LMXProxyServerClass</c>.
|
||||
/// No COM event source is wired in this mode — tests drive
|
||||
/// <see cref="HandleDataChange"/> directly to simulate an
|
||||
@@ -140,29 +139,6 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes an MXAccess <c>OnDataChange</c> callback into a
|
||||
/// <see cref="SubtagValueChange"/> and raises <see cref="ValueChanged"/>
|
||||
@@ -223,8 +199,7 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
|
||||
{
|
||||
foreach (KeyValuePair<int, string> 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 */ }
|
||||
|
||||
@@ -27,9 +27,11 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
/// <para>
|
||||
/// The subtag path is event-driven, so <see cref="PollOnce"/> 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 <c>AlarmAckByGUID</c> / <c>AlarmAckByName</c>.
|
||||
/// rather than an explicit poll. Acknowledgment in subtag mode resolves
|
||||
/// to the target's writable ack-comment subtag
|
||||
/// (<see cref="ResolveAckTarget"/>); <see cref="AlarmAckWriter"/>
|
||||
/// performs the operator-authenticated write on its own MXAccess
|
||||
/// server, so this consumer neither advises nor writes that subtag.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="source">The subtag value source to advise and write through.</param>
|
||||
/// <param name="source">The subtag value source to advise.</param>
|
||||
/// <param name="watchList">The alarm subtag targets to observe.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="source"/> or <paramref name="watchList"/>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<string> addresses, string? address)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
/// and operated from an STA thread; the worker's
|
||||
/// <see cref="MxAccessStaSession"/> runs an STA pump that hosts it.
|
||||
/// The consumer owns <em>no</em> internal timer: every COM call
|
||||
/// (<c>Subscribe</c>, <c>PollOnce</c>, <c>AcknowledgeBy*</c>) must
|
||||
/// (<c>Subscribe</c>, <c>PollOnce</c>) must
|
||||
/// be invoked on the STA that created the consumer. Polling cadence
|
||||
/// is driven externally by the worker's STA via
|
||||
/// <c>StaRuntime.InvokeAsync(() => consumer.PollOnce())</c>, 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Acknowledgement.</strong> This consumer only receives
|
||||
/// alarms. The wnwrap <c>AlarmAckBy*</c> calls were accepted and then
|
||||
/// inert on the dev rig (see <c>docs/AlarmProbeFindings.md</c>), so
|
||||
/// acknowledgement no longer goes through wnwrap: the consumer resolves
|
||||
/// a Galaxy alarm to its <c><tag>.AckMsg</c> item
|
||||
/// (<see cref="ResolveAckTarget"/>) and <see cref="AlarmAckWriter"/>
|
||||
/// writes that item over MXAccess as the authenticated operator.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user