/// /// Copyright (c) 2018 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using log4net; using TBF.Boxes; using TBF.BenchControl.Generic; using TBF.BenchControl.GenericDevices; namespace TBF.BenchControl.Various.TankWithLevelMsrmnt { public class Tank : TBF.BenchControl.MettlerToledo.TankDraining, IDevice, IScaleOrTank, IVolumeMeter, IOperation { /// /// Info: StartMassMeasurement() and "Balnce response = .., Mass = ..." /// Warn: Start measurement when busy is true /// private static readonly ILog log = LogManager.GetLogger(typeof(Tank)); public override string ToString() { return string.Format("TankWithLevelMsrmnt({0})", Cfg.ToString(1)); } readonly TankCfg tankCfg; bool useCalibTable; double[] calibHeight; double[] calibVolume; public enum CurrentOp { None, ReadVolume, ReadStableVolume, ManualReadVolume, } CurrentOp currentOp; DoubleBox volumeBox; double sdev; TBF.BenchControl.GenericDevices.ILevelMeter levelMeter; TBF.BenchControl.GenericDevices.ILevelMeter manualLevelMeter; DoubleBox levelBox; IOperation readLevelOp; /// /// Enumeration of balances via static fields and methods /// static int nextTankIdx = 0; public static new void ResetStaticProperties() { nextTankIdx = 0; } public static int TanksCount { get { return nextTankIdx; } } public static Tank[] Tanks; /// protected int tankNr; /// 0-based tank number public int ScaleNr { get { return tankNr + 1; } } /// Formula fits CEVAK config., TODO: Make ScaleNr a configurable parameter /// public double Capacity { get { return tankCfg.Capacity; } } public int EmptyTimeSec { get { return tankCfg.DrainTimeSec; } } public IValve DrainValve2 { get { return drainValve2; } } IValve drainValve2; public string Format { get { return "F3"; } } public double Volume { get { return Level2Volume(levelMeter.Level); } } public Tank() { } /// /// Constructor /// /// Balance properties /// A list of components loaded so far public Tank(Generic.IComponentCfg cfg) : base(cfg) { tankCfg = cfg as TankCfg; tankNr = nextTankIdx++; /// if (Tanks == null || Tanks.Length < nextTankIdx) { Tank[] tanksSoFar = Tanks; Tanks = new Tank[nextTankIdx]; if (tanksSoFar != null) for (int i = 0; i < tanksSoFar.Length; i++) Tanks[i] = tanksSoFar[i]; Tanks[nextTankIdx - 1] = this; } currentOp = CurrentOp.None; levelBox = new DoubleBox(); log.Debug(this.ToString()); } /// /// IDevice interface (required to call base and initialize 'DrainValve') /// public void Initialize() { base.Initalize(); drainValve2 = TbfComponents.FindComponent(tankCfg.DrainValve2) as IValve; manualLevelMeter = new Various.ManualLevelMsrmnt.ManualLevelMsrmnt(); levelMeter = TbfComponents.FindComponent(tankCfg.LevelMeasurement) as ILevelMeter; if (levelMeter == null) levelMeter = manualLevelMeter; useCalibTable = LoadCalibrationTable(tankCfg.CalibrationTable); } public void RunDeviceBefore() { } public void RunDeviceAfter() { } public void StopDevice() { } public void StopDevice2() { } public override bool IsEmpty() { return false; } public override bool ContainsMoreThen(float thld) { return (Volume >= thld); } /// /// Loads a calibration table in CSV format (column1 = height[mm], column2 = volume[l]). /// /// Path name of the CSV file /// true when loaded successfully bool LoadCalibrationTable(string pathName) { try { IList heights = new List(); IList volumes = new List(); using (TextReader reader = new StreamReader(pathName)) { while (true) { double h, v; string line = reader.ReadLine(); if (line == null) break; string[] columns = line.Split(new char[] { ';' }); if (columns.Length != 2 || !double.TryParse(columns[0], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out h) || !double.TryParse(columns[1], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v)) { break; } if (heights.Count > 0 && h <= heights[heights.Count - 1]) { throw new Exception("Calibration file content error: height is not monotonically increasing"); } heights.Add(h); volumes.Add(v); } } if ((heights.Count == 0) || (heights.Count != volumes.Count)) { throw new Exception("No data in the calibration file"); } calibHeight = new double[heights.Count]; calibVolume = new double[heights.Count]; for (int i = 0; i < heights.Count; i++) { calibHeight[i] = heights[i]; calibVolume[i] = volumes[i]; } log.WarnFormat("calibration table {0} was successfully loaded.", pathName); return true; } catch (Exception) { log.Warn("calibration table was NOT loaded."); return false; } } /// /// Converts water level to volume. /// Tank capcacity is used o distinguish bettween existing tanks: /// CEVAL small tank: Capacity = 2000 .. 2220 /// CEVAL large tank: Capacity = 10000 .. 10850 /// /// Height in mm /// Volume in l double Level2Volume(double h) { if (useCalibTable) { return Level2VolumeTable(h); } else if (Capacity >= 2000 && Capacity <= 2220) { return Level2VolumeCevakSmall(h); } else if (Capacity >= 10000 && Capacity <= 10850) { return Level2VolumeCevakLarge(h); } else { return 0; } } /// /// Converts water level to volume using calibration table. /// /// Height in mm /// Volume in l double Level2VolumeTable(double h) { if (h <= calibHeight[0]) return calibVolume[0]; for (int i = 1; i < calibHeight.Length; i++) { if (h <= calibHeight[i]) { double h1 = calibHeight[i - 1]; double h2 = calibHeight[i]; double v1 = calibVolume[i - 1]; double v2 = calibVolume[i]; return v1 + (v2 - v1) * (h - h1) / (h2 - h1); } } return calibVolume[calibHeight.Length - 1]; } /// /// Converts water level to volume for 2000 liter water tank in CEVAK Ceske Budejovice. /// /// Height in mm /// Volume in l double Level2VolumeCevakSmall(double h) { if (h < 150) { return 0; } else if (h < 1265.0) { return -302.70018 + 2.0099983 * h - 0.000034838627 * h * h + 1.8667691E-8 * h * h * h; } else { return 2220; } } /// /// Converts water level to volume for 10000 liter water tank in CEVAK Ceske Budejovice. /// /// Height in mm /// Volume in l double Level2VolumeCevakLarge(double h) { double h1 = 150; double h2 = 890; double h3 = 1075; double h4 = 1320; double h5 = 1505; if (h <= h1) { return 0; } else if (/* h > h1 && */ h < h2) { double v1 = Level2VolumeCevakLarge(h1); double v2 = Level2VolumeCevakLarge(h2); return v1 + (v2 - v1) * (h - h1) / (h2 - h1); } else if (/* h >= h2 && */ h <= h3) { return -818.19344 + 5.8227917 * h + 0.00010646841 * h * h; } else if (/* h > h3 && */ h < h4) { double v3 = Level2VolumeCevakLarge(h3); double v4 = Level2VolumeCevakLarge(h4); return v3 + (v4 - v3) * (h - h3) / (h4 - h3); } else if (/* h >= h4 && */ h <= h5) { return 1002.1633 + 4.7686186 * h + 0.0011826136 * h * h; } else { return Level2VolumeCevakLarge(h5); } } /// Reference to the operation public IOperation ReadVolumeOp(ref DoubleBox volume) { if (currentOp != CurrentOp.None) throw new Exception("Sequence error"); this.volumeBox = volume; readLevelOp = levelMeter.ReadLevelOp(ref levelBox); currentOp = CurrentOp.ReadVolume; return this; } /// Reference to the operation public IOperation ReadStableVolumeOp(ref DoubleBox volume, double sdev) { if (currentOp != CurrentOp.None) throw new Exception("Sequence error"); this.volumeBox = volume; this.sdev = sdev; readLevelOp = levelMeter.ReadStableLevelOp(ref levelBox, sdev); currentOp = CurrentOp.ReadStableVolume; return this; } /// Reference to the operation public IOperation ManualReadVolumeOp(ref DoubleBox volume) { if (currentOp != CurrentOp.None) throw new Exception("Sequence error"); this.volumeBox = volume; readLevelOp = manualLevelMeter.ReadLevelOp(ref levelBox); currentOp = CurrentOp.ManualReadVolume; return this; } public void Start() { readLevelOp.Start(); } public Event Run() { if (readLevelOp.Run() == Event.LevelDone) { volumeBox.Val = Level2Volume(levelBox.Val); return Event.VolumeDone; } else { return Event.Busy; } } public void Stop() { currentOp = CurrentOp.None; readLevelOp.Stop(); } } }