feat(alarms): DriverHostActor routes native alarm transitions to Part 9 conditions (Phase B WS-4c)
This commit is contained in:
@@ -110,6 +110,17 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
private readonly Dictionary<string, (string DriverInstanceId, string FullName)> _driverRefByNodeId =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>(DriverInstanceId, FullName = alarm SourceNodeId) → folder-scoped condition NodeId(s).
|
||||
/// Built from EquipmentTags whose plan carries Alarm, alongside the value maps; resolves a native
|
||||
/// alarm transition to the materialised Part 9 condition node(s). Alarm tags are conditions, not
|
||||
/// value variables, so they are kept OUT of the value maps + value-subscription set.</summary>
|
||||
private readonly Dictionary<(string DriverInstanceId, string FullName), HashSet<string>> _alarmNodeIdByDriverRef = new();
|
||||
|
||||
/// <summary>Derives a full Part 9 condition snapshot from each native alarm transition delta,
|
||||
/// tracking per-condition-NodeId prior state. <see cref="Clear"/>'d on every apply alongside the
|
||||
/// value maps so stale condition state never leaks across redeploys.</summary>
|
||||
private readonly NativeAlarmProjector _nativeAlarmProjector = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cached local <see cref="RedundancyRole"/> from the latest <see cref="RedundancyStateChanged"/>
|
||||
/// snapshot (null = unknown until the first snapshot arrives, or no local node match). The inbound
|
||||
@@ -406,6 +417,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
Receive<DispatchDeployment>(HandleDispatchFromSteady);
|
||||
Receive<GetDiagnostics>(HandleGetDiagnostics);
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<RestartDriver>(HandleRestartDriver);
|
||||
Receive<ReconnectDriver>(HandleReconnectDriver);
|
||||
Receive<RouteNodeWrite>(HandleRouteNodeWrite);
|
||||
@@ -429,6 +441,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
});
|
||||
Receive<GetDiagnostics>(HandleGetDiagnostics);
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<RestartDriver>(HandleRestartDriver);
|
||||
Receive<ReconnectDriver>(HandleReconnectDriver);
|
||||
Receive<RouteNodeWrite>(HandleRouteNodeWrite);
|
||||
@@ -466,6 +479,36 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a native alarm transition (published by a driver child as
|
||||
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/>) to its materialised Part 9 condition
|
||||
/// node(s). The alarm path analogue of <see cref="ForwardToMux"/>: the driver fires keyed by the
|
||||
/// alarm source's <c>SourceNodeId</c> (the equipment tag's wire-ref FullName), which the
|
||||
/// <see cref="_alarmNodeIdByDriverRef"/> map — built each apply from the alarm-bearing EquipmentTags —
|
||||
/// resolves to the folder-scoped condition NodeId(s) the materialiser placed the condition(s) at.
|
||||
/// For each node the <see cref="_nativeAlarmProjector"/> projects the transition delta into a full
|
||||
/// <c>AlarmConditionSnapshot</c>, then this Tells <see cref="ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AlarmStateUpdate"/>
|
||||
/// — the SAME message scripted alarms use, so it routes through <c>WriteAlarmCondition</c>. An
|
||||
/// unknown ref is Debug-logged and dropped (mirrors the value drop). The <c>/alerts</c> fan-out is a
|
||||
/// separate concern (Task 7) and is NOT emitted here.
|
||||
/// </summary>
|
||||
private void ForwardNativeAlarm(DriverInstanceActor.AttributeAlarmPublished msg)
|
||||
{
|
||||
if (_opcUaPublishActor is null) return;
|
||||
if (!_alarmNodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, msg.Args.SourceNodeId), out var nodeIds))
|
||||
{
|
||||
_log.Debug("DriverHost {Node}: no alarm condition for ({Driver},{Ref}) — transition dropped",
|
||||
_localNode, msg.DriverInstanceId, msg.Args.SourceNodeId);
|
||||
return;
|
||||
}
|
||||
foreach (var nodeId in nodeIds)
|
||||
{
|
||||
var snapshot = _nativeAlarmProjector.Project(nodeId, msg.Args);
|
||||
_opcUaPublishActor.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AlarmStateUpdate(
|
||||
nodeId, snapshot, msg.Args.SourceTimestampUtc));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes an inbound operator write (Task 11 Asks this from the OPC UA node-manager side) to the
|
||||
/// owning driver child. Order matters:
|
||||
@@ -731,7 +774,11 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
return;
|
||||
}
|
||||
|
||||
// Value-subscription set: alarm-bearing tags are Part 9 conditions, not value variables, so they
|
||||
// are excluded — the driver must not value-subscribe an alarm attribute (it is fed via the native
|
||||
// alarm event stream, routed by ForwardNativeAlarm).
|
||||
var refsByDriver = composition.EquipmentTags
|
||||
.Where(t => t.Alarm is null)
|
||||
.GroupBy(t => t.DriverInstanceId, StringComparer.Ordinal)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
@@ -752,10 +799,27 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
// reflected. Each NodeId maps to exactly one driver ref (a variable is backed by a single driver
|
||||
// attribute), so last-writer-wins on the rare duplicate is harmless.
|
||||
_driverRefByNodeId.Clear();
|
||||
// Alarm condition routing map: (DriverInstanceId, FullName = alarm SourceNodeId) → folder-scoped
|
||||
// condition NodeId(s). Built from the SAME EquipmentTags pass (alarm-bearing tags only) so
|
||||
// ForwardNativeAlarm can land a native transition on the right condition node. Clear-and-rebuild
|
||||
// every apply; the projector is Clear()'d too so stale per-condition state never leaks across
|
||||
// redeploys (renames/removals/address-space rebuilds).
|
||||
_alarmNodeIdByDriverRef.Clear();
|
||||
_nativeAlarmProjector.Clear();
|
||||
foreach (var t in composition.EquipmentTags)
|
||||
{
|
||||
var key = (t.DriverInstanceId, t.FullName);
|
||||
var nodeId = EquipmentNodeIds.Variable(t.EquipmentId, t.FolderPath, t.Name);
|
||||
if (t.Alarm is not null)
|
||||
{
|
||||
// Alarm tags are conditions, not value variables: route them ONLY into the alarm map and
|
||||
// keep them OUT of the value maps + value-subscription set (so they don't get both a value
|
||||
// variable AND a condition).
|
||||
if (!_alarmNodeIdByDriverRef.TryGetValue(key, out var aset))
|
||||
_alarmNodeIdByDriverRef[key] = aset = new HashSet<string>(StringComparer.Ordinal);
|
||||
aset.Add(nodeId);
|
||||
continue;
|
||||
}
|
||||
if (!_nodeIdByDriverRef.TryGetValue(key, out var set))
|
||||
_nodeIdByDriverRef[key] = set = new HashSet<string>(StringComparer.Ordinal);
|
||||
set.Add(nodeId);
|
||||
|
||||
Reference in New Issue
Block a user