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); }
/// <summary> /// Creates a new COMTRADE configuration <see cref="Schema"/>. /// </summary> /// <param name="metadata">Schema <see cref="ChannelMetadata"/> records.</param> /// <param name="stationName">Station name for the schema.</param> /// <param name="deviceID">Device ID for the schema.</param> /// <param name="dataStartTime">Data start time.</param> /// <param name="sampleCount">Total data samples (i.e., total number of rows).</param> /// <param name="isBinary">Determines if data file should be binary or ASCII - defaults to <c>true</c> for binary.</param> /// <param name="timeFactor">Time factor to use in schema - defaults to 1000.</param> /// <param name="samplingRate">Desired sampling rate - defaults to 33.3333Hz.</param> /// <param name="nominalFrequency">Nominal frequency - defaults to 60Hz.</param> /// <param name="includeFracSecDefinition">Determines if the FRACSEC word digital definitions should be included - defaults to <c>true</c>.</param> /// <returns>New COMTRADE configuration <see cref="Schema"/>.</returns> /// <remarks> /// This function is primarily intended to create a configuration based on synchrophasor data /// (see Annex H: Schema for Phasor Data 2150 Using the COMTRADE File Standard in IEEE C37.111-2010), /// it may be necessary to manually create a schema object for other COMTRADE needs. You can call /// the <see cref="Schema.FileImage"/> property to return a string that that can be written to a file /// that will be the contents of the configuration file. /// </remarks> public static Schema CreateSchema(IEnumerable<ChannelMetadata> metadata, string stationName, string deviceID, Ticks dataStartTime, int sampleCount, bool isBinary = true, double timeFactor = 1.0D, double samplingRate = 30.0D, double nominalFrequency = 60.0D, bool includeFracSecDefinition = true) { Schema schema = new Schema(); schema.StationName = stationName; schema.DeviceID = deviceID; SampleRate samplingFrequency = new SampleRate(); samplingFrequency.Rate = samplingRate; samplingFrequency.EndSample = sampleCount; schema.SampleRates = new[] { samplingFrequency }; Timestamp startTime; startTime.Value = dataStartTime; schema.StartTime = startTime; schema.TriggerTime = startTime; schema.FileType = isBinary ? FileType.Binary : FileType.Ascii; schema.TimeFactor = timeFactor; List<AnalogChannel> analogChannels = new List<AnalogChannel>(); List<DigitalChannel> digitalChannels = new List<DigitalChannel>(); int analogIndex = 1; int digitalIndex = 1; if (includeFracSecDefinition) { // Add default time quality digitals for IEEE C37.118 FRACSEC word. Note that these flags, as // defined in Annex H of the IEEE C37.111-2010 standard, assume full export was all from one // source device. This a poor assumption since data can be exported from historical data for any // number of points which could have come from any number of devices all with different FRACSEC // values. Regardless there is only one FRACSEC definition defined and, if included, it must // come as the first set of digitals in the COMTRADE configuration. for (int i = 0; i < 4; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_CNT" + i, PhaseID = "T" + digitalIndex++ }); } digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSPND", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSOCC", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "TQ_LSDIR", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "RSV", PhaseID = "T" + digitalIndex++ }); for (int i = 1; i < 9; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex, Name = "RESV" + i, PhaseID = "T" + digitalIndex++ }); } } // Add meta data for selected points sorted analogs followed by status flags then digitals foreach (ChannelMetadata record in metadata.OrderBy(m => m, ChannelMetadataSorter.Default)) { if (record.IsDigital) { // Every synchrophasor digital is 16-bits for (int i = 0; i < 16; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name, PhaseID = "B" + i.ToString("X") }); } } else { switch (record.SignalType) { case SignalType.IPHM: // Current Magnitude analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pm", Units = "A", Multiplier = 0.05D }); break; case SignalType.VPHM: // Voltage Magnitude analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pm", Units = "V", Multiplier = 5.77362D }); break; case SignalType.IPHA: // Current Phase Angle case SignalType.VPHA: // Voltage Phase Angle analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "Pa", Units = "Rads", Multiplier = 1.0E-4D }); break; case SignalType.FREQ: // Frequency analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "F", Units = "Hz", Adder = (double)nominalFrequency, Multiplier = 0.001D }); break; case SignalType.DFDT: // Frequency Delta (dF/dt) analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "dF", Units = "Hz/s", Multiplier = 0.01D }); break; case SignalType.FLAG: // Status flags // Add synchrophasor status flag specific digitals int statusIndex = 0; for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":TRG" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 3; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":UNLK" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SEC" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":CFGCH", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":PMUTR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SORT", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":SYNC", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":PMUERR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel { Index = digitalIndex++, Name = record.Name + ":DTVLD", PhaseID = "S" + statusIndex.ToString("X") }); break; default: // All other signals assumed to be analog values analogChannels.Add(new AnalogChannel { Index = analogIndex++, Name = record.Name, PhaseID = "" }); break; } } } schema.AnalogChannels = analogChannels.ToArray(); schema.DigitalChannels = digitalChannels.ToArray(); schema.NominalFrequency = nominalFrequency; return schema; }
/// <summary> /// Creates a new COMTRADE configuration <see cref="Schema"/>. /// </summary> /// <param name="metadata">Schema <see cref="ChannelMetadata"/> records.</param> /// <param name="stationName">Station name for the schema.</param> /// <param name="deviceID">Device ID for the schema.</param> /// <param name="dataStartTime">Data start time.</param> /// <param name="sampleCount">Total data samples (i.e., total number of rows).</param> /// <param name="version">Target schema version - defaults to 1999.</param> /// <param name="fileType">Determines the data file type for the schema.</param> /// <param name="timeFactor">Time factor to use in schema - defaults to 1000.</param> /// <param name="samplingRate">Desired sampling rate - defaults to 33.3333Hz.</param> /// <param name="nominalFrequency">Nominal frequency - defaults to 60Hz.</param> /// <param name="includeFracSecDefinition">Determines if the FRACSEC word digital definitions should be included - defaults to <c>true</c>.</param> /// <returns>New COMTRADE configuration <see cref="Schema"/>.</returns> /// <remarks> /// <para> /// This function is primarily intended to create a configuration based on synchrophasor data /// (see Annex H: Schema for Phasor Data 2150 Using the COMTRADE File Standard in IEEE C37.111-2010), /// it may be necessary to manually create a schema object for other COMTRADE needs. You can call /// the <see cref="Schema.FileImage"/> property to return a string that can be written to a file /// that will be the contents of the configuration file. /// </para> /// <para> /// Linear scaling factors for analog channels, i.e., adders and multipliers, will be set to reasonable /// values based on the channel type. These should be adjusted as needed based on actual channel value /// ranges. Note that for <see cref="FileType.Float32"/> the multiplier will be <c>1.0</c> and the adder /// will be <c>0.0</c> for all analog values. /// </para> /// </remarks> public static Schema CreateSchema(IEnumerable <ChannelMetadata> metadata, string stationName, string deviceID, Ticks dataStartTime, int sampleCount, int version = 1999, FileType fileType = FileType.Binary, double timeFactor = 1.0D, double samplingRate = 30.0D, double nominalFrequency = 60.0D, bool includeFracSecDefinition = true) { Schema schema = new Schema { StationName = stationName, DeviceID = deviceID, Version = version }; SampleRate samplingFrequency = new SampleRate { Rate = samplingRate, EndSample = sampleCount }; schema.SampleRates = new[] { samplingFrequency }; Timestamp startTime; startTime.Value = dataStartTime; schema.StartTime = startTime; schema.TriggerTime = startTime; schema.FileType = fileType; schema.TimeFactor = timeFactor; List <AnalogChannel> analogChannels = new List <AnalogChannel>(); List <DigitalChannel> digitalChannels = new List <DigitalChannel>(); bool targetFloatingPoint = fileType == FileType.Float32; int analogIndex = 1; int digitalIndex = 1; if (includeFracSecDefinition) { // Add default time quality digitals for IEEE C37.118 FRACSEC word. Note that these flags, as // defined in Annex H of the IEEE C37.111-2010 standard, assume full export was all from one // source device. This a poor assumption since data can be exported from historical data for any // number of points which could have come from any number of devices all with different FRACSEC // values. Regardless there is only one FRACSEC definition defined and, if included, it must // come as the first set of digitals in the COMTRADE configuration. for (int i = 0; i < 4; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "TQ_CNT" + i, PhaseID = "T" + digitalIndex++ }); } digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "TQ_LSPND", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "TQ_LSOCC", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "TQ_LSDIR", PhaseID = "T" + digitalIndex++ }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "RSV", PhaseID = "T" + digitalIndex++ }); for (int i = 1; i < 9; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex, Name = "RESV" + i, PhaseID = "T" + digitalIndex++ }); } } // Add meta data for selected points sorted analogs followed by status flags then digitals foreach (ChannelMetadata record in metadata.OrderBy(m => m, ChannelMetadataSorter.Default)) { if (record.IsDigital) { // Every synchrophasor digital is 16-bits for (int i = 0; i < 16; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name, PhaseID = "B" + i.ToString("X") }); } } else { switch (record.SignalType) { case SignalType.IPHM: // Current Magnitude analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, Units = record.Units ?? "A", PhaseID = "Pm", CircuitComponent = record.CircuitComponent, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultCurrentMagnitudeMultiplier }); break; case SignalType.VPHM: // Voltage Magnitude analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, Units = record.Units ?? "V", PhaseID = "Pm", CircuitComponent = record.CircuitComponent, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultVoltageMagnitudeMultiplier }); break; case SignalType.IPHA: // Current Phase Angle case SignalType.VPHA: // Voltage Phase Angle analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, Units = record.Units ?? "Rads", PhaseID = "Pa", CircuitComponent = record.CircuitComponent, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultPhaseAngleMultiplier }); break; case SignalType.FREQ: // Frequency analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, Units = record.Units ?? "Hz", PhaseID = "F", CircuitComponent = record.CircuitComponent, Adder = targetFloatingPoint ? 0.0D : nominalFrequency, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultFrequencyMultiplier }); break; case SignalType.DFDT: // Frequency Delta (dF/dt) analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, Units = record.Units ?? "Hz/s", PhaseID = "dF", CircuitComponent = record.CircuitComponent, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultDfDtMultiplier }); break; case SignalType.FLAG: // Status flags // Add synchrophasor status flag specific digitals int statusIndex = 0; for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":TRG" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 3; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":UNLK" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } for (int i = 1; i < 5; i++) { digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":SEC" + i, PhaseID = "S" + statusIndex++.ToString("X") }); } digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":CFGCH", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":PMUTR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":SORT", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":SYNC", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":PMUERR", PhaseID = "S" + statusIndex++.ToString("X") }); digitalChannels.Add(new DigitalChannel(schema.Version) { Index = digitalIndex++, Name = record.Name + ":DTVLD", PhaseID = "S" + statusIndex.ToString("X") }); break; default: // All other signals assumed to be analog values analogChannels.Add(new AnalogChannel(schema.Version, targetFloatingPoint) { Index = analogIndex++, Name = record.Name, PhaseID = "", Units = record.Units, CircuitComponent = record.CircuitComponent, Multiplier = targetFloatingPoint ? 1.0D : AnalogChannel.DefaultAnalogMultipler }); break; } } } schema.AnalogChannels = analogChannels.ToArray(); schema.DigitalChannels = digitalChannels.ToArray(); schema.NominalFrequency = nominalFrequency; return(schema); }
// 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) { m_streamIndex++; // There is more to read if there is another file return(m_streamIndex < m_fileStreams.Length && ReadNext()); } if (bytesRead == recordLength) { int index = 0; // Read sample index uint sample = LittleEndian.ToUInt32(buffer, index); index += 4; // Get timestamp of this record m_timestamp = DateTime.MinValue; // If sample rates are defined, this is the preferred method for timestamp resolution if (m_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) { m_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 (m_timestamp == DateTime.MinValue) { m_timestamp = new DateTime(Ticks.FromMicroseconds(microseconds * m_schema.TimeFactor) + m_schema.StartTime.Value); } // Parse all analog record values for (int i = 0; i < m_schema.AnalogChannels.Length; i++) { // Read next value m_values[i] = LittleEndian.ToInt16(buffer, index) * m_schema.AnalogChannels[i].Multiplier + m_schema.AnalogChannels[i].Adder; index += 2; } int valueIndex = m_schema.AnalogChannels.Length; int digitalWords = m_schema.DigitalWords; ushort digitalWord; for (int i = 0; i < digitalWords; i++) { // Read next digital word 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 < m_values.Length; j++, valueIndex++) { m_values[valueIndex] = digitalWord.CheckBits(BitExtensions.BitVal(j)) ? 1.0D : 0.0D; } } } else { throw new InvalidOperationException("Failed to read enough bytes from COMTRADE file for a record as defined by schema - possible schema/data file mismatch or file corruption."); } return(true); }
// Handle ASCII file read private bool ReadNextAscii() { 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 != m_values.Length + 2) { if (reader.EndOfStream) { m_streamIndex++; // There is more to read if there is another file return(m_streamIndex < m_fileStreams.Length && ReadNext()); } 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 m_timestamp = DateTime.MinValue; // If sample rates are defined, this is the preferred method for timestamp resolution if (m_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) { m_timestamp = new DateTime(Ticks.FromSeconds(1.0D / sampleRate.Rate * sample) + m_schema.StartTime.Value); } } // Fall back on specified microsecond time if (m_timestamp == DateTime.MinValue) { m_timestamp = new DateTime(Ticks.FromMicroseconds(uint.Parse(elems[1]) * m_schema.TimeFactor) + m_schema.StartTime.Value); } // Parse all record values for (int i = 0; i < m_values.Length; i++) { m_values[i] = double.Parse(elems[i + 2]); if (i < m_schema.AnalogChannels.Length) { m_values[i] *= m_schema.AnalogChannels[i].Multiplier; m_values[i] += m_schema.AnalogChannels[i].Adder; } } return(true); }
// 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); }