Files
DP44/Common/DTS.Common.Serialization/FtssCsv/FtssTsv.File.Writer.cs
2026-04-17 14:55:32 -04:00

913 lines
47 KiB
C#

/*
* FtssTsv.File.Writer.cs
*
* Copyright © 2009
* Diversified Technical Systems, Inc.
* All Rights Reserved
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.IO;
using System.Text;
using DTS.Common.DAS.Concepts;
using DTS.Common.DAS.Concepts.DAS.Channel;
using DTS.Common.Utilities;
using DTS.Common.Utilities.DotNetProgrammingConstructs;
using DTS.Common.Utilities.Logging;
using DTS.Common;
using DTS.Common.Enums.Sensors;
using DTS.Common.Utils;
namespace DTS.Serialization.FtssTsv
{
// *** see FtssTsv.File.cs ***
public partial class File
{ ///
/// <summary>
/// Utility object for serializing <see cref="DTS.Serialization.Test"/>s to disk
/// in the FTSS CSV format.
/// </summary>
///
public partial class Writer : Writer<File>, IWriter<Test>
{
/// <summary>
/// Initialize an instance of the FtssTsv.File.Writer class.
/// </summary>
///
/// <param name="fileType">
/// The associated <see cref="DTS.SErialization.FtssTsv.File"/> object.
/// </param>
///
internal Writer(File fileType, int encoding)
: base(fileType, encoding)
{
}
/// <summary>
/// The different export modes that should theoretically be supported by this format.
/// </summary>
public enum ExportMode
{
FtssExcel,
Ttc,
Standard,
}
private const string NUMBER_FORMAT = "F8";
private const string TAB_LIST_SEPARATOR = "\t"; //Tab
/// <summary>
/// Get/set the current CSV export format.
/// </summary>
public ExportMode CurrentExportMode
{
get => _CurrentExportMode.Value;
set => _CurrentExportMode.Value = value;
}
private readonly Property<ExportMode> _CurrentExportMode
= new Property<ExportMode>(
typeof(Writer).Namespace + ".File.Writer.CurrentExportMode",
ExportMode.FtssExcel,
true
);
/// <summary>
/// Notify <see cref="DTS.Serialization.BeginEventHandler"/> subscribers that the write
/// is starting.
/// </summary>
public event BeginEventHandler OnBegin;
/// <summary>
/// Notify <see cref="DTS.Serialization.EndEventHandler"/> subscribers that the write
/// is finished.
/// </summary>
public event EndEventHandler OnEnd;
/// <summary>
/// Notify <see cref="DTS.Serialization.TickEventHandler"/> subscribers that we are one
/// tick closer to write completion.
/// </summary>
public event TickEventHandler OnTick;
/// <summary>
/// notify subscribers that the write was cancelled
/// </summary>
public event CancelEventHandler OnCancel;
/// <summary>
/// notify subscribers that the writer encountered fatal error
/// </summary>
public event ErrorEventHandler OnError;
/// <summary>
/// The number of data samples that need to be written for a "tick" to be dispatched.
/// </summary>
private int DataSamplesPerTick => 1000;
/// <summary>
/// Return the number of data to be written per "tick".
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
private uint GetChannelTicks(Test.Module.Channel channel)
{
try
{ //
// Most of our wait time will be spent writing data, so we need to give
// the process a little finer granularity.
//
return (uint)(channel.PersistentChannelInfo.Length / DataSamplesPerTick);
}
catch (System.Exception ex)
{
throw new Exception("encountered problem determining number of status ticks for channel " + (null != channel ? "\"" + channel.Number.ToString() + "\"" : "<NULL>"), ex);
}
}
/// <summary>
/// Get/set the filtered channel data. If this list is supplied, the corresponding test
/// channel data values will be supplied from this list.
/// </summary>
public List<FilteredData> FilteredChannelData
{
get => _FilteredChannelData.Value;
set => _FilteredChannelData.Value = value;
}
private readonly Property<List<FilteredData>> _FilteredChannelData
= new Property<List<FilteredData>>(
"FilteredChannelData",
new List<FilteredData>(),
true
);
public string DataChannelFilename { get; set; }
public string LaboratoryName { get; set; }
public string LaboratoryContactName { get; set; }
public string LaboratoryContactPhone { get; set; }
public string LaboratoryContactEmail { get; set; }
public string TestEngineerName { get; set; }
public string TestEngineerPhone { get; set; }
public string TestEngineerEmail { get; set; }
public int NumChannelsWritten { get; set; }
public bool UseISOCodeFilterMapping { get; set; }
public bool UseZeroForUnfiltered { get; set; }
/// <summary>
/// Write the specified test to the specified pathname.
/// </summary>
///
/// <param name="pathname">
/// The <see cref="string"/> pathname to which the specified test should be serialized.
/// </param>
///
/// <param name="test">
/// The <see cref="DTS.Serialization.Test"/> to be written out.
/// </param>
///
public void Write(string pathname, string id, Test test, bool bFiltering, bool includeGroupNameInISOExport, double minStartTime, int dataCollectionLength)
{
try
{
Write(pathname, id, null, test, bFiltering, includeGroupNameInISOExport, null, null, 0, null, null, null, null, null, null, minStartTime, dataCollectionLength);
}
catch (System.Exception ex)
{
throw new Exception("encountered problem non-event notified writing test", ex);
}
}
/// <summary>
/// Write the representation file/files of the specified DTS.Serialization.Test
/// at the given pathname.
/// </summary>
///
/// <param name="targetPathname">
/// The <see cref="string"/> pathname of the specified object's resulting file
/// representation.
/// </param>
///
public void Write(string pathname,
string id,
string dataFolder,
Test test,
bool bFiltering,
bool includeGroupNameInISOExport,
FilteredData fd,
Test.Module.Channel tmChannel,
int channelNumber,
BeginEventHandler beginEventHandler,
CancelEventHandler cancelEventHandler,
EndEventHandler endEventHandler,
TickEventHandler tickEventHandler,
ErrorEventHandler errorEventHandler,
CancelRequested cancelRequested,
double minStartTime,
int dataCollectionLength)
{
System.Exception exception = null;
try
{
//this.OnBegin += beginEventHandler; //Since we are called once for every file, we don't want to continually set progress to 0%
//this.OnEnd += endEventHandler; //Nor do we want to increment the number of exports that have finished
OnTick += tickEventHandler;
OnCancel += cancelEventHandler;
OnError += errorEventHandler;
// Compute the total number of write ticks that will be dispatched during this
// write, and let the caller know that we're underway.
uint channelTicks = 0;
uint totalWriteTicksNeeded = 0;
if (test.Channels.Count > 0)
{
channelTicks = GetChannelTicks(test.Channels[0]);
totalWriteTicksNeeded = channelTicks * (uint)test.Channels.Count;
}
var totalWriteTicksDispatched = channelTicks * (uint)NumChannelsWritten;
APILogger.Log("opening ", pathname);
//DateTime start = DateTime.Now;
if (!Directory.Exists(Path.GetDirectoryName(pathname)))
{
Directory.CreateDirectory(Path.GetDirectoryName(pathname));
}
if (System.IO.File.Exists(pathname))
{
FileUtils.DeleteFileOrMove(pathname, APILogger.Log);
}
using (var fileWriter = new StreamWriter(pathname, false, Encoding.Default))
{
//List<Test.Module.Channel> exportChannels = test.Channels;
switch (CurrentExportMode)
{
case ExportMode.FtssExcel:
WriteChannelInfo(fileWriter,
id,
test,
FilteredChannelData,
pathname,
tickEventHandler,
totalWriteTicksNeeded,
ref totalWriteTicksDispatched,
cancelRequested,
channelNumber,
minStartTime,
dataCollectionLength);
break;
default:
throw new NotSupportedException("FtssTsv::File::Writer ExportMode not supported: " + CurrentExportMode.ToString());
}
}
}
catch (System.Exception ex)
{
APILogger.Log("encountered problem writing TSV test files", ex);
exception = new Exception("encountered problem writing TSV test files", ex);
}
finally
{
if (null != cancelRequested && cancelRequested())
{
OnCancel?.Invoke(this);
}
if (null != exception && null != errorEventHandler)
{
errorEventHandler(this, exception);
}
else if (null != exception)
{
throw exception;
}
}
}
public void Initialize(string pathname,
string id,
string dataFolder,
Test test,
bool bFiltering,
bool includeGroupNameInISOExport,
FilteredData fd,
Test.Module.Channel tmChannel,
int channelNumber,
BeginEventHandler beginEventHandler,
CancelEventHandler cancelEventHandler,
EndEventHandler endEventHandler,
TickEventHandler tickEventHandler,
ErrorEventHandler errorEventHandler,
CancelRequested cancelRequested)
{
}
/// <summary>
/// The various header lines (and associated text) that appear in this file format.
/// </summary>
private enum FtssHeaderLine
{
[Description("Headers")]
Headers = 0,
[Description("Test Date")]
TestDate,
[Description("Test Time")]
TestTime,
[Description("Test ID")]
TestId,
[Description("Test Description")]
TestDescription,
[Description("Laboratory Name")]
LaboratoryName,
[Description("Laboratory Contact Name")]
LaboratoryContactName,
[Description("Laboratory Contact Phone")]
LaboratoryContactPhone,
[Description("Laboratory Contact Email")]
LaboratoryContactEmail,
[Description("Test Engineer Name")]
TestEngineerName,
[Description("Test Engineer Phone")]
TestEngineerPhone,
[Description("Test Engineer Email")]
TestEngineerEmail,
[Description("Sample Rate (Hz)")]
SampleRate,
[Description("Hardware AA Filter (-3dB)")]
HardwareAntiAliasFilter,
[Description("Data Channel Name")]
DataChannelName,
[Description("ISO Channel Code")]
IsoCode,
[Description("Channel Description")]
ChannelDescription,
[Description("Channel Location")]
ChannelLocation,
[Description("Sensor S/N")]
SensorSerialNumber,
[Description("Software Filter (SAE Class)")]
SoftwareFilter,
[Description("Software Filter (-3dB)")]
SoftwareFilterDb,
[Description("Engineering Unit")]
EngineeringUnits,
[Description("User Comment")]
UserComment,
[Description("Number of Pre-Zero Data Pts")]
PreZero,
[Description("Number of Post-Zero Data Pts")]
PostZero,
[Description("Data Zero (CNTS)")]
DataZero,
[Description("Scale Factor (EU/CNT)")]
ScaleEu,
[Description("Scale Factor (mV/CNT)")]
ScaleMv,
[Description("Data Starts Here")]
DataStart,
[Description("Time")]
Labels,
}
/// <summary>
/// Generate a best-estimate export size for the specified dataset, and check it against the
/// current disk availability stats.
/// </summary>
///
/// <param name="testname">
/// The <see cref="string"/> name of the test to be verified.
/// </param>
///
/// <param name="saveLocation">
/// The <see cref="string"/> name of the target location into which the test must fit.
/// </param>
///
/// <param name="headerLines">
/// An <see cref="System.Collections.Generic.IDictionary<TKey,TValue>"/> containing pairs
/// of header entry name <see cref="string"/>s and corresponding value <see cref="string"/>s.
/// </param>
///
/// <param name="coder">
/// A <see cref="DescriptionAttributeCoder<TargetType>"/> for en/coding
/// <see cref="DTS.Serialization.FtssTsv.File.Writer.FtssHeaderLine"/> header enumerations.
/// </param>
///
/// <param name="channelsWithMeta">
/// A populated <see cref="System.Collections.Generic.List<T>"/> of
/// <see cref="DTS.Serialization.FtssTsv.File.Writer.ChannelWithMeta"/>s generated from
/// the test to be exported.
/// </param>
///
/// <param name="dataCollectionLength">
/// The <see cref="int"/> length of the data collection within this test that is to be
/// exported.
/// </param>
///
/// <param name="minStartTime">
/// The earliest <see cref="double"/> T0-relative start time on any channel within the
/// test to be exported.
/// </param>
///
/// <param name="sampleRate">
/// The <see cref="double"/> sample rate of the test to be exported.
/// </param>
///
private void VerifyExportedFileWillFitOnDisk(
string testname,
string saveLocation,
IDictionary<FtssHeaderLine, List<string>> headerLines,
DescriptionAttributeCoder<FtssHeaderLine> coder,
List<ChannelWithMeta> channelsWithMeta,
int dataCollectionLength,
double minStartTime,
double sampleRate,
CancelRequested cancelRequested
)
{
try
{ //
// Compute the size of the header information.
//
ulong predictedExportSize = 0;
foreach (FtssHeaderLine ftssSizeHeaderLine in Enum.GetValues(typeof(FtssHeaderLine)))
{
if (headerLines.Keys.Contains(ftssSizeHeaderLine))
{
var channelValues = headerLines[ftssSizeHeaderLine];
predictedExportSize += (ulong)(coder.DecodeAttributeValue(ftssSizeHeaderLine).Length + TAB_LIST_SEPARATOR.Length);
foreach (var channelValue in channelValues)
predictedExportSize += (ulong)(channelValue.Length + TAB_LIST_SEPARATOR.Length);
}
}
//
// Compute the size of the data portion.
// this computation is expensive for large data collections, so lets just subsample them for now
// for every 2 million samples collected we'll increase the size of subsamples by 1.
var segmentSize = dataCollectionLength / 2000000;
if (segmentSize < 1) { segmentSize = 1; }
for (var i = 0; i < dataCollectionLength; i += segmentSize)
{
if (null != cancelRequested && cancelRequested()) { break; }
var thisSegmentSize = (ulong)((minStartTime + i * 1.0 / sampleRate).ToString(NUMBER_FORMAT) + TAB_LIST_SEPARATOR).Length;
foreach (var channelWithMeta in channelsWithMeta)
{
if (null != cancelRequested && cancelRequested()) { break; }
var thisChannelsIndexAtCurrentTime = i - (int)((channelWithMeta.StartTime - minStartTime) * channelWithMeta.SampleRate);
thisSegmentSize += thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime < channelWithMeta.Channel.PersistentChannelInfo.Length ?
9U : 1U;
}
var segmentJump = i + segmentSize > dataCollectionLength ? dataCollectionLength - i : segmentSize;
predictedExportSize += thisSegmentSize * (ulong)segmentJump;
}
predictedExportSize *= sizeof(char);
// Get the stats on available disk space.
var errorcode = DiskUtility.GetDiskFreeSpaceEx(saveLocation, out ulong freeBytesAvailable, out ulong totalNumberOfBytes, out ulong totalNumberOfFreeBytes);
if (0 != errorcode)
{
// Do the comparison.
if (freeBytesAvailable < predictedExportSize)
{
var bytesNeeded = DiskUtility.GetHumanReadableBytes(predictedExportSize);
var bytesAvailable = DiskUtility.GetHumanReadableBytes(freeBytesAvailable);
throw new UserException("Export requires " + bytesNeeded + " but there are only " + bytesAvailable + " bytes available on \"" + saveLocation + "\"");
}
}
else
{
APILogger.Log("Failed to get free disk space, windows error: ", errorcode);
}
}
catch (System.Exception ex)
{
throw new Exception("encountered problem trying to determing if CSV export of test " + (testname ?? "<NULL>") + " will fit at location " + (saveLocation ?? "<NULL>"), ex);
}
}
/// <summary>
/// Write the specified test to the specified stream.
/// </summary>
///
/// <param name="fileWriter">
/// The <see cref="System.IO.StreamWriter"/> to which the specified test should be serialized.
/// </param>
///
/// <param name="test">
/// The <see cref="DTS.Serialization.Test"/> to be serialized.
/// </param>
///
/// <param name="writeEvent">
/// The <see cref="FtssTsv.File.Writer.EventHandler"/> that should handle write-progress
/// reporting; null if none is to be used.
/// <param name="targetPath">
/// The <see cref="string"/> path that the file writer will be writing to.
/// </param>
///
/// <param name="tickEventHandler">
/// The <see cref="DTS.Serialization.TickEventHandler"/> that will process "tick" events
/// indicating progress during the export procedure.
/// </param>
///
/// <param name="totalWriteTicksNeeded">
/// The total <see cref="uint"/> number of progress ticks that will be issued during the
/// export procedure.
/// </param>
///
/// <param name="totalWriteTicksDispatched">
/// Returns the total <see cref="uint"/> number of tick events that have been dispatched.
/// </param>
///
protected void WriteChannelInfo
(
StreamWriter fileWriter,
string id,
Test test,
List<FilteredData> filteredData,
string targetPath,
TickEventHandler tickEventHandler,
uint totalWriteTicksNeeded,
ref uint totalWriteTicksDispatched,
CancelRequested cancelRequested,
int channelNumber,
double minStartTime,
int dataCollectionLength)
{
try
{
//FB14621: Get the sample rate of the channels we're going to care about (i.e. channels with meta), rather than the whole test/roi
var sampleRate = test.Channels.Find(ch => DataChannelFilename == Path.GetFileName(((Test.Module.AnalogInputChannel)ch).FileName)).ParentModule.SampleRateHz;
if (1 < SubSampleInterval)
{
var ChannelList = test.Channels;
for (var iChannel = 0; iChannel < ChannelList.Count; iChannel++)
{
if (true == Filtered)
{
var SubSample = new NHTSASubSample<double>();
SubSample.data = FilteredChannelData[iChannel].Data;
SubSample.preTriggerSamples = test.Modules[0].TriggerSampleNumbers[0] - test.Modules[0].StartRecordSampleNumber;
SubSample.sampleRate = sampleRate;
SubSample.subSampleInterval = SubSampleInterval;
SubSample.SubSample();
FilteredChannelData[iChannel].Data = SubSample.data;
ChannelList[iChannel].IsSubsampled = true; // I don't think anyone will consume this, but for completeness.
}
else
{
var SubSample = new NHTSASubSample<short>();
SubSample.data = ChannelList[iChannel].PersistentChannelInfo.Data;
SubSample.preTriggerSamples = test.Modules[0].TriggerSampleNumbers[0] - test.Modules[0].StartRecordSampleNumber;
SubSample.sampleRate = sampleRate;
SubSample.subSampleInterval = SubSampleInterval;
SubSample.SubSample();
ChannelList[iChannel].PersistentChannelInfo.Data = SubSample.data;
ChannelList[iChannel].IsSubsampled = true; // I don't think anyone will consume this, but for completeness.
}
}
sampleRate = sampleRate / SubSampleInterval;
}
//
// Initialize the header information.
//
var coder = new DescriptionAttributeCoder<FtssHeaderLine>();
IDictionary<FtssHeaderLine, List<string>> headerLines = new Dictionary<FtssHeaderLine, List<string>>();
headerLines.Add(FtssHeaderLine.Headers, new List<string>());
headerLines.Add(FtssHeaderLine.TestDate, new List<string>());
headerLines.Add(FtssHeaderLine.TestTime, new List<string>());
headerLines.Add(FtssHeaderLine.TestId, new List<string>());
headerLines.Add(FtssHeaderLine.TestDescription, new List<string>());
headerLines.Add(FtssHeaderLine.LaboratoryName, new List<string>());
headerLines.Add(FtssHeaderLine.LaboratoryContactName, new List<string>());
headerLines.Add(FtssHeaderLine.LaboratoryContactPhone, new List<string>());
headerLines.Add(FtssHeaderLine.LaboratoryContactEmail, new List<string>());
headerLines.Add(FtssHeaderLine.TestEngineerName, new List<string>());
headerLines.Add(FtssHeaderLine.TestEngineerPhone, new List<string>());
headerLines.Add(FtssHeaderLine.TestEngineerEmail, new List<string>());
headerLines.Add(FtssHeaderLine.SampleRate, new List<string>());
headerLines.Add(FtssHeaderLine.HardwareAntiAliasFilter, new List<string>());
headerLines.Add(FtssHeaderLine.DataChannelName, new List<string>());
headerLines.Add(FtssHeaderLine.IsoCode, new List<string>());
headerLines.Add(FtssHeaderLine.ChannelDescription, new List<string>());
headerLines.Add(FtssHeaderLine.ChannelLocation, new List<string>());
headerLines.Add(FtssHeaderLine.SensorSerialNumber, new List<string>());
headerLines.Add(FtssHeaderLine.SoftwareFilter, new List<string>());
headerLines.Add(FtssHeaderLine.SoftwareFilterDb, new List<string>());
headerLines.Add(FtssHeaderLine.EngineeringUnits, new List<string>());
headerLines.Add(FtssHeaderLine.UserComment, new List<string>());
headerLines.Add(FtssHeaderLine.PreZero, new List<string>());
headerLines.Add(FtssHeaderLine.PostZero, new List<string>());
headerLines.Add(FtssHeaderLine.DataZero, new List<string>());
headerLines.Add(FtssHeaderLine.ScaleEu, new List<string>());
headerLines.Add(FtssHeaderLine.ScaleMv, new List<string>());
headerLines.Add(FtssHeaderLine.DataStart, new List<string>());
headerLines.Add(FtssHeaderLine.Labels, new List<string>());
var channelsWithMeta = new List<ChannelWithMeta>();
var allChannelsWithMeta = new List<ChannelWithMeta>();
var filteredDataIndex = 0;
foreach (var channel in test.Channels)
{
if (Path.GetFileName((channel as Test.Module.AnalogInputChannel).FileName) == DataChannelFilename)
{
//
// Tack on the header information for each channel.
//
var filteredChannelData = null != filteredData && filteredData.Count == 1 ? filteredData[0] : null;
headerLines[FtssHeaderLine.Headers].Add(TAB_LIST_SEPARATOR);
headerLines[FtssHeaderLine.TestDate].Add(test.InceptionDate.ToShortDateString());
headerLines[FtssHeaderLine.TestTime].Add(test.InceptionDate.ToLongTimeString());
headerLines[FtssHeaderLine.TestId].Add(id);
headerLines[FtssHeaderLine.TestDescription].Add(test.Description);
headerLines[FtssHeaderLine.LaboratoryName].Add(LaboratoryName);
headerLines[FtssHeaderLine.LaboratoryContactName].Add(LaboratoryContactName);
headerLines[FtssHeaderLine.LaboratoryContactPhone].Add(LaboratoryContactPhone);
headerLines[FtssHeaderLine.LaboratoryContactEmail].Add(LaboratoryContactEmail);
headerLines[FtssHeaderLine.TestEngineerName].Add(TestEngineerName);
headerLines[FtssHeaderLine.TestEngineerPhone].Add(TestEngineerPhone);
headerLines[FtssHeaderLine.TestEngineerEmail].Add(TestEngineerEmail);
headerLines[FtssHeaderLine.SampleRate].Add(channel.ParentModule.SampleRateHz.ToString("F0"));
headerLines[FtssHeaderLine.HardwareAntiAliasFilter].Add(channel.ParentModule.AaFilterRateHz.ToString("F0"));
headerLines[FtssHeaderLine.DataChannelName].Add(DataChannelFilename);
//13157 Software filter class (SAE Class) incorrect in TSV filtered export.
//if we are filtering AND we have use isocode filter mapping on, then adjust the isocode to match
var isocode = channel is IIsoCodeAware ? (channel as IIsoCodeAware).IsoCode : "";
var swFilter = Filtered && filteredChannelData != null ? filteredChannelData.FilterDescription : "NONE";
if (UseISOCodeFilterMapping)
{
var iso = new Common.ISO.IsoCode(isocode);
iso.FilterClass = CFCFilterDTSFileStringConverter.GetIsoCodeFromString(swFilter, UseZeroForUnfiltered);
isocode = iso.StringRepresentation;
}
headerLines[FtssHeaderLine.IsoCode].Add(isocode);
headerLines[FtssHeaderLine.ChannelDescription].Add(channel.ChannelDescriptionString);
headerLines[FtssHeaderLine.ChannelLocation].Add(channel.ChannelName2);
headerLines[FtssHeaderLine.SensorSerialNumber].Add(channel is ISerialNumberAware ? (channel as ISerialNumberAware).SerialNumber : "");
headerLines[FtssHeaderLine.SoftwareFilter].Add(swFilter);
headerLines[FtssHeaderLine.SoftwareFilterDb].Add(Filtered && filteredChannelData != null ? filteredChannelData.FilterFrequencyHz.ToString("F0") : "NONE");
headerLines[FtssHeaderLine.EngineeringUnits].Add(channel is IEngineeringUnitAware ? (channel as IEngineeringUnitAware).EngineeringUnits.TrimEnd() : "");
headerLines[FtssHeaderLine.UserComment].Add("");
var preTriggerSamples = channel.ParentModule.TriggerSampleNumbers[0] - (double)channel.ParentModule.StartRecordSampleNumber;
if (preTriggerSamples < 0) { preTriggerSamples = 0D; }
if (preTriggerSamples > channel.ParentModule.NumberOfSamples) { preTriggerSamples = channel.ParentModule.NumberOfSamples; }
if (preTriggerSamples > Start * sampleRate) { preTriggerSamples = Math.Truncate(Start * sampleRate); }
headerLines[FtssHeaderLine.PreZero].Add((preTriggerSamples / SubSampleInterval).ToString());
var postTriggerSamples = channel.ParentModule.NumberOfSamples - preTriggerSamples;
if (postTriggerSamples < 0) { postTriggerSamples = 0D; }
if (postTriggerSamples > Stop * sampleRate) { postTriggerSamples = Math.Truncate(Stop * sampleRate); }
headerLines[FtssHeaderLine.PostZero].Add((postTriggerSamples / SubSampleInterval).ToString());
headerLines[FtssHeaderLine.DataZero].Add(channel.DataZeroLevelAdc.ToString());
var scaler = new DataScaler();
scaler.IsInverted = channel is IInversionAware ? (channel as IInversionAware).IsInverted : false;
if ((channel as ILinearized).LinearizationFormula.IsValid())
{
scaler.SetLinearizationFormula((channel as ILinearized).LinearizationFormula);
}
else
{
scaler.SetLinearizationFormula(null);
}
scaler.Digital = (channel as Test.Module.AnalogInputChannel).IsDigital();
scaler.SetDigitalMultiplier((channel as Test.Module.AnalogInputChannel).DigitalMultiplier);
scaler.DigitalMode = (channel as Test.Module.AnalogInputChannel).DigitalMode;
scaler.SetScaleFactorMv(channel.Data.ScaleFactorMv);
scaler.SetScaleFactorEU(channel.Data.ScaleFactorEU);
scaler.SetUseEUScaleFactors(channel.Data.UseEUScaleFactors);
scaler.UnitConversion = (channel as Test.Module.AnalogInputChannel).UnitConversion;
scaler.BasedOnOutputAtCapacity = (channel as Test.Module.AnalogInputChannel).AtCapacity;
scaler.CapacityOutputIsBasedOn = (channel as Test.Module.AnalogInputChannel).CapacityOutputIsBasedOn;
scaler.SensitivityUnits = (channel as Test.Module.AnalogInputChannel).SensitivityUnits;
scaler.Multiplier = (channel as Test.Module.AnalogInputChannel).Multiplier;
if (channel is Test.Module.AnalogInputChannel)
{
scaler.IEPE = (channel as Test.Module.AnalogInputChannel).Bridge == SensorConstants.BridgeType.IEPE;
scaler.Digital = (channel as Test.Module.AnalogInputChannel).Bridge == SensorConstants.BridgeType.DigitalInput;
}
scaler.SetMvPerEu(channel.Data.MvPerEu);
scaler.SetDataZeroLevelADC(channel.DataZeroLevelAdc);
scaler.SetRemovedADC(channel.RemovedADC);
scaler.SetRemovedInternalADC(channel.RemovedInternalADC);
scaler.SetZeroMvInADC(channel.ZeroMvInADC);
try { scaler.SetWindowAverageADC(channel.WindowAverageADC); }
catch (System.Exception) { }
if (channel is Test.Module.AnalogInputChannel)
{ //
// Maybe these should be replaced by DTS.DAS.Concepts versions for casting? Would need to
// add excitation voltage as a concept, and proportional to excitation.
//
var analogChannel = channel as Test.Module.AnalogInputChannel;
scaler.SetInitialOffset(analogChannel.InitialOffset);
scaler.ZeroMethodType = analogChannel.ZeroMethod;
scaler.NominalExcitationVoltage = analogChannel.ExcitationVoltage;
if (analogChannel.MeasuredExcitationVoltageValid)
{
try { scaler.MeasuredExcitationVoltage = analogChannel.MeasuredExcitationVoltage; }
catch { };
}
if (analogChannel.FactoryExcitationVoltageValid)
{
try { scaler.FactoryExcitationVoltage = analogChannel.FactoryExcitationVoltage; }
catch { };
}
scaler.ProportionalToExcitation = analogChannel.ProportionalToExcitation;
}
var dStartTime = channel.ParentModule.StartRecordSampleNumber / (double)channel.ParentModule.SampleRateHz;
if (channel.ParentModule.TriggerSampleNumbers.Count > 0)
{
dStartTime -= channel.ParentModule.TriggerSampleNumbers[0] / (double)channel.ParentModule.SampleRateHz;
}
channelsWithMeta.Add(
new ChannelWithMeta(
channel,
scaler,
channel.ParentModule.SampleRateHz,
dStartTime
)
);
headerLines[FtssHeaderLine.ScaleEu].Add(scaler.GetAdcToEuScalingFactor().ToString());
headerLines[FtssHeaderLine.ScaleMv].Add(scaler.GetAdcToMvScalingFactor().ToString());
headerLines[FtssHeaderLine.DataStart].Add(TAB_LIST_SEPARATOR);
headerLines[FtssHeaderLine.Labels].Add(channel.ChannelDescriptionString);
break;
}
}
//FB14621: Recalculate minStartTime and dataCollectionLength here after pulling out channelsWithMeta,
// rather than in Export which is doing the whole test/ROI
minStartTime = ChannelWithMeta.GetMinStartTime(Start, channelsWithMeta);
var minStopTime = ChannelWithMeta.GetMinStopTime(Stop, channelsWithMeta);
dataCollectionLength = (int)((minStopTime - minStartTime) * sampleRate);
VerifyExportedFileWillFitOnDisk(test.Id,
targetPath.Remove(targetPath.IndexOf(Path.VolumeSeparatorChar) + 1),
headerLines,
coder,
channelsWithMeta,
dataCollectionLength,
minStartTime,
sampleRate,
cancelRequested);
// Apply filtering to channel data at this point?
// Thinking maybe since all the cool filter utilities are up in the Slice Control layer, it
// may just be a better idea to create some filtered test at that level using the event channels
// and then just add some code here that gracefully handles Data-data instead of PersistentChannelInfo
// data...?
//Dictionary<ChannelWithMeta, double[ ]> filteredData = new Dictionary<ChannelWithMeta, double[ ]>( );
//SaeJ211.FilterUtility filterUtility = new SaeJ211.FilterUtility( );
//foreach ( ChannelWithMeta channelWithMeta in channelsWithMeta )
//{
// this.CurrentFilter = Slice.Control.Event.Module.Channel.DefaultSaeJ211Filter.Parse( thatAnalogChannel.SoftwareFilter );
// filterUtility.SampleRate = channelWithMeta.SampleRate;
// filterUtility.Cfc = ( channelWithMeta.Channel as Test.Module.AnalogInputChannel ).SoftwareFilter;
//}
foreach (FtssHeaderLine ftssHeaderLine in Enum.GetValues(typeof(FtssHeaderLine)))
{ //
// Write out the assembled header information to the specified filestream.
//
if (headerLines.Keys.Contains(ftssHeaderLine))
{
var channelValues = headerLines[ftssHeaderLine];
fileWriter.Write(coder.DecodeAttributeValue(ftssHeaderLine) + TAB_LIST_SEPARATOR);
foreach (var channelValue in channelValues)
fileWriter.Write(channelValue + TAB_LIST_SEPARATOR);
fileWriter.WriteLine();
}
}
for (var i = 0; i <= dataCollectionLength; i++)
{
if (null != cancelRequested && cancelRequested()) { break; }
fileWriter.Write(string.Format("{0}{1}", (minStartTime + i * 1.0 / sampleRate).ToString(NUMBER_FORMAT), TAB_LIST_SEPARATOR));
var bNeedComma = false;
foreach (var channelWithMeta in channelsWithMeta)
{
if (bNeedComma) { fileWriter.Write(TAB_LIST_SEPARATOR); }
else { bNeedComma = true; }
//
// If this channel's data is valid at the current time/index then go ahead and send it out
// to the file; otherwise write out a blank space.
//
var thisChannelsIndexAtCurrentTime = i - (int)((channelWithMeta.StartTime - minStartTime) * channelWithMeta.SampleRate);
if (Filtered)
{
if (filteredDataIndex < filteredData.Count)
{
if (thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime < filteredData[filteredDataIndex].Data.Length)
{
fileWriter.Write(filteredData[filteredDataIndex].Data[thisChannelsIndexAtCurrentTime].ToString(NUMBER_FORMAT));
}
}
else
{
fileWriter.Write(double.NaN.ToString());
}
}
else
{
double dInitialEU = 0;
var aChannel = channelWithMeta.Channel as Test.Module.AnalogInputChannel;
if (null != aChannel) { dInitialEU = aChannel.InitialEu; }
if (thisChannelsIndexAtCurrentTime >= 0 && (ulong)thisChannelsIndexAtCurrentTime < aChannel.ParentModule.NumberOfSamples)
{
fileWriter.Write(channelWithMeta.Scaler.GetEU(channelWithMeta.Channel.PersistentChannelInfo[thisChannelsIndexAtCurrentTime]).ToString(NUMBER_FORMAT));
}
}
filteredDataIndex++;
}
if (0 == i % DataSamplesPerTick)
{
OnTick?.Invoke(this, (double)totalWriteTicksDispatched++ / totalWriteTicksNeeded * 100);
System.Windows.Forms.Application.DoEvents();
}
fileWriter.WriteLine();
filteredDataIndex = 0;
}
fileWriter.Flush();
}
catch (System.Exception ex)
{
throw new Exception("encountered problem writing FTSS/Excel channel headers", ex);
}
}
public double Start { get; set; } = 0D;
public double Stop { get; set; } = 0D;
public ushort SubSampleInterval { get; set; }
public bool Filtered { get; set; }
}
}
}