feat(alarms): wire NativeAlarmAckRouter to DriverHostActor in host DI [H6e]

This commit is contained in:
Joseph Doherty
2026-06-15 14:54:12 -04:00
parent 93d9160dae
commit 30315185a3
2 changed files with 62 additions and 0 deletions
@@ -139,6 +139,45 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
}
});
// Wire the reverse-path native-alarm-ack router (H6e): a client Acknowledge of a NATIVE (driver-fed,
// e.g. Galaxy) Part 9 condition that passes the node manager's AlarmAck gate routes the acknowledge to
// the owning driver child. The node manager maps the inbound ack to an OpcUaServer-local NativeAlarmAck;
// HERE the host maps that to the Runtime DriverHostActor.RouteNativeAlarmAck (field-for-field) and Tells
// it into the local DriverHostActor, which Primary-gates + resolves the owning DriverInstanceActor. The
// mapping happens at this boundary so Runtime stays free of an OpcUaServer dependency. The Tell is
// fire-and-forget so the handler — which runs under the SDK's Lock — never blocks. The DriverHostActor
// ref is resolved LAZILY per ack (this StartAsync runs before the Akka DriverHostActor registers, so a
// one-shot resolve here would always miss); by ack time the registry has it, mirroring the node-write
// gateway below.
_server.SetNativeAlarmAckRouter(nativeAck =>
{
try
{
if (_actorRegistry.TryGet<DriverHostActorKey>(out var driverHost))
{
driverHost.Tell(new DriverHostActor.RouteNativeAlarmAck(
nativeAck.ConditionNodeId, nativeAck.Comment, nativeAck.OperatorUser));
}
else
{
// No driver host registered yet (admin-only node, or pre-registration race). The Part 9 ack
// already committed the local condition state; the missed routing surfaces as a non-applied
// upstream acknowledge, not a client-visible error.
_logger.LogWarning(
"OtOpcUaServerHostedService: native alarm ack for {ConditionNodeId} dropped — no DriverHostActor registered",
nativeAck.ConditionNodeId);
}
}
catch (Exception ex)
{
// The router runs under the SDK Lock on a server thread; a hiccup must not escape into the SDK's
// Call path. Log + drop — the client still gets Good for the condition-state change.
_logger.LogWarning(ex,
"OtOpcUaServerHostedService: failed to route native alarm ack for {ConditionNodeId}",
nativeAck.ConditionNodeId);
}
});
// Wire the reverse-path inbound operator-write gateway: a client write to a writable equipment-tag
// node that passes the node manager's WriteOperate gate routes the write to the owning driver child
// (RouteNodeWrite → NodeWriteResult) via the local DriverHostActor. The node manager calls the
@@ -185,6 +224,8 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
_deferredServiceLevel.SetInner(null);
// Restore the Null write gateway so a late client write doesn't Ask a stopping DriverHostActor.
_server?.SetNodeWriteGateway(null);
// Clear the native-alarm-ack router so a late ack doesn't Tell a stopping DriverHostActor.
_server?.SetNativeAlarmAckRouter(null);
// Restore the Null historian so a late HistoryRead doesn't hit a disposed read client.
_server?.SetHistorianDataSource(null);
return Task.CompletedTask;
@@ -37,6 +37,27 @@ public sealed class OtOpcUaSdkServer : StandardServer
return true;
}
/// <summary>
/// Wire the reverse-path router for inbound Part 9 Acknowledge of a NATIVE (driver-fed, e.g. Galaxy)
/// condition onto the created <see cref="OtOpcUaNodeManager"/>. The host calls this after start with a
/// non-blocking router that Tells a <c>DriverHostActor.RouteNativeAlarmAck</c> into the local
/// <c>DriverHostActor</c>, which (Primary-gated) routes the acknowledge to the owning driver child.
/// Native conditions are not owned by the scripted-alarm engine, so the node manager branches their
/// inbound Acknowledge to this seam instead of the scripted <see cref="SetAlarmCommandRouter"/> path.
/// Passing <c>null</c> clears it. No-op (returns <c>false</c>) when the node manager has not been
/// created yet, so the caller can detect a too-early call (mirrors <see cref="SetAlarmCommandRouter"/>).
/// </summary>
/// <param name="router">The router invoked by the native condition's Acknowledge handler once the
/// <c>AlarmAck</c> gate passes; may be <c>null</c> to clear it.</param>
/// <returns><c>true</c> when the router was set on a live node manager; <c>false</c> when no node
/// manager exists yet.</returns>
public bool SetNativeAlarmAckRouter(Action<NativeAlarmAck>? router)
{
if (_otOpcUaNodeManager is null) return false;
_otOpcUaNodeManager.NativeAlarmAckRouter = router;
return true;
}
/// <summary>
/// Wire the reverse-path gateway for inbound operator writes to writable equipment-tag nodes onto the
/// created <see cref="OtOpcUaNodeManager"/>. The host calls this after start with an