feat(templateengine): flatten native alarm sources (inherit/compose/override)
This commit is contained in:
@@ -96,6 +96,13 @@ public class FlatteningService
|
|||||||
// Step 7: Resolve alarm on-trigger script references to canonical names
|
// Step 7: Resolve alarm on-trigger script references to canonical names
|
||||||
ResolveAlarmScriptReferences(alarms, alarmScriptIds, scriptCanonicalById);
|
ResolveAlarmScriptReferences(alarms, alarmScriptIds, scriptCanonicalById);
|
||||||
|
|
||||||
|
// Step 7b: Resolve native alarm source bindings (connection + source
|
||||||
|
// reference). Conditions under each source are discovered at runtime;
|
||||||
|
// flattening only resolves the binding through inherit/compose/override.
|
||||||
|
var nativeAlarmSources = ResolveInheritedNativeAlarmSources(templateChain, prefix: null);
|
||||||
|
ResolveComposedNativeAlarmSources(templateChain, compositionMap, composedTemplateChains, nativeAlarmSources);
|
||||||
|
ApplyInstanceNativeAlarmSourceOverrides(instance.NativeAlarmSourceOverrides, nativeAlarmSources);
|
||||||
|
|
||||||
// Step 8: Collect connection configurations for deployment packaging
|
// Step 8: Collect connection configurations for deployment packaging
|
||||||
var connections = new Dictionary<string, ConnectionConfig>();
|
var connections = new Dictionary<string, ConnectionConfig>();
|
||||||
foreach (var attr in attributes.Values)
|
foreach (var attr in attributes.Values)
|
||||||
@@ -125,6 +132,7 @@ public class FlatteningService
|
|||||||
AreaId = instance.AreaId,
|
AreaId = instance.AreaId,
|
||||||
Attributes = attributes.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
Attributes = attributes.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||||
Alarms = alarms.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
Alarms = alarms.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||||
|
NativeAlarmSources = nativeAlarmSources.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||||
Scripts = scripts.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
|
Scripts = scripts.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||||
Connections = connections.Count > 0 ? connections : null,
|
Connections = connections.Count > 0 ? connections : null,
|
||||||
GeneratedAtUtc = DateTimeOffset.UtcNow
|
GeneratedAtUtc = DateTimeOffset.UtcNow
|
||||||
@@ -649,6 +657,137 @@ public class FlatteningService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves native alarm source bindings from an inheritance chain. Keys of
|
||||||
|
/// the returned dictionary are bare binding names (caller path-qualifies for
|
||||||
|
/// composed modules). Derived templates win unless the base binding is
|
||||||
|
/// locked; <see cref="TemplateNativeAlarmSource.IsInherited"/> placeholders
|
||||||
|
/// never shadow a live base binding.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, ResolvedNativeAlarmSource> ResolveInheritedNativeAlarmSources(
|
||||||
|
IReadOnlyList<Template> templateChain,
|
||||||
|
string? prefix)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, ResolvedNativeAlarmSource>(StringComparer.Ordinal);
|
||||||
|
// Tracks bindings the base locked, so derived templates cannot override them.
|
||||||
|
var lockedNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
for (int i = templateChain.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var template = templateChain[i];
|
||||||
|
var source = i == 0 ? "Template" : "Inherited";
|
||||||
|
|
||||||
|
foreach (var binding in template.NativeAlarmSources)
|
||||||
|
{
|
||||||
|
if (result.ContainsKey(binding.Name))
|
||||||
|
{
|
||||||
|
if (lockedNames.Contains(binding.Name))
|
||||||
|
continue;
|
||||||
|
// IsInherited rows on a derived template are placeholders that
|
||||||
|
// must not shadow the live base binding.
|
||||||
|
if (binding.IsInherited)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[binding.Name] = new ResolvedNativeAlarmSource
|
||||||
|
{
|
||||||
|
CanonicalName = binding.Name,
|
||||||
|
ConnectionName = binding.ConnectionName,
|
||||||
|
SourceReference = binding.SourceReference,
|
||||||
|
ConditionFilter = binding.ConditionFilter,
|
||||||
|
Source = source
|
||||||
|
};
|
||||||
|
|
||||||
|
if (binding.IsLocked)
|
||||||
|
lockedNames.Add(binding.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResolveComposedNativeAlarmSources(
|
||||||
|
IReadOnlyList<Template> templateChain,
|
||||||
|
IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
|
||||||
|
IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
|
||||||
|
Dictionary<string, ResolvedNativeAlarmSource> sources)
|
||||||
|
{
|
||||||
|
foreach (var template in templateChain)
|
||||||
|
{
|
||||||
|
if (!compositionMap.TryGetValue(template.Id, out var compositions))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var composition in compositions)
|
||||||
|
ResolveComposedNativeAlarmSourcesRecursive(
|
||||||
|
composition, composition.InstanceName,
|
||||||
|
compositionMap, composedTemplateChains, sources, new HashSet<int>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively resolves native alarm source bindings of a composed module
|
||||||
|
/// and nested modules, path-qualifying each canonical name with the
|
||||||
|
/// accumulated <paramref name="prefix"/>. The source reference is a raw
|
||||||
|
/// connection address, so (unlike alarm trigger configs) it is not rewritten.
|
||||||
|
/// </summary>
|
||||||
|
private static void ResolveComposedNativeAlarmSourcesRecursive(
|
||||||
|
TemplateComposition composition,
|
||||||
|
string prefix,
|
||||||
|
IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
|
||||||
|
IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
|
||||||
|
Dictionary<string, ResolvedNativeAlarmSource> sources,
|
||||||
|
HashSet<int> visited)
|
||||||
|
{
|
||||||
|
if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var composedSources = ResolveInheritedNativeAlarmSources(composedChain, prefix);
|
||||||
|
foreach (var (name, binding) in composedSources)
|
||||||
|
{
|
||||||
|
var canonicalName = $"{prefix}.{name}";
|
||||||
|
if (!sources.ContainsKey(canonicalName))
|
||||||
|
{
|
||||||
|
sources[canonicalName] = binding with
|
||||||
|
{
|
||||||
|
CanonicalName = canonicalName,
|
||||||
|
Source = "Composed"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var composedTemplate in composedChain)
|
||||||
|
{
|
||||||
|
if (!visited.Add(composedTemplate.Id))
|
||||||
|
continue;
|
||||||
|
if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var nested in nestedCompositions)
|
||||||
|
ResolveComposedNativeAlarmSourcesRecursive(
|
||||||
|
nested, $"{prefix}.{nested.InstanceName}",
|
||||||
|
compositionMap, composedTemplateChains, sources, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyInstanceNativeAlarmSourceOverrides(
|
||||||
|
ICollection<InstanceNativeAlarmSourceOverride> overrides,
|
||||||
|
Dictionary<string, ResolvedNativeAlarmSource> sources)
|
||||||
|
{
|
||||||
|
foreach (var ovr in overrides)
|
||||||
|
{
|
||||||
|
if (!sources.TryGetValue(ovr.SourceCanonicalName, out var existing))
|
||||||
|
continue; // Cannot add new bindings via overrides.
|
||||||
|
|
||||||
|
sources[ovr.SourceCanonicalName] = existing with
|
||||||
|
{
|
||||||
|
ConnectionName = ovr.ConnectionNameOverride ?? existing.ConnectionName,
|
||||||
|
SourceReference = ovr.SourceReferenceOverride ?? existing.SourceReference,
|
||||||
|
ConditionFilter = ovr.ConditionFilterOverride ?? existing.ConditionFilter,
|
||||||
|
Source = "Override"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves scripts from an inheritance chain. The returned dictionary is
|
/// Resolves scripts from an inheritance chain. The returned dictionary is
|
||||||
/// keyed by bare script name. <paramref name="scriptCanonicalById"/> is
|
/// keyed by bare script name. <paramref name="scriptCanonicalById"/> is
|
||||||
|
|||||||
@@ -666,4 +666,38 @@ public class FlatteningServiceTests
|
|||||||
// Parent module of a depth-2 script is the enclosing Pump module.
|
// Parent module of a depth-2 script is the enclosing Pump module.
|
||||||
Assert.Equal("MainPump", depth2.Scope.ParentPath);
|
Assert.Equal("MainPump", depth2.Scope.ParentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Flatten_ResolvesNativeAlarmSources_FromTemplate()
|
||||||
|
{
|
||||||
|
var template = CreateTemplate(1, "Base");
|
||||||
|
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
|
||||||
|
{ ConnectionName = "Opc", SourceReference = "ns=2;s=P1" });
|
||||||
|
var result = _sut.Flatten(CreateInstance(), [template],
|
||||||
|
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||||||
|
new Dictionary<int, IReadOnlyList<Template>>(),
|
||||||
|
new Dictionary<int, DataConnection>());
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Single(result.Value.NativeAlarmSources);
|
||||||
|
Assert.Equal("Pressure", result.Value.NativeAlarmSources[0].CanonicalName);
|
||||||
|
Assert.Equal("ns=2;s=P1", result.Value.NativeAlarmSources[0].SourceReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Flatten_InstanceOverride_ReplacesNativeAlarmSourceReference()
|
||||||
|
{
|
||||||
|
var template = CreateTemplate(1, "Base");
|
||||||
|
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
|
||||||
|
{ ConnectionName = "Opc", SourceReference = "ns=2;s=DEFAULT" });
|
||||||
|
var instance = CreateInstance();
|
||||||
|
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
|
||||||
|
{ SourceReferenceOverride = "ns=2;s=Tank07" });
|
||||||
|
var result = _sut.Flatten(instance, [template],
|
||||||
|
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||||||
|
new Dictionary<int, IReadOnlyList<Template>>(),
|
||||||
|
new Dictionary<int, DataConnection>());
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal("ns=2;s=Tank07", result.Value.NativeAlarmSources[0].SourceReference);
|
||||||
|
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user