tbf/TestBenchFramework/BenchControl/WaterMeters/iPerl/WaterMeter.cs

724 lines
24 KiB
C#

///
/// Copyright (c) 2015 Sensus Metering Systems
/// Author: Milan Hanajík
///
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Threading;
using log4net;
using TBF.BenchControl;
using TBF.BenchControl.Generic;
namespace TBF.BenchControl.WaterMeters.iPerl
{
/// <summary>
/// This component = instance of this class is a placeholder for a combined main watermeter
/// </summary>
public class WaterMeter : ComponentBase, IDevice, GenericDevices.IWaterMeter, GenericDevices.IRegisterReader, IOperation
{
private static readonly ILog log = LogManager.GetLogger(typeof(WaterMeter));
public override string ToString() { return string.Format("iPerl({0})", Cfg.ToString(1)); }
readonly WaterMeterCfg iPerlCfg;
public int MuxBoardNr { get { return iPerlCfg.MuxBoardNr; } }
public int Group { get { return iPerlCfg.Group; } }
public float PulsesPerLtr { get { return iPerlCfg.ProcParams.PulsesPerLtr; } }
/// <summary>WaterMeter producer</summary>
public string Producer { get { return iPerlCfg.ProcParams.Producer; } }
/// <summary>Nominal water flow in [m3/h]</summary>
public float Qn { get { return iPerlCfg.ProcParams.Qn; } }
/// <summary>WaterMeter approval information or signature</summary>
public string ApprovalInfo { get { return iPerlCfg.ProcParams.ApprovalInfo; } }
/// <summary>Metrological class</summary>
public string MetrologicalClass { get { return iPerlCfg.ProcParams.MetrologicalClass; } }
/// Properties set by the Begin and the End form
public string SerialNr
{
get
{
if (ConfigStruct != null) return configStruct.PCBNumber2String();
else return string.Empty;
}
set { serialNr = value; }
}
string serialNr;
public string EndState
{
get { return endState; }
set { endState = value; }
}
string endState;
public string BeginState
{
get { return beginState; }
set { beginState = value; }
}
string beginState;
public bool Disabled
{
get { return disabled; }
set { disabled = value; }
}
bool disabled;
/// <summary> ConfigStruct of the water meter obtained or updated by iPerlCommunication </summary>
public ConfigStruct ConfigStruct
{
get { return configStruct; }
set { configStruct = value; }
}
ConfigStruct configStruct;
/// <summary> CalibrationStruct of the water meter obtained or updated by iPerlCommunication </summary>
public CalibrationStruct CalibrationStruct
{
get { return calibrationStruct; }
set { calibrationStruct = value; }
}
CalibrationStruct calibrationStruct;
public ushort CalibrationFactor { get { return CalibrationStruct.Calibration; } }
/// <summary> Result of the last test used to calculate Q2 correction factors, etc </summary>
public Entities.MeterTestResult LastTestResult;
public double NominalTestFlow; /// liter per hour
/// <summary>
/// New calibration factor calculated from the original factor (argument)
/// and results of the last test.
/// </summary>
/// <param name="originalCalibrationFactor">Original calibration factor</param>
/// <returns>New calibration factor</returns>
public UInt16 CalculateNewCalibFactor(UInt16 originalCalibrationFactor)
{
double volumeMeter = Math.Abs(VolumeLtrEnd - VolumeLtrStart);
if (volumeMeter > 1E-2)
{
UInt16 newFactor = (UInt16)((double)originalCalibrationFactor * VolumeLtrRef / volumeMeter + 0.5);
log.InfoFormat("Calibration factor: orig={0} new={1} V_1={2} V_2={3} Vdiff={4} Vref={5}",
originalCalibrationFactor, newFactor,
VolumeLtrStart.ToString("F3"), VolumeLtrEnd.ToString("F3"), volumeMeter.ToString("F3"),
VolumeLtrRef.ToString("F3"));
if (newFactor < iPerlCfg.CalibrationMin) newFactor = iPerlCfg.CalibrationMin;
if (newFactor > iPerlCfg.CalibrationMax) newFactor = iPerlCfg.CalibrationMax;
return newFactor;
}
else
{
log.ErrorFormat("Calibration factor: orig={0} new={0} (unchanged!) Vdiff={1}",
originalCalibrationFactor, volumeMeter.ToString("F3"));
return originalCalibrationFactor; /// Too small volume in the denominator -> no correction at all
}
}
/// <summary>
/// Q2 correction factor calculated from the last test (Q2).
/// This factors should be used only for R800 meters.
/// </summary>
/// <returns>Q2 correction factor</returns>
public double Q2CorrectionFactor()
{
if (Math.Abs(LastTestResult.VolumeErrorPct) <= 0.5f) return 0; /// No Q2 correction if error < +/-0.5 %
double A = 16.0 / ScalingFactor(); /// Raw units per ml: DN15=16, DN20=8, DN25=4, DN32=2, DN40=1
const double B = 8.0; /// Raw units per minute, 8
const double C = B * 60.0; /// Raw units per hour, 480
double D = C / A; /// ml correction per hour
double F = D / (NominalTestFlow * 10.0); /// Error corrected with 8 Raw Units per minute [%]
double G = F / B; /// Error corrected with 1 Raw Unit per minute [%]
float errLimitLo = LastTestResult.TestResult.ErrLimLo;
float errLimitHi = LastTestResult.TestResult.ErrLimHi;
float volumeMeterErrLimLo = LastTestResult.VolumeRef * (100.0f + errLimitLo) / 100.0f;
float volumeMeterErrLimHi = LastTestResult.VolumeRef * (100.0f + errLimitHi) / 100.0f;
double corrFactorHi = (-1) * (errLimitLo / G) * (LastTestResult.VolumeRef / volumeMeterErrLimLo); /// > 0
double corrFactorLo = (-1) * (errLimitHi / G) * (LastTestResult.VolumeRef / volumeMeterErrLimHi); /// < 0
double corrFactor = (-1) * (LastTestResult.VolumeErrorPct / G) * (LastTestResult.VolumeRef / LastTestResult.VolumeMeter);
double origCalulatedCorrFactor = corrFactor;
if (corrFactor < corrFactorLo) corrFactor = corrFactorLo;
if (corrFactor > corrFactorHi) corrFactor = corrFactorHi;
log.WarnFormat("Q2 correction: WM={0}, LoLim={1}, HiLim={2}, calculated={3}, corrFactor={4}",
Name, corrFactorLo, corrFactorHi, origCalulatedCorrFactor, corrFactor);
return corrFactor;
}
/// <summary> Name set by the test, to be used as a part og the opto-data log file name </summary>
public string TestName;
/// <summary> Name set by the test, to be used as a part og the opto-data log file name </summary>
public string BenchName;
///
/// Volume of water from the opto telegram
///
bool lastVolumeRawValid; /// true = valid
private Int32 lastVolumeRaw; /// Last read raw volume
private double volumeLtr; ///
private double volumeLtr0;
public double VolumeLtrStart; /// Test start volume for metrology
public double VolumeLtrEnd; /// Test end volume for metrology
public double VolumeLtrRef; /// Reference volume or metrology
double volumeLtrEnd1; /// auxiliary buffer1 to keep the end volume before test stops
double volumeLtrEnd2; /// auxiliary buffer2 to keep the end volume before test stops
double volumeLtrEnd3; /// auxiliary buffer3 to keep the end volume before test stops
///
/// Timestamp from the opto telegram
///
bool lastTimestampValid;
private Int64 lastTimestamp;
private double timestampSec;
private double timestampSec0;
public double TimestampSecStart;
public double TimestampSecEnd;
double timestampSecEnd1;
double timestampSecEnd2;
double timestampSecEnd3;
private Boxes.IntBox pulses;
private Boxes.IntBox refPulses;
private Boxes.DoubleBox timeSec;
OptoTelegramRaw[] optoData;
const int MaxOptoDataCount = 40000;
int optoDataCount;
string optoDataLogFileName;
///
/// Opto serial port and worker thread related private variables
///
private SerialPort optoSerialPort;
public WaterMeter()
{
synchronized = false;
synchronized2 = false;
partOfTelegram = string.Empty;
flushBuffer = new char[8192];
}
public WaterMeter(WaterMeterCfg cfg)
: base(cfg)
{
iPerlCfg = cfg;
log.Debug(this.ToString());
}
int NrFormName(string name)
{
int len = name.Length;
if (len < 2) return 0;
int loNr = (int)name[len - 1] - (int)'0';
int hiNr = (int)name[len - 2] - (int)'0';
if (hiNr < 1 || hiNr > 4) hiNr = 0;
return 10 * hiNr + loNr;
}
public void Initialize()
{
if (DebugLevel == Entities.DebugMode.Normal)
{
//int nr = NrFormName(Name);
//optoDataLogger = LogManager.GetLogger("OptoData_" + nr.ToString());
//optoDataLogger.Fatal("------------------------------------------------------------------------");
//optoDataLogger.Fatal("Program restarted");
/// Allocate memory for opto-data from iPerl
optoData = new OptoTelegramRaw[MaxOptoDataCount];
for (int i = 0; i < MaxOptoDataCount; i++) optoData[i] = new OptoTelegramRaw();
/// Prepare serial port
optoSerialPort = new SerialPort(string.Format("COM{0}", iPerlCfg.OptoComPortNr),
9600, Parity.None, 8, StopBits.One);
optoSerialPort.Handshake = Handshake.None;
OpenSerialPort();
}
}
public void RunDeviceBefore()
{
if (DebugLevel == Entities.DebugMode.Normal)
{
try
{
if (optoSerialPortParsingEnabled)
ReadOptoSerialPort();
else
FlushOptoSerialPort();
}
catch (Exception e)
{
DebugLevel = Entities.DebugMode.FailureDuringOperation;
log.FatalFormat("Redrawing results failed : {0}", e.Message);
if (e.InnerException != null)
{
log.FatalFormat("InnerMessage : {0}", e.InnerException.Message);
}
log.FatalFormat("StackTrace : {0}{1}", Environment.NewLine, e.StackTrace);
}
}
else if (DebugLevel == Entities.DebugMode.FailureDuringOperation)
{
}
}
public void RunDeviceAfter()
{
}
public void StopDevice()
{
if (DebugLevel == Entities.DebugMode.Normal)
{
CloseSerialPort();
}
}
/// <summary>
/// Events: Event.ReadRegisterDone, Event.Error
/// </summary>
/// <param name="pulses">Reference to a variable for the water meter pulses</param>
/// <returns>ReadWaterMeter instance reference casted to IOperaton</returns>
public IOperation ReadRegisterOp(ref Boxes.IntBox pulses)
{
this.pulses = pulses;
return this;
}
/// <summary>
/// Events: Event.ReadRegisterDone, Event.Error
/// </summary>
/// <param name="pulses">Reference to a variable for the water meter pulses</param>
/// <param name="refPulses">Reference to a variable for the related reference flowmeter pulses</param>
/// <returns>Reference to operation object instance</returns>
public IOperation ReadRegisterOp(ref Boxes.IntBox pulses, ref Boxes.IntBox refPulses)
{
this.pulses = pulses;
this.refPulses = refPulses;
return this;
}
/// <summary>
/// Events: Event.ReadRegisterDone, Event.Error
/// </summary>
/// <param name="pulses">Reference to a variable for the water meter pulses</param>
/// <param name="timeSec">Reference to a variable for the related time in seconds</param>
/// <returns>Reference to operation object instance</returns>
public IOperation ReadRegisterOp(ref Boxes.IntBox pulses, ref Boxes.DoubleBox timeSec)
{
this.pulses = pulses;
this.timeSec = timeSec;
return this;
}
/// <summary>
/// Events: ReadReferenceDone, Error
/// </summary>
/// <param name="pulses">Reference to a variable for the water meter pulses</param>
/// <param name="timeSec">Reference to a variable for the related time in seconds</param>
/// <param name="refPulses">Reference to a variable for the water meter pulses</param>
/// <returns>Reference to operation object instance</returns>
public IOperation ReadRegisterOp(ref Boxes.IntBox pulses, ref Boxes.DoubleBox timeSec, ref Boxes.IntBox refPulses)
{
this.pulses = pulses;
this.timeSec = timeSec;
this.refPulses = refPulses;
return this;
}
/// <summary>
/// Events: ReadReferenceDone, Error
/// </summary>
/// <param name="pulses">Reference to a variable for the water meter pulses</param>
/// <param name="refPulses">Reference to a variable for the water meter pulses</param>
/// <returns>Reference to operation object instance</returns>
public IOperation ReadReferenceForRegisterOp(ref Boxes.IntBox pulses, ref Boxes.IntBox refPulses)
{
this.pulses = pulses;
this.refPulses = refPulses;
return this;
}
int sampleNr; /// This is to determine when the test start sample should be taken
/// <summary>Start this operation</summary>
public void Start()
{
/// Reset opto data
optoDataCount = 0;
optoDataLogFileName = string.Format("{0}_{1}_{2}_{3}_{4}",
(ConfigStruct != null) ? ConfigStruct.PCBNumber2String() : "UnknownPcbNr",
Utils.ToMyString(DateTime.Now),
BenchName,
Name,
(TestName != null ? TestName : string.Empty));
sampleNr = 0;
volumeLtr = 0;
volumeLtr0 = 0;
timestampSec = 0;
timestampSec0 = 0;
CloseSerialPort();
OpenSerialPort();
ReadPulses();
}
/// <summary>Run this operation</summary>
/// <returns>eventDone</returns>
public Event Run()
{
sampleNr++;
ReadPulses();
if (sampleNr == 4)
{
/// Take the test start sample
VolumeLtrStart = volumeLtr;
TimestampSecStart = timestampSec;
}
/// Shift data in pipelines
VolumeLtrEnd = volumeLtrEnd3;
//volumeLtrEnd6 = volumeLtrEnd5;
//volumeLtrEnd5 = volumeLtrEnd4;
//volumeLtrEnd4 = volumeLtrEnd3;
volumeLtrEnd3 = volumeLtrEnd2;
volumeLtrEnd2 = volumeLtrEnd1;
volumeLtrEnd1 = volumeLtr;
TimestampSecEnd = timestampSecEnd3;
//timestampSecEnd6 = timestampSecEnd5;
//timestampSecEnd5 = timestampSecEnd4;
//timestampSecEnd4 = timestampSecEnd3;
timestampSecEnd3 = timestampSecEnd2;
timestampSecEnd2 = timestampSecEnd1;
timestampSecEnd1 = timestampSec;
return Event.ReadRegisterDone;
}
/// <summary>Stop this operation</summary>
public void Stop()
{
SaveOptoData();
}
void SaveOptoData()
{
string directory = "C:\\TBF\\ProcessData\\"; /// TODO: determine correct relative path
Directory.CreateDirectory(directory);
TextWriter optoLogFile = new StreamWriter(directory + optoDataLogFileName + ".txt");
for (int i = 0; i < optoDataCount; i++) optoLogFile.WriteLine(optoData[i].ToString());
optoLogFile.Close();
}
void ReadPulses()
{
pulses.Val = (int)((volumeLtr - volumeLtr0) * (double)PulsesPerLtr + 0.5);
if (timeSec != null) timeSec.Val = timestampSec - timestampSec0;
if (refPulses != null) refPulses.Val = StateMachine.ControlBoard.EtPulses(0);
}
bool optoSerialPortParsingEnabled;
bool synchronized;
bool synchronized2;
string partOfTelegram;
/// <summary> Open RFID serial port and start parsing serial data </summary>
private void OpenSerialPort()
{
if (optoSerialPort != null)
{
optoSerialPort.Open();
StartParsingOptoSerialPort();
}
}
/// <summary> Stop parsing serial data and close RFID serial port </summary>
private void CloseSerialPort()
{
if (optoSerialPort != null)
{
StopParsingOptoSerialPort();
optoSerialPort.Close();
}
}
/// <summary> Flush internal buffers and start parsing the opto serial port data </summary>
void StartParsingOptoSerialPort()
{
synchronized = false;
synchronized2 = false;
partOfTelegram = string.Empty;
optoSerialPortParsingEnabled = true;
}
/// <summary> Stop parsig the opto serial port data </summary>
void StopParsingOptoSerialPort()
{
optoSerialPortParsingEnabled = false;
}
char[] flushBuffer;
void FlushOptoSerialPort()
{
int nrBytes = optoSerialPort.BytesToRead;
if (nrBytes > 0) optoSerialPort.Read(flushBuffer, 0, nrBytes);
}
/// <summary>
/// 9600 Bd, 8 data bits, 1 stop bit, no parity
///
/// Telegram description:
///
/// AAAAAA[tab]BBBB[tab]CCCC[tab]DDDDDD[tab]EEEE[tab]FFFFFFFF[tab]GG[cr][lf] (42 bytes)
///
/// Example:
/// FFFFFE 51EA 0000 65324E 0087 F6319DFF 86
/// FFDD3A 51F9 0000 65324E 0088 F631A60B 45
/// ...
/// </summary>
void ReadOptoSerialPort()
{
int nrBytes = optoSerialPort.BytesToRead;
if (nrBytes > 0)
{
char[] buffer = new char[nrBytes];
optoSerialPort.Read(buffer, 0, nrBytes);
string received = new string(buffer);
string allRcvd = partOfTelegram + received;
while (true)
{
int pos = allRcvd.IndexOf("\r\n");
if (pos < 0)
{
/// No CR+LF found, wait for more characters in the next invocation
partOfTelegram = allRcvd;
return;
}
// CR+LF found
else if (pos < OptoTelegram.Length - 2)
{
/// CR+LF found too early, truncate the beginning incl CR+LF and keep scanning in this loop
allRcvd = allRcvd.Substring(pos + 2);
if (synchronized)
{
optoData[optoDataCount++].SetFlags(OptoTelegramFlags.SyncError);
}
synchronized = true;
}
// CR+LF found and (pos >= OptoTelegram.Length - 2)
else if (optoData[optoDataCount].UpdateFromString(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2)))
{
OptoTelegramRreceived(optoData[optoDataCount++], synchronized2);
synchronized2 = synchronized;
allRcvd = allRcvd.Substring(pos + 2);
}
else
{
optoData[optoDataCount++].SetFlags(OptoTelegramFlags.InvalidTelegram);
allRcvd = allRcvd.Substring(pos + 2);
}
}
//OnOptoReceived(this, new OptoReceivedEventArgs(s));
}
else
{
//OnOptoReceived(this, new OptoReceivedEventArgs("."));
}
}
void OptoTelegramRreceived(OptoTelegramRaw optoTelegram, bool async)
{
Int32 uncorrectedRawVolume = 0;
if (!lastVolumeRawValid)
{
lastVolumeRaw = optoTelegram.VolumeRaw;
lastVolumeRawValid = true;
}
else
{
uncorrectedRawVolume = (Int32)((lastVolumeRaw & 0x7F000000) | (optoTelegram.VolumeRaw & 0x00FFFFFF));
if (Math.Abs(uncorrectedRawVolume - lastVolumeRaw) <= 0x007FFFFF)
{
lastVolumeRaw = uncorrectedRawVolume;
}
else if (Math.Abs(uncorrectedRawVolume + 0x01000000 - lastVolumeRaw) <= 0x007FFFFF)
{
lastVolumeRaw = uncorrectedRawVolume + 0x01000000;
}
else if (Math.Abs(uncorrectedRawVolume - 0x01000000 - lastVolumeRaw) <= 0x007FFFFF)
{
lastVolumeRaw = uncorrectedRawVolume - 0x01000000;
}
else
{
lastVolumeRaw = uncorrectedRawVolume; /// This should never happen
}
}
Int64 uncorrectedTimestamp = 0;
if (!lastTimestampValid)
{
lastTimestamp = optoTelegram.Timestamp;
lastTimestampValid = true;
}
else
{
uncorrectedTimestamp = (Int64)((lastTimestamp & 0x7FFFFFFF00000000) | (optoTelegram.Timestamp & 0xFFFFFFFF));
if (Math.Abs(uncorrectedTimestamp - lastTimestamp) <= 0x7FFFFFFF)
{
lastTimestamp = uncorrectedTimestamp;
}
else if (Math.Abs(uncorrectedTimestamp + 0x100000000 - lastTimestamp) <= 0x7FFFFFFF)
{
lastTimestamp = uncorrectedTimestamp + 0x100000000;
}
else if (Math.Abs(uncorrectedTimestamp - 0x100000000 - lastTimestamp) <= 0x7FFFFFFF)
{
lastTimestamp = uncorrectedTimestamp - 0x100000000;
}
else
{
lastTimestamp = uncorrectedTimestamp; /// This should never happen
}
}
if (volumeLtr == 0 && volumeLtr0 == 0)
{
volumeLtr = (double)lastVolumeRaw * ScalingFactor() / 16000.0;
volumeLtr0 = volumeLtr;
}
else
{
volumeLtr = (double)lastVolumeRaw * ScalingFactor() / 16000.0;
}
if (timestampSec == 0 && timestampSec0 == 0)
{
timestampSec = (double)lastTimestamp / 8192.0;
timestampSec0 = timestampSec;
}
else
{
timestampSec = (double)lastTimestamp / 8192.0;
}
//optoDataLogger.InfoFormat("{0} {1} {2} ltr {3} {4}", optoTelegram, lastTimestamp, lastVolumeRaw.ToString("X8"), timestampSec.ToString("F1"), volumeLtr.ToString("F3"));
}
/// <summary>
/// Called from the state machine when a test is selected and UI needs to be updated.
/// </summary>
public void OnOptoReceived(object sender, OptoReceivedEventArgs args)
{
if (OptoReceivedHandler == null) return;
try { OptoReceivedHandler(sender, args); }
catch (Exception) { }
}
public event EventHandler<OptoReceivedEventArgs> OptoReceivedHandler;
public static double UnitVolume(VolumeUnits units)
{
switch (units)
{
default:
case VolumeUnits.m3: return 1000.0; /// liter
case VolumeUnits.UK_gallon: return 4.546092; /// liter
case VolumeUnits.US_gallon: return 3.785412; // liter
}
}
public double ScalingFactor()
{
if ((iPerlCfg.MeterType == MeterType.AutoDetect) && (CalibrationStruct != null))
{
return WaterMeter.ScalingFactor(CalibrationStruct.MeterType);
}
else if (iPerlCfg.MeterType != MeterType.AutoDetect)
{
return WaterMeter.ScalingFactor(iPerlCfg.MeterType);
}
else
{
return WaterMeter.ScalingFactor(MeterType.DN20);
}
}
/// <summary>
/// Scaling factor:
/// 0, 1 (DN15, Coax) . . . . 1
/// 2 (DN20) . . . . . . . . 2
/// 3 (DN25) . . . . . . . . 4
/// 4, 5 (DN26, DN32) . . . . 8
/// 6 (DN40) . . . . . . . . 16
/// </summary>
/// <param name="meterType">MeterType (0..6)</param>
/// <returns>Scaling factor</returns>
public static double ScalingFactor(MeterType meterType)
{
switch (meterType)
{
default:
case MeterType.DN15:
case MeterType.CoaxManifold: return 1.0;
case MeterType.DN20: return 2.0;
case MeterType.DN25: return 4.0;
case MeterType.DN26:
case MeterType.DN32: return 8.0;
case MeterType.DN40: return 16.0;
}
}
}
}