1387 lines
73 KiB
C#
1387 lines
73 KiB
C#
/*
|
|
* FtssCsv.File.Writer.cs
|
|
*
|
|
* Copyright © 2009
|
|
* Diversified Technical Systems, Inc.
|
|
* All Rights Reserved
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
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.Utils;
|
|
using DTS.Common.Enums.Sensors;
|
|
using DTS.Common.Strings;
|
|
using DTS.Common.Interface.ExportData;
|
|
using DTS.Common.Enums;
|
|
using static DTS.Serialization.Test.Module;
|
|
|
|
namespace DTS.Serialization.FtssCsv
|
|
{
|
|
// *** see FtssCsv.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 FtssCsv.File.Writer class.
|
|
/// </summary>
|
|
///
|
|
/// <param name="fileType">
|
|
/// The associated <see cref="DTS.SErialization.FtssCsv.File"/> object.
|
|
/// </param>
|
|
///
|
|
internal Writer(File fileType, int encoding)
|
|
: base(fileType, encoding)
|
|
{
|
|
WriterParent = fileType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// the owning file that controls this writer
|
|
/// </summary>
|
|
internal File WriterParent { get; }
|
|
|
|
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
|
|
);
|
|
|
|
|
|
private readonly Property<SortedList<long, Dictionary<GPSSentenceTypes, string>>> _nmeaData
|
|
= new Property<SortedList<long, Dictionary<GPSSentenceTypes, string>>>(
|
|
"NMEAData",
|
|
null,
|
|
true
|
|
);
|
|
public SortedList<long, Dictionary<GPSSentenceTypes, string>> NMEAData
|
|
{
|
|
get => _nmeaData.Value;
|
|
set => _nmeaData.Value = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notify <see cref="BeginEventHandler"/> subscribers that the write
|
|
/// is starting.
|
|
/// </summary>
|
|
public event BeginEventHandler OnBegin;
|
|
|
|
/// <summary>
|
|
/// Notify <see cref="EndEventHandler"/> subscribers that the write
|
|
/// is finished.
|
|
/// </summary>
|
|
public event EndEventHandler OnEnd;
|
|
|
|
/// <summary>
|
|
/// Notify <see cref="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 + "\"" : "<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 Dictionary<string, FilteredData> FilteredChannelData
|
|
{
|
|
get => _FilteredChannelData.Value;
|
|
set => _FilteredChannelData.Value = value;
|
|
}
|
|
|
|
private readonly Property<Dictionary<string, FilteredData>> _FilteredChannelData
|
|
= new Property<Dictionary<string, FilteredData>>(
|
|
"FilteredChannelData",
|
|
new Dictionary<string, FilteredData>(),
|
|
true
|
|
);
|
|
|
|
/// <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
|
|
{
|
|
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);
|
|
|
|
if (System.IO.File.Exists(pathname))
|
|
{
|
|
FileUtils.DeleteFileOrMove(pathname, APILogger.Log);
|
|
}
|
|
APILogger.Log("opening ", pathname);
|
|
using (var fs = new FileStream(pathname, FileMode.Create))
|
|
{
|
|
|
|
Encoding encoder;
|
|
|
|
try
|
|
{
|
|
encoder = FileUtils.GetEncoding(DefaultEncoding);
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
APILogger.Log("Problem getting encoder", ex);
|
|
encoder = Encoding.Default;
|
|
}
|
|
|
|
//DateTime start = DateTime.Now;
|
|
using (var fileWriter = new StreamWriter(fs, encoder, 1024 * 1000))
|
|
{
|
|
//List<Test.Module.Channel> exportChannels = test.Channels;
|
|
|
|
switch (CurrentExportMode)
|
|
{
|
|
case ExportMode.FtssExcel:
|
|
WriteChannelInfo(fileWriter,
|
|
id,
|
|
test,
|
|
FilteredChannelData,
|
|
pathname,
|
|
tickEventHandler,
|
|
totalWriteTicksNeeded,
|
|
ref totalWriteTicksDispatched,
|
|
cancelRequested);
|
|
break;
|
|
|
|
default:
|
|
throw new NotSupportedException(
|
|
"FtssCsv::File::Writer ExportMode not supported: " + CurrentExportMode);
|
|
}
|
|
}
|
|
|
|
fs.Close();
|
|
}
|
|
}
|
|
|
|
catch (System.Exception ex)
|
|
{
|
|
APILogger.Log("encountered problem writing CSV test files", ex);
|
|
exception = new Exception("encountered problem writing CSV 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>
|
|
/// 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="FtssHeaderLine"/> header enumerations.
|
|
/// </param>
|
|
///
|
|
/// <param name="channelsWithMeta">
|
|
/// A populated <see cref="System.Collections.Generic.List<T>"/> of
|
|
/// <see cref="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) ||
|
|
headerLines[ftssSizeHeaderLine].TrueForAll(x => x == null)) continue;
|
|
var channelValues = headerLines[ftssSizeHeaderLine];
|
|
predictedExportSize += (ulong)(coder.DecodeAttributeValue(ftssSizeHeaderLine).Length +
|
|
CultureInfo.CurrentCulture.TextInfo
|
|
.ListSeparator.Length);
|
|
foreach (var channelValue in channelValues)
|
|
predictedExportSize += (ulong)(channelValue.Length + CultureInfo
|
|
.CurrentCulture.TextInfo.ListSeparator.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) +
|
|
CultureInfo.CurrentCulture.TextInfo.ListSeparator).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);
|
|
}
|
|
}
|
|
private IDictionary<FtssHeaderLine, List<string>> _headerLines =
|
|
new Dictionary<FtssHeaderLine, List<string>>();
|
|
/// <summary>
|
|
/// FB 6410 Determine if the header is one of the user selected headers, if it is add it to headerlines
|
|
/// </summary>
|
|
/// <param name="headerLine">header line to check and add</param>
|
|
private void AddheaderLine(FtssHeaderLine headerLine)
|
|
{
|
|
string headerName = headerLine.GetDescription();
|
|
if (headerLine == FtssHeaderLine.Headers ||
|
|
headerLine == FtssHeaderLine.DataStart ||
|
|
headerLine == FtssHeaderLine.Labels ||
|
|
ExportHeaders.Exists(p => p.IsSelected && p.HeaderName == headerName))
|
|
{
|
|
_headerLines.Add(headerLine, new List<string>());
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// returns true if UART header line is included in export
|
|
/// </summary>
|
|
private bool ContainsLine(UartHeaders line)
|
|
{
|
|
var description = line.GetDescription();
|
|
if (ExportHeaders.Exists(x => description.Equals(x.HeaderName) && x.IsSelected))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/// <summary>
|
|
/// Adds a header line for UART (if included in export)
|
|
/// </summary>
|
|
private void AddHeaderLineValue(UartHeaders line, HashSet<UartHeaders> hash)
|
|
{
|
|
var description = line.GetDescription();
|
|
if (hash.Contains(line))
|
|
{
|
|
_headerLines[FtssHeaderLine.Labels].Add(description);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// FB 6410 Determine if the header is present in the user selected header and add the value for that header
|
|
/// </summary>
|
|
/// <param name="headerLine">The header</param>
|
|
/// <param name="value">The value for this header</param>
|
|
private void AddheaderLineValue(FtssHeaderLine headerLine, string value)
|
|
{
|
|
if (!_headerLines.ContainsKey(headerLine))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_headerLines[headerLine].Add(value);
|
|
}
|
|
|
|
private string GetT0Timestamp(Test.Module.Channel channel, Test test)
|
|
{
|
|
var noMaster = !test.Modules.Exists(m => m.PTPMasterSync);
|
|
if (channel.ParentModule.PTPMasterSync || (noMaster && channel.ParentModule.TriggerTimestampNanoSec > 0))
|
|
{
|
|
return PTP1588Timestamps.ToDateTimeString(channel.ParentModule.TriggerTimestampSec, channel.ParentModule.TriggerTimestampNanoSec);
|
|
}
|
|
return Strings.NotApplicable;
|
|
}
|
|
/// <summary>
|
|
/// returns a hashset populated by any UART headers included in the export
|
|
/// </summary>
|
|
private HashSet<UartHeaders> GetUartLookup()
|
|
{
|
|
var hash = new HashSet<UartHeaders>();
|
|
var keys = Enum.GetValues(typeof(UartHeaders)).Cast<UartHeaders>().ToArray();
|
|
foreach( var key in keys)
|
|
{
|
|
if (ContainsLine(key)) { hash.Add(key); }
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
/// <summary>
|
|
/// returns a hash populated by any channels included in export (or not mentioned for some reason)
|
|
/// </summary>
|
|
private HashSet<string> GetChannelLookup(Test test)
|
|
{
|
|
var hash = new HashSet<string>();
|
|
foreach( var channel in test.Channels)
|
|
{
|
|
var key = $"{channel.AbsoluteDisplayOrder:000} - {channel.ChannelName2}";
|
|
|
|
var match = ExportHeaders.Where(x => key.Equals(x.HeaderName));
|
|
//if we don't find it at all, or if we find it and it's selected, then add it
|
|
if (!match.Any() || match.First().IsSelected)
|
|
{
|
|
hash.Add(key);
|
|
}
|
|
}
|
|
return hash;
|
|
}
|
|
/// <summary>
|
|
/// Write the specified test to the specified stream.
|
|
/// </summary>
|
|
///
|
|
/// <param name="fileWriter">
|
|
/// The <see cref="StreamWriter"/> to which the specified test should be serialized.
|
|
/// </param>
|
|
///
|
|
/// <param name="test">
|
|
/// The <see cref="Test"/> to be serialized.
|
|
/// </param>
|
|
///
|
|
/// <param name="writeEvent">
|
|
/// The <see cref="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="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,
|
|
Dictionary<string, FilteredData> filteredData,
|
|
string targetPath,
|
|
TickEventHandler tickEventHandler,
|
|
uint totalWriteTicksNeeded,
|
|
ref uint totalWriteTicksDispatched,
|
|
CancelRequested cancelRequested)
|
|
{
|
|
try
|
|
{
|
|
_headerLines.Clear();
|
|
double sampleRate = (from ch in test.Channels select ch.ParentModule.SampleRateHz).Max();
|
|
var distinctRates = (from ch in test.Channels select ch.ParentModule.SampleRateHz).Distinct().ToArray();
|
|
|
|
foreach (var rate in distinctRates)
|
|
{
|
|
var mod = sampleRate % rate;
|
|
if (0 != mod) { throw new System.Exception($"sample rate [{rate}] is not a multiple of max sample rate in test [{sampleRate}]"); }
|
|
}
|
|
|
|
if (1 < SubSampleInterval)
|
|
{
|
|
if (distinctRates.Length > 1) { throw new System.Exception("we are both super sampling and subsampling in this export."); }
|
|
var ChannelList = test.Channels;
|
|
for (var iChannel = 0; iChannel < ChannelList.Count(); iChannel++)
|
|
{
|
|
if (true == Filtered)
|
|
{
|
|
var SubSample = new NHTSASubSample<double>();
|
|
SubSample.data = filteredData[ChannelList[iChannel].ChannelId].Data;
|
|
SubSample.preTriggerSamples =
|
|
test.Modules[0].TriggerSampleNumbers[0] - test.Modules[0].StartRecordSampleNumber;
|
|
SubSample.sampleRate = sampleRate;
|
|
SubSample.subSampleInterval = SubSampleInterval;
|
|
SubSample.SubSample();
|
|
filteredData[ChannelList[iChannel].ChannelId].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 channelNumber = 0;
|
|
var coder = new DescriptionAttributeCoder<FtssHeaderLine>();
|
|
AddheaderLine(FtssHeaderLine.Headers);
|
|
AddheaderLine(FtssHeaderLine.TestDate);
|
|
AddheaderLine(FtssHeaderLine.TestTime);
|
|
AddheaderLine(FtssHeaderLine.TestId);
|
|
AddheaderLine(FtssHeaderLine.TestDescription);
|
|
AddheaderLine(FtssHeaderLine.SampleRate);
|
|
AddheaderLine(FtssHeaderLine.HardwareAntiAliasFilter);
|
|
AddheaderLine(FtssHeaderLine.DataChannelNumber);
|
|
|
|
if (null != WriterParent && (WriterParent.ISOViewMode == IsoViewMode.ISOAndUserCode || WriterParent.ISOViewMode == IsoViewMode.ISOOnly))
|
|
{
|
|
AddheaderLine(FtssHeaderLine.IsoCode);
|
|
}
|
|
|
|
if (null != WriterParent && (WriterParent.ISOViewMode == IsoViewMode.UserCodeOnly || WriterParent.ISOViewMode == IsoViewMode.ISOAndUserCode))
|
|
{
|
|
AddheaderLine(FtssHeaderLine.UserCode);
|
|
}
|
|
|
|
AddheaderLine(FtssHeaderLine.ChannelDescription);
|
|
AddheaderLine(FtssHeaderLine.ChannelLocation);
|
|
AddheaderLine(FtssHeaderLine.SensorSerialNumber);
|
|
AddheaderLine(FtssHeaderLine.SensorCalDate); //17651: include sensor cal date in CSV exports
|
|
AddheaderLine(FtssHeaderLine.SoftwareFilter);
|
|
AddheaderLine(FtssHeaderLine.SoftwareFilterDb);
|
|
AddheaderLine(FtssHeaderLine.EngineeringUnits);
|
|
AddheaderLine(FtssHeaderLine.UserComment);
|
|
AddheaderLine(FtssHeaderLine.PreZero);
|
|
AddheaderLine(FtssHeaderLine.PostZero);
|
|
AddheaderLine(FtssHeaderLine.DataZero);
|
|
AddheaderLine(FtssHeaderLine.ScaleEu);
|
|
AddheaderLine(FtssHeaderLine.ScaleMv);
|
|
AddheaderLine(FtssHeaderLine.ChannelName);
|
|
AddheaderLine(FtssHeaderLine.DisplayName);
|
|
if (AddHardwareLine)
|
|
{
|
|
AddheaderLine(FtssHeaderLine.HardwareLine);
|
|
}
|
|
AddheaderLine(FtssHeaderLine.ZeroMethod);
|
|
AddheaderLine(FtssHeaderLine.RemoveOffset);
|
|
AddheaderLine(FtssHeaderLine.GroupName);
|
|
if (UnixTime)
|
|
{
|
|
// FB15333: Add PTP/RTC timestamp column for CSV exports
|
|
AddheaderLine(FtssHeaderLine.Timestamp);
|
|
}
|
|
AddheaderLine(FtssHeaderLine.DataStart);
|
|
AddheaderLine(FtssHeaderLine.Labels);
|
|
|
|
var channelsWithMeta = new List<ChannelWithMeta>();
|
|
var channelIncludedHash = GetChannelLookup(test);
|
|
foreach (var channel in test.Channels)
|
|
{
|
|
var key = $"{channel.AbsoluteDisplayOrder:000} - {channel.ChannelName2}";
|
|
if (!channelIncludedHash.Contains(key)) { continue; }
|
|
//
|
|
// Tack on the header information for each channel.
|
|
//
|
|
|
|
var filteredChannelData = filteredData.ContainsKey(channel.ChannelId) ? filteredData[channel.ChannelId] : null;
|
|
|
|
AddheaderLineValue(FtssHeaderLine.Headers, CultureInfo.CurrentCulture.TextInfo.ListSeparator);
|
|
AddheaderLineValue(FtssHeaderLine.TestDate, test.InceptionDate.ToShortDateString());
|
|
AddheaderLineValue(FtssHeaderLine.TestTime, test.InceptionDate.ToLongTimeString());
|
|
AddheaderLineValue(FtssHeaderLine.TestId, id);
|
|
AddheaderLineValue(FtssHeaderLine.TestDescription, test.Description);
|
|
AddheaderLineValue(FtssHeaderLine.SampleRate, sampleRate.ToString("F0"));
|
|
bool isDigitalInput = false;
|
|
bool isSquib = false;
|
|
if (channel is Test.Module.AnalogInputChannel)
|
|
{
|
|
isDigitalInput = (channel as Test.Module.AnalogInputChannel).Bridge == SensorConstants.BridgeType.DigitalInput;
|
|
isSquib = (channel as Test.Module.AnalogInputChannel).Bridge == SensorConstants.BridgeType.SQUIB;
|
|
}
|
|
AddheaderLineValue(FtssHeaderLine.HardwareAntiAliasFilter, isDigitalInput ? string.Empty : channel.ParentModule.AaFilterRateHz.ToString("F0"));
|
|
AddheaderLineValue(FtssHeaderLine.DataChannelNumber, (channelNumber + 1).ToString());
|
|
|
|
if (null != WriterParent && (WriterParent.ISOViewMode == IsoViewMode.ISOOnly || WriterParent.ISOViewMode == IsoViewMode.ISOAndUserCode))
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.IsoCode, channel is IIsoCodeAware ? (channel as IIsoCodeAware).IsoCode : "");
|
|
}
|
|
|
|
if (null != WriterParent && (WriterParent.ISOViewMode == IsoViewMode.UserCodeOnly || WriterParent.ISOViewMode == IsoViewMode.ISOAndUserCode))
|
|
{
|
|
if (channel is Test.Module.AnalogInputChannel aic)
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.UserCode, aic.UserCode);
|
|
}
|
|
else
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.UserCode, channel.UserCode);
|
|
}
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.ChannelDescription, string.IsNullOrEmpty(channel.IsoChannelName)
|
|
? isSquib ? channel.ChannelDescriptionString.ReplaceStrings(Common.Constants.ExportNameFilters, StringReplacementMode.Last) : channel.ChannelDescriptionString //17650: sanitize squib name output for certain exports
|
|
: channel.IsoChannelName);
|
|
|
|
AddheaderLineValue(FtssHeaderLine.ChannelLocation, "NONE");
|
|
AddheaderLineValue(FtssHeaderLine.SensorSerialNumber, channel is ISerialNumberAware
|
|
? (channel as ISerialNumberAware).SerialNumber
|
|
: "");
|
|
AddheaderLineValue(FtssHeaderLine.SensorCalDate, channel.LastCalibrationDate.ToString(CultureInfo.InvariantCulture)); //17651: include sensor cal date in CSV exports
|
|
if (MV || ADC)
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.SoftwareFilter, "NONE");
|
|
AddheaderLineValue(FtssHeaderLine.SoftwareFilterDb, "NONE");
|
|
}
|
|
else
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.SoftwareFilter, Filtered && filteredChannelData != null
|
|
? filteredChannelData.FilterDescription
|
|
: "NONE");
|
|
AddheaderLineValue(FtssHeaderLine.SoftwareFilterDb, Filtered && filteredChannelData != null
|
|
? filteredChannelData.FilterFrequencyHz.ToString("F0")
|
|
: "NONE");
|
|
AddheaderLineValue(FtssHeaderLine.EngineeringUnits, channel is IEngineeringUnitAware
|
|
? (channel as IEngineeringUnitAware).EngineeringUnits.TrimEnd()
|
|
: "");
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.UserComment, "");
|
|
|
|
var rate = Convert.ToInt32(Math.Ceiling(sampleRate / channel.ParentModule.SampleRateHz));
|
|
|
|
var preTriggerSamples = rate * (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);
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.PreZero, (preTriggerSamples / SubSampleInterval).ToString());
|
|
|
|
var postTriggerSamples = (channel.ParentModule.NumberOfSamples - preTriggerSamples) * rate;
|
|
if (postTriggerSamples < 0)
|
|
{
|
|
postTriggerSamples = 0D;
|
|
}
|
|
|
|
if (postTriggerSamples > Stop * sampleRate)
|
|
{
|
|
postTriggerSamples = Math.Truncate(Stop * sampleRate);
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.PostZero, (postTriggerSamples / SubSampleInterval).ToString());
|
|
|
|
AddheaderLineValue(FtssHeaderLine.DataZero, 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;
|
|
scaler.UserOffsetEU = (channel as Test.Module.AnalogInputChannel).UserOffsetEU;
|
|
|
|
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
|
|
)
|
|
);
|
|
|
|
AddheaderLineValue(FtssHeaderLine.ScaleEu, scaler.GetAdcToEuScalingFactor().ToString());
|
|
AddheaderLineValue(FtssHeaderLine.ScaleMv, scaler.GetAdcToMvScalingFactor().ToString());
|
|
var originalChannelName = channel.OriginalChannelName;
|
|
|
|
if (string.IsNullOrWhiteSpace(originalChannelName))
|
|
{
|
|
//if original channel name is blank, attempt to fall back on names based on whatever the current iso view mode is
|
|
//this has to be done because outputsquibchannel does not currently store the property OriginalChannelName
|
|
//15619 Channel name is empty in csv export if data is from squib
|
|
if (channel is Test.Module.AnalogInputChannel aic)
|
|
{
|
|
switch (WriterParent.ISOViewMode)
|
|
{
|
|
case IsoViewMode.ISOOnly:
|
|
originalChannelName = aic.IsoChannelName;
|
|
break;
|
|
case IsoViewMode.ISOAndUserCode:
|
|
originalChannelName = $"{aic.IsoChannelName}\\{aic.UserChannelName}";
|
|
break;
|
|
case IsoViewMode.UserCodeOnly:
|
|
originalChannelName = aic.UserChannelName;
|
|
break;
|
|
case IsoViewMode.ChannelNameOnly:
|
|
originalChannelName = aic.UserChannelName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.ChannelName, originalChannelName);
|
|
AddheaderLineValue(FtssHeaderLine.DisplayName, isSquib ? channel.ChannelName2.ReplaceStrings(Common.Constants.ExportNameFilters, StringReplacementMode.Last) : channel.ChannelName2); //17650: sanitize squib name output for certain exports
|
|
|
|
if (AddHardwareLine)
|
|
{
|
|
var serialNumber = Strings.Table_NA;
|
|
if (channel is AnalogInputChannel aic && null != aic.ParentModule)
|
|
{
|
|
serialNumber = aic.ParentModule.BaseSerialNumber;
|
|
}
|
|
AddheaderLineValue(FtssHeaderLine.HardwareLine, serialNumber);
|
|
}
|
|
|
|
//Zero Method and if Average Over Time, then Begin/End
|
|
var sb = new StringBuilder(scaler.ZeroMethodType.ToString());
|
|
if (scaler.ZeroMethodType == ZeroMethodType.AverageOverTime)
|
|
{
|
|
sb.Append(" (");
|
|
sb.Append((channel as Test.Module.AnalogInputChannel).ZeroAverageWindow.Begin);
|
|
sb.Append("/");
|
|
sb.Append((channel as Test.Module.AnalogInputChannel).ZeroAverageWindow.End);
|
|
sb.Append(")");
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.ZeroMethod, sb.ToString());
|
|
AddheaderLineValue(FtssHeaderLine.RemoveOffset, (channel as Test.Module.AnalogInputChannel).RemoveOffset.ToString());
|
|
var split = (channel as Test.Module.AnalogInputChannel).ChannelId.Split('_');
|
|
if (split.Length == 1)
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.GroupName, (channel as Test.Module.AnalogInputChannel).ChannelGroupName);
|
|
}
|
|
else
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.GroupName, !Guid.TryParse(split[0], out var dummyGuid)
|
|
? (channel as Test.Module.AnalogInputChannel).ChannelGroupName
|
|
: split[1]);
|
|
}
|
|
|
|
AddheaderLineValue(FtssHeaderLine.DataStart, CultureInfo.CurrentCulture.TextInfo.ListSeparator);
|
|
AddheaderLineValue(FtssHeaderLine.Labels, "Chan " + channelNumber + ":" + (channel is ISerialNumberAware
|
|
? (channel as ISerialNumberAware).SerialNumber
|
|
: "NO SERIAL NUMBER AVAILABLE"));
|
|
|
|
if (UnixTime)
|
|
{
|
|
// FB15333: Add PTP/RTC timestamp column for CSV exports
|
|
if (PTP1588Timestamps.IsValidTimeStamp(channel.ParentModule.TriggerTimestampSec))
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.Timestamp, UnixTimeEpoch ?
|
|
channel.ParentModule.TriggerTimestampSec.ToString() + CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator + channel.ParentModule.TriggerTimestampNanoSec.ToString()
|
|
: PTP1588Timestamps.ToDateTimeString(channel.ParentModule.TriggerTimestampSec, channel.ParentModule.TriggerTimestampNanoSec));
|
|
}
|
|
else
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.Timestamp, Strings.NotApplicable);
|
|
}
|
|
}
|
|
channelNumber++;
|
|
}
|
|
var lookup = GetUartLookup();
|
|
if (null != NMEAData)
|
|
{
|
|
AddheaderLineValue(FtssHeaderLine.EngineeringUnits, CultureInfo.CurrentCulture.TextInfo.ListSeparator);
|
|
// find the first data entry with GPGGA data and extract units for altitude
|
|
// if GPS doesn't output GPGGA sentences, just put a blank entry
|
|
var firstwGGA = NMEAData.FirstOrDefault(nmea => nmea.Value.ContainsKey(GPSSentenceTypes.GPGGA));
|
|
var altUnits = null != firstwGGA.Value && firstwGGA.Value[GPSSentenceTypes.GPGGA].Split(",".ToCharArray()).Length > Common.Constants.NMEA_GPGGA_ALTU_POSN ?
|
|
firstwGGA.Value[GPSSentenceTypes.GPGGA].Split(",".ToCharArray())[Common.Constants.NMEA_GPGGA_ALTU_POSN] :
|
|
String.Empty;
|
|
//only add the Alt/Vel/Direction alternate Eu if we are including those columns
|
|
if (lookup.Contains(UartHeaders.Altitude)){ AddheaderLineValue(FtssHeaderLine.EngineeringUnits, altUnits); }
|
|
if (lookup.Contains(UartHeaders.Velocity)){ AddheaderLineValue(FtssHeaderLine.EngineeringUnits, "Knots"); }
|
|
if (lookup.Contains(UartHeaders.Direction)) { AddheaderLineValue(FtssHeaderLine.EngineeringUnits, "Degrees"); }
|
|
AddHeaderLineValue(UartHeaders.Latitude, lookup);
|
|
AddHeaderLineValue(UartHeaders.Longitude, lookup);
|
|
AddHeaderLineValue(UartHeaders.Altitude, lookup);
|
|
AddHeaderLineValue(UartHeaders.Velocity, lookup);
|
|
AddHeaderLineValue(UartHeaders.Direction, lookup);
|
|
AddHeaderLineValue(UartHeaders.Valid, lookup);
|
|
AddHeaderLineValue(UartHeaders.GPRMC, lookup);
|
|
AddHeaderLineValue(UartHeaders.GPGGA, lookup);
|
|
}
|
|
var minStartTime = ChannelWithMeta.GetMinStartTime(Start, channelsWithMeta);
|
|
var minStopTime = ChannelWithMeta.GetMinStopTime(Stop, channelsWithMeta);
|
|
var dataCollectionLength = (int)((minStopTime - minStartTime) * sampleRate);
|
|
|
|
VerifyExportedFileWillFitOnDisk(test.Id,
|
|
targetPath.Remove(targetPath.IndexOf(Path.VolumeSeparatorChar) + 1),
|
|
_headerLines,
|
|
coder,
|
|
channelsWithMeta,
|
|
dataCollectionLength,
|
|
minStartTime,
|
|
sampleRate,
|
|
cancelRequested);
|
|
|
|
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) +
|
|
CultureInfo.CurrentCulture.TextInfo.ListSeparator);
|
|
foreach (var channelValue in channelValues)
|
|
fileWriter.Write(channelValue + CultureInfo.CurrentCulture.TextInfo
|
|
.ListSeparator);
|
|
fileWriter.WriteLine();
|
|
}
|
|
}
|
|
//FB 29410 Create and pass list of timestamps with true PTPMasterSync to MinUnixTime method to get the min unix time
|
|
var basemodules = test.Modules.GroupBy(module => module.BaseSerialNumber).Select(group => group.First());
|
|
|
|
var testModuleTimeStamps = GetModuleTimeStamps(basemodules, out var testModuleStartTimestamps);
|
|
|
|
//get the minimum start record or trigger timestamp if available
|
|
Tuple<double, double> minUnixTime = null;
|
|
if ((UnixTime || null != NMEAData) && (testModuleTimeStamps.Any() || testModuleStartTimestamps.Any()))
|
|
{
|
|
if (testModuleTimeStamps.Any())
|
|
{
|
|
minUnixTime = TestUtils.MinUnixTime(testModuleTimeStamps);
|
|
}
|
|
else if (testModuleStartTimestamps.Any())
|
|
{
|
|
minUnixTime = TestUtils.MinUnixTime(testModuleStartTimestamps);
|
|
}
|
|
}
|
|
List<long> nmeaData = NMEAData?.Keys.ToList();
|
|
|
|
for (var i = 0; i <= dataCollectionLength; i++)
|
|
{
|
|
if (null != cancelRequested && cancelRequested())
|
|
{
|
|
break;
|
|
}
|
|
|
|
var thisTime = (decimal)minStartTime + i * (decimal)1.0 / (decimal)sampleRate;
|
|
var timestring = thisTime.ToString(NUMBER_FORMAT);
|
|
Dictionary<GPSSentenceTypes, string> nmeaDataNow = null;
|
|
// FB15333: Add PTP/RTC timestamp column for CSV exports
|
|
if (null != minUnixTime && PTP1588Timestamps.IsValidTimeStamp(minUnixTime.Item1))
|
|
{
|
|
//31783 UTC not being exported with CSV with TSR AIR
|
|
//this is going to be a little confusing - minUnixTime is either the trigger time OR the start time of recording
|
|
//if it's trigger time and we have a negative time we are adjusting backwards in time (from the trigger)
|
|
//in the case of recorder mode our timestamp is the start, but then this time is 0, so we are going to end up adjusting forwards as we go
|
|
var truncTime = Math.Truncate(thisTime);
|
|
var unixSeconds = (decimal)minUnixTime.Item1 + (decimal)truncTime;
|
|
var nanos = (thisTime - truncTime) * Common.Constants.NANOS_PER_SECOND;
|
|
//39377 round to int, leaving as double/decimal was causing a multiple-decimal-point issue
|
|
var unixNanos = (decimal)minUnixTime.Item2 + (int)nanos;
|
|
while (unixNanos < 0)
|
|
{
|
|
unixSeconds--;
|
|
unixNanos += Common.Constants.NANOS_PER_SECOND;
|
|
}
|
|
while (unixNanos >= Common.Constants.NANOS_PER_SECOND)
|
|
{
|
|
unixSeconds++;
|
|
unixNanos -= Common.Constants.NANOS_PER_SECOND;
|
|
}
|
|
if (UnixTime) timestring = UnixTimeEpoch
|
|
? unixSeconds.ToString() + CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator + unixNanos.ToString()
|
|
: PTP1588Timestamps.ToDateTimeString(unixSeconds, unixNanos);
|
|
var now = (long)(10000000 * (unixSeconds + unixNanos / Common.Constants.NANOS_PER_SECOND));
|
|
// 18361: Add NMEA data (if available) to CSV exports
|
|
if (null != NMEAData)
|
|
{
|
|
var unixIndex = 0;
|
|
//we want to find the corresponding time in the list of NMEA data
|
|
//if we find the first entry after
|
|
//if there's none, but there's an entry = or before our time, then use that
|
|
//either way we step back one step and use that it's available
|
|
//the choie of using index0 and index last were already in existing code
|
|
try
|
|
{
|
|
if ( nmeaData.Exists(x=> x > now))
|
|
{
|
|
unixIndex = nmeaData.IndexOf(nmeaData.First(x => x > now));
|
|
}
|
|
else if( nmeaData.Exists(x=> x <= now))
|
|
{
|
|
unixIndex = nmeaData.IndexOf(nmeaData.Last(x => x <= now)) + 1;
|
|
}
|
|
}
|
|
catch( Exception ex) { APILogger.Log(ex); }
|
|
|
|
if (unixIndex < 0) unixIndex = 0;
|
|
if (unixIndex >= 1) { unixIndex--; }
|
|
if (unixIndex > NMEAData.Values.Count - 1) unixIndex = NMEAData.Values.Count - 1;
|
|
nmeaDataNow = NMEAData.Values[unixIndex];
|
|
}
|
|
}
|
|
|
|
fileWriter.Write(string.Format("{0}{1}",
|
|
timestring,
|
|
CultureInfo.CurrentCulture.TextInfo.ListSeparator));
|
|
var bNeedComma = false;
|
|
foreach (var channelWithMeta in channelsWithMeta)
|
|
{
|
|
var key = $"{channelWithMeta.Channel.AbsoluteDisplayOrder:000} - {channelWithMeta.Channel.ChannelName2}";
|
|
if (!channelIncludedHash.Contains(key)) { continue; }
|
|
if (bNeedComma)
|
|
{
|
|
fileWriter.Write(CultureInfo.CurrentCulture.TextInfo
|
|
.ListSeparator);
|
|
}
|
|
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.
|
|
//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 channelOffsetStart = Convert.ToInt32(delta * Convert.ToDecimal(channelWithMeta.SampleRate));
|
|
var rate = sampleRate / channelWithMeta.SampleRate;
|
|
var indexAtCurrentTime = (i - channelOffsetStart) / rate;
|
|
var thisChannelsIndexAtCurrentTime = Convert.ToInt32(Math.Floor(indexAtCurrentTime));
|
|
var step = indexAtCurrentTime - thisChannelsIndexAtCurrentTime;
|
|
if (Filtered)
|
|
{
|
|
if (filteredData.ContainsKey(channelWithMeta.Channel.ChannelId))
|
|
{
|
|
var channelFilteredData = filteredData[channelWithMeta.Channel.ChannelId];
|
|
if (thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime <
|
|
channelFilteredData.Data.Length)
|
|
{
|
|
var curValue = channelFilteredData.Data[thisChannelsIndexAtCurrentTime];
|
|
|
|
var nextValue = curValue;
|
|
if ((1 + thisChannelsIndexAtCurrentTime) < channelFilteredData.Data.Length)
|
|
{
|
|
nextValue = channelFilteredData.Data[1 + thisChannelsIndexAtCurrentTime];
|
|
}
|
|
|
|
fileWriter.Write((step * (nextValue - curValue) + curValue).ToString(NUMBER_FORMAT));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fileWriter.Write(double.NaN.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double dInitialEU = 0;
|
|
var aChannel = channelWithMeta.Channel as Test.Module.AnalogInputChannel;
|
|
if (thisChannelsIndexAtCurrentTime >= 0 && thisChannelsIndexAtCurrentTime <
|
|
channelWithMeta.Channel.PersistentChannelInfo.Length)
|
|
{
|
|
var currentValue = channelWithMeta.Channel.PersistentChannelInfo[thisChannelsIndexAtCurrentTime];
|
|
var nextValue = currentValue;
|
|
if ((1 + thisChannelsIndexAtCurrentTime) < channelWithMeta.Channel.PersistentChannelInfo.Length)
|
|
{
|
|
nextValue = channelWithMeta.Channel.PersistentChannelInfo[1 + thisChannelsIndexAtCurrentTime];
|
|
}
|
|
if (null != aChannel)
|
|
{
|
|
dInitialEU = aChannel.InitialEu;
|
|
}
|
|
|
|
var superSampledADC = step * (nextValue - currentValue) + currentValue;
|
|
if (MV)
|
|
{
|
|
fileWriter.Write(channelWithMeta.Scaler.GetMv(superSampledADC).ToString(NUMBER_FORMAT));
|
|
}
|
|
else if (ADC)
|
|
{
|
|
fileWriter.Write(superSampledADC.ToString("F0"));
|
|
}
|
|
else
|
|
{
|
|
fileWriter.Write(channelWithMeta.Scaler.GetEU(Convert.ToDouble(superSampledADC)).ToString(NUMBER_FORMAT));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (null != nmeaDataNow)
|
|
{
|
|
//GPS unit may or may not be configured to send GPRMC and GPGGA or may send sentences with empty data. check data as we go
|
|
var rmcData = nmeaDataNow.ContainsKey(GPSSentenceTypes.GPRMC) ? nmeaDataNow[GPSSentenceTypes.GPRMC].Split(",".ToCharArray(), StringSplitOptions.None) : [];
|
|
var ggaData = nmeaDataNow.ContainsKey(GPSSentenceTypes.GPGGA) ? nmeaDataNow[GPSSentenceTypes.GPGGA].Split(",".ToCharArray(), StringSplitOptions.None) : [];
|
|
fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator);
|
|
|
|
//Latitude
|
|
if (rmcData.Length > Common.Constants.NMEA_GPRMC_LAT_POSN && rmcData[Common.Constants.NMEA_GPRMC_LAT_POSN].Length > 2
|
|
&& lookup.Contains(UartHeaders.Latitude))
|
|
{
|
|
var latString = rmcData[Common.Constants.NMEA_GPRMC_LAT_POSN];
|
|
var latitude = Math.Round(decimal.Parse(latString.Substring(0, 2), CultureInfo.InvariantCulture) + (decimal.Parse(latString.Substring(2), CultureInfo.InvariantCulture) / 60), 6);
|
|
fileWriter.Write(rmcData[Common.Constants.NMEA_GPRMC_LATD_POSN] + latitude.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
if (lookup.Contains(UartHeaders.Latitude)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//Longitude
|
|
if (rmcData.Length > Common.Constants.NMEA_GPRMC_LONG_POSN && rmcData[Common.Constants.NMEA_GPRMC_LONG_POSN].Length > 3
|
|
&& lookup.Contains(UartHeaders.Longitude))
|
|
{
|
|
var longString = rmcData[Common.Constants.NMEA_GPRMC_LONG_POSN];
|
|
var longitude = Math.Round(decimal.Parse(longString.Substring(0, 3), CultureInfo.InvariantCulture) + (decimal.Parse(longString.Substring(3), CultureInfo.InvariantCulture) / 60), 6);
|
|
fileWriter.Write(rmcData[Common.Constants.NMEA_GPRMC_LONGD_POSN] + longitude.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
if (lookup.Contains(UartHeaders.Longitude)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//Altitude
|
|
if (ggaData.Length > Common.Constants.NMEA_GPGGA_ALT_POSN && lookup.Contains(UartHeaders.Altitude))
|
|
{
|
|
fileWriter.Write(ggaData[Common.Constants.NMEA_GPGGA_ALT_POSN]);
|
|
}
|
|
if (lookup.Contains(UartHeaders.Altitude)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//Velocity
|
|
if (rmcData.Length > Common.Constants.NMEA_GPRMC_VELO_POSN && lookup.Contains(UartHeaders.Velocity))
|
|
{
|
|
fileWriter.Write(rmcData[Common.Constants.NMEA_GPRMC_VELO_POSN]);
|
|
}
|
|
if (lookup.Contains(UartHeaders.Velocity)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//Direction
|
|
if (rmcData.Length > Common.Constants.NMEA_GPRMC_DIR_POSN && lookup.Contains(UartHeaders.Direction))
|
|
{
|
|
fileWriter.Write(rmcData[Common.Constants.NMEA_GPRMC_DIR_POSN]);
|
|
}
|
|
if (lookup.Contains(UartHeaders.Direction)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//Valid?
|
|
if (rmcData.Length > Common.Constants.NMEA_GPRMC_VALID_POSN && lookup.Contains(UartHeaders.Valid))
|
|
{
|
|
var rmcValid = "A" == rmcData[Common.Constants.NMEA_GPRMC_VALID_POSN];
|
|
// if we only have GPRMC data to go on, skip GGA validation. if we have both, use both
|
|
var ggaValid = true;
|
|
if (ggaData.Length > Common.Constants.NMEA_GPGGA_VALID_POSN)
|
|
{
|
|
var fixIndicator = ggaData[Common.Constants.NMEA_GPGGA_VALID_POSN] != string.Empty ? int.Parse(ggaData[Common.Constants.NMEA_GPGGA_VALID_POSN]) : 0; // if we can't parse, just say invalid
|
|
ggaValid = 1 == fixIndicator /*GPS SPS Mode, fix valid*/ || 2 == fixIndicator /*Differential GPS, SPS mode, fix valid*/ || 6 == fixIndicator /*Dead Reckoning mode, fix valid*/;
|
|
}
|
|
fileWriter.Write(rmcValid && ggaValid ? "Y" : "N");
|
|
}
|
|
if (lookup.Contains(UartHeaders.Valid)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//GPRMC
|
|
if (nmeaDataNow.ContainsKey(GPSSentenceTypes.GPRMC) && lookup.Contains(UartHeaders.GPRMC))
|
|
{
|
|
fileWriter.Write(nmeaDataNow[GPSSentenceTypes.GPRMC].Replace(",", ";")); //NMEA data already comma-separated
|
|
}
|
|
if (lookup.Contains(UartHeaders.GPRMC)) { fileWriter.Write(CultureInfo.CurrentCulture.TextInfo.ListSeparator); }
|
|
|
|
//GPGGA
|
|
if (nmeaDataNow.ContainsKey(GPSSentenceTypes.GPGGA) && lookup.Contains(UartHeaders.GPGGA))
|
|
{
|
|
fileWriter.Write(nmeaDataNow[GPSSentenceTypes.GPGGA].Replace(",", ";")); //NMEA data already comma-separated
|
|
}
|
|
|
|
}
|
|
|
|
if (0 == i % DataSamplesPerTick)
|
|
{
|
|
OnTick?.Invoke(this, (double)totalWriteTicksDispatched++ / totalWriteTicksNeeded * 100);
|
|
System.Windows.Forms.Application.DoEvents();
|
|
}
|
|
|
|
fileWriter.WriteLine();
|
|
}
|
|
|
|
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; }
|
|
|
|
public bool ADC { get; set; }
|
|
public bool MV { get; set; }
|
|
|
|
// FB15333: Add PTP/RTC timestamp column for CSV exports
|
|
public bool UnixTime { get; set; }
|
|
// FB30129: Add option for CSV export to use time since 1970 instead of UTC
|
|
private bool _unixTimeEpoch = false;
|
|
public bool UnixTimeEpoch
|
|
{
|
|
get => _unixTimeEpoch;
|
|
set
|
|
{
|
|
_unixTimeEpoch = value;
|
|
if (_unixTimeEpoch)
|
|
{
|
|
//we wouldn't change the unix time display type if we weren't trying to display in unix time
|
|
UnixTime = true;
|
|
}
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// whether to add a line into the export for DAS Serial Number
|
|
/// 35544 - Internal-DataPRO-feature-reqsuest-CSV-export-add-a-line-for-DAS-serial-number-used-by-each-channel
|
|
/// </summary>
|
|
public bool AddHardwareLine { get; set; }
|
|
public List<IExportHeader> ExportHeaders { get; set; }
|
|
}
|
|
}
|
|
}
|