//****************************************************************************************************** // Parser.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: // ---------------------------------------------------------------------------------------------------- // 06/17/2012 - J. Ritchie Carroll // Generated original version of source code. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using GSF.IO; using GSF.Units; using GSF.Units.EE; namespace GSF.COMTRADE { /// /// COMTRADE data file(s) parser. /// public class Parser : IDisposable { #region [ Members ] // Fields private Schema m_schema; private bool m_disposed; private FileStream[] m_fileStreams; private StreamReader[] m_fileReaders; private int m_streamIndex; private double[] m_primaryValues; private double[] m_secondaryValues; #endregion #region [ Constructors ] /// /// Creates a new instance of the COMTRADE . /// public Parser() { InferTimeFromSampleRates = true; } /// /// Releases the unmanaged resources before the object is reclaimed by . /// ~Parser() { Dispose(false); } #endregion #region [ Properties ] /// /// Gets or sets associated COMTRADE schema for this . /// /// /// Upon assignment, any value will be used to /// initialize the parser's initial property when an associated /// data file that ends with either ".dat" or ".d00" is found to exist. /// public Schema Schema { get { return m_schema; } set { m_schema = value; if ((object)m_schema != null) { if (m_schema.TotalChannels > 0) Values = new double[m_schema.TotalChannels]; else throw new InvalidOperationException("Invalid schema: total channels defined in schema is zero."); if (m_schema.TotalSampleRates == 0) InferTimeFromSampleRates = false; // If no data file name is already defined, attempt to find initial data file with same // root file name as schema configuration file but with a ".dat" or ".d00" extension: if (string.IsNullOrWhiteSpace(FileName) && !string.IsNullOrWhiteSpace(m_schema.FileName)) { string directory = FilePath.GetDirectoryName(m_schema.FileName); string rootFileName = FilePath.GetFileNameWithoutExtension(m_schema.FileName); string dataFile1 = Path.Combine(directory, $"{rootFileName}.dat"); string dataFile2 = Path.Combine(directory, $"{rootFileName}.d00"); if (File.Exists(dataFile1)) FileName = dataFile1; else if (File.Exists(dataFile2)) FileName = dataFile2; } } else { Values = null; } } } /// /// Gets or sets COMTRADE data filename. If there are more than one data files in a set, this should be set to first file name in the set, e.g., DATA123.D00. /// public string FileName { get; set; } /// /// Gets or sets flag that determines if time should be inferred from sample rates. /// public bool InferTimeFromSampleRates { get; set; } /// /// Gets timestamp of current record. /// public DateTime Timestamp { get; private set; } /// /// Gets values of current record. /// public double[] Values { get; private set; } /// /// Gets values of current record with secondary analog channels scaled to primary values. /// public double[] PrimaryValues { get { if ((object)m_primaryValues == null) { m_primaryValues = new double[Values.Length]; for (int i = 0; i < m_primaryValues.Length; i++) { double value = Values[i]; if (i < m_schema.AnalogChannels.Length) { if (char.ToUpper(m_schema.AnalogChannels[i].ScalingIdentifier) == 'S') value *= m_schema.AnalogChannels[i].PrimaryRatio / m_schema.AnalogChannels[i].SecondaryRatio; } m_primaryValues[i] = value; } } return m_primaryValues; } } /// /// Gets values of current record with primary analog channels scaled to secondary values. /// public double[] SecondaryValues { get { if ((object)m_secondaryValues == null) { m_secondaryValues = new double[Values.Length]; for (int i = 0; i < m_secondaryValues.Length; i++) { double value = Values[i]; if (i < m_schema.AnalogChannels.Length) { if (char.ToUpper(m_schema.AnalogChannels[i].ScalingIdentifier) == 'P') value *= m_schema.AnalogChannels[i].SecondaryRatio / m_schema.AnalogChannels[i].PrimaryRatio; } m_secondaryValues[i] = value; } } return m_secondaryValues; } } /// /// Gets or sets flag that determines if schemas that include a time code for a UTC offset /// should apply the adjustment so that timestamps are parsed as UTC. /// public bool AdjustToUTC { get; set; } = true; /// /// Gets or sets target angle unit for any analog channels that are angles. /// /// /// When assigned, any encountered angles will be converted to the specified unit while parsing. /// public AngleUnit? TargetAngleUnit { get; set; } #endregion #region [ Methods ] /// /// Releases all the resources used by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { CloseFiles(); } } finally { m_disposed = true; // Prevent duplicate dispose. } } } /// /// Opens all COMTRADE data file streams. /// public void OpenFiles() { if (string.IsNullOrWhiteSpace(FileName)) throw new InvalidOperationException("First COMTRADE data file name was not specified, cannot open files."); if (!File.Exists(FileName)) throw new FileNotFoundException($"Specified COMTRADE data file \"{FileName}\" was not found, cannot open files."); // Get all data files in the collection const string FileRegex = @"(?:\.dat|\.d\d\d)$"; string directory = FilePath.GetDirectoryName(FileName); string fileNamePattern = $"{FilePath.GetFileNameWithoutExtension(FileName)}.d*"; string[] fileNames = FilePath.GetFileList(Path.Combine(directory, fileNamePattern)) .Where(fileName => Regex.IsMatch(fileName, FileRegex, RegexOptions.IgnoreCase)) .OrderBy(fileName => fileName, StringComparer.OrdinalIgnoreCase) .ToArray(); // Create a new file stream for each file m_fileStreams = new FileStream[fileNames.Length]; for (int i = 0; i < fileNames.Length; i++) m_fileStreams[i] = new FileStream(fileNames[i], FileMode.Open, FileAccess.Read, FileShare.Read); m_streamIndex = 0; } /// /// Closes all COMTRADE data file streams. /// public void CloseFiles() { if ((object)m_fileStreams != null) { foreach (FileStream fileStream in m_fileStreams) fileStream?.Dispose(); } m_fileStreams = null; } /// /// Reads next COMTRADE record. /// /// true if read succeeded; otherwise false if end of data set was reached. public bool ReadNext() { if ((object)m_fileStreams == null) throw new InvalidOperationException("COMTRADE data files are not open, cannot read next record."); if ((object)m_schema == null) throw new InvalidOperationException("No COMTRADE schema has been defined, cannot read records."); if (m_streamIndex > m_fileStreams.Length) throw new EndOfStreamException("All COMTRADE data has been read, cannot read more records."); m_primaryValues = null; m_secondaryValues = null; switch (m_schema.FileType) { case FileType.Ascii: return ReadNextAscii(); case FileType.Binary: return ReadNextBinary(); case FileType.Binary32: return ReadNextBinary32(); case FileType.Float32: return ReadNextFloat32(); default: return false; } } // Handle ASCII file read private bool ReadNextAscii() { // For ASCII files, we wrap file streams with file readers if ((object)m_fileReaders == null) { m_fileReaders = new StreamReader[m_fileStreams.Length]; for (int i = 0; i < m_fileStreams.Length; i++) m_fileReaders[i] = new StreamReader(m_fileStreams[i]); } // Read next line of record values StreamReader reader = m_fileReaders[m_streamIndex]; string line = reader.ReadLine(); string[] elems = ((object)line != null) ? line.Split(',') : null; // See if we have reached the end of this file if ((object)elems == null || elems.Length != Values.Length + 2) { if (reader.EndOfStream) return ReadNextFile(); throw new InvalidOperationException("COMTRADE schema does not match number of elements found in ASCII data file."); } // Parse row of data uint sample = uint.Parse(elems[0]); // Get timestamp of this record Timestamp = DateTime.MinValue; // If sample rates are defined, this is the preferred method for timestamp resolution if (InferTimeFromSampleRates && m_schema.SampleRates.Length > 0) { // Find rate for given sample SampleRate sampleRate = m_schema.SampleRates.LastOrDefault(sr => sample <= sr.EndSample); if (sampleRate.Rate > 0.0D) Timestamp = new DateTime(Ticks.FromSeconds(1.0D / sampleRate.Rate * sample) + m_schema.StartTime.Value); } // Fall back on specified microsecond time if (Timestamp == DateTime.MinValue) Timestamp = new DateTime(Ticks.FromMicroseconds(uint.Parse(elems[1]) * m_schema.TimeFactor) + m_schema.StartTime.Value); // Apply timestamp offset to restore UTC timezone if (AdjustToUTC) { TimeOffset offset = Schema.TimeCode ?? new TimeOffset(); Timestamp = new DateTime(Timestamp.Ticks + offset.TickOffset, DateTimeKind.Utc); } // Parse all record values for (int i = 0; i < Values.Length; i++) { Values[i] = double.Parse(elems[i + 2]); if (i < m_schema.AnalogChannels.Length) Values[i] = AdjustValue(Values[i], i); } return true; } // Handle binary file read private bool ReadNextBinary() { FileStream currentFile = m_fileStreams[m_streamIndex]; int recordLength = m_schema.BinaryRecordLength; byte[] buffer = new byte[recordLength]; // Read next record from file int bytesRead = currentFile.Read(buffer, 0, recordLength); // See if we have reached the end of this file if (bytesRead == 0) return ReadNextFile(); if (bytesRead == recordLength) { int index = ReadTimestamp(buffer); index = ReadAnalogValues(buffer, index, ReadInt16, 2); ReadDigitalValues(buffer, index); } else { throw new InvalidOperationException("Failed to read enough bytes from COMTRADE BINARY file for a record as defined by schema - possible schema/data file mismatch or file corruption."); } return true; } // Handle binary32 file read private bool ReadNextBinary32() { FileStream currentFile = m_fileStreams[m_streamIndex]; int recordLength = m_schema.Binary32RecordLength; byte[] buffer = new byte[recordLength]; // Read next record from file int bytesRead = currentFile.Read(buffer, 0, recordLength); // See if we have reached the end of this file if (bytesRead == 0) return ReadNextFile(); if (bytesRead == recordLength) { int index = ReadTimestamp(buffer); index = ReadAnalogValues(buffer, index, ReadInt32, 4); ReadDigitalValues(buffer, index); } else { throw new InvalidOperationException("Failed to read enough bytes from COMTRADE BINARY32 file for a record as defined by schema - possible schema/data file mismatch or file corruption."); } return true; } // Handle float32 file read private bool ReadNextFloat32() { FileStream currentFile = m_fileStreams[m_streamIndex]; int recordLength = m_schema.Float32RecordLength; byte[] buffer = new byte[recordLength]; // Read next record from file int bytesRead = currentFile.Read(buffer, 0, recordLength); // See if we have reached the end of this file if (bytesRead == 0) return ReadNextFile(); if (bytesRead == recordLength) { int index = ReadTimestamp(buffer); index = ReadAnalogValues(buffer, index, ReadFloat, 4); ReadDigitalValues(buffer, index); } else { throw new InvalidOperationException("Failed to read enough bytes from COMTRADE FLOAT32 file for a record as defined by schema - possible schema/data file mismatch or file corruption."); } return true; } private bool ReadNextFile() { m_streamIndex++; // See if there is more to read if there is another file return m_streamIndex < m_fileStreams.Length && ReadNext(); } private int ReadTimestamp(byte[] buffer) { int index = 0; // Read sample index uint sample = LittleEndian.ToUInt32(buffer, index); index += 4; // Get timestamp of this record Timestamp = DateTime.MinValue; // If sample rates are defined, this is the preferred method for timestamp resolution if (InferTimeFromSampleRates && m_schema.SampleRates.Length > 0) { // Find rate for given sample SampleRate sampleRate = m_schema.SampleRates.LastOrDefault(sr => sample <= sr.EndSample); if (sampleRate.Rate > 0.0D) Timestamp = new DateTime(Ticks.FromSeconds(1.0D / sampleRate.Rate * sample) + m_schema.StartTime.Value); } // Read microsecond timestamp uint microseconds = LittleEndian.ToUInt32(buffer, index); index += 4; // Fall back on specified microsecond time if (Timestamp == DateTime.MinValue) Timestamp = new DateTime(Ticks.FromMicroseconds(microseconds * m_schema.TimeFactor) + m_schema.StartTime.Value); // Apply timestamp offset to restore UTC timezone if (AdjustToUTC) { TimeOffset offset = Schema.TimeCode ?? new TimeOffset(); Timestamp = new DateTime(Timestamp.Ticks + offset.TickOffset, DateTimeKind.Utc); } return index; } private int ReadAnalogValues(byte[] buffer, int index, Func byteConverter, int byteSize) { // Parse all analog record values for (int i = 0; i < m_schema.AnalogChannels.Length; i++) { // Read next analog value Values[i] = AdjustValue(byteConverter(buffer, index), i); index += byteSize; } return index; } private double AdjustValue(double value, int channelIndex) { AnalogChannel channel = m_schema.AnalogChannels[channelIndex]; value = value * channel.Multiplier + channel.Adder; if (channel.SignalKind == SignalKind.Angle && TargetAngleUnit.HasValue) value = Angle.ConvertFrom(value, channel.AngleUnit).ConvertTo(TargetAngleUnit.Value); return value; } private void ReadDigitalValues(byte[] buffer, int index) { int valueIndex = m_schema.AnalogChannels.Length; // Parse all digital record values for (int i = 0; i < m_schema.DigitalWords; i++) { // Read next digital word ushort digitalWord = LittleEndian.ToUInt16(buffer, index); index += 2; // Distribute each bit of digital word through next 16 digital values for (int j = 0; j < 16 && valueIndex < Values.Length; j++, valueIndex++) Values[valueIndex] = digitalWord.CheckBits(BitExtensions.BitVal(j)) ? 1.0D : 0.0D; } } #endregion #region [ Static ] // Static Methods private static double ReadInt16(byte[] buffer, int startIndex) => LittleEndian.ToInt16(buffer, startIndex); private static double ReadInt32(byte[] buffer, int startIndex) => LittleEndian.ToInt32(buffer, startIndex); private static double ReadFloat(byte[] buffer, int startIndex) => LittleEndian.ToSingle(buffer, startIndex); #endregion } }