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

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; }
}
}
}