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
47 changes: 47 additions & 0 deletions docs/arsubsv-field-mode-p4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# ArSubsv Field Mode P4

Field Mode is the practical receiver workflow for live IEC 61850 Sampled Values and offline PCAP/PCAPNG analysis.

The P4 application branch is rebuilt directly on merged `main` and pins the field-core engine by its exact commit SHA. The draft PR therefore contains only Field Mode changes and remains safe to test locally without merging.

## Five evidence axes

ArSubsv separates:

- `CAPTURE`: frames available to the application;
- `PROTOCOL`: Ethernet/SV APDU decoding;
- `STREAM`: sample-counter continuity and payload consistency;
- `CONFIGURATION`: observed traffic versus bound CID/SCD;
- `MEASUREMENT`: channel semantics, scaling, CT/VT context, and signal confidence.

A configuration mismatch does not make a clean protocol stream BAD. Unknown measurement semantics remain UNKNOWN rather than being presented as amperes or volts.

## Offline workflow

1. Open `.pcap` or `.pcapng`.
2. Select a discovered stream.
3. Review raw decoded values and continuity without SCL.
4. Import `.cid`, `.scd`, `.icd`, or `.iid` when available.
5. Review scored SCL binding and expected-versus-observed findings.
6. Enter explicit CT/VT context only from reviewed evidence.
7. Export a support bundle for reproducible review.

## Support bundle

The initial P4 bundle is metadata-only by default and contains:

- manifest and SHA-256 checksums;
- receiver evidence in Markdown and JSON;
- five-axis field summary;
- selected-stream diagnostics;
- measurement context when configured;
- application and engine revision evidence;
- SCL hash when an SCL file is loaded.

The full capture and original SCL are not copied silently. Capture-excerpt and privacy-selection UI are planned as the next tranche.

## Current limitations

- Known-injection comparison exists in ARIEC61850 but the ArSubsv entry dialog is not yet implemented.
- Quiet/noise-floor classification is evidence-based and non-destructive, but waveform minimum-axis presentation still needs local UI review.
- Real Ahmed Mohaisn PCAPNG/CID replay remains a local acceptance gate; deterministic tests do not replace real-device evidence.
10 changes: 5 additions & 5 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"schemaVersion": 1,
"repository": "masarray/ARIEC61850",
"ref": "main",
"commit": "0f8453182957900bc6d91287fb8177c8d9762188",
"pairedPullRequest": 45,
"updatedAt": "2026-07-27T07:30:00Z",
"purpose": "Pinned merged engine revision for paired local, CI, CodeQL, packaging, and release validation."
"ref": "agent/sv-field-core-p4",
"commit": "bcecea84b50c628ddce699f1ddafa966aa3255f8",
"pairedPullRequest": 47,
"updatedAt": "2026-07-28T09:30:00Z",
"purpose": "Pinned P4 field-core revision for ArSubsv PCAP/PCAPNG, layered health, signal-state, scored SCL binding, known-injection, and support-bundle validation."
}
17 changes: 11 additions & 6 deletions scripts/test-with-coverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ if ($NoRestore) {
}

# ARSVIN tests execute against the pinned sibling ARIEC61850 source of truth.
# The engine repository owns its full-suite percentage gate. This integration gate checks:
# The engine repository owns its full-suite percentage gate, including the P4 Field layer.
# This application integration gate checks:
# 1. the application-consumed protocol core remains strongly covered; and
# 2. the absolute amount of exercised production code does not regress when the engine grows.
$arguments += @(
Expand Down Expand Up @@ -99,6 +100,10 @@ function Test-IsProtocolCoreFile {
if ($path.Contains('/AR.Iec61850/Asn1/')) { return $true }
if ($path.Contains('/AR.Iec61850/Ethernet/')) { return $true }
if ($path.Contains('/AR.Iec61850/Transports/')) { return $true }

# P4 Field contracts are tested by ARIEC61850's own deterministic suite. Excluding them here
# prevents application coverage from being diluted merely because the shared engine grows.
if ($path.Contains('/AR.Iec61850/SampledValues/Field/')) { return $false }
if ($path.Contains('/AR.Iec61850/SampledValues/')) { return $true }

if ($path.Contains('/AR.Iec61850/Capture/')) {
Expand Down Expand Up @@ -145,9 +150,9 @@ Write-Host "ARIEC61850 instrumented lines: $overallLinesValid"
Write-Host "ARIEC61850 covered lines exercised by ARSVIN: $overallLinesCovered"
Write-Host "Minimum covered production lines: $MinimumWholeEngineCoveredLines"
Write-Host "Informational whole-engine line coverage: $overallLineCoverage%"
Write-Host "Protocol core files: $($coreFiles.Count)"
Write-Host "Protocol core lines: $coreLinesValid"
Write-Host "Protocol core covered lines: $coreLinesCovered"
Write-Host "Application-consumed protocol core files: $($coreFiles.Count)"
Write-Host "Application-consumed protocol core lines: $coreLinesValid"
Write-Host "Application-consumed protocol core covered lines: $coreLinesCovered"
Write-Host "Minimum protocol-core covered lines: $MinimumProtocolCoreCoveredLines"
Write-Host "Protocol core line coverage: $coreLineCoverage%"
Write-Host "Protocol core minimum percentage: $MinimumLineCoverage%"
Expand All @@ -157,15 +162,15 @@ if ($env:GITHUB_STEP_SUMMARY) {
@"
## ARSVIN integration coverage against pinned ARIEC61850

The ARIEC61850 repository owns the full-engine percentage gate. This paired gate measures the reusable code actually exercised by ARSVIN Publisher and ArSubsv.
ARIEC61850 owns the full-engine and P4 Field-layer test gates. This paired gate measures the reusable protocol core exercised directly by ARSVIN Publisher and ArSubsv.

| Metric | Result |
|---|---:|
| Whole `AR.Iec61850` instrumented lines | **$overallLinesValid** |
| Production lines exercised by ARSVIN | **$overallLinesCovered** |
| Minimum exercised production lines | **$MinimumWholeEngineCoveredLines** |
| Informational whole-engine line coverage | **$overallLineCoverage%** |
| Tested protocol-core files | **$($coreFiles.Count)** |
| Tested application-consumed protocol-core files | **$($coreFiles.Count)** |
| Protocol-core instrumented lines | **$coreLinesValid** |
| Protocol-core covered lines | **$coreLinesCovered** |
| Minimum protocol-core covered lines | **$MinimumProtocolCoreCoveredLines** |
Expand Down
185 changes: 185 additions & 0 deletions src/ARSVIN.Subscriber/KnownInjectionWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using AR.Iec61850.SampledValues.Field;
using ARSVIN.Subscriber.Models;

namespace ARSVIN.Subscriber;

public sealed class KnownInjectionWindow : Window
{
private readonly ComboBox _channel = new();
private readonly TextBox _expectedRms = new();
private readonly TextBox _amplitudeTolerance = new();
private readonly TextBox _expectedAngle = new();
private readonly TextBox _angleTolerance = new();
private readonly TextBox _expectedFrequency = new();
private readonly TextBox _frequencyTolerance = new();
private readonly TextBlock _measured = new();
private readonly IReadOnlyList<PhasorVector> _phasors;

public KnownInjectionWindow(IReadOnlyList<PhasorVector> phasors)
{
_phasors = phasors?.Where(item => item.IsValid).ToArray()
?? throw new ArgumentNullException(nameof(phasors));

Title = "Validate Known Injection";
Width = 520;
Height = 500;
MinWidth = 480;
MinHeight = 460;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.CanResizeWithGrip;
Content = BuildContent();

_channel.ItemsSource = _phasors;
_channel.DisplayMemberPath = nameof(PhasorVector.RmsText);
_channel.SelectedIndex = _phasors.Count > 0 ? 0 : -1;
_channel.SelectionChanged += (_, _) => RefreshMeasured();
RefreshMeasured();
}

public SvKnownInjectionExpectation? Expectation { get; private set; }
public SvKnownInjectionMeasurement? Measurement { get; private set; }
public SvKnownInjectionComparison? Comparison { get; private set; }

private UIElement BuildContent()
{
var root = new Grid { Margin = new Thickness(18) };
for (var index = 0; index < 9; index++)
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(185) });
root.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

AddRow(root, 0, "Channel / measured phasor", _channel);
AddRow(root, 1, "Measured", _measured);
AddRow(root, 2, "Expected RMS", _expectedRms);
AddRow(root, 3, "Amplitude tolerance (%)", _amplitudeTolerance);
AddRow(root, 4, "Expected angle (deg)", _expectedAngle);
AddRow(root, 5, "Angle tolerance (deg)", _angleTolerance);
AddRow(root, 6, "Expected frequency (Hz)", _expectedFrequency);
AddRow(root, 7, "Frequency tolerance (Hz)", _frequencyTolerance);

var note = new TextBlock
{
Text = "A tolerance is optional. Without tolerance, ArSubsv reports REVIEW with numerical error rather than inventing PASS/FAIL. Values use the currently displayed engineering domain and provenance.",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 12, 0, 12),
Foreground = System.Windows.Media.Brushes.DimGray
};
Grid.SetRow(note, 8);
Grid.SetColumnSpan(note, 2);
root.Children.Add(note);

var buttons = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 12, 0, 0)
};
var validate = new Button { Content = "Validate", MinWidth = 96, Padding = new Thickness(12, 6, 12, 6), IsDefault = true };
validate.Click += Validate_Click;
var cancel = new Button { Content = "Cancel", MinWidth = 88, Padding = new Thickness(12, 6, 12, 6), Margin = new Thickness(8, 0, 0, 0), IsCancel = true };
buttons.Children.Add(validate);
buttons.Children.Add(cancel);
Grid.SetRow(buttons, 10);
Grid.SetColumnSpan(buttons, 2);
root.Children.Add(buttons);

return root;
}

private static void AddRow(Grid grid, int row, string label, UIElement editor)
{
var labelBlock = new TextBlock
{
Text = label,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 5, 12, 5)
};
Grid.SetRow(labelBlock, row);
Grid.SetColumn(labelBlock, 0);
grid.Children.Add(labelBlock);

if (editor is FrameworkElement element)
element.Margin = new Thickness(0, 5, 0, 5);
Grid.SetRow(editor, row);
Grid.SetColumn(editor, 1);
grid.Children.Add(editor);
}

private void RefreshMeasured()
{
_measured.Text = _channel.SelectedItem is PhasorVector selected
? $"{selected.Rms:0.######} {selected.Unit} RMS · {selected.AngleDegrees:0.###}° · {selected.Kind}"
: "No validated phasor is available for this stream.";
}

private void Validate_Click(object sender, RoutedEventArgs e)
{
if (_channel.SelectedItem is not PhasorVector selected)
{
MessageBox.Show(this, "No valid phasor is available. Resolve SCL mapping, timebase, and waveform confidence first.", Title, MessageBoxButton.OK, MessageBoxImage.Information);
return;
}

if (!TryParseRequired(_expectedRms.Text, out var expectedRms) || expectedRms < 0)
{
MessageBox.Show(this, "Expected RMS must be a finite non-negative number.", Title, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}

if (!TryParseOptional(_amplitudeTolerance.Text, out var amplitudeTolerance) ||
!TryParseOptional(_expectedAngle.Text, out var expectedAngle) ||
!TryParseOptional(_angleTolerance.Text, out var angleTolerance) ||
!TryParseOptional(_expectedFrequency.Text, out var expectedFrequency) ||
!TryParseOptional(_frequencyTolerance.Text, out var frequencyTolerance))
{
MessageBox.Show(this, "One or more optional values are not valid numbers.", Title, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}

Expectation = new SvKnownInjectionExpectation
{
Channel = selected.Channel,
ExpectedRms = expectedRms,
Unit = selected.Unit,
Domain = "display",
ExpectedAngleDegrees = expectedAngle,
ExpectedFrequencyHz = expectedFrequency,
AmplitudeTolerancePercent = amplitudeTolerance,
AngleToleranceDegrees = angleTolerance,
FrequencyToleranceHz = frequencyTolerance
};
Measurement = new SvKnownInjectionMeasurement
{
MeasuredRms = selected.Rms,
MeasuredAngleDegrees = selected.AngleDegrees,
MeasuredFrequencyHz = null
};
Comparison = SvKnownInjectionComparator.Compare(Expectation, Measurement);
DialogResult = true;
}

private static bool TryParseRequired(string text, out double value)
{
if (double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out value) ||
double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out value))
return double.IsFinite(value);
value = 0;
return false;
}

private static bool TryParseOptional(string text, out double? value)
{
value = null;
if (string.IsNullOrWhiteSpace(text))
return true;
if (!TryParseRequired(text, out var parsed))
return false;
value = parsed;
return true;
}
}
Loading
Loading