Skip to content

Commit 63387df

Browse files
Merge pull request #477 from GridProtectionAlliance/sel-cws-updates
Updates to SEL CWS rolling phase estimator
2 parents 7d16a3d + 0dc6662 commit 63387df

File tree

10 files changed

+1333
-245
lines changed

10 files changed

+1333
-245
lines changed

Source/Libraries/GSF.PhasorProtocols/GSF.PhasorProtocols.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@
6060
<Reference Include="System.Data.DataSetExtensions" />
6161
<Reference Include="System.Design" />
6262
<Reference Include="System.Drawing" />
63+
<Reference Include="System.Memory, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
64+
<SpecificVersion>False</SpecificVersion>
65+
<HintPath>..\..\Dependencies\NuGet\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
66+
</Reference>
6367
<Reference Include="System.Runtime.Serialization" />
6468
<Reference Include="System.Runtime.Serialization.Formatters.Soap" />
6569
<Reference Include="System.Windows.Forms" />
@@ -157,6 +161,7 @@
157161
<Compile Include="IHeaderFrameParsingState.cs" />
158162
<Compile Include="IPhasorDefinition.cs" />
159163
<Compile Include="IPhasorValue.cs" />
164+
<Compile Include="IsExternalInit.cs" />
160165
<Compile Include="MultiProtocolFrameParser.cs" />
161166
<Compile Include="NamespaceDoc.cs" />
162167
<Compile Include="PhasorDefinitionBase.cs" />
@@ -291,6 +296,8 @@
291296
<Compile Include="Macrodyne\FrequencyValue.cs" />
292297
<Compile Include="Macrodyne\PhasorDefinition.cs" />
293298
<Compile Include="Macrodyne\PhasorValue.cs" />
299+
<Compile Include="ReplayTimer.cs" />
300+
<Compile Include="RequiredMemberAttribute.cs" />
294301
<Compile Include="SelCWS\AnalogDefinition.cs" />
295302
<Compile Include="SelCWS\AnalogValue.cs" />
296303
<Compile Include="SelCWS\Common.cs" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// ReSharper disable once CheckNamespace
2+
namespace System.Runtime.CompilerServices;
3+
4+
// Class defined in .NET 5.0 and later, but not in .NET Framework or Standard 2.x.
5+
// Declared manually here to allow 'init' property operations.
6+
internal static class IsExternalInit { }

Source/Libraries/GSF.PhasorProtocols/MultiProtocolFrameParser.cs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
using GSF.Parsing;
8484
using GSF.PhasorProtocols.IEEEC37_118;
8585
using GSF.PhasorProtocols.Macrodyne;
86+
using GSF.PhasorProtocols.SelCWS;
8687
using GSF.PhasorProtocols.SelFastMessage;
8788
using GSF.Threading;
8889
using GSF.TimeSeries;
@@ -1460,14 +1461,14 @@ private void SharedClient_SendDataException(object sender, EventArgs<Exception>
14601461
private IClient m_commandChannel;
14611462
private IPAddress m_receiveFromAddress;
14621463
private IPAddress m_multicastServerAddress;
1464+
private ReplayTimer m_replayTimer;
14631465
private PrecisionInputTimer m_inputTimer;
14641466
private ShortSynchronizedOperation m_readNextBuffer;
14651467
private SharedTimer m_rateCalcTimer;
14661468
private IConfigurationFrame m_configurationFrame;
14671469
private CheckSumValidationFrameTypes m_checkSumValidationFrameTypes;
14681470
private long m_dataStreamStartTime;
14691471
private long m_missingFramesOverflow;
1470-
private long m_lastFrameReceivedTime;
14711472
private int m_frameRateTotal;
14721473
private int m_byteRateTotal;
14731474
private int m_parsingExceptionCount;
@@ -2553,11 +2554,41 @@ private void InitializeFrameParser(Dictionary<string, string> settings)
25532554
if (settings.TryGetValue("calculatePhaseEstimates", out setting))
25542555
selCWSParameters.CalculatePhaseEstimates = setting.ParseBoolean();
25552556

2556-
if (settings.TryGetValue("frameRate", out setting) && ushort.TryParse(setting, out ushort frameRate))
2557-
selCWSParameters.FrameRate = frameRate;
2558-
25592557
if (settings.TryGetValue("nominalFrequency", out setting) && Enum.TryParse(setting, true, out LineFrequency nominalFrequency))
25602558
selCWSParameters.NominalFrequency = nominalFrequency;
2559+
2560+
if (settings.TryGetValue("calculationFrameRate", out setting) && ushort.TryParse(setting, out ushort calculationFrameRate))
2561+
selCWSParameters.CalculationFrameRate = calculationFrameRate;
2562+
2563+
if (settings.TryGetValue("repeatLastCalculatedValueWhenDownSampling", out setting))
2564+
selCWSParameters.RepeatLastCalculatedValueWhenDownSampling = setting.ParseBoolean();
2565+
2566+
if (settings.TryGetValue("referenceChannel", out setting) && Enum.TryParse(setting, true, out PhaseChannel referenceChannel))
2567+
selCWSParameters.ReferenceChannel = referenceChannel;
2568+
2569+
if (settings.TryGetValue("targetCycles", out setting) && int.TryParse(setting, out int targetCycles))
2570+
selCWSParameters.TargetCycles = targetCycles;
2571+
2572+
if (settings.TryGetValue("publishAnglesTauSeconds", out setting) && double.TryParse(setting, out double publishAnglesTauSeconds))
2573+
selCWSParameters.PublishAnglesTauSeconds = publishAnglesTauSeconds;
2574+
2575+
if (settings.TryGetValue("publishMagnitudesTauSeconds", out setting) && double.TryParse(setting, out double publishMagnitudesTauSeconds))
2576+
selCWSParameters.PublishMagnitudesTauSeconds = publishMagnitudesTauSeconds;
2577+
2578+
if (settings.TryGetValue("publishFrequencyTauSeconds", out setting) && double.TryParse(setting, out double publishFrequencyTauSeconds))
2579+
selCWSParameters.PublishFrequencyTauSeconds = publishFrequencyTauSeconds;
2580+
2581+
if (settings.TryGetValue("publishRocofTauSeconds", out setting) && double.TryParse(setting, out double publishRocofTauSeconds))
2582+
selCWSParameters.PublishRocofTauSeconds = publishRocofTauSeconds;
2583+
2584+
if (settings.TryGetValue("sampleFrequencyTauSeconds", out setting) && double.TryParse(setting, out double sampleFrequencyTauSeconds))
2585+
selCWSParameters.SampleFrequencyTauSeconds = sampleFrequencyTauSeconds;
2586+
2587+
if (settings.TryGetValue("sampleRocofTauSeconds", out setting) && double.TryParse(setting, out double sampleRocofTauSeconds))
2588+
selCWSParameters.SampleRocofTauSeconds = sampleRocofTauSeconds;
2589+
2590+
if (settings.TryGetValue("recalculationCycles", out setting) && int.TryParse(setting, out int recalculationCycles))
2591+
selCWSParameters.RecalculationCycles = recalculationCycles;
25612592
}
25622593
break;
25632594
default:
@@ -3574,19 +3605,12 @@ private long MaintainCapturedFrameReplayTiming()
35743605
{
35753606
if (m_inputTimer is null)
35763607
{
3577-
if (m_lastFrameReceivedTime > 0L)
3578-
{
3579-
// To maintain timing on "frames per second", we wait for defined frame rate interval
3580-
double sleepTime = 1.0D / m_definedFrameRate - (DateTime.UtcNow.Ticks - m_lastFrameReceivedTime) / (double)Ticks.PerSecond;
3581-
3582-
// Thread sleep time is a minimum suggested sleep time depending on system activity, when not using high-resolution
3583-
// input timer we assume getting close is good enough
3584-
if (sleepTime > 0.0D)
3585-
Thread.Sleep((int)(sleepTime * 1000.0D));
3586-
}
3608+
if (m_replayTimer?.DefinedFrameRate != m_definedFrameRate)
3609+
m_replayTimer = new ReplayTimer(m_definedFrameRate);
35873610

3588-
m_lastFrameReceivedTime = DateTime.UtcNow.Ticks;
3589-
return Ticks.AlignToMillisecondDistribution(m_lastFrameReceivedTime, m_definedFrameRate).Value;
3611+
m_replayTimer.WaitNext();
3612+
3613+
return Ticks.AlignToMillisecondDistribution(DateTime.UtcNow.Ticks, m_definedFrameRate).Value;
35903614
}
35913615

35923616
// When high resolution input timing is requested, we only need to wait for the next signal...
@@ -3608,7 +3632,7 @@ private void m_frameParser_ReceivedChannelFrame(object sender, EventArgs<IChanne
36083632
frame.Timestamp = Ticks.AlignToMillisecondDistribution(DateTime.UtcNow.Ticks, m_definedFrameRate);
36093633

36103634
// Keep reading file data
3611-
if (m_transportProtocol == TransportProtocol.File && QueuedOutputs < 2 && QueuedBuffers < 10)
3635+
if (m_transportProtocol == TransportProtocol.File && (QueuedOutputs < 2 || QueuedBuffers < 10))
36123636
m_readNextBuffer?.RunOnceAsync();
36133637

36143638
if (ReceivedChannelFrame is not null && e.Argument2)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//******************************************************************************************************
2+
// ReplayTimer.cs - Gbtc
3+
//
4+
// Copyright © 2025, Grid Protection Alliance. All Rights Reserved.
5+
//
6+
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+
// file except in compliance with the License. You may obtain a copy of the License at:
10+
//
11+
// http://opensource.org/licenses/MIT
12+
//
13+
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+
// License for the specific language governing permissions and limitations.
16+
//
17+
// Code Modification History:
18+
// ----------------------------------------------------------------------------------------------------
19+
// 12/29/2025 - J. Ritchie Carroll
20+
// Generated original version of source code.
21+
//
22+
//******************************************************************************************************
23+
24+
using System;
25+
using System.Diagnostics;
26+
using System.Threading;
27+
28+
namespace GSF.PhasorProtocols;
29+
30+
/// <summary>
31+
/// Replay timer class used to pace frame publication for file-based playback.
32+
/// </summary>
33+
/// <remarks>
34+
/// This class uses a combination of coarse sleeping, yielding, and spinning for
35+
/// accurate wait times with minimal CPU usage. Supports common frame rates, e.g.,
36+
/// 30 or 60, and very high frame rates, e.g., 3000.
37+
/// </remarks>
38+
public sealed class ReplayTimer
39+
{
40+
private readonly long m_periodTicks; // Query Performance Counter (QPC) ticks per frame
41+
private long m_nextTick; // Next scheduled QPC tick (not equal to DateTime ticks)
42+
43+
/// <summary>
44+
/// Creates a new <see cref="ReplayTimer"/> instance.
45+
/// </summary>
46+
/// <param name="definedFrameRate">The defined frame rate in frames per second.</param>
47+
public ReplayTimer(int definedFrameRate)
48+
{
49+
if (definedFrameRate <= 0)
50+
throw new ArgumentOutOfRangeException(nameof(definedFrameRate));
51+
52+
m_periodTicks = (long)Math.Round(Stopwatch.Frequency / (double)definedFrameRate);
53+
m_nextTick = Stopwatch.GetTimestamp();
54+
55+
DefinedFrameRate = definedFrameRate;
56+
}
57+
58+
/// <summary>
59+
/// Gets the defined frame rate for this timer.
60+
/// </summary>
61+
public int DefinedFrameRate { get; }
62+
63+
/// <summary>
64+
/// Blocks until the next scheduled frame rate interval.
65+
/// </summary>
66+
public void WaitNext()
67+
{
68+
m_nextTick += m_periodTicks;
69+
70+
while (true)
71+
{
72+
long now = Stopwatch.GetTimestamp();
73+
long ticksRemaining = m_nextTick - now;
74+
75+
if (ticksRemaining <= 0L)
76+
return;
77+
78+
// Convert to milliseconds for coarse decisions
79+
double remaining = ticksRemaining * 1000.0 / Stopwatch.Frequency;
80+
81+
switch (remaining)
82+
{
83+
case >= 2.0D:
84+
// Coarse sleep -- 1ms is the finest useful Sleep on Windows
85+
Thread.Sleep(1);
86+
break;
87+
case >= 0.3D:
88+
// Yield to reduce CPU but avoid oversleeping
89+
Thread.Yield();
90+
break;
91+
default:
92+
// Just spin to hit sub-millisecond cadence
93+
SpinWait sw = new();
94+
95+
while (Stopwatch.GetTimestamp() < m_nextTick)
96+
sw.SpinOnce();
97+
98+
return;
99+
}
100+
}
101+
}
102+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#pragma warning disable
2+
3+
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace System.Runtime.CompilerServices
7+
{
8+
#if !NET7_0_OR_GREATER
9+
10+
#if (NET40_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NET5_0_OR_GREATER)
11+
[ExcludeFromCodeCoverage]
12+
#endif
13+
[DebuggerNonUserCode]
14+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
15+
internal sealed class RequiredMemberAttribute : Attribute { }
16+
17+
#if (NET40_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NET5_0_OR_GREATER)
18+
[ExcludeFromCodeCoverage]
19+
#endif
20+
[DebuggerNonUserCode]
21+
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
22+
internal sealed class CompilerFeatureRequiredAttribute : Attribute
23+
{
24+
public CompilerFeatureRequiredAttribute(string featureName)
25+
{
26+
FeatureName = featureName;
27+
}
28+
29+
public string FeatureName { get; }
30+
31+
public bool IsOptional { get; set; } // Originally, this was 'Init', but that does not seem necessary and may collide with the IsExternalInit package
32+
33+
public const string RefStructs = nameof(RefStructs);
34+
public const string RequiredMembers = nameof(RequiredMembers);
35+
}
36+
37+
#endif // !NET7_0_OR_GREATER
38+
}
39+
40+
namespace System.Diagnostics.CodeAnalysis
41+
{
42+
#if !NET7_0_OR_GREATER
43+
#if (NET40_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NET5_0_OR_GREATER)
44+
[ExcludeFromCodeCoverage]
45+
#endif
46+
[DebuggerNonUserCode]
47+
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
48+
internal sealed class SetsRequiredMembersAttribute : Attribute { }
49+
#endif
50+
}
51+
52+
#pragma warning restore

Source/Libraries/GSF.PhasorProtocols/SelCWS/Common.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
// Modified Header.
2525
//
2626
//******************************************************************************************************
27+
// ReSharper disable InconsistentNaming
2728

2829
using System;
2930
using GSF.Units.EE;
@@ -48,6 +49,37 @@ public enum FrameType : byte
4849
ConfigurationFrame = 0x01
4950
}
5051

52+
/// <summary>
53+
/// Phase channels for SEL CWS PoW analogs.
54+
/// </summary>
55+
public enum PhaseChannel
56+
{
57+
/// <summary>
58+
/// Phase A current (IA).
59+
/// </summary>
60+
IA = 0,
61+
/// <summary>
62+
/// Phase B current (IB).
63+
/// </summary>
64+
IB = 1,
65+
/// <summary>
66+
/// Phase C current (IC).
67+
/// </summary>
68+
IC = 2,
69+
/// <summary>
70+
/// Phase A voltage (VA).
71+
/// </summary>
72+
VA = 3,
73+
/// <summary>
74+
/// Phase B voltage (VB).
75+
/// </summary>
76+
VB = 4,
77+
/// <summary>
78+
/// Phase C voltage (VC).
79+
/// </summary>
80+
VC = 5
81+
}
82+
5183
#endregion
5284

5385
/// <summary>

Source/Libraries/GSF.PhasorProtocols/SelCWS/CommonFrameHeader.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ namespace GSF.PhasorProtocols.SelCWS;
3737
/// <summary>
3838
/// Represents the common header for a SEL CWS frame of data.
3939
/// </summary>
40-
public class CommonFrameHeader : CommonHeaderBase<FrameType>
40+
[Serializable]
41+
public class CommonFrameHeader : CommonHeaderBase<FrameType>, ISerializable
4142
{
4243
#region [ Members ]
4344

@@ -173,7 +174,7 @@ public FundamentalFrameType FundamentalFrameType
173174
/// <param name="attributes">Dictionary to append header specific attributes to.</param>
174175
internal void AppendHeaderAttributes(Dictionary<string, string> attributes)
175176
{
176-
attributes.Add("Frame Type", $"{(ushort)m_frameType}: {m_frameType}");
177+
attributes.Add("Frame Type", $"{(byte)m_frameType}: {m_frameType}");
177178
attributes.Add("Frame Length", FrameLength.ToString());
178179
attributes.Add("Version", $"0x{Common.Version:X2}");
179180
attributes.Add("Channel ID", ChannelID.ToString());

0 commit comments

Comments
 (0)