Skip to content
Draft
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
91 changes: 83 additions & 8 deletions MainWindow.IedBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public partial class MainWindow
private readonly IedBrowserDetailProjection _iedBrowserDetails = new();
private readonly IedBrowserNavigationTrail _iedBrowserNavigation = new();
private bool _iedBrowserSyncingSelection;
private string? _iedBrowserSelectedDoReference;
private int _iedBrowserNavigationRenderTicket;
private long _iedBrowserLastLiveVersion = -1;
private LiveIedCanonicalModel? _iedBrowserLastCfCanonicalModel;
Expand Down Expand Up @@ -240,7 +241,13 @@ private async void IedBrowserReadValues_Click(object sender, RoutedEventArgs e)
}
}
var model = device.LiveDiscoveryModel ?? device.SclWorkspace?.DesignModel;
var targets = IedBrowserReadPlan.Build(model, _iedBrowserNavigation.Current, readAll);
var currentPage = _iedBrowserNavigation.Current;
var selectedDo = !readAll &&
ReferenceEquals(_iedBrowserRenderedPage, currentPage) &&
currentPage?.Payload is LiveIedLogicalNodeModel
? _iedBrowserSelectedDoReference : null;
var targets = IedBrowserReadPlan.Build(
model, currentPage, readAll, selectedDo);
if (targets.Count == 0)
{
IedBrowserSubtitle.Text = "No model-verified readable FC attributes in this selection.";
Expand Down Expand Up @@ -305,7 +312,8 @@ void DrainReadResults()
RefreshIedBrowserVisibleValues();
}
device.ResetDiscoveryProgress("Preparing read-only MMS values…",
readAll ? "Reading all IED values" : "Reading Browser values");
readAll ? "Reading all IED values" : selectedDo is not null
? "Reading selected DO " + selectedDo : "Reading Browser values");
device.IsBrowserReading = true; // Preserve the ongoing report monitor lifecycle.
IedBrowserReadPageButton.IsEnabled = false;
IedBrowserReadAllButton.IsEnabled = false;
Expand Down Expand Up @@ -995,6 +1003,75 @@ private void RefreshIedBrowser()
UpdateIedBrowserRcbReportDetails(); // metadata may arrive without a scalar update
}

private static bool IedBrowserClickOriginIsButton(
object originalSource, DependencyObject container)
{
var node = originalSource as DependencyObject;
while (node is not null && !ReferenceEquals(node, container))
{
if (node is System.Windows.Controls.Primitives.ButtonBase)
return true;
node = node is FrameworkContentElement content
? content.Parent
: System.Windows.Media.VisualTreeHelper.GetParent(node);
}
return false;
}

private void IedBrowserTreeRow_MouseUp(
object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ClickCount != 1 ||
sender is not DataGridRow { DataContext: IedBrowserTreeRow row } ||
!row.HasChildren || IedBrowserClickOriginIsButton(e.OriginalSource,
(DependencyObject)sender))
return;
// A virtual collection reset must not replace the clicked selection
// with a transient null and erase the navigation trail.
_iedBrowserSyncingSelection = true;
try
{
_iedBrowserTree.Toggle(row);
IedBrowserTreeGrid.SelectedItem = row;
}
finally { _iedBrowserSyncingSelection = false; }
_iedBrowserNavigation.SelectFromTree(row);
var owner = SelectedDevice;
var ticket = ++_iedBrowserNavigationRenderTicket;
Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
if (ticket != _iedBrowserNavigationRenderTicket ||
MainTabs.SelectedIndex != IedBrowserWorkspaceIndex ||
!ReferenceEquals(SelectedDevice, owner) ||
!ReferenceEquals(_iedBrowserNavigation.Current, row))
return;
var model = owner?.LiveDiscoveryModel ?? owner?.SclWorkspace?.DesignModel;
RenderIedBrowserSelection(row, model, owner?.IsConnected == true);
}));
e.Handled = true;
}

private void IedBrowserDetailRow_MouseUp(
object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.ClickCount != 1 ||
sender is not ListViewItem { DataContext: IedBrowserDetailRow detail } ||
IedBrowserClickOriginIsButton(e.OriginalSource,
(DependencyObject)sender))
return;
if (!string.IsNullOrEmpty(detail.ReadObjectReference) &&
_iedBrowserNavigation.Current?.Payload is LiveIedLogicalNodeModel)
{
_iedBrowserSelectedDoReference = detail.ReadObjectReference;
IedBrowserReadPageButton.ToolTip =
"Read all model-proven safe FC attributes in selected DO " +
detail.ReadObjectReference + "; no writes or automatic polling.";
}
if (!detail.HasChildren) return;
ToggleIedBrowserDetails(detail);
e.Handled = true;
}

private void IedBrowserTreeToggle_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: IedBrowserTreeRow row })
Expand Down Expand Up @@ -1056,12 +1133,8 @@ private void IedBrowserDetailItem_DoubleClick(object sender, System.Windows.Inpu
return;
}

if (!detail.HasChildren)
return;
// Quality/time and ordered dataset member rows drill down lazily in place,
// preserving the selected IEC object and the surrounding report context.
ToggleIedBrowserDetails(detail);
e.Handled = true;
// Full-row single click handles expansion. Double-click retains only
// genuine object navigation; never undo the first toggle here.
}

private void IedBrowserBack_Click(object sender, RoutedEventArgs e)
Expand Down Expand Up @@ -1130,6 +1203,8 @@ private void ToggleIedBrowserDetails(IedBrowserDetailRow row)
private void RenderIedBrowserSelection(
IedBrowserTreeRow? row, LiveIedModelDiscoveryDocument? model, bool connected)
{
if (!ReferenceEquals(_iedBrowserRenderedPage, row))
_iedBrowserSelectedDoReference = null;
_iedBrowserRenderedPage = row;
_iedBrowserRenderedConnected = connected;
_iedBrowserGooseExpected = null;
Expand Down
9 changes: 9 additions & 0 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,13 @@
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True"
SelectionChanged="IedBrowserTree_SelectionChanged">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<EventSetter Event="PreviewMouseLeftButtonUp"
Handler="IedBrowserTreeRow_MouseUp"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Objects" Width="*">
<DataGridTemplateColumn.CellTemplate>
Expand Down Expand Up @@ -2115,6 +2122,8 @@
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="MouseDoubleClick" Handler="IedBrowserDetailItem_DoubleClick"/>
<EventSetter Event="PreviewMouseLeftButtonUp"
Handler="IedBrowserDetailRow_MouseUp"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
Expand Down
3 changes: 3 additions & 0 deletions Models/IedBrowserPresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ public string ObservationTooltip
public string EvidenceKind { get; init; } = "";
public string FunctionalConstraint { get; init; } = "";
public bool IsDataObject { get; init; }
// Exact whole IEC DO for an operator's explicit Read choice. Unlike
// ValueReference, never points to an arbitrary preferred scalar leaf.
public string ReadObjectReference { get; init; } = "";
// IEC DataSet position, not a live value; only a validated frame can populate it.
public int GooseSlot { get; init; } = -1;
public string GooseReference { get; init; } = "";
Expand Down
12 changes: 10 additions & 2 deletions Models/IedBrowserReadPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public static IedBrowserReadSelectionKind SelectionKind(IedBrowserTreeRow? page)
["ST", "MX", "CF", "DC", "SP", "SG", "SE", "EX"], StringComparer.OrdinalIgnoreCase);

public static IReadOnlyList<IedBrowserReadTarget> Build(
LiveIedModelDiscoveryDocument? model, IedBrowserTreeRow? page, bool readAll)
LiveIedModelDiscoveryDocument? model, IedBrowserTreeRow? page,
bool readAll, string? selectedDoReference = null)
{
if (model is null) return Array.Empty<IedBrowserReadTarget>();
var schema = new IedBrowserSchemaIndex(model);
Expand All @@ -52,7 +53,14 @@ public static IReadOnlyList<IedBrowserReadTarget> Build(
string.Equals(a.FunctionalConstraint, attribute.FunctionalConstraint,
StringComparison.OrdinalIgnoreCase)),
LiveIedDataObjectModel obj => obj.Attributes,
LiveIedLogicalNodeModel ln => ln.DataObjects.SelectMany(o => o.Attributes),
// An explicit DO focus in an LN page scopes Read to every
// model-verified safe FC leaf of that exact DO. No first-child
// selection, prefix matching, or matching names in other LNs.
LiveIedLogicalNodeModel ln => ln.DataObjects
.Where(o => selectedDoReference is null ||
string.Equals(o.Reference, selectedDoReference,
StringComparison.Ordinal))
.SelectMany(o => o.Attributes),
LiveIedLogicalDeviceModel ld => ld.LogicalNodes.SelectMany(ln => ln.DataObjects)
.SelectMany(o => o.Attributes),
LiveIedDataSetModel set => ForDataSet(all, schema, set),
Expand Down
1 change: 1 addition & 0 deletions Models/IedBrowserStructuredDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
$"IEC DO declaration · {obj.Reference} · CDC {obj.InferredCdc}")
{
IsDataObject = true,
ReadObjectReference = obj.Reference,
ScalarComponents = scalarComponents,
PolarPlan = polarPlan,
PhasePlans = phasePlans,
Expand Down Expand Up @@ -309,7 +310,7 @@
var hasDirect = matching.attr is not null;
var attr = hasDirect ? matching.attr : groupArray[0].attr;
var descendants = groupArray.Where(x => x.parts.Length > pathIndex + 1).ToArray();
var fullPrefix = IedBrowserLiveValues.CanonicalReference(attr.ObjectReference) +

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 313 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.
"." + string.Join(".", groupArray[0].parts.Take(pathIndex + 1));
var polar = allowVector
? IedBrowserEngineeringComposite.BuildPolar(
Expand Down
100 changes: 100 additions & 0 deletions tests/ARSAS.Tests/M214SelectedDataObjectReadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using AR.Iec61850.Discovery;
using ArIED61850Tester.Models;

namespace ARSAS.Tests;

public sealed class M214SelectedDataObjectReadTests
{
private static LiveIedDataAttributeModel Attribute(string owner, string fc, string leaf)
=> new()
{
ObjectReference = owner, FunctionalConstraint = fc,
AttributePath = owner.Split('.')[1] + "." + leaf,
MmsReference = owner.Replace(".", "$" + fc + "$")
.Replace("$" + fc + "$", "$" + fc + "$") + "$" +
leaf.Replace('.', '$')
};

private static (LiveIedModelDiscoveryDocument Model, IedBrowserTreeRow Page)
Fixture()
{
const string first = "LD/CSWI1.Pos";
const string other = "LD/CSWI1.Mod";
var ln = new LiveIedLogicalNodeModel
{
Name = "CSWI1",
DataObjects =
[
new LiveIedDataObjectModel
{
Reference = first, Name = "Pos",
Attributes =
[
Attribute(first, "ST", "stVal"),
Attribute(first, "ST", "q"),
Attribute(first, "ST", "t"),
Attribute(first, "CF", "ctlModel"),
Attribute(first, "DC", "d"),
Attribute(first, "CO", "Oper")
]
},
new LiveIedDataObjectModel
{
Reference = other, Name = "Mod",
Attributes = [Attribute(other, "ST", "stVal")]
}
]
};
return (new LiveIedModelDiscoveryDocument
{
LogicalDevices = [new LiveIedLogicalDeviceModel
{
MmsDomain = "LD", LogicalNodes = [ln]
}]
}, new IedBrowserTreeRow("model/ln/LD/CSWI1", "CSWI1", "LN", 2, ln));
}

[Fact]
public void SelectedDoRead_ContainsAllReadOnlyFcAttributes_NotJustStVal()
{
var (model, page) = Fixture();
var targets = IedBrowserReadPlan.Build(
model, page, readAll: false, selectedDoReference: "LD/CSWI1.Pos");
Assert.Equal(5, targets.Count);
Assert.Contains(targets, x => x.Reference.EndsWith("$Pos$stVal") &&
x.FunctionalConstraint == "ST");
Assert.Contains(targets, x => x.Reference.EndsWith("$Pos$q"));
Assert.Contains(targets, x => x.Reference.EndsWith("$Pos$t"));
Assert.Contains(targets, x => x.Reference.EndsWith("$Pos$ctlModel") &&
x.FunctionalConstraint == "CF");
Assert.Contains(targets, x => x.Reference.EndsWith("$Pos$d") &&
x.FunctionalConstraint == "DC");
Assert.DoesNotContain(targets, x => x.Reference.Contains("$Mod$") ||
x.FunctionalConstraint == "CO");
}

[Fact]
public void DoFocusIsExact_AndCannotBorrowAnotherLogicalNodeOrBeReadAllShortcut()
{
var (model, page) = Fixture();
Assert.Empty(IedBrowserReadPlan.Build(
model, page, false, "LD/CSWI2.Pos"));
Assert.Equal(6, IedBrowserReadPlan.Build(model, page, false).Count);
// Read All remains a global explicit operation regardless of right-pane focus.
Assert.Equal(6, IedBrowserReadPlan.Build(
model, page, true, "LD/CSWI1.Pos").Count);
}

[Fact]
public void DataObjectReadFocusNeverAliasesPreferredValueLeaf()
{
var (model, page) = Fixture();
var ln = (LiveIedLogicalNodeModel)page.Payload!;
var pos = ln.DataObjects[0];
var detail = IedBrowserStructuredDetails.DataObject(pos, "ST");
Assert.Equal(pos.Reference, detail.ReadObjectReference);
Assert.True(detail.HasChildren);
Assert.NotEmpty(IedBrowserReadPlan.Build(
model, page, false, detail.ReadObjectReference));
}
}
Loading