/// /// Copyright (c) 2015-2022 Sensus Slovensko a.s. /// using System; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Linq; using log4net; using Common; using Common.Iperl; using Config.Entities; using TBF.Rig.Generic; using TBF.Rig.GenericDevices; using TBF.Rig.Output; using TBF.Rig.Sequences; namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead { /// /// This component = instance of this class is a placeholder for a combined main watermeter /// public class IperlHead : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation { private static readonly ILog log = LogManager.GetLogger(typeof(IperlHead)); public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } #if TURA_SPECIAL public const int OptoDataBufferSize = 250000; #else public const int OptoDataBufferSize = 40000; /// Opto data count is not limitted by the buffer size #endif public const string OptoDataDirectory = "C:\\TBF\\ProcessData"; public const int StartOptoDataCount = OptoDataBufferSize / 2; public const int EndOptoDataCount = OptoDataBufferSize - StartOptoDataCount; public const int StartEndFilterSamplesCount2 = 20; /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1 public const int FeatureVectorSize = 9; 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 iPerlHead.MeterType MeterType { get { return iperlHeadCfg.MeterType; } } public int Position { get { int firstDigitPos = Name.IndexOfAny(new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' }); int position; return (firstDigitPos < 0) ? 0 : (int.TryParse(Name.Substring(firstDigitPos), out position) ? position : 0); } } public RegisterReaderType RegisterReaderType { get { return RegisterReaderType.DataStream; } } public double PulsesPerLtr { get { return 1000.0; } } public double LtrsPerPulse { get { return 1 / PulsesPerLtr; } } public double CalibTarget { get { return iperlHeadCfg.ProcParams.CalibTarget; } } public ushort FactorLimitLo { get { return (ushort)iperlHeadCfg.ProcParams.FactorLimitLo; } } public ushort FactorLimitHi { get { return (ushort)iperlHeadCfg.ProcParams.FactorLimitHi; } } public Counting InitFlowDir { get { return (iperlHeadCfg != null && iperlHeadCfg.ProcParams != null) ? iperlHeadCfg.ProcParams.Counting : Counting.Arbitrary; } } /// Properties set by the Begin and the End form public string SerialNr { get { if (ConfigStruct != null) return ConfigStruct.GetPcbNrString(); else if (simulatedPcbNr != null) return simulatedPcbNr; else return string.Empty; } set { simulatedPcbNr = value; } } public bool Disabled; public bool CommFailed; public int ResultCode; string extraDataPath; public string ExtraDataPath { get { return extraDataPath; } } float[] x; public float[] X { get { return x; } } /// /// Passed to OptoTelegramRaw.UpdateFromString(...) /// Int64 volumeRawExtLast; Int64 timestampExtLast; FlowDirectionDetection flowDirectionDetection; public bool PositiveCounting; public ConfigStruct ConfigStruct; /// ConfigStruct of WM obtained or updated by iPerlCommunication public CalibrationStruct CalibrationStruct; /// CalibrationStruct of WM obtained or updated by iPerlCommunication public CalibrationStructV4 CalibrationStructV4; /// CalibrationStruct of WM obtained or updated by iPerlCommunication public Byte OrigTestModeConfig; /// Written to by StartTestingSealedMeter(), read from by EndTestingSealedMeter() public ushort OrigCalibFactor; public ushort CalibFactor { get { return (CalibrationStruct != null) ? CalibrationStruct.Calibration : ((CalibrationStructV4 != null) ? CalibrationStructV4.Calibration : (ushort)0); } } public ushort OrigCalibFactorLNA; public ushort CalibFactorLNA { get { return (CalibrationStructV4 != null) ? CalibrationStructV4.CalibrationLNA : (ushort)0; } } public double Q2ErrWOCorrection; public int Q2CorrRL; public int Q2CorrLR; public double Diff2Hz8Hz; public bool Hz2CorrectionDone; public int Hz2Correction; public string FWVersion { get { return (CalibrationStruct != null) ? CalibrationStruct.FWVersionStr() : ((CalibrationStructV4 != null) ? CalibrationStructV4.FWVersionStr() : string.Empty); } } /// Result of the last test used to calculate Q2 correction factors, etc public Results.Entities.MeterTestRslt LastTestResult; public Results.Entities.MeterTestRslt LastTestResult2; /// /// Required for IRegisterReader interface /// public int WMPulses { get { return wmPulses; } } public int WMRefPulses { get { return wmRefPulses; } } public double BeginWMState { get { return beginWMState; } } public double EndWMState { get { return endWMState; } } public double WMVolume { get { return wmVolume; } } public double WMTestTime { get { return wmTestTime; } } string simulatedPcbNr = null; int wmPulses; int wmRefPulses; double beginWMState; double endWMState; double wmVolume; double wmTestTime; /// /// New calibration factor calculated from the original factor (argument) /// and results of any test(s). /// Uses also: this.CalibTarget, this.VolumeLtrStart, this.VolumeLtrEnd /// Side effects: this.OrigCalibFactor, this.PositiveCounting /// /// 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 targetVolume = adjustTestResult.VolumeRef * (1.0f + CalibTarget / 100.0f); OrigCalibFactor = originalCalibrationFactor; if (meterVolume > 1E-2) { PositiveCounting = VolumeLtrEnd > VolumeLtrStart; UInt16 newFactor = (UInt16)((double)originalCalibrationFactor * targetVolume / meterVolume + 0.5); log.InfoFormat("Calibration factor: orig={0} new={1} V_iperl={2} V_ref={3} V_target={4}", originalCalibrationFactor, newFactor, meterVolume.ToString("F3"), adjustTestResult.VolumeRef.ToString("F3"), targetVolume.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 factor should be used only for R800 meters. /// /// A test result from which to calculate the factor /// Nominal flow in m3/h /// 0 or the current Q2 correction factor when updating the factor /// Calculated Q2 correction factor public double CalculateQ2CorrectionFactor(Results.Entities.MeterTestRslt currentQ2Result, int currentFactor, double nominalFlow, double errorTarget = 0) { double nominalTestFlowLph = Units.ConvertTo(Unit.lph, nominalFlow); double volumeRefShiftedToTarget = currentQ2Result.VolumeRef * (1.0 + errorTarget / 100.0); double q2adjErrorShiftedToTarget = Config.Formulas.ErrorFromVolumes(currentQ2Result.VolumeMeter, volumeRefShiftedToTarget); 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 [%] /// Do not change the factor for an invalid measurement (q2adjResult.VolumeMeter == 0) double q2CorrectionFactor = (Math.Abs(currentQ2Result.VolumeMeter) <= float.Epsilon) ? Convert.ToDouble(currentFactor) : Convert.ToDouble(currentFactor) - (q2adjErrorShiftedToTarget / G) * (volumeRefShiftedToTarget / currentQ2Result.VolumeMeter); log.WarnFormat("CalculateQ2CorrectionFactor() : Pos={0}, PCB#={1}, Error={2}%, Target={3}%, Current factor={4} New factor={5}", Name, SerialNr, currentQ2Result.Error.ToString("F2"), errorTarget.ToString("F3"), currentFactor.ToString("F1"), q2CorrectionFactor.ToString("F1")); return q2CorrectionFactor; } /// /// 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; } /// /// Store/update values to be used as a part of the opto-data log file name. /// Stop data stream processing and saving, if it is enabled. /// /// Currently executed test /// Currently executed repetition number public void TestIsGoingToStartSoon(Test _test, int _repetitionNr) { /// Store/update values to be used as a part of the opto-data log file name this.test = _test; this.repetitionNr = _repetitionNr; if (IsDataStreamProcessing()) { StopDataStreamProcessing(); /// Dummy '#### start test ####' and '#### end of test ####' marks are added /// to the raw data file on request of Joern Goege if (optoDataCount >= 100 && optoDataCount <= optoData.Length && optoData[40].Flags == OptoTelegramFlags.OK && optoData[optoDataCount - 40].Flags == OptoTelegramFlags.OK) { optoData[40].Flags = OptoTelegramFlags.OK_TestStart; optoData[optoDataCount - 40].Flags = OptoTelegramFlags.OK_TestEnd; } DataStreamPostProcessing(); string pcbNr = (ConfigStruct != null) ? ConfigStruct.GetPcbNrString() : "UnknownPcbNr"; string wmPosition = Name.Substring(5); /// WMPosition is extracted from a component name in form 'iPerl#' if (wmPosition.Length == 1) wmPosition = "0" + wmPosition; string cycleStartTime = StateMachine.CycleStartTimeStamp.ToString("HH_mm_ss"); /// string relativeDirectory = Path.Combine(StateMachine.CycleStartTimeStamp.ToString("yy"), StateMachine.CycleStartTimeStamp.ToString("MM"), StateMachine.CycleStartTimeStamp.ToString("dd")); string directory = Path.Combine(OptoDataDirectory, relativeDirectory); string fileName = string.Format("{0}_{1}_{2}_{3}.txt", pcbNr, wmPosition, "WM", cycleStartTime); if (SaveOptoDataToFile(directory, fileName)) { extraDataPath = Path.Combine(relativeDirectory, fileName); } } } /// Test test; int repetitionNr; /// /// Indices to determine centers of start / end samples /// public int TestStartTelegramIx; public int TestEndTelegramIx; int endTelegramIdx1; int endTelegramIdx2; int endTelegramIdx3; int currentTelegramIx; bool startSampleAcquired; /// /// Timestamp from the opto telegram /// private Int64 lastTimestamp; private double timestampSec; private double timestampSec0; /// Test start volume for metrology in seconds public double TimestampSecStart { get { return TimeFromSamples(optoData, optoDataCount, TestStartTelegramIx, StartEndFilterSamplesCount2); } } /// Test end time for metrology in seconds public double TimestampSecEnd { get { return TimeFromSamples(optoData, optoDataCount, TestEndTelegramIx, StartEndFilterSamplesCount2); } } /// public bool NoSamples { get { return TimestampSecStart == 0 || TimestampSecEnd == 0 || (TimestampSecEnd - TimestampSecStart) < float.Epsilon; } } /// /// Volume of water from the opto telegram /// private Int64 lastVolumeRaw; /// Last read raw volume private double volumeLtr; private double volumeLtr0; /// Test start volume for metrology in liters public double VolumeLtrStart { get { return NoSamples ? 0 : VolumeFromSamples(optoData, optoDataCount, TestStartTelegramIx, ScalingFactor(), StartEndFilterSamplesCount2); } } /// Test end volume for metrology in liters public double VolumeLtrEnd { get { return NoSamples ? 0 : VolumeFromSamples(optoData, optoDataCount, TestEndTelegramIx, ScalingFactor(), StartEndFilterSamplesCount2); } } OptoTelegramRaw[] optoData; int optoDataCount; /// Real opto deta count, can be larger then optoData.Length /// OptoTelegramRaw toBeFlushed; /// /// Opto serial port and worker thread related private variables /// private SerialPort optoSerialPort; public IperlHead() { } public IperlHead(Generic.IComponentCfg cfg) : base(cfg) { iperlHeadCfg = cfg as IperlHeadCfg; } public override void Initialize() { x = new float[FeatureVectorSize]; flowDirectionDetection = new FlowDirectionDetection(); /// Allocate memory for opto-data from iPerl optoData = new OptoTelegramRaw[OptoDataBufferSize]; for (int i = 0; i < OptoDataBufferSize; i++) { optoData[i] = new OptoTelegramRaw(); } toBeFlushed = new OptoTelegramRaw(); dataStreamState = DataStreamState.Flush; synchronized = false; synchronized2 = false; partOfTelegram = string.Empty; if (DebugLevel == DebugMode.Normal) { /// Open serial port: 9600 Bd, 8 data bits, 1 stop bit, no parity string portName = string.Format("COM{0}", iperlHeadCfg.OptoComPortNr); optoSerialPort = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One); optoSerialPort.Handshake = Handshake.None; optoSerialPort.Open(); log.FatalFormat("{0} initialized: {1}", Name, this); } else { optoSerialPort = null; log.FatalFormat("{0} simulated: {1}", Name, this); } } /// /// Clear data related to a specific water meter /// public void StartSession() { ResultCode = 0; Disabled = false; CommFailed = false; ConfigStruct = null; CalibrationStruct = null; CalibrationStructV4 = null; OrigTestModeConfig = 0; LastTestResult = null; LastTestResult2 = null; OrigCalibFactor = 0; OrigCalibFactorLNA = 0; Q2ErrWOCorrection = 0; Q2CorrRL = 0; Q2CorrLR = 0; simulatedPcbNr = null; dataStreamState = DataStreamState.Flush; currentFlowDir = InitFlowDir; } public void SaveMark(object mark) { /// TODO } public void EndSession() { StopDataStreamProcessing(); } Counting currentFlowDir; /// public OptoHeadState CheckFlowDirection() { return (flowDirectionDetection != null) ? flowDirectionDetection.CheckFlowDirection(currentFlowDir, Name) : OptoHeadState.DirNok; } /// public void ChangeFlowDirection() { switch (InitFlowDir) { case Counting.Positive: currentFlowDir = Counting.Negative; break; case Counting.Negative: currentFlowDir = Counting.Positive; break; case Counting.Arbitrary: default: currentFlowDir = Counting.Arbitrary; break; } if (flowDirectionDetection != null) flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection } public void RunDeviceBefore() { if (DebugLevel == DebugMode.Normal) { try { ReadOptoData(dataStreamState); } 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 { } } public void StopDevice2() { } /// /// 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; volumeLtr = 0; volumeLtr0 = 0; timestampSec = 0; timestampSec0 = 0; extraDataPath = null; /// Clear the feature vector for (int i = 0; i < FeatureVectorSize; i++) { x[i] = 0; } } public void TestCompleted() { /// TODO: Implement } int timeFromStart; /// [s] Time from test start to determine when the test start sample should be taken /// /// Start this operation /// public void Start() { lock (this) { Clear(); ReadPulses(); StartDataStreamProcessing(); } } /// /// Run this operation /// /// eventDone public Event Run() { lock (this) { timeFromStart += StateMachine.Period; ReadPulses(); if (!startSampleAcquired && (timeFromStart >= 8) && (currentTelegramIx >= 0)) { /// Take the test start sample startSampleAcquired = true; TestStartTelegramIx = currentTelegramIx; } else if (startSampleAcquired) { /// Shift data in pipelines TestEndTelegramIx = endTelegramIdx3; endTelegramIdx3 = endTelegramIdx2; endTelegramIdx2 = endTelegramIdx1; endTelegramIdx1 = currentTelegramIx; } } return Event.ReadRegisterDone; } /// /// Stop this operation /// public void Stop() { log.DebugFormat("Flow filtering end, feature vector calculation start: {0:HH:mm:ss.fff}", DateTime.Now); int startIx; int endIx; lock (this) { StopDataStreamProcessing(); AddTestStartEndMarksToData(out startIx, out endIx); } DataStreamPostProcessing(); // TODO: Enable when calculations completed // // float[] offsetV, kOhmsR, kOhmsC, dutFlow, refFlow, flowRatio, magField, emfV; // x = Common.StatisticalMetrics.Calculate(optoData, optoDataCount, startIx, endIx, true, // out offsetV, out kOhmsR, out kOhmsC, out dutFlow, // out refFlow, out flowRatio, out magField, out emfV); // // log.DebugFormat("Feature vector calculation end, save opto-file start: {0:HH:mm:ss.fff}", DateTime.Now); #if ORACLE_DB if (test.RawDataId + (test.Repeats - repetitionNr) * test.RawDataIdRepetMulti != 0) { string relativeDirectory = Path.Combine(StateMachine.CycleStartTimeStamp.ToString("yy"), StateMachine.CycleStartTimeStamp.ToString("MM"), StateMachine.CycleStartTimeStamp.ToString("dd")); string fileName = DetermineExtraDataFileName(); if (SaveOptoDataToFile(Path.Combine(OptoDataDirectory, relativeDirectory), fileName)) { extraDataPath = Path.Combine(relativeDirectory, fileName); } log.WarnFormat("IperlHead.Stop() startIx={0} endIx={1} len={2} raw data file = {3}", startIx, endIx, optoData.Length, fileName); } else #endif { log.WarnFormat("IperlHead.Stop() startIx={0} endIx={1} len={2} no raw data file", startIx, endIx, optoData.Length); } if (TestStartTelegramIx == 0 || optoDataCount < 100) { ResultCode |= (int)Results.Entities.ResultCode.MissingOptoData; } else if (VolumeLtrEnd == VolumeLtrStart) { ResultCode |= (int)Results.Entities.ResultCode.OptoDataWithZeroFlow; } } void AddTestStartEndMarksToData(out int startIx, out int endIx) { startIx = BufferIdx(TestStartTelegramIx); if ((startIx > 0) && (startIx < StartOptoDataCount) && (optoData[startIx].Flags == OptoTelegramFlags.OK)) { optoData[startIx].Flags = OptoTelegramFlags.OK_TestStart; OptoTelegramRaw.TestStartTimestampDec = optoData[startIx].TimestampDec(); } else { for (int ix = 0; ix < StartOptoDataCount; ix++) { if (optoData[ix].Flags == OptoTelegramFlags.OK) { /// This is the first correct opto-telegram received OptoTelegramRaw.TestStartTimestampDec = optoData[ix].TimestampDec(); break; } } } endIx = BufferIdx(TestEndTelegramIx); if ((endIx > 0) && (optoData[endIx].Flags == OptoTelegramFlags.OK)) { optoData[endIx].Flags = OptoTelegramFlags.OK_TestEnd; } } /// /// Data stream post processing: /// Flow from a reference flowmeter is FIR filtered /// void DataStreamPostProcessing() { if (optoDataCount <= OptoDataBufferSize) { FIRFilterFlow(optoData, 0, optoDataCount - 1); } else { FIRFilterFlow(optoData, 0, StartOptoDataCount - 1); FIRFilterFlow(optoData, (optoDataCount - EndOptoDataCount), optoDataCount - 1); } } /// /// Determine opto data file name: PCB_AA_BB_HH_MI_SS..txt /// /// Test name /// Test repeats count (>= 1) /// Repetition number (1 .. testRepeats) /// string DetermineExtraDataFileName() { /// /// Get required pieces of information /// string pcbNr = (ConfigStruct != null) ? ConfigStruct.GetPcbNrString() : "UnknownPcbNr"; string wmPosition = Name.Substring(5); /// WMPosition is extracted from a component name in form 'iPerl#' if (wmPosition.Length == 1) wmPosition = "0" + wmPosition; string cycleStartTime = StateMachine.CycleStartTimeStamp.ToString("HH_mm_ss"); #if ORACLE_DB string[] designations = string.IsNullOrEmpty(test.RawDataDesignation) ? new string[0] : test.RawDataDesignation.Split(new char[] { '~' }); int testId = test.RawDataId + (test.Repeats - repetitionNr) * test.RawDataIdRepetMulti; string designation = string.IsNullOrEmpty(test.RawDataDesignation) ? testId.ToString(testId > 0 ? "D2" : "D1") /// Name is generated from Id : (designations.Length > repetitionNr - 1) ? designations[repetitionNr - 1] /// Name is from 'RawDataDesignation' parameter : string.Format("{0}-{1}", designations[0], repetitionNr); /// Name is form test name and repetition nr. #else int testId = 0; string designation = (test.Repeats == 1) ? test.Name : string.Format("{0}-{1}", test.Name, repetitionNr); #endif /// /// Return the file name /// return string.Format("{0}_{1}_{2}_{3}.txt", pcbNr, wmPosition, designation, cycleStartTime); } /// /// Save opto data to a file. /// bool SaveOptoDataToFile(string directory, string fileName) { string fullFileName = Path.Combine(directory, fileName); log.WarnFormat("Saving {0} raw data to {1}", Name, fullFileName); try { Directory.CreateDirectory(directory); double scalFact = ScalingFactor(); using (TextWriter optoLogFile = new StreamWriter(fullFileName)) { if (optoDataCount <= OptoDataBufferSize) { /// Telegrams are stored continuously, save them. optoLogFile.WriteLine(optoData[0].ToString(scalFact, null)); for (int i = 1; i < optoDataCount; i++) { optoLogFile.WriteLine(optoData[i].ToString(scalFact, optoData[i - 1])); } } else /// if (optoDataCount > MaxOptoDataCount) { /// Buffer overflow /// First part of the buffer is saved as is optoLogFile.WriteLine(optoData[0].ToString(scalFact, null)); for (int i = 1; i < StartOptoDataCount; i++) { optoLogFile.WriteLine(optoData[i].ToString(scalFact, optoData[i - 1])); } optoLogFile.WriteLine(" ..."); /// Second part of the buffer is an overflowed circular buffer optoLogFile.WriteLine(optoData[BufferIdx(optoDataCount)].ToString(scalFact, null)); for (int i = optoDataCount - EndOptoDataCount + 1; i < optoDataCount; i++) { optoLogFile.WriteLine(optoData[BufferIdx(i)].ToString(scalFact, optoData[BufferIdx(i - 1)])); } } log.WarnFormat("{0} opto data successfully saved: {1} lines", Name, optoDataCount); optoLogFile.Close(); } return true; } 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)); return false; } } void ReadPulses() { beginWMState = volumeLtr0; endWMState = volumeLtr; wmVolume = Math.Abs(endWMState - beginWMState); wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5); wmRefPulses = StateMachine.ControlBoard.RefPulses; wmTestTime = timestampSec - timestampSec0; } DataStreamState dataStreamState; /// /// Reset counters / indices / time and start processing and saving datastream data /// public void StartDataStreamProcessing() { /// Reset opto-data, etc. optoDataCount = 0; timeFromStart = 0; currentTelegramIx = -1; startSampleAcquired = false; TestStartTelegramIx = 0; endTelegramIdx1 = 0; endTelegramIdx2 = 0; endTelegramIdx3 = 0; TestEndTelegramIx = 0; if (optoSerialPort != null && optoSerialPort.IsOpen) optoSerialPort.DiscardInBuffer(); if (flowDirectionDetection != null) flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection /// Enable opto-data parsing and saving dataStreamState = DataStreamState.ProcessAndSave; } /// /// Returns true when processing and saving datastream data is in progress /// bool IsDataStreamProcessing() { return dataStreamState == DataStreamState.ProcessAndSave; } /// /// Stop processing and saving datastream data /// void StopDataStreamProcessing() { dataStreamState = DataStreamState.Flush; } /// /// Variables storing the context of serial port data parsing (ReadOptoSerialPort(...)) /// bool synchronized; bool synchronized2; string partOfTelegram; /// /// Reads opto-datastream via serual port. Invoked from RunDeviceBefore() /// /// 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(DataStreamState optoState) { lock (this) { 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 == DataStreamState.ProcessAndSave) { int bufferIx = BufferIdx(optoDataCount); 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[bufferIx].Counter = optoDataCount; optoData[bufferIx].SetFlags(OptoTelegramFlags.SyncError); } synchronized = true; } else if (optoData[bufferIx].UpdateFromString(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2), optoDataCount, Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), ref volumeRawExtLast, ref timestampExtLast)) { /// CR+LF was found && (pos >= OptoTelegramRaw.Length - 2) && the telegram is OK flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast); OptoTelegramReceived(optoDataCount, synchronized2, volumeRawExtLast, timestampExtLast); synchronized2 = synchronized; allRcvd = allRcvd.Substring(pos + 2); } else { /// CR+LF was found && (pos >= OptoTelegramRaw.Length - 2) but the telgram was not OK optoData[bufferIx].Counter = optoDataCount; optoDataCount++; allRcvd = allRcvd.Substring(pos + 2); } optoDataCount++; } 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.UpdateFromString(allRcvd.Substring(pos - OptoTelegramRaw.Length + 2), 0, Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), ref volumeRawExtLast, ref timestampExtLast)) { flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast); synchronized2 = synchronized; allRcvd = allRcvd.Substring(pos + 2); } else { allRcvd = allRcvd.Substring(pos + 2); } } } } //OnOptoReceived(this, new OptoReceivedEventArgs(s)); } else { //OnOptoReceived(this, new OptoReceivedEventArgs(".")); } } } void OptoTelegramReceived(int currentIx, bool async, Int64 volumeRawExt, Int64 timestampRawExt) { currentTelegramIx = currentIx; lastVolumeRaw = volumeRawExt; lastTimestamp = timestampRawExt; 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; } } /// /// 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 Common.Units.ConvertFrom(Common.Unit.m3, 1.0); /// 1 liter case VolumeUnits.UK_gallon: return Common.Units.ConvertFrom(Common.Unit.UKgal, 1.0); /// 1 imperial gallon case VolumeUnits.US_gallon: return Common.Units.ConvertFrom(Common.Unit.USgal, 1.0); /// 1 US gallon } } 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.DN25_Q3_10: case MeterType.DN32: return 8.0; case MeterType.DN40: return 16.0; } } /// /// Calculate a filtered volume from data stream samples /// /// Samples used in calculation are centered around unwrappedIx /// Count of samples used in calculation is 2 * smaplesCount2 + 1 /// Filtered volume double VolumeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double scalingFactor, int samplesCount2 = 0) { if (samplesCount2 < 0) samplesCount2 = 0; if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0; Int64 sum = 0; for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++) { int wrappedIx = BufferIdx(i); if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK && optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart && optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd) { return 0; } sum += optoData[wrappedIx].VolumeRawExt; } return 0.0000625 * scalingFactor * sum / (double)(2 * samplesCount2 + 1); } /// /// Calculate a filtered time from data stream samples /// /// Samples used in calculation are centered around unwrappedIx /// Count of samples used in calculation is 2 * smaplesCount2 + 1 /// Filtered time double TimeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesCount2 = 0) { if (samplesCount2 < 0) samplesCount2 = 0; if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0; Int64 sum = 0; for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++) { int wrappedIx = BufferIdx(i); if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK && optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart && optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd) { return 0; } sum += optoData[wrappedIx].TimestampExt; } return sum / (double)(8192 * (2 * samplesCount2 + 1)); } /// /// Filter RefFlow data in an array of OptoTelegramRaw objects by a FIR filter: /// /// kSize = 5, kSize2 = 2 /// /// i k /// --------------------------------------------------------------------------- /// 0 -5 filtered[0] = data[0] /// 1 -4 filtered[1] = data[1] /// 2 -3 filtered[2] = data[0]*k[0] + ... + data[4]*k[4] /// 3 -2 filtered[3] = data[1]*k[0] + ... + data[5]*k[4] /// 4 -1 filtered[4] = data[2]*k[0] + ... + data[6]*k[4] /// 5 0 data[0] = filtered[0], filtered[0] = data[3]*k[0] + ... + data[7]*k[4] /// 6 1 data[1] = filtered[1], filtered[1] = data[4]*k[0] + ... + data[8]*k[4] /// 7 ... /// /// array of OptoTelegramRaw objects /// Index of the first optoData item to process /// Index of the last optoData item to process public static void FIRFilterFlow(OptoTelegramRaw[] optoData, int from, int to) { float[] kernel = new float[] { 0.1f, 0.2f, 0.4f, 0.2f, 0.1f }; int kSize = kernel.Length; int kSize2 = kernel.Length / 2; float[] filtered = new float[kSize]; for (int i = from; i <= to; i++) { if ((i < from + kSize2) || (i > to - kSize2)) { /// Beginning or end of optoData buffer => Just copy data (=do not filter) filtered[i % kSize] = optoData[BufferIdx(i)].RefFlow; } else { /// Make a convolution of optoData and the kernel float weightedSum = 0; for (int j = -kSize2; j <= kSize2; j++) weightedSum += optoData[BufferIdx(i + j)].RefFlow * kernel[j + kSize2]; filtered[i % kSize] = weightedSum; } if (i >= from + kSize) { /// filtered[] buffer full => copy filtered data to optoData optoData[BufferIdx(i - kSize)].RefFlow = filtered[i % kSize]; } } for (int i = to - kSize + 1; i <= to; i++) { if (i >= 0) optoData[BufferIdx(i)].RefFlow = filtered[i % kSize]; } } /// /// Get index to optoData buffer /// /// Original unwrapped index /// Index to the buffer public static int BufferIdx(int index) { if (index < IperlHead.OptoDataBufferSize) { return index; } else { return IperlHead.StartOptoDataCount + (index - IperlHead.OptoDataBufferSize) % IperlHead.EndOptoDataCount; } } public void WriteBinary(BinaryWriter writer) { writer.Write(Disabled); writer.Write(CommFailed); writer.Write(ResultCode); writer.Write(PositiveCounting); if (ConfigStruct != null) { writer.Write(true); ConfigStruct.WriteBinary(writer); } else writer.Write(false); if (CalibrationStruct != null) { writer.Write(true); CalibrationStruct.WriteBinary(writer); } else writer.Write(false); if (CalibrationStructV4 != null) { writer.Write(true); CalibrationStructV4.WriteBinary(writer); } else writer.Write(false); writer.Write(OrigCalibFactor); writer.Write(OrigCalibFactorLNA); writer.Write(Q2ErrWOCorrection); writer.Write(Q2CorrRL); writer.Write(Q2CorrLR); writer.Write(Diff2Hz8Hz); writer.Write(Hz2CorrectionDone); writer.Write(Hz2Correction); if (LastTestResult != null) { writer.Write(true); LastTestResult.WriteBinary(writer); } else writer.Write(false); if (LastTestResult2 != null) { writer.Write(true); LastTestResult2.WriteBinary(writer); } else writer.Write(false); } public void ReadBinary(BinaryReader reader) { Disabled = reader.ReadBoolean(); CommFailed = reader.ReadBoolean(); ResultCode = reader.ReadInt32(); PositiveCounting = reader.ReadBoolean(); if (reader.ReadBoolean()) (ConfigStruct = new ConfigStruct()).ReadBinary(reader); if (reader.ReadBoolean()) (CalibrationStruct = new CalibrationStruct()).ReadBinary(reader); if (reader.ReadBoolean()) (CalibrationStructV4 = new CalibrationStructV4()).ReadBinary(reader); OrigCalibFactor = reader.ReadUInt16(); OrigCalibFactorLNA = reader.ReadUInt16(); Q2ErrWOCorrection = reader.ReadDouble(); Q2CorrRL = reader.ReadInt32(); Q2CorrLR = reader.ReadInt32(); Diff2Hz8Hz = reader.ReadDouble(); Hz2CorrectionDone = reader.ReadBoolean(); Hz2Correction = reader.ReadInt32(); if (reader.ReadBoolean()) (LastTestResult = new Results.Entities.MeterTestRslt()).ReadBinary(reader, null); if (reader.ReadBoolean()) (LastTestResult2 = new Results.Entities.MeterTestRslt()).ReadBinary(reader, null); } } }