Files
DP44/Common/DTS.Common.Serialization/ToyotaCsv.File.Writer.cs

586 lines
28 KiB
C#
Raw Normal View History

2026-04-17 14:55:32 -04:00
/*
* ToyotaCsv.File.Writer.cs
*
* Copyright © 2009
* Diversified Technical Systems, Inc.
* All Rights Reserved
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using DTS.Common.DAS.Concepts;
using DTS.Common.Utilities.DotNetProgrammingConstructs;
using DTS.Common.Enums.Sensors;
using DTS.Common.Utilities.Logging;
namespace DTS.Serialization.ToyotaCsv
{
// *** see ToyotaCsv.File.cs ***
public partial class File
{ ///
/// <summary>
/// Utility object for serializing <see cref="DTS.Serialization.Test"/>s to disk
/// in the Toyota CSV format.
/// </summary>
///
public class Writer : Writer<File>, IWriter<Test>
{
/// <summary>
/// Initialize an instance of the ToyotaCsv.File.Writer class.
/// </summary>
///
/// <param name="fileType">
/// The associated <see cref="DTS.SErialization.ToyotaCsv.File"/> object.
/// </param>
///
internal Writer(File fileType, int encoding)
: base(fileType, encoding)
{
}
private const string NUMBER_FORMAT = "F8";
/// <summary>
/// The different export modes that should theoretically be supported by this format.
/// </summary>
public enum ExportMode
{
FtssExcel,
Ttc,
Standard,
}
/// <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 writing is cancelling
/// </summary>
public event CancelEventHandler OnCancel;
/// <summary>
/// notify subscribers that 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>
/// 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>
/// 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<double[]> FilteredChannelData
{
get => _FilteredChannelData.Value;
set => _FilteredChannelData.Value = value;
}
private readonly Property<List<double[]>> _FilteredChannelData
= new Property<List<double[]>>(
typeof(Writer).FullName + ".FilteredChannelData",
new List<double[]>(),
true
);
/// <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
{
OnBegin += beginEventHandler;
OnEnd += endEventHandler;
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 totalWriteTicksNeeded = 0;
uint totalWriteTicksDispatched = 0;
if (test.Channels.Count > 0)
totalWriteTicksNeeded = GetChannelTicks(test.Channels[0]);
OnBegin?.Invoke(this, totalWriteTicksNeeded);
var fs = new FileStream(pathname, FileMode.Create);
using (var fileWriter = new StreamWriter(fs, Encoding.Unicode))
{
var exportChannels = test.Channels;
switch (CurrentExportMode)
{
case ExportMode.FtssExcel:
case ExportMode.Ttc:
WriteChannelInfo(fileWriter,
test,
FilteredChannelData,
tickEventHandler,
totalWriteTicksNeeded,
ref totalWriteTicksDispatched,
cancelRequested);
break;
default:
throw new NotSupportedException("ToyotaCSV::File::Writer export mode not supported: " + CurrentExportMode.ToString());
}
}
}
catch (System.Exception ex)
{
exception = new Exception("encountered problem writing TTS test files", ex);
APILogger.Log("encountered problem writing TTS test files", ex);
}
finally
{
OnEnd?.Invoke(this);
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>
/// A collection of a channel and associated metadata that will all be needed together when
/// this file format starts laying down bytes.
/// </summary>
private sealed class ChannelWithMeta
{
/// <summary>
/// Get/set the <see cref="DTS.Serialization.Test.Module.Channel"/> to be serialized.
/// </summary>
public Test.Module.Channel Channel { get; set; }
/// <summary>
/// Get/set the <see cref="DataScaler"/> associated with the
/// specified channel.
/// </summary>
public DataScaler Scaler { get; }
/// <summary>
/// Get/set the <see cref="double"/> sample rate associated with the specified channel.
/// </summary>
public double SampleRate { get; }
/// <summary>
/// Get/set the <see cref="double"/> start time associated with the specified channel.
/// </summary>
public double StartTime { get; }
/// <summary>
/// Initialize an instance of the ChannelWithMeta class.
/// </summary>
///
/// <param name="channel">
/// A <see cref="DTS.Serialization.Test.Module.Channel"/> to be serialized.
/// </param>
///
/// <param name="scaler">
/// The <see cref="DataScaler"/> containing scaling information for
/// the specified channel.
/// </param>
///
/// <param name="samplerate">
/// The <see cref="double"/> sample rate of the associated channel.
/// </param>
///
/// <param name="starttime">
/// The <see cref="double"/> start time of the associated channel.
/// </param>
///
public ChannelWithMeta(Test.Module.Channel channel, DataScaler scaler, double samplerate, double starttime)
{
Channel = channel;
Scaler = scaler;
SampleRate = samplerate;
StartTime = starttime;
}
}
/// <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="ToyotaCsv.File.Writer.EventHandler"/> that should handle write-progress
/// reporting; null if none is to be used.
/// </param>
///
protected void WriteChannelInfo
(
StreamWriter fileWriter,
Test test,
List<double[]> filteredData,
TickEventHandler tickEventHandler,
uint totalWriteTicksNeeded,
ref uint totalWriteTicksDispatched,
CancelRequested cancelRequested
)
{
try
{
// Row 1: Test number
fileWriter.WriteLine(test.Id);
// Row 2:
fileWriter.WriteLine(test.Id);
// Row 3: Test Date
fileWriter.WriteLine($"{test.InceptionDate.Year - 2000}-{test.InceptionDate.Month}-{test.InceptionDate.Day}");
// Row 4: Impact Speed
fileWriter.WriteLine("");
// Row 5, 6:
fileWriter.WriteLine();
fileWriter.WriteLine();
// Row 7:
fileWriter.WriteLine(test.Description);
// Row 8: Sampling Rate
fileWriter.WriteLine(test.Modules.Count > 0 ? test.Modules[0].SampleRateHz.ToString() : "NO MODULES");
// Row 9, 10, 11, 12:
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
// Row 13:
//fprintf(fpExport, "%s,\n", TestPtr->VehicleList[0].Description);
// Row 14: Dummies
fileWriter.WriteLine();
//for(iIndex = 0; iIndex < MAX_DEVICE_DISPLAY; iIndex++)
//{
// if(strlen(TestPtr->DeviceList[iIndex].DeviceID) > 0)
// {
// fprintf(fpExport, "%s,", TestPtr->DeviceList[iIndex].DeviceID);
// }
//}
//fprintf(fpExport, "\n");
// Row 15, 16, 17, 18, 19, 20:
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
// Row 21: Header: Time and codes
fileWriter.Write("TIME,");
foreach (var channel in test.Channels)
fileWriter.Write(channel.ChannelDescriptionString + ",");
fileWriter.WriteLine();
//for(iIndex = 0; iIndex < iChannelListCount; iIndex++)
//{
// RackIndex = ChannelList[iIndex].iRack;
// ModuleIndex = ChannelList[iIndex].iModule;
// ChannelIndex = ChannelList[iIndex].iChannel;
// ChannelPtr = &SystemPtr->HWRack[RackIndex].HWModuleList[ModuleIndex].HWChannelList[ChannelIndex];
// if(IsChannelValid(ChannelPtr))
// {
// if(strlen(ChannelPtr->SensorUserChannelDescription) > 0)
// {
// fprintf(fpExport, "%s,", ChannelPtr->SensorUserChannelDescription);
// }
// else if(ChannelPtr->UserPointerValid[0])
// {
// fprintf(fpExport, "%s,", ((SensorDBItem *)(ChannelPtr->UserPointer[0]))->Code);
// }
// }
//}
//fprintf(fpExport, "\n");
// Row 22, 23, 24:
fileWriter.WriteLine();
fileWriter.WriteLine();
fileWriter.WriteLine();
var channelsWithMeta = new List<ChannelWithMeta>();
var filteredDataIndex = 0;
foreach (var channel in test.Channels)
{ //
// Bundle data and associated scaling information for in preparation for
// upcoming write.
//
var scaler = new DataScaler();
scaler.IsInverted = channel is Common.DAS.Concepts.DAS.Channel.IInversionAware ? (channel as Common.DAS.Concepts.DAS.Channel.IInversionAware).IsInverted : false;
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;
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.SetDigitalMultiplier((channel as Test.Module.AnalogInputChannel).DigitalMultiplier);
scaler.DigitalMode = (channel as Test.Module.AnalogInputChannel).DigitalMode;
}
if ((channel as Common.DAS.Concepts.DAS.Channel.ILinearized).LinearizationFormula.IsValid())
{
scaler.SetLinearizationFormula((channel as Common.DAS.Concepts.DAS.Channel.ILinearized).LinearizationFormula);
}
else
{
scaler.SetLinearizationFormula(null);
}
try
{
scaler.SetRemovedADC(channel.RemovedADC);
scaler.SetRemovedInternalADC(channel.RemovedInternalADC);
scaler.SetDataZeroLevelADC(channel.DataZeroLevelAdc);
scaler.SetZeroMvInADC(channel.ZeroMvInADC);
try { scaler.SetWindowAverageADC(channel.WindowAverageADC); }
catch (System.Exception) { }
//try { scaler.SetIRTraccZeros(Convert.ToDouble(channel.DataZeroLevelAdc), Convert.ToDouble(channel.PreTestZeroLevelAdc)); }
//catch (System.Exception) { }
//initialEU
}
catch (System.Exception) { }
scaler.SetMvPerEu(channel.Data.MvPerEu);
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;
try
{
scaler.SetInitialOffset(analogChannel.InitialOffset);
}
catch (System.Exception) { }
scaler.ZeroMethodType = analogChannel.ZeroMethod;
scaler.NominalExcitationVoltage = analogChannel.ExcitationVoltage;
try { scaler.FactoryExcitationVoltage = analogChannel.FactoryExcitationVoltage; }
catch { };
try { scaler.MeasuredExcitationVoltage = analogChannel.MeasuredExcitationVoltage; }
catch { };
scaler.ProportionalToExcitation = analogChannel.ProportionalToExcitation;
}
channelsWithMeta.Add(
new ChannelWithMeta(
channel,
scaler,
channel.ParentModule.SampleRateHz,
channel.ParentModule.TriggerSampleNumbers.Count > 0 ? channel.ParentModule.TriggerSampleNumbers[0] / channel.ParentModule.SampleRateHz * -1.0 : 0
)
);
}
var minStartTime = channelsWithMeta.Min(cwm => cwm.StartTime);
var maxStartTime = channelsWithMeta.Max(cwm => cwm.StartTime);
// LINQ statement will sometimes hand back float-rounding error numbers (4.999999999999995 v 5.0), use same rounding we use for time.
var minStopTime = double.Parse(channelsWithMeta.Min(cwm => cwm.StartTime + (cwm.Channel.PersistentChannelInfo.Length - 1) * 1.0 / cwm.SampleRate).ToString(NUMBER_FORMAT));
var maxStopTime = double.Parse(channelsWithMeta.Max(cwm => cwm.StartTime + (cwm.Channel.PersistentChannelInfo.Length - 1) * 1.0 / cwm.SampleRate).ToString(NUMBER_FORMAT));
var minSampleRate = channelsWithMeta.Min(cwm => cwm.SampleRate);
var maxSampleRate = channelsWithMeta.Max(cwm => cwm.SampleRate);
if (minSampleRate != maxSampleRate)
throw new UserException("Lowest sample rate (" + minSampleRate.ToString() + ") does not match highest sample rate (" + maxSampleRate.ToString() + ").\nThey must be identical.");
var sampleRate = minSampleRate;
var dataCollectionLength = (int)((maxStopTime - minStartTime) * sampleRate); //maxDataLengthInAllChannels;
for (var i = 0; i < dataCollectionLength; i++)
{
if (null != cancelRequested && cancelRequested()) { break; }
double time;
fileWriter.Write((time = minStartTime + i * 1.0 / sampleRate).ToString("F6") + ",");
foreach (var channelWithMeta in channelsWithMeta)
{
if (null != cancelRequested && cancelRequested()) { break; }
//
// 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.
//14513 double rounding error causing data to be offset by one sample incorrectly
// note that using decimals will fix the imprecision issue, but will use more time ...
var delta = Convert.ToDecimal(channelWithMeta.StartTime) - Convert.ToDecimal(minStartTime);
var thisChannelsIndexAtCurrentTime = i - Convert.ToInt32(delta * Convert.ToDecimal(channelWithMeta.SampleRate));
if (null != filteredData && filteredData.Count > 0)
fileWriter.Write((thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime < filteredData[filteredDataIndex].Length
? (filteredData[filteredDataIndex][thisChannelsIndexAtCurrentTime]).ToString("F6")
: "") + ",");
else
fileWriter.Write((thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime < channelWithMeta.Channel.PersistentChannelInfo.Length
? channelWithMeta.Scaler.GetEU(channelWithMeta.Channel.PersistentChannelInfo[thisChannelsIndexAtCurrentTime]).ToString("F6")
: "") + ",");
filteredDataIndex++;
}
if (0 == i % DataSamplesPerTick)
{
OnTick?.Invoke(this, ((double)totalWriteTicksDispatched++) / totalWriteTicksNeeded * 100);
System.Windows.Forms.Application.DoEvents();
}
fileWriter.WriteLine();
}
}
catch (System.Exception ex)
{
throw new Exception("encountered problem writing FTSS/Excel channel headers", ex);
}
}
}
}
}