diff --git a/SmvViewerWindow.xaml.cs b/SmvViewerWindow.xaml.cs
index 5778b613b..6113a54f8 100644
--- a/SmvViewerWindow.xaml.cs
+++ b/SmvViewerWindow.xaml.cs
@@ -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.";
@@ -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;
}
@@ -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)
{
@@ -556,6 +559,25 @@ private void Raise([CallerMemberName] string? propertyName = null)
public sealed class SmvStreamRow
{
+ ///
+ /// Passive first-stream discovery. The capture request normalizes "-"
+ /// into unrestricted APPID, MAC, svID and timebase hints.
+ ///
+ 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;
diff --git a/tests/ARSAS.Tests/ArstackM3hSvInteropTests.cs b/tests/ARSAS.Tests/ArstackM3hSvInteropTests.cs
new file mode 100644
index 000000000..48893dc9f
--- /dev/null
+++ b/tests/ARSAS.Tests/ArstackM3hSvInteropTests.cs
@@ -0,0 +1,80 @@
+using System.Buffers.Binary;
+using System.Text;
+using AR.Iec61850.SampledValues;
+using Xunit;
+
+namespace ARSAS.Tests;
+
+///
+/// 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.
+///
+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;
+ }
+}
diff --git a/tests/ARSAS.Tests/SmvFirstReceivedStreamTests.cs b/tests/ARSAS.Tests/SmvFirstReceivedStreamTests.cs
new file mode 100644
index 000000000..2afa619f4
--- /dev/null
+++ b/tests/ARSAS.Tests/SmvFirstReceivedStreamTests.cs
@@ -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);
+ }
+}