diff --git a/MainWindow.xaml b/MainWindow.xaml index c14ff0b23..a6faf4f24 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -377,6 +377,19 @@ + + + + + @@ -481,7 +494,9 @@ - + + diff --git a/Models/IedBrowserReadPlan.cs b/Models/IedBrowserReadPlan.cs index 0ce1bf267..ed50f2eb1 100644 --- a/Models/IedBrowserReadPlan.cs +++ b/Models/IedBrowserReadPlan.cs @@ -28,8 +28,10 @@ public static IedBrowserReadSelectionKind SelectionKind(IedBrowserTreeRow? page) _ => IedBrowserReadSelectionKind.FcAttributes }; + // Explicit, schema-verified MMS Read includes substitution (SV), originator + // (OR) and blocking (BL) attributes. These are observations, never writes. private static readonly HashSet ReadableFc = new( - ["ST", "MX", "CF", "DC", "SP", "SG", "SE", "EX"], StringComparer.OrdinalIgnoreCase); + ["ST", "MX", "CF", "DC", "SP", "SG", "SE", "EX", "SV", "OR", "BL"], StringComparer.OrdinalIgnoreCase); /// /// FC CO denotes command parameters, not scalar MMS Read targets. diff --git a/tests/ARSAS.Tests/P0FBrowserReadCoverageAndCardTests.cs b/tests/ARSAS.Tests/P0FBrowserReadCoverageAndCardTests.cs new file mode 100644 index 000000000..760597384 --- /dev/null +++ b/tests/ARSAS.Tests/P0FBrowserReadCoverageAndCardTests.cs @@ -0,0 +1,67 @@ +using System.Xml.Linq; +using AR.Iec61850.Discovery; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class P0FBrowserReadCoverageAndCardTests +{ + [Fact] + public void ExplicitReadLnIncludesVerifiedSubstitutionOriginAndBlockingWithoutControlOperations() + { + var attributes = new[] { ("subEna", "SV"), ("subVal", "SV"), ("subQ", "SV"), + ("subID", "SV"), ("opRcvd", "OR"), ("opOk", "OR"), + ("blkEna", "BL"), ("stVal", "ST"), ("ctlVal", "CO") }; + var dataObject = new LiveIedDataObjectModel + { + Name = "Pos", Reference = "LD/XCBR1.Pos", + Attributes = attributes.Select(a => new LiveIedDataAttributeModel + { + ObjectReference = "LD/XCBR1.Pos", + AttributePath = "Pos." + a.Item1, + MmsReference = "LD/XCBR1$" + a.Item2 + "$Pos$" + a.Item1, + FunctionalConstraint = a.Item2, + MmsType = "Boolean", + TypeSource = "LiveTypeSpecification" + }).ToArray() + }; + var ln = new LiveIedLogicalNodeModel { DataObjects = [dataObject] }; + var model = new LiveIedModelDiscoveryDocument + { + LogicalDevices = [new LiveIedLogicalDeviceModel + { + LogicalNodes = [ln] + }] + }; + var page = new IedBrowserTreeRow("model/ln/XCBR1", "XCBR1", "LN", 1, ln); + var plan = IedBrowserReadPlan.Build(model, page, readAll: false); + Assert.Equal(8, plan.Count); + Assert.All(new[] { "SV", "OR", "BL" }, + fc => Assert.Contains(plan, target => target.FunctionalConstraint == fc)); + Assert.DoesNotContain(plan, target => target.FunctionalConstraint == "CO"); + Assert.Contains(plan, target => target.Reference.EndsWith("$subEna", StringComparison.Ordinal)); + Assert.Contains(plan, target => target.Reference.EndsWith("$blkEna", StringComparison.Ordinal)); + } + + [Fact] + public void BrowserCardReadUsesPermanentLaneAndNeverInvokesBusyOverlay() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + string? file = null; + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, "MainWindow.xaml"); + if (File.Exists(candidate)) { file = candidate; break; } + dir = dir.Parent; + } + Assert.NotNull(file); + var xaml = File.ReadAllText(file!); + Assert.Contains("Height=\"6\" Margin=\"8,5,8,0\"", xaml); + Assert.Contains("x:Name=\"IedCardBrowserReadProgress\"", xaml); + Assert.Contains("TargetName=\"IedCardBrowserReadProgress\" Property=\"Opacity\" Value=\"1\"", xaml); + Assert.DoesNotContain( + "\n