//******************************************************************************************************// StringExtensions.cs - Gbtc//// Copyright © 2017, 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/01/2016 - Billy Ernest// Created original version of source code modeled after "Event Report" class// 08/04/2017 - F. Russell Robertson// Re-factored class to utilize new StringParser Class in GSF, new GSF string extensions and Log4Net// Added SectionsDefinions Class and support for new ByteSum class// 08/30/2018 - F. Russell Robertson// Fixed settings parsing error found by LG&E for SEL 421 produced file with complete re-write of settings block parser// -- As before, error persists that for the SEL devices produce multi-line values for keys, ONLY the first line of values is captured.// -- Now only one settings region for settings key:value pairs. No duplication in keys have been found.// Added options to not process digital data or settings to reduce parsing time for those only interested in analog data// 08/14/2019 F. Russell Robertson// Modified logic to be able to process (to some degree) .cev files with three header lines like for SEL 321s// Improved parsing robustness as part of this addition//****************************************************************************************************** using System;using System.Collections;using System.Collections.Generic;using System.Text;using GSF.Parsing; namespace GSF.SELEventParser{ public class CommaSeparatedEventReport { #region [ Members ] // Nested Types public struct SectionDefinition { public string Name; public int StartLine; public int Length; public SectionDefinition(string Name, int StartLine, int Length) { this.StartLine = StartLine; this.Length = Length; this.Name = Name; } public override string ToString() { if (string.IsNullOrEmpty(Name)) return ""; return $"{Name}: StartLine = {StartLine}, Length = {Length}"; } } // Fields private const string m_triggerFieldName = "TRIG"; //this is the field name that is the trigger and must separate the analog and digital data private const string DoubleQuote = "\""; #endregion #region [ Properties ] public Dictionary<string, string> Settings { get; set; } public Header Header { get; set; } public Firmware Firmware { get; set; } public AnalogSection AnalogSection { get; set; } public double FrequencyAverage { get; set; } public double FrequencyNominal { get; set; } = 60D; public double SamplesPerCycleAnalog { get; set; } public double SamplesPerCycleDigital { get; set; } public double NumberOfCycles { get; set; } public string Event { get; set; } public int TriggerIndex { get; set; } public int InitialReadingIndex { get; set; } public string[] SettingsRegions { get; set; } public int ExpectedAnalogCount { get; set; } public int ExpectedDigitalCount { get; set; } public int ExpectedNumberDigitalCharacters { get; set; } public int ExpectedDataRecordValueCount { get; set; } public int ExpectedSampleCount { get; set; } public bool ProcessSettings { get; set; } public bool ProcessDigitals { get; set; } #endregion #region [ Methods ] /// <summary> /// Returns the value for a specific setting group and key. /// </summary> /// <param name="groupName">Group name.</param> /// <param name="valueKey">Value key.</param> public string GetSettingValue(string groupName, string valueKey) { if (string.IsNullOrEmpty(groupName) || string.IsNullOrEmpty(valueKey)) return string.Empty; valueKey = string.Concat(groupName.Trim(), ":", valueKey.Trim()); if (Settings.ContainsKey(valueKey)) return Settings[valueKey]; return string.Empty; } /// <summary> /// Returns the value for a specific setting full key. /// </summary> /// <param name="fullKey">Full key</param> public string GetSettingValue(string fullKey) { if (string.IsNullOrEmpty(fullKey)) return string.Empty; if (Settings.ContainsKey(fullKey)) return Settings[fullKey]; return string.Empty; } #endregion #region [ Static ] // Static Fields public static event EventHandler<EventArgs<string>> DebugMessage; // Static Methods /// <summary> /// Parses CEV files. /// </summary> /// <param name="lines">The string array of SEL.cev lines to process</param> /// <param name="processSettings">Set to TRUE to process settings block</param> /// <param name="processDigitals">Set to TRUE to process digital values block</param> /// <param name="fileIdentifier">For error logging, an identifier of the file being processed -- typically the filename.</param> /// <param name="maxFileDuration">Set to a positive value limit the number of data records processed.</param> /// <remarks>Removed lineIndex since file must be processed in sequence. </remarks> /// <returns>Data model representing the comma separated event report.</returns> public static CommaSeparatedEventReport Parse(string[] lines, bool processDigitals = true, bool processSettings = false, string fileIdentifier = "", double maxFileDuration = 0.0D) { OnDebugMessage(string.Format($"Parsing SEL CEV file: {fileIdentifier}; Number of lines in file = {lines.Length}")); if (lines == null || lines.Length == 0) { OnDebugMessage($"SEL CEV Parse aborted. Nothing to do. Sent line array from file {fileIdentifier} is null or empty."); return null; } CommaSeparatedEventReport cSER = new CommaSeparatedEventReport(); cSER.Firmware = new Firmware(); cSER.Header = new Header(); cSER.ProcessSettings = processSettings; cSER.ProcessDigitals = processDigitals; int lineIndex = 0; //relative to the first line in the file string inString; int headerRecordNumber = 1; string[] headerFields = null; string[] lastHeaderFields = null; int SEL321Flag = 0; int triggerFieldPosition = 0; //------------------------------------------------ THE HEADER BLOCK -------------------------------------------------------- //Header Section -- either 3 or 7 records expected //It's reasonable to assume that for a file to be valid it must contain the correct number of headers in the proper order //Returns null if header is significantly malformed. //However, it will try to survive bad bytesum checks //Advance to first non-blank line while (lineIndex < lines.Length) { if (string.IsNullOrEmpty(lines[lineIndex].RemoveControlCharacters().RemoveWhiteSpace())) lineIndex++; else break; } if (lineIndex >= lines.Length) { OnDebugMessage($"CEV file to process is effectively empty. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } headerFields = StringParser.ParseStandardCSV(lines[lineIndex]); OnDebugMessage($"Processing CEV Header Information ! {lines[lineIndex]}"); //VALIDATION PHASE ONLY //Does the file contain "FID" then 7 header records expected. if (headerFields == null || headerFields.Length == 0) { OnDebugMessage($"No SEL Header data found. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } else if (headerFields.Length == 1) { if (headerFields[0].ToUpper().Contains("CEV")) { lineIndex++; headerFields = StringParser.ParseStandardCSV(lines[lineIndex]); if (headerFields != null && headerFields[0].ToUpper().Contains("FID")) { SEL321Flag = 1; } } } else if(headerFields[0].ToUpper().Contains("FID")) { SEL321Flag = 1; OnDebugMessage($"Header fields found for devices that have at least 7 lines of header information."); //VALIDATION PHASE ONLY } else if(headerFields.Length > 1 && headerFields[1].ToUpper().Contains("MONTH")) { SEL321Flag = 2; OnDebugMessage($"Header fields found for devices that have at least 3 lines of header information -- like an SEL321."); //VALIDATION PHASE ONLY } else { OnDebugMessage($"Unexpected SEL Header data found. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } if(SEL321Flag == 1) { //Process header records while (headerRecordNumber < 8) { inString = lines[lineIndex].Trim(); if (!string.IsNullOrEmpty(inString)) { ByteSum byteSum = new ByteSum(); headerFields = StringParser.ParseStandardCSV(inString); //reprocess the first time through switch (headerRecordNumber) { case 1: //field names for headerRecord 2 -- already verified that field 0 contains 'FID' //byteSum.Check(inString, fileIdentifier); //if (!byteSum.Match) //Not worried about the first line bytesum check // OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 2) OnDebugMessage($"Processing SEL CEV header record 1 for SEL CEV file: {fileIdentifier} Expected at least 2 fields and {headerFields.Length} were found."); headerRecordNumber++; break; case 2: //The FID and Firmware Version Number byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 2) OnDebugMessage($"Processing SEL CEV header record 2 for SEL CEV file: {fileIdentifier} Expected at least 2 fields and {headerFields.Length} were found."); if (!headerFields[0].Contains("SEL")) { OnDebugMessage($"Processing field 0 for header record 2 for file {fileIdentifier} Expected string to contain 'SEL' and '{headerFields[0]}' was found."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } if (headerFields.Length > 2) cSER.Firmware.ID = headerFields[1].Trim(); headerRecordNumber++; break; case 3: //The headers for the date data byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); string[] expectedFieldNames3 = { "MONTH", "DAY", "YEAR", "HOUR", "MIN", "SEC", "MSEC" }; if (!StringParser.ExpectedFieldNamesMatch(expectedFieldNames3, headerFields, true, 6)) { OnDebugMessage("Processing SEL CEV header record 3, field names for date header. The expected values for the date labels did not match."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } headerRecordNumber++; break; case 4: //The file date values byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 7) OnDebugMessage($"Processing SEL CEV header record 4 for SEL CEV file: {fileIdentifier} Expected at least 6 fields and {headerFields.Length} were found."); else { if (!TryConvertInt32(headerFields, out int[] values, headerFields.Length - 1)) { OnDebugMessage($"One or more date fields in header record 4 did not parse to integers. Event time not set in SEL CEV File: {fileIdentifier}"); } else if (headerFields.Length == 7) { cSER.Header.EventTime = new DateTime(values[2], values[0], values[1], values[3], values[4], values[5]); if (cSER.Header.EventTime.CompareTo(Convert.ToDateTime("01/01/2000")) < 0) OnDebugMessage($"The event time of {cSER.Header.EventTime.ToShortDateString()} is prior to January 1, 2000."); } else if (headerFields.Length == 8) { cSER.Header.EventTime = new DateTime(values[2], values[0], values[1], values[3], values[4], values[5], values[6]); if (cSER.Header.EventTime.CompareTo(Convert.ToDateTime("01/01/2000")) < 0) OnDebugMessage($"The event time of {cSER.Header.EventTime.ToShortDateString()} is prior to January 1, 2000."); } } headerRecordNumber++; break; case 5: //The headers for the summary data - fields can appear in any order if (headerFields == null || headerFields.Length == 0) { OnDebugMessage("Processing header record 5 -- no summary data header values were found"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (StringParser.FindIndex("FREQ", headerFields) < 0 || StringParser.FindIndex("EVENT", headerFields) < 0) //just check a couple { OnDebugMessage("Processing header record 5 and the minimum expected values of 'FREQ' and 'EVENT' were not found."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } headerRecordNumber++; lastHeaderFields = headerFields; break; case 6: //The summary data - no try-parse data tests performed since there is confirmation of the availability of specific fields byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length != lastHeaderFields.Length) { OnDebugMessage($"Processing header record 6 -- expected {lastHeaderFields.Length} values and {headerFields.Length} were found"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } //For completeness, not needed cSER.Header.SerialNumber = 0; cSER.Header.RelayID = ""; cSER.Header.StationID = ""; //set key class properties //nominal frequency is based on average found. cSER.FrequencyAverage = Convert.ToDouble(headerFields[StringParser.FindIndex("FREQ", lastHeaderFields)]); if (cSER.FrequencyAverage > 48D && cSER.FrequencyAverage < 52D) cSER.FrequencyNominal = 50D; else cSER.FrequencyNominal = 60D; cSER.Event = headerFields[StringParser.FindIndex("EVENT", lastHeaderFields)]; int labelIndex = StringParser.FindIndex("SAM/CYC_A", lastHeaderFields); if (labelIndex > 0) cSER.SamplesPerCycleAnalog = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("SAM/CYC_D", lastHeaderFields); if (labelIndex > 0) cSER.SamplesPerCycleDigital = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_OF_CYC", lastHeaderFields); if (labelIndex > 0) cSER.NumberOfCycles = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_CH_A", lastHeaderFields); if (labelIndex > 0) cSER.ExpectedAnalogCount = Convert.ToInt32(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_CH_D", lastHeaderFields); if (labelIndex > 0) cSER.ExpectedDigitalCount = Convert.ToInt32(headerFields[labelIndex]); headerRecordNumber++; break; case 7: //The header for the data records - assume valid if all three phase currents are present //Note for some files, this record can spans multiple lines, will keep reading lines until the quotes match. StringBuilder sb = new StringBuilder(lines[lineIndex]); //find all the data field names - spanning multiple lines while (sb.ToString().CharCount('\"') % 2 == 1) { sb.Append(lines[++lineIndex]); if (lineIndex >= lines.Length) { OnDebugMessage($"Only partial CEV header data found. Processing of SEL CEV file: {fileIdentifier} aborted at line {lineIndex.ToString()}"); return null; } } headerFields = StringParser.ParseStandardCSV(sb.ToString().Trim()); if (headerFields == null || headerFields.Length == 0) { OnDebugMessage("Header record 7, the data value field names, was missing or empty."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (StringParser.FindIndex("IA", headerFields, false, true) < 0 || StringParser.FindIndex("IB", headerFields, false, true) < 0 || StringParser.FindIndex("IC", headerFields, false, true) < 0) { OnDebugMessage("Processing header record 7, the field names for the data records, and did not find the minimum set of 'IA', 'IB' and 'IC'"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } //determine the number of analog data fields based on the position of "TRIG" (the trigger field name) and setup up Analog Section //all digital labels are packed within the last field name triggerFieldPosition = Array.FindIndex(headerFields, x => x.ToUpper().Contains(m_triggerFieldName)); if (triggerFieldPosition < 0) //TRIG not found { OnDebugMessage($"Processing value field names, the field names for data, searching for {m_triggerFieldName} as the analog/digital data field separator. It was not found within the values of {headerFields}."); OnDebugMessage($"No Digital Values will be processed in SEL CEV file: {fileIdentifier}"); triggerFieldPosition = headerFields.Length; cSER.ProcessDigitals = false; } else if (headerFields.Length < triggerFieldPosition + 2) //too few field names past separator { OnDebugMessage($"Processing header record 8, the field names for data, too few field names found past {m_triggerFieldName} (the analog/digital data field separator) within the values of {headerFields}."); OnDebugMessage($"No Digital Values will be processed in SEL CEV file: {fileIdentifier}"); cSER.ProcessDigitals = false; } headerRecordNumber++; lastHeaderFields = headerFields; //needed below break; } } else { OnDebugMessage($"Unexpected empty header reader in advance of header record number {headerRecordNumber} for SEL CEV file: {fileIdentifier}"); } lineIndex++; if (lineIndex >= lines.Length) { OnDebugMessage($"Only partial CEV header data found. Processing of SEL CEV file: {fileIdentifier} aborted at line {lineIndex.ToString()}"); return null; } } } if (SEL321Flag == 2) { while (headerRecordNumber < 4) { inString = lines[lineIndex].Trim(); if (!string.IsNullOrEmpty(inString)) { ByteSum byteSum = new ByteSum();Lines 1–500 of 1100. Load more
//****************************************************************************************************** // StringExtensions.cs - Gbtc // // Copyright © 2017, 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/01/2016 - Billy Ernest // Created original version of source code modeled after "Event Report" class // 08/04/2017 - F. Russell Robertson // Re-factored class to utilize new StringParser Class in GSF, new GSF string extensions and Log4Net // Added SectionsDefinions Class and support for new ByteSum class // 08/30/2018 - F. Russell Robertson // Fixed settings parsing error found by LG&E for SEL 421 produced file with complete re-write of settings block parser // -- As before, error persists that for the SEL devices produce multi-line values for keys, ONLY the first line of values is captured. // -- Now only one settings region for settings key:value pairs. No duplication in keys have been found. // Added options to not process digital data or settings to reduce parsing time for those only interested in analog data // 08/14/2019 F. Russell Robertson // Modified logic to be able to process (to some degree) .cev files with three header lines like for SEL 321s // Improved parsing robustness as part of this addition //****************************************************************************************************** using System; using System.Collections; using System.Collections.Generic; using System.Text; using GSF.Parsing; namespace GSF.SELEventParser { public class CommaSeparatedEventReport { #region [ Members ] // Nested Types public struct SectionDefinition { public string Name; public int StartLine; public int Length; public SectionDefinition(string Name, int StartLine, int Length) { this.StartLine = StartLine; this.Length = Length; this.Name = Name; } public override string ToString() { if (string.IsNullOrEmpty(Name)) return ""; return $"{Name}: StartLine = {StartLine}, Length = {Length}"; } } // Fields private const string m_triggerFieldName = "TRIG"; //this is the field name that is the trigger and must separate the analog and digital data private const string DoubleQuote = "\""; #endregion #region [ Properties ] public Dictionary<string, string> Settings { get; set; } public Header Header { get; set; } public Firmware Firmware { get; set; } public AnalogSection AnalogSection { get; set; } public double FrequencyAverage { get; set; } public double FrequencyNominal { get; set; } = 60D; public double SamplesPerCycleAnalog { get; set; } public double SamplesPerCycleDigital { get; set; } public double NumberOfCycles { get; set; } public string Event { get; set; } public int TriggerIndex { get; set; } public int InitialReadingIndex { get; set; } public string[] SettingsRegions { get; set; } public int ExpectedAnalogCount { get; set; } public int ExpectedDigitalCount { get; set; } public int ExpectedNumberDigitalCharacters { get; set; } public int ExpectedDataRecordValueCount { get; set; } public int ExpectedSampleCount { get; set; } public bool ProcessSettings { get; set; } public bool ProcessDigitals { get; set; } #endregion #region [ Methods ] /// <summary> /// Returns the value for a specific setting group and key. /// </summary> /// <param name="groupName">Group name.</param> /// <param name="valueKey">Value key.</param> public string GetSettingValue(string groupName, string valueKey) { if (string.IsNullOrEmpty(groupName) || string.IsNullOrEmpty(valueKey)) return string.Empty; valueKey = string.Concat(groupName.Trim(), ":", valueKey.Trim()); if (Settings.ContainsKey(valueKey)) return Settings[valueKey]; return string.Empty; } /// <summary> /// Returns the value for a specific setting full key. /// </summary> /// <param name="fullKey">Full key</param> public string GetSettingValue(string fullKey) { if (string.IsNullOrEmpty(fullKey)) return string.Empty; if (Settings.ContainsKey(fullKey)) return Settings[fullKey]; return string.Empty; } #endregion #region [ Static ] // Static Fields public static event EventHandler<EventArgs<string>> DebugMessage; // Static Methods /// <summary> /// Parses CEV files. /// </summary> /// <param name="lines">The string array of SEL.cev lines to process</param> /// <param name="processSettings">Set to TRUE to process settings block</param> /// <param name="processDigitals">Set to TRUE to process digital values block</param> /// <param name="fileIdentifier">For error logging, an identifier of the file being processed -- typically the filename.</param> /// <param name="maxFileDuration">Set to a positive value limit the number of data records processed.</param> /// <remarks>Removed lineIndex since file must be processed in sequence. </remarks> /// <returns>Data model representing the comma separated event report.</returns> public static CommaSeparatedEventReport Parse(string[] lines, bool processDigitals = true, bool processSettings = false, string fileIdentifier = "", double maxFileDuration = 0.0D) { OnDebugMessage(string.Format($"Parsing SEL CEV file: {fileIdentifier}; Number of lines in file = {lines.Length}")); if (lines == null || lines.Length == 0) { OnDebugMessage($"SEL CEV Parse aborted. Nothing to do. Sent line array from file {fileIdentifier} is null or empty."); return null; } CommaSeparatedEventReport cSER = new CommaSeparatedEventReport(); cSER.Firmware = new Firmware(); cSER.Header = new Header(); cSER.ProcessSettings = processSettings; cSER.ProcessDigitals = processDigitals; int lineIndex = 0; //relative to the first line in the file string inString; int headerRecordNumber = 1; string[] headerFields = null; string[] lastHeaderFields = null; int SEL321Flag = 0; int triggerFieldPosition = 0; //------------------------------------------------ THE HEADER BLOCK -------------------------------------------------------- //Header Section -- either 3 or 7 records expected //It's reasonable to assume that for a file to be valid it must contain the correct number of headers in the proper order //Returns null if header is significantly malformed. //However, it will try to survive bad bytesum checks //Advance to first non-blank line while (lineIndex < lines.Length) { if (string.IsNullOrEmpty(lines[lineIndex].RemoveControlCharacters().RemoveWhiteSpace())) lineIndex++; else break; } if (lineIndex >= lines.Length) { OnDebugMessage($"CEV file to process is effectively empty. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } headerFields = StringParser.ParseStandardCSV(lines[lineIndex]); OnDebugMessage($"Processing CEV Header Information ! {lines[lineIndex]}"); //VALIDATION PHASE ONLY //Does the file contain "FID" then 7 header records expected. if (headerFields == null || headerFields.Length == 0) { OnDebugMessage($"No SEL Header data found. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } else if (headerFields.Length == 1) { if (headerFields[0].ToUpper().Contains("CEV")) { lineIndex++; headerFields = StringParser.ParseStandardCSV(lines[lineIndex]); if (headerFields != null && headerFields[0].ToUpper().Contains("FID")) { SEL321Flag = 1; } } } else if(headerFields[0].ToUpper().Contains("FID")) { SEL321Flag = 1; OnDebugMessage($"Header fields found for devices that have at least 7 lines of header information."); //VALIDATION PHASE ONLY } else if(headerFields.Length > 1 && headerFields[1].ToUpper().Contains("MONTH")) { SEL321Flag = 2; OnDebugMessage($"Header fields found for devices that have at least 3 lines of header information -- like an SEL321."); //VALIDATION PHASE ONLY } else { OnDebugMessage($"Unexpected SEL Header data found. Nothing to do processing file {fileIdentifier} of length {lines.Length.ToString()}"); return null; } if(SEL321Flag == 1) { //Process header records while (headerRecordNumber < 8) { inString = lines[lineIndex].Trim(); if (!string.IsNullOrEmpty(inString)) { ByteSum byteSum = new ByteSum(); headerFields = StringParser.ParseStandardCSV(inString); //reprocess the first time through switch (headerRecordNumber) { case 1: //field names for headerRecord 2 -- already verified that field 0 contains 'FID' //byteSum.Check(inString, fileIdentifier); //if (!byteSum.Match) //Not worried about the first line bytesum check // OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 2) OnDebugMessage($"Processing SEL CEV header record 1 for SEL CEV file: {fileIdentifier} Expected at least 2 fields and {headerFields.Length} were found."); headerRecordNumber++; break; case 2: //The FID and Firmware Version Number byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 2) OnDebugMessage($"Processing SEL CEV header record 2 for SEL CEV file: {fileIdentifier} Expected at least 2 fields and {headerFields.Length} were found."); if (!headerFields[0].Contains("SEL")) { OnDebugMessage($"Processing field 0 for header record 2 for file {fileIdentifier} Expected string to contain 'SEL' and '{headerFields[0]}' was found."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } if (headerFields.Length > 2) cSER.Firmware.ID = headerFields[1].Trim(); headerRecordNumber++; break; case 3: //The headers for the date data byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); string[] expectedFieldNames3 = { "MONTH", "DAY", "YEAR", "HOUR", "MIN", "SEC", "MSEC" }; if (!StringParser.ExpectedFieldNamesMatch(expectedFieldNames3, headerFields, true, 6)) { OnDebugMessage("Processing SEL CEV header record 3, field names for date header. The expected values for the date labels did not match."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } headerRecordNumber++; break; case 4: //The file date values byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length < 7) OnDebugMessage($"Processing SEL CEV header record 4 for SEL CEV file: {fileIdentifier} Expected at least 6 fields and {headerFields.Length} were found."); else { if (!TryConvertInt32(headerFields, out int[] values, headerFields.Length - 1)) { OnDebugMessage($"One or more date fields in header record 4 did not parse to integers. Event time not set in SEL CEV File: {fileIdentifier}"); } else if (headerFields.Length == 7) { cSER.Header.EventTime = new DateTime(values[2], values[0], values[1], values[3], values[4], values[5]); if (cSER.Header.EventTime.CompareTo(Convert.ToDateTime("01/01/2000")) < 0) OnDebugMessage($"The event time of {cSER.Header.EventTime.ToShortDateString()} is prior to January 1, 2000."); } else if (headerFields.Length == 8) { cSER.Header.EventTime = new DateTime(values[2], values[0], values[1], values[3], values[4], values[5], values[6]); if (cSER.Header.EventTime.CompareTo(Convert.ToDateTime("01/01/2000")) < 0) OnDebugMessage($"The event time of {cSER.Header.EventTime.ToShortDateString()} is prior to January 1, 2000."); } } headerRecordNumber++; break; case 5: //The headers for the summary data - fields can appear in any order if (headerFields == null || headerFields.Length == 0) { OnDebugMessage("Processing header record 5 -- no summary data header values were found"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (StringParser.FindIndex("FREQ", headerFields) < 0 || StringParser.FindIndex("EVENT", headerFields) < 0) //just check a couple { OnDebugMessage("Processing header record 5 and the minimum expected values of 'FREQ' and 'EVENT' were not found."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } headerRecordNumber++; lastHeaderFields = headerFields; break; case 6: //The summary data - no try-parse data tests performed since there is confirmation of the availability of specific fields byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (headerFields.Length != lastHeaderFields.Length) { OnDebugMessage($"Processing header record 6 -- expected {lastHeaderFields.Length} values and {headerFields.Length} were found"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } //For completeness, not needed cSER.Header.SerialNumber = 0; cSER.Header.RelayID = ""; cSER.Header.StationID = ""; //set key class properties //nominal frequency is based on average found. cSER.FrequencyAverage = Convert.ToDouble(headerFields[StringParser.FindIndex("FREQ", lastHeaderFields)]); if (cSER.FrequencyAverage > 48D && cSER.FrequencyAverage < 52D) cSER.FrequencyNominal = 50D; else cSER.FrequencyNominal = 60D; cSER.Event = headerFields[StringParser.FindIndex("EVENT", lastHeaderFields)]; int labelIndex = StringParser.FindIndex("SAM/CYC_A", lastHeaderFields); if (labelIndex > 0) cSER.SamplesPerCycleAnalog = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("SAM/CYC_D", lastHeaderFields); if (labelIndex > 0) cSER.SamplesPerCycleDigital = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_OF_CYC", lastHeaderFields); if (labelIndex > 0) cSER.NumberOfCycles = Convert.ToDouble(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_CH_A", lastHeaderFields); if (labelIndex > 0) cSER.ExpectedAnalogCount = Convert.ToInt32(headerFields[labelIndex]); labelIndex = StringParser.FindIndex("NUM_CH_D", lastHeaderFields); if (labelIndex > 0) cSER.ExpectedDigitalCount = Convert.ToInt32(headerFields[labelIndex]); headerRecordNumber++; break; case 7: //The header for the data records - assume valid if all three phase currents are present //Note for some files, this record can spans multiple lines, will keep reading lines until the quotes match. StringBuilder sb = new StringBuilder(lines[lineIndex]); //find all the data field names - spanning multiple lines while (sb.ToString().CharCount('\"') % 2 == 1) { sb.Append(lines[++lineIndex]); if (lineIndex >= lines.Length) { OnDebugMessage($"Only partial CEV header data found. Processing of SEL CEV file: {fileIdentifier} aborted at line {lineIndex.ToString()}"); return null; } } headerFields = StringParser.ParseStandardCSV(sb.ToString().Trim()); if (headerFields == null || headerFields.Length == 0) { OnDebugMessage("Header record 7, the data value field names, was missing or empty."); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } byteSum.Check(inString, fileIdentifier); if (!byteSum.Match) //moved inside case switch due to case 7 OnDebugMessage($"ByteSum check failed for header record {headerRecordNumber} in SEL CEV file: {fileIdentifier}"); if (StringParser.FindIndex("IA", headerFields, false, true) < 0 || StringParser.FindIndex("IB", headerFields, false, true) < 0 || StringParser.FindIndex("IC", headerFields, false, true) < 0) { OnDebugMessage("Processing header record 7, the field names for the data records, and did not find the minimum set of 'IA', 'IB' and 'IC'"); OnDebugMessage($"Processing TERMINATED for SEL CEV file: {fileIdentifier}"); return null; } //determine the number of analog data fields based on the position of "TRIG" (the trigger field name) and setup up Analog Section //all digital labels are packed within the last field name triggerFieldPosition = Array.FindIndex(headerFields, x => x.ToUpper().Contains(m_triggerFieldName)); if (triggerFieldPosition < 0) //TRIG not found { OnDebugMessage($"Processing value field names, the field names for data, searching for {m_triggerFieldName} as the analog/digital data field separator. It was not found within the values of {headerFields}."); OnDebugMessage($"No Digital Values will be processed in SEL CEV file: {fileIdentifier}"); triggerFieldPosition = headerFields.Length; cSER.ProcessDigitals = false; } else if (headerFields.Length < triggerFieldPosition + 2) //too few field names past separator { OnDebugMessage($"Processing header record 8, the field names for data, too few field names found past {m_triggerFieldName} (the analog/digital data field separator) within the values of {headerFields}."); OnDebugMessage($"No Digital Values will be processed in SEL CEV file: {fileIdentifier}"); cSER.ProcessDigitals = false; } headerRecordNumber++; lastHeaderFields = headerFields; //needed below break; } } else { OnDebugMessage($"Unexpected empty header reader in advance of header record number {headerRecordNumber} for SEL CEV file: {fileIdentifier}"); } lineIndex++; if (lineIndex >= lines.Length) { OnDebugMessage($"Only partial CEV header data found. Processing of SEL CEV file: {fileIdentifier} aborted at line {lineIndex.ToString()}"); return null; } } } if (SEL321Flag == 2) { while (headerRecordNumber < 4) { inString = lines[lineIndex].Trim(); if (!string.IsNullOrEmpty(inString)) { ByteSum byteSum = new ByteSum();