Twelfth PR of the alarms-over-gateway epic (docs/plans/alarms-over-gateway.md). Depends on PR B.1 (EventPump dispatch, merged) and PR E.2 (.NET SDK alarm methods, merged). Restores the v1 IAlarmSource capability that PR 7.2 retired with the legacy Galaxy.Host / Galaxy.Proxy projects. GalaxyDriver gains: - IAlarmSource on the class declaration → eight capabilities total (IDriver / ITagDiscovery / IReadable / IWritable / ISubscribable / IRediscoverable / IHostConnectivityProbe / IAlarmSource). - SubscribeAlarmsAsync — returns a sentinel handle and starts the shared EventPump (alarm wiring is lazy on first sub). Multiple handles share the same gateway stream; the server-side AlarmConditionService dispatches per-source-node downstream. - UnsubscribeAlarmsAsync — symmetric handle removal; rejects handles not issued by this driver. - AcknowledgeAsync — issues one gateway RPC per acknowledgement through IGalaxyAlarmAcknowledger. ConditionId carries the alarm full reference; falls back to SourceNodeId when empty. - OnAlarmEvent — bridges EventPump.OnAlarmTransition (B.1) onto AlarmEventArgs. Suppressed when no alarm subscription is active so untracked transitions don't leak through. New runtime types: - IGalaxyAlarmAcknowledger — test seam. - GatewayGalaxyAlarmAcknowledger — production wrapper around MxGatewayClient.AcknowledgeAlarmAsync (PR E.2). Maps native MxStatus failures to a logged warning rather than a thrown exception so a transient MxAccess hiccup doesn't fail the operator's Acknowledge. - GalaxyAlarmSubscriptionHandle — driver-side IAlarmSubscriptionHandle. Production runtime construction in BuildProductionRuntimeAsync wires the acknowledger when not pre-injected; tests inject a fake via the internal ctor. Tests: - 7 new tests in GalaxyDriverAlarmSourceTests — subscribe → event fire path, suppress without subscription, unsubscribe stops flow, foreign-handle rejection, ack routes per-request, ack falls back to SourceNodeId, ack throws NotSupported without acknowledger. - Full Driver.Galaxy.Tests: 203 passed (was 196; 7 new). Operates as a "stub-ready" surface — runtime ack calls will return PERMISSION_DENIED until A.3 ships the gateway-side dispatch, and no alarm transitions will arrive until A.2 adds the worker MxAccess subscription. Both will activate this code path automatically when the gateway side lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.6 KiB
C#
66 lines
2.6 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using MxGateway.Client;
|
|
using MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
|
|
|
/// <summary>
|
|
/// Production <see cref="IGalaxyAlarmAcknowledger"/> backed by the
|
|
/// <c>MxGatewayClient.AcknowledgeAlarmAsync</c> RPC (PR E.2). Maps the
|
|
/// reply's protocol status into a thrown exception when the gateway
|
|
/// reports a non-OK condition; native MxStatus failures inside the reply
|
|
/// surface as a logged warning so operator workflows aren't blocked by a
|
|
/// transient MxAccess hiccup.
|
|
/// </summary>
|
|
internal sealed class GatewayGalaxyAlarmAcknowledger : IGalaxyAlarmAcknowledger
|
|
{
|
|
private readonly MxGatewayClient _client;
|
|
private readonly GalaxyMxSession _session;
|
|
private readonly ILogger _logger;
|
|
|
|
public GatewayGalaxyAlarmAcknowledger(
|
|
MxGatewayClient client,
|
|
GalaxyMxSession session,
|
|
ILogger logger)
|
|
{
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async Task AcknowledgeAsync(
|
|
string alarmFullReference,
|
|
string comment,
|
|
string operatorUser,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrEmpty(alarmFullReference);
|
|
|
|
var session = _session.Session
|
|
?? throw new InvalidOperationException(
|
|
"GatewayGalaxyAlarmAcknowledger requires a connected GalaxyMxSession; underlying gateway session is null.");
|
|
var sessionId = session.SessionId;
|
|
|
|
var reply = await _client.AcknowledgeAlarmAsync(
|
|
new AcknowledgeAlarmRequest
|
|
{
|
|
SessionId = sessionId,
|
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
AlarmFullReference = alarmFullReference,
|
|
Comment = comment ?? string.Empty,
|
|
OperatorUser = operatorUser ?? string.Empty,
|
|
},
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
if (reply.Status is { Success: 0 } status)
|
|
{
|
|
// Native MxAccess rejected the ack — log but don't throw. Treat as a
|
|
// best-effort operator workflow; the operator can retry via the OPC UA
|
|
// session if necessary.
|
|
_logger.LogWarning(
|
|
"Galaxy AcknowledgeAlarm for {AlarmRef} returned MxStatus failure: category={Category} detail={Detail} text={Text}",
|
|
alarmFullReference, status.Category, status.Detail, status.DiagnosticText);
|
|
}
|
|
}
|
|
}
|