//****************************************************************************************************** // CompactMeasurement.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: // ---------------------------------------------------------------------------------------------------- // 08/23/2010 - J. Ritchie Carroll // Generated original version of source code. // 05/15/2011 - J. Ritchie Carroll // Added runtime size optimizations. // 06/07/2011 - J. Ritchie Carroll // Implemented initialize issue fix as found and proposed by Luc Cezard. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Linq; using GSF.IO; using GSF.IO.Compression; using GSF.Parsing; namespace GSF.TimeSeries.Transport { #region [ Enumerations ] /// /// state flags. /// [Flags] internal enum CompactMeasurementStateFlags : byte { /// /// A data range flag was set. /// DataRange = (byte)Bits.Bit00, /// /// A data quality flag was set. /// DataQuality = (byte)Bits.Bit01, /// /// A time quality flag was set. /// TimeQuality = (byte)Bits.Bit02, /// /// A system flag was set. /// SystemIssue = (byte)Bits.Bit03, /// /// Calculated value bit was set. /// CalculatedValue = (byte)Bits.Bit04, /// /// Discarded value bit was set. /// DiscardedValue = (byte)Bits.Bit05, /// /// Compact measurement timestamp was serialized using base time offset when set. /// BaseTimeOffset = (byte)Bits.Bit06, /// /// Use odd time index (i.e., 1) when bit is set; even time index (i.e., 0) when bit is clear. /// TimeIndex = (byte)Bits.Bit07, /// /// No flags. /// NoFlags = (byte)Bits.Nil } /// /// Defines static methods for mapping between compact and full measurement states. /// internal static class MeasurementStateMappingExtensions { private const MeasurementStateFlags DataRangeMask = MeasurementStateFlags.OverRangeError | MeasurementStateFlags.UnderRangeError | MeasurementStateFlags.AlarmHigh | MeasurementStateFlags.AlarmLow | MeasurementStateFlags.WarningHigh | MeasurementStateFlags.WarningLow; private const MeasurementStateFlags DataQualityMask = MeasurementStateFlags.BadData | MeasurementStateFlags.SuspectData | MeasurementStateFlags.FlatlineAlarm | MeasurementStateFlags.ComparisonAlarm | MeasurementStateFlags.ROCAlarm | MeasurementStateFlags.ReceivedAsBad | MeasurementStateFlags.CalculationError | MeasurementStateFlags.CalculationWarning | MeasurementStateFlags.ReservedQualityFlag; private const MeasurementStateFlags TimeQualityMask = MeasurementStateFlags.BadTime | MeasurementStateFlags.SuspectTime | MeasurementStateFlags.LateTimeAlarm | MeasurementStateFlags.FutureTimeAlarm | MeasurementStateFlags.UpSampled | MeasurementStateFlags.DownSampled | MeasurementStateFlags.ReservedTimeFlag; private const MeasurementStateFlags SystemIssueMask = MeasurementStateFlags.SystemError | MeasurementStateFlags.SystemWarning | MeasurementStateFlags.MeasurementError; private const MeasurementStateFlags CalculatedValueMask = MeasurementStateFlags.CalculatedValue; private const MeasurementStateFlags DiscardedValueMask = MeasurementStateFlags.DiscardedValue; /// /// Maps to . /// /// Flags to map. /// mapped from . public static CompactMeasurementStateFlags MapToCompactFlags(this MeasurementStateFlags stateFlags) { CompactMeasurementStateFlags mappedStateFlags = CompactMeasurementStateFlags.NoFlags; if ((stateFlags & DataRangeMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.DataRange; if ((stateFlags & DataQualityMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.DataQuality; if ((stateFlags & TimeQualityMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.TimeQuality; if ((stateFlags & SystemIssueMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.SystemIssue; if ((stateFlags & CalculatedValueMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.CalculatedValue; if ((stateFlags & DiscardedValueMask) > 0) mappedStateFlags |= CompactMeasurementStateFlags.DiscardedValue; return mappedStateFlags; } /// /// Maps to . /// /// Flags to map. /// mapped from . public static MeasurementStateFlags MapToFullFlags(this CompactMeasurementStateFlags stateFlags) { MeasurementStateFlags mappedStateFlags = MeasurementStateFlags.Normal; if ((stateFlags & CompactMeasurementStateFlags.DataRange) > 0) mappedStateFlags |= DataRangeMask; if ((stateFlags & CompactMeasurementStateFlags.DataQuality) > 0) mappedStateFlags |= DataQualityMask; if ((stateFlags & CompactMeasurementStateFlags.TimeQuality) > 0) mappedStateFlags |= TimeQualityMask; if ((stateFlags & CompactMeasurementStateFlags.SystemIssue) > 0) mappedStateFlags |= SystemIssueMask; if ((stateFlags & CompactMeasurementStateFlags.CalculatedValue) > 0) mappedStateFlags |= CalculatedValueMask; if ((stateFlags & CompactMeasurementStateFlags.DiscardedValue) > 0) mappedStateFlags |= DiscardedValueMask; return mappedStateFlags; } } #endregion /// /// Represents a that can be serialized with minimal size. /// /// /// This measurement implementation is serialized through /// to allow complete control of binary format. Only critical measurements properties are /// serialized and every attempt is made to optimize the binary image for purposes of size /// reduction. /// public class CompactMeasurement : Measurement, IBinaryMeasurement { #region [ Members ] // Constants /// /// Fixed byte length of a . /// public const int FixedLength = 7; // Members private readonly SignalIndexCache m_signalIndexCache; private readonly long[] m_baseTimeOffsets; private int m_timeIndex; private readonly bool m_useMillisecondResolution; private bool m_usingBaseTimeOffset; private readonly bool m_includeTime; #endregion #region [ Constructors ] /// /// Creates a new . /// /// Signal index cache used to serialize or deserialize runtime information. /// Set to true to include time in serialized packet; otherwise false. /// Base time offset array - set to null to use full fidelity measurement time. /// Time index to use for base offset. /// Flag that determines if millisecond resolution is in use for this serialization. public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) { m_signalIndexCache = signalIndexCache; m_includeTime = includeTime; // We keep a clone of the base time offsets, if provided, since array contents can change at any time if ((object)baseTimeOffsets == null) m_baseTimeOffsets = s_emptyBaseTimeOffsets; else m_baseTimeOffsets = new[] { baseTimeOffsets[0], baseTimeOffsets[1] }; m_timeIndex = timeIndex; m_useMillisecondResolution = useMillisecondResolution; } /// /// Creates a new from an existing value. /// /// Source value. /// Signal index cache used to serialize or deserialize runtime information. /// Set to true to include time in serialized packet; otherwise false. /// Base time offset array - set to null to use full fidelity measurement time. /// Time index to use for base offset. /// Flag that determines if millisecond resolution is in use for this serialization. public CompactMeasurement(IMeasurement measurement, SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false) { Metadata = measurement.Metadata; Value = measurement.Value; Timestamp = measurement.Timestamp; StateFlags = measurement.StateFlags; m_signalIndexCache = signalIndexCache; m_includeTime = includeTime; // We keep a clone of the base time offsets, if provided, since array contents can change at any time if ((object)baseTimeOffsets == null) m_baseTimeOffsets = s_emptyBaseTimeOffsets; else m_baseTimeOffsets = new[] { baseTimeOffsets[0], baseTimeOffsets[1] }; m_timeIndex = timeIndex; m_useMillisecondResolution = useMillisecondResolution; } #endregion #region [ Properties ] /// /// Gets flag that determines if time is serialized into measurement binary image. /// public bool IncludeTime { get { return m_includeTime; } } /// /// Gets the length of the . /// public int BinaryLength { get { int length = FixedLength; if (m_includeTime) { long baseTimeOffset = m_baseTimeOffsets[m_timeIndex]; if (baseTimeOffset > 0) { // See if timestamp will fit within space allowed for active base offset. We cache result so that post call // to binary length, result will speed other subsequent parsing operations by not having to reevaluate. long difference = (long)Timestamp - m_baseTimeOffsets[m_timeIndex]; if (difference > 0) m_usingBaseTimeOffset = m_useMillisecondResolution ? difference / Ticks.PerMillisecond < ushort.MaxValue : difference < uint.MaxValue; else m_usingBaseTimeOffset = false; if (m_usingBaseTimeOffset) { if (m_useMillisecondResolution) length += 2; // Use two bytes for millisecond resolution timestamp with valid offset else length += 4; // Use four bytes for tick resolution timestamp with valid offset } else { // Use eight bytes for full fidelity time length += 8; } } else { // Use eight bytes for full fidelity time length += 8; } } return length; } } /// /// Gets offset compressed millisecond-resolution 2-byte timestamp. /// public ushort TimestampC2 { get { return (ushort)(Timestamp - m_baseTimeOffsets[m_timeIndex]).ToMilliseconds(); } } /// /// Gets offset compressed tick-resolution 4-byte timestamp. /// public uint TimestampC4 { get { return (uint)((long)Timestamp - m_baseTimeOffsets[m_timeIndex]); } } /// /// Gets or sets byte level compact state flags with encoded time index and base time offset bits. /// public byte CompactStateFlags { get { // Encode compact state flags CompactMeasurementStateFlags flags = StateFlags.MapToCompactFlags(); if (m_timeIndex != 0) flags |= CompactMeasurementStateFlags.TimeIndex; if (m_usingBaseTimeOffset) flags |= CompactMeasurementStateFlags.BaseTimeOffset; return (byte)flags; } set { // Decode compact state flags CompactMeasurementStateFlags flags = (CompactMeasurementStateFlags)value; StateFlags = flags.MapToFullFlags(); m_timeIndex = (flags & CompactMeasurementStateFlags.TimeIndex) > 0 ? 1 : 0; m_usingBaseTimeOffset = (flags & CompactMeasurementStateFlags.BaseTimeOffset) > 0; } } /// /// Gets or sets the 2-byte run-time signal index for this measurement. /// public ushort RuntimeID { get { return m_signalIndexCache.GetSignalIndex(Key); } set { // Attempt to restore signal identification MeasurementKey key; if (m_signalIndexCache.Reference.TryGetValue(value, out key)) { Metadata = key.Metadata; } else throw new InvalidOperationException("Failed to find associated signal identification for runtime ID " + value); } } #endregion #region [ Methods ] /// /// Initializes from the specified binary image. /// /// Buffer containing binary image to parse. /// 0-based starting index in the to start parsing. /// Valid number of bytes within from . /// The number of bytes used for initialization in the (i.e., the number of bytes parsed). /// Not enough buffer available to deserialize measurement. /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int ParseBinaryImage(byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); if (length < 1) throw new InvalidOperationException("Not enough buffer available to deserialize measurement."); int index = startIndex; // Decode state flags CompactStateFlags = buffer[index++]; // Decode runtime ID RuntimeID = BigEndian.ToUInt16(buffer, index); index += 2; // Decode value Value = BigEndian.ToSingle(buffer, index); index += 4; if (m_includeTime) { if (m_usingBaseTimeOffset) { long baseTimeOffset = m_baseTimeOffsets[m_timeIndex]; if (m_useMillisecondResolution) { // Decode 2-byte millisecond offset timestamp if (baseTimeOffset > 0) Timestamp = baseTimeOffset + BigEndian.ToUInt16(buffer, index) * Ticks.PerMillisecond; index += 2; } else { // Decode 4-byte tick offset timestamp if (baseTimeOffset > 0) Timestamp = baseTimeOffset + BigEndian.ToUInt32(buffer, index); index += 4; } } else { // Decode 8-byte full fidelity timestamp Timestamp = BigEndian.ToInt64(buffer, index); index += 8; } } return index - startIndex; } /// /// Generates binary image of the and copies it into the given buffer, for bytes. /// /// Buffer used to hold generated binary image of the source object. /// 0-based starting index in the to start writing. /// The number of bytes written to the . /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// /// /// /// Field: Bytes:
/// -------- -------
/// Flags 1
/// ID 2
/// Value 4
/// [Time] 2?
///
/// /// Constant Length = 7
/// Variable Length = 0, 2, 4 or 8 (i.e., total size is 7, 9, 11 or 15) ///
///
public int GenerateBinaryImage(byte[] buffer, int startIndex) { // Call to binary length property caches result of m_usingBaseTimeOffset int length = BinaryLength; buffer.ValidateParameters(startIndex, length); // Added encoded compact state flags to beginning of buffer buffer[startIndex++] = CompactStateFlags; // Encode runtime ID startIndex += BigEndian.CopyBytes(RuntimeID, buffer, startIndex); // Encode adjusted value (accounts for adder and multiplier) startIndex += BigEndian.CopyBytes((float)AdjustedValue, buffer, startIndex); if (m_includeTime) { if (m_usingBaseTimeOffset) { if (m_useMillisecondResolution) { // Encode 2-byte millisecond offset timestamp BigEndian.CopyBytes(TimestampC2, buffer, startIndex); } else { // Encode 4-byte ticks offset timestamp BigEndian.CopyBytes(TimestampC4, buffer, startIndex); } } else { // Encode 8-byte full fidelity timestamp BigEndian.CopyBytes((long)Timestamp, buffer, startIndex); } } return length; } #endregion #region [ Static ] // Static Fields private static readonly long[] s_emptyBaseTimeOffsets = new long[] { 0, 0 }; #endregion } /// /// Defines extension functions related to . /// public static class CompactMeasurementExtensions { /// /// Attempts to compress payload of values onto the stream. /// /// Payload of values. /// Memory based stream to hold compressed payload. /// Compression strength to use. /// Flag that determines if time should be included in the compressed payload. /// Current . /// true if payload was compressed and encoded onto stream; otherwise false. /// /// /// Compressed payload will only be encoded onto stream if compressed size would be smaller /// than normal serialized size. /// /// /// As an optimization this function uses a compression method that uses pointers to native structures, as such the /// endian order encoding of the compressed data will always be in the native-endian order of the operating system. /// This will be an important consideration when writing a endian order neutral payload decompressor. To help with /// this the actual endian order used during compression is marked in the data flags. However, measurements values /// are consistently encoded in big-endian order prior to buffer compression. /// /// public static bool CompressPayload(this IEnumerable compactMeasurements, BlockAllocatedMemoryStream destination, byte compressionStrength, bool includeTime, ref DataPacketFlags flags) { // Instantiate a buffer that is larger than we'll need byte[] buffer = new byte[ushort.MaxValue]; // Go ahead an enumerate all the measurements - this will cast all values to compact measurements CompactMeasurement[] measurements = compactMeasurements.ToArray(); int measurementCount = measurements.Length; int sizeToBeat = measurementCount * measurements[0].BinaryLength; int index = 0; // Encode compact state flags and runtime IDs together -- // Together these are three bytes, so we pad with a zero byte. // The zero byte and state flags are considered to be more compressible // than the runtime ID, so these are stored in the higher order bytes. for (int i = 0; i < measurementCount; i++) { uint value = ((uint)measurements[i].CompactStateFlags << 16) | measurements[i].RuntimeID; index += NativeEndianOrder.Default.CopyBytes(value, buffer, index); } // Encode values for (int i = 0; i < measurementCount; i++) { // Encode using adjusted value (accounts for adder and multiplier) index += NativeEndianOrder.Default.CopyBytes((float)measurements[i].AdjustedValue, buffer, index); } if (includeTime) { // Encode timestamps for (int i = 0; i < measurementCount; i++) { // Since large majority of 8-byte tick values will be repeated, they should compress well index += NativeEndianOrder.Default.CopyBytes((long)measurements[i].Timestamp, buffer, index); } } // Attempt to compress buffer int compressedSize = PatternCompressor.CompressBuffer(buffer, 0, index, ushort.MaxValue, compressionStrength); // Only encode compressed buffer if compression actually helped payload size if (compressedSize <= sizeToBeat) { // Set payload compression flag flags |= DataPacketFlags.Compressed; // Make sure decompressor knows original endian encoding order if (BitConverter.IsLittleEndian) flags |= DataPacketFlags.LittleEndianCompression; else flags &= ~DataPacketFlags.LittleEndianCompression; // Copy compressed payload onto destination stream destination.Write(buffer, 0, compressedSize); return true; } // Clear payload compression flag flags &= ~DataPacketFlags.Compressed; return false; } /// /// Decompresses values from the given buffer. /// /// Buffer with compressed payload. /// Current . /// Index into buffer where compressed payload begins. /// Length of all data within buffer. /// Number of compressed measurements in the payload. /// Flag that determines if timestamps as included in the payload. /// Current . /// Decompressed values from the given buffer. public static CompactMeasurement[] DecompressPayload(this byte[] source, SignalIndexCache signalIndexCache, int index, int dataLength, int measurementCount, bool includeTime, DataPacketFlags flags) { CompactMeasurement[] measurements = new CompactMeasurement[measurementCount]; // Actual data length has to take into account response byte and in-response-to server command byte in the payload header //int dataLength = length - index - 2; int bufferLength = PatternDecompressor.MaximumSizeDecompressed(dataLength); // Copy source data into a decompression buffer byte[] buffer = new byte[bufferLength]; Buffer.BlockCopy(source, index, buffer, 0, dataLength); // Check that OS endian-order matches endian-order of compressed data if (!(BitConverter.IsLittleEndian && (flags & DataPacketFlags.LittleEndianCompression) > 0)) { // can be modified to decompress a payload that is in a non-native Endian order throw new NotImplementedException("Cannot currently decompress payload that is not in native endian-order."); } // Attempt to decompress buffer int uncompressedSize = PatternDecompressor.DecompressBuffer(buffer, 0, dataLength, bufferLength); if (uncompressedSize == 0) throw new InvalidOperationException("Failed to decompress payload buffer - possible data corruption."); index = 0; // Decode ID and state flags for (int i = 0; i < measurementCount; i++) { uint value = NativeEndianOrder.Default.ToUInt32(buffer, index); measurements[i] = new CompactMeasurement(signalIndexCache, includeTime) { CompactStateFlags = (byte)(value >> 16), RuntimeID = (ushort)value }; index += 4; } // Decode values for (int i = 0; i < measurementCount; i++) { measurements[i].Value = NativeEndianOrder.Default.ToSingle(buffer, index); index += 4; } if (includeTime) { // Decode timestamps for (int i = 0; i < measurementCount; i++) { measurements[i].Timestamp = NativeEndianOrder.Default.ToInt64(buffer, index); index += 8; } } return measurements; } } }