602 lines
19 KiB
C#
602 lines
19 KiB
C#
///
|
|
/// Copyright (c) 2015 Sensus Metering Systems
|
|
/// Author: Milan Hanajík
|
|
///
|
|
using System;
|
|
using System.Collections.Generic;
|
|
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));
|
|
protected ILog optoDataLogger; /// Instance logger for opto-port data
|
|
public override string ToString() { return string.Format("iPerl({0})", Cfg.ToString(1)); }
|
|
|
|
|
|
readonly WaterMeterCfg iPerlCfg;
|
|
|
|
public int RfidComPortNr { get { return iPerlCfg.RfidComPortNr; } }
|
|
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; } }
|
|
|
|
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("CalculateNewCalibFactor: 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"));
|
|
return newFactor;
|
|
}
|
|
else
|
|
{
|
|
log.ErrorFormat("CalculateNewCalibFactor: orig={0} new={0} (unchanged!) Vdiff={1}",
|
|
originalCalibrationFactor, volumeMeter.ToString("F3"));
|
|
return originalCalibrationFactor; /// Too small volume in the denominator -> no correction at all
|
|
}
|
|
}
|
|
|
|
|
|
int sampleNr;
|
|
|
|
///
|
|
/// 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
|
|
|
|
///
|
|
/// 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;
|
|
|
|
private Boxes.IntBox pulses;
|
|
private Boxes.IntBox refPulses;
|
|
private Boxes.DoubleBox timeSec;
|
|
|
|
|
|
///
|
|
/// Opto serial port and worker thread related private variables
|
|
///
|
|
private SerialPort optoSerialPort;
|
|
|
|
|
|
public WaterMeter()
|
|
{
|
|
synchronized = false;
|
|
synchronized2 = false;
|
|
partOfTelegram = string.Empty;
|
|
}
|
|
|
|
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");
|
|
|
|
/// 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()
|
|
{
|
|
ReadOptoSerialPort();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>Start this operation</summary>
|
|
public void Start()
|
|
{
|
|
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 == 3)
|
|
{
|
|
VolumeLtrStart = volumeLtr;
|
|
TimestampSecStart = timestampSec;
|
|
}
|
|
|
|
/// Shift data in pipelines
|
|
VolumeLtrEnd = volumeLtrEnd2;
|
|
volumeLtrEnd2 = volumeLtrEnd1;
|
|
volumeLtrEnd1 = volumeLtr;
|
|
|
|
TimestampSecEnd = timestampSecEnd2;
|
|
timestampSecEnd2 = timestampSecEnd1;
|
|
timestampSecEnd1 = timestampSec;
|
|
|
|
return Event.ReadRegisterDone;
|
|
}
|
|
|
|
/// <summary>Stop this operation</summary>
|
|
public void Stop()
|
|
{
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
/// <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()
|
|
{
|
|
if (!optoSerialPortParsingEnabled) return;
|
|
|
|
if (DebugLevel == Entities.DebugMode.Normal || DebugLevel == Entities.DebugMode.Record)
|
|
{
|
|
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;
|
|
|
|
OptoTelegramRaw opto;
|
|
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)
|
|
{
|
|
optoDataLogger.ErrorFormat("{0}: Opto data syncronisation error", Name);
|
|
}
|
|
synchronized = true;
|
|
}
|
|
// CR+LF found and (pos >= OptoTelegram.Length - 2)
|
|
else if (null != (opto = OptoTelegramRaw.FromString(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2))))
|
|
{
|
|
OptoTelegramRreceived(opto, synchronized2);
|
|
synchronized2 = synchronized;
|
|
allRcvd = allRcvd.Substring(pos + 2);
|
|
}
|
|
else
|
|
{
|
|
optoDataLogger.ErrorFormat("{0}: Wrong format of opto data", Name);
|
|
allRcvd = allRcvd.Substring(pos + 2);
|
|
}
|
|
}
|
|
|
|
//OnOptoReceived(this, new OptoReceivedEventArgs(s));
|
|
}
|
|
else
|
|
{
|
|
//OnOptoReceived(this, new OptoReceivedEventArgs("."));
|
|
}
|
|
}
|
|
else if (DebugLevel == Entities.DebugMode.Simulate)
|
|
{
|
|
}
|
|
else if (DebugLevel == Entities.DebugMode.Reply)
|
|
{
|
|
}
|
|
}
|
|
|
|
|
|
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
|
|
{
|
|
optoDataLogger.Info("Fatal error");
|
|
}
|
|
}
|
|
|
|
|
|
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
|
|
{
|
|
optoDataLogger.Info("Fatal error2");
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|