/*
* Chapter10.File.Writer.cs
*
* Copyright © 2020
* Diversified Technical Systems, Inc.
* All Rights Reserved
*/
using System;
using System.Collections.Generic;
using System.Drawing.Text;
using System.IO;
using System.Linq;
using System.Text;
using DTS.Common;
using DTS.Common.DAS.Concepts;
using DTS.Common.Enums;
using DTS.Common.Utilities.Logging;
using DTS.Common.Utils;
using static DTS.Serialization.Test.Module;
// ReSharper disable PossiblyMistakenUseOfParamsMethod
namespace DTS.Serialization.IRIGCH10
{
public partial class File
{
///
/// implementation of the Serialization.File.Writer class for Chapter10
///
public class Writer : Writer, IWriter
{
#region properties
///
/// the owning file that controls this writer
///
internal File WriterParent { get; }
public double Start { get; set; }
public double Stop { get; set; }
public bool Filtered { get; set; }
#endregion
#region methods
///
/// writes out test to given path
///
///
///
///
///
///
///
///
public void Write(string pathname, string id, Test test, bool bFiltering, bool includeGroupNameInISOExport, double minStartTime, int dataCollectionLength)
{
throw new NotImplementedException();
}
///
/// updates the progress if possible
///
///
///
private void UpdateProgress(double dValue, TickEventHandler tickEventHandler)
{
tickEventHandler?.Invoke(this, dValue);
}
///
/// returns a name for the given channel
///
///
///
private static string GetChannelName(Channel channel)
{
if (!(channel is AnalogInputChannel aic))
{
return channel.ChannelName2;
}
switch (IsoViewModeStatic.ViewMode)
{
case IsoViewMode.ISOOnly:
return $"{aic.IsoChannelName}";
case IsoViewMode.ISOAndUserCode:
return $"{aic.IsoChannelName}\\{aic.UserChannelName}";
case IsoViewMode.UserCodeOnly:
return $"{aic.UserCode}";
case IsoViewMode.ChannelNameOnly:
return $"{aic.UserChannelName}";
}
return aic.HardwareChannelName;
}
///
/// we don't really have an RTC, so we make up one
/// with an arbitrary value, this is actually a 6byte value,
/// we're just using a long for convenience
///
public const long BASE_RTC = 141989612500056L;
private void GetTimeStamp(Test test, out int nanoseconds, out int seconds)
{
nanoseconds = 0;
seconds = 0;
var basemodules = test.Modules.GroupBy(module => module.BaseSerialNumber).Select(group => group.First());
var testModuleTimeStamps = GetModuleTimeStamps(basemodules, out var testModuleStartTimestamps);
Tuple minUnixTime = null;
if (testModuleTimeStamps.Any())
{
minUnixTime = TestUtils.MinUnixTime(testModuleTimeStamps);
}
else if (testModuleStartTimestamps.Any())
{
minUnixTime = TestUtils.MinUnixTime(testModuleStartTimestamps);
}
if (null == minUnixTime)
{
var ticks = test.InceptionDate.ToUniversalTime().Ticks;
var s = Math.Truncate((decimal)ticks / TimeSpan.TicksPerSecond);
ticks -= Convert.ToInt64(s * TimeSpan.TicksPerSecond);
var n = Math.Truncate(ticks * Common.Constants.NANOS_PER_SECOND / TimeSpan.TicksPerSecond);
minUnixTime = new Tuple(Convert.ToDouble(n), Convert.ToDouble(s));
}
var minStartTime = double.MinValue;
foreach (var module in test.Modules)
{
var dStartTime = (double)module.StartRecordSampleNumber / module.SampleRateHz;
if (module.TriggerSampleNumbers.Count > 0)
{
dStartTime -= (double)module.TriggerSampleNumbers[0] / module.SampleRateHz;
}
minStartTime = Math.Max(minStartTime, dStartTime);
}
var nanos = ((decimal)(minStartTime) * Common.Constants.NANOS_PER_SECOND) + (decimal)minUnixTime.Item2 + (decimal)minUnixTime.Item1 * Common.Constants.NANOS_PER_SECOND;
seconds = Convert.ToInt32(Math.Truncate(nanos / Common.Constants.NANOS_PER_SECOND));
nanos -= seconds * Common.Constants.NANOS_PER_SECOND;
nanoseconds = Convert.ToInt32(nanos);
}
///
/// whether to include secondary time header or not
/// 33199 Add Time format 1 as an export option for CH 10 export
///
public bool IncludeSecondaryHeader { get; set; } = true;
///
/// Whether to use analog format or not during export
///
public bool UseAnalogFormat { get; set; } = true;
///
/// Whether to use PCM format or not during export
///
public bool UsePCMFormat { get; set; } = false;
///
/// returns a list of binary readers given a test that all point at the start of data
///
///
///
private List GetBinaryReaders(Test test, IReadOnlyDictionary lookup)
{
var binaryReaders = new List();
for (var i = 0; i < test.Channels.Count; i++)
{
var channel = test.Channels[i];
//get the filename and the start of data before we go and nuke the persistantchannelinfo object ...
var fileName = lookup[channel].ChannelFileName;
var offset = lookup[channel].OffsetDataStart;
//get the reader and advance it to the start of data
var br = new BinaryReader(new FileStream(fileName, FileMode.Open));
br.ReadBytes(Convert.ToInt32(offset));
lookup[channel].Reader = br;
binaryReaders.Add(br);
}
return binaryReaders;
}
///
/// internal class for encapsulating some of the information from analoginputdaschannels without keeping any files open
///
internal class ChannelInformation
{
public string ChannelFileName;
public ulong OffsetDataStart;
public ulong NumberOfSamples;
public short MinADC { get; set; } = short.MaxValue;
public short MaxADC { get; set; } = short.MinValue;
public BinaryReader Reader;
public ChannelInformation(AnalogInputChannel channel)
{
ChannelFileName = channel.PersistentChannelInfo.Filename;
OffsetDataStart = channel.PersistentChannelInfo.OffsetOfSampleDataStart;
NumberOfSamples = channel.PersistentChannelInfo.NumberOfSamples;
}
}
///
/// retrieves channel summary information for channels in a test
///
///
///
private IReadOnlyDictionary GetChannelSummaries(Test test)
{
var lookup = new Dictionary();
foreach (var channel in test.Channels)
{
if (channel is AnalogInputChannel aic)
{
lookup[channel] = new ChannelInformation(aic);
}
}
return lookup;
}
///
/// closes and disposes of all readers in list, then clears the list
///
///
private void CloseAndDisposeReaders(List readers)
{
for (var i = 0; i < readers.Count; i++)
{
readers[i].Close();
readers[i].Dispose();
}
readers.Clear();
}
///
/// go throughs all binary readers and determines the min and max ADC from files
///
///
///
///
private void GetMinAndMax(Test test, IReadOnlyDictionary lookup,
TickEventHandler tickEventHandler)
{
var index = 0;
foreach (var channel in test.Channels)
{
if (!lookup.ContainsKey(channel)) { return; }
var info = lookup[channel];
try
{
for (var i = 0UL; i < info.NumberOfSamples; i++)
{
var adc = ReadShort(info.Reader);
if (adc < info.MinADC) { info.MinADC = adc; }
else if (adc > info.MaxADC) { info.MaxADC = adc; }
}
index++;
tickEventHandler?.Invoke(this, (50D * index) / (double)test.Channels.Count);
}
catch (Exception ex)
{
APILogger.Log($"Failed to get Min/Max for file {info.ChannelFileName} - ", ex);
throw;
}
}
}
///
/// Readers a single signed short from file
///
///
///
private short ReadShort(BinaryReader reader)
{
try
{
var bytes = reader.ReadBytes(2);
//to get the next sample from the file we just read two bytes and make a short out of it
//I'm not sure why to use this version rather than alternatives, but the existing code
//used this method, so I just preserved it here ...
return (short)((bytes[0] & 0xFFFF) + ((bytes[1] & 0xFFFF) << 8));
}
catch (Exception ex)
{
APILogger.Log($"Failed to read from file", ex);
throw;
}
}
///
/// writes out test to given path
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
public void Write(string pathname,
string id,
string dataFolder,
Test test,
bool bFiltering,
bool includeGroupNameInISOExport,
FilteredData fd,
Channel tmChannel,
int channelNumber,
BeginEventHandler beginEventHandler,
CancelEventHandler cancelEventHandler,
EndEventHandler endEventHandler,
TickEventHandler tickEventHandler,
ErrorEventHandler errorEventHandler,
CancelRequested cancelRequested,
double minStartTime,
int dataCollectionLength)
{
System.Exception caughtException = null;
try
{
if (!Directory.Exists(Path.GetDirectoryName(pathname)))
{
_ = Directory.CreateDirectory(Path.GetDirectoryName(pathname));
}
var summaryLookup = GetChannelSummaries(test);
var binaryReaders = GetBinaryReaders(test, summaryLookup);
GetMinAndMax(test, summaryLookup, tickEventHandler);
tickEventHandler?.Invoke(this, 50D);
CloseAndDisposeReaders(binaryReaders);
string tmatsDoc = UseAnalogFormat ? GetTMATSAnalog(test, summaryLookup) : GetTMATSPCM(test, summaryLookup);
GetTimeStamp(test, out var nanoseconds, out var seconds);
var channelsCount = test.Channels.Count;
binaryReaders = GetBinaryReaders(test, summaryLookup);
if (UseAnalogFormat)
{
Chapter10File.WriteFileAnalog(tmatsDoc,
(int chIdx) =>
{
return ReadShort(binaryReaders[chIdx]);
},
(int chIdx) => (long)summaryLookup[test.Channels[chIdx]].NumberOfSamples,
channelsCount, nanoseconds, seconds, test.Modules[0].SampleRateHz,
IncludeSecondaryHeader, pathname, tickEventHandler, this);
}
else
{
Chapter10File.WriteFilePCM(tmatsDoc,
(int chIdx) => { return ReadShort(binaryReaders[chIdx]); },
(int chIdx) => (long)summaryLookup[test.Channels[chIdx]].NumberOfSamples,
channelsCount, nanoseconds, seconds, test.Modules[0].SampleRateHz,
IncludeSecondaryHeader, pathname, tickEventHandler, this);
}
CloseAndDisposeReaders(binaryReaders);
tickEventHandler?.Invoke(this, 100D);
}
catch (IOException ioException)
{
if (ioException.HResult == -2147024816)
{
var msg = $"failed to create file, check that file isn't currently open: {pathname}";
APILogger.Log(msg, ioException);
caughtException = new IOException(msg, ioException);
}
else
{
APILogger.Log("encountered problem writing Ch10 file", pathname, ioException);
caughtException = ioException;
}
}
catch (System.Exception ex)
{
APILogger.Log("encountered problem writing Ch10 file", pathname, ex);
caughtException = ex;
}
finally
{
tickEventHandler?.Invoke(this, 100D);
endEventHandler?.Invoke(this);
if (null != caughtException)
{
errorEventHandler?.Invoke(this, caughtException);
}
}
}
private const int DATA_CHANNEL_ID = 3;
///
/// returns a string with multiple lines for a single channel for a PCM TMATS file
///
private static string GetTMATSChannelPCM(Test test, IReadOnlyDictionary summaryLookup)
{
var sb = new StringBuilder();
var baseText = System.IO.File.ReadAllText(@"TMTTemplates\S6ATMTTemplate_PCM_ExportChannel.tmt");
for ( var i = 0; i< test.Channels.Count; i++)
{
sb.AppendLine(GetTMATSChannelPCM(test, summaryLookup, i, baseText));
}
return sb.ToString();
}
///
/// returns a string with multiple lines for a single channel for a PCM TMATS file
///
private static string GetTMATSChannelPCM(Test test, IReadOnlyDictionary summaryLookup, int channelIdx,
string unmodifiedText)
{
//we'll return the basetext, but just make it clear the original string isn't modified, strings are immutable and we
//are creating new strings and we're holding onto the old string
var baseText = unmodifiedText;
var channel = test.Channels[channelIdx];
try
{
var summary = summaryLookup[channel];
if (channel is AnalogInputChannel aic)
{
var ds = SliceRaw.File.Reader.GetDataScaler(aic);
var eu = aic.EngineeringUnits?.Trim() ?? "";
var scaler = ds.GetAdcToEuScalingFactor();
//we want 0 * ds = ds.GetEU(0), but the way to do that is to add in ds.GetEU(0)
//so that's the offset to add back in ... I'm not sure why analog is using a different formula
var offset = ds.GetEU(0);
var minEU = ds.GetEU(summary.MinADC);
var maxEU = ds.GetEU(summary.MaxADC);
var channelName = GetChannelName(channel);
baseText = baseText.Replace("{CHANNEL NUMBER}", $"{1 + channelIdx}");
baseText = baseText.Replace("{CHANNEL NAME}", channelName);
baseText = baseText.Replace("{CHANNEL OFFSET EU}", $"{offset:0.0000}");
baseText = baseText.Replace("{CHANNEL SCALEFACTOR EU}", $"{scaler:0.0000}");
baseText = baseText.Replace("{CHANNEL EU}", PrepareOutput(eu));
baseText = baseText.Replace("{CHANNEL MAX RANGE EU}", $"{maxEU:0.0000}");
baseText = baseText.Replace("{CHANNEL MIN RANGE EU}", $"{minEU:0.0000}");
}
}
catch (Exception ex)
{
APILogger.Log($"Failed to get TMATS for channel: {channelIdx} - {channel.ChannelName2}", ex);
}
return baseText;
}
private static string GetTMATSChannelsAnalog(Test test, IReadOnlyDictionary summaryLookup)
{
var sb = new StringBuilder();
sb.AppendLine(@"R-1\COM:--------------------- Start of channels ---------------------;");
for (var i = 0; i < test.Channels.Count; i++)
{
var channel = test.Channels[i];
try
{
var summary = summaryLookup[channel];
var ds = new DataScaler();
var eu = "EU";
if (channel is AnalogInputChannel aic)
{
ds = SliceRaw.File.Reader.GetDataScaler(aic);
eu = aic.EngineeringUnits?.Trim() ?? "";
}
var channelName = PrepareOutput(GetChannelName(channel));
sb.Append(GetTMATSChannel(channelName, summary.MinADC, summary.MaxADC, 1 + i, ds, eu));
sb.AppendLine();
}
catch (Exception ex)
{
APILogger.Log($"Failed to get TMATS for channel: {i} - {channel.ChannelName2}", ex);
}
}
sb.AppendLine(@"R-1\COM:--------------------- End of channels ---------------------;");
sb.AppendLine(@"R-1\COM:--------------------- End of TMATS ---------------------;");
return sb.ToString();
}
private static string GetTMATSChannel(string channelName, short ADCMin, short ADCMax, int channelNumber, DataScaler ds,
string eu)
{
var baseText = System.IO.File.ReadAllText(@"TMTTemplates\S6ATMTTemplate_ANALOG_ExportChannel.tmt");
baseText = baseText.Replace("{CHANNEL_NUMBER}", $"{channelNumber}");
baseText = baseText.Replace("{CHANNEL_NAME}", channelName);
var scaler = ds.GetAdcToEuScalingFactor();
var adcToEU = ds.GetAdcToEuScalingFactor();
var midPointRemoval = adcToEU * Constants.ADC_MIDPOINT;
//the scaler is already aware of datazerolevelADC, so just get 0 and datazerolevel is applied, as is
//initial eu or user offset
var offset = ds.GetEU(0) - midPointRemoval;
var minEU = ds.GetEU(ADCMin);
var maxEU = ds.GetEU(ADCMax);
baseText = baseText.Replace("{CHANNEL_OFFSETEU}", $"{offset}");
baseText = baseText.Replace("{CHANNEL_SCALEFACTOREU}", $"{scaler}");
baseText = baseText.Replace("{CHANNEL_EU}", PrepareOutput(eu));
baseText = baseText.Replace("{CHANNEL_MAXRANGEEU}", $"{maxEU}");
baseText = baseText.Replace("{CHANNEL_MINRANGEEU}", $"{minEU}");
return baseText;
}
private static string PrepareOutput(string s)
{
if (string.IsNullOrEmpty(s)) { return string.Empty; }
return s.Replace(' ', '_');
}
///
/// returns the entire TMATS document as a string for a PCM export
///
private static string GetTMATSPCM(Test test, IReadOnlyDictionary summaryLookup)
{
var sb = new StringBuilder();
var baseText = System.IO.File.ReadAllText(@"TMTTemplates\S6ATMTTemplate_PCM_ExportBase.tmt");
baseText = baseText.Replace("{NAME OF PROGRAM}", "DataPRO");
baseText = baseText.Replace("{TEST ID}", test.Modules[0].SerialNumber);
baseText = baseText.Replace("{STREAM TIME FORMAT}", "2");
baseText = baseText.Replace("{DAS SERIAL NUMBER}", test.Modules[0].SerialNumber);
int channelCount = test.Modules.Sum(m => m.Channels.Count);
var bitsPerFrame = 32 + 16 * channelCount;
baseText = baseText.Replace("{DAS BIT RATE}", $"{bitsPerFrame * test.Modules[0].SampleRateHz:0}");
var now = DateTime.UtcNow;
baseText = baseText.Replace("{CREATE DATE}", $"{now.Year:0000}-{now.Month:00}-{now.Day:00} {now.Hour:00}:{now.Minute:00}:{now.Second:00}");
baseText = baseText.Replace("{DAS SAMPLE RATE}", $"{test.Modules[0].SampleRateHz}");
baseText = baseText.Replace("{NUMBER OF CHANNELS}", $"{channelCount}");
baseText = baseText.Replace("{NUMBER OF WORDS}", $"{1 + channelCount}");
baseText = baseText.Replace("{NUMBER OF BITS}", $"{32 + 16*channelCount}");
sb.Append(baseText);
sb.Append(GetTMATSChannelPCM(test, summaryLookup));
return sb.ToString();
}
private string GetTMATSAnalog(Test test, IReadOnlyDictionary summaryLookup)
{
var sb = new StringBuilder();
var baseText = System.IO.File.ReadAllText(@"TMTTemplates\S6ATMTTemplate_ANALOG_ExportBase.tmt");
baseText = baseText.Replace("{NAME OF PROGRAM}", PrepareOutput(GetApplicationVersion()));
baseText = baseText.Replace("{TEST ID}", PrepareOutput(test.Id));
var now = DateTime.UtcNow;
baseText = baseText.Replace("{CREATE DATE}", $"{now.Year:0000}-{now.Month:00}-{now.Day:00} {now.Hour:00}:{now.Minute:00}:{now.Second:00}");
baseText = baseText.Replace("{UDP STREAM DATA CHANNEL ID}", $"{DATA_CHANNEL_ID}");
baseText = baseText.Replace("{DAS SAMPLE RATE}", $"{test.Modules[0].SampleRateHz}");
baseText = baseText.Replace("{NUMBER_OF_CHANNELS}", $"{test.Modules.Sum(m => m.Channels.Count)}");
sb.Append(baseText);
sb.AppendLine();
sb.Append(GetTMATSChannelsAnalog(test, summaryLookup));
return sb.ToString();
}
private static string GetApplicationVersion()
{
var v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
return string.Format("DataPRO {0}.{1:00}.{2:00000}", v.Major, v.Minor, v.Build);
}
#endregion
///
/// constructs the writer with a given file and encoding
///
///
///
internal Writer(File fileType, int encoding)
: base(fileType, encoding)
{
WriterParent = fileType;
}
///
/// initializes the writer
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
public void Initialize(string pathname,
string id,
string dataFolder,
Test test,
bool bFiltering,
bool includeGroupNameInISOExport,
FilteredData fd,
Channel tmChannel,
int channelNumber,
BeginEventHandler beginEventHandler,
CancelEventHandler cancelEventHandler,
EndEventHandler endEventHandler,
TickEventHandler tickEventHandler,
ErrorEventHandler errorEventHandler,
CancelRequested cancelRequested)
{
}
}
}
}