tbf/TBF/BenchControl/Various/TankWithLevelMsrmnt/Tank.cs

371 lines
9.6 KiB
C#

///
/// 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
{
/// <summary>
/// Info: StartMassMeasurement() and "Balnce response = .., Mass = ..."
/// Warn: Start measurement when busy is true
/// </summary>
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;
/// <summary>
/// Enumeration of balances via static fields and methods
/// </summary>
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()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="cfg">Balance properties</param>
/// <param components="cfg">A list of components loaded so far</param>
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);
}
/// <summary>
/// Loads a calibration table in CSV format (column1 = height[mm], column2 = volume[l]).
/// </summary>
/// <param name="pathName">Path name of the CSV file</param>
/// <returns>true when loaded successfully</returns>
bool LoadCalibrationTable(string pathName)
{
try
{
IList<double> heights = new List<double>();
IList<double> volumes = new List<double>();
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;
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="h">Height in mm</param>
/// <returns>Volume in l</returns>
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;
}
}
/// <summary>
/// Converts water level to volume using calibration table.
/// </summary>
/// <param name="h">Height in mm</param>
/// <returns>Volume in l</returns>
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];
}
/// <summary>
/// Converts water level to volume for 2000 liter water tank in CEVAK Ceske Budejovice.
/// </summary>
/// <param name="h">Height in mm</param>
/// <returns>Volume in l</returns>
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;
}
}
/// <summary>
/// Converts water level to volume for 10000 liter water tank in CEVAK Ceske Budejovice.
/// </summary>
/// <param name="h">Height in mm</param>
/// <returns>Volume in l</returns>
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);
}
}
/// <returns>Reference to the operation</returns>
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;
}
/// <returns>Reference to the operation</returns>
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;
}
/// <returns>Reference to the operation</returns>
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();
}
}
}