feat(alarms): thread isNative through MaterialiseAlarmCondition; node manager tracks native conditions [H6a]
This commit is contained in:
@@ -36,6 +36,13 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
private readonly ConcurrentDictionary<string, BaseDataVariableState> _variables = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, FolderState> _folders = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, AlarmConditionState> _alarmConditions = new(StringComparer.Ordinal);
|
||||
/// <summary>H6a: the subset of <see cref="_alarmConditions"/> node ids materialised as NATIVE
|
||||
/// (driver-fed, e.g. Galaxy equipment-tag alarms) rather than scripted. A later task routes a native
|
||||
/// condition's inbound Acknowledge to the driver instead of the scripted engine, so the node manager
|
||||
/// must know which conditions are native. Maintained in lock-step with <see cref="_alarmConditions"/>:
|
||||
/// a native re-materialise adds, and <see cref="RebuildAddressSpace"/> clears it alongside
|
||||
/// <see cref="_alarmConditions"/> so a re-materialise as the other kind is correct.</summary>
|
||||
private readonly HashSet<string> _nativeAlarmNodeIds = new(StringComparer.Ordinal);
|
||||
/// <summary>Phase C: NodeId → resolved historian tagname for every variable materialised
|
||||
/// Historizing. Populated by <see cref="EnsureVariable"/> when a historian tagname is supplied; the
|
||||
/// (later) HistoryRead override resolves a HistoryRead request's NodeId against this map. Cleared on
|
||||
@@ -535,7 +542,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <see cref="AlarmConditionState"/>. LimitAlarm deliberately falls back to base per the T13
|
||||
/// notes — a script alarm carries no High/Low limits to populate.</para>
|
||||
/// </remarks>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity)
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
@@ -550,6 +557,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
PredefinedNodes?.Remove(existing.NodeId);
|
||||
}
|
||||
|
||||
// H6a: re-materialising the same id as the OTHER kind (native↔scripted) must reflect the new
|
||||
// kind, so always drop the stale native flag first and only re-add it below when isNative.
|
||||
_nativeAlarmNodeIds.Remove(alarmNodeId);
|
||||
|
||||
var parent = ResolveParentFolder(equipmentNodeId);
|
||||
|
||||
AlarmConditionState alarm = CreateAlarmConditionOfType(alarmType, parent);
|
||||
@@ -632,9 +643,23 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
|
||||
AddPredefinedNode(SystemContext, alarm);
|
||||
_alarmConditions[alarmNodeId] = alarm;
|
||||
// H6a: record native (driver-fed) conditions so a later task can route their inbound
|
||||
// Acknowledge to the driver rather than the scripted engine.
|
||||
if (isNative) _nativeAlarmNodeIds.Add(alarmNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>H6a — true if the condition materialised at <paramref name="alarmNodeId"/> is a NATIVE
|
||||
/// (driver-fed) alarm rather than a scripted one. A later task uses this to route a native condition's
|
||||
/// inbound Acknowledge to the driver instead of the scripted engine.</summary>
|
||||
/// <param name="alarmNodeId">The alarm condition node id.</param>
|
||||
internal bool IsNativeAlarmNode(string alarmNodeId)
|
||||
{
|
||||
// _nativeAlarmNodeIds is a plain HashSet mutated only under Lock (in MaterialiseAlarmCondition /
|
||||
// RebuildAddressSpace), so guard the read with the same Lock rather than risk a torn concurrent read.
|
||||
lock (Lock) return _nativeAlarmNodeIds.Contains(alarmNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared body for every inbound Part 9 alarm method handler (T18). Resolves the calling
|
||||
/// principal off the SDK <paramref name="context"/>, applies the <c>AlarmAck</c> role gate
|
||||
@@ -1289,6 +1314,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
PredefinedNodes?.Remove(alarm.NodeId);
|
||||
}
|
||||
_alarmConditions.Clear();
|
||||
// H6a: drop the native-alarm flags in lock-step with the conditions they classify, so a
|
||||
// re-materialise on the next apply (possibly as the other kind) starts from a clean slate.
|
||||
_nativeAlarmNodeIds.Clear();
|
||||
|
||||
foreach (var f in _folders.Values)
|
||||
{
|
||||
|
||||
@@ -201,7 +201,7 @@ public sealed class Phase7Applier
|
||||
{
|
||||
// Native alarm tag → a real Part 9 condition node (reuses the scripted-alarm path),
|
||||
// NOT a value variable. Parent is the sub-folder when set, else the equipment folder.
|
||||
SafeMaterialiseAlarmCondition(nodeId, parent, tag.Name, tag.Alarm.AlarmType, tag.Alarm.Severity);
|
||||
SafeMaterialiseAlarmCondition(nodeId, parent, tag.Name, tag.Alarm.AlarmType, tag.Alarm.Severity, isNative: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -292,7 +292,7 @@ public sealed class Phase7Applier
|
||||
foreach (var alarm in composition.EquipmentScriptedAlarms)
|
||||
{
|
||||
if (!alarm.Enabled) continue;
|
||||
SafeMaterialiseAlarmCondition(alarm.ScriptedAlarmId, alarm.EquipmentId, alarm.Name, alarm.AlarmType, alarm.Severity);
|
||||
SafeMaterialiseAlarmCondition(alarm.ScriptedAlarmId, alarm.EquipmentId, alarm.Name, alarm.AlarmType, alarm.Severity, isNative: false);
|
||||
materialised++;
|
||||
}
|
||||
|
||||
@@ -333,9 +333,9 @@ public sealed class Phase7Applier
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmCondition threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity)
|
||||
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
|
||||
{
|
||||
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity); }
|
||||
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
/// <param name="displayName">The human-readable condition name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity)
|
||||
=> _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity);
|
||||
/// <param name="isNative">True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm.</param>
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
=> _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
||||
|
||||
/// <summary>Ensures a folder node exists in the address space.</summary>
|
||||
/// <param name="folderNodeId">The folder node identifier.</param>
|
||||
|
||||
Reference in New Issue
Block a user