using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using DTS.Common; using DTS.Common.Classes.Sensors; using DTS.Common.Events; using DTS.Common.Interface; using DTS.Common.Settings; using DTS.Common.Utilities.Logging; using DTS.Serialization; using Prism.Ioc; using Prism.Events; // ReSharper disable InconsistentNaming namespace DTS.Viewer.TestModification.Model { public class TestModelManipulation { #region constants /// /// parameters in test serialization /// public const double UNUSED_START_TIME = 0; public const int UNUSED_DATA_COLLECTION_LENGTH = 0; /// /// .chn file can back up just the header or the entire file /// if backed up as a header the extension will be .header.bak /// private const string BackupHeaderExtension = ".header.bak"; /// /// the extension for full backup files (.chn.bak, .dts.bak) /// private const string BackupFileExtension = ".bak"; #endregion /// /// revert changes since original file, this includes the .DTS file and all binary files /// public static void UndoAllModification(ITestModificationModel model) { RestoreDTSFileIfModified(model); var f = new Serialization.SliceRaw.File { DefaultEncoding = Encoding.Unicode.CodePage }; f.Importer.Read(model.SelectedChannel.BinaryFilePath, out var target); foreach (var m in target.Modules) { foreach (var c in m.Channels) { RestoreChannelIfModified(c); } foreach (var cc in m.CalculatedChannels) { RestoreChannelIfModified(cc); } } var testId = GetTestIdFromBinaryFileName(model.SelectedChannel.BinaryFileName); var dtsFileName = Path.Combine(model.SelectedChannel.BinaryFilePath, testId + ".dts"); var eventAggregator = ContainerLocator.Container.Resolve(); //if DeriveROI from all is true, wait till the derive function is complete otherwise you could be //reading and writing from the same files at the same time if (!SettingsDB.GetGlobalValueBool("DeriveROIFromAll", false)) { eventAggregator?.GetEvent().Publish(new List { /*model.SelectedChannel*/ }); eventAggregator?.GetEvent().Publish(dtsFileName); } eventAggregator?.GetEvent() .Publish(new TestModificationArgs(model.SelectedChannel.BinaryFilePath, testId)); } /// /// returns whether a backup file exists for the given model /// public static bool BackupExists(ITestModificationModel model) { if (model.SelectedChannel != null) { var testId = GetTestIdFromBinaryFileName(model.SelectedChannel.BinaryFileName); var dtsFileName = Path.Combine(model.SelectedChannel.BinaryFilePath, testId + ".dts"); var dtsBackupFileName = dtsFileName + BackupFileExtension; var chnBackupFileName = Path.Combine(model.SelectedChannel.BinaryFilePath, model.SelectedChannel.BinaryFileName + BackupFileExtension); return System.IO.File.Exists(dtsBackupFileName) || System.IO.File.Exists(chnBackupFileName); } return false; } /// /// returns the test id from a binary filename /// it didn't seem the test id was available from any of the structures I had access to /// but it's the portion of the file name without all the decoration /// the filename is in the form of TestIdChxxx.yyy.chn /// /// /// private static string GetTestIdFromBinaryFileName(string filename) { var index = filename.IndexOf('.'); filename = filename.Substring(0, index); index = filename.LastIndexOf("Ch", StringComparison.Ordinal); //we expect the Ch, but if we don't find it, use the what portion we have return index >= 0 ? filename.Substring(0, index) : filename; } /// /// restores DTS file (if a backup file exists) /// private static void RestoreDTSFileIfModified(ITestModificationModel model) { var testId = GetTestIdFromBinaryFileName(model.SelectedChannel.BinaryFileName); var dtsFileName = Path.Combine(model.SelectedChannel.BinaryFilePath, testId + ".dts"); var dtsBackupFileName = dtsFileName + BackupFileExtension; if (!System.IO.File.Exists(dtsBackupFileName)) return; //Delete the current .dts file Common.Utils.FileUtils.DeleteFileOrMove(dtsFileName, APILogger.Log); System.IO.File.Move(dtsBackupFileName, dtsFileName); var eventAggregator = ContainerLocator.Container.Resolve(); eventAggregator?.GetEvent() .Publish(new TestModificationArgs(model.SelectedChannel.BinaryFilePath, testId)); } /// /// restores an individual channel binary file [assuming a backup exists] /// note this does not restore any xml entries in the .dts file for the channel /// /// private static void RestoreChannelIfModified(Test.Module.Channel channelToRestore) { var chnFileName = ((Test.Module.AnalogInputChannel)channelToRestore).PersistentChannelInfo.Filename; var chnHeaderBackupFileName = chnFileName + BackupHeaderExtension; var chnBackupFileName = chnFileName + BackupFileExtension; //binary files can be backed up two different ways, a full backup or just the header //if we find the header, we can restore be just overwriting the bytes from the backup header if (System.IO.File.Exists(chnHeaderBackupFileName)) { var originalFile = System.IO.File.ReadAllBytes(chnHeaderBackupFileName); var headerLength = channelToRestore.PersistentChannelInfo.GetFileOffsetOf(Serialization.SliceRaw .File.PersistentChannel.Field.BeginningOfData); using (var fs = System.IO.File.OpenWrite(chnFileName)) { fs.Write(originalFile, 0, headerLength); //146 } //Delete the copy of the original header Common.Utils.FileUtils.DeleteFileOrMove(chnHeaderBackupFileName, APILogger.Log); } //if there's a full backup, then just replace the entire file with the backup, if there's no full backup //we are already done if (!System.IO.File.Exists(chnBackupFileName)) return; //Delete Current File Common.Utils.FileUtils.DeleteFileOrMove(chnFileName, APILogger.Log); //Restore original file System.IO.File.Copy(chnBackupFileName, chnFileName); //Delete the copy of the original header Common.Utils.FileUtils.DeleteFileOrMove(chnBackupFileName, APILogger.Log); } /// /// backs up a single binary channel /// binary channels can be backed up two ways, either the entire binary file or just the header /// we backup the entire binary file if we are modifying data /// if we are just modifying the binary header, then we just back up the header (since it's significantly smaller usually) /// /// /// public static void BackupChannelIfNeeded(Test.Module.Channel testModuleChannel, bool headerOnly) { //Back up .chn file if necessary var chnFilePath = testModuleChannel.PersistentChannelInfo.Filename; var chnBackupFilePath = chnFilePath + BackupFileExtension; // If we have a full backup of the CHN, quit if (System.IO.File.Exists(chnBackupFilePath)) return; var chnHeaderBackupFilePath = chnFilePath + BackupHeaderExtension; byte[] originalFileHeader = null; // If we are backing up the header and have a header backup, quit // Else if we have to backup the whole file, get the header backup bytes then delete the header backup if (System.IO.File.Exists(chnHeaderBackupFilePath)) { if (headerOnly) { return; } //Read contents of header backup originalFileHeader = System.IO.File.ReadAllBytes(chnHeaderBackupFilePath); //Delete the copy of the original header Common.Utils.FileUtils.DeleteFileOrMove(chnHeaderBackupFilePath, APILogger.Log); } // Get the header bytes from the original chn and write the header backup if (headerOnly) { var originalFile = System.IO.File.ReadAllBytes(chnFilePath); // Get the end of the header byte offset index var writeEndIndex = testModuleChannel.PersistentChannelInfo.GetFileOffsetOf(Serialization.SliceRaw.File.PersistentChannel.Field.BeginningOfData); // open the header backup to write using (var fs = System.IO.File.OpenWrite(chnHeaderBackupFilePath)) { fs.Write(originalFile, 0, writeEndIndex); } } else { // Copy the whole chn to the backup location System.IO.File.Copy(chnFilePath, chnBackupFilePath); // If we dont have a header backup, we are done if (originalFileHeader == null) return; // Get the end of the header byte offset index var writeEndIndex = testModuleChannel.PersistentChannelInfo.GetFileOffsetOf(Serialization.SliceRaw.File.PersistentChannel.Field.BeginningOfData); // open the full backup file to write the original header on top of using (var fs = System.IO.File.OpenWrite(chnBackupFilePath)) { fs.Write(originalFileHeader, 0, writeEndIndex); } } } /// /// backs up the DTS file /// if the backup file already exists we don't need to do any work, the backup file will be /// the original version of the file /// /// /// private static void BackupDTSFile(string directory, string testId) { var dtsFilePath = Path.Combine(directory, testId + ".dts"); var dtsBackupFilePath = dtsFilePath + ".bak"; //Back up dts file only if a backup file does not exist. This should guarantee that the //original file is the only one that is preserved. if (false == System.IO.File.Exists(dtsBackupFilePath)) { System.IO.File.Copy(dtsFilePath, dtsBackupFilePath); } } /// /// writes all modifications to binary files/dts file /// /// /// controls whether to change isocode field for software filter when the software filter is modified. /// /// /// /// public static bool SaveModification(ITestModificationModel model, bool useISOCodeFilterMapping, bool bUseZeroForUnfiltered) { var bWriteDTSFile = model.IsModifiedDataFlag || model.IsModifiedFilter || model.IsModifiedDescription || model.IsModifiedT0 || model.IsModifiedEuMultiplier || model.IsModifiedEuOffset || model.IsModifiedSensitivity; var bWriteChnBackup = model.IsModifiedT0 || model.IsModifiedSensitivity || model.IsModifiedLineFit; var bWriteHeaderOnly = model.IsModifiedSensitivity || model.IsModifiedT0 || (model.IsModifiedFilter && useISOCodeFilterMapping); if (!bWriteDTSFile && !bWriteChnBackup) { return true; } var testId = GetTestIdFromBinaryFileName(model.SelectedChannel.BinaryFileName); if (bWriteDTSFile) { BackupDTSFile(model.SelectedChannel.BinaryFilePath, testId); } var dtsFile = Path.Combine(model.SelectedChannel.BinaryFilePath, $"{testId}.dts"); Serialization.SliceRaw.File.ReadTestSetup(dtsFile, out var target, out var testSetup, false); foreach (var module in target.Modules) { if (model.IsModifiedT0 && model.T0Mode == T0Mode.Test) { //if we are applying T0 channes to the entire test, then this module will be updated regardless of what channel is selected ApplyT0Change(model, module); } if (model.IsModifiedT0 && model.T0Mode == T0Mode.DAS && module.Channels.TrueForAll(ch => ch.ChannelId != model.SelectedChannel.ChannelId) && module.BaseSerialNumber == target.Modules.First(m => m.Channels.Exists(ch => ch.ChannelId == model.SelectedChannel.ChannelId))?.BaseSerialNumber) { //the model has a modified T0, we're in DAS mode, the module does *not* have the selected channel, but is on the same DAS as the one that does ApplyT0Change(model, module); } foreach (var channel in module.Channels) { //find the specific channel in the dts file/binaries that matches the selected channel in the UI if ((channel.ChannelId != model.SelectedChannel.ChannelId || !channel.PersistentChannelInfo.Filename.EndsWith(model.SelectedChannel.BinaryFileName)) && !model.IsModifiedT0) continue; if (model.IsModifiedFilter && channel.ChannelId == model.SelectedChannel.ChannelId && useISOCodeFilterMapping) { bWriteChnBackup = true; } if (bWriteChnBackup) { BackupChannelIfNeeded(channel, bWriteHeaderOnly); } var aic = (Test.Module.AnalogInputChannel)channel; if (model.IsModifiedDescription && channel.ChannelId == model.SelectedChannel.ChannelId) { channel.ChannelDescriptionString = model.Description; model.SelectedChannel.SetChannelDescriptionAndDisplayName(model.Description); } if (model.IsModifiedDataFlag && channel.ChannelId == model.SelectedChannel.ChannelId) { channel.DataFlag = (int)model.SelectedDataFlag; model.SelectedChannel.DataFlag = channel.DataFlag; } if (model.IsModifiedFilter && channel.ChannelId == model.SelectedChannel.ChannelId) { //FB 13120 aic.SoftwareFilter = CFCFilterDTSFileStringConverter.FilterClassToString(model.SelectedFilter); model.SelectedChannel.SoftwareFilter = aic.SoftwareFilter; if (useISOCodeFilterMapping) { var iso = new Common.ISO.IsoCode(aic.IsoCode); //FB 13120 iso.FilterClass = CFCFilterDTSFileStringConverter.FilterClassToCFC(model.SelectedFilter, bUseZeroForUnfiltered); aic.IsoCode = iso.StringRepresentation; var notUsed = aic.PersistentChannelInfo.NumberOfTriggers; aic.PersistentChannelInfo.IsoCode = iso.StringRepresentation.ToArray(); model.SelectedChannel.IsoCode = iso.StringRepresentation; } } if (model.IsModifiedEuMultiplier && channel.ChannelId == model.SelectedChannel.ChannelId) { aic.Multiplier = model.EuMultiplier; model.SelectedChannel.Multiplier = model.EuMultiplier; } if (model.IsModifiedEuOffset && channel.ChannelId == model.SelectedChannel.ChannelId) { aic.UserOffsetEU = model.EuOffset; model.SelectedChannel.UserOffsetEu = model.EuOffset; } if (model.IsModifiedSensitivity && channel.ChannelId == model.SelectedChannel.ChannelId) { //modifying sensitivity involves both changing the dts file and the .chn binary var mvPerEU = Math.Abs(model.Sensitivity); aic.Sensitivity = model.Sensitivity; model.SelectedChannel.Sensitivity = model.Sensitivity; var p = aic.PersistentChannelInfo; //this is to force the values to be initialized before setting // ReSharper disable once UnusedVariable var notUsed = p.MvPerEu; p.MvPerEu = mvPerEU; } if (model.IsModifiedT0 && model.T0Mode == T0Mode.DAS && channel.ChannelId == model.SelectedChannel.ChannelId) { ApplyT0Change(model, module); } if (model.IsModifiedLineFit && channel.ChannelId == model.SelectedChannel.ChannelId) { ApplyLineFit(model, channel); } if (bWriteChnBackup && channel.ChannelId == model.SelectedChannel.ChannelId || model.IsModifiedT0) { var p = aic.PersistentChannelInfo; p.StampCrc(); } } } if (bWriteDTSFile) { WriteDTSFileChanges(model, dtsFile, target, testSetup, testId); } //this is to publish that the selected channel has changed var eventAggregator = ContainerLocator.Container.Resolve(); eventAggregator?.GetEvent() .Publish(new TestModificationArgs(model.SelectedChannel.BinaryFilePath, testId)); return true; } // added TestId parameter to correct incorrect target.TestId values, function now uses passed in test id rather than target.testid //14897 modifying T0 in view data creates a second dts file holding the name of the test setup and not the test ID private static void WriteDTSFileChanges(ITestModificationModel model, string dtsFile, Test target, TestSetup testSetup, string testId) { var f = new Serialization.SliceRaw.File { DefaultEncoding = Encoding.Unicode.CodePage }; f.Exporter.Write(model.SelectedChannel.BinaryFilePath, testId, target, false, false, UNUSED_START_TIME, UNUSED_DATA_COLLECTION_LENGTH); //Append the DataPRO info to the end of the existing .dts file which already contains SLICEWare-compatible info using (var writer = new StringWriter()) { new System.Xml.Serialization.XmlSerializer(typeof(TestSetup)).Serialize(writer, testSetup); Encoding encoder; try { //force UTF-16 for the dts file, it contains "UTF-16" in the xml by default and isn't consumed by anything that requires //codepage exports (CSV/excel) encoder = Encoding.Unicode; //UTF-16 } catch (Exception ex) { APILogger.Log("Problem getting encoder", ex); encoder = Encoding.Default; } using (var fileWriter = new StreamWriter(dtsFile, true, encoder)) { fileWriter.Write(fileWriter.NewLine + writer); } System.Threading.Thread.Sleep(10); } } public static bool SaveModificationDataFlag(ITestModificationModel model) { if (!model.IsModifiedDataFlag) { return true; } var testId = GetTestIdFromBinaryFileName(model.SelectedChannel.BinaryFileName); BackupDTSFile(model.SelectedChannel.BinaryFilePath, testId); var dtsFile = Path.Combine(model.SelectedChannel.BinaryFilePath, $"{testId}.dts"); Serialization.SliceRaw.File.ReadTestSetup(dtsFile, out var target, out var testSetup, false); foreach (var module in target.Modules) { foreach (var channel in module.Channels) { //find the specific channel in the dts file/binaries that matches the selected channel in the UI if (channel.ChannelId != model.SelectedChannel.ChannelId) continue; if (!model.IsModifiedDataFlag) continue; channel.DataFlag = (int)model.SelectedDataFlag; model.SelectedChannel.DataFlag = channel.DataFlag; } } WriteDTSFileChanges(model, dtsFile, target, testSetup, testId); //this is to publish that the selected channel has changed var eventAggregator = ContainerLocator.Container.Resolve(); eventAggregator?.GetEvent().Publish(new List { model.SelectedChannel }); PopulateFromChannel(model); return true; } /// /// this applies line fit on the selected channel /// /// /// public static void ApplyLineFit(ITestModificationModel model, Test.Module.Channel channel) { BackupChannelIfNeeded(channel, false); using (var p = channel.PersistentChannelInfo) { p.StampCrc(); //Force init of all fields var adcData = p.Data.ToList(); var startIndex = GetSampleIndexFromMilliseconds(channel.ParentModule, model.T1); var endIndex = GetSampleIndexFromMilliseconds(channel.ParentModule, model.T2); //swap start and end if they are offset if ( startIndex > endIndex) { var temp = endIndex; endIndex = startIndex; startIndex = temp; } var start = startIndex + 1; var dataLength = (ulong)adcData.LongCount(); if ( start >= dataLength) { return; } if (start > int.MaxValue) { start = int.MaxValue; } if (endIndex >= dataLength) { endIndex = dataLength - 1; } if (startIndex > endIndex) { var temp = endIndex; endIndex = startIndex; startIndex = temp; } var end = endIndex - 1; var deltaIndex = endIndex - startIndex; var startADC = adcData[(int)startIndex]; var deltaADC = adcData[(int)endIndex] - startADC; //http://manuscript.dts.local/f/cases/36697/Line-fit-feature-not-working-properly-after-modifying-data //modified this to just do a straight line fit between two points - DTM 2024-02-05 for (var i = start; i <= end; i++) { adcData[(int)i] = (short)(startADC + (deltaADC * ((double)(i - start) / deltaIndex))); } p.ReplaceData(adcData.ToArray()); } } /// /// preview line fit will update the data in the UI for the selected channel, but not on disk /// public static void PreviewLineFit(ITestModificationModel model) { var f = new Serialization.SliceRaw.File { DefaultEncoding = Encoding.Unicode.CodePage }; f.Importer.Read(model.SelectedChannel.BinaryFilePath, out var target); Test.Module.Channel ch = null; foreach (var module in target.Modules) { if (null != ch) { break; } foreach (var channel in module.Channels) { if (channel.ChannelId == model.SelectedChannel.ChannelId) { ch = channel; } } } if (null == ch) { return; } var startIndex = GetSampleIndexFromMilliseconds(ch.ParentModule, model.T1); var endIndex = GetSampleIndexFromMilliseconds(ch.ParentModule, model.T2); var eventAggregator = ContainerLocator.Container.Resolve(); eventAggregator?.GetEvent().Publish(new LineFitArgs(model.SelectedChannel, startIndex, endIndex)); } /// /// Returns the sample index from the file based on the given milliseconds /// /// /// /// private static ulong GetSampleIndexFromMilliseconds(Test.Module m, double milliSeconds) { try { var seconds = milliSeconds / 1000.0; var triggerSampleNumber = Convert.ToDouble(m.TriggerSampleNumbers[0]); var offset = m.SampleRateHz * seconds; if (triggerSampleNumber + offset > m.StartRecordSampleNumber) { return Convert.ToUInt64(triggerSampleNumber + offset - m.StartRecordSampleNumber); } } catch( OverflowException ex) { if (milliSeconds > 0) { return ulong.MaxValue; } return 0UL; } return 0UL; } /// /// applies an adjustment to T0 for the module provided /// /// /// private static void ApplyT0Change(ITestModificationModel model, Test.Module module) { //apply t0 changes var sampleNumberChange = Convert.ToInt64(-1 * Math.Truncate(model.T0 * module.SampleRateHz / 1000D)); var timeChange = 0 != module.SampleRateHz ? -1D * sampleNumberChange / module.SampleRateHz : model.T0 / 1000D; if (module.TriggerSampleNumbers.Any()) { var oldTrigger = module.TriggerSampleNumbers[0]; var newTrigger = 0UL; if ((long)oldTrigger > sampleNumberChange) { newTrigger = Convert.ToUInt64((long)oldTrigger - sampleNumberChange); module.RequestedPreTriggerSeconds = module.RequestedPreTriggerSeconds - timeChange; module.RequestedPostTriggerSeconds = module.RequestedPostTriggerSeconds - timeChange; } module.TriggerSampleNumbers = new List { newTrigger }; foreach (var channel in module.Channels) { channel.TimeOfFirstSampleSec -= timeChange; BackupModifyT0AndStampChannel(channel, newTrigger); } foreach (var channel in module.CalculatedChannels) { channel.TimeOfFirstSampleSec -= timeChange; BackupModifyT0AndStampChannel(channel, newTrigger); } } } private static void BackupModifyT0AndStampChannel(Test.Module.Channel channel, ulong newTrigger) { BackupChannelIfNeeded(channel, true); var aic = (Test.Module.AnalogInputChannel)channel; var p = aic.PersistentChannelInfo; //this is done just to assure the property is initialized // ReSharper disable once UnusedVariable var notUsed = p.TriggerSampleNumbers; p.TriggerSampleNumbers = new[] { newTrigger }; p.StampCrc(); } /// /// revert changes since last save /// public static void UndoModification(ITestModificationModel model) { PopulateFromChannel(model); var eventAggregator = ContainerLocator.Container.Resolve(); eventAggregator?.GetEvent().Publish(new List { model.SelectedChannel }); } /// /// Populates/Initializes UI from selected channel /// public static void PopulateFromChannel(ITestModificationModel model) { model.Description = model.SelectedChannel?.ChannelDescriptionString; model.EuMultiplier = model.SelectedChannel?.Multiplier ?? 1D; model.T0 = 0D; model.EuOffset = model.SelectedChannel?.UserOffsetEu ?? 0D; //FB 13120 model.SelectedFilter = FilterClass.GetFilterClassFromString(model.SelectedChannel?.SoftwareFilter); model.T1 = 0D; model.T2 = 0D; model.Sensitivity = model.SelectedChannel?.Sensitivity ?? 1D; model.SelectedDataFlag = null == model.SelectedChannel ? DataFlag.None : (DataFlag)model.SelectedChannel.DataFlag; } } }