Skip to content
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.";
Comment on lines +190 to +194

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the no-command preflight result

When the live preflight finds the process already at the requested state, NativeIec61850Client.ControlAlreadyAtRequestedState returns IsSuccess = true, ServiceAccepted = false, and CompletionState = "NotSent". This branch nevertheless replaces its accurate “Already at requested state” stage with “Command accepted” and asserts that the control service accepted a command, misleading operators even though no SBO/Operate request was sent. Gate this wording on actual service acceptance rather than general success.

AGENTS.md reference: AGENTS.md:L143-L153

Useful? React with 👍 / 👎.

}
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.

P1 Badge Drive easing to the final analysis sample

When consecutive cursor results have the same vector topology, this replaces the authoritative target with one exponentially blended sample, but no animation/render callback invokes SmoothVectors again. After the final native request completes, ExecuteAnalysisRequestAsync stops the composition pump, so the displayed magnitudes, angles, and legend values remain permanently between the previous and final DFT frames; ComtradeHarmonicsWorkstationView.ShowSpectra has the same behavior. Preserve the exact target and continue presentation frames until it is committed, or snap the final scrub result.

AGENTS.md reference: AGENTS.md:L94-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
Loading
Loading