//******************************************************************************************************
// DataFrame.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/20/2011 - J. Ritchie Carroll
// Added DST file support.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Text;
using GSF.IO.Checksums;
using GSF.Parsing;
namespace GSF.PhasorProtocols.BPAPDCstream
{
///
/// Represents the BPA PDCstream implementation of a that can be sent or received.
///
[Serializable]
public class DataFrame : DataFrameBase, ISupportSourceIdentifiableFrameImage
{
#region [ Members ]
// Fields
private CommonFrameHeader m_frameHeader;
private ushort m_sampleNumber;
#endregion
#region [ Constructors ]
///
/// Creates a new .
///
///
/// This constructor is used by to parse a BPA PDCstream data frame.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public DataFrame()
: base(new DataCellCollection(), 0, null)
{
}
///
/// Creates a new from specified parameters.
///
/// The exact timestamp, in , of the data represented by this .
/// The associated with this .
/// Packet number for this .
/// Sample number for this .
///
/// This constructor is used by a consumer to generate a BPA PDCstream data frame.
///
public DataFrame(Ticks timestamp, ConfigurationFrame configurationFrame, byte packetNumber, ushort sampleNumber)
: base(new DataCellCollection(), timestamp, configurationFrame)
{
PacketNumber = packetNumber;
m_sampleNumber = sampleNumber;
}
///
/// Creates a new from serialization parameters.
///
/// The with populated with data.
/// The source for this deserialization.
protected DataFrame(SerializationInfo info, StreamingContext context)
: base(info, context)
{
// Deserialize data frame
m_frameHeader = (CommonFrameHeader)info.GetValue("frameHeader", typeof(CommonFrameHeader));
m_sampleNumber = info.GetUInt16("sampleNumber");
}
#endregion
#region [ Properties ]
///
/// Gets reference to the for this .
///
public new DataCellCollection Cells => base.Cells as DataCellCollection;
///
/// Gets or sets associated with this .
///
public new ConfigurationFrame ConfigurationFrame
{
get => base.ConfigurationFrame as ConfigurationFrame;
set => base.ConfigurationFrame = value;
}
///
/// Gets or sets the parsing state for the this .
///
public new DataFrameParsingState State
{
get => base.State as DataFrameParsingState;
set => base.State = value;
}
///
/// Gets the identifier that is used to identify the IEEE C37.118 frame.
///
public FrameType TypeID => BPAPDCstream.FrameType.DataFrame;
///
/// Gets or sets current .
///
public CommonFrameHeader CommonHeader
{
// Make sure frame header exists - using base class timestamp to
// prevent recursion (m_frameHeader doesn't exist yet)
get => m_frameHeader ??= new CommonFrameHeader(1);
set
{
m_frameHeader = value;
if (m_frameHeader is null)
return;
State = m_frameHeader.State as DataFrameParsingState;
Timestamp = m_frameHeader.RoughTimestamp;
UsePhasorDataFileFormat = m_frameHeader.UsePhasorDataFileFormat;
}
}
// This interface implementation satisfies ISupportFrameImage.CommonHeader
ICommonHeader ISupportFrameImage.CommonHeader
{
get => CommonHeader;
set => CommonHeader = value as CommonFrameHeader;
}
///
/// Gets or sets the packet number of this .
///
public byte PacketNumber
{
get => CommonHeader.PacketNumber;
set => CommonHeader.PacketNumber = value;
}
///
/// Gets or sets the sample number of this .
///
public ushort SampleNumber
{
get => m_sampleNumber;
set => m_sampleNumber = value;
}
///
/// Gets the timestamp of this formatted as NTP.
///
public NtpTimeTag NtpTimeTag => new(Timestamp);
///
/// Gets the legacy labels parsed from the , if any.
///
public string[] LegacyLabels { get; private set; }
///
/// 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; private set; }
///
/// Gets the length of the .
///
protected override int HeaderLength =>
ConfigurationFrame is null || ConfigurationFrame.StreamType != StreamType.Legacy ? 12 : 12 + ConfigurationFrame.Cells.Count * 8;
///
/// Gets the binary header image of the object.
///
protected override byte[] HeaderImage
{
get
{
byte[] buffer = new byte[HeaderLength];
int index = 0;
// Make sure to provide proper frame length for use in the common header image
CommonHeader.FrameLength = unchecked((ushort)BinaryLength);
// Copy in common frame header portion of header image
CommonHeader.BinaryImage.CopyImage(buffer, ref index, CommonFrameHeader.FixedLength);
if (ConfigurationFrame.RevisionNumber == RevisionNumber.Revision0)
BigEndian.CopyBytes((uint)Math.Truncate(NtpTimeTag.Value), buffer, 4);
else
BigEndian.CopyBytes((uint)Math.Truncate(TimeTag.Value), buffer, 4);
BigEndian.CopyBytes(m_sampleNumber, buffer, 8);
BigEndian.CopyBytes((ushort)Cells.Count, buffer, 10);
index += 8;
// If producing a legacy format, include additional header
if (ConfigurationFrame.StreamType == StreamType.Legacy)
{
ushort offset = 0;
for (int x = 0; x < Cells.Count; x++)
{
DataCell dataCell = Cells[x];
// Add label to data frame header
Encoding.ASCII.GetBytes(dataCell.IDLabel).CopyImage(buffer, ref index, 4);
// Skip reserved bytes
index += 2;
// Add offset to data frame header
BigEndian.CopyBytes(offset, buffer, index);
index += 2;
offset += (ushort)dataCell.BinaryLength;
}
}
return buffer;
}
}
///
/// of string based property names and values for the object.
///
public override Dictionary Attributes
{
get
{
Dictionary baseAttributes = base.Attributes;
CommonHeader.AppendHeaderAttributes(baseAttributes);
baseAttributes.Add("Sample Number", m_sampleNumber.ToString());
if (LegacyLabels is not null)
{
baseAttributes.Add("Legacy Label Count", LegacyLabels.Length.ToString());
for (int x = 0; x < LegacyLabels.Length; x++)
baseAttributes.Add($" Legacy Label {x}", LegacyLabels[x]);
}
return baseAttributes;
}
}
#endregion
#region [ Methods ]
///
/// Parses the binary image.
///
/// Binary image to parse.
/// Start index into to begin parsing.
/// Length of valid data within .
/// The length of the data that was parsed.
///
/// This method is overridden to compensate for lack of CRC in DST files.
///
public override int ParseBinaryImage(byte[] buffer, int startIndex, int length)
{
int parsedLength = base.ParseBinaryImage(buffer, startIndex, length);
// Subtract 2 bytes from total length when using phasor data file format, DST files do not use CRC
if (UsePhasorDataFileFormat)
parsedLength -= 2;
return parsedLength;
}
///
/// Parses the binary header image.
///
/// Binary image to parse.
/// Start index into to begin parsing.
/// Length of valid data within .
/// The length of the data that was parsed.
protected override int ParseHeaderImage(byte[] buffer, int startIndex, int length)
{
IDataFrameParsingState state = State;
// Check for unlikely occurrence of unexpected configuration frame type
if (state.ConfigurationFrame is not ConfigurationFrame configurationFrame)
throw new InvalidOperationException("Unexpected configuration frame encountered - BPA PDCstream configuration frame expected, cannot parse data frame.");
if (UsePhasorDataFileFormat)
{
// Because in cases where PDCxchng is being used the data cell count will be smaller than the
// configuration cell count - we save this count to calculate the offsets later
state.CellCount = unchecked((int)configurationFrame.CommonHeader.PmuCount);
if (state.CellCount > configurationFrame.Cells.Count)
throw new InvalidOperationException($"Stream/Config File Mismatch: PMU count ({state.CellCount}) in stream does not match defined count in configuration file ({configurationFrame.Cells.Count})");
return CommonFrameHeader.FixedLength;
}
// Only need to parse what wasn't already parsed in common frame header
int index = startIndex + CommonFrameHeader.FixedLength;
// Parse frame timestamp
uint secondOfCentury = BigEndian.ToUInt32(buffer, index);
m_sampleNumber = BigEndian.ToUInt16(buffer, index + 4);
index += 6;
if (configurationFrame.RevisionNumber == RevisionNumber.Revision0)
Timestamp = new NtpTimeTag(secondOfCentury, 0).ToDateTime().Ticks + (long)((m_sampleNumber - 1) * configurationFrame.TicksPerFrame);
else
Timestamp = new UnixTimeTag(secondOfCentury).ToDateTime().Ticks + (long)((m_sampleNumber - 1) * configurationFrame.TicksPerFrame);
// Because in cases where PDCxchng is being used the data cell count will be smaller than the
// configuration cell count - we save this count to calculate the offsets later
state.CellCount = BigEndian.ToUInt16(buffer, index);
index += 2;
if (state.CellCount > configurationFrame.Cells.Count)
throw new InvalidOperationException($"Stream/Config File Mismatch: PMU count ({state.CellCount}) in stream does not match defined count in configuration file ({configurationFrame.Cells.Count})");
// We'll at least retrieve legacy labels if defined (might be useful for debugging dynamic changes in data-stream)
if (configurationFrame.StreamType == StreamType.Legacy)
{
LegacyLabels = new string[state.CellCount];
for (int x = 0; x < state.CellCount; x++)
{
LegacyLabels[x] = Encoding.ASCII.GetString(buffer, index, 4);
// We don't need offsets, so we skip them...
index += 8;
}
}
return index - startIndex;
}
///
/// Calculates checksum of given .
///
/// Buffer image over which to calculate checksum.
/// Start index into to calculate checksum.
/// Length of data within to calculate checksum.
/// Checksum over specified portion of .
protected override ushort CalculateChecksum(byte[] buffer, int offset, int length)
{
// PDCstream uses a 16-bit XOR based check sum
return buffer.Xor16Checksum(offset, length);
}
///
/// Appends checksum onto starting at .
///
/// Buffer image on which to append checksum.
/// Index into where checksum should be appended.
///
/// We override default implementation since BPA PDCstream implements check sum for frames in little-endian.
///
protected override void AppendChecksum(byte[] buffer, int startIndex)
{
// Oddly enough, check sum for frames in BPA PDC stream is little-endian
LittleEndian.CopyBytes(CalculateChecksum(buffer, 0, startIndex), buffer, startIndex);
}
///
/// Determines if checksum in the is valid.
///
/// Buffer image to validate.
/// Start index into to perform checksum.
/// Flag that determines if checksum over is valid.
///
/// We override default implementation since BPA PDCstream implements check sum for frames in little-endian.
///
protected override bool ChecksumIsValid(byte[] buffer, int startIndex)
{
// DST files don't use checksums
if (UsePhasorDataFileFormat)
return true;
int sumLength = BinaryLength - 2;
return LittleEndian.ToUInt16(buffer, startIndex + sumLength) == CalculateChecksum(buffer, startIndex, sumLength);
}
///
/// Populates a with the data needed to serialize the target object.
///
/// The to populate with data.
/// The destination for this serialization.
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
// Serialize data frame
info.AddValue("frameHeader", m_frameHeader, typeof(CommonFrameHeader));
info.AddValue("sampleNumber", m_sampleNumber);
}
#endregion
}
}