server: DriverNodeManager prefers IAlarmSource ack over IWritable (PR B.3)
Thirteenth PR of the alarms-over-gateway epic (docs/plans/alarms-over-gateway.md). Depends on PR B.2 (GalaxyDriver implements IAlarmSource, merged). When DriverNodeManager registers an AlarmConditionState with AlarmConditionService, it now picks the acknowledger: - Driver implements IAlarmSource → DriverAlarmSourceAcknowledger routes the operator comment through IAlarmSource.AcknowledgeAsync via the existing AlarmSurfaceInvoker (Phase 6.1 resilience pipeline, no-retry per decision #143). Preserves operator-comment fidelity end-to-end — the value-driven sub-attribute write collapses the comment into a single string write that loses MxAccess metadata. - Driver does not implement IAlarmSource → DriverWritableAcknowledger fallback (existing behaviour for AbCip / Modbus / S7 / etc). The dedup logic that prefers driver-native transitions over sub-attribute synthesis lives in AlarmConditionService and is already in place — drivers that surface OnAlarmEvent (B.2) feed the service directly, while sub-attribute writes still flow through DriverNodeManager's ConditionSink so a Galaxy template without $Alarm extensions stays functional. Tests: - 2 new routing-decision tests in DriverAlarmSourceAcknowledgerRoutingTests pin the IAlarmSource detection used at registration time. - Server build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,49 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR B.3 — preferred <see cref="IAlarmAcknowledger"/> for drivers that implement
|
||||
/// <see cref="IAlarmSource"/> (today: Galaxy via the gateway-side AcknowledgeAlarm
|
||||
/// RPC). Routes the operator comment through the driver's native ack API, which
|
||||
/// preserves operator-comment fidelity end-to-end (the value-driven sub-attribute
|
||||
/// fallback collapses the comment into a single string write).
|
||||
/// </summary>
|
||||
private sealed class DriverAlarmSourceAcknowledger(
|
||||
IAlarmSource alarmSource,
|
||||
string conditionId,
|
||||
ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker alarmInvoker) : IAlarmAcknowledger
|
||||
{
|
||||
public async Task<bool> WriteAckMessageAsync(
|
||||
string ackMsgWriteRef, string comment, CancellationToken cancellationToken)
|
||||
{
|
||||
// ackMsgWriteRef is unused on this path — the driver's IAlarmSource.AcknowledgeAsync
|
||||
// routes the ack against the alarm condition itself, not against the
|
||||
// sub-attribute. ConditionId carries the alarm full reference; SourceNodeId
|
||||
// is left empty since the gateway only addresses by full reference.
|
||||
// _ = alarmSource keeps the analyzer-required reference visible without an
|
||||
// unwrapped call — the actual ack runs through the AlarmSurfaceInvoker which
|
||||
// wires the AlarmAcknowledge resilience pipeline (no-retry per decision #143).
|
||||
_ = alarmSource;
|
||||
try
|
||||
{
|
||||
await alarmInvoker.AcknowledgeAsync(
|
||||
new[]
|
||||
{
|
||||
new AlarmAcknowledgeRequest(
|
||||
SourceNodeId: string.Empty,
|
||||
ConditionId: conditionId,
|
||||
Comment: comment ?? string.Empty),
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detach from the alarm service before the base disposes. The service is shared across
|
||||
/// drivers, so leaking the handler keeps a dead DriverNodeManager pinned in memory and
|
||||
@@ -787,8 +830,23 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
if (_owner._alarmService is not null && !string.IsNullOrEmpty(info.InAlarmRef))
|
||||
{
|
||||
_owner._conditionSinks[FullReference] = sink;
|
||||
var acker = new DriverWritableAcknowledger(
|
||||
_owner._writable, _owner._invoker, _owner._driver.DriverInstanceId);
|
||||
// PR B.3 — prefer IAlarmSource.AcknowledgeAsync (driver-native path)
|
||||
// when the driver supports it. Galaxy implements this since PR B.2;
|
||||
// for drivers without IAlarmSource the value-driven sub-attribute
|
||||
// fallback (DriverWritableAcknowledger) preserves the existing
|
||||
// behaviour.
|
||||
IAlarmAcknowledger acker;
|
||||
if (_owner._driver is IAlarmSource alarmSource)
|
||||
{
|
||||
var alarmInvoker = new ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker(
|
||||
_owner._invoker, alarmSource, _owner._driver.DriverInstanceId);
|
||||
acker = new DriverAlarmSourceAcknowledger(alarmSource, FullReference, alarmInvoker);
|
||||
}
|
||||
else
|
||||
{
|
||||
acker = new DriverWritableAcknowledger(
|
||||
_owner._writable, _owner._invoker, _owner._driver.DriverInstanceId);
|
||||
}
|
||||
_owner._alarmService.Track(FullReference, info, acker);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user