/// /// Copyright (c) 2015-2017 Sensus Metering Systems /// using System; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Threading; using System.Windows.Forms; using log4net; using Config.Entities; using TBF.BenchControl; using TBF.BenchControl.Generic; using TBF.Resources; namespace TBF.BenchControl.TestMethods.iPerlCommunication.iPerlHead { /// /// This component = instance of this class is a placeholder for a combined main watermeter /// public class IperlHead : ComponentBase, IDevice, GenericDevices.IRegisterReader, IOperation { private static readonly ILog log = LogManager.GetLogger(typeof(IperlHead)); public override string ToString() { return string.Format("iPerl({0})", Cfg.ToString(1)); } readonly IperlHeadCfg iperlHeadCfg; public int RfidComPortNr { get { return iperlHeadCfg.RfidComPortNr; } } public int MuxBoardNrOrGroup14 { get { return iperlHeadCfg.MuxBoardNr; } } public int Group { get { return iperlHeadCfg.Group; } } public double PulsesPerLtr { get { return 1000.0; } } public double LtrsPerPulse { get { return 1 / PulsesPerLtr; } } #if ORACLE_DB public int WMType_ID { get { return iperlHeadCfg.ProcParams.WMType_ID; } } /// Required by Oracle DB public int WMType_Rev { get { return iperlHeadCfg.ProcParams.WMType_Rev; } } /// Required by Oracle DB #endif public ushort FactorLimitLo { get { return (ushort)iperlHeadCfg.ProcParams.FactorLimitLo; } } public ushort FactorLimitHi { get { return (ushort)iperlHeadCfg.ProcParams.FactorLimitHi; } } /// Properties set by the Begin and the End form public string SerialNr { get { if (ConfigStruct != null) return configStruct.PCBNumber2String(); else return string.Empty; } set { } } 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; public bool CommFailed { get { return commFailed; } set { commFailed = value; } } bool commFailed; public int ResultCode { get { return resultCode; } } int resultCode; /// /// Passed to OptoTelegramRaw.UpdateFromString(...) /// Int64 volumeRawExtLast; Int64 timestampExtLast; public bool PositiveCounting; /// ConfigStruct of the water meter obtained or updated by iPerlCommunication public ConfigStruct ConfigStruct { get { return configStruct; } set { configStruct = value; } } ConfigStruct configStruct; /// CalibrationStruct of the water meter obtained or updated by iPerlCommunication public CalibrationStruct CalibrationStruct { get { return calibrationStruct; } set { calibrationStruct = value; } } CalibrationStruct calibrationStruct; public ushort OrigCalibFactor; public ushort CalibFactor { get { return (CalibrationStruct != null) ? CalibrationStruct.Calibration : (ushort)0; } } public double Q2ErrWOCorrection; public bool Q2CorrectionDone; public double Q2Correction; public int Q2CorrRFlow; public int Q2CorrLFlow; public double Diff2Hz8Hz; public bool Hz2CorrectionDone; public int Hz2Correction; public string FWVersion { get { return (CalibrationStruct != null) ? CalibrationStruct.FWVersionStr() : string.Empty; } } /// Result of the last test used to calculate Q2 correction factors, etc public Results.Entities.MeterTestRslt LastTestResult2; public Results.Entities.MeterTestRslt LastTestResult; public double NominalTestFlowLph; /// in liter per hour /// /// Required for IRegisterReader interface /// public int WMPulses { get { return wmPulses; } } public int WMRefPulses { get { return wmRefPulses; } } public double WMVolume { get { return wmVolume; } } public double BeginWMState { get { return beginWMState; } } public double EndWMState { get { return endWMState; } } public double WMTestTime { get { return wmTestTime; } } double beginWMState; double endWMState; double wmVolume; int wmPulses; int wmRefPulses; double wmTestTime; /// /// New calibration factor calculated from the original factor (argument) /// and results of any test(s). /// /// Test result for calculations /// Original calibration factor /// Lower limit for the calibration factor /// Upper limit for the calibration factor /// New calibration factor or 0 (= Out of range) public UInt16 CalculateNewCalibFactor(Results.Entities.MeterTestRslt adjustTestResult, UInt16 originalCalibrationFactor, UInt16 FactorLimitLo, UInt16 FactorLimitHi) { double meterVolume = adjustTestResult.VolumeMeter; double refVolume = adjustTestResult.VolumeRef; OrigCalibFactor = originalCalibrationFactor; if (meterVolume > 1E-2) { PositiveCounting = VolumeLtrEnd > VolumeLtrStart; UInt16 newFactor = (UInt16)((double)originalCalibrationFactor * refVolume / meterVolume + 0.5); log.InfoFormat("Calibration factor: orig={0} new={1} V_iperl={2} V_ref={3}", originalCalibrationFactor, newFactor, meterVolume.ToString("F3"), refVolume.ToString("F3")); if (newFactor < FactorLimitLo || newFactor > FactorLimitHi) return 0; return newFactor; } else { log.ErrorFormat("Calibration factor: orig={0} new={0} (unchanged!) V_iperl={1}", originalCalibrationFactor, meterVolume.ToString("F3")); return originalCalibrationFactor; /// Too small volume in the denominator -> no correction at all } } /// /// Q2 correction factor calculated from the last test (Q2). /// This factors should be used only for R800 meters. /// /// Test result from which to calculate the factor /// The calculated Q2 correction factor /// true = OK, false = failed public bool CalculateQ2CorrectionFactor(Results.Entities.MeterTestRslt q2adjTestResult, Results.Entities.TestData q2adjTestData, out double q2CorrectionFactor) { q2CorrectionFactor = 0; if (q2adjTestResult == null) return false; /// Q2 test result is missing ==> water meter failed double errLimitLo = q2adjTestData.ErrLimLo + q2adjTestData.Uncertainty; double errLimitHi = q2adjTestData.ErrLimHi - q2adjTestData.Uncertainty; if ((q2adjTestResult.Error < errLimitLo) || (q2adjTestResult.Error > errLimitHi)) { return false; /// Q2 error too large ==> water meter failed } 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 / (NominalTestFlowLph * 10.0); /// Error corrected with 8 Raw Units per minute [%] double G = F / B; /// Error corrected with 1 Raw Unit per minute [%] q2CorrectionFactor = (-1) * (q2adjTestResult.Error / G) * (q2adjTestResult.VolumeRef / q2adjTestResult.VolumeMeter); log.WarnFormat("Q2 correction: Pos={0}, PCB#={1}, corrFactor={2}, error={3}% [Lo={4}%, Hi={5}%]", Name, SerialNr, q2CorrectionFactor.ToString("F1"), q2adjTestResult.Error.ToString("F2"), errLimitLo.ToString("F1"), errLimitHi.ToString("F1")); return true; } /// /// 2 Hz correction factor calculated from two Q3 tests - done at 2Hz and at 8Hz. /// This factors should be used only for DN32 and DN40 meters. /// /// Test result @2Hz from which to calculate the factor /// Test result @8Hz from which to calculate the factor /// The calculated Q2 correction factor /// true = OK, false = failed public bool Calculate2HzCorrectionFactor(Results.Entities.MeterTestRslt resultAt2Hz, Results.Entities.MeterTestRslt resultAt8Hz, out double diff2Hz8Hz, out int hz2CorrectionFactor) { hz2CorrectionFactor = 0; diff2Hz8Hz = 0; if ((resultAt2Hz == null) || (resultAt8Hz == null)) { return false; /// Test result @2Hz and/or @8Hz is missing ==> water meter failed } diff2Hz8Hz = resultAt2Hz.Error - resultAt8Hz.Error; if (Math.Abs(diff2Hz8Hz) > 2.5) return false; /// Difference of errors > 2.5 % ==> water meter failed hz2CorrectionFactor = -1 * (int)Math.Round(10 * diff2Hz8Hz); log.WarnFormat("2Hz correction: Pos={0}, PCB#={1}, corrFactor={2}, erro@2Hz={3}%, erro@8Hz={4}%", Name, SerialNr, hz2CorrectionFactor, resultAt2Hz.Error.ToString("F2"), resultAt8Hz.Error.ToString("F2")); return true; } /// Name set by the test, to be used as a part of the opto-data log file name public string TestName; /// Name set by the test, to be used as a part of the opto-data log file name public string BenchName; /// /// Volume of water from the opto telegram /// private Int64 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 bool NoSamples { get { return (timestampSecEnd - timestampSecStart) < float.Epsilon; } } public double TimestampSecStart { get { return timestampSecStart; } } public double TimestampSecEnd { get { return timestampSecEnd; } } double timestampSecStart; double timestampSecEnd; double timestampSecEnd1; double timestampSecEnd2; double timestampSecEnd3; private int telegramIx; public int TestStartTelegramIx; public int TestEndTelegramIx; OptoTelegramRaw[] optoData; const int MaxOptoDataCount = 40000; int optoDataCount; string optoDataLogFileName; OptoTelegramRaw toBeFlushed; int flushedDataCount; public int FlushedDataCount { get { return flushedDataCount; } set { flushedDataCount = lastFlushedDataCount = lastFlushedDataCount_1 = value; } } int lastFlushedDataCount_1; int lastFlushedDataCount; public int FlushedDataDelta { get { int retval = Math.Max(flushedDataCount - lastFlushedDataCount, lastFlushedDataCount - lastFlushedDataCount_1); lastFlushedDataCount_1 = lastFlushedDataCount; lastFlushedDataCount = flushedDataCount; return retval; } } /// /// Opto serial port and worker thread related private variables /// private SerialPort optoSerialPort; public IperlHead() { } public IperlHead(Generic.IComponentCfg cfg) : base(cfg) { ClearData(); iperlHeadCfg = cfg as IperlHeadCfg; log.Debug(this.ToString()); } /// /// Clear data related to a specific water meter /// public void ClearData() { resultCode = 0; disabled = false; commFailed = false; endState = string.Empty; beginState = string.Empty; configStruct = null; calibrationStruct = null; LastTestResult = null; NominalTestFlowLph = 0; OrigCalibFactor = 0; Q2ErrWOCorrection = 0; Q2CorrectionDone = false; Q2CorrRFlow = 0; optoDataCount = 0; } public void Initialize() { ClearData(); if (DebugLevel == DebugMode.Normal) { optoSerialPortParsingEnabled = false; /// Allocate memory for opto-data from iPerl optoData = new OptoTelegramRaw[MaxOptoDataCount]; for (int i = 0; i < MaxOptoDataCount; i++) optoData[i] = new OptoTelegramRaw(); toBeFlushed = new OptoTelegramRaw(); flushedDataCount = 0; synchronized = false; synchronized2 = false; partOfTelegram = string.Empty; /// Prepare serial port optoSerialPort = new SerialPort(string.Format("COM{0}", iperlHeadCfg.OptoComPortNr), 9600, Parity.None, 8, StopBits.One); optoSerialPort.Handshake = Handshake.None; optoSerialPort.Open(); } } public void RunDeviceBefore() { if (DebugLevel == DebugMode.Normal) { try { if (optoSerialPortParsingEnabled) ReadOptoData(OptoState.Read); else ReadOptoData(OptoState.Flush); } catch (Exception e) { DebugLevel = DebugMode.FailureDuringOperation; log.FatalFormat("Opto-data serial port failure : {0}", e.Message); if (e.InnerException != null) { log.FatalFormat("InnerMessage : {0}", e.InnerException.Message); } } } else if (DebugLevel == DebugMode.FailureDuringOperation) { } } public void RunDeviceAfter() { } public void StopDevice() { try { if (DebugLevel == DebugMode.Normal && optoSerialPort != null) { optoSerialPort.Close(); optoSerialPort = null; } } catch { } } /// /// Events: Event.ReadRegisterDone, Event.Error /// /// ReadWaterMeter instance reference casted to IOperaton public IOperation ReadRegisterOp() { return this; } /// /// Clear data/counters related to a specific tests /// public void Clear() { resultCode = 0; sampleNr = 0; volumeLtr = 0; volumeLtr0 = 0; timestampSec = 0; timestampSec0 = 0; ReadPulses(); } public void TestCompleted() { /// TODO: Implement } int sampleNr; /// This is to determine when the test start sample should be taken /// Start this operation public void Start() { Clear(); /// Reset opto data optoDataCount = 0; TestStartTelegramIx = 0; TestEndTelegramIx = 0; /// File name is: PCB_AA_BB_HH_MI_SS..txt string wmPosition = Name.Substring(5); if (wmPosition.Length == 1) wmPosition = "0" + wmPosition; string flowNr; if (TestName != null && TestName.ToLower().Contains("q1")) flowNr = "01"; else if (TestName != null && TestName.ToLower().Contains("q2")) flowNr = "02"; else if (TestName != null && TestName.ToLower().Contains("q3")) flowNr = "03"; else if (TestName != null && TestName.ToLower().Contains("adj")) flowNr = "0-1"; else flowNr = "99"; optoDataLogFileName = string.Format("{0}_{1}_{2}_{3}.txt", (ConfigStruct != null) ? ConfigStruct.PCBNumber2String() : "UnknownPcbNr", wmPosition, flowNr, StateMachine.CycleStartTimeStamp.ToString("HH_mm_ss")); StartParsingOptoSerialPort(); } /// Run this operation /// eventDone public Event Run() { sampleNr++; ReadPulses(); if (sampleNr == 4) { /// Take the test start sample VolumeLtrStart = volumeLtr; timestampSecStart = timestampSec; TestStartTelegramIx = telegramIx; } /// Shift data in pipelines VolumeLtrEnd = volumeLtrEnd3; volumeLtrEnd3 = volumeLtrEnd2; volumeLtrEnd2 = volumeLtrEnd1; volumeLtrEnd1 = volumeLtr; timestampSecEnd = timestampSecEnd3; timestampSecEnd3 = timestampSecEnd2; timestampSecEnd2 = timestampSecEnd1; timestampSecEnd1 = timestampSec; TestEndTelegramIx = telegramIx - 3; return Event.ReadRegisterDone; } /// Stop this operation public void Stop() { if (TestStartTelegramIx > 0 && TestStartTelegramIx < optoData.Length && optoData[TestStartTelegramIx].Flags == OptoTelegramFlags.OK) { optoData[TestStartTelegramIx].Flags = OptoTelegramFlags.OK_TestStart; OptoTelegramRaw.TestStartTimestampDec = optoData[TestStartTelegramIx].TimestampDec(); } if (TestEndTelegramIx > 0 && TestEndTelegramIx < optoData.Length && optoData[TestEndTelegramIx].Flags == OptoTelegramFlags.OK) { optoData[TestEndTelegramIx].Flags = OptoTelegramFlags.OK_TestEnd; } StopParsingOptoSerialPort(); OptoTelegramRaw.FIRFilterFlow(optoData, optoDataCount); SaveOptoData(); if (TestStartTelegramIx == 0 || optoDataCount < 100) { resultCode |= (int)Results.Entities.ResultCode.MissingOptoData; } else if (VolumeLtrEnd == VolumeLtrStart) { resultCode |= (int)Results.Entities.ResultCode.OptoDataWithZeroFlow; } } void SaveOptoData() { string directory = string.Format("C:\\TBF\\ProcessData\\{0}\\{1}\\{2}\\", StateMachine.CycleStartTimeStamp.ToString("yy"), StateMachine.CycleStartTimeStamp.ToString("MM"), StateMachine.CycleStartTimeStamp.ToString("dd")); string fullFileName = directory + optoDataLogFileName; try { Directory.CreateDirectory(directory); double scalFact = ScalingFactor(); using (TextWriter optoLogFile = new StreamWriter(fullFileName)) { optoLogFile.WriteLine(optoData[0].ToString(scalFact, null)); for (int i = 1; i < optoDataCount; i++) optoLogFile.WriteLine(optoData[i].ToString(scalFact, optoData[i - 1])); optoLogFile.Close(); } } catch (Exception exc) { File.Delete(fullFileName); log.ErrorFormat(string.Format("Error writing into file {0}", fullFileName)); log.ErrorFormat(string.Format("Exception message: {0}", exc.Message)); } } void ReadPulses() { beginWMState = volumeLtr0; endWMState = volumeLtr; wmVolume = Math.Abs(endWMState - beginWMState); wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5); wmRefPulses = StateMachine.ControlBoard.EtPulses(0); wmTestTime = timestampSec - timestampSec0; } bool optoSerialPortParsingEnabled; /// Flush internal buffers and start parsing the opto serial port data void StartParsingOptoSerialPort() { optoSerialPortParsingEnabled = true; } /// Stop parsig the opto serial port data void StopParsingOptoSerialPort() { optoSerialPortParsingEnabled = false; } /// /// Variables storing the context of serial port data parsing (ReadOptoSerialPort(...)) /// bool synchronized; bool synchronized2; string partOfTelegram; /// /// 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 /// ... /// /// OptoState.Read or OptoState.Flush void ReadOptoData(OptoState optoState) { 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; } else { // CR+LF found if (optoState == OptoState.Read) { if (pos < OptoTelegramRaw.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].Counter = optoDataCount; optoData[optoDataCount++].SetFlags(OptoTelegramFlags.SyncError); } synchronized = true; } // CR+LF found and (pos >= OptoTelegramRaw.Length - 2) else if (optoData[optoDataCount].UpdateFromString(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2), optoDataCount, ref volumeRawExtLast, ref timestampExtLast)) { OptoTelegramRreceived(optoDataCount++, synchronized2); synchronized2 = synchronized; allRcvd = allRcvd.Substring(pos + 2); } else { optoData[optoDataCount].Counter = optoDataCount; optoDataCount++; allRcvd = allRcvd.Substring(pos + 2); } } else /// optoState == OptoState.Flush { if (pos < OptoTelegramRaw.Length - 2) { /// CR+LF found too early, truncate the beginning incl CR+LF and keep scanning in this loop allRcvd = allRcvd.Substring(pos + 2); synchronized = true; } // CR+LF found and (pos >= OptoTelegram.Length - 2) else if (toBeFlushed.UpdateFromStringDummy(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2))) { flushedDataCount++; synchronized2 = synchronized; allRcvd = allRcvd.Substring(pos + 2); } else { allRcvd = allRcvd.Substring(pos + 2); } } } } //OnOptoReceived(this, new OptoReceivedEventArgs(s)); } else { //OnOptoReceived(this, new OptoReceivedEventArgs(".")); } } void OptoTelegramRreceived(int currentIx, bool async) { OptoTelegramRaw optoTelegram = optoData[currentIx]; telegramIx = currentIx; lastVolumeRaw = volumeRawExtLast; lastTimestamp = timestampExtLast; 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")); } /// /// Called from the state machine when a test is selected and UI needs to be updated. /// public void OnOptoReceived(object sender, OptoReceivedEventArgs args) { if (OptoReceivedHandler == null) return; try { OptoReceivedHandler(sender, args); } catch (Exception) { } } public event EventHandler OptoReceivedHandler; /// /// Compares CalibrationStruct.MeterType with iPerlCfg.MeterType. /// iPerlCfg.MeterType == MeterType.AutoDetect disables type checking /// CalibrationStruct == null disables type checking ... /// ... so that failed RFID communication does not cause type verification failure) /// /// true when type is OK public bool VerifyIPerlType() { if (iperlHeadCfg.MeterType == MeterType.AutoDetect || CalibrationStruct == null) { return true; } return iperlHeadCfg.MeterType == CalibrationStruct.MeterType; } public static double UnitVolume(VolumeUnits units) { switch (units) { default: case VolumeUnits.m3: return Config.Units.ConvertFrom(Config.Unit.m3, 1.0); /// liter case VolumeUnits.UK_gallon: return Config.Units.ConvertFrom(Config.Unit.gal_UK, 1.0); /// liter case VolumeUnits.US_gallon: return Config.Units.ConvertFrom(Config.Unit.gal_US, 1.0); /// liter } } public double ScalingFactor() { if ((iperlHeadCfg.MeterType == MeterType.AutoDetect) && (CalibrationStruct != null)) { return IperlHead.ScalingFactor(CalibrationStruct.MeterType); } else if (iperlHeadCfg.MeterType != MeterType.AutoDetect) { return IperlHead.ScalingFactor(iperlHeadCfg.MeterType); } else { return IperlHead.ScalingFactor(MeterType.DN20); } } /// /// Scaling factor: /// 0, 1 (DN15, Coax) . . . . 1 /// 2 (DN20) . . . . . . . . 2 /// 3 (DN25) . . . . . . . . 4 /// 4, 5 (DN26, DN32) . . . . 8 /// 6 (DN40) . . . . . . . . 16 /// /// MeterType (0..6) /// Scaling factor 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; } } int PositionNrFormWMName(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; } } }