Skip to content
Closed
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
8 changes: 6 additions & 2 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -607,12 +607,16 @@
</Border>
</Grid>

<DataGrid Grid.Row="2" ItemsSource="{Binding SelectedDevice.Points}" IsReadOnly="True"
<DataGrid x:Name="ExplorerLiveValuesGrid" Grid.Row="2"
ItemsSource="{Binding SelectedDevice.Points}" IsReadOnly="True"
Loaded="ExplorerLiveGrid_Loaded"
Style="{StaticResource ModernDataGrid}" FrozenColumnCount="2"
MinHeight="0" VerticalAlignment="Stretch"
EnableRowVirtualization="True" EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True" ScrollViewer.HorizontalScrollBarVisibility="Auto">
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow" BasedOn="{StaticResource {x:Type DataGridRow}}">
<Style.Triggers>
Expand Down
19 changes: 19 additions & 0 deletions Services/NativeIec61850Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4360,6 +4360,25 @@ private static string NormalizeFunctionalConstraint(string functionalConstraint,

private Iec61850ReadValue? ProjectReadValue(ArMms.MmsDataValue? value, string dataType, string readReference, string requestedReference)
{
// A successful, *typed* MMS OCTET STRING of length zero is a
// legitimate observation, not a missing Read or an "unavailable"
// scalar. This matters for optional origin.orIdent attributes.
// Do not apply to VisibleString, Unknown BER or malformed responses.
if (value?.Kind == ArMms.MmsDataKind.OctetString &&
value.RawValue.Count == 0 &&
(dataType.StartsWith("Octet", StringComparison.OrdinalIgnoreCase) ||
dataType.Equals("OctetString", StringComparison.OrdinalIgnoreCase)))
{
return new Iec61850ReadValue
{
Value = "Empty OCTET STRING (0 bytes)",
DisplayValue = "Empty OCTET STRING (0 bytes)",
SourceReference = requestedReference,
ReadReference = readReference,
Projection = "typed-empty-octet-string"
};
}

// Explicit object-level DataSet members must be projected before generic
// first-scalar semantic binding. Otherwise a three-phase THD object collapses
// to phase A and loses the B/C values carried by the same MMS structure.
Expand Down
51 changes: 51 additions & 0 deletions tests/ARSAS.Tests/P0DLiveSignalViewportContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Xml.Linq;

namespace ARSAS.Tests;

/// <summary>
/// Compile-time WPF layout contract; runtime DPI screenshot acceptance remains
/// a separate Windows integration test, never asserted by XML inspection.
/// </summary>
public sealed class P0DLiveSignalViewportContractTests
{
[Fact]
public void LiveSignalViewport_RemainsInStarRegion_WithIndependentScrolling()
{
var root = FindRepositoryRoot();
var xaml = XDocument.Load(Path.Combine(root, "MainWindow.xaml"));
var grid = xaml.Descendants().Single(x => x.Name.LocalName == "DataGrid" &&
(string?)x.Attribute(XName.Get("Name",
"http://schemas.microsoft.com/winfx/2006/xaml")) == "ExplorerLiveValuesGrid");
Assert.Equal("2", (string?)grid.Attribute("Grid.Row"));
Assert.Equal("Stretch", (string?)grid.Attribute("VerticalAlignment"));
Assert.Equal("0", (string?)grid.Attribute("MinHeight"));
Assert.Equal("Auto", (string?)grid.Attribute(XName.Get(
"ScrollViewer.VerticalScrollBarVisibility",
"http://schemas.microsoft.com/winfx/2006/xaml/presentation")));
Assert.Equal("True", (string?)grid.Attribute(XName.Get(
"VirtualizingPanel.IsVirtualizing",
"http://schemas.microsoft.com/winfx/2006/xaml/presentation")));
Assert.Equal("Recycling", (string?)grid.Attribute(XName.Get(
"VirtualizingPanel.VirtualizationMode",
"http://schemas.microsoft.com/winfx/2006/xaml/presentation")));
var container = grid.Parent;
Assert.NotNull(container);
var definitions = container!.Elements().First(x =>
x.Name.LocalName == "Grid.RowDefinitions")
.Elements().ToArray();
Assert.Equal("*", (string?)definitions[2].Attribute("Height"));
var outerGrid = container.Parent?.Parent;
Assert.NotNull(outerGrid);
Assert.Equal("Grid", outerGrid!.Name.LocalName);
}

private static string FindRepositoryRoot()
{
var folder = new DirectoryInfo(AppContext.BaseDirectory);
while (folder is not null &&
!File.Exists(Path.Combine(folder.FullName, "MainWindow.xaml")))
folder = folder.Parent;
return folder?.FullName ?? throw new InvalidOperationException(
"Test requires the tracked MainWindow.xaml.");
}
}
49 changes: 49 additions & 0 deletions tests/ARSAS.Tests/P0DTypedEmptyOctetEvidenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using AR.Iec61850.Mms;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;
using System.Reflection;

namespace ARSAS.Tests;

public sealed class P0DTypedEmptyOctetEvidenceTests
{
[Fact]
public void ExactTypedEmptyOriginIdentifier_IsObserved_NotReadFailed()
{
var client = new NativeIec61850Client();
var method = typeof(NativeIec61850Client).GetMethod(
"ProjectReadValue", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(method);
const string reference = "LABPROT/LLN0.Mod.origin.orIdent";
var value = MmsDataValue.OctetString(ReadOnlySpan<byte>.Empty);
var read = method!.Invoke(client, [value, "Octet64", reference, reference])
as Iec61850ReadValue;
Assert.NotNull(read);
Assert.Equal("typed-empty-octet-string", read.Projection);
Assert.Equal("Empty OCTET STRING (0 bytes)", read.ToString());
Assert.True(IedBrowserReadValuePolicy.TryAcceptDisplay(
read.ToString(), out _, out _));
var ledger = new IedBrowserLiveValues();
var association = ledger.BeginAssociation();
Assert.True(ledger.PublishForAssociation(association, reference, "ST",
read.ToString(), "", "", "MMS Read"));
Assert.True(ledger.TryGet(reference, "ST", out var observed));
Assert.Equal("Empty OCTET STRING (0 bytes)", observed.Value);
}

[Fact]
public void EmptyTextOrUnknownTag_NeverGetsOctetWireAuthority()
{
using var client = new NativeIec61850Client();

Check failure on line 37 in tests/ARSAS.Tests/P0DTypedEmptyOctetEvidenceTests.cs

View workflow job for this annotation

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

'NativeIec61850Client': type used in a using statement must implement 'System.IDisposable'. Did you mean 'await using' rather than 'using'?

Check failure on line 37 in tests/ARSAS.Tests/P0DTypedEmptyOctetEvidenceTests.cs

View workflow job for this annotation

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

'NativeIec61850Client': type used in a using statement must implement 'System.IDisposable'. Did you mean 'await using' rather than 'using'?
var method = typeof(NativeIec61850Client).GetMethod(
"ProjectReadValue", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(method);
const string reference = "LABPROT/LLN0.Mod.origin.orIdent";
var emptyVisible = method!.Invoke(client,
[MmsDataValue.VisibleString(""), "Octet64", reference, reference])
as Iec61850ReadValue;
Assert.NotEqual("typed-empty-octet-string", emptyVisible?.Projection);
Assert.False(IedBrowserReadValuePolicy.TryAcceptDisplay(
"", out _, out _));
}
}
Loading