Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Services/LiveDiscoverySignalSelectionPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using ArIED61850Tester.Models;

namespace ArIED61850Tester.Services;

/// <summary>
/// Signal Selection policy for broad online MMS discovery.
///
/// Online GetNameList/VAA discovery is intentionally exhaustive. It contains process
/// values together with quality/timestamp companions, nameplate/configuration attributes,
/// substitution state, control-service structure and other engineering leaves. Those
/// objects remain available in the typed live model, but they are not independent operator
/// points and must not leak into Signal Selection.
///
/// Static DataSet membership is authoritative and therefore wins over this presentation
/// noise classifier: an object explicitly configured in a DataSet remains visible even if
/// its object-level FCD identity is not an exact runtime value leaf.
/// </summary>
public static class LiveDiscoverySignalSelectionPolicy
{
private static readonly HashSet<string> NoiseSegments = new(StringComparer.OrdinalIgnoreCase)
{
"mod", "beh", "health", "eehealth", "namplt",
"q", "t", "tm", "d", "du",
"origin", "orcat", "orident",
"ctlmodel", "ctlval", "ctlnum", "stseld",
"sbo", "sbow", "oper", "cancel", "check", "test",
"datans", "subena", "subval", "subq", "subid", "blkena",
"configrev", "vendor", "swrev", "lnns", "numpts", "olddata"
};

public static bool IsVisible(SignalDefinition? signal)
{
if (signal is null || string.IsNullOrWhiteSpace(signal.ObjectReference))
return false;

if (!string.IsNullOrWhiteSpace(signal.DataSetReference))
return true;

if (IsProtocolOrEngineeringNoise(signal.ObjectReference))
return false;

return SasOperationalSignalPolicy.IsVisible(signal);
}

public static bool IsProtocolOrEngineeringNoise(string? reference)
{
if (string.IsNullOrWhiteSpace(reference))
return true;

var normalized = reference.Trim().Replace('$', '.');
var slash = normalized.IndexOf('/');
var path = slash >= 0 && slash < normalized.Length - 1
? normalized[(slash + 1)..]
: normalized;
var segments = path.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

// Segment 0 is the Logical Node. Only data/control path segments are classified.
for (var index = 1; index < segments.Length; index++)
{
if (NoiseSegments.Contains(segments[index]))
return true;
}

return false;
}
}
79 changes: 79 additions & 0 deletions SignalSelectionWizardWindow.LiveDiscoveryOperationalFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Windows;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

/// <summary>
/// Owns the Signal Selection presentation contract after a live MMS discovery.
///
/// The ARIEC/live SignalDefinition inventory intentionally remains complete enough for
/// diagnostics, comparison, report planning, persistence and engineering inspection.
/// Signal Selection is narrower: it shows operator points and mandatory static DataSet
/// members, never protocol/service leaves such as Mod/Beh/Health/NamPlt/origin/q/t.
///
/// This filter is installed on the wizard after its constructor has installed search and
/// column filters. That lifecycle point is important because a default WPF collection view
/// can be shared with another DataGrid; a previously installed global filter can otherwise
/// be replaced by the wizard's own FilterSignal predicate.
/// </summary>
public partial class SignalSelectionWizardWindow
{
private Predicate<object>? _signalSelectionBaseFilter;
private Predicate<object>? _signalSelectionOperationalFilter;
private bool _signalSelectionOperationalFilterInstalled;

static SignalSelectionWizardWindow()
{
EventManager.RegisterClassHandler(
typeof(SignalSelectionWizardWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(OnSignalSelectionOperationalFilterLoaded));
}

private static void OnSignalSelectionOperationalFilterLoaded(object sender, RoutedEventArgs args)
{
if (sender is SignalSelectionWizardWindow window)
window.InstallSignalSelectionOperationalFilter();
}

private void InstallSignalSelectionOperationalFilter()
{
if (_signalSelectionOperationalFilterInstalled)
return;

_signalSelectionBaseFilter = SignalsView.Filter;
_signalSelectionOperationalFilter = item =>
{
if (_signalSelectionBaseFilter is not null && !_signalSelectionBaseFilter(item))
return false;

return item is SignalDefinition signal &&
LiveDiscoverySignalSelectionPolicy.IsVisible(signal);
};

SignalsView.Filter = _signalSelectionOperationalFilter;
_signalSelectionOperationalFilterInstalled = true;
Closed -= SignalSelectionOperationalFilter_Closed;
Closed += SignalSelectionOperationalFilter_Closed;
SignalsView.Refresh();
RefreshViewState();
}

private void SignalSelectionOperationalFilter_Closed(object? sender, EventArgs args)
{
Closed -= SignalSelectionOperationalFilter_Closed;

// Restore only our own wrapper. If another owner intentionally changed the shared
// collection view while this window was open, do not overwrite that newer filter.
if (_signalSelectionOperationalFilter is not null &&
Equals(SignalsView.Filter, _signalSelectionOperationalFilter))
{
SignalsView.Filter = _signalSelectionBaseFilter;
}

_signalSelectionOperationalFilterInstalled = false;
_signalSelectionOperationalFilter = null;
_signalSelectionBaseFilter = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ARSAS.Tests;

public sealed class LiveDiscoverySignalSelectionNoiseRegressionTests
{
[Theory]
[InlineData("AA1C1F13R4ADD/GAPC1.Mod.stVal", "GAPC1", "ST", "Boolean", "Status")]
[InlineData("AA1C1F13R4ADD/GAPC1.Beh.stVal", "GAPC1", "ST", "Boolean", "Status")]
[InlineData("AA1C1F13R4ADD/GAPC1.Health.stVal", "GAPC1", "ST", "Boolean", "Status")]
[InlineData("AA1C1F13R4ADD/GAPC1.NamPlt.d", "GAPC1", "DC", "VisString255", "Metadata")]
[InlineData("AA1C1F13R4ADD/GGIO1.LocOpnCMDsta.subVal", "GGIO1", "SV", "Boolean", "Status")]
[InlineData("AA1C1F13R4ADD/GGIO1.LocOpnCMDsta.subQ", "GGIO1", "SV", "Quality", "Quality")]
[InlineData("AA1C1F13R4ADD/GGIO1.LocOpnCMDsta.blkEna", "GGIO1", "BL", "Boolean", "Status")]
[InlineData("AA1C1F13R4ADD/GGIO1.LocOpnCMDsta.d.stVal", "GGIO1", "ST", "Boolean", "Status")]
[InlineData("AA1C1F13R4Application/LLN0.q.stVal", "LLN0", "ST", "Boolean", "Status")]
public void EngineeringAndProtocolLeaves_AreRejectedFromLiveSignalSelection(
string reference,
string logicalNode,
string fc,
string dataType,
string category)
{
var signal = NewSignal(reference, logicalNode, fc, dataType, category);

Assert.True(LiveDiscoverySignalSelectionPolicy.IsProtocolOrEngineeringNoise(reference));
Assert.False(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
}

[Theory]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.Oper.origin.orCat")]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.Oper.origin.orIdent")]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.Oper.ctlVal")]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.Oper.ctlNum")]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.Oper.Check")]
[InlineData("AA1C1F13R4ADD/GGIO1.Mod.ctlModel")]
public void ControlServiceLeaves_AreRejectedFromLiveSignalSelection(string reference)
{
var signal = NewSignal(reference, "GGIO1", "CO", "Struct", "Control");
signal.IsControlSignal = true;
signal.ControlCdc = "SPC";

Assert.True(LiveDiscoverySignalSelectionPolicy.IsProtocolOrEngineeringNoise(reference));
Assert.False(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
}

[Theory]
[InlineData("IEDLD/XCBR1.Pos.stVal", "XCBR1", "ST", "Enum", "Position")]
[InlineData("IEDLD/MMXU1.A.phsA.cVal.mag.f", "MMXU1", "MX", "Float32", "Measurement")]
[InlineData("IEDLD/PTRC1.Tr.general", "PTRC1", "ST", "Boolean", "Protection")]
[InlineData("IEDLD/GGIO1.Ind15.stVal", "GGIO1", "ST", "Boolean", "Status")]
public void RealOperatorPoints_RemainVisible(
string reference,
string logicalNode,
string fc,
string dataType,
string category)
{
var signal = NewSignal(reference, logicalNode, fc, dataType, category);

Assert.False(LiveDiscoverySignalSelectionPolicy.IsProtocolOrEngineeringNoise(reference));
Assert.True(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
}

[Fact]
public void RealPositionControl_RemainsVisible()
{
var signal = NewSignal("IEDLD/CSWI1.Pos", "CSWI1", "CO", "Struct", "Control");
signal.IsControlSignal = true;
signal.ControlCdc = "DPC";

Assert.True(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
}

[Fact]
public void StaticDataSetObjectLevelMember_RemainsVisibleEvenWithoutRuntimeLeaf()
{
var signal = NewSignal(
"AA1C1F13R4ADD/GGIO6.CBOpnd",
"GGIO6",
"ST",
"Boolean",
"DataSet");
signal.DataSetReference = "AA1C1F13R4Application/LLN0$Digital";
signal.DisplayReference = signal.ObjectReference;

Assert.False(SasOperationalSignalPolicy.IsVisible(signal));
Assert.True(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
Assert.DoesNotContain(".stVal", signal.DisplayReference, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void StaticDataSetAuthority_WinsEvenIfConfiguredMemberLooksLikeEngineeringState()
{
var signal = NewSignal(
"IEDLD/LLN0.Beh",
"LLN0",
"ST",
"Enum",
"DataSet");
signal.DataSetReference = "IEDLD/LLN0$Configured";

Assert.True(LiveDiscoverySignalSelectionPolicy.IsVisible(signal));
}

[Fact]
public void SignalSelectionWizard_InstallsOperationalFilterAfterConstructorFilters()
{
var source = File.ReadAllText(FindRepoFile("SignalSelectionWizardWindow.LiveDiscoveryOperationalFilter.cs"));

Assert.Contains("FrameworkElement.LoadedEvent", source, StringComparison.Ordinal);
Assert.Contains("_signalSelectionBaseFilter = SignalsView.Filter", source, StringComparison.Ordinal);
Assert.Contains("LiveDiscoverySignalSelectionPolicy.IsVisible(signal)", source, StringComparison.Ordinal);
Assert.Contains("SignalsView.Filter = _signalSelectionOperationalFilter", source, StringComparison.Ordinal);
Assert.Contains("SignalsView.Filter = _signalSelectionBaseFilter", source, StringComparison.Ordinal);
Assert.DoesNotContain("RemoveAt(", source, StringComparison.Ordinal);
Assert.DoesNotContain("Signals.Clear", source, StringComparison.Ordinal);
}

private static SignalDefinition NewSignal(
string reference,
string logicalNode,
string fc,
string dataType,
string category)
=> new()
{
Name = logicalNode,
ObjectReference = reference,
FunctionalConstraint = fc,
DataType = dataType,
Category = category,
Source = "ARIEC61850 live discovery",
ProbeStatus = "Readable",
Value = "0",
Quality = "Good"
};

private static string FindRepoFile(string relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory != null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
return candidate;
directory = directory.Parent;
}

throw new FileNotFoundException(relativePath);
}
}
Loading