//****************************************************************************************************** // 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 } }