361 lines
9.4 KiB
C#
361 lines
9.4 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.Rig.Generic;
|
|
using TBF.Rig.GenericDevices;
|
|
|
|
namespace TBF.Rig.Various.TankWithLevelMsrmnt
|
|
{
|
|
public class Tank : TBF.Rig.MettlerToledo.TankDraining, 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("{0}({1})", ClassName, 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.Rig.GenericDevices.ILevelMeter levelMeter;
|
|
TBF.Rig.GenericDevices.ILevelMeter manualLevelMeter;
|
|
DoubleBox levelBox;
|
|
IOperation readLevelOp;
|
|
|
|
|
|
/// <summary>
|
|
/// Enumeration of balances via static fields and methods
|
|
/// </summary>
|
|
static int 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;
|
|
}
|
|
|
|
|
|
///
|
|
/// IDevice interface (required to call base and initialize 'DrainValve')
|
|
///
|
|
public override void Initialize()
|
|
{
|
|
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;
|
|
}
|
|
|
|
base.Initialize();
|
|
|
|
currentOp = CurrentOp.None;
|
|
levelBox = new DoubleBox();
|
|
|
|
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);
|
|
|
|
log.FatalFormat("{0} initialized: {1}", Name, this);
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|