Repository navigation
Expand file tree
/
Copy pathComtradeWorkspaceWindow.P1D5Display.cs
More file actions
250 lines (214 loc) · 10.3 KB
/
Copy pathComtradeWorkspaceWindow.P1D5Display.cs
File metadata and controls
250 lines (214 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ArIED61850Tester.Controls;
using ArIED61850Tester.Services;
namespace ArIED61850Tester;
public partial class ComtradeWorkspaceWindow
{
private const int P1D5RepresentationSecondary = 1;
private const int P1D5RepresentationPrimary = 2;
private enum P1D5WaveformTraceMode
{
Instantaneous,
Rms
}
private readonly Dictionary<uint, ComtradeAnalogSemantics?> _p1d5SemanticsCache = new();
private P1D5WaveformTraceMode _p1d5WaveformTraceMode = P1D5WaveformTraceMode.Instantaneous;
private int _p1d5ValueRepresentation = P1D5RepresentationSecondary;
private bool _p1d5PresentationRefreshRunning;
private bool? _p1d5HasConvertibleAnalog;
private bool P1D5IsRmsTrace => _p1d5WaveformTraceMode == P1D5WaveformTraceMode.Rms;
private string P1D5RepresentationLabel => _p1d5ValueRepresentation == P1D5RepresentationPrimary ? "Primary" : "Secondary";
private void P1D5Workspace_Loaded(object sender, RoutedEventArgs e)
{
var work = SystemParameters.WorkArea;
if (WindowState == WindowState.Normal)
{
Width = Math.Min(Math.Max(MinWidth, 1360.0), Math.Max(MinWidth, work.Width - 20.0));
Height = Math.Min(Math.Max(MinHeight, 960.0), Math.Max(MinHeight, work.Height - 20.0));
}
InitializeP1D5LocusUi();
RefreshP1D5PresentationButtons();
DisturbanceView.SetAnalogRepresentationLabel(P1D5RepresentationLabel);
QueueP1D5CursorMeasurements();
}
private async void InstantTrace_Click(object sender, RoutedEventArgs e)
=> await SetP1D5WaveformTraceModeAsync(P1D5WaveformTraceMode.Instantaneous).ConfigureAwait(true);
private async void RmsTrace_Click(object sender, RoutedEventArgs e)
=> await SetP1D5WaveformTraceModeAsync(P1D5WaveformTraceMode.Rms).ConfigureAwait(true);
private async void SecondaryValue_Click(object sender, RoutedEventArgs e)
=> await SetP1D5ValueRepresentationAsync(P1D5RepresentationSecondary).ConfigureAwait(true);
private async void PrimaryValue_Click(object sender, RoutedEventArgs e)
=> await SetP1D5ValueRepresentationAsync(P1D5RepresentationPrimary).ConfigureAwait(true);
private async Task SetP1D5WaveformTraceModeAsync(P1D5WaveformTraceMode mode)
{
if (_p1d5WaveformTraceMode == mode || _p1d5PresentationRefreshRunning)
return;
_p1d5WaveformTraceMode = mode;
RefreshP1D5PresentationButtons();
await RefreshP1D5PresentationAsync(reloadWaveform: true).ConfigureAwait(true);
}
private async Task SetP1D5ValueRepresentationAsync(int representation)
{
representation = representation == P1D5RepresentationPrimary
? P1D5RepresentationPrimary
: P1D5RepresentationSecondary;
if (_p1d5ValueRepresentation == representation || _p1d5PresentationRefreshRunning)
return;
_p1d5ValueRepresentation = representation;
RefreshP1D5PresentationButtons();
await RefreshP1D5RepresentationAsync().ConfigureAwait(true);
}
/// <summary>
/// PRI/SEC is a positive per-channel engineering scale. Because every Time Signals lane is
/// independently auto-ranged, multiplying all samples in one lane by that scale cannot change
/// its normalized waveform geometry. Re-reading/rebuilding every source frame was therefore
/// pure latency. Existing C1/C2 measurements are projected immediately through the cached
/// transformer ratio, while native work is requested only for a value that is genuinely absent.
/// </summary>
private async Task RefreshP1D5RepresentationAsync()
{
if (_p1d5PresentationRefreshRunning)
return;
_p1d5PresentationRefreshRunning = true;
try
{
InvalidateP1D5AnalysisPresentationCaches();
InvalidateP1D5CursorMeasurementGeneration();
var missingMeasurements = _analysisMode == AnalysisMode.Waveform
? PresentP1D5CachedRepresentation(_p1d5ValueRepresentation)
: P1D5MeasurementTargets.None;
DisturbanceView.SetAnalogRepresentationLabel(P1D5RepresentationLabel);
if (_p1d5LocusActive)
{
// PRI/SEC changes the engineering basis of R/X. Never interpolate between points
// expressed in different unit bases; discard transient presentation state first.
StopP1D5LocusPresentationPump();
ResetP1D5LocusPresentation();
await RefreshP1D5LocusStaticAsync(forceReopen: false).ConfigureAwait(true);
QueueP1D5LocusCursorRefresh();
}
else if (_analysisMode == AnalysisMode.Waveform)
{
if (missingMeasurements != P1D5MeasurementTargets.None)
QueueP1D5CursorMeasurements(missingMeasurements, isFinal: true);
}
else
{
QueueP1D4LiveAnalysisScrub(isFinal: true);
}
if (_analysisMode == AnalysisMode.Waveform && _disturbanceLoadedViewport.FrameCount > 0)
{
var traceMode = P1D5IsRmsTrace ? "RMS" : "instantaneous";
StatusTextBlock.Text = $"Time Signals • {_disturbanceVisibleSignals.Count} tracks • {traceMode} • " +
$"{P1D5RepresentationLabel} • {_disturbanceLoadedViewport.FrameCount:N0} source frames • retained waveform";
}
}
finally
{
_p1d5PresentationRefreshRunning = false;
}
}
private async Task RefreshP1D5PresentationAsync(bool reloadWaveform)
{
if (_p1d5PresentationRefreshRunning)
return;
_p1d5PresentationRefreshRunning = true;
try
{
InvalidateP1D5AnalysisPresentationCaches();
if (reloadWaveform && _disturbanceInitialized)
{
await ReloadDisturbanceAsync(
CurrentDisturbanceViewport(),
initialLoad: false,
preserveLocalView: true).ConfigureAwait(true);
DisturbanceView.SetAnalogRepresentationLabel(P1D5RepresentationLabel);
}
if (_p1d5LocusActive)
{
await RefreshP1D5LocusStaticAsync(forceReopen: false).ConfigureAwait(true);
QueueP1D5LocusCursorRefresh();
}
else
{
QueueP1D5CursorMeasurements();
if (_analysisMode != AnalysisMode.Waveform)
QueueP1D4LiveAnalysisScrub(isFinal: true);
}
}
finally
{
_p1d5PresentationRefreshRunning = false;
}
}
private void InvalidateP1D5AnalysisPresentationCaches()
{
_p1d4PhasorFrameCache.Clear();
_phasorFrameCache.Clear();
_lastRenderedPhasorFrame = ulong.MaxValue;
_p1d4LastRenderedHarmonicOverviewFrame = ulong.MaxValue;
_p1d4LastRenderedHarmonicOverviewSignature = string.Empty;
}
private void RefreshP1D5PresentationButtons()
{
ApplyP1D5ToggleButton(InstantTraceButton, !P1D5IsRmsTrace);
ApplyP1D5ToggleButton(RmsTraceButton, P1D5IsRmsTrace);
ApplyP1D5ToggleButton(SecondaryValueButton, _p1d5ValueRepresentation == P1D5RepresentationSecondary);
ApplyP1D5ToggleButton(PrimaryValueButton, _p1d5ValueRepresentation == P1D5RepresentationPrimary);
var hasConvertibleAnalog = P1D5HasConvertibleAnalog();
PrimaryValueButton.IsEnabled = hasConvertibleAnalog;
PrimaryValueButton.ToolTip = hasConvertibleAnalog
? "Display analog values in primary engineering quantities using COMTRADE transformer ratios."
: "No valid primary/secondary transformer ratio is declared by this COMTRADE record.";
}
private bool P1D5HasConvertibleAnalog()
{
if (_p1d5HasConvertibleAnalog is { } cached)
return cached;
// Deliberately walk all channels once. Besides answering the button-enabled question this
// pre-warms the authoritative native semantics cache, so the first PRI/SEC toggle performs
// no channel-semantics bridge calls on the UI thread.
var result = false;
for (var index = 0; index < _record.AnalogChannels.Count; index++)
{
var semantics = P1D5AnalogSemantics(checked((uint)index));
if (semantics is { HasValidTransformerRatio: true })
result = true;
}
_p1d5HasConvertibleAnalog = result;
return result;
}
private static void ApplyP1D5ToggleButton(Button button, bool selected)
{
button.Foreground = new SolidColorBrush(selected ? Color.FromRgb(35, 86, 153) : Color.FromRgb(93, 111, 133));
button.Background = new SolidColorBrush(selected ? Color.FromRgb(234, 243, 255) : Colors.White);
button.BorderBrush = new SolidColorBrush(selected ? Color.FromRgb(140, 177, 221) : Color.FromRgb(203, 216, 231));
button.BorderThickness = new Thickness(1);
}
private ComtradeAnalogSemantics? P1D5AnalogSemantics(uint channelIndex)
{
if (_p1d5SemanticsCache.TryGetValue(channelIndex, out var cached))
return cached;
ComtradeAnalogSemantics? semantics = null;
if (_record.Supports(ArdIrecNativeBridge.CapChannelSemantics))
_record.TryReadAnalogSemantics(channelIndex, out semantics);
_p1d5SemanticsCache[channelIndex] = semantics;
return semantics;
}
private double P1D5DisplayScale(uint channelIndex)
=> P1D5DisplayScale(channelIndex, _p1d5ValueRepresentation);
private double P1D5DisplayScale(uint channelIndex, int representation)
{
var semantics = P1D5AnalogSemantics(channelIndex);
if (semantics is not { HasValidTransformerRatio: true })
return 1.0;
var scale = representation == P1D5RepresentationPrimary
? semantics.ScaleToPrimary
: semantics.ScaleToSecondary;
return ComtradeRepresentationScaleMath.IsUsableScale(scale) ? scale : 1.0;
}
private double[] P1D5ScaleInstantaneous(IReadOnlyList<double> values, uint channelIndex)
=> ComtradeRmsSeriesBuilder.ApplyScale(values, P1D5DisplayScale(channelIndex));
}