//****************************************************************************************************** // ChannelFrameBase.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: // ---------------------------------------------------------------------------------------------------- // 01/14/2005 - J. Ritchie Carroll // Generated original version of source code. // 09/15/2009 - Stephen C. Wills // Added new header and license agreement. // 12/17/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.Serialization; using GSF.Parsing; using GSF.TimeSeries; // ReSharper disable NonReadonlyMemberInGetHashCode namespace GSF.PhasorProtocols { /// /// Represents the protocol independent common implementation of any frame of data that can be sent or received. /// /// /// /// This phasor protocol implementation defines a "frame" as a collection of cells (logical units of data). /// For example, a could be defined as a PMU within a frame of data, a /// (derived from ), that contains multiple PMU's coming from a PDC. /// /// /// This class implements the interface so it can be cooperatively integrated into measurement concentration. /// For more information see the class. /// /// /// Specific type that the contains. [Serializable] public abstract class ChannelFrameBase : ChannelBase, IChannelFrame where T : IChannelCell { #region [ Members ] // Fields private ushort m_idCode; // Numeric identifier of this frame of data (e.g., ID code of the PDC) private readonly IChannelCellCollection m_cells; // Collection of "cells" within this frame of data (e.g., PMU's in the PDC frame) private Ticks m_timestamp; // Time, represented as 100-nanosecond ticks, of this frame of data private readonly ShortTime m_lifespan; // Elapsed time since creation of this frame of data private SourceChannel m_source; // Defines source channel (e.g., data or command) for channel frame private bool m_trustHeaderLength; // Determines if parsed header lengths should be trusted (normally true) private bool m_validateCheckSum; // Allows bypass of check-sum validation if devices are behaving badly private bool m_published; // Determines if this frame of data has been published (IFrame.Published) private int m_sortedMeasurements; // Total measurements published into this frame (IFrame.SortedMeasurements) private IMeasurement m_lastSortedMeasurement; // Last measurement sorted into this frame (IFrame.LastSortedMeasurement) #endregion #region [ Constructors ] /// /// Creates a new from specified parameters. /// /// The ID code of this . /// The reference to the collection of cells for this . /// The exact timestamp, in , of the data represented by this . protected ChannelFrameBase(ushort idCode, IChannelCellCollection cells, Ticks timestamp) { m_idCode = idCode; m_cells = cells; m_timestamp = timestamp; m_lifespan = ShortTime.Now; m_trustHeaderLength = true; m_validateCheckSum = true; Measurements = new ConcurrentDictionary(); m_sortedMeasurements = -1; } /// /// Creates a new from serialization parameters. /// /// The with populated with data. /// The source for this deserialization. protected ChannelFrameBase(SerializationInfo info, StreamingContext context) { // Deserialize key frame elements... m_idCode = info.GetUInt16("idCode"); m_cells = (IChannelCellCollection)info.GetValue("cells", typeof(IChannelCellCollection)); m_timestamp = info.GetInt64("timestamp"); m_lifespan = ShortTime.Now; m_trustHeaderLength = true; m_validateCheckSum = true; Measurements = new ConcurrentDictionary(); m_sortedMeasurements = -1; } #endregion #region [ Properties ] /// /// Gets the for this . /// public abstract FundamentalFrameType FrameType { get; } /// /// Gets or sets the data source identifier for this . /// public virtual SourceChannel Source { get => m_source; set => m_source = value; } /// /// Gets flag that determines if frame image can be queued for publication or should be processed immediately. /// /// /// Some frames, e.g., a configuration or key frame, may be critical to processing of other frames. In this /// case, these types of frames should be published immediately so that subsequent frame parsing can have /// access to needed critical information. All other frames are assumed to be queued by default. /// public virtual bool AllowQueuedPublication => true; /// /// Gets the strongly-typed reference to the collection of cells for this . /// public virtual IChannelCellCollection Cells => m_cells; // Gets the simple object reference to the cell collection to satisfy IChannelFrame.Cells object IChannelFrame.Cells => m_cells; /// /// Keyed measurements in this . /// /// /// Represents a dictionary of measurements, keyed by . /// public ConcurrentDictionary Measurements { get; } /// /// Gets or sets the ID code of this . /// public virtual ushort IDCode { get => m_idCode; set => m_idCode = value; } /// /// Gets or sets exact timestamp, in ticks, of the data represented by this . /// /// /// The value of this property represents the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001. /// public virtual Ticks Timestamp { get => m_timestamp; set => m_timestamp = value; } /// /// Gets the life-span of this since its creation. /// public virtual ShortTime Lifespan => m_lifespan; /// /// Gets timestamp, in ticks, of when this was created. /// public virtual Ticks CreatedTimestamp => m_lifespan.UtcTime.Ticks; /// /// Gets UNIX based time representation of the ticks of this . /// public virtual UnixTimeTag TimeTag => new(Timestamp); /// /// Gets or sets the parsing state for the this . /// public new virtual IChannelFrameParsingState State { get => base.State as IChannelFrameParsingState; set { base.State = value; if (value is null) return; TrustHeaderLength = value.TrustHeaderLength; ValidateCheckSum = value.ValidateCheckSum; } } /// /// Gets or sets reference to last that was sorted into this . /// /// /// This value is used to help monitor slow moving measurements that are being sorted into the . /// public IMeasurement LastSortedMeasurement { get => m_lastSortedMeasurement; set => m_lastSortedMeasurement = value; } /// /// Gets or sets published state of this . /// public bool Published { get => m_published; set => m_published = value; } /// /// Gets or sets total number of measurements that have been sorted into this . /// public int SortedMeasurements { get => m_sortedMeasurements == -1 ? Measurements.Count : m_sortedMeasurements; set => m_sortedMeasurements = value; } /// /// Gets the length of the . /// /// /// This property is overridden so the length can be extended to include a checksum. /// public override int BinaryLength { get { // We override normal binary length so we can extend length to include checksum. // Also, if frame length was parsed from stream header - we use that length // instead of the calculated length... if (ParsedBinaryLength > 0) return ParsedBinaryLength; return 2 + base.BinaryLength; } } /// /// Gets the binary image of this . /// /// /// This property is overridden to include a checksum in the image. /// public override byte[] BinaryImage { get { // We override normal binary image to include checksum byte[] buffer = new byte[BinaryLength]; int index = 0; // Copy in base image base.BinaryImage.CopyImage(buffer, ref index, base.BinaryLength); // Add check sum AppendChecksum(buffer, index); return buffer; } } /// /// Gets or sets flag that determines if header lengths should be trusted over parsed byte count. /// public virtual bool TrustHeaderLength { get => m_trustHeaderLength; set => m_trustHeaderLength = value; } /// /// Gets or sets flag that determines if frame's check-sum should be validated - this should normally be left as true. /// public virtual bool ValidateCheckSum { get => m_validateCheckSum; set => m_validateCheckSum = value; } /// /// Gets calculated checksum for this . /// public ushort Checksum { get { byte[] buffer = BinaryImage; return BigEndian.ToUInt16(buffer, buffer.Length - 2); } } /// /// Gets the length of the . /// /// /// The length of the body image is the combined length of all the . /// protected override int BodyLength => m_cells.BinaryLength; /// /// Gets the binary body image of this . /// /// /// The body image of the is the combined images of all the . /// protected override byte[] BodyImage => m_cells.BinaryImage(); /// /// Gets the parsed binary length derived from the parsing state, if any. /// protected int ParsedBinaryLength { get; private set; } /// /// of string based property names and values for the object. /// public override Dictionary Attributes { get { Dictionary baseAttributes = base.Attributes; baseAttributes.Add("Total Cells", Cells is null ? "null" : Cells.Count.ToString()); baseAttributes.Add("Fundamental Frame Type", $"{(int)FrameType}: {FrameType}"); baseAttributes.Add("ID Code", IDCode.ToString()); baseAttributes.Add("Published", Published.ToString()); baseAttributes.Add("Ticks", ((long)Timestamp).ToString()); baseAttributes.Add("Timestamp", ((DateTime)Timestamp).ToString("yyyy-MM-dd HH:mm:ss.fff")); baseAttributes.Add("Trusting Header Length", TrustHeaderLength.ToString()); baseAttributes.Add("Validating Check-sum", ValidateCheckSum.ToString()); 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 validate the checksum in the . /// /// Invalid binary image detected - check sum did not match. public override int ParseBinaryImage(byte[] buffer, int startIndex, int length) { // We use data length parsed from data stream if available - in many cases we'll have to as we won't enough // information about cell contents at this early parsing stage ParsedBinaryLength = State.ParsedBinaryLength; // Normal binary image parsing is overridden for a frame so that checksum can be validated if (!ChecksumIsValid(buffer, startIndex)) { // If user selects incorrect protocol, image may be very large - so we don't log the image // byte[] binaryImageErr = buffer.BlockCopy(startIndex, length); // + BitConverter.ToString(binaryImageErr) throw new CrcException($"Invalid binary image detected - check sum of {GetType().Name} did not match"); } if (TrustHeaderLength) { // Normally one would expect image size returned should match parsed image size - but it's possible that these may // differ, so we assume the parsed length header length is better of the two values. This can accommodate the case // where some devices add padding to the end of the frame. base.ParseBinaryImage(buffer, startIndex, length); return State.ParsedBinaryLength; } // Include 2 bytes for CRC in returned parsed length return base.ParseBinaryImage(buffer, startIndex, length) + 2; } /// /// Parses the binary body image. /// /// Binary image to parse. /// Start index into to begin parsing. /// Length of valid data within . /// The length of the data that was parsed. /// /// The body image of the is parsed to create a collection of . /// protected override int ParseBodyImage(byte[] buffer, int startIndex, int length) { IChannelFrameParsingState state = State; int index = startIndex; // Parse all frame cells for (int x = 0; x < state.CellCount; x++) { T cell = state.CreateNewCell(this, state, x, buffer, index, out int parsedLength); m_cells.Add(cell); index += parsedLength; } return index - 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. /// /// Default implementation expects 2-byte big-endian ordered checksum. Override method if protocol checksum /// implementation is different. /// protected virtual bool ChecksumIsValid(byte[] buffer, int startIndex) { if (ValidateCheckSum) { int sumLength = BinaryLength - 2; return BigEndian.ToUInt16(buffer, startIndex + sumLength) == CalculateChecksum(buffer, startIndex, sumLength); } return true; } /// /// Appends checksum onto starting at . /// /// Buffer image on which to append checksum. /// Index into where checksum should be appended. /// /// Default implementation encodes checksum in big-endian order and expects buffer size large enough to accommodate /// 2-byte checksum representation. Override method if protocol expectations are different. /// protected virtual void AppendChecksum(byte[] buffer, int startIndex) { BigEndian.CopyBytes(CalculateChecksum(buffer, 0, startIndex), buffer, 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 . /// /// Override with needed checksum calculation for particular protocol. /// /// This example provides a CRC-CCITT checksum: /// /// using GSF.IO.Checksums; /// /// protected override ushort CalculateChecksum(byte[] buffer, int offset, int length) /// { /// // Return calculated CRC-CCITT over given buffer... /// return buffer.CrcCCITTChecksum(offset, length); /// } /// /// /// protected abstract ushort CalculateChecksum(byte[] buffer, int offset, int length); /// /// Compares the with an . /// /// The to compare with the current . /// A 32-bit signed integer that indicates the relative order of the objects being compared. /// This frame implementation compares itself by timestamp. public virtual int CompareTo(IFrame other) { // We sort frames by timestamp return m_timestamp.CompareTo(other.Timestamp); } /// /// Compares the with the specified . /// /// The to compare with the current . /// A 32-bit signed integer that indicates the relative order of the objects being compared. /// is not an . /// This frame implementation compares itself by timestamp. public virtual int CompareTo(object obj) { if (obj is IFrame other) return CompareTo(other); throw new ArgumentException("Frame can only be compared with other IFrames..."); } /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// /// true if the specified is equal to the current ; /// otherwise, false. /// /// This frame implementation compares itself by timestamp. public virtual bool Equals(IFrame other) { return CompareTo(other) == 0; } /// /// Determines whether the specified is equal to the current . /// /// The to compare with the current . /// /// true if the specified is equal to the current ; /// otherwise, false. /// /// is not an . public override bool Equals(object obj) { if (obj is IFrame other) return Equals(other); return false; } /// /// Returns the hash code for this instance. /// /// A 32-bit signed integer hash code. public override int GetHashCode() => m_timestamp.GetHashCode(); /// /// 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) { // Add key frame elements for serialization... info.AddValue("idCode", m_idCode); info.AddValue("cells", m_cells, typeof(IChannelCellCollection)); info.AddValue("timestamp", (long)m_timestamp); } #endregion } }