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
38 changes: 30 additions & 8 deletions SmvViewerWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public partial class SmvViewerWindow : Window, INotifyPropertyChanged
private CancellationTokenSource? _captureCancellation;
private SmvSnapshotResult? _snapshot;
private string _statusText = string.Empty;
private string _captureStatusText = "Select an adapter and stream, then capture a bounded two-cycle snapshot.";
private string _captureStatusText = "Select an Ethernet adapter, then capture the first received SV stream or select a configured stream.";
private string _snapshotBadgeText = "No snapshot";
private string _snapshotSummaryText = "No Sampled Values snapshot has been captured yet.";
private string _snapshotEvidenceText = "The snapshot will report stream identity, payload shape, sample-counter continuity and a static raw waveform.";
Expand All @@ -52,10 +52,11 @@ public SmvViewerWindow(Iec61850MonitorDevice device)
foreach (var row in BuildRows(device))
Streams.Add(row);

SelectedStream = Streams.FirstOrDefault();
StatusText = Streams.Count == 0
? "No Sampled Value control block is configured in the opened SCL model or discovered from this IED."
: $"{Streams.Count:N0} Sampled Value stream definition(s) are available. Snapshot capture is passive and does not write to the IED.";
SelectedStream = Streams[0];
var configured = Streams.Count - 1;
StatusText = configured == 0
? "Ready for passive SV capture on Ethernet. Select the correct Npcap adapter and Capture to discover the first stream."
: $"Capture any received SV stream or choose one of {configured:N0} configured stream(s). No network frame is transmitted.";
DataContext = this;
}

Expand Down Expand Up @@ -127,9 +128,11 @@ public SmvStreamRow? SelectedStream
}

public string SelectedStreamDetail => SelectedStream == null
? "Select one stream before starting the bounded capture."
: $"{SelectedStream.Source} • {SelectedStream.ControlReference} • DataSet {SelectedStream.DataSetReference} • " +
$"APPID {SelectedStream.AppId} • {SelectedStream.MemberCount} member(s).";
? "Select an SV stream or the auto-discovery option."
: SelectedStream.Source == "Live Ethernet (auto)"
? "Automatically capture the first decodable SV stream arriving on the selected Ethernet adapter. No SCL, APPID, MAC or svID filter is required."
: $"{SelectedStream.Source} • {SelectedStream.ControlReference} • DataSet {SelectedStream.DataSetReference} • " +
$"APPID {SelectedStream.AppId} • {SelectedStream.MemberCount} member(s).";

private void Window_Loaded(object sender, RoutedEventArgs e)
{
Expand Down Expand Up @@ -556,6 +559,25 @@ private void Raise([CallerMemberName] string? propertyName = null)

public sealed class SmvStreamRow
{
/// <summary>
/// Passive first-stream discovery. The capture request normalizes "-"
/// into unrestricted APPID, MAC, svID and timebase hints.
/// </summary>
public static SmvStreamRow AnyReceived() => new()
{
Source = "Live Ethernet (auto)",
ControlReference = "First received SV stream",
StreamId = "-",
DataSetReference = "-",
AppId = "-",
DestinationMac = "-",
Vlan = "-",
SampleRate = "-",
SampleMode = "-",
NumberOfAsdu = "-",
MemberCount = 0
};

public string Source { get; init; } = string.Empty;
public string LiveKey { get; init; } = string.Empty;
public string ControlReference { get; init; } = string.Empty;
Expand Down
80 changes: 80 additions & 0 deletions tests/ARSAS.Tests/ArstackM3hSvInteropTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Buffers.Binary;
using System.Text;
using AR.Iec61850.SampledValues;
using Xunit;

namespace ARSAS.Tests;

/// <summary>
/// Checks that ARSAS's pinned native SV parser accepts the ARStack M3.H
/// reference layout, including priority-tagged VLAN ID 0.
/// This is a wire-decoder test, not evidence of physical Ethernet delivery.
/// </summary>
public sealed class ArstackM3hSvInteropTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SnapshotEngine_RecognizesM3h4I4VFrame(bool vlanPriorityTag)
{
var sampleData = new byte[64];
var analogValues = new[] { 2500, -1250, -1250, 0, 330000, -165000, -165000, 0 };
for (var i = 0; i < analogValues.Length; i++)
BinaryPrimitives.WriteInt32BigEndian(sampleData.AsSpan(i * 8, 4), analogValues[i]);

var asdu = Tlv(0x30,
Tlv(0x80, Encoding.ASCII.GetBytes("MU01_SV1")),
Tlv(0x82, U16(3999)),
Tlv(0x83, new byte[] { 0x00, 0x00, 0x00, 0x01 }),
Tlv(0x85, new byte[] { 0x02 }),
Tlv(0x87, sampleData),
Tlv(0x89, new byte[] { 0x02, 0x11, 0x22, 0xFF, 0xFE, 0x33, 0x44, 0x55 }));

var apdu = Tlv(0x60,
Tlv(0x80, new byte[] { 0x01 }),
Tlv(0xA2, asdu));
var payload = Join(U16(0x4000), U16((ushort)(8 + apdu.Length)),
U16(0), U16(0), apdu);
var ethernet = vlanPriorityTag
? Join(DestinationMac, SourceMac, U16(0x8100), U16(0x8000), U16(0x88BA), payload)
: Join(DestinationMac, SourceMac, U16(0x88BA), payload);

Assert.True(SampledValuesFrameParser.TryParseEthernetFrame(ethernet, out var frame));
Assert.Equal((ushort)0x4000, frame.AppId);
Assert.Equal(vlanPriorityTag, frame.Vlan is not null);
if (vlanPriorityTag)
Assert.Equal((ushort)0, frame.Vlan?.VlanId);
var decoded = Assert.Single(frame.Pdu.Asdus);
Assert.Equal("MU01_SV1", decoded.SvId);
Assert.Equal((ushort)3999, decoded.SampleCount);
Assert.Equal(64, decoded.SamplePayload.Length);
}

private static readonly byte[] DestinationMac = { 0x01, 0x0C, 0xCD, 0x04, 0x00, 0x01 };
private static readonly byte[] SourceMac = { 0x02, 0x11, 0x22, 0x33, 0x44, 0x55 };

private static byte[] U16(ushort number) => new[] { (byte)(number >> 8), (byte)number };

private static byte[] Tlv(byte tag, params byte[][] parts)
{
var content = Join(parts);
if (content.Length < 128)
return Join(new byte[] { tag, (byte)content.Length }, content);
if (content.Length < 256)
return Join(new byte[] { tag, 0x81, (byte)content.Length }, content);
return Join(new byte[] { tag, 0x82, (byte)(content.Length >> 8), (byte)content.Length }, content);
}

private static byte[] Join(params byte[][] arrays)
{
var size = arrays.Sum(a => a.Length);
var result = new byte[size];
var index = 0;
foreach (var array in arrays)
{
array.CopyTo(result, index);
index += array.Length;
}
return result;
}
}
20 changes: 20 additions & 0 deletions tests/ARSAS.Tests/SmvFirstReceivedStreamTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using ArIED61850Tester;

namespace ARSAS.Tests;

public sealed class SmvFirstReceivedStreamTests
{
[Fact]
public void NewOperatorCanReceiveAnySvWithoutMatchingSclIdentity()
{
var row = SmvStreamRow.AnyReceived();

Assert.Equal("Live Ethernet (auto)", row.Source);
Assert.Equal("-", row.AppId);
Assert.Equal("-", row.StreamId);
Assert.Equal("-", row.DestinationMac);
Assert.Equal("-", row.SampleRate);
Assert.Equal("-", row.SampleMode);
Assert.Equal(0, row.MemberCount);
}
}
Loading