Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dcab71f
trial: migrate protocol-only trusted-SCL lane
masarray Sep 15, 2026
8feca63
trial: pin canonical green SCL engine
masarray Sep 15, 2026
e2336b5
trial: add trusted SCL static report adapter
masarray Sep 15, 2026
027f22f
ci: apply isolated trusted SCL runtime wiring
masarray Sep 15, 2026
81fb42f
ci: stage trusted SCL runtime patch script
masarray Sep 15, 2026
f4fa0a2
ci: simplify trusted SCL runtime patch runner
masarray Sep 15, 2026
fe603c0
trial: wire trusted SCL golden path into runtime
github-actions[bot] Sep 15, 2026
3705dc4
ci: stage trusted SCL compile fix
masarray Sep 15, 2026
ffa72b5
ci: apply trusted SCL compile fix
masarray Sep 15, 2026
eb6a555
trial: fix trusted SCL directory out contract
github-actions[bot] Sep 15, 2026
8e9edbb
docs: define trusted SCL physical trial gate
masarray Sep 15, 2026
0aeec7f
ux: restore thread-safe smart IED onboarding
masarray Sep 15, 2026
10c537a
test: guard smart onboarding and dispatcher safety
masarray Sep 15, 2026
3d517c4
ux: install smart IED onboarding on golden-wire build
masarray Sep 15, 2026
5feec23
engine: repin to quoted AP-title compatible ARIEC
masarray Sep 15, 2026
a149b3e
fix: retain trusted SCL initial values and indexed RCB identity
masarray Sep 15, 2026
886828b
build: pin indexed trusted-SCL engine
masarray Sep 15, 2026
b602440
fix(reporting): request one-shot GI for trusted SCL static RCBs
masarray Sep 15, 2026
e3f527e
test(reporting): update trusted SCL GI golden-wire contract
masarray Sep 15, 2026
c6e2b8a
chore(engine): pin fail-closed GI trial engine
masarray Sep 15, 2026
5dafdd1
chore(engine): advance GI trial pin after analyzer fix
masarray Sep 15, 2026
131a17d
fix(engine-lock): preserve reviewed ancestry on GI trial pin
masarray Sep 15, 2026
a6628a4
chore: transplant targeted fix onto v1.6.36 field-proven branch
masarray Sep 16, 2026
cd1e172
fix(v1636): real IED feedback and smooth analyzers
github-actions[bot] Sep 16, 2026
7d81322
Repin combined v1.6.36 candidate to green ARIEC convergence
masarray Sep 16, 2026
579b781
Align combined field trial with ARIEC convergence and PR 312 authority
masarray Sep 16, 2026
cd24a3b
chore(ci): apply v1.6.36 real IED authority root fix
masarray Sep 16, 2026
e442ecf
Merge PR #314: qualify ARIEC convergence on v1.6.36 combined head
masarray Sep 16, 2026
7f6a63a
fix(control): keep process state authoritative to IED reports
masarray Sep 16, 2026
f1700cc
test: align reconnect regression with independent IED report authority
masarray Sep 16, 2026
4f5c274
Merge PR #312: real IED process authority and smooth analyzers
masarray Sep 16, 2026
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
21 changes: 21 additions & 0 deletions App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

Expand All @@ -22,6 +23,20 @@ protected override void OnStartup(StartupEventArgs e)
WindowsApplicationIdentity.Apply();
base.OnStartup(e);

if (SclSafeTrialCommand.IsRequested(e.Args))
{
var trial = SclSafeTrialRunner.RunAsync(e.Args, CancellationToken.None)
.GetAwaiter()
.GetResult();
MessageBox.Show(
$"{trial.Message}\n\nEvidence: {trial.EvidencePath}",
trial.IsSuccess ? "SCL Safe Trial — PASS" : "SCL Safe Trial — NOT PROVEN",
MessageBoxButton.OK,
trial.IsSuccess ? MessageBoxImage.Information : MessageBoxImage.Warning);
Shutdown(trial.ExitCode);
return;
}

// P2 installs one calm industrial visual system before StartupUri materializes.
// Existing XAML keeps its semantic resource keys while the overlay replaces
// glare-heavy white/blue surfaces with Blue Steel + Light Greige equivalents.
Expand Down Expand Up @@ -64,6 +79,12 @@ protected override void OnActivated(EventArgs e)
// tiny P2 adapter when windows activate so newly opened FAT workspaces also
// inherit the selected industrial theme without touching engine workflows.
P2BlueSteelGreigeUx.ApplyToOpenWindows(this);

// Keep onboarding task-first while preserving the existing protocol handlers:
// Add IED exposes SCL/CID/ICD or IP discovery, and bulk connect appears only
// when multiple loaded IEDs make the action useful. The behavior is dispatcher-safe.
if (Current?.MainWindow is MainWindow mainWindow)
SmartIedOnboardingBehavior.Install(mainWindow);
}

private void InstallP2BlueSteelGreigeTheme()
Expand Down
10 changes: 6 additions & 4 deletions ControlCommandWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,12 @@ private async void SendCommand_Click(object sender, RoutedEventArgs e)
},
_cancellation.Token);

CommandStage = result.Stage;
CommandStage = result.IsSuccess && !TestMode ? "Command accepted" : result.Stage;
CommandStatus = BuildCommandResultText(result);
if (!string.IsNullOrWhiteSpace(result.FeedbackValue) && result.FeedbackValue != "-")
CurrentValue = result.FeedbackValue;
if (result.IsSuccess && !TestMode)
{
CommandStatus += " Command accepted by the IEC 61850 control service. Waiting for independent IED process feedback; monitored stVal is not changed from the command path.";
}
SetResultTone(result.IsSuccess ? "Success" : "Error");
}
catch (OperationCanceledException)
Expand Down Expand Up @@ -312,7 +314,7 @@ private static string BuildCommandResultText(Iec61850ControlCommandResult result
if (!string.IsNullOrWhiteSpace(result.ElapsedText) && result.ElapsedText != "-")
details.Add($"Control service: {result.ElapsedText}.");
if (!string.IsNullOrWhiteSpace(result.FeedbackElapsedText) && result.FeedbackElapsedText != "-")
details.Add($"Process feedback: {result.FeedbackElapsedText}.");
details.Add($"Control-side feedback verification: {result.FeedbackElapsedText}. This does not overwrite monitored stVal.");
if (!string.IsNullOrWhiteSpace(result.TotalElapsedText) && result.TotalElapsedText != "-")
details.Add($"Total: {result.TotalElapsedText}.");
return string.Join(" ", details.Where(text => !string.IsNullOrWhiteSpace(text)));
Expand Down
88 changes: 87 additions & 1 deletion Controls/ComtradeHarmonicsWorkstationView.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Input;
Expand Down Expand Up @@ -65,7 +66,10 @@ public sealed class ComtradeHarmonicsWorkstationView : FrameworkElement
private static readonly Pen FooterDividerPen = FreezePen(Color.FromRgb(233, 237, 243), 1);
private static readonly string[] OrderLabels = CreateOrderLabels();

private const double PresentationTimeConstantMs = 92.0;
private IReadOnlyList<ComtradeHarmonicOverviewSpectrum> _spectra = Array.Empty<ComtradeHarmonicOverviewSpectrum>();
private ComtradeHarmonicOverviewSpectrum[] _smoothedSpectra = Array.Empty<ComtradeHarmonicOverviewSpectrum>();
private long _lastPresentationTimestamp;
private PreparedSpectrumRow[] _preparedRows = Array.Empty<PreparedSpectrumRow>();
private int _maximumDisplayedOrder = -1;
private string _title = "Harmonics";
Expand Down Expand Up @@ -105,7 +109,14 @@ internal void ShowSpectra(
{
_title = title ?? string.Empty;
_subtitle = subtitle ?? string.Empty;
_spectra = spectra ?? Array.Empty<ComtradeHarmonicOverviewSpectrum>();
var targetSpectra = spectra ?? Array.Empty<ComtradeHarmonicOverviewSpectrum>();
var now = Stopwatch.GetTimestamp();
var elapsedMilliseconds = _lastPresentationTimestamp == 0
? double.PositiveInfinity
: Stopwatch.GetElapsedTime(_lastPresentationTimestamp, now).TotalMilliseconds;
_lastPresentationTimestamp = now;
_smoothedSpectra = SmoothSpectra(_smoothedSpectra, targetSpectra, elapsedMilliseconds);
_spectra = _smoothedSpectra;
_maximumDisplayedOrder = ResolveMaximumDisplayedOrder(_spectra);
_selectedOrder = Math.Clamp(_selectedOrder, 0, Math.Max(0, _maximumDisplayedOrder));
_preparedRows = PrepareRows(_spectra, _maximumDisplayedOrder);
Expand All @@ -118,6 +129,8 @@ internal void ShowMessage(string title, string message)
_title = title ?? string.Empty;
_subtitle = message ?? string.Empty;
_spectra = Array.Empty<ComtradeHarmonicOverviewSpectrum>();
_smoothedSpectra = Array.Empty<ComtradeHarmonicOverviewSpectrum>();
_lastPresentationTimestamp = 0;
_preparedRows = Array.Empty<PreparedSpectrumRow>();
_maximumDisplayedOrder = -1;
_rowTargets.Clear();
Expand Down Expand Up @@ -296,6 +309,79 @@ private void DrawFooter(DrawingContext dc, Rect bounds, double dpi)
FooterRateBrush, new Point(bounds.Right - 16, y), dpi);
}

private static ComtradeHarmonicOverviewSpectrum[] SmoothSpectra(
IReadOnlyList<ComtradeHarmonicOverviewSpectrum> previous,
IReadOnlyList<ComtradeHarmonicOverviewSpectrum> target,
double elapsedMilliseconds)
{
if (target.Count == 0)
return Array.Empty<ComtradeHarmonicOverviewSpectrum>();

var topologyMatches = previous.Count == target.Count && previous.Count > 0;
if (topologyMatches)
{
for (var spectrumIndex = 0; spectrumIndex < target.Count; spectrumIndex++)
{
var before = previous[spectrumIndex];
var next = target[spectrumIndex];
if (!string.Equals(before.SignalName, next.SignalName, StringComparison.Ordinal) ||
!string.Equals(before.Units, next.Units, StringComparison.Ordinal) ||
before.Bins.Count != next.Bins.Count)
{
topologyMatches = false;
break;
}
for (var binIndex = 0; binIndex < next.Bins.Count; binIndex++)
{
if (before.Bins[binIndex].Order != next.Bins[binIndex].Order)
{
topologyMatches = false;
break;
}
}
if (!topologyMatches) break;
}
}

var result = new ComtradeHarmonicOverviewSpectrum[target.Count];
for (var spectrumIndex = 0; spectrumIndex < target.Count; spectrumIndex++)
{
var next = target[spectrumIndex];
if (!topologyMatches)
{
result[spectrumIndex] = next with { Bins = next.Bins.ToArray() };
continue; // First sample/channel-set change snaps; never invent a ramp from zero.
}

var before = previous[spectrumIndex];
var bins = new ComtradeHarmonicDisplayBin[next.Bins.Count];
for (var binIndex = 0; binIndex < bins.Length; binIndex++)
{
var previousBin = before.Bins[binIndex];
var targetBin = next.Bins[binIndex];
bins[binIndex] = new ComtradeHarmonicDisplayBin(
targetBin.Order,
PresentationEasingMath.Smooth(previousBin.MagnitudeRms, targetBin.MagnitudeRms, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.Smooth(previousBin.PercentOfFundamental, targetBin.PercentOfFundamental, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.SmoothAngleDegrees(previousBin.AngleDegrees, targetBin.AngleDegrees, elapsedMilliseconds, PresentationTimeConstantMs));
}

result[spectrumIndex] = new ComtradeHarmonicOverviewSpectrum(
next.SignalName,
next.Units,
PresentationEasingMath.Smooth(before.DcComponent, next.DcComponent, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.Smooth(before.FundamentalRms, next.FundamentalRms, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.Smooth(before.ThdPercent, next.ThdPercent, elapsedMilliseconds, PresentationTimeConstantMs),
next.DominantOrder,
PresentationEasingMath.Smooth(before.DominantRms, next.DominantRms, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.Smooth(before.DominantPercent, next.DominantPercent, elapsedMilliseconds, PresentationTimeConstantMs),
next.EstimatedSampleRateHz,
next.MaximumResolvableOrder,
bins);
}
return result;
}

private static PreparedSpectrumRow[] PrepareRows(
IReadOnlyList<ComtradeHarmonicOverviewSpectrum> spectra,
int maximumOrder)
Expand Down
67 changes: 65 additions & 2 deletions Controls/ComtradePhasorView.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Media;
using ArIED61850Tester.Services;

namespace ArIED61850Tester.Controls;

Expand Down Expand Up @@ -34,9 +36,13 @@ public sealed class ComtradePhasorView : FrameworkElement

private PreparedPhasorPanel _voltagePanel = PreparedPhasorPanel.Empty;
private PreparedPhasorPanel _currentPanel = PreparedPhasorPanel.Empty;
private const double PresentationTimeConstantMs = 78.0;
private string _headerLabel = "Fundamental phasors at C1";
private string _referenceDetail = "Select a valid analysis reference";
private string _message = string.Empty;
private ComtradePhasorVector[] _smoothedVoltageVectors = Array.Empty<ComtradePhasorVector>();
private ComtradePhasorVector[] _smoothedCurrentVectors = Array.Empty<ComtradePhasorVector>();
private long _lastPresentationTimestamp;

internal void ShowPhasors(
string referenceLabel,
Expand All @@ -47,8 +53,17 @@ internal void ShowPhasors(
var resolvedReference = string.IsNullOrWhiteSpace(referenceLabel) ? "Reference" : referenceLabel;
_headerLabel = $"Fundamental phasors at {resolvedReference}";
_referenceDetail = referenceDetail ?? string.Empty;
_voltagePanel = PreparePanel(voltageVectors);
_currentPanel = PreparePanel(currentVectors);

var now = Stopwatch.GetTimestamp();
var elapsedMilliseconds = _lastPresentationTimestamp == 0
? double.PositiveInfinity
: Stopwatch.GetElapsedTime(_lastPresentationTimestamp, now).TotalMilliseconds;
_lastPresentationTimestamp = now;

_smoothedVoltageVectors = SmoothVectors(_smoothedVoltageVectors, voltageVectors, elapsedMilliseconds);
_smoothedCurrentVectors = SmoothVectors(_smoothedCurrentVectors, currentVectors, elapsedMilliseconds);
_voltagePanel = PreparePanel(_smoothedVoltageVectors);
_currentPanel = PreparePanel(_smoothedCurrentVectors);
Comment on lines +63 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Snap the analyzer to the final scrub target

Each ShowPhasors call advances the eased values only once, and the final scrub request is rendered through the same interpolation with no subsequent timer or exact-target commit. If the user releases the cursor shortly after the preceding frame, the displayed magnitude and angle remain permanently between the old and final engineering values until another interaction occurs; the harmonic view has the same one-shot behavior in ShowSpectra. The final request needs to snap to the target or schedule bounded continuation frames that end with the exact value.

AGENTS.md reference: AGENTS.md:L96-L106

Useful? React with 👍 / 👎.

_message = string.Empty;
InvalidateVisual();
}
Expand All @@ -60,6 +75,9 @@ internal void ShowMessage(string title, string message)
_referenceDetail = message ?? string.Empty;
_voltagePanel = PreparedPhasorPanel.Empty;
_currentPanel = PreparedPhasorPanel.Empty;
_smoothedVoltageVectors = Array.Empty<ComtradePhasorVector>();
_smoothedCurrentVectors = Array.Empty<ComtradePhasorVector>();
_lastPresentationTimestamp = 0;
_message = message ?? string.Empty;
InvalidateVisual();
}
Expand Down Expand Up @@ -111,6 +129,51 @@ protected override void OnRender(DrawingContext dc)
}
}

private static ComtradePhasorVector[] SmoothVectors(
IReadOnlyList<ComtradePhasorVector> previous,
IReadOnlyList<ComtradePhasorVector>? target,
double elapsedMilliseconds)
{
if (target is null || target.Count == 0)
return Array.Empty<ComtradePhasorVector>();

var topologyMatches = previous.Count == target.Count && previous.Count > 0;
if (topologyMatches)
{
for (var index = 0; index < target.Count; index++)
{
if (!string.Equals(previous[index].Label, target[index].Label, StringComparison.Ordinal) ||
!string.Equals(previous[index].Phase, target[index].Phase, StringComparison.Ordinal) ||
!string.Equals(previous[index].Units, target[index].Units, StringComparison.Ordinal))
{
topologyMatches = false;
break;
}
}
}

var output = new ComtradePhasorVector[target.Count];
if (!topologyMatches)
{
for (var index = 0; index < target.Count; index++)
output[index] = target[index];
return output; // First sample/topology change snaps: no artificial ramp from zero.
}

for (var index = 0; index < target.Count; index++)
{
var before = previous[index];
var next = target[index];
output[index] = new ComtradePhasorVector(
next.Label,
next.Phase,
next.Units,
PresentationEasingMath.Smooth(before.MagnitudeRms, next.MagnitudeRms, elapsedMilliseconds, PresentationTimeConstantMs),
PresentationEasingMath.SmoothAngleDegrees(before.AngleDegrees, next.AngleDegrees, elapsedMilliseconds, PresentationTimeConstantMs));
}
return output;
}

private static PreparedPhasorPanel PreparePanel(IReadOnlyList<ComtradePhasorVector>? source)
{
if (source is null || source.Count == 0)
Expand Down
29 changes: 29 additions & 0 deletions Services/Iec61850ConnectionPathPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using ArIED61850Tester.Models;

namespace ArIED61850Tester.Services;

public enum Iec61850ConnectionPath
{
FullDiscovery,
CachedLiveModel,
SclAssisted
}

/// <summary>
/// Pure routing policy. SCL design authority is distinct from a saved live-discovery
/// cache: Play/Connect uses the SCL-assisted path, while Re-scan remains an explicit
/// caller of full discovery.
/// </summary>
public static class Iec61850ConnectionPathPolicy
{
public static Iec61850ConnectionPath SelectForFastConnect(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);

if (device.HasSclDesignModel)
return Iec61850ConnectionPath.SclAssisted;
if (device.HasDiscoveryCache && device.Signals.Count > 0)
return Iec61850ConnectionPath.CachedLiveModel;
return Iec61850ConnectionPath.FullDiscovery;
}
}
Loading
Loading