//****************************************************************************************************** // Schema.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: // ---------------------------------------------------------------------------------------------------- // 05/18/2011 - J. Ritchie Carroll // Generated original version of source code. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.IO; using System.Text; using GSF.IO; namespace GSF.COMTRADE { #region [ Enumerations ] /// /// file type enumeration. /// public enum FileType { /// /// ASCII file type. /// Ascii, /// /// Binary file type, analog channel size = uint16. /// Binary, /// /// Binary file type, analog channel size = uint32. /// Binary32, /// /// Binary file type, analog channel size = float32. /// Float32 } /// /// time quality indicator code. /// public enum TimeQualityIndicatorCode : byte { /// /// Clock locked, Normal operation. /// Locked = 0x0, /// /// Clock fault, time not reliable. /// Failure = 0xF, /// /// Clock unlocked, time within 10^1s. /// Unlocked10Seconds = 0xB, /// /// Clock unlocked, time within 10^0s. /// Unlocked1Second = 0xA, /// /// Clock unlocked, time within 10^-1s. /// UnlockedPoint1Seconds = 0x9, /// /// Clock unlocked, time within 10^-2s. /// UnlockedPoint01Seconds = 0x8, /// /// Clock unlocked, time within 10^-3s. /// UnlockedPoint001Seconds = 0x7, /// /// Clock unlocked, time within 10^-4s. /// UnlockedPoint0001Seconds = 0x6, /// /// Clock unlocked, time within 10^-5s. /// UnlockedPoint00001Seconds = 0x5, /// /// Clock unlocked, time within 10^-6s. /// UnlockedPoint000001Seconds = 0x4, /// /// Clock unlocked, time within 10^-7s. /// UnlockedPoint0000001Seconds = 0x3, /// /// Clock unlocked, time within 10^-8s. /// UnlockedPoint00000001Seconds = 0x2, /// /// Clock unlocked, time within 10^-9s. /// UnlockedPoint000000001Seconds = 0x1 } // 4-bits, 1-nibble /// /// leap second indicator. /// public enum LeapSecondIndicator : byte { /// /// No leap second adjustment was added during recorded data set. /// NoLeapSecondAdjustment = 0, /// /// Leap second adjustment was added during recorded data set. /// LeapSecondWasAdded = 1, /// /// Leap second adjustment was subtracted during recorded data set. /// LeapSecondWasSubtracted = 2, /// /// Leap second adjustment not capable for recording device. /// NoLeapSecondCapacity = 3 } #endregion /// /// Represents the schema for a configuration file in the COMTRADE file standard, IEEE Std C37.111-1999/2013. /// public class Schema { #region [ Members ] // Fields private double m_nominalFrequency; private SampleRate[] m_sampleRates; #endregion #region [ Constructors ] /// /// Creates a new instance of the . /// public Schema() { Version = 1999; m_nominalFrequency = 60.0D; FileType = FileType.Binary; TimeFactor = 1.0D; SampleRates = null; } /// /// Creates a new instance of the from an existing configuration file name. /// /// File name of configuration file to parse. public Schema(string fileName) : this(fileName, false) { } /// /// Creates a new instance of the from an existing configuration file name. /// /// File name of configuration file to parse. /// Indicates whether to relax validation on the number of line image elements. public Schema(string fileName, bool useRelaxedValidation) { if (!File.Exists(fileName)) throw new FileNotFoundException($"Configuration file \"{fileName}\" does not exist."); // Cache configuration file name used to create schema FileName = fileName; IsCombinedFileFormat = HasCFFExtension(FileName); string[] lines; if (IsCombinedFileFormat) { // Read out configuration file section using (StreamReader fileReader = new StreamReader(FileName)) { List fileLines = new List(); bool firstLine = true; do { string line = fileReader.ReadLine(); if (firstLine) { if (!IsFileSectionSeparator(line, out string sectionType) || sectionType != "CFG") throw new InvalidOperationException($"Unexpected file section separator for configuration file type: expected \"--- file type: CFG ---\"{Environment.NewLine}Image = {line}"); firstLine = false; continue; } if (line is null || IsFileSectionSeparator(line)) break; fileLines.Add(line); } while (true); lines = fileLines.ToArray(); } } else { lines = File.ReadAllLines(fileName); } int lineNumber = 0; // Parse version line string[] parts = lines[lineNumber++].Split(','); if (parts.Length < 2 || (!useRelaxedValidation && parts.Length != 2 && parts.Length != 3)) throw new InvalidOperationException($"Unexpected number of line image elements for first configuration file line: {parts.Length} - expected 2 or 3{Environment.NewLine}Image = {lines[lineNumber - 1]}"); StationName = parts[0].Trim(); DeviceID = parts[1].Trim(); if (parts.Length >= 3 && !string.IsNullOrWhiteSpace(parts[2])) Version = int.Parse(parts[2].Trim()); else Version = 1991; // Parse totals line parts = lines[lineNumber++].Split(','); if (parts.Length < 3 || (!useRelaxedValidation && parts.Length != 3)) throw new InvalidOperationException($"Unexpected number of line image elements for second configuration file line: {parts.Length} - expected 3{Environment.NewLine}Image = {lines[lineNumber - 1]}"); int totalChannels = int.Parse(parts[0].Trim()); int totalAnalogChannels = int.Parse(parts[1].Trim().Split('A')[0]); int totalDigitalChannels = int.Parse(parts[2].Trim().Split('D')[0]); if (totalChannels != totalAnalogChannels + totalDigitalChannels) throw new InvalidOperationException($"Total defined channels must equal the sum of the total number of analog and digital channel definitions.{Environment.NewLine}Image = {lines[lineNumber - 1]}"); // Cache analog line image definitions - will parse after file type is known List analogLineImages = new List(); for (int i = 0; i < totalAnalogChannels; i++) analogLineImages.Add(lines[lineNumber++]); // Parse digital definitions List digitalChannels = new List(); for (int i = 0; i < totalDigitalChannels; i++) digitalChannels.Add(new DigitalChannel(lines[lineNumber++], Version, useRelaxedValidation)); DigitalChannels = digitalChannels.ToArray(); // Parse line frequency NominalFrequency = double.Parse(lines[lineNumber++]); // Parse total number of sample rates int totalSampleRates = int.Parse(lines[lineNumber++]); if (totalSampleRates == 0) totalSampleRates = 1; // Parse each sample rate List sampleRates = new List(); for (int i = 0; i < totalSampleRates; i++) sampleRates.Add(new SampleRate(lines[lineNumber++], useRelaxedValidation)); SampleRates = sampleRates.ToArray(); // Parse timestamps StartTime = new Timestamp(lines[lineNumber++]); TriggerTime = new Timestamp(lines[lineNumber++]); // Parse file type Enum.TryParse(lines[lineNumber++], true, out FileType fileType); FileType = fileType; // Parse analog definitions - we do this after knowing file type to better assign default linear scaling factors List analogChannels = new List(); bool targetFloatingPoint = fileType == FileType.Float32; for (int i = 0; i < analogLineImages.Count; i++) analogChannels.Add(new AnalogChannel(analogLineImages[i], Version, targetFloatingPoint, useRelaxedValidation)); AnalogChannels = analogChannels.ToArray(); // Parse time factor TimeFactor = lines.Length < lineNumber ? double.Parse(lines[lineNumber++]) : 1; // Parse time information line if (lines.Length < lineNumber) { parts = lines[lineNumber++].Split(','); if (parts.Length > 0) TimeCode = new TimeOffset(parts[0].Trim()); if (parts.Length > 1) LocalCode = new TimeOffset(parts[1].Trim()); } // Parse time state line if (lines.Length < lineNumber) { parts = lines[lineNumber/*++*/].Split(','); if (parts.Length > 0) TimeQualityIndicatorCode = (TimeQualityIndicatorCode)byte.Parse(parts[0], NumberStyles.HexNumber); if (parts.Length > 1) LeapSecondIndicator = (LeapSecondIndicator)byte.Parse(parts[1]); } } #endregion #region [ Properties ] /// /// Gets the file name used to generate this schema when constructed with one; otherwise, null. /// public string FileName { get; } /// /// Gets flag that determines if is a Combined File Format (.cff) file. /// public bool IsCombinedFileFormat { get; } /// /// Gets or sets free-form station name for this . /// public string StationName { get; set; } /// /// Gets or sets free-form device ID for this . /// public string DeviceID { get; set; } /// /// Gets or sets version number of the IEEE Std C37.111 used by this . /// public int Version { get; set; } /// /// Gets total number of analog and digital channels of this . /// public int TotalChannels => TotalAnalogChannels + TotalDigitalChannels; /// /// Gets total number of analog channels of this . /// public int TotalAnalogChannels => AnalogChannels?.Length ?? 0; /// /// Gets total number of digital channels of this . /// public int TotalDigitalChannels => DigitalChannels?.Length ?? 0; /// /// Gets or sets analog channels of this . /// public AnalogChannel[] AnalogChannels { get; set; } /// /// Gets or sets digital channels of this . /// public DigitalChannel[] DigitalChannels { get; set; } /// /// Gets or sets nominal frequency of this . /// public double NominalFrequency { get => m_nominalFrequency; set { m_nominalFrequency = value; // Cascade nominal frequency update to analog channels if (!(AnalogChannels is null)) { foreach (AnalogChannel analogChannel in AnalogChannels) analogChannel.NominalFrequency = m_nominalFrequency; } } } /// /// Gets or sets sampling rates of this . A file of phasor data will normally be made using a single sampling rate, so this will usually be 1. /// public SampleRate[] SampleRates { get => m_sampleRates; set => m_sampleRates = value ?? new[] { new SampleRate { Rate = 0, EndSample = 1 } }; } /// /// Gets total number of sample rates of this . /// public int TotalSampleRates => m_sampleRates.Length == 1 && m_sampleRates[0].Rate == 0.0D ? 0 : m_sampleRates.Length; /// /// Gets total number of samples, i.e., rows per timestamp, as reported by the sample rates. /// public long TotalSamples => m_sampleRates.Max(sampleRate => sampleRate.EndSample); /// /// Gets total number of channels values, i.e., TotalChannels * TotalSamples. /// public long TotalChannelValues => TotalChannels * TotalSamples; /// /// Gets or sets start timestamp of this . /// public Timestamp StartTime { get; set; } /// /// Gets or sets trigger timestamp of this . /// public Timestamp TriggerTime { get; set; } /// /// Gets or sets file type of this . /// public FileType FileType { get; set; } /// /// Gets or sets the multiplication factor for the time differential (timestamp) field in this . /// public double TimeFactor { get; set; } /// /// Gets or sets UTC offset for channel timestamps - format HhMM. /// public TimeOffset TimeCode { get; set; } = new TimeOffset("0h00"); /// /// Gets or sets UTC offset for timezone of recording device, or "x" for not applicable - format HhMM. /// public TimeOffset LocalCode { get; set; } = new TimeOffset("x"); /// /// Gets or sets the time quality indicator code for the recorded data set. /// public TimeQualityIndicatorCode TimeQualityIndicatorCode { get; set; } = TimeQualityIndicatorCode.Locked; /// /// Gets or sets the leap second indicator for the recorded data set. /// public LeapSecondIndicator LeapSecondIndicator { get; set; } = LeapSecondIndicator.NoLeapSecondAdjustment; /// /// Gets the total digital words for given number of digital values when the file type is binary. /// public int DigitalWords => (int)Math.Ceiling(DigitalChannels.Length / 16.0D); /// /// Calculates the size of a record, in bytes, when the file type is binary. /// public int BinaryRecordLength => 8 + 2 * AnalogChannels.Length + 2 * DigitalWords; /// /// Calculates the size of a record, in bytes, when the file type is binary32. /// public int Binary32RecordLength => 8 + 4 * AnalogChannels.Length + 2 * DigitalWords; /// /// Calculates the size of a record, in bytes, when the file type is float32. /// public int Float32RecordLength => 8 + 4 * AnalogChannels.Length + 2 * DigitalWords; /// /// Gets the file image of this . /// [JsonIgnore] public string FileImage { get { StringBuilder fileImage = new StringBuilder(); void appendLine(string line) { // The standard .NET "Environment.NewLine" constant can just be a line feed on some operating systems, // but the COMTRADE standard requires that end of line markers be both a carriage return and line feed. fileImage.Append(line); fileImage.Append(Writer.CRLF); } // Write version line appendLine($"{StationName},{DeviceID}{(Version >= 1999 ? $",{Version}" : "")}"); // Write totals line appendLine($"{TotalChannels},{TotalAnalogChannels}A,{TotalDigitalChannels}D"); // Write analog definitions for (int i = 0; i < TotalAnalogChannels; i++) appendLine(AnalogChannels[i].ToString()); // Write digital definitions for (int i = 0; i < TotalDigitalChannels; i++) appendLine(DigitalChannels[i].ToString()); // Write line frequency appendLine(NominalFrequency.ToString(CultureInfo.InvariantCulture)); // Write total number of sample rates (zero signifies no fixed sample rates) appendLine(TotalSampleRates.ToString()); int totalSampleRates = TotalSampleRates; if (totalSampleRates == 0) totalSampleRates = 1; // Write sample rates for (int i = 0; i < totalSampleRates; i++) appendLine(SampleRates[i].ToString()); // Write timestamps appendLine(StartTime.ToString()); appendLine(TriggerTime.ToString()); // Write file type appendLine(FileType.ToString().ToUpper()); // Write time factor if (Version >= 1999) appendLine(TimeFactor.ToString(CultureInfo.InvariantCulture)); // Write per data set time info and state if (Version >= 2013) { appendLine($"{TimeCode},{LocalCode}"); appendLine($"{(byte)TimeQualityIndicatorCode:X},{(byte)LeapSecondIndicator}"); } return fileImage.ToString(); } } #endregion #region [ Static ] // Static Methods internal static bool HasCFFExtension(string fileName) => !(fileName is null) && string.Equals(FilePath.GetExtension(fileName), ".cff", StringComparison.OrdinalIgnoreCase); internal static bool IsFileSectionSeparator(string line) => IsFileSectionSeparator(line, out _, out _); internal static bool IsFileSectionSeparator(string line, out string sectionType) => IsFileSectionSeparator(line, out sectionType, out _); internal static bool IsFileSectionSeparator(string line, out string sectionType, out long byteCount) { if (line?.Trim().StartsWith("---") ?? false) { string[] parts = line.Replace("---", "").Trim().Split(':'); if (parts.Length >= 2 && string.Equals(parts[0].Trim(), "file type", StringComparison.OrdinalIgnoreCase)) { sectionType = parts[1].Trim().ToUpperInvariant(); if (parts.Length > 2 && sectionType == "DAT BINARY") long.TryParse(parts[2].Trim(), out byteCount); else byteCount = default; return true; } } sectionType = default; byteCount = default; return false; } #endregion } }