using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using DTS.Common.Classes.Sensors; using DTS.Common.DAS.Concepts; using DTS.Common.Enums.Sensors; namespace DTS.SensorDB { /// /// class for to and from Sensor Information Files (*.SIF) /// These are TDC sensor information files /// public class SensorInformationFile { /// /// just chose this out of the latest version of TDAS I have's .ini (which is actually 7.1.1x) /// as far as I know it doesn't need to be set, it may just be for informational purposes /// public const string LASTKNOWN_TDCSOFTWAREVERSION = "7.1.1w"; /// /// The SIF is a linear text file, with either a blank line, or a header, or a data entry /// on each line /// here are all the possible headers/lines we can find, /// we probably expect to see all of them, but it's probably not fatal if we don't see /// say description or something /// public enum Fields { SoftwareVersion, DummyLine1, ChannelDescription, SerialNumber, OffsetLowTol, OffsetHighTol, CalMode, CalStep, ShuntValue, ProportionalToExcitation, Sensitivity, Gain, ExcitationVoltage, EngUnits, SoftwareFilter, InvertData, ZeroReference, DesiredMaxRangeInEU, CalibrationDate, RemoveNaturalSensorOffset, InitialEUValue, SensorIDType, SensorID, ISOCode, SensorCategory } /// /// saves a sensor calibration and sensor data entry to a SIF /// will back up the file first before saving it /// /// input sensor data /// input sensor calibration /// the filename including path to save to /// any errors or warnings during the export /// true if the file was saved, false if the file was not saved public static bool SaveSIF(SensorData sd, SensorCalibration sc, string filename, ref List errors) { try { BackupFile(filename); WriteSIF(filename, sd, sc, ref errors); } catch (Exception ex) { errors.Add(ex.Message); return false; } return true; } public const string ERROR_SEPARATOR = " - "; /// /// retrieves a sensor from a SIF /// /// filename and full path to file to read /// sensor data (may be null if file could not be processed) /// sensor calibration (may be ull if file could not be processed) /// any warnings or errors that occurred during processing the file /// true if sensor was read, false if sensor could not be read public static bool LoadFromSIF(string filename, out SensorData sd, out SensorCalibration sc, ref List errors) { sd = null; sc = null; try { //check the file exists if (!File.Exists(filename)) { return false; } var lines = File.ReadAllLines(filename); sd = new SensorData(); sc = new SensorCalibration(); var category = TDCSensorCategory.Normal; var sensitivities = new List(); //go through all the lines in the file, a lot of times we'll jump two lines //but we don't assume that, we just assume one line then jump two when we need to for (var i = 0; i < lines.Length; i++) { var field = GetFieldForTag(lines[i]); int y = 0; switch (field) { case Fields.CalibrationDate: { i++;//also handle the data line var tokens = lines[i].Split(new[] { "_" }, StringSplitOptions.None); if (3 != tokens.Length) { throw new InvalidDataException(field.ToString()); } if (!int.TryParse(tokens[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out int m)) { throw new InvalidDataException(field.ToString()); } if (!int.TryParse(tokens[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out int d)) { throw new InvalidDataException(field.ToString()); } if (!int.TryParse(tokens[2], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out y)) { throw new InvalidDataException(field.ToString()); } sc.CalibrationDate = new DateTime(y, m, d); } break; case Fields.CalMode: { i++; //also handle the data line var cm = new CalMode(lines[i]); sd.Shunt = cm.ShuntCheck ? ShuntMode.Emulation : ShuntMode.None; sd.Bridge = cm.FullBridge ? SensorConstants.BridgeType.FullBridge : SensorConstants.BridgeType.HalfBridge; sd.ByPassFilter = !cm.Filter; } break; case Fields.CalStep: i++; break;//not used by datapro case Fields.ChannelDescription: { i++; //also handle the data line sd.Comment = lines[i]; } break; case Fields.DesiredMaxRangeInEU: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { //the SIF doesn't have ranges in it, so we just set all the ranges to one value to avoid confusion sd.Capacity = d; sd.RangeHigh = d; sd.RangeMedium = d; sd.RangeLow = d; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.DummyLine1: break; //empty line case Fields.EngUnits: { i++; //also handle the data line // This call insures that the display unit will be added to the list. MeasurementUnitList.GetMeasurementUnit(lines[i]); //store EU into both storage areas. DataPRO has display units and cal units //so we just set both to the same unit since SIF only has one sd.DisplayUnit = lines[i]; Array.ForEach(sc.Records.Records, record => record.EngineeringUnits = lines[i]); //FB16398: set units on all records, not just first } break; case Fields.ExcitationVoltage: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { var excitation = Test.Module.Channel.Sensor.GetExcitationVoltageEnumFromMagnitude(d); sd.SupportedExcitation = new[] { excitation }; sc.Records.Records[0].Excitation = excitation; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.Gain: i++; break;//not used case Fields.InitialEUValue: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sc.InitialOffsets = new InitialOffsets(new InitialOffset(d)); } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.InvertData: { i++; //also handle the data line switch (lines[i].ToUpper()) { case "0": case "N": case "F": sd.Invert = false; break; default: sd.Invert = true; break; } } break; case Fields.ISOCode: { i++; //also handle the data line sd.ISOCode = sd.BuildIsoCodeFromFilter(lines[i], sd.Filter.FClass); } break; case Fields.OffsetHighTol: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sd.OffsetToleranceHigh = d; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.OffsetLowTol: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sd.OffsetToleranceLow = d; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.ProportionalToExcitation: { i++; //also handle the data line switch (lines[i].ToUpper()) { case "0": case "N": case "F": sc.IsProportional = false; break; default: sc.IsProportional = true; break; } } break; case Fields.RemoveNaturalSensorOffset: { i++; //also handle the data line switch (lines[i].ToUpper()) { case "0": case "N": case "F": sc.RemoveOffset = false; break; default: sc.RemoveOffset = true; break; } } break; case Fields.Sensitivity: { i++; //also handle the data line //sensitivities in the SIF can vary based on software version, some versions may only have one decimal //some will have 4 (polynomial) note that having 4 entries doesn't mean that all 4 entries are used var tokens = lines[i].Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); sensitivities.Clear(); foreach (var t in tokens) { if (double.TryParse(t, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sensitivities.Add(d); } } } break; case Fields.SensorCategory: { i++; //also handle the data line if (int.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out int temp)) { category = (TDCSensorCategory)temp; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.SensorID: { i++; //also handle the data line sd.EID = lines[i]; if (sd.EID.ToLower().Contains("none")) { sd.EID = ""; } } break; case Fields.SensorIDType: i++; break;//not used currently case Fields.SerialNumber: { i++; //also handle the data line sd.SerialNumber = lines[i]; sc.SerialNumber = lines[i]; } break; case Fields.ShuntValue: { i++; //also handle the data line if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sd.BridgeResistance = d; } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.SoftwareFilter: { i++; //also handle the data line if (string.IsNullOrEmpty(lines[i])) { sd.Filter.FClass = FilterClassType.None; } else if (double.TryParse(lines[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double d)) { sd.Filter = new FilterClass(d); } else { throw new InvalidDataException(field + ERROR_SEPARATOR + lines[i]); } } break; case Fields.SoftwareVersion: if (false == string.IsNullOrEmpty(lines[i + 1])) { //need to read advance the line if the version is on the next line if (int.TryParse(lines[i + 1][0].ToString(), out var notUsed)) { i++; } } break; case Fields.ZeroReference: { i++; //also handle the data line //note that the SIF doesn't contain the average window start/stop for average over time var zr = new ZeroRef(lines[i]); switch (zr.ZeroMethod) { case ZeroRef.ZeroType.AverageOverTime: sc.ZeroMethods.Methods[0].Method = ZeroMethodType.AverageOverTime; break; case ZeroRef.ZeroType.UsePreEventDiagnostics: sc.ZeroMethods.Methods[0].Method = ZeroMethodType.UsePreEventDiagnosticsZero; break; case ZeroRef.ZeroType.UseZeroMv: sc.ZeroMethods.Methods[0].Method = ZeroMethodType.None; break; } } break; default: throw new NotSupportedException("unknown field: " + field); } } //now set the sensitivities based on what the sensor category is switch (category) { case TDCSensorCategory.IRTracc: if (2 > sensitivities.Count) { throw new NotSupportedException(category + ERROR_SEPARATOR + "requires 2 sensitivity entries"); } sc.NonLinear = true; if (sc.Records.Records[0].Poly.NonLinearStyle == NonLinearStyles.IRTraccAverageOverTime) { sc.Records.Records[0].Poly.NonLinearStyle = NonLinearStyles.IRTraccAverageOverTime; sc.ZeroMethods.Methods[0].Method = ZeroMethodType.AverageOverTime; sc.Records.Records[0].Poly.MMPerV = 1000D / sensitivities[0]; sc.Records.Records[0].Poly.LinearizationExponent = sensitivities[1]; } break; case TDCSensorCategory.Normal: case TDCSensorCategory.POT: sc.Records.Records[0].Sensitivity = sensitivities[0]; break; case TDCSensorCategory.Polynomial: if (8 != sensitivities.Count) { throw new InvalidDataException(category + ERROR_SEPARATOR + "requires 4 sensitivity entries"); } sc.NonLinear = true; sc.Records.Records[0].Poly.NonLinearStyle = NonLinearStyles.Polynomial; sc.Records.Records[0].Poly.SetCoefficient(3D, sensitivities[2]); sc.Records.Records[0].Poly.SetCoefficient(2D, sensitivities[3]); sc.Records.Records[0].Poly.SetCoefficient(1D, sensitivities[4]); sc.Records.Records[0].Poly.SetCoefficient(0D, sensitivities[5]); sc.ZeroMethods.Methods[0].Method = ZeroMethodType.None; break; default: throw new NotSupportedException("unknown category: " + category); } } catch (Exception ex) { errors.Add(ex.Message); return false; } return true; } /// /// returns the string header for a given field /// /// /// public static string GetTagForField(Fields field) { //treating these as all hardcoded as they are in TDC switch (field) { case Fields.CalibrationDate: return "---- Calibration Date M_D_Y ----"; case Fields.CalMode: return "---- Cal Mode - I=Voltage Insertion, S=Shunt ----"; case Fields.CalStep: return "---- Cal Step - shunt resistor (Ohms) ----"; case Fields.ChannelDescription: return "---- Channel Description ----"; case Fields.DesiredMaxRangeInEU: return "---- Desired Max Range in Eng Units ----"; case Fields.DummyLine1: return ""; case Fields.EngUnits: return "---- Eng Unit ----"; case Fields.ExcitationVoltage: return "---- Excitation Voltage must be a valid voltage from list ----"; case Fields.Gain: return "---- Gain - must be a valid gain from list ----"; case Fields.InitialEUValue: return "---- Initial EU Value ----"; case Fields.InvertData: return "---- Invert Data - 0=no invert, 1=yes invert ----"; case Fields.ISOCode: return "---- ISO Code ----"; case Fields.OffsetHighTol: return "---- Offset High Tol (mV) ----"; case Fields.OffsetLowTol: return "---- Offset Low Tol (mV) ----"; case Fields.ProportionalToExcitation: return "---- Proportional to Excitation ----"; case Fields.RemoveNaturalSensorOffset: return "---- Remove Natural Sensor Offset? ----"; case Fields.Sensitivity: return "---- Sensitivity (mV/V/eng unit) ----"; case Fields.SensorCategory: return "---- Sensor Category (Use 0 for most sensors) ----"; case Fields.SensorID: return "---- Sensor ID No ----"; case Fields.SensorIDType: return "---- Sensor ID Type ----"; case Fields.SerialNumber: return "---- Serial Number ----"; case Fields.ShuntValue: return "---- Shunt Value - corresponding value of shunt resistor in Eng Units ----"; case Fields.SoftwareFilter: return "---- Software Filter, -3dB point (Hz) ----"; case Fields.SoftwareVersion: return "Software Version: "; case Fields.ZeroReference: return "---- Zero Reference - 0=use 30 msec avg, 1=use prezero, 2=equals zero mV ----"; default: throw new NotSupportedException("unknown field: " + field); } } /// /// returns a field given a header line /// /// /// public static Fields GetFieldForTag(string tag) { if (tag.StartsWith(GetTagForField(Fields.SoftwareVersion))) { return Fields.SoftwareVersion; } switch (tag) { case "---- Calibration Date M_D_Y ----": return Fields.CalibrationDate; case "---- Cal Mode - I=Voltage Insertion, S=Shunt ----": return Fields.CalMode; case "---- Cal Step - shunt resistor (Ohms) ----": return Fields.CalStep; case "---- Channel Description ----": return Fields.ChannelDescription; case "---- Desired Max Range in Eng Units ----": return Fields.DesiredMaxRangeInEU; case "": return Fields.DummyLine1; case "---- Eng Unit ----": return Fields.EngUnits; case "---- Excitation Voltage must be a valid voltage from list ----": return Fields.ExcitationVoltage; case "---- Gain - must be a valid gain from list ----": return Fields.Gain; case "---- Initial EU Value ----": case "---- Initial EU Value": // Older TDC sifs have this return Fields.InitialEUValue; case "---- Invert Data - 0=no invert, 1=yes invert ----": return Fields.InvertData; case "---- ISO Code ----": return Fields.ISOCode; case "---- Offset High Tol (mV) ----": return Fields.OffsetHighTol; case "---- Offset Low Tol (mV) ----": return Fields.OffsetLowTol; case "---- Proportional to Excitation ----": return Fields.ProportionalToExcitation; case "---- Remove Natural Sensor Offset? ----": return Fields.RemoveNaturalSensorOffset; case "---- Sensitivity (mV/V/eng unit) ----": return Fields.Sensitivity; case "---- Sensor Category (Use 0 for most sensors) ----": return Fields.SensorCategory; case "---- Sensor ID No ----": return Fields.SensorID; case "---- Sensor ID Type ----": return Fields.SensorIDType; case "---- Serial Number ----": return Fields.SerialNumber; case "---- Shunt Value - corresponding value of shunt resistor in Eng Units ----": return Fields.ShuntValue; case "---- Software Filter, -3dB point (Hz) ----": case "---- Software Filter, -3dB point (HZ) ----": // Older TDC sifs have this return Fields.SoftwareFilter; case "Software Version: ": case "Software Version:": // Older TDC sifs have this return Fields.SoftwareVersion; case "---- Zero Reference - 0=use 30 msec avg, 1=use prezero, 2=equals zero mV ----": return Fields.ZeroReference; default: throw new NotSupportedException("unknown field: " + tag); } } /// /// moves file to a backup destination location /// will delete existing backup if it's already present /// /// private static void BackupFile(string fileName) { if (File.Exists(fileName)) { var fi = new FileInfo(fileName); var backupName = fileName.Replace(fi.Extension, ".BAK"); if (File.Exists(backupName)) { File.Delete(backupName); } File.Move(fileName, backupName); } } /// /// these are constants from TDAS Control for the field SensorCategory /// public enum TDCSensorCategory { Normal = 0, POT = 1, IRTracc = 2, Polynomial = 3 } /// /// Writes a sensor entry and calibration to a Sensor Information File (SIF) /// /// filename and full path to file to write /// /// /// any errors or warnings that occurred during writing private static void WriteSIF(string filename, SensorData sd, SensorCalibration sc, ref List errors) { var sb = new StringBuilder(1500); var fields = Enum.GetValues(typeof(Fields)).Cast().ToArray(); var capacity = sd.Capacity * MeasurementUnitList.GetMeasurementUnit(sc.Records.Records[0].EngineeringUnits).GetScalerConversionFrom(sd.DisplayUnit); var excitation = Test.Module.Channel.Sensor.GetExcitationVoltageMagnitudeFromEnum(sd.SupportedExcitation[0]); var calmode = new CalMode() { Filter = !sd.ByPassFilter, FullBridge = sd.Bridge == SensorConstants.BridgeType.FullBridge, ShuntCheck = sd.Shunt == ShuntMode.Emulation }; ZeroRef zeroref = null; //TODO: linear/nonlinear switch (sc.ZeroMethods.Methods[0].Method) { case ZeroMethodType.AverageOverTime: zeroref = new ZeroRef(ZeroRef.ZeroType.AverageOverTime); break; case ZeroMethodType.None: zeroref = new ZeroRef(ZeroRef.ZeroType.UseZeroMv); break; case ZeroMethodType.UsePreEventDiagnosticsZero: zeroref = new ZeroRef(ZeroRef.ZeroType.UsePreEventDiagnostics); break; default: throw new NotSupportedException("unknown zero method type: " + sc.ZeroMethods.Methods[0].Method); } foreach (var f in fields) { if (f == Fields.SoftwareVersion) { sb.AppendFormat("{0}{1}\n", GetTagForField(f), LASTKNOWN_TDCSOFTWAREVERSION); } else { var tag = GetTagForField(f); sb.AppendLine(tag); switch (f) { case Fields.CalibrationDate: sb.AppendFormat("{0}_{1}_{2}\n", sc.CalibrationDate.Month, sc.CalibrationDate.Day, sc.CalibrationDate.Year); break; case Fields.CalMode: sb.AppendLine(calmode.ToString()); break; case Fields.CalStep: sb.AppendLine("-1"); break;//for now we only support emulation? case Fields.ChannelDescription: sb.AppendLine(sd.Comment); break; case Fields.DesiredMaxRangeInEU: sb.AppendLine(capacity.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.DummyLine1: break; case Fields.EngUnits: sb.AppendLine(sc.Records.Records[0].EngineeringUnits); break; case Fields.ExcitationVoltage: sb.AppendFormat("{0:0.0}", excitation); break; case Fields.Gain: sb.AppendFormat("1.0"); break;//not used in datapro ... maybe we need to calculate it case Fields.InitialEUValue: sb.Append(SensorData.GetInitialEUValue(sc, sd.SupportedExcitation[0], sc.InitialOffsets.Offsets[0]).ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.InvertData: if (sd.Invert) { sb.AppendLine("1"); } else { sb.AppendLine("0"); } break; case Fields.ISOCode: sb.AppendLine(sd.ISOCode); break; case Fields.OffsetHighTol: sb.AppendLine(sd.OffsetToleranceHigh.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.OffsetLowTol: sb.AppendLine(sd.OffsetToleranceLow.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.ProportionalToExcitation: if (sc.IsProportional) { sb.AppendLine("Y"); } else { sb.AppendLine("N"); } break; case Fields.RemoveNaturalSensorOffset: if (sc.RemoveOffset) { sb.AppendLine("Y"); } else { sb.AppendLine("N"); } break; case Fields.Sensitivity: if (sc.NonLinear) { double d1 = 1, d2 = 0, d3 = 0, d4 = 0, d5 = 0, d6 = 0, d7 = 0, d8 = 0; if (sc.Records.Records[0].Poly.NonLinearStyle == NonLinearStyles.Polynomial) { d8 = sc.Records.Records[0].Poly.GetCoefficient(5D); d7 = sc.Records.Records[0].Poly.GetCoefficient(4D); d6 = sc.Records.Records[0].Poly.GetCoefficient(3D); d5 = sc.Records.Records[0].Poly.GetCoefficient(2D); d4 = sc.Records.Records[0].Poly.GetCoefficient(1D); d3 = sc.Records.Records[0].Poly.GetCoefficient(0D); } else { d1 = sc.Records.Records[0].Poly.MMPerV / 1000D; d2 = sc.Records.Records[0].Poly.LinearizationExponent; } sb.AppendFormat("{0},{1},{2},{3},{4},{5},{6},{7}\n", d1.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d2.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d3.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d4.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d5.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d6.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d7.ToString("F12", System.Globalization.CultureInfo.InvariantCulture), d8.ToString("F12", System.Globalization.CultureInfo.InvariantCulture)); } else { sb.AppendLine(sc.Records.Records[0].Sensitivity.ToString("F12", System.Globalization.CultureInfo.InvariantCulture)); } break; case Fields.SensorCategory: var cat = TDCSensorCategory.Normal; if (sc.NonLinear) { if (sc.Records.Records[0].Poly.NonLinearStyle == NonLinearStyles.Polynomial) { cat = TDCSensorCategory.Polynomial; } else { cat = TDCSensorCategory.IRTracc; } } sb.AppendLine(((int)cat).ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.SensorID: sb.AppendLine(sd.EID); break; case Fields.SensorIDType: sb.AppendLine("Dallas"); break; //D or S? case Fields.SerialNumber: sb.AppendLine(sd.SerialNumber); break; case Fields.ShuntValue: sb.AppendLine(sd.BridgeResistance.ToString(System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.SoftwareFilter: sb.AppendLine(sd.Filter.Frequency.ToString("F0", System.Globalization.CultureInfo.InvariantCulture)); break; case Fields.SoftwareVersion: break; case Fields.ZeroReference: sb.AppendLine(zeroref.ToString()); break; default: throw new NotSupportedException("unsupported field: " + f); } } } } } }