//******************************************************************************************************
// CommonFrameHeader.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 11/12/2004 - J. Ritchie Carroll
// Generated original version of source code.
// 09/15/2009 - Stephen C. Wills
// Added new header and license agreement.
// 05/19/2011 - Ritchie
// Added DST file support.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;
using GSF.Parsing;
namespace GSF.PhasorProtocols.BPAPDCstream
{
///
/// Represents the common header for all BPA PDCstream frames of data.
///
[Serializable]
public class CommonFrameHeader : ICommonHeader, ISerializable
{
#region [ Members ]
// Constants
///
/// Total fixed length of , streaming data or data row in phasor file format data.
///
public const ushort FixedLength = 4;
///
/// Total fixed length of for header in phasor file format data.
///
public const ushort DstHeaderFixedLength = 132;
// Fields
private byte m_packetNumber;
private ushort m_wordCount;
private uint m_rowFlags;
private bool m_usePhasorDataFileFormat;
private FileType m_fileType;
private FileVersion m_fileVersion;
private string m_sourceID;
private uint m_startSample;
private ushort m_sampleInterval;
private ushort m_sampleRate;
private uint m_rowLength;
private uint m_totalRows;
private Ticks m_triggerTime;
private uint m_triggerSample;
private uint m_preTriggerRows;
private ushort m_triggerPMU;
private ushort m_triggerType;
private string m_userInformation;
private uint m_pmuCount;
private IChannelParsingState m_state;
#endregion
#region [ Constructors ]
///
/// Creates a new from specified parameters.
///
/// The BPA PDCstream packet number, configuration frame is packet zero.
public CommonFrameHeader(byte packetNumber)
{
m_packetNumber = packetNumber;
}
///
/// Creates a new from given .
///
/// Defines flag that interprets word count in packet header from a byte instead of a word.
/// Defines flag that determines if source data is in the Phasor Data File Format (i.e., a DST file).
/// Previously parsed configuration frame, if available.
/// Buffer that contains data to parse.
/// Start index into buffer where valid data begins.
/// Maximum length of valid data from start index.
public CommonFrameHeader(bool parseWordCountFromByte, bool usePhasorDataFileFormat, ConfigurationFrame configFrame, byte[] buffer, int startIndex, int length)
{
uint secondOfCentury;
// Determines if format is for DST file or streaming data
m_usePhasorDataFileFormat = usePhasorDataFileFormat;
if (m_usePhasorDataFileFormat)
{
// Handle phasor file format data protocol steps
if (buffer[startIndex] == PhasorProtocols.Common.SyncByte && buffer[startIndex + 1] == Common.PhasorFileFormatFlag)
{
// Bail out and leave frame length zero if there's not enough buffer to parse complete fixed portion of header
if (length >= DstHeaderFixedLength)
{
// Read full DST header
m_packetNumber = (byte)BPAPDCstream.FrameType.ConfigurationFrame;
m_fileType = (FileType)buffer[startIndex + 2];
m_fileVersion = (FileVersion)buffer[startIndex + 3];
m_sourceID = Encoding.ASCII.GetString(buffer, startIndex + 4, 4);
uint headerLength = BigEndian.ToUInt32(buffer, startIndex + 8);
secondOfCentury = BigEndian.ToUInt32(buffer, startIndex + 12);
RoughTimestamp = m_fileType switch
{
FileType.PdcNtp => new NtpTimeTag(secondOfCentury, 0).ToDateTime().Ticks,
FileType.PdcUnix => new UnixTimeTag(secondOfCentury).ToDateTime().Ticks,
_ => 0,
};
m_startSample = BigEndian.ToUInt32(buffer, startIndex + 16);
m_sampleInterval = BigEndian.ToUInt16(buffer, startIndex + 20);
m_sampleRate = BigEndian.ToUInt16(buffer, startIndex + 22);
m_rowLength = BigEndian.ToUInt32(buffer, startIndex + 24);
m_totalRows = BigEndian.ToUInt32(buffer, startIndex + 28);
secondOfCentury = BigEndian.ToUInt32(buffer, startIndex + 32);
m_triggerTime = m_fileType switch
{
FileType.PdcNtp => new NtpTimeTag(secondOfCentury, 0).ToDateTime().Ticks,
FileType.PdcUnix => new UnixTimeTag(secondOfCentury).ToDateTime().Ticks,
_ => 0,
};
m_triggerSample = BigEndian.ToUInt32(buffer, startIndex + 36);
m_preTriggerRows = BigEndian.ToUInt32(buffer, startIndex + 40);
m_triggerPMU = BigEndian.ToUInt16(buffer, startIndex + 44);
m_triggerType = BigEndian.ToUInt16(buffer, startIndex + 46);
m_userInformation = Encoding.ASCII.GetString(buffer, startIndex + 48, 80).Trim();
m_pmuCount = BigEndian.ToUInt32(buffer, startIndex + 128);
FrameLength = unchecked((ushort)headerLength);
}
}
else
{
// Must assume this is a data row if there are no sync bytes
m_packetNumber = (byte)BPAPDCstream.FrameType.DataFrame;
m_rowFlags = BigEndian.ToUInt32(buffer, startIndex);
if (configFrame is null)
{
FrameLength = FixedLength;
}
else
{
uint sampleIndex = configFrame.SampleIndex;
CommonFrameHeader configFrameHeader = configFrame.CommonHeader;
if (configFrameHeader is null)
{
FrameLength = FixedLength;
}
else
{
// Assign row length to make sure parser knows how much data it needs
FrameLength = unchecked((ushort)configFrameHeader.RowLength);
// Calculate timestamp as offset plus sample index * frame rate
RoughTimestamp = configFrameHeader.RoughTimestamp + Ticks.FromSeconds(sampleIndex * (1.0D / configFrameHeader.FrameRate));
}
// Increment sample index for next row
configFrame.SampleIndex = sampleIndex + 1;
}
}
}
else
{
// Handle streaming data protocol steps
if (buffer[startIndex] != PhasorProtocols.Common.SyncByte)
throw new InvalidOperationException($"Bad data stream, expected sync byte 0xAA as first byte in BPA PDCstream frame, got 0x{buffer[startIndex].ToString("X").PadLeft(2, '0')}");
// Get packet number
m_packetNumber = buffer[startIndex + 1];
// Some older streams have a bad word count (e.g., some data streams have a 0x01 as the third byte
// in the stream - this should be a 0x00 to make the word count come out correctly). The following
// compensates for this erratic behavior
m_wordCount = parseWordCountFromByte ? buffer[startIndex + 3] : BigEndian.ToUInt16(buffer, startIndex + 2);
// If this is a data frame get a rough timestamp down to the second (full parse will get accurate timestamp), this way
// data frames that don't get fully parsed because configuration hasn't been received will still show a timestamp
if (m_packetNumber > 0 && length > 8)
{
secondOfCentury = BigEndian.ToUInt32(buffer, startIndex + 4);
// Until configuration is available, we make a guess at time tag type - this will just be
// used for display purposes until a configuration frame arrives. If second of century
// is greater than 3155673600 (SOC value for NTP timestamp 1/1/2007), then this is likely
// an NTP time stamp (else this is a Unix time tag for the year 2069 - not likely).
RoughTimestamp = secondOfCentury > 3155673600 ?
new NtpTimeTag(secondOfCentury, 0).ToDateTime().Ticks :
new UnixTimeTag(secondOfCentury).ToDateTime().Ticks;
}
}
}
///
/// Creates a new from serialization parameters.
///
/// The with populated with data.
/// The source for this deserialization.
protected CommonFrameHeader(SerializationInfo info, StreamingContext context)
{
// Deserialize common frame header
m_packetNumber = info.GetByte("packetNumber");
m_wordCount = info.GetUInt16("wordCount");
// The usePhasorDataFileFormat flag and other new elements did not exist in prior versions so we protect against possible deserialization failures
try
{
m_usePhasorDataFileFormat = info.GetBoolean("usePhasorDataFileFormat");
RoughTimestamp = info.GetInt64("roughTimestamp");
m_fileType = (FileType)info.GetValue("fileType", typeof(FileType));
m_fileVersion = (FileVersion)info.GetValue("fileVersion", typeof(FileVersion));
m_sourceID = info.GetString("sourceID");
m_startSample = info.GetUInt32("startSample");
}
catch (SerializationException)
{
m_usePhasorDataFileFormat = false;
RoughTimestamp = 0;
m_fileType = FileType.PdcUnix;
m_fileVersion = FileVersion.PdcWithoutDbuf;
m_sourceID = "UNDF";
m_startSample = 0;
}
}
#endregion
#region [ Properties ]
///
/// Gets or sets the BPA PDCstream specific frame type of this frame.
///
///
///
/// This returns the protocol specific frame classification which uniquely identifies the frame type.
///
///
/// This is the implementation.
///
///
public FrameType TypeID => m_packetNumber == 0 ? BPAPDCstream.FrameType.ConfigurationFrame : BPAPDCstream.FrameType.DataFrame;
///
/// Gets or sets flag that determines if source data is in the Phasor Data File Format (i.e., a DST file).
///
public bool UsePhasorDataFileFormat
{
get => m_usePhasorDataFileFormat;
set => m_usePhasorDataFileFormat = value;
}
///
/// Gets or sets the BPA PDCstream packet number of this frame - set to 00 for configuration frame
///
public byte PacketNumber
{
get => m_packetNumber;
set => m_packetNumber = value;
}
///
/// Gets or sets the BPA PDCstream frame length of this frame.
///
public ushort FrameLength
{
get => (ushort)(2 * m_wordCount);
set => m_wordCount = (ushort)(value / 2);
}
///
/// Gets or sets the BPA PDcstream word count.
///
public ushort WordCount
{
get => m_wordCount;
set => m_wordCount = value;
}
///
/// Gets or sets the length of the data in the BPA PDCstream frame (i.e., the minus the header length and checksum: - 8).
///
public ushort DataLength
{
// Data length will be frame length minus common header length minus crc16
get => (ushort)(FrameLength - FixedLength - (m_usePhasorDataFileFormat ? 0 : 2));
set
{
if (value > Common.MaximumDataLength)
throw new OverflowException($"Data length value cannot exceed {Common.MaximumDataLength}");
FrameLength = (ushort)(value + FixedLength + (m_usePhasorDataFileFormat ? 0 : 2));
}
}
///
/// Gets rough timestamp, accurate to the second, that can be used until configuration frame arrives.
///
public Ticks RoughTimestamp { get; }
///
/// Gets or sets row flags for this when frame is a data frame and use phasor file format is true.
///
public uint RowFlags
{
get => m_rowFlags;
set => m_rowFlags = value;
}
///
/// Gets or sets file type for this used when frame is configuration frame and use phasor file format is true.
///
public FileType FileType
{
get => m_fileType;
set => m_fileType = value;
}
///
/// Gets or sets file version for this used when frame is configuration frame and use phasor file format is true.
///
public FileVersion FileVersion
{
get => m_fileVersion;
set => m_fileVersion = value;
}
///
/// Gets or sets source ID for the object.
///
public string SourceID
{
get => m_sourceID;
set
{
value = value.Trim();
m_sourceID = string.IsNullOrEmpty(value) ? "UNDF" : value;
if (m_sourceID.Length > 4)
m_sourceID = m_sourceID.Substring(0, 4);
m_sourceID = m_sourceID.PadRight(4);
}
}
///
/// Gets or sets start sample for the object.
///
public uint StartSample
{
get => m_startSample;
set => m_startSample = value;
}
///
/// Gets or sets sample interval for the object.
///
public ushort SampleInterval
{
get => m_sampleInterval;
set => m_sampleInterval = value;
}
///
/// Gets or sets sample rate for the object.
///
public ushort SampleRate
{
get => m_sampleRate;
set => m_sampleRate = value;
}
///
/// Gets frame rate based on sample rate and sample interval for the object.
///
public double FrameRate => m_sampleRate / (double)m_sampleInterval;
///
/// Gets or sets row length for the object when use phasor file format is true.
///
public uint RowLength
{
get => m_rowLength;
set => m_rowLength = value;
}
///
/// Gets or sets total number of data rows for the object when use phasor file format is true.
///
public uint TotalRows
{
get => m_totalRows;
set => m_totalRows = value;
}
///
/// Gets or sets trigger time for the object when use phasor file format is true.
///
public Ticks TriggerTime
{
get => m_triggerTime;
set => m_triggerTime = value;
}
///
/// Gets or sets trigger sample for the object when use phasor file format is true.
///
public uint TriggerSample
{
get => m_triggerSample;
set => m_triggerSample = value;
}
///
/// Gets or sets pre-trigger rows for the object when use phasor file format is true.
///
public uint PreTriggerRows
{
get => m_preTriggerRows;
set => m_preTriggerRows = value;
}
///
/// Gets or sets trigger PMU number for the object when use phasor file format is true.
///
public ushort TriggerPMU
{
get => m_triggerPMU;
set => m_triggerPMU = value;
}
///
/// Gets or sets trigger type for the object when use phasor file format is true.
///
public ushort TriggerType
{
get => m_triggerType;
set => m_triggerType = value;
}
///
/// Gets or sets free-form user information that can be added to the object when use phasor file format is true.
///
public string UserInformation
{
get => m_userInformation;
set
{
m_userInformation = string.IsNullOrWhiteSpace(value) ? "" : value;
if (m_userInformation.Length > 80)
m_userInformation = m_userInformation.Substring(0, 80);
}
}
///
/// Gets or sets PMU count (i.e., number of cells) for the object when use phasor file format is true.
///
public uint PmuCount
{
get => m_pmuCount;
set => m_pmuCount = value;
}
///
/// Gets or sets the parsing state for the object.
///
public IChannelParsingState State
{
get => m_state;
set => m_state = value;
}
// Gets or sets any additional state information - satisfies ICommonHeader.State interface property
object ICommonHeader.State
{
get => m_state;
set => m_state = value as IChannelParsingState;
}
///
/// Gets the fundamental frame type of this frame.
///
///
/// Frames are generally classified as data, configuration or header frames. This returns the general frame classification.
///
public FundamentalFrameType FrameType
{
get
{
// Translate BPA PDCstream specific frame type to fundamental frame type
return TypeID switch
{
BPAPDCstream.FrameType.DataFrame => FundamentalFrameType.DataFrame,
BPAPDCstream.FrameType.ConfigurationFrame => FundamentalFrameType.ConfigurationFrame,
_ => FundamentalFrameType.Undetermined,
};
}
}
///
/// Gets the binary image of the common header portion of this frame.
///
public byte[] BinaryImage
{
get
{
if (m_usePhasorDataFileFormat)
throw new NotSupportedException("Creation of the phasor file format (i.e., DST files) is not currently supported.");
byte[] buffer = new byte[FixedLength];
buffer[0] = PhasorProtocols.Common.SyncByte;
buffer[1] = m_packetNumber;
BigEndian.CopyBytes(m_wordCount, buffer, 2);
return buffer;
}
}
#endregion
#region [ Methods ]
///
/// Appends header specific attributes to dictionary.
///
/// Dictionary to append header specific attributes to.
internal void AppendHeaderAttributes(Dictionary attributes)
{
attributes.Add("Frame Type", $"{(ushort)TypeID}: {TypeID}");
attributes.Add("Using Phasor File Format", UsePhasorDataFileFormat.ToString());
attributes.Add("Frame Length", FrameLength.ToString());
attributes.Add("Packet Number", PacketNumber.ToString());
attributes.Add("Word Count", WordCount.ToString());
}
///
/// Populates a with the data needed to serialize the target object.
///
/// The to populate with data.
/// The destination for this serialization.
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
// Serialize unique common frame header values
info.AddValue("packetNumber", m_packetNumber);
info.AddValue("wordCount", m_wordCount);
info.AddValue("usePhasorDataFileFormat", m_usePhasorDataFileFormat);
info.AddValue("roughTimestamp", (long)RoughTimestamp);
info.AddValue("fileType", m_fileType, typeof(FileType));
info.AddValue("fileVersion", m_fileVersion, typeof(FileVersion));
info.AddValue("sourceID", m_sourceID);
info.AddValue("startSample", m_startSample);
}
#endregion
}
}