1100 lines · 54.8 KB · C#
RawDownload
//******************************************************************************************************//  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.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t