From 23c83ab452453047d963eeead7d0b5354479c86a Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Thu, 26 Mar 2026 14:45:36 +0100 Subject: [PATCH 1/5] Enhance `GenesisSmartReader` multi-channel processing and add tests for telegram handling: - Refactor `ProcessOptoLine` logic to support output buffer resets at configurable repetition intervals. - Add tests: - `ProcessOptoLine_Block_f_h1_h2_h3_f_ShouldResetBuffersOnlyAfterLastF` - `ReadOptoData_FullBlock_ShouldDiscardBuffersAfterClosingF` - `ProcessOptoLine_LastF_AfterH3_ShouldRequestBufferReset` - `ProcessOptoLine_ShouldReset --- TBF/Properties/AssemblyInfo.cs | 4 +- .../communication/Utils/SerialDriver.cs | 14 + .../implementations/GenesisSmartReader.cs | 1284 +++++++++++------ .../implementations/FakeSerialDriver.cs | 3 +- ...GenesisSmartReaderChannelAveragingTests.cs | 135 +- .../implementations/GenesisSmartReaderTest.cs | 273 +++- 6 files changed, 1219 insertions(+), 494 deletions(-) diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index c861fe1d7..6f85795d5 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("3.9.3013.1")] -[assembly: AssemblyFileVersion("3.9.3013.1")] +[assembly: AssemblyVersion("3.9.3016.1")] +[assembly: AssemblyFileVersion("3.9.3016.1")] diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/communication/Utils/SerialDriver.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/communication/Utils/SerialDriver.cs index add5c6b79..1e6bfe5a3 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/communication/Utils/SerialDriver.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/communication/Utils/SerialDriver.cs @@ -227,6 +227,20 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication.Utils return _serialPort.ReadExisting(); } + public void ResetInputBuffer() + { + if (!IsOpen) + throw new InvalidOperationException("Serial port not open"); + _serialPort.DiscardInBuffer(); + } + + public void ResetOutputBuffer() + { + if (!IsOpen) + throw new InvalidOperationException("Serial port not open"); + _serialPort.DiscardOutBuffer(); + } + public Encoding Encoding => _serialPort?.Encoding ?? _encoding; public int BytesToRead => (_serialPort != null && _serialPort.IsOpen) ? _serialPort.BytesToRead : 0; diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs index 150f2b72d..21aab9931 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs @@ -26,11 +26,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// /// based on IPerlReader class /// - public class GenesisSmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ISmartReader, IRegReaderSmart + public class GenesisSmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, + ISmartReader, IRegReaderSmart { private static readonly ILog log = LogManager.GetLogger(typeof(GenesisSmartReader)); private static readonly ILog logStream = LogManager.GetLogger("StreamData"); + public override string ToString() { string cfgText; @@ -44,20 +46,27 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return string.Format("{0}({1})", ClassName, cfgText); } - + #if TURA_SPECIAL public const int OptoDataBufferSize = 250000; #else - public const int OptoDataBufferSize = 40000; /// Opto data count is not limitted by the buffer size + 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 = 2; //20 /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1 + + public const int + StartEndFilterSamplesCount2 = + 2; //20 /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1 + public const int FeatureVectorSize = 9; - + private OptoHeadTest _optoHeadTest; - + public OptoHeadTest OptoHeadTest { get @@ -68,20 +77,44 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } set { _optoHeadTest = value; } } - - - readonly GenesisCfg genesisHeadCfg; + + + readonly GenesisCfg genesisHeadCfg; string ISmartReader.CommInterface => _commInterface; - public int RfidComPortNr { get { return genesisHeadCfg.RfidComPortNr; } } + public int RfidComPortNr + { + get { return genesisHeadCfg.RfidComPortNr; } + } + public bool CommFailed { get; set; } public bool Disabled { get; set; } - public int OptoComPortNr { get { return genesisHeadCfg.OptoComPortNr; } } - public int MuxBoardNrOrGroup14 { get { return genesisHeadCfg.MuxBoardNr; } } - public int Group { get { return genesisHeadCfg.Group; } } - public MeterType MeterType { get { return genesisHeadCfg.MeterType; } } - public CommunicationInterface CommInterface { get { return genesisHeadCfg.CommunicationInterface; } } + + public int OptoComPortNr + { + get { return genesisHeadCfg.OptoComPortNr; } + } + + public int MuxBoardNrOrGroup14 + { + get { return genesisHeadCfg.MuxBoardNr; } + } + + public int Group + { + get { return genesisHeadCfg.Group; } + } + + public MeterType MeterType + { + get { return genesisHeadCfg.MeterType; } + } + + public CommunicationInterface CommInterface + { + get { return genesisHeadCfg.CommunicationInterface; } + } public int Position { @@ -89,27 +122,60 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { 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); + return (firstDigitPos < 0) + ? 0 + : (int.TryParse(Name.Substring(firstDigitPos), out position) ? position : 0); } } - public RegisterReaderType RegisterReaderType { get { return RegisterReaderType.DataStream; } } - public double PulsesPerLtr { + + public RegisterReaderType RegisterReaderType + { + get { return RegisterReaderType.DataStream; } + } + + public double PulsesPerLtr + { get { return 1000.0; } set { } } - public double LtrsPerPulse { get { return 1 / PulsesPerLtr; } } + + public double LtrsPerPulse + { + get { return 1 / PulsesPerLtr; } + } + public string QuantityUnits { get; set; } - public double CalibTarget { get { return genesisHeadCfg.ProcParams.CalibTarget; } } - public ushort FactorLimitLo { get { return (ushort)genesisHeadCfg.ProcParams.FactorLimitLo; } } - public ushort FactorLimitHi { get { return (ushort)genesisHeadCfg.ProcParams.FactorLimitHi; } } - public Counting InitFlowDir { get { return (genesisHeadCfg != null && genesisHeadCfg.ProcParams != null) ? genesisHeadCfg.ProcParams.Counting : Counting.Arbitrary; } } + public double CalibTarget + { + get { return genesisHeadCfg.ProcParams.CalibTarget; } + } + + public ushort FactorLimitLo + { + get { return (ushort)genesisHeadCfg.ProcParams.FactorLimitLo; } + } + + public ushort FactorLimitHi + { + get { return (ushort)genesisHeadCfg.ProcParams.FactorLimitHi; } + } + + public Counting InitFlowDir + { + get + { + return (genesisHeadCfg != null && genesisHeadCfg.ProcParams != null) + ? genesisHeadCfg.ProcParams.Counting + : Counting.Arbitrary; + } + } /// Properties set by the Begin and the End form - public string SerialNr - { - get + public string SerialNr + { + get { if (ConfigStruct != null) return ConfigStruct.GetPcbNrString(); @@ -118,30 +184,37 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations else return string.Empty; } - set - { - simulatedPcbNr = value; - } - } + set { simulatedPcbNr = value; } + } - //public bool Disabled; + //public bool Disabled; //public bool CommFailed; public int ResultCode; string extraDataPath; - public string ExtraDataPath { get { return extraDataPath; } } + + public string ExtraDataPath + { + get { return extraDataPath; } + } float[] x; - public float[] X { get { return x; } } + + public float[] X + { + get { return x; } + } private static int iChanelsCount = 3; private int firstChanel; + /// /// Passed to OptoTelegramRaw.UpdateFromString(...) /// double[] volumeRawExtLast; + double[] timestampExtLast; FlowDirectionDetection flowDirectionDetection; @@ -149,64 +222,106 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations 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 ConfigStruct ConfigStruct; - public Byte OrigTestModeConfig; /// Written to by StartTestingSealedMeter(), read from by EndTestingSealedMeter() + /// 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 + + public ushort CalibFactor { get { - return (CalibrationStruct != null) ? CalibrationStruct.Calibration - : ((CalibrationStructV4 != null) ? CalibrationStructV4.Calibration - : (ushort)0); + 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 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 double Diff2Hz8Hz; + public bool Hz2CorrectionDone; + public int Hz2Correction; - public string FWVersion + public string FWVersion { get { - return (CalibrationStruct != null) ? CalibrationStruct.FWVersionStr() - : ((CalibrationStructV4 != null) ? CalibrationStructV4.FWVersionStr() - : string.Empty); + 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; + /// 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 ResolveNaNDouble(beginWMState); } } + /// + /// Required for IRegisterReader interface + /// + public int WMPulses + { + get { return wmPulses; } + } + + public int WMRefPulses + { + get { return wmRefPulses; } + } + + public double BeginWMState + { + get { return ResolveNaNDouble(beginWMState); } + } + double ISmartReader.EndWMState { get; set; } double ISmartReader.BeginWMState { get; set; } double ICommonRegReader.EndWMState { get; set; } double ICommonRegReader.BeginWMState { get; set; } - public double EndWMState { get { return ResolveNaNDouble(endWMState); } } - public double WMVolume { get { return ResolveNaNDouble(wmVolume); } } - public double WMTestTime { get { return ResolveNaNDouble(wmTestTime); } } + + public double EndWMState + { + get { return ResolveNaNDouble(endWMState); } + } + + public double WMVolume + { + get { return ResolveNaNDouble(wmVolume); } + } + + public double WMTestTime + { + get { return ResolveNaNDouble(wmTestTime); } + } string simulatedPcbNr = null; @@ -223,128 +338,134 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations int wmPulses; int wmRefPulses; double beginWMState; - double endWMState; - double wmVolume; - double wmTestTime; - + double endWMState; + double wmVolume; + double wmTestTime; - /// - /// New calibration factor calculated from the original factor (argument) - /// and results of any test(s). + + /// + /// 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; + /// + /// 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; + OrigCalibFactor = originalCalibrationFactor; - if (meterVolume > 1E-2) - { - PositiveCounting = VolumeLtrEnd > VolumeLtrStart; + 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")); + 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; + 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 - } - } + 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 + /// + /// 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) - { + 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 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 Units per minute [%] + 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 Units 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); + 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")); + 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; + /// + /// 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 - } + if ((resultAt2Hz == null) || (resultAt8Hz == null)) + { + return false; /// Test result @2Hz and/or @8Hz is missing ==> water meter failed + } - diff2Hz8Hz = resultAt2Hz.Error - resultAt8Hz.Error; + diff2Hz8Hz = resultAt2Hz.Error - resultAt8Hz.Error; - if (Math.Abs(diff2Hz8Hz) > 2.5) return false; /// Difference of errors > 2.5 % ==> water meter failed + if (Math.Abs(diff2Hz8Hz) > 2.5) return false; /// Difference of errors > 2.5 % ==> water meter failed - hz2CorrectionFactor = -1 * (int)Math.Round(10 * diff2Hz8Hz); + 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")); + 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; - } + return true; + } /// @@ -376,13 +497,14 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations DataStreamPostProcessing(); string pcbNr = (ConfigStruct != null) ? ConfigStruct.GetPcbNrString() : "UnknownPcbNr"; - string wmPosition = Name.Substring(5); /// WMPosition is extracted from a component name in form 'iPerl#' + 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")); + 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); @@ -392,8 +514,10 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } } + /// Test test; + int repetitionNr; @@ -401,6 +525,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Indices to determine centers of start / end samples /// public int TestStartTelegramIx; + public int TestEndTelegramIx; int endTelegramIdx1; int endTelegramIdx2; @@ -409,39 +534,69 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations int currentTelegramIx; bool startSampleAcquired; - /// + /// /// Timestamp from the opto telegram /// private double[] lastTimestamp; + private double[] timestampSec; private double[] timestampSec0; /// Test start volume for metrology in seconds public double TimestampSecStart { - get { return TimeFromChannelsAtStart(optoData, optoDataCount, TestStartTelegramIx); } + get + { + if (TestStartTelegramIx < 0 || TestStartTelegramIx >= optoDataCount) + return 0; + + return AverageNullable(TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)); + } } + /// Test end time for metrology in seconds public double TimestampSecEnd { - get { return TimeFromChannelsAtEnd(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1); } + get + { + if (TestEndTelegramIx < 0 || TestEndTelegramIx >= optoDataCount) + return 0; + + return AverageNullable( + TimeEndPerChannel( + optoData, + optoDataCount, + TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)); + } } + /// public bool NoSamples { - get { return TimestampSecStart == 0 || TimestampSecEnd == 0 || (TimestampSecEnd - TimestampSecStart) < float.Epsilon; } + get + { + return optoDataCount <= 0 || + TestStartTelegramIx < 0 || + TestEndTelegramIx < 0 || + TestStartTelegramIx >= optoDataCount || + TestEndTelegramIx >= optoDataCount || + TimestampSecEnd < TimestampSecStart; + } } /// /// Volume of water from the opto telegram /// - private double[] lastVolumeRaw; /// Last read raw volume + private double[] lastVolumeRaw; + + /// Last read raw volume private double[] volumeLtr; + private double[] volumeLtr0; - private int channel0 = -1; - + private int channel0 = -1; + private double Average(double[] data) { @@ -451,18 +606,34 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Test start volume for metrology in liters public double VolumeLtrStart { - get { return NoSamples ? 0 : VolumeFromChannelsAtStart(optoData, optoDataCount, TestStartTelegramIx); } + get + { + if (NoSamples) return 0; + return AverageNullable(VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)); + } } + /// Test end volume for metrology in liters public double VolumeLtrEnd { - get { return NoSamples ? 0 : VolumeFromChannelsAtEnd(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1); } + get + { + if (NoSamples) return 0; + return AverageNullable( + VolumeEndPerChannel( + optoData, + optoDataCount, + TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)); + } } - OptoTelegramRaw[] optoData; - int optoDataCount; /// Real opto deta count, can be larger then optoData.Length + + OptoTelegramRaw[] optoData; + int optoDataCount; + + /// Real opto deta count, can be larger then optoData.Length /// OptoTelegramRaw toBeFlushed; @@ -472,14 +643,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public ISerialDriver optoSerialPort; - public GenesisSmartReader() { } - - public GenesisSmartReader(Generic.IComponentCfg cfg) - : base(cfg) + public GenesisSmartReader() { - genesisHeadCfg = cfg as GenesisCfg; - } - + } + + public GenesisSmartReader(Generic.IComponentCfg cfg) + : base(cfg) + { + genesisHeadCfg = cfg as GenesisCfg; + } + private readonly Func _serialFactory; public GenesisSmartReader(Generic.IComponentCfg cfg, Func serialFactory = null) @@ -488,21 +661,21 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations genesisHeadCfg = cfg as GenesisCfg; _serialFactory = serialFactory; } - + public override void Initialize() { x = new float[FeatureVectorSize]; flowDirectionDetection = new FlowDirectionDetection(); - + volumeRawExtLast = new double[iChanelsCount]; timestampExtLast = new double[iChanelsCount]; - + lastTimestamp = new double[iChanelsCount]; timestampSec = new double[iChanelsCount]; timestampSec0 = new double[iChanelsCount]; - - lastVolumeRaw = new double[iChanelsCount]; /// Last read raw volume + + lastVolumeRaw = new double[iChanelsCount]; /// Last read raw volume volumeLtr = new double[iChanelsCount]; volumeLtr0 = new double[iChanelsCount]; @@ -520,7 +693,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations synchronized2 = false; partOfTelegram = string.Empty; optoSerialPort = null; - + if (DebugLevel == DebugMode.Normal) { @@ -528,7 +701,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Check whether head is connected, working try { - OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, Handshake.None); + OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, + Handshake.None); CloseOptoSerialPort(); log.FatalFormat($"{Name} initialized: {this}"); } @@ -543,30 +717,30 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.FatalFormat($"{Name} simulated: {this}"); } } - + /// - /// Clear data related to a specific water meter - /// - public void StartSession() - { + /// Clear data related to a specific water meter + /// + public void StartSession() + { ResultCode = 0; - Disabled = false; - CommFailed = false; + Disabled = false; + CommFailed = false; - ConfigStruct = null; - CalibrationStruct = null; + ConfigStruct = null; + CalibrationStruct = null; CalibrationStructV4 = null; OrigTestModeConfig = 0; - LastTestResult = null; + LastTestResult = null; LastTestResult2 = null; - OrigCalibFactor = 0; + OrigCalibFactor = 0; OrigCalibFactorLNA = 0; - Q2ErrWOCorrection = 0; - Q2CorrRL = 0; + Q2ErrWOCorrection = 0; + Q2CorrRL = 0; Q2CorrLR = 0; simulatedPcbNr = null; @@ -588,6 +762,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations Counting currentFlowDir; + /// // public OptoHeadState CheckFlowDirection() // { @@ -618,66 +793,70 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public void RunDeviceBefore() { - if (DebugLevel == DebugMode.Normal) - { - try - { + if (DebugLevel == DebugMode.Normal) + { + try + { ReadOptoData(dataStreamState); - } - catch (Exception e) - { - DebugLevel = DebugMode.FailureDuringOperation; + } + 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) - { - } - } + 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 RunDeviceAfter() + { + } public void StopDevice() { - try - { - if (optoSerialPort != null) - { + try + { + if (optoSerialPort != null) + { CloseOptoSerialPort(); - } - } - catch - { - } - } + } + } + catch + { + } + } - public void StopDevice2() { } + public void StopDevice2() + { + } - /// - /// Events: Event.ReadRegisterDone, Event.Error - /// - /// ReadWaterMeter instance reference casted to IOperaton - public IOperation ReadRegisterOp() - { - return this; - } + /// + /// 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() - { + /// + /// Clear data/counters related to a specific tests + /// + public void Clear() + { ResultCode = 0; - volumeLtr = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); - volumeLtr0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); - timestampSec = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); - timestampSec0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); + volumeLtr = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); + volumeLtr0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); + timestampSec = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); + timestampSec0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray(); extraDataPath = null; @@ -686,35 +865,38 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { x[i] = 0; } - } + } - public void TestCompleted() - { - /// TODO: Implement - } + public void TestCompleted() + { + /// TODO: Implement + } - int timeFromStart; /// [s] Time from test start to determine when the test start sample should be taken + int timeFromStart; - /// + /// [s] Time from test start to determine when the test start sample should be taken + + /// /// Start this operation /// - public void Start() - { + public void Start() + { lock (this) { + ReasetDataBuffer(); Clear(); ReadPulses(); StartDataStreamProcessing(); } } - /// + /// /// Run this operation /// - /// eventDone - public Event Run() - { + /// eventDone + public Event Run() + { lock (this) { timeFromStart += StateMachine.Period; @@ -736,14 +918,14 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } - return Event.ReadRegisterDone; - } + return Event.ReadRegisterDone; + } - /// + /// /// Stop this operation /// - public void Stop() - { + public void Stop() + { log.DebugFormat("Flow filtering end, feature vector calculation start: {0:HH:mm:ss.fff}", DateTime.Now); int startIx; @@ -756,6 +938,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } DataStreamPostProcessing(); + ReasetDataBuffer(); // TODO: Enable when calculations completed // @@ -784,17 +967,18 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations else #endif { - log.WarnFormat("IperlHead.Stop() startIx={0} endIx={1} len={2} no raw data file", startIx, endIx, optoData.Length); + 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; - } + 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) @@ -855,11 +1039,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// 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#' + 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[] { '~' }); + 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 @@ -880,7 +1065,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// 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); @@ -917,7 +1102,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations 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)])); + optoLogFile.WriteLine(optoData[BufferIdx(i)] + .ToString(scalFact, optoData[BufferIdx(i - 1)])); } } @@ -937,14 +1123,14 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } - void ReadPulses() - { + void ReadPulses() + { if (channel0 == -1) return; if (channel0 == -2) { - + beginWMState = 0; endWMState = CalculateVolumeByChannels(); wmVolume = Math.Abs(endWMState - beginWMState); @@ -955,21 +1141,21 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations else wmRefPulses = 0; wmTestTime = CalculateTimeByChannels(); - + return; } - - beginWMState = volumeLtr0[channel0]; - endWMState = volumeLtr[channel0]; - wmVolume = Math.Abs(endWMState - beginWMState); - wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5); + + beginWMState = volumeLtr0[channel0]; + endWMState = volumeLtr[channel0]; + wmVolume = Math.Abs(endWMState - beginWMState); + wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5); log.Debug("wmPulses = " + wmPulses + "wmVolume = " + wmVolume + "PulsesPerLtr = " + PulsesPerLtr + ""); if (StateMachine.ControlBoardMain != null) wmRefPulses = StateMachine.ControlBoardMain.RefPulses; else wmRefPulses = 0; - wmTestTime = timestampSec[channel0] - timestampSec0[channel0]; - } + wmTestTime = timestampSec[channel0] - timestampSec0[channel0]; + } private double CalculateTimeByChannels() { @@ -989,7 +1175,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { volumeDelta[iChanel] = this.volumeLtr[iChanel] - this.volumeLtr0[iChanel]; } - + return Average(volumeDelta); } @@ -1055,7 +1241,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations private void CloseOptoSerialPort() { - if (optoSerialPort != null) + if (optoSerialPort != null) { optoSerialPort.Close(); optoSerialPort = null; @@ -1072,11 +1258,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { try { - OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, Handshake.None); + OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, + Handshake.None); } catch (Exception) { } + /// Reset opto-data, etc. optoDataCount = 0; timeFromStart = 0; @@ -1090,7 +1278,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (optoSerialPort != null && optoSerialPort.IsOpen) optoSerialPort.DiscardInBuffer(); - if (flowDirectionDetection != null) flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection + if (flowDirectionDetection != null) + flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection /// Enable opto-data parsing and saving dataStreamState = DataStreamState.ProcessAndSave; @@ -1127,11 +1316,92 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Variables storing the context of serial port data parsing (ReadOptoSerialPort(...)) /// bool synchronized; - bool synchronized2; - string partOfTelegram; - + + bool synchronized2; + string partOfTelegram; + private string _commInterface; + + #region block variables to check results block + + //block varaibles to check results block + private int _completedBlockCount = 0; + private int _resetAfterBlockRepetitions = 3; + private bool _blockStartedWithF = false; + private bool _channel1SeenInBlock = false; + private bool _channel2SeenInBlock = false; + private bool _channel3SeenInBlock = false; + + public int ResetAfterBlockRepetitions + { + get { return _resetAfterBlockRepetitions; } + set { _resetAfterBlockRepetitions = value < 1 ? 1 : value; } + } + + private void ResetBlockState() + { + _blockStartedWithF = false; + _channel1SeenInBlock = false; + _channel2SeenInBlock = false; + _channel3SeenInBlock = false; + } + + private void MarkCalibrationChannelSeen(int channel) + { + if (!_blockStartedWithF) + return; + + switch (channel) + { + case 1: + _channel1SeenInBlock = true; + break; + case 2: + _channel2SeenInBlock = true; + break; + case 3: + _channel3SeenInBlock = true; + break; + } + } + + private bool HasCompleteHBlock() + { + return _blockStartedWithF && + _channel1SeenInBlock && + _channel2SeenInBlock && + _channel3SeenInBlock; + } + + private bool HandleFlowRecordForBlockCompletion() + { + // first @f starts block + if (!_blockStartedWithF) + { + _blockStartedWithF = true; + return false; + } + + // second @f may close block + if (!HasCompleteHBlock()) + { + ResetBlockState(); + _blockStartedWithF = true; + return false; + } + + _completedBlockCount++; + + bool shouldReset = _completedBlockCount >= _resetAfterBlockRepetitions; + + if (shouldReset) + _completedBlockCount = 0; + + ResetBlockState(); + return shouldReset; + } + /// /// Reads opto-datastream via serial port. Invoked from RunDeviceBefore() /// @@ -1151,7 +1421,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { // This will now wait max 3 seconds (ReadTimeout) string line = optoSerialPort.ReadLine(); - ProcessOptoLine(line, optoState); + log.DebugFormat("Read Opto Data Line: {0}", line); + ProcessOptoLine(line, optoState, out bool ResetBuffers); + if (ResetBuffers) + { + ReasetDataBuffer(); + } } } catch (TimeoutException) @@ -1167,27 +1442,41 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } } - - public void ProcessOptoLine(string line, DataStreamState optoState) + #endregion + + public void ProcessOptoLine(string line, DataStreamState optoState, out bool bRestBuffer) { - var encoding = optoSerialPort?.Encoding ?? Encoding.ASCII; + var encoding = optoSerialPort != null ? optoSerialPort.Encoding : Encoding.ASCII; byte[] bytes = encoding.GetBytes(line); log.Debug("ComPort: " + OptoComPortNr + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - StreamingDecoder streamingDecode = new StreamingDecoder(true); + var streamingDecode = new StreamingDecoder(true); streamingDecode.DecodeMsg(line); - CalibrationRecord calibData = streamingDecode.DataCalib; - if (calibData != null && calibData.IsValid) - log.Debug("ComPort: " + OptoComPortNr +"Decoded Calib:" + calibData + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - FlowTestRecord data = streamingDecode.DataFlowTest; - - if(!(data != null && data.IsValid)) + bRestBuffer = false; + + CalibrationRecord data = streamingDecode.DataCalib; + if (data != null && data.IsValid) + { + log.Debug("ComPort: " + OptoComPortNr + "Decoded Calib:" + data + " OPTHO RX ← " + + HexFormatter.ToSerialHex(bytes)); + + MarkCalibrationChannelSeen(data.Channel); + } + + FlowTestRecord dataFlow = streamingDecode.DataFlowTest; + if (dataFlow != null && dataFlow.IsValid) + { + log.Debug("ComPort: " + OptoComPortNr + "Decoded Flow data:" + dataFlow + " OPTHO RX ← " + + HexFormatter.ToSerialHex(bytes)); + + bRestBuffer = HandleFlowRecordForBlockCompletion(); + } + + if (data == null || !data.IsValid) return; - - log.Debug("ComPort: " + OptoComPortNr +"Decoded Flow data:" + data + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - + if (optoState == DataStreamState.ProcessAndSave) { int bufferIx = BufferIdx(optoDataCount); @@ -1198,51 +1487,50 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations optoData[bufferIx].SetFlags(OptoTelegramFlags.SyncError); } - if (data != null) + int iChanel = data.Channel - 1; + if (iChanel >= 0 && iChanel < iChanelsCount) { - int iChanel = 0;//data.Channel - 1; - if (iChanel >= 0 && iChanel < iChanelsCount) - { - optoData[bufferIx].UpdateFromSmart( - data, - optoDataCount, - Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), - ref volumeRawExtLast[iChanel], - ref timestampExtLast[iChanel]); + optoData[bufferIx].UpdateFromSmart( + data, + optoDataCount, + Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), + ref volumeRawExtLast[iChanel], + ref timestampExtLast[iChanel]); - flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); + flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); - OptoTelegramReceived( - optoDataCount, - true, - volumeRawExtLast[iChanel], - timestampExtLast[iChanel], - iChanel); - } + OptoTelegramReceived( + optoDataCount, + true, + volumeRawExtLast[iChanel], + timestampExtLast[iChanel], + iChanel); } optoDataCount++; } else { - if (data != null) + int iChanel = data.Channel - 1; + if (iChanel >= 0 && iChanel < iChanelsCount) { - int iChanel = data.Channel - 1; - if (iChanel >= 0 && iChanel < iChanelsCount) - { - flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); - } + flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); } } } - + void ISmartReader.ResetNfcInterface(bool? nfc_on) { ResetNfcInterface(nfc_on); } - private string _rxBuffer = ""; + /// + /// Reset buffer - opto serial Read Data Buffer + /// + int lastChannelReadOptoData = -1; + private string _rxBuffer = ""; + public string ReadOptoData() { if (optoSerialPort is null) return ""; @@ -1253,20 +1541,33 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations try { string line = optoSerialPort.ReadLine(); - + var encoding = optoSerialPort?.Encoding ?? Encoding.ASCII; byte[] bytes = encoding.GetBytes(line); received = HexFormatter.ToSerialHex(bytes); log.Debug("RX ← " + received); - + try { StreamingDecoder _streamingDecode = new StreamingDecoder(true); _streamingDecode.DecodeMsg(line); CalibrationRecord data = _streamingDecode.DataCalib; - log.Info($"OPTHO {OptoComPortNr} DataCalib Parsed optho data:" + data + " RX ← " + received); + if (data != null && data.IsValid) + { + lastChannelReadOptoData = data.Channel; + log.Info($"OPTHO {OptoComPortNr} DataCalib Parsed optho data:" + data + " RX ← " + received); + } FlowTestRecord dataFlow = _streamingDecode.DataFlowTest; - log.Info($"OPTHO {OptoComPortNr} FLOW Parsed optho data:" + data + " RX ← " + received); + if (dataFlow != null && dataFlow.IsValid) + { + log.Info($"OPTHO {OptoComPortNr} FLOW Parsed optho data:" + data + " RX ← " + received); + if (lastChannelReadOptoData == 3) + { + lastChannelReadOptoData = -1; + ReasetDataBuffer(); + } + } + } catch (Exception ex) { @@ -1322,9 +1623,22 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Error($"ReadOptoData() OPTHO {OptoComPortNr} Read error: {ex.Message}"); } } + return received; } + private void ReasetDataBuffer() + { + if (optoSerialPort != null) + { + optoSerialPort.DiscardInBuffer(); + optoSerialPort.DiscardOutBuffer(); + log.Debug("-- Reaset Data Buffer --"); + return; + } + log.Debug("-- Reaset Data Buffer - no serial port --"); + } + void ISmartReader.SetNfcInterface() { SetNfcInterface(); @@ -1339,7 +1653,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { lock (this) { - if (optoSerialPort== null) return string.Empty; + if (optoSerialPort == null) return string.Empty; try { string line = optoSerialPort.ReadLine(); @@ -1375,7 +1689,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug("ReadOptoData timeout after " + timeoutMs + " ms"); return string.Empty; // timeout case } - + public string ReadOptoDataWithTimeout(int timeoutMs = 5000) { try @@ -1392,7 +1706,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations void OptoTelegramReceived(int currentIx, bool async, double volumeRawExt, double timestampRawExt, int iChanel) - { + { currentTelegramIx = currentIx; lastVolumeRaw[iChanel] = volumeRawExt; @@ -1403,7 +1717,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations channel0 = iChanel; } - if (Double.IsNaN(volumeLtr[iChanel]) && Double.IsNaN(iChanel)) + if (Double.IsNaN(volumeLtr[iChanel]) && Double.IsNaN(volumeLtr0[iChanel])) { volumeLtr[iChanel] = lastVolumeRaw[iChanel]; volumeLtr0[iChanel] = volumeLtr[iChanel]; @@ -1413,7 +1727,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations volumeLtr[iChanel] = lastVolumeRaw[iChanel]; } - if (Double.IsNaN(timestampSec[iChanel])&& Double.IsNaN(timestampSec0[iChanel])) + if (Double.IsNaN(timestampSec[iChanel]) && Double.IsNaN(timestampSec0[iChanel])) { timestampSec[iChanel] = lastTimestamp[iChanel]; timestampSec0[iChanel] = timestampSec[iChanel]; @@ -1432,8 +1746,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public void OnOptoReceived(object sender, OptoReceivedEventArgs args) { if (OptoReceivedHandler == null) return; - try { OptoReceivedHandler(sender, args); } - catch (Exception) { } + try + { + OptoReceivedHandler(sender, args); + } + catch (Exception) + { + } } public event EventHandler OptoReceivedHandler; @@ -1461,9 +1780,10 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations 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 + 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 } } @@ -1522,7 +1842,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// 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) + double VolumeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double scalingFactor, + int samplesCount2 = 0) { log.Debug("-- Get VolumeFromSamples() --"); if (samplesCount2 == 0) @@ -1550,29 +1871,29 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations //TODO BUMI - do result as average from data - usually 5 samples - + if (samplesCount2 < 0) samplesCount2 = 0; if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0; - - + + double 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 sum / (double)(2 * samplesCount2 + 1); //return 0.0000625 * scalingFactor * sum / (double)(2 * samplesCount2 + 1); - + } /// @@ -1590,7 +1911,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations $"-- FAILED TimeFromSamples() - unwrappedIx {unwrappedIx} >= optoDataCount{optoDataCount}--"); return 0; } - + int wrappedIx = BufferIdx(unwrappedIx); if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK && @@ -1600,9 +1921,10 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug($"-- Get TimeFromSamples() - Quit because:{optoData[wrappedIx].Flags}--"); return 0; } + log.Debug($"Valid data TimestampExt: {optoData[wrappedIx].TimestampExt}"); return optoData[wrappedIx].TimestampExt; - + // if (samplesCount2 < 0) samplesCount2 = 0; // if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0; // @@ -1680,8 +2002,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (i >= 0) optoData[BufferIdx(i)].RefFlow = filtered[i % kSize]; } } - - + + /// /// Get index to optoData buffer @@ -1696,7 +2018,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } else { - return IperlHead.StartOptoDataCount + (index - IperlHead.OptoDataBufferSize) % IperlHead.EndOptoDataCount; + return IperlHead.StartOptoDataCount + + (index - IperlHead.OptoDataBufferSize) % IperlHead.EndOptoDataCount; } } @@ -1706,7 +2029,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations writer.Write(CommFailed); writer.Write(ResultCode); writer.Write(PositiveCounting); - + if (ConfigStruct != null) { writer.Write(true); @@ -1781,11 +2104,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (genesisHeadCfg.HeadCommunicationComPortNr == 0) return; SERIAL_Driver _SERIAL_Driver_Head_Config = new SERIAL_Driver(); - _SERIAL_Driver_Head_Config.OpenConnection($"COM{genesisHeadCfg.HeadCommunicationComPortNr}", 9600, 8, Parity.None, StopBits.One); + _SERIAL_Driver_Head_Config.OpenConnection($"COM{genesisHeadCfg.HeadCommunicationComPortNr}", 9600, 8, + Parity.None, StopBits.One); NFCHeadConfig _NFCHead_Config = new NFCHeadConfig(_SERIAL_Driver_Head_Config); - if (nfc_on == null || nfc_on == false) _NFCHead_Config.NFCHeadConfig_SetInterface(false); // set RFID interface - if (nfc_on == null || nfc_on == true ) _NFCHead_Config.NFCHeadConfig_SetInterface(true); // set NFC interface + if (nfc_on == null || nfc_on == false) + _NFCHead_Config.NFCHeadConfig_SetInterface(false); // set RFID interface + if (nfc_on == null || nfc_on == true) _NFCHead_Config.NFCHeadConfig_SetInterface(true); // set NFC interface _SERIAL_Driver_Head_Config.Close(); _SERIAL_Driver_Head_Config.Dispose(); } @@ -1810,8 +2135,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations try { var cmpntEntities = session.QueryOver() - .OrderBy(x => x.ItemNr).Asc - .List(); + .OrderBy(x => x.ItemNr).Asc + .List(); var cmpnt = cmpntEntities.Where(x => x.Name == Name).First(); if (cmpnt != null) { @@ -1826,8 +2151,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations session.SaveOrUpdate(cmpnt); tx.Commit(); log.FatalFormat($"Set CommunicationInterface {Name} to {commInterface.ToString()}"); - } - } + } + } } } catch (Exception ex) @@ -1848,35 +2173,35 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { log.Debug("called DataEntry_ReadSerialNumber()"); if (!string.IsNullOrEmpty(SerialNr)) return SerialNr; - + //need to find serial number SerialNr = await DataEntry_ReadSerialNumberAsync(); - + return SerialNr; } public Task DataEntry_ReadBeginVolume() { log.Debug("called DataEntry_ReadBeginVolumer()"); - + Task readedVolume = DataEntry_BeginVolumeAsync(); - + return readedVolume; } public Task DataEntry_ReadEndVolume() { log.Debug("called DataEntry_ReadBeginVolumer()"); - + Task readedVolume = DataEntry_EndVolumeAsync(); - + return readedVolume; } - - + + public async Task DataEntry_EndVolumeAsync() { - + if (optoSerialPort == null || !optoSerialPort.IsOpen) { StartDataStreamProcessing(); @@ -1886,7 +2211,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return Double.NaN; } } - + return await Task.Run(() => { log.Debug($"Try get End Volume! COM: {this.OptoComPortNr}"); @@ -1898,7 +2223,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations counter++; try { - string readOptoDataWithTimeout = ReadOptoDataWithTimeout(2000); + string readOptoDataWithTimeout = ReadOptoDataWithTimeout(3000); if (!string.IsNullOrEmpty(readOptoDataWithTimeout)) { try @@ -1939,9 +2264,11 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations endWMState += VOL_RANGE_LITERS; volumeLtr[channel0] = endWMState; ReadPulses(); - log.Debug($"Solve roll over! Upgraded endWMState: {endWMState}, beginWMState: {beginWMState}"); + log.Debug( + $"Solve roll over! Upgraded endWMState: {endWMState}, beginWMState: {beginWMState}"); } } + return endWMState; } //} @@ -2030,32 +2357,32 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return await Task.Run(() => { - + log.Debug($"Try get ReadSerialNr! COM: {this.RfidComPortNr}"); if (OptoHeadTest.ReadSerialNr()) { SerialNr = this.ConfigStruct.PCBNumberString; log.Debug("ReadSerialNr successful"); } - + //optoHeadTest.CloseConnection(); - + return SerialNr; }); } - - - private static bool IsValidVolumeRecord(OptoTelegramRaw record) - { - return record != null && - (record.Flags == OptoTelegramFlags.OK || - record.Flags == OptoTelegramFlags.OK_TestStart || - record.Flags == OptoTelegramFlags.OK_TestEnd) && - record.IChannel() >= 0; - } - - private static int chenelsSwichCount = 1; - + + + // private static bool IsValidVolumeRecord(OptoTelegramRaw record) + // { + // return record != null && + // (record.Flags == OptoTelegramFlags.OK || + // record.Flags == OptoTelegramFlags.OK_TestStart || + // record.Flags == OptoTelegramFlags.OK_TestEnd) && + // record.IChannel() >= 0; + // } + + private static int chenelsSwichCount = 3; + private double VolumeFromChannelsAtStart(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { if (unwrappedIx < 0 || unwrappedIx >= optoDataCount) @@ -2085,8 +2412,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return foundChannels > 0 ? sum / foundChannels : 0; } - - private double VolumeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesPerChannel) + + private double VolumeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, + int samplesPerChannel) { if (unwrappedIx < 0 || unwrappedIx >= optoDataCount) return 0; @@ -2126,7 +2454,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return channelCount > 0 ? channelSum / channelCount : 0; } - + private double TimeFromChannelsAtStart(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { if (unwrappedIx < 0 || unwrappedIx >= optoDataCount) @@ -2157,7 +2485,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return foundChannels > 0 ? sum / foundChannels : 0; } - private double TimeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesPerChannel) + private double TimeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, + int samplesPerChannel) { if (unwrappedIx < 0 || unwrappedIx >= optoDataCount) return 0; @@ -2197,8 +2526,36 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return channelCount > 0 ? channelSum / channelCount : 0; } + + private const int ChannelCount = 3; - + private static bool IsValidVolumeRecord(OptoTelegramRaw record) + { + return record != null && + (record.Flags == OptoTelegramFlags.OK || + record.Flags == OptoTelegramFlags.OK_TestStart || + record.Flags == OptoTelegramFlags.OK_TestEnd) && + record.IChannel() >= 0 && + record.IChannel() < ChannelCount; + } + + private static double AverageNullable(double?[] values) + { + double sum = 0; + int count = 0; + + for (int i = 0; i < values.Length; i++) + { + if (values[i].HasValue) + { + sum += values[i].Value; + count++; + } + } + + return count > 0 ? sum / count : 0; + } + private double?[] VolumeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { var result = new double?[chenelsSwichCount]; @@ -2226,8 +2583,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } - - private double?[] VolumeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesPerChannel) + + private double?[] VolumeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, + int samplesPerChannel) { var result = new double?[chenelsSwichCount]; @@ -2263,7 +2621,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } - + private double?[] TimeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { var result = new double?[chenelsSwichCount]; @@ -2291,8 +2649,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } - - private double?[] TimeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesPerChannel) + + private double?[] TimeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, + int samplesPerChannel) { var result = new double?[chenelsSwichCount]; @@ -2329,5 +2688,120 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } + + public double VolumeLtrStartCh1 + { + get + { + if (NoSamples) return 0; + return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[0] ?? 0; + } + } + + public double VolumeLtrStartCh2 + { + get + { + if (NoSamples) return 0; + return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[1] ?? 0; + } + } + + public double VolumeLtrStartCh3 + { + get + { + if (NoSamples) return 0; + return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[2] ?? 0; + } + } + + public double VolumeLtrEndCh1 + { + get + { + if (NoSamples) return 0; + return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[0] ?? 0; + } + } + + public double VolumeLtrEndCh2 + { + get + { + if (NoSamples) return 0; + return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[1] ?? 0; + } + } + + public double VolumeLtrEndCh3 + { + get + { + if (NoSamples) return 0; + return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[2] ?? 0; + } + } + + public double TimestampSecStartCh1 + { + get + { + if (NoSamples) return 0; + return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[0] ?? 0; + } + } + + public double TimestampSecStartCh2 + { + get + { + if (NoSamples) return 0; + return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[1] ?? 0; + } + } + + public double TimestampSecStartCh3 + { + get + { + if (NoSamples) return 0; + return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[2] ?? 0; + } + } + + public double TimestampSecEndCh1 + { + get + { + if (NoSamples) return 0; + return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[0] ?? 0; + } + } + + public double TimestampSecEndCh2 + { + get + { + if (NoSamples) return 0; + return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[1] ?? 0; + } + } + + public double TimestampSecEndCh3 + { + get + { + if (NoSamples) return 0; + return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, + 2 * StartEndFilterSamplesCount2 + 1)[2] ?? 0; + } + } + } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/FakeSerialDriver.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/FakeSerialDriver.cs index f8b9eaba0..ace22054a 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/FakeSerialDriver.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/FakeSerialDriver.cs @@ -16,10 +16,11 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations public int CloseCalls { get; private set; } public int DiscardInCalls { get; private set; } public int DiscardOutCalls { get; private set; } + public Encoding Encoding { get; set; } = Encoding.ASCII; - public int BytesToRead => _lines.Count > 0 ? 1 : 0; + public int BytesToRead => _isOpen && _lines.Count > 0 ? 1 : 0; public int BytesToWrite => 0; public void EnqueueLine(string line) => _lines.Enqueue(line); diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs index 0d1626d0c..7c9302682 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs @@ -2,7 +2,6 @@ using System; using System.Reflection; using Common; using Microsoft.VisualStudio.TestTools.UnitTesting; -using TBF.Rig.Generic; using TBF.Rig.RegisterReaders.GenesisRegReader; using TBF.Rig.RegisterReaders.GenesisRegReader.common; using TBF.Rig.RegisterReaders.GenesisRegReader.implementations; @@ -14,6 +13,12 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations { private const int ChannelCount = 3; + [TestInitialize] + public void TestInitialize() + { + SetPrivateStaticField(typeof(GenesisSmartReader), "chenelsSwichCount", ChannelCount); + } + private static void SetPrivateField(object target, string fieldName, object value) { var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); @@ -32,13 +37,13 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations return (T)field.GetValue(target); } - private static void InvokePrivateMethod(object target, string methodName) + private static void SetPrivateStaticField(Type type, string fieldName, object value) { - var method = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - if (method == null) - throw new MissingMethodException(target.GetType().FullName, methodName); + var field = type.GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic); + if (field == null) + throw new MissingFieldException(type.FullName, fieldName); - method.Invoke(target, null); + field.SetValue(null, value); } private static OptoTelegramRaw CreateOptoRecord(int channel, double volumeRawExt, double timestampExt, int counter) @@ -53,8 +58,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations DateTime = DateTime.Now }; } - - + private static GenesisCfg CreateCfg() { var cfg = new GenesisCfg(null); @@ -75,21 +79,27 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations return cfg; } - - [TestMethod] - public void SimulatedInterleaved20x3ChannelStream_ShouldComputeStartAndEndVolumeAndTimeCorrectly() - { - var fake = new FakeSerialDriver(); - var reader = new GenesisSmartReader(CreateCfg(), () => fake); - SetPrivateField(reader, "volumeRawExtLast", new double[3]); - SetPrivateField(reader, "timestampExtLast", new double[3]); - SetPrivateField(reader, "flowDirectionDetection", new FlowDirectionDetection()); + private static GenesisSmartReader CreateReaderWithOptoBuffer() + { + var reader = new GenesisSmartReader(CreateCfg(), null); + SetPrivateField(reader, "optoData", new OptoTelegramRaw[GenesisSmartReader.OptoDataBufferSize]); var optoData = GetPrivateField(reader, "optoData"); for (int i = 0; i < optoData.Length; i++) + { optoData[i] = new OptoTelegramRaw(); + } + + return reader; + } + + [TestMethod] + public void SimulatedInterleaved20x3ChannelStream_ShouldComputeStartAndEndVolumeAndTimeCorrectly() + { + var reader = CreateReaderWithOptoBuffer(); + var optoData = GetPrivateField(reader, "optoData"); int index = 0; for (int g = 0; g < 20; g++) @@ -114,60 +124,44 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.AreEqual(1.0, reader.TimestampSecStart, 1e-9); Assert.AreEqual(18.0, reader.TimestampSecEnd, 1e-9); } - + [TestMethod] public void VolumeLtrEnd_ShouldIgnoreInvalidTrailingRecords_AndUsePreviousValidPerChannelSamples() { - var reader = new GenesisSmartReader(CreateCfg(), null); - - SetPrivateField(reader, "optoData", new OptoTelegramRaw[GenesisSmartReader.OptoDataBufferSize]); - + var reader = CreateReaderWithOptoBuffer(); var optoData = GetPrivateField(reader, "optoData"); - for (int i = 0; i < optoData.Length; i++) - optoData[i] = new OptoTelegramRaw(); int index = 0; - // Build 10 groups of valid 3-channel data for (int g = 0; g < 10; g++) { - optoData[index++] = CreateOptoRecord(0, 100 + g, 1 + g, index); - optoData[index++] = CreateOptoRecord(1, 200 + g, 1 + g, index); - optoData[index++] = CreateOptoRecord(2, 300 + g, 1 + g, index); + optoData[index] = CreateOptoRecord(0, 100 + g, 1 + g, index); + index++; + + optoData[index] = CreateOptoRecord(1, 200 + g, 1 + g, index); + index++; + + optoData[index] = CreateOptoRecord(2, 300 + g, 1 + g, index); + index++; } - // Corrupt the last two channel-1 records - optoData[25].Flags = OptoTelegramFlags.InvalidTelegram; // group 8, channel 1 - optoData[28].Flags = OptoTelegramFlags.InvalidTelegram; // group 9, channel 1 + // group 8 ch1 is index 25, group 9 ch1 is index 28 + optoData[25].Flags = OptoTelegramFlags.InvalidTelegram; + optoData[28].Flags = OptoTelegramFlags.InvalidTelegram; SetPrivateField(reader, "optoDataCount", index); reader.TestStartTelegramIx = 2; reader.TestEndTelegramIx = index - 1; - // Start still comes from first full valid 3-channel snapshot: - // (100 + 200 + 300) / 3 = 200 Assert.AreEqual(200.0, reader.VolumeLtrStart, 1e-9); - - // End with last 5 valid per channel: - // ch0 valid last 5: 105,106,107,108,109 => avg 107 - // ch1 valid last 5: 203,204,205,206,207 => avg 205 - // because 208 and 209 were invalid - // ch2 valid last 5: 305,306,307,308,309 => avg 307 - // total avg = (107 + 205 + 307) / 3 = 206.3333333333... Assert.AreEqual(206.33333333333334, reader.VolumeLtrEnd, 1e-9); } - - + [TestMethod] public void VolumeLtrStart_And_End_ShouldBeZero_WhenAllSamplesAreInvalid() { - var reader = new GenesisSmartReader(CreateCfg(), null); - - SetPrivateField(reader, "optoData", new OptoTelegramRaw[GenesisSmartReader.OptoDataBufferSize]); - + var reader = CreateReaderWithOptoBuffer(); var optoData = GetPrivateField(reader, "optoData"); - for (int i = 0; i < optoData.Length; i++) - optoData[i] = new OptoTelegramRaw(); for (int i = 0; i < 9; i++) { @@ -187,5 +181,50 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.AreEqual(0.0, reader.VolumeLtrStart, 1e-9); Assert.AreEqual(0.0, reader.VolumeLtrEnd, 1e-9); } + + [TestMethod] + public void SimulatedInterleaved20x3ChannelStream_ShouldComputePerChannelStartAndEndValues() + { + var reader = CreateReaderWithOptoBuffer(); + var optoData = GetPrivateField(reader, "optoData"); + + int index = 0; + for (int g = 0; g < 20; g++) + { + optoData[index] = CreateOptoRecord(0, 100 + g, 1 + g, index); + index++; + + optoData[index] = CreateOptoRecord(1, 200 + g, 1 + g, index); + index++; + + optoData[index] = CreateOptoRecord(2, 300 + g, 1 + g, index); + index++; + } + + SetPrivateField(reader, "optoDataCount", index); + reader.TestStartTelegramIx = 2; + reader.TestEndTelegramIx = index - 1; + + Assert.AreEqual(100.0, reader.VolumeLtrStartCh1, 1e-9); + Assert.AreEqual(200.0, reader.VolumeLtrStartCh2, 1e-9); + Assert.AreEqual(300.0, reader.VolumeLtrStartCh3, 1e-9); + + Assert.AreEqual(117.0, reader.VolumeLtrEndCh1, 1e-9); + Assert.AreEqual(217.0, reader.VolumeLtrEndCh2, 1e-9); + Assert.AreEqual(317.0, reader.VolumeLtrEndCh3, 1e-9); + + Assert.AreEqual(1.0, reader.TimestampSecStartCh1, 1e-9); + Assert.AreEqual(1.0, reader.TimestampSecStartCh2, 1e-9); + Assert.AreEqual(1.0, reader.TimestampSecStartCh3, 1e-9); + + Assert.AreEqual(18.0, reader.TimestampSecEndCh1, 1e-9); + Assert.AreEqual(18.0, reader.TimestampSecEndCh2, 1e-9); + Assert.AreEqual(18.0, reader.TimestampSecEndCh3, 1e-9); + + Assert.AreEqual(200.0, reader.VolumeLtrStart, 1e-9); + Assert.AreEqual(217.0, reader.VolumeLtrEnd, 1e-9); + Assert.AreEqual(1.0, reader.TimestampSecStart, 1e-9); + Assert.AreEqual(18.0, reader.TimestampSecEnd, 1e-9); + } } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs index 474accf0d..448152e78 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs @@ -105,24 +105,33 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations } [TestMethod] - public void ProcessOptoLine_ShouldIncreaseOptoDataCount() + public void ProcessOptoLine_ShouldInsertMultipleTelegrams_WithCorrectChannels() { var fake = new FakeSerialDriver(); - var reader = new GenesisSmartReader(CreateCfg(), () => fake); - - reader.Initialize(); - InitializeThreeChannelState(reader); - - var line = - "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331"; - - reader.optoSerialPort = fake; fake.Open(); - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; - int optoDataCount = GetPrivateField(reader, "optoDataCount"); - Assert.AreEqual(1, optoDataCount); + bool reset; + + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + var optoData = GetPrivateField(reader, "optoData"); + var optoDataCount = GetPrivateField(reader, "optoDataCount"); + + Assert.AreEqual(3, optoDataCount); + Assert.AreEqual(0, optoData[0].IChannel()); + Assert.AreEqual(1, optoData[1].IChannel()); + Assert.AreEqual(2, optoData[2].IChannel()); } [DataTestMethod] @@ -146,7 +155,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.optoSerialPort = fake; InitializeThreeChannelState(reader); - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); var volumeRawExtLast = GetPrivateField(reader, "volumeRawExtLast"); var timestampExtLast = GetPrivateField(reader, "timestampExtLast"); @@ -204,7 +213,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var line = "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD"; - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); int currentTelegramIx = GetPrivateField(reader, "currentTelegramIx"); Assert.AreEqual(0, currentTelegramIx); @@ -223,7 +232,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.Initialize(); reader.optoSerialPort = fake; - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); var volumeRawExtLast = GetPrivateField(reader, "volumeRawExtLast"); var timestampExtLast = GetPrivateField(reader, "timestampExtLast"); @@ -266,28 +275,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations "Inserted telegram TimestampExt should match updated channel cache."); } - [TestMethod] - public void ProcessOptoLine_ShouldInsertMultipleTelegrams_WithCorrectChannels() - { - var fake = new FakeSerialDriver(); - fake.Open(); - - var reader = new GenesisSmartReader(CreateCfg(), () => fake); - reader.Initialize(); - reader.optoSerialPort = fake; - - reader.ProcessOptoLine( "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", DataStreamState.ProcessAndSave); - reader.ProcessOptoLine( "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", DataStreamState.ProcessAndSave); - reader.ProcessOptoLine( "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", DataStreamState.ProcessAndSave); - - var optoData = GetPrivateField(reader, "optoData"); - var optoDataCount = GetPrivateField(reader, "optoDataCount"); - - Assert.AreEqual(3, optoDataCount); - Assert.AreEqual(0, optoData[0].IChannel()); - Assert.AreEqual(1, optoData[1].IChannel()); - Assert.AreEqual(2, optoData[2].IChannel()); - } + [TestMethod] public void VolumeLtrStart_And_VolumeLtrEnd_ShouldUsePerChannelAverages() @@ -358,5 +346,214 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations // ch0 avg = 12, ch1 avg = 22 => total avg = 17 Assert.AreEqual(17.0, reader.VolumeLtrEnd, 1e-9); } + + [TestMethod] + public void ProcessOptoLine_Block_f_h1_h2_h3_f_ShouldResetBuffersOnlyAfterLastF() + { + var fake = new FakeSerialDriver(); + fake.Open(); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; + + bool reset; + + // first @f + reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen on the first @f."); + + // @h 1 + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h 1."); + + // @h 2 + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h 2."); + + // @h 3 + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen immediately after @h 3."); + + // trailing @f + reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); + Assert.IsTrue(reset, "Reset must happen after trailing @f that closes the h1/h2/h3 block."); + } + + [TestMethod] + public void ReadOptoData_FullBlock_ShouldDiscardBuffersAfterClosingF() + { + var fake = new FakeSerialDriver(); + + fake.EnqueueLine("@f AA754B 4D0CEE78 5D89"); + fake.EnqueueLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331"); + fake.EnqueueLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD"); + fake.EnqueueLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E"); + fake.EnqueueLine("@f AA7C01 4D0CFE76 B08F"); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + + fake.Open(); // important + reader.optoSerialPort = fake; // assign opened fake + reader.ResetAfterBlockRepetitions = 1; + + var readMethod = typeof(GenesisSmartReader).GetMethod( + "ReadOptoData", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(DataStreamState) }, + null); + + Assert.IsNotNull(readMethod); + + for (int i = 0; i < 5; i++) + { + readMethod.Invoke(reader, new object[] { DataStreamState.ProcessAndSave }); + } + + Assert.AreEqual(1, fake.DiscardInCalls, "Input buffer should be discarded once after completed block."); + Assert.AreEqual(1, fake.DiscardOutCalls, "Output buffer should be discarded once after completed block."); + } + + [TestMethod] + public void ProcessOptoLine_LastF_AfterH3_ShouldRequestBufferReset() + { + var fake = new FakeSerialDriver(); + fake.Open(); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; + reader.ResetAfterBlockRepetitions = 1; + + bool reset; + + reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Opening @f must not reset buffers."); + + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); + Assert.IsTrue(reset, "Closing @f after @h 3 must request reset."); + } + + [TestMethod] + public void ProcessOptoLine_LastF_AfterH3_ShouldNotRequestReset_WhenRepetitionCountIsGreaterThanOne() + { + var fake = new FakeSerialDriver(); + fake.Open(); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; + reader.ResetAfterBlockRepetitions = 3; + + bool reset; + + reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset); + + reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after the first completed block when repetition count is 3."); + } + + [TestMethod] + public void ProcessOptoLine_ShouldResetOnlyAfterThirdCompletedBlock() + { + var fake = new FakeSerialDriver(); + fake.Open(); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; + reader.ResetAfterBlockRepetitions = 3; + + bool reset; + + for (int repetition = 1; repetition <= 3; repetition++) + { + reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen on opening @f, repetition " + repetition); + + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h1, repetition " + repetition); + + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h2, repetition " + repetition); + + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h3, repetition " + repetition); + + reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); + + if (repetition < 3) + Assert.IsFalse(reset, "Reset must not happen before third full block."); + else + Assert.IsTrue(reset, "Reset must happen on third full block."); + } + } + + [TestMethod] + public void ProcessOptoLine_ShouldResetOnlyAfterThirdCompletedBlock_2() + { + var fake = new FakeSerialDriver(); + fake.Open(); + + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + reader.Initialize(); + reader.optoSerialPort = fake; + reader.ResetAfterBlockRepetitions = 5; + + bool reset; + + for (int repetition = 1; repetition <= 5; repetition++) + { + reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen on opening @f, repetition " + repetition); + + reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h1, repetition " + repetition); + + reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h2, repetition " + repetition); + + reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", + DataStreamState.ProcessAndSave, out reset); + Assert.IsFalse(reset, "Reset must not happen after @h3, repetition " + repetition); + + reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); + + if (repetition < 5) + Assert.IsFalse(reset, "Reset must not happen before third full block."); + else + Assert.IsTrue(reset, "Reset must happen on third full block."); + } + } + } } \ No newline at end of file From 836ba5a41ff09f78071b745377be3f75eeaa879a Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Fri, 27 Mar 2026 13:18:37 +0100 Subject: [PATCH 2/5] Refactor `GenesisSmartReader` processing logic and add advanced multi-channel volume and timestamp handling: - Introduce `TimeDeltaPerChannel` and `GroupRecordsPerChannel` for calculating time and volume deltas across channels. - Add recalibration logic to synchronize volume and timestamp data with channel-specific maxima. - Fix `ResetDataBuffer` implementation and replace redundant `ReasetDataBuffer` calls. - Update handling of flow markers and block completion logic for improved buffer management. - Add `Copy` method to `OptoTelegramRaw` for efficient cloning and recalculation. - Bump `AssemblyVersion` and `AssemblyFileVersion` to `3.9.3019.1`. --- TBF/Properties/AssemblyInfo.cs | 4 +- .../common/OptoTelegramRaw.cs | 18 ++ .../implementations/GenesisSmartReader.cs | 271 +++++++++++++++--- 3 files changed, 245 insertions(+), 48 deletions(-) diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index 6f85795d5..3533ba3a4 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("3.9.3016.1")] -[assembly: AssemblyFileVersion("3.9.3016.1")] +[assembly: AssemblyVersion("3.9.3019.1")] +[assembly: AssemblyFileVersion("3.9.3019.1")] diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs index f55652631..69c0d7e64 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs @@ -349,5 +349,23 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common Label()); } } + + public void Copy(OptoTelegramRaw optoTelegramRaw) + { + this.Flags = Flags; + this.DateTime = DateTime; + this.RefFlow = RefFlow; + this.Counter = Counter; + this.EmfRaw = EmfRaw; + this.MagneticFieldRaw = MagneticFieldRaw; + this.FlowRaw = FlowRaw; + this.VolumeRaw = VolumeRaw; + this.VolumeRawExt = VolumeRawExt; + this.Impedance = Impedance; + this.Timestamp = Timestamp; + this.TimestampExt = TimestampExt; + this.CheckSum = CheckSum; + this.iChannel = iChannel; + } } } diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs index 21aab9931..4ac2be485 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Linq; @@ -705,6 +706,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations Handshake.None); CloseOptoSerialPort(); log.FatalFormat($"{Name} initialized: {this}"); + ResetDataBuffer(); } catch (Exception ex) { @@ -748,6 +750,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations dataStreamState = DataStreamState.Flush; currentFlowDir = InitFlowDir; + + ResetBlockCountersAndState(); } public void SaveMark(object mark) @@ -884,8 +888,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { lock (this) { - ReasetDataBuffer(); Clear(); + ResetBlockCountersAndState(); ReadPulses(); StartDataStreamProcessing(); } @@ -938,7 +942,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } DataStreamPostProcessing(); - ReasetDataBuffer(); + ResetDataBuffer(); // TODO: Enable when calculations completed // @@ -1283,6 +1287,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Enable opto-data parsing and saving dataStreamState = DataStreamState.ProcessAndSave; + ResetDataBuffer(); } public void SetCommunicationInterface(string commInterface) @@ -1324,21 +1329,22 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations #region block variables to check results block - + //block varaibles to check results block private int _completedBlockCount = 0; - private int _resetAfterBlockRepetitions = 3; + private int _resetAfterBlockRepetitions = 1; + private bool _blockStartedWithF = false; private bool _channel1SeenInBlock = false; private bool _channel2SeenInBlock = false; private bool _channel3SeenInBlock = false; - + public int ResetAfterBlockRepetitions { get { return _resetAfterBlockRepetitions; } set { _resetAfterBlockRepetitions = value < 1 ? 1 : value; } } - + private void ResetBlockState() { _blockStartedWithF = false; @@ -1346,7 +1352,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations _channel2SeenInBlock = false; _channel3SeenInBlock = false; } - + + private void ResetBlockCountersAndState() + { + _completedBlockCount = 0; + ResetBlockState(); + } + private void MarkCalibrationChannelSeen(int channel) { if (!_blockStartedWithF) @@ -1365,7 +1377,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations break; } } - + private bool HasCompleteHBlock() { return _blockStartedWithF && @@ -1373,28 +1385,37 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations _channel2SeenInBlock && _channel3SeenInBlock; } - - private bool HandleFlowRecordForBlockCompletion() + + /// + /// Handles @f marker. First @f starts block, second @f closes it if h1/h2/h3 were seen. + /// Returns true when buffers should be reset. + /// + private bool HandleFlowMarker() { // first @f starts block if (!_blockStartedWithF) { _blockStartedWithF = true; + _channel1SeenInBlock = false; + _channel2SeenInBlock = false; + _channel3SeenInBlock = false; return false; } - // second @f may close block + // second @f closes block only if all H telegrams were seen if (!HasCompleteHBlock()) { - ResetBlockState(); + // start a fresh block from this @f _blockStartedWithF = true; + _channel1SeenInBlock = false; + _channel2SeenInBlock = false; + _channel3SeenInBlock = false; return false; } _completedBlockCount++; bool shouldReset = _completedBlockCount >= _resetAfterBlockRepetitions; - if (shouldReset) _completedBlockCount = 0; @@ -1402,6 +1423,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return shouldReset; } + #endregion + + /// /// Reads opto-datastream via serial port. Invoked from RunDeviceBefore() /// @@ -1422,10 +1446,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations // This will now wait max 3 seconds (ReadTimeout) string line = optoSerialPort.ReadLine(); log.DebugFormat("Read Opto Data Line: {0}", line); - ProcessOptoLine(line, optoState, out bool ResetBuffers); - if (ResetBuffers) + bool resetBuffer; + ProcessOptoLine(line, optoState, out resetBuffer); + + if (resetBuffer) { - ReasetDataBuffer(); + ResetDataBuffer(); } } } @@ -1442,39 +1468,38 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } } - - #endregion - public void ProcessOptoLine(string line, DataStreamState optoState, out bool bRestBuffer) + + public void ProcessOptoLine(string line, DataStreamState optoState, out bool resetBuffer) { var encoding = optoSerialPort != null ? optoSerialPort.Encoding : Encoding.ASCII; byte[] bytes = encoding.GetBytes(line); log.Debug("ComPort: " + OptoComPortNr + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); + resetBuffer = false; + var streamingDecode = new StreamingDecoder(true); streamingDecode.DecodeMsg(line); - bRestBuffer = false; + CalibrationRecord calibData = streamingDecode.DataCalib; - CalibrationRecord data = streamingDecode.DataCalib; - if (data != null && data.IsValid) + if (streamingDecode.DataFlowTest != null && streamingDecode.DataFlowTest.IsValid) { - log.Debug("ComPort: " + OptoComPortNr + "Decoded Calib:" + data + " OPTHO RX ← " + + resetBuffer = HandleFlowMarker(); + log.Debug("ComPort: " + OptoComPortNr + " Decoded Flow data: " + streamingDecode.DataFlowTest + + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - - MarkCalibrationChannelSeen(data.Channel); } - FlowTestRecord dataFlow = streamingDecode.DataFlowTest; - if (dataFlow != null && dataFlow.IsValid) + if (calibData != null && calibData.IsValid) { - log.Debug("ComPort: " + OptoComPortNr + "Decoded Flow data:" + dataFlow + " OPTHO RX ← " + + log.Debug("ComPort: " + OptoComPortNr + " Decoded Calib: " + calibData + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - bRestBuffer = HandleFlowRecordForBlockCompletion(); + MarkCalibrationChannelSeen(calibData.Channel); } - if (data == null || !data.IsValid) + if (calibData == null || !calibData.IsValid) return; if (optoState == DataStreamState.ProcessAndSave) @@ -1487,11 +1512,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations optoData[bufferIx].SetFlags(OptoTelegramFlags.SyncError); } - int iChanel = data.Channel - 1; + int iChanel = calibData.Channel - 1; if (iChanel >= 0 && iChanel < iChanelsCount) { + log.Debug("ComPort: " + OptoComPortNr + " Channel: " + iChanel); optoData[bufferIx].UpdateFromSmart( - data, + calibData, optoDataCount, Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), ref volumeRawExtLast[iChanel], @@ -1511,7 +1537,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } else { - int iChanel = data.Channel - 1; + int iChanel = calibData.Channel - 1; if (iChanel >= 0 && iChanel < iChanelsCount) { flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); @@ -1519,6 +1545,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } + void ISmartReader.ResetNfcInterface(bool? nfc_on) { ResetNfcInterface(nfc_on); @@ -1528,7 +1555,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Reset buffer - opto serial Read Data Buffer /// int lastChannelReadOptoData = -1; - + private string _rxBuffer = ""; public string ReadOptoData() @@ -1554,20 +1581,20 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations CalibrationRecord data = _streamingDecode.DataCalib; if (data != null && data.IsValid) { - lastChannelReadOptoData = data.Channel; - log.Info($"OPTHO {OptoComPortNr} DataCalib Parsed optho data:" + data + " RX ← " + received); + log.Info($"OPTHO {OptoComPortNr} DataCalib Parsed opto data: " + data + " RX ← " + + received); + MarkCalibrationChannelSeen(data.Channel); } + FlowTestRecord dataFlow = _streamingDecode.DataFlowTest; if (dataFlow != null && dataFlow.IsValid) { - log.Info($"OPTHO {OptoComPortNr} FLOW Parsed optho data:" + data + " RX ← " + received); - if (lastChannelReadOptoData == 3) - { - lastChannelReadOptoData = -1; - ReasetDataBuffer(); - } + log.Info($"OPTHO {OptoComPortNr} FLOW Parsed opto data: " + dataFlow + " RX ← " + received); + + if (HandleFlowMarker()) + ResetDataBuffer(); } - + } catch (Exception ex) { @@ -1627,7 +1654,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return received; } - private void ReasetDataBuffer() + private void ResetDataBuffer() { if (optoSerialPort != null) { @@ -1636,6 +1663,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug("-- Reaset Data Buffer --"); return; } + log.Debug("-- Reaset Data Buffer - no serial port --"); } @@ -2527,8 +2555,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return channelCount > 0 ? channelSum / channelCount : 0; } + //channels count - do not change! private const int ChannelCount = 3; - + private static bool IsValidVolumeRecord(OptoTelegramRaw record) { return record != null && @@ -2555,6 +2584,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return count > 0 ? sum / count : 0; } + + private double?[] VolumeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { @@ -2622,6 +2653,155 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } + + private (int channel, double? delta, double? deltaVolume)[] TimeDeltaPerChannel( + OptoTelegramRaw[][] grouped) + { + var result = new (int channel, double? delta, double? deltaVolume)[ChannelCount]; + + for (int ch = 0; ch < ChannelCount; ch++) + { + var records = grouped[ch]; + + if (records == null || records.Length < 2) + { + result[ch] = (ch, null, null); + continue; + } + + double minTime = double.MaxValue; + double maxTime = double.MinValue; + + double minVol = double.MaxValue; + double maxVol = double.MinValue; + + foreach (var r in records) + { + double t = r.TimestampExt; // TimestampExt is in seconds + double v = r.VolumeRawExt; // VolumeRawExt is in mL + + if (t < minTime) minTime = t; + if (t > maxTime) maxTime = t; + + if (v < minVol) minVol = v; + if (v > maxVol) maxVol = v; + } + + result[ch] = (ch, maxTime - minTime, maxVol - minVol); + } + + return result; + } + + + private OptoTelegramRaw[][] GroupRecordsPerChannel( + OptoTelegramRaw[] data, + int startIx, + int endIx, + int dataCount) + { + var result = new List[ChannelCount]; + + // init lists + for (int ch = 0; ch < ChannelCount; ch++) + result[ch] = new List(); + + if (startIx < 0 || endIx < 0 || startIx >= dataCount || endIx >= dataCount) + return result.Select(l => l.ToArray()).ToArray(); + + // ensure forward direction + if (startIx > endIx) + { + var tmp = startIx; + startIx = endIx; + endIx = tmp; + } + + for (int i = startIx; i <= endIx; i++) + { + int wrappedIx = BufferIdx(i); + var record = data[wrappedIx]; + + if (!IsValidVolumeRecord(record)) + continue; + + int ch = record.IChannel(); + + if (ch < 0 || ch >= ChannelCount) + continue; + + result[ch].Add(record); + } + + // convert List[] → array[] + return result.Select(l => l.ToArray()).ToArray(); + } + + private void RecalculateVolumeAndTimeDeltaPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double?[] time ) + { + + OptoTelegramRaw[][] recordByChannel = GroupRecordsPerChannel( + optoData, + 0, + optoDataCount - 2, + optoDataCount); + + (int channel, double? deltaTime, double? deltaVolume)[] timeDelta = TimeDeltaPerChannel(recordByChannel); + + //Time max delta value + var result = timeDelta + .Where(x => x.deltaTime.HasValue) + .OrderByDescending(x => x.deltaTime) + .FirstOrDefault(); + + int maxChannel = result.channel; + double? maxValueTime = result.deltaTime; + + // optional safety if all null + if (!result.deltaTime.HasValue) + { + maxChannel = -1; + maxValueTime = null; + } + + //Recalculate volume + var recalculatedVariablesByChannel = new OptoTelegramRaw[ChannelCount][]; + for (int channel = 0; channel < ChannelCount; channel++) + { + recalculatedVariablesByChannel[channel] = new OptoTelegramRaw[2]; + } // [per chanel] [2 - start val, end val] + + for (int channel = 0; channel < ChannelCount; channel++) + { + recalculatedVariablesByChannel[channel][0] = new OptoTelegramRaw(); //start val + recalculatedVariablesByChannel[channel][0].Copy( recordByChannel[channel][0]); + recalculatedVariablesByChannel[channel][1] = new OptoTelegramRaw(); //end val + recalculatedVariablesByChannel[channel][1].Copy( recordByChannel[channel][1]); + + //set the same time + recalculatedVariablesByChannel[channel][0].TimestampExt = recordByChannel[maxChannel][0].TimestampExt; + recalculatedVariablesByChannel[channel][1].TimestampExt = recordByChannel[maxChannel][1].TimestampExt; + + //start, end val - end in case if valid data are not accessible + recalculatedVariablesByChannel[channel][0].VolumeRawExt = recordByChannel[maxChannel][0].VolumeRawExt; + recalculatedVariablesByChannel[channel][1].VolumeRawExt = recordByChannel[maxChannel][1].VolumeRawExt; + + } + + + //recalculation volumes to max Time + for (int channel = 0; channel < ChannelCount; channel++) + { + double? timeKoef = maxValueTime / timeDelta[channel].deltaTime; + double? deltaVolKoef = timeDelta[channel].deltaVolume * timeKoef; + + if (deltaVolKoef.HasValue) + { + recalculatedVariablesByChannel[channel][1].VolumeRawExt = deltaVolKoef.Value; + } + } + } + private double?[] TimeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { var result = new double?[chenelsSwichCount]; @@ -2802,6 +2982,5 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations 2 * StartEndFilterSamplesCount2 + 1)[2] ?? 0; } } - } } \ No newline at end of file From a34740e8387b70ae496b5c76f6436372e9ee66dd Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Fri, 27 Mar 2026 13:19:10 +0100 Subject: [PATCH 3/5] Update `GenesisSmartReaderTest` to handle `DiscardOutCalls` and configure block repetition resets: - Modify tests to validate `DiscardOutCalls` alongside `DiscardInCalls`. - Add support for configuring `ResetAfterBlockRepetitions` in test initialization. --- .../implementations/GenesisSmartReaderTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs index 448152e78..6db6517e9 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs @@ -65,7 +65,8 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.IsTrue(fake.IsOpen); Assert.IsTrue(fake.OpenCalls >= 1); - Assert.AreEqual(1, fake.DiscardInCalls); + Assert.AreEqual(2, fake.DiscardInCalls); + Assert.AreEqual(1, fake.DiscardOutCalls); } [TestMethod] @@ -356,6 +357,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var reader = new GenesisSmartReader(CreateCfg(), () => fake); reader.Initialize(); reader.optoSerialPort = fake; + reader.ResetAfterBlockRepetitions = 1; bool reset; From d5c8fd1c8f32ea5e6f492cb4b5dad80e9f63dc7f Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Fri, 27 Mar 2026 15:12:43 +0100 Subject: [PATCH 4/5] Refactor `GenesisSmartReader` with enhanced channel-specific volume and timestamp processing: - Replace `AverageNullable` with `AverageCachedTime` and introduce `GetCachedVolume`/`GetCachedTime` helpers. - Add recalibration logic with `_recalculatedStartEndByChannel` and `_rawStartEndByChannel`. - Implement `PrepareCalculatedChannelData` for refined multi-channel synchronization. - Add unit tests (`GenesisSmartReaderChannelTests`) for improved coverage and validation of new methods. - Update `OptoTelegramRaw.Copy` to fix field assignment issues. --- .../common/OptoTelegramRaw.cs | 28 +- .../implementations/GenesisSmartReader.cs | 340 ++++++------ .../GenesisSmartReaderChannelTests.cs | 507 ++++++++++++++++++ TBFTests/TBFTests.csproj | 1 + 4 files changed, 695 insertions(+), 181 deletions(-) create mode 100644 TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelTests.cs diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs index 69c0d7e64..54b952346 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs @@ -352,20 +352,20 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common public void Copy(OptoTelegramRaw optoTelegramRaw) { - this.Flags = Flags; - this.DateTime = DateTime; - this.RefFlow = RefFlow; - this.Counter = Counter; - this.EmfRaw = EmfRaw; - this.MagneticFieldRaw = MagneticFieldRaw; - this.FlowRaw = FlowRaw; - this.VolumeRaw = VolumeRaw; - this.VolumeRawExt = VolumeRawExt; - this.Impedance = Impedance; - this.Timestamp = Timestamp; - this.TimestampExt = TimestampExt; - this.CheckSum = CheckSum; - this.iChannel = iChannel; + this.Flags = optoTelegramRaw.Flags; + this.DateTime = optoTelegramRaw.DateTime; + this.RefFlow = optoTelegramRaw.RefFlow; + this.Counter = optoTelegramRaw.Counter; + this.EmfRaw = optoTelegramRaw.EmfRaw; + this.MagneticFieldRaw = optoTelegramRaw.MagneticFieldRaw; + this.FlowRaw = optoTelegramRaw.FlowRaw; + this.VolumeRaw = optoTelegramRaw.VolumeRaw; + this.VolumeRawExt = optoTelegramRaw.VolumeRawExt; + this.Impedance = optoTelegramRaw.Impedance; + this.Timestamp = optoTelegramRaw.Timestamp; + this.TimestampExt = optoTelegramRaw.TimestampExt; + this.CheckSum = optoTelegramRaw.CheckSum; + this.iChannel = optoTelegramRaw.iChannel; } } } diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs index 4ac2be485..fbeeddcd5 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs @@ -551,11 +551,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (TestStartTelegramIx < 0 || TestStartTelegramIx >= optoDataCount) return 0; - return AverageNullable(TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)); + return AverageCachedTime(_recalculatedStartEndByChannel, 0); } } /// Test end time for metrology in seconds + public double TimestampSecEnd { get @@ -563,12 +564,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (TestEndTelegramIx < 0 || TestEndTelegramIx >= optoDataCount) return 0; - return AverageNullable( - TimeEndPerChannel( - optoData, - optoDataCount, - TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)); + return AverageCachedTime(_recalculatedStartEndByChannel, 1); } } @@ -582,6 +578,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations TestEndTelegramIx < 0 || TestStartTelegramIx >= optoDataCount || TestEndTelegramIx >= optoDataCount || + _recalculatedStartEndByChannel == null || TimestampSecEnd < TimestampSecStart; } } @@ -603,7 +600,27 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { return data.Sum() / data.Length; } + + public double VolumeLtrStartCh1 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 0, 0); + public double VolumeLtrStartCh2 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 1, 0); + public double VolumeLtrStartCh3 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 2, 0); + public double VolumeLtrEndCh1 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 0, 1); + public double VolumeLtrEndCh2 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 1, 1); + public double VolumeLtrEndCh3 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 2, 1); + + public double TimestampSecStartCh1 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 0, 0); + public double TimestampSecStartCh2 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 1, 0); + public double TimestampSecStartCh3 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 2, 0); + + public double TimestampSecEndCh1 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 0, 1); + public double TimestampSecEndCh2 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 1, 1); + public double TimestampSecEndCh3 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 2, 1); + public double VolumeLtrStartRaw => NoSamples ? 0 : AverageCachedVolume(_rawStartEndByChannel, 0); + public double VolumeLtrEndRaw => NoSamples ? 0 : AverageCachedVolume(_rawStartEndByChannel, 1); + + + /// Test start volume for metrology in liters public double VolumeLtrStart { @@ -942,6 +959,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } DataStreamPostProcessing(); + PrepareCalculatedChannelData(); ResetDataBuffer(); // TODO: Enable when calculations completed @@ -2653,11 +2671,50 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } + + private OptoTelegramRaw[][] _rawStartEndByChannel; + private OptoTelegramRaw[][] _recalculatedStartEndByChannel; + + private OptoTelegramRaw[][] BuildStartEndByChannel(OptoTelegramRaw[] optoData, int optoDataCount) + { + var grouped = GroupRecordsPerChannel(optoData, 0, optoDataCount - 1, optoDataCount); + var result = new OptoTelegramRaw[ChannelCount][]; - private (int channel, double? delta, double? deltaVolume)[] TimeDeltaPerChannel( + for (int ch = 0; ch < ChannelCount; ch++) + { + result[ch] = new OptoTelegramRaw[2]; + + if (grouped[ch] == null || grouped[ch].Length < 2) + continue; + + result[ch][0] = new OptoTelegramRaw(); + result[ch][0].Copy(grouped[ch][0]); + + result[ch][1] = new OptoTelegramRaw(); + result[ch][1].Copy(grouped[ch][grouped[ch].Length - 1]); + } + + return result; + } + + private void PrepareCalculatedChannelData() + { + if (optoData == null || optoDataCount <= 0) + { + _rawStartEndByChannel = null; + _recalculatedStartEndByChannel = null; + return; + } + + _rawStartEndByChannel = BuildStartEndByChannel(optoData, optoDataCount); + _recalculatedStartEndByChannel = RecalculateVolumeAndTimeDeltaPerChannel(optoData, optoDataCount); + } + + + private (int channel, double? deltaTime, double? deltaVolume)[] TimeDeltaPerChannel( OptoTelegramRaw[][] grouped) { - var result = new (int channel, double? delta, double? deltaVolume)[ChannelCount]; + var result = new (int channel, double? deltaTime, double? deltaVolume)[ChannelCount]; for (int ch = 0; ch < ChannelCount; ch++) { @@ -2669,31 +2726,21 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations continue; } - double minTime = double.MaxValue; - double maxTime = double.MinValue; + var first = records[0]; + var last = records[records.Length - 1]; - double minVol = double.MaxValue; - double maxVol = double.MinValue; - - foreach (var r in records) - { - double t = r.TimestampExt; // TimestampExt is in seconds - double v = r.VolumeRawExt; // VolumeRawExt is in mL - - if (t < minTime) minTime = t; - if (t > maxTime) maxTime = t; - - if (v < minVol) minVol = v; - if (v > maxVol) maxVol = v; - } - - result[ch] = (ch, maxTime - minTime, maxVol - minVol); + result[ch] = ( + ch, + last.TimestampExt - first.TimestampExt, + last.VolumeRawExt - first.VolumeRawExt + ); } return result; } + private OptoTelegramRaw[][] GroupRecordsPerChannel( OptoTelegramRaw[] data, int startIx, @@ -2736,72 +2783,77 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations // convert List[] → array[] return result.Select(l => l.ToArray()).ToArray(); } - - private void RecalculateVolumeAndTimeDeltaPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double?[] time ) - { - OptoTelegramRaw[][] recordByChannel = GroupRecordsPerChannel( + private OptoTelegramRaw[][] RecalculateVolumeAndTimeDeltaPerChannel( + OptoTelegramRaw[] optoData, + int optoDataCount) + { + var recordByChannel = GroupRecordsPerChannel( optoData, 0, - optoDataCount - 2, + optoDataCount - 1, optoDataCount); - (int channel, double? deltaTime, double? deltaVolume)[] timeDelta = TimeDeltaPerChannel(recordByChannel); - - //Time max delta value + var timeDelta = TimeDeltaPerChannel(recordByChannel); + var result = timeDelta .Where(x => x.deltaTime.HasValue) - .OrderByDescending(x => x.deltaTime) + .OrderByDescending(x => x.deltaTime.Value) .FirstOrDefault(); int maxChannel = result.channel; double? maxValueTime = result.deltaTime; - // optional safety if all null - if (!result.deltaTime.HasValue) - { - maxChannel = -1; - maxValueTime = null; - } - - //Recalculate volume - var recalculatedVariablesByChannel = new OptoTelegramRaw[ChannelCount][]; + if (maxChannel < 0 || !maxValueTime.HasValue) + return null; + + if (recordByChannel[maxChannel] == null || recordByChannel[maxChannel].Length < 2) + return null; + + var maxStartRecord = recordByChannel[maxChannel][0]; + var maxEndRecord = recordByChannel[maxChannel][recordByChannel[maxChannel].Length - 1]; + + OptoTelegramRaw[][] recalculatedVariablesByChannel = new OptoTelegramRaw[ChannelCount][]; for (int channel = 0; channel < ChannelCount; channel++) { recalculatedVariablesByChannel[channel] = new OptoTelegramRaw[2]; - } // [per chanel] [2 - start val, end val] - - for (int channel = 0; channel < ChannelCount; channel++) - { - recalculatedVariablesByChannel[channel][0] = new OptoTelegramRaw(); //start val - recalculatedVariablesByChannel[channel][0].Copy( recordByChannel[channel][0]); - recalculatedVariablesByChannel[channel][1] = new OptoTelegramRaw(); //end val - recalculatedVariablesByChannel[channel][1].Copy( recordByChannel[channel][1]); - - //set the same time - recalculatedVariablesByChannel[channel][0].TimestampExt = recordByChannel[maxChannel][0].TimestampExt; - recalculatedVariablesByChannel[channel][1].TimestampExt = recordByChannel[maxChannel][1].TimestampExt; - - //start, end val - end in case if valid data are not accessible - recalculatedVariablesByChannel[channel][0].VolumeRawExt = recordByChannel[maxChannel][0].VolumeRawExt; - recalculatedVariablesByChannel[channel][1].VolumeRawExt = recordByChannel[maxChannel][1].VolumeRawExt; - } - - - //recalculation volumes to max Time + for (int channel = 0; channel < ChannelCount; channel++) { - double? timeKoef = maxValueTime / timeDelta[channel].deltaTime; - double? deltaVolKoef = timeDelta[channel].deltaVolume * timeKoef; + if (recordByChannel[channel] == null || recordByChannel[channel].Length < 2) + continue; - if (deltaVolKoef.HasValue) + var channelStartRecord = recordByChannel[channel][0]; + var channelEndRecord = recordByChannel[channel][recordByChannel[channel].Length - 1]; + + recalculatedVariablesByChannel[channel][0] = new OptoTelegramRaw(); + recalculatedVariablesByChannel[channel][1] = new OptoTelegramRaw(); + + recalculatedVariablesByChannel[channel][0].Copy(channelStartRecord); + recalculatedVariablesByChannel[channel][1].Copy(channelEndRecord); + + recalculatedVariablesByChannel[channel][0].TimestampExt = maxStartRecord.TimestampExt; + recalculatedVariablesByChannel[channel][1].TimestampExt = maxEndRecord.TimestampExt; + + var channelDeltaTime = timeDelta[channel].deltaTime; + var channelDeltaVolume = timeDelta[channel].deltaVolume; + + if (channelDeltaTime.HasValue && + channelDeltaVolume.HasValue && + channelDeltaTime.Value != 0) { - recalculatedVariablesByChannel[channel][1].VolumeRawExt = deltaVolKoef.Value; + double timeCoef = maxValueTime.Value / channelDeltaTime.Value; + double recalculatedDeltaVolume = channelDeltaVolume.Value * timeCoef; + + recalculatedVariablesByChannel[channel][1].VolumeRawExt = + recalculatedVariablesByChannel[channel][0].VolumeRawExt + recalculatedDeltaVolume; } } + + return recalculatedVariablesByChannel; } - + private double?[] TimeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx) { var result = new double?[chenelsSwichCount]; @@ -2867,120 +2919,74 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return result; } - - - public double VolumeLtrStartCh1 + + + private static double GetCachedVolume(OptoTelegramRaw[][] data, int channel, int startEndIndex) { - get - { - if (NoSamples) return 0; - return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[0] ?? 0; - } + if (data == null || + channel < 0 || channel >= data.Length || + data[channel] == null || + startEndIndex < 0 || startEndIndex >= data[channel].Length || + data[channel][startEndIndex] == null) + return 0; + + return data[channel][startEndIndex].VolumeRawExt; } - public double VolumeLtrStartCh2 + private static double GetCachedTime(OptoTelegramRaw[][] data, int channel, int startEndIndex) { - get - { - if (NoSamples) return 0; - return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[1] ?? 0; - } + if (data == null || + channel < 0 || channel >= data.Length || + data[channel] == null || + startEndIndex < 0 || startEndIndex >= data[channel].Length || + data[channel][startEndIndex] == null) + return 0; + + return data[channel][startEndIndex].TimestampExt; } - public double VolumeLtrStartCh3 + private static double AverageCachedVolume(OptoTelegramRaw[][] data, int startEndIndex) { - get + double sum = 0; + int count = 0; + + if (data == null) + return 0; + + for (int ch = 0; ch < ChannelCount; ch++) { - if (NoSamples) return 0; - return VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[2] ?? 0; + if (data[ch] != null && + data[ch].Length > startEndIndex && + data[ch][startEndIndex] != null) + { + sum += data[ch][startEndIndex].VolumeRawExt; + count++; + } } + + return count > 0 ? sum / count : 0; } - public double VolumeLtrEndCh1 + private static double AverageCachedTime(OptoTelegramRaw[][] data, int startEndIndex) { - get - { - if (NoSamples) return 0; - return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[0] ?? 0; - } - } + double sum = 0; + int count = 0; - public double VolumeLtrEndCh2 - { - get - { - if (NoSamples) return 0; - return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[1] ?? 0; - } - } + if (data == null) + return 0; - public double VolumeLtrEndCh3 - { - get + for (int ch = 0; ch < ChannelCount; ch++) { - if (NoSamples) return 0; - return VolumeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[2] ?? 0; + if (data[ch] != null && + data[ch].Length > startEndIndex && + data[ch][startEndIndex] != null) + { + sum += data[ch][startEndIndex].TimestampExt; + count++; + } } - } - public double TimestampSecStartCh1 - { - get - { - if (NoSamples) return 0; - return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[0] ?? 0; - } - } - - public double TimestampSecStartCh2 - { - get - { - if (NoSamples) return 0; - return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[1] ?? 0; - } - } - - public double TimestampSecStartCh3 - { - get - { - if (NoSamples) return 0; - return TimeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)[2] ?? 0; - } - } - - public double TimestampSecEndCh1 - { - get - { - if (NoSamples) return 0; - return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[0] ?? 0; - } - } - - public double TimestampSecEndCh2 - { - get - { - if (NoSamples) return 0; - return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[1] ?? 0; - } - } - - public double TimestampSecEndCh3 - { - get - { - if (NoSamples) return 0; - return TimeEndPerChannel(optoData, optoDataCount, TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)[2] ?? 0; - } + return count > 0 ? sum / count : 0; } } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelTests.cs new file mode 100644 index 000000000..5034ce379 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelTests.cs @@ -0,0 +1,507 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.GenesisRegReader.common; +using TBF.Rig.RegisterReaders.GenesisRegReader.implementations; + +namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations +{ + [TestClass] + public class GenesisSmartReaderChannelTests + { + private GenesisSmartReader _reader; + + [TestInitialize] + public void Setup() + { + _reader = new GenesisSmartReader(); + } + + private static OptoTelegramRaw Rec( + int channel, + double time, + double volume, + OptoTelegramFlags flags = OptoTelegramFlags.OK) + { + return new OptoTelegramRaw + { + iChannel = channel, + TimestampExt = time, + VolumeRawExt = volume, + Flags = flags + }; + } + + private object InvokePrivate(string methodName, params object[] args) + { + var mi = typeof(GenesisSmartReader).GetMethod( + methodName, + BindingFlags.NonPublic | BindingFlags.Instance); + + Assert.IsNotNull(mi, $"Method '{methodName}' was not found."); + + return mi.Invoke(_reader, args); + } + + private static T GetPrivateField(object instance, string fieldName) + { + var fi = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(fi, $"Field '{fieldName}' was not found."); + return (T)fi.GetValue(instance); + } + + private static void SetPrivateField(object instance, string fieldName, object value) + { + var fi = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(fi, $"Field '{fieldName}' was not found."); + fi.SetValue(instance, value); + } + + [TestMethod] + public void GroupRecordsPerChannel_Groups_Valid_Records_By_Channel() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + Rec(0, 13, 110), + Rec(1, 14, 220), + Rec(2, 15, 330), + }; + + var grouped = (OptoTelegramRaw[][])InvokePrivate( + "GroupRecordsPerChannel", + (object)input, 0, input.Length - 1, input.Length); + + Assert.AreEqual(3, grouped.Length); + + Assert.AreEqual(2, grouped[0].Length); + Assert.AreEqual(2, grouped[1].Length); + Assert.AreEqual(2, grouped[2].Length); + + Assert.AreEqual(100, grouped[0][0].VolumeRawExt); + Assert.AreEqual(110, grouped[0][1].VolumeRawExt); + + Assert.AreEqual(200, grouped[1][0].VolumeRawExt); + Assert.AreEqual(220, grouped[1][1].VolumeRawExt); + + Assert.AreEqual(300, grouped[2][0].VolumeRawExt); + Assert.AreEqual(330, grouped[2][1].VolumeRawExt); + } + + [TestMethod] + public void GroupRecordsPerChannel_Ignores_Invalid_Records() + { + var input = new[] + { + Rec(0, 10, 100, OptoTelegramFlags.OK), + Rec(1, 11, 200, OptoTelegramFlags.InvalidTelegram), + Rec(2, 12, 300, OptoTelegramFlags.OK), + Rec(0, 13, 110, OptoTelegramFlags.SyncError), + Rec(1, 14, 220, OptoTelegramFlags.OK), + }; + + var grouped = (OptoTelegramRaw[][])InvokePrivate( + "GroupRecordsPerChannel", + (object)input, 0, input.Length - 1, input.Length); + + Assert.AreEqual(1, grouped[0].Length); + Assert.AreEqual(1, grouped[1].Length); + Assert.AreEqual(1, grouped[2].Length); + + Assert.AreEqual(100, grouped[0][0].VolumeRawExt); + Assert.AreEqual(220, grouped[1][0].VolumeRawExt); + Assert.AreEqual(300, grouped[2][0].VolumeRawExt); + } + + [TestMethod] + public void GroupRecordsPerChannel_Swaps_Start_And_End_When_Reversed() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(0, 12, 120), + }; + + var grouped = (OptoTelegramRaw[][])InvokePrivate( + "GroupRecordsPerChannel", + (object)input, 2, 0, input.Length); + + Assert.AreEqual(2, grouped[0].Length); + Assert.AreEqual(1, grouped[1].Length); + Assert.AreEqual(0, grouped[2].Length); + } + + [TestMethod] + public void GroupRecordsPerChannel_Returns_Empty_For_Invalid_Range() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + }; + + var grouped = (OptoTelegramRaw[][])InvokePrivate( + "GroupRecordsPerChannel", + (object)input, -1, 1, input.Length); + + Assert.AreEqual(3, grouped.Length); + Assert.IsTrue(grouped.All(x => x.Length == 0)); + } + + [TestMethod] + public void TimeDeltaPerChannel_Computes_First_Last_Deltas() + { + var grouped = new[] + { + new[] + { + Rec(0, 10, 100), + Rec(0, 18, 170), + Rec(0, 15, 120) + }, + new[] + { + Rec(1, 20, 500), + Rec(1, 23, 530) + }, + new[] + { + Rec(2, 30, 900) + } + }; + + var result = (ValueTuple[])InvokePrivate( + "TimeDeltaPerChannel", + (object)grouped); + + Assert.AreEqual(0, result[0].Item1); + Assert.AreEqual(5, result[0].Item2); + Assert.AreEqual(20, result[0].Item3); + + Assert.AreEqual(1, result[1].Item1); + Assert.AreEqual(3, result[1].Item2); + Assert.AreEqual(30, result[1].Item3); + + Assert.AreEqual(2, result[2].Item1); + Assert.IsNull(result[2].Item2); + Assert.IsNull(result[2].Item3); + } + + [TestMethod] + public void VolumeStartPerChannel_Returns_Last_Seen_Value_Backward_From_Index() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(0, 12, 120), + Rec(2, 13, 300), + Rec(1, 14, 220), + }; + + var result = (double?[])InvokePrivate( + "VolumeStartPerChannel", + (object)input, input.Length, 4); + + Assert.AreEqual(120, result[0]); + Assert.AreEqual(220, result[1]); + Assert.AreEqual(300, result[2]); + } + + [TestMethod] + public void TimeStartPerChannel_Returns_Last_Seen_Time_Backward_From_Index() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(0, 12, 120), + Rec(2, 13, 300), + Rec(1, 14, 220), + }; + + var result = (double?[])InvokePrivate( + "TimeStartPerChannel", + (object)input, input.Length, 4); + + Assert.AreEqual(12, result[0]); + Assert.AreEqual(14, result[1]); + Assert.AreEqual(13, result[2]); + } + + [TestMethod] + public void VolumeEndPerChannel_Averages_Last_N_Samples_Per_Channel() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + Rec(0, 13, 120), + Rec(1, 14, 220), + Rec(2, 15, 330), + Rec(0, 16, 140), + Rec(1, 17, 240), + Rec(2, 18, 360), + }; + + var result = (double?[])InvokePrivate( + "VolumeEndPerChannel", + (object)input, input.Length, 8, 2); + + Assert.AreEqual((140 + 120) / 2.0, result[0]); + Assert.AreEqual((240 + 220) / 2.0, result[1]); + Assert.AreEqual((360 + 330) / 2.0, result[2]); + } + + [TestMethod] + public void TimeEndPerChannel_Averages_Last_N_Samples_Per_Channel() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + Rec(0, 13, 120), + Rec(1, 14, 220), + Rec(2, 15, 330), + Rec(0, 16, 140), + Rec(1, 17, 240), + Rec(2, 18, 360), + }; + + var result = (double?[])InvokePrivate( + "TimeEndPerChannel", + (object)input, input.Length, 8, 2); + + Assert.AreEqual((16 + 13) / 2.0, result[0]); + Assert.AreEqual((17 + 14) / 2.0, result[1]); + Assert.AreEqual((18 + 15) / 2.0, result[2]); + } + + [TestMethod] + public void VolumeStartPerChannel_Returns_All_Nulls_For_OutOfRange_Index() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + }; + + var result = (double?[])InvokePrivate( + "VolumeStartPerChannel", + (object)input, input.Length, -1); + + Assert.IsNull(result[0]); + Assert.IsNull(result[1]); + Assert.IsNull(result[2]); + } + + [TestMethod] + public void TimeEndPerChannel_Uses_One_Sample_When_SamplesPerChannel_Is_Zero() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + Rec(0, 13, 120), + Rec(1, 14, 220), + Rec(2, 15, 330), + }; + + var result = (double?[])InvokePrivate( + "TimeEndPerChannel", + (object)input, input.Length, 5, 0); + + Assert.AreEqual(13, result[0]); + Assert.AreEqual(14, result[1]); + Assert.AreEqual(15, result[2]); + } + + [TestMethod] + public void Precreated_OptoData_Private_Methods_Read_Correct_Values() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + Rec(0, 13, 120), + Rec(1, 14, 220), + Rec(2, 15, 330), + Rec(0, 16, 140), + Rec(1, 17, 240), + Rec(2, 18, 360), + }; + + SetPrivateField(_reader, "optoData", input); + SetPrivateField(_reader, "optoDataCount", input.Length); + + _reader.TestStartTelegramIx = 5; + _reader.TestEndTelegramIx = 8; + + Assert.AreEqual(input.Length, GetPrivateField(_reader, "optoDataCount")); + + var startVolumes = (double?[])InvokePrivate( + "VolumeStartPerChannel", + (object)input, input.Length, 5); + + var endVolumes = (double?[])InvokePrivate( + "VolumeEndPerChannel", + (object)input, input.Length, 8, 2 * GenesisSmartReader.StartEndFilterSamplesCount2 + 1); + + var startTimes = (double?[])InvokePrivate( + "TimeStartPerChannel", + (object)input, input.Length, 5); + + var endTimes = (double?[])InvokePrivate( + "TimeEndPerChannel", + (object)input, input.Length, 8, 2 * GenesisSmartReader.StartEndFilterSamplesCount2 + 1); + + Assert.AreEqual(120, startVolumes[0]); + Assert.AreEqual(220, startVolumes[1]); + Assert.AreEqual(330, startVolumes[2]); + + Assert.AreEqual(13, startTimes[0]); + Assert.AreEqual(14, startTimes[1]); + Assert.AreEqual(15, startTimes[2]); + + Assert.AreEqual((140 + 120 + 100) / 3.0, endVolumes[0]); + Assert.AreEqual((240 + 220 + 200) / 3.0, endVolumes[1]); + Assert.AreEqual((360 + 330 + 300) / 3.0, endVolumes[2]); + + Assert.AreEqual((16 + 13 + 10) / 3.0, endTimes[0]); + Assert.AreEqual((17 + 14 + 11) / 3.0, endTimes[1]); + Assert.AreEqual((18 + 15 + 12) / 3.0, endTimes[2]); + } + + [TestMethod] + public void Public_Channel_Properties_Default_To_Zero_When_NoSamples_Is_True() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 11, 200), + Rec(2, 12, 300), + }; + + SetPrivateField(_reader, "optoData", input); + SetPrivateField(_reader, "optoDataCount", input.Length); + + _reader.TestStartTelegramIx = 5; + _reader.TestEndTelegramIx = 8; + + Assert.IsTrue(_reader.NoSamples); + + Assert.AreEqual(0, _reader.VolumeLtrStartCh1); + Assert.AreEqual(0, _reader.VolumeLtrStartCh2); + Assert.AreEqual(0, _reader.VolumeLtrStartCh3); + + Assert.AreEqual(0, _reader.VolumeLtrEndCh1); + Assert.AreEqual(0, _reader.VolumeLtrEndCh2); + Assert.AreEqual(0, _reader.VolumeLtrEndCh3); + + Assert.AreEqual(0, _reader.TimestampSecStartCh1); + Assert.AreEqual(0, _reader.TimestampSecStartCh2); + Assert.AreEqual(0, _reader.TimestampSecStartCh3); + + Assert.AreEqual(0, _reader.TimestampSecEndCh1); + Assert.AreEqual(0, _reader.TimestampSecEndCh2); + Assert.AreEqual(0, _reader.TimestampSecEndCh3); + } + + [TestMethod] + public void Copy_Should_Copy_From_Source_Record() + { + var source = Rec(2, 123.5, 456.7, OptoTelegramFlags.OK_TestEnd); + source.Counter = 77; + source.RefFlow = 9.5f; + source.FlowRaw = 111; + source.VolumeRaw = 222; + source.Impedance = 333; + source.CheckSum = 44; + + var target = new OptoTelegramRaw(); + target.Copy(source); + + Assert.AreEqual(source.iChannel, target.iChannel); + Assert.AreEqual(source.TimestampExt, target.TimestampExt); + Assert.AreEqual(source.VolumeRawExt, target.VolumeRawExt); + Assert.AreEqual(source.Flags, target.Flags); + Assert.AreEqual(source.Counter, target.Counter); + Assert.AreEqual(source.RefFlow, target.RefFlow); + Assert.AreEqual(source.FlowRaw, target.FlowRaw); + Assert.AreEqual(source.VolumeRaw, target.VolumeRaw); + Assert.AreEqual(source.Impedance, target.Impedance); + Assert.AreEqual(source.CheckSum, target.CheckSum); + } + + [TestMethod] + public void RecalculateVolumeAndTimeDeltaPerChannel_Returns_Recalculated_Output() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(0, 20, 200), + + Rec(1, 10, 500), + Rec(1, 30, 560), + + Rec(2, 10, 1000), + Rec(2, 15, 1050), + }; + + var result = (OptoTelegramRaw[][])InvokePrivate( + "RecalculateVolumeAndTimeDeltaPerChannel", + (object)input, + input.Length); + + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Length); + + for (int ch = 0; ch < 3; ch++) + { + Assert.IsNotNull(result[ch]); + Assert.AreEqual(2, result[ch].Length); + Assert.IsNotNull(result[ch][0]); + Assert.IsNotNull(result[ch][1]); + + Assert.AreEqual(10, result[ch][0].TimestampExt, 0.0001); + Assert.AreEqual(30, result[ch][1].TimestampExt, 0.0001); + } + + Assert.AreEqual(100, result[0][0].VolumeRawExt, 0.0001); + Assert.AreEqual(300, result[0][1].VolumeRawExt, 0.0001); + + Assert.AreEqual(500, result[1][0].VolumeRawExt, 0.0001); + Assert.AreEqual(560, result[1][1].VolumeRawExt, 0.0001); + + Assert.AreEqual(1000, result[2][0].VolumeRawExt, 0.0001); + Assert.AreEqual(1200, result[2][1].VolumeRawExt, 0.0001); + } + + [TestMethod] + public void RecalculateVolumeAndTimeDeltaPerChannel_Returns_Null_When_No_Channel_Has_Two_Records() + { + var input = new[] + { + Rec(0, 10, 100), + Rec(1, 20, 200), + Rec(2, 30, 300), + }; + + var result = (OptoTelegramRaw[][])InvokePrivate( + "RecalculateVolumeAndTimeDeltaPerChannel", + (object)input, + input.Length); + + Assert.IsNull(result); + } + } +} \ No newline at end of file diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index ed1f7ef05..7dd5a077b 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -109,6 +109,7 @@ + From 08dff1272bb08082e8da17d437f46fc335cf0553 Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Thu, 2 Apr 2026 09:25:17 +0200 Subject: [PATCH 5/5] Add overflow volume/timestamp handling and related tests for `GenesisSmartReader`: - Extend `CalibrationRecord` constructor to include overflow fields (`OverflowVolumeCm`, `OverflowTimeS`). - Implement additional unit tests in `OptoTelegramRawTest` to validate multi-rollover scenarios and extended calculations. - Add `GenesisSmartReaderThreadedReadTests` to cover threaded read-loop functionality and serial processing. - Enhance logging in `Pump` classes (`TurnOff` methods). - Update project file to include new test files. --- TBF/Properties/AssemblyInfo.cs | 4 +- TBF/Rig/BuiltIn/PumpTandem/Pump.cs | 2 + TBF/Rig/Danfoss/VLT2800/Pump.cs | 2 + TBF/Rig/Modbus/PumpFM/DanfossVLT/Pump.cs | 2 + TBF/Rig/Modbus/PumpFM/Grundfoss/Pump.cs | 2 + .../common/OptoTelegramRaw.cs | 76 +- .../implementations/GenesisSmartReader.cs | 758 ++++++++++++++---- TBF/Rig/Sequences/SequenceBase.cs | 3 +- .../FlyingStartMassCollectionSeq.cs | 77 +- .../common/OptoTelegramRawTest.cs | 472 ++++++++++- .../implementations/GenesisReaderTests.cs | 220 +++++ ...GenesisSmartReaderChannelAveragingTests.cs | 116 ++- .../implementations/GenesisSmartReaderTest.cs | 107 ++- .../GenesisSmartReaderThreadedReadTests.cs | 220 +++++ TBFTests/TBFTests.csproj | 2 + 15 files changed, 1749 insertions(+), 314 deletions(-) create mode 100644 TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisReaderTests.cs create mode 100644 TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderThreadedReadTests.cs diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs index 3533ba3a4..4891e220b 100644 --- a/TBF/Properties/AssemblyInfo.cs +++ b/TBF/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("3.9.3019.1")] -[assembly: AssemblyFileVersion("3.9.3019.1")] +[assembly: AssemblyVersion("3.9.3030.1")] +[assembly: AssemblyFileVersion("3.9.3030.1")] diff --git a/TBF/Rig/BuiltIn/PumpTandem/Pump.cs b/TBF/Rig/BuiltIn/PumpTandem/Pump.cs index a6a5ab259..4e7377466 100644 --- a/TBF/Rig/BuiltIn/PumpTandem/Pump.cs +++ b/TBF/Rig/BuiltIn/PumpTandem/Pump.cs @@ -111,6 +111,7 @@ namespace TBF.Rig.BuiltIn.PumpTandem public void TurnOff() { + log.DebugFormat("{0}.TurnOff()", Name); if ((pumpFm1 != null) && (pumpFm2 != null)) { pumpFm1.TurnOff(); @@ -120,6 +121,7 @@ namespace TBF.Rig.BuiltIn.PumpTandem { // TODO } + log.DebugFormat("DONE {0}.TurnOff()", Name); } diff --git a/TBF/Rig/Danfoss/VLT2800/Pump.cs b/TBF/Rig/Danfoss/VLT2800/Pump.cs index 093b4aa8d..7dd59e327 100644 --- a/TBF/Rig/Danfoss/VLT2800/Pump.cs +++ b/TBF/Rig/Danfoss/VLT2800/Pump.cs @@ -332,6 +332,8 @@ namespace TBF.Rig.Danfoss.VLT2800 Telegram.UpdateTelegramChecksum(msg); } SendData(msg); + + log.DebugFormat("DONE {0}.TurnOff()", Name); } diff --git a/TBF/Rig/Modbus/PumpFM/DanfossVLT/Pump.cs b/TBF/Rig/Modbus/PumpFM/DanfossVLT/Pump.cs index e1b932283..8512ff737 100644 --- a/TBF/Rig/Modbus/PumpFM/DanfossVLT/Pump.cs +++ b/TBF/Rig/Modbus/PumpFM/DanfossVLT/Pump.cs @@ -264,6 +264,8 @@ namespace TBF.Rig.Modbus.PumpFM.DanfossVLT modbus.SendMessage(msg, Name); } + + log.DebugFormat("DONE {0}.TurnOff()", Name); } diff --git a/TBF/Rig/Modbus/PumpFM/Grundfoss/Pump.cs b/TBF/Rig/Modbus/PumpFM/Grundfoss/Pump.cs index ba8792c0b..f2300603a 100644 --- a/TBF/Rig/Modbus/PumpFM/Grundfoss/Pump.cs +++ b/TBF/Rig/Modbus/PumpFM/Grundfoss/Pump.cs @@ -416,6 +416,8 @@ namespace TBF.Rig.Modbus.PumpFM.Grundfoss // --- DIAGNOSTIKA --- LiveLogDiag.Log1("GF TurnOff msg = " + BitConverter.ToString(msg)); } + + log.DebugFormat("DONE {0}.TurnOff()", Name); } /// diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs index 54b952346..f8dc924ff 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRaw.cs @@ -6,7 +6,7 @@ using System; using System.Globalization; using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis.DataPackages.MeasurementRecords; using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis.Protocols.StreamingProtocol; - +using Xylem.Common.Metrology.Measurements; namespace TBF.Rig.RegisterReaders.GenesisRegReader.common @@ -25,6 +25,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common public static readonly int Length = 42; private static CultureInfo culture; + public MeasurementRecord data; /// /// Strobed value @@ -124,11 +125,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common } - // -------- TIMESTAMP (seconds) -------- - private const double TS_RANGE = StreamingDecoder.CpuTimeOverflowS; - // -------- VOLUME (liters) -------- - private const double VOL_RANGE = StreamingDecoder.DefaultAccuDutOverflowVolumeCm * 1000; /// /// update data by CalibrationRecord @@ -149,7 +146,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common if (data == null) throw new ArgumentNullException(nameof(data)); - UpdateData( data.Channel, data.VolumeCm, data.TimeS, counter, refFlow, ref volumeRawExtLast, ref timestampExtLast); + this.data = data; + UpdateData( data.Channel, data.VolumeCm, data.OverflowVolumeCm, data.TimeS, data.OverflowTimeS, counter, refFlow, ref volumeRawExtLast, ref timestampExtLast); } /// @@ -170,8 +168,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common { if (data == null) throw new ArgumentNullException(nameof(data)); - - UpdateData( data.Channel, data.VolumeCm, data.TimeS, counter, refFlow, ref volumeRawExtLast, ref timestampExtLast); + this.data = data; + UpdateData( data.Channel, data.VolumeCm, data.OverflowVolumeCm, data.TimeS, data.OverflowTimeS, counter, refFlow, ref volumeRawExtLast, ref timestampExtLast); } /// @@ -187,7 +185,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common private void UpdateData( int channel, double volumeCm, + double OverflowVolumeCm, double timeS, + double OverflowTimeS, int counter, float refFlow, ref double volumeRawExtLast, @@ -200,29 +200,30 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common Flags = OptoTelegramFlags.OK; FlowRaw = 0; - VolumeRaw = volumeCm * 1000.0; // liters + double nV = NormalizeByOverflow(volumeCm, OverflowVolumeCm); + VolumeRaw = nV * 1000.0; // liters CheckSum = 0; Impedance = 0; EmfRaw = 0; MagneticFieldRaw = 0; - double ts = NormalizeTimestamp(timeS); + double ts = NormalizeByOverflow(timeS, OverflowTimeS); Timestamp = ts; - VolumeRawExt = UnwrapVolume(VolumeRaw, ref volumeRawExtLast); - TimestampExt = UnwrapTimestamp(ts, ref timestampExtLast); + VolumeRawExt = UnwrapVolume(VolumeRaw,OverflowVolumeCm * 1000, ref volumeRawExtLast); + TimestampExt = UnwrapTimestamp(ts,OverflowTimeS, ref timestampExtLast); } - private static double NormalizeTimestamp(double timeS) + private static double NormalizeByOverflow(double value, double overfValue = 0) { - double ts = timeS % TS_RANGE; - if (ts < 0) - ts += TS_RANGE; + double nValue = value % overfValue; + if (nValue < 0) + nValue += overfValue; - return ts; + return nValue; } - private static double UnwrapVolume(double currentVolume, ref double volumeRawExtLast) + private static double UnwrapVolume(double currentVolume, double overfValue, ref double volumeRawExtLast) { double result; @@ -232,16 +233,25 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common } else { - result = currentVolume < volumeRawExtLast - ? currentVolume + VOL_RANGE - : currentVolume; + double lastMod = volumeRawExtLast % overfValue; + if (lastMod < 0) + lastMod += overfValue; + + double delta = currentVolume - lastMod; + + if (delta < -overfValue / 2.0) + delta += overfValue; + else if (delta > overfValue / 2.0) + delta -= overfValue; + + result = volumeRawExtLast + delta; } volumeRawExtLast = result; return result; } - private static double UnwrapTimestamp(double currentTimestamp, ref double timestampExtLast) + private static double UnwrapTimestamp(double currentTimestamp,double overfValue, ref double timestampExtLast) { double result; @@ -251,16 +261,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common } else { - double lastMod = timestampExtLast % TS_RANGE; + double lastMod = timestampExtLast % overfValue; if (lastMod < 0) - lastMod += TS_RANGE; + lastMod += overfValue; double delta = currentTimestamp - lastMod; - if (delta < -TS_RANGE / 2.0) - delta += TS_RANGE; - else if (delta > TS_RANGE / 2.0) - delta -= TS_RANGE; + if (delta < -overfValue / 2.0) + delta += overfValue; + else if (delta > overfValue / 2.0) + delta -= overfValue; result = timestampExtLast + delta; } @@ -367,5 +377,15 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.common this.CheckSum = optoTelegramRaw.CheckSum; this.iChannel = optoTelegramRaw.iChannel; } + + public string rawDataToString() + { + if (data != null) + { + return string.Format("Channel: {0}, VolumeCm:{1}, OverflowVolumeCm:{2}, TimeS: {3}, OverflowTimeS:{4} ", + data.Channel, data.VolumeCm, data.OverflowVolumeCm, data.TimeS, data.OverflowTimeS); + } + return string.Empty; + } } } diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs index fbeeddcd5..ab81d0415 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs @@ -1,15 +1,18 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Common; using Config.Entities; using log4net; using NHibernate; +using NHibernate.Hql.Ast; using Sensus.iPerl.NfcHandler; using TBF.Rig.Generic; using TBF.Rig.GenericDevices; @@ -36,16 +39,30 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public override string ToString() { - string cfgText; + try + { + string cfgText; - if (genesisHeadCfg != null) - cfgText = genesisHeadCfg.ToString(-1); - else if (Cfg != null) - cfgText = Cfg.ToString(); - else - cfgText = ""; + try + { + if (genesisHeadCfg != null) + cfgText = genesisHeadCfg.ToString(-1); + else if (Cfg != null) + cfgText = Cfg.ToString(); + else + cfgText = ""; + } + catch (Exception ex) + { + cfgText = $""; + } - return string.Format("{0}({1})", ClassName, cfgText); + return $"{GetType().Name}({cfgText})"; + } + catch + { + return GetType().Name; + } } #if TURA_SPECIAL @@ -477,6 +494,8 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Currently executed repetition number public void TestIsGoingToStartSoon(Test _test, int _repetitionNr) { + + log.Debug($"TestIsGoingToStartSoon({_test.Name}, {_repetitionNr})"); /// Store/update values to be used as a part of the opto-data log file name this.test = _test; this.repetitionNr = _repetitionNr; @@ -514,6 +533,21 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations extraDataPath = Path.Combine(relativeDirectory, fileName); } } + + + try + { + log.DebugFormat("TestIsGoingToStartSoon - OpenOptoSerialPortIfNotInit called"); + OpenOptoSerialPortIfNotInit ($"COM{genesisHeadCfg.OptoComPortNr}", + 115200, + Parity.None, + 8, + StopBits.One, + Handshake.None); + } + catch (Exception) + { + } } /// @@ -626,8 +660,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { get { - if (NoSamples) return 0; - return AverageNullable(VolumeStartPerChannel(optoData, optoDataCount, TestStartTelegramIx)); + return VolumeLtrStartRaw; } } @@ -636,13 +669,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { get { - if (NoSamples) return 0; - return AverageNullable( - VolumeEndPerChannel( - optoData, - optoDataCount, - TestEndTelegramIx, - 2 * StartEndFilterSamplesCount2 + 1)); + return VolumeLtrEndRaw; } } @@ -659,6 +686,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// Opto serial port and worker thread related private variables /// public ISerialDriver optoSerialPort; + private volatile bool startDataProcessing = false; public GenesisSmartReader() @@ -683,6 +711,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public override void Initialize() { + log.DebugFormat("Initialize() called"); x = new float[FeatureVectorSize]; flowDirectionDetection = new FlowDirectionDetection(); @@ -711,8 +740,10 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations synchronized2 = false; partOfTelegram = string.Empty; optoSerialPort = null; + startDataProcessing = false; + - + log.DebugFormat($"Initialize - OpenOptoSerialPort: {genesisHeadCfg.OptoComPortNr}"); if (DebugLevel == DebugMode.Normal) { /// Open serial port: 9600 Bd, 8 data bits, 1 stop bit, no parity @@ -721,9 +752,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, Handshake.None); + ResetDataBuffer(); CloseOptoSerialPort(); log.FatalFormat($"{Name} initialized: {this}"); - ResetDataBuffer(); } catch (Exception ex) { @@ -735,6 +766,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { log.FatalFormat($"{Name} simulated: {this}"); } + } /// @@ -742,6 +774,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// public void StartSession() { + log.DebugFormat("StartSession() called"); ResultCode = 0; Disabled = false; @@ -769,6 +802,30 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations currentFlowDir = InitFlowDir; ResetBlockCountersAndState(); + + startDataProcessing = false; + //Start connection + log.DebugFormat($"StartSession - OpenOptoSerialPort: {genesisHeadCfg.OptoComPortNr}"); + if (DebugLevel == DebugMode.Normal) + { + /// Open serial port: 9600 Bd, 8 data bits, 1 stop bit, no parity + /// Check whether head is connected, working + try + { + OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, + Handshake.None); + log.FatalFormat($"{Name} StartSession - OpenOptoSerialPort: {genesisHeadCfg.OptoComPortNr}"); + } + catch (Exception ex) + { + log.FatalFormat($"{Name} initialization failed: {ex}"); + throw; + } + } + else + { + log.FatalFormat($"{Name} simulated: {this}"); + } } public void SaveMark(object mark) @@ -814,6 +871,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public void RunDeviceBefore() { + //log.DebugFormat("RunDeviceBefore() called"); if (DebugLevel == DebugMode.Normal) { try @@ -842,6 +900,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public void StopDevice() { + log.DebugFormat("StopDevice() called"); try { if (optoSerialPort != null) @@ -903,6 +962,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// public void Start() { + log.DebugFormat("Start: {0:HH:mm:ss.fff}", DateTime.Now); lock (this) { Clear(); @@ -920,22 +980,28 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations { lock (this) { + log.DebugFormat("Run: {0:HH:mm:ss.fff}", DateTime.Now); timeFromStart += StateMachine.Period; ReadPulses(); - if (!startSampleAcquired && (timeFromStart >= 8) && (currentTelegramIx >= 0)) + // Start sample after 1 second + if (!startSampleAcquired && (timeFromStart >= 1) && (currentTelegramIx >= 0)) { - /// Take the test start sample startSampleAcquired = true; TestStartTelegramIx = currentTelegramIx; + + // initialize end at the same point + TestEndTelegramIx = currentTelegramIx; + + log.DebugFormat( + "Test start acquired at ix={0}, timeFromStart={1}", + TestStartTelegramIx, + timeFromStart); } - else if (startSampleAcquired) + else if (startSampleAcquired && currentTelegramIx >= 0) { - /// Shift data in pipelines - TestEndTelegramIx = endTelegramIdx3; - endTelegramIdx3 = endTelegramIdx2; - endTelegramIdx2 = endTelegramIdx1; - endTelegramIdx1 = currentTelegramIx; + // always keep latest telegram as end + TestEndTelegramIx = currentTelegramIx; } } @@ -947,51 +1013,40 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// public void Stop() { - log.DebugFormat("Flow filtering end, feature vector calculation start: {0:HH:mm:ss.fff}", DateTime.Now); + log.DebugFormat("Stop: {0:HH:mm:ss.fff}", DateTime.Now); int startIx; int endIx; lock (this) { - StopDataStreamProcessing(); + // 1. stop new reads from serial + StopOptoReadLoop(); + + // 2. clear data stream processing + ClearReceivedLines();//Empty othrs + // 2. process everything already queued + //DrainQueuedLines(); + + ResetDataBuffer(); + + // 3. now it is safe to close the port + CloseOptoSerialPort(); + + // 4. mark state as stopped + dataStreamState = DataStreamState.Flush; + startDataProcessing = false; + + // 5. add marks using fully processed data AddTestStartEndMarksToData(out startIx, out endIx); } DataStreamPostProcessing(); - PrepareCalculatedChannelData(); - ResetDataBuffer(); + + - // 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); - } + log.WarnFormat("Genesis.Stop() startIx={0} endIx={1} len={2} no raw data file", + startIx, endIx, optoData.Length); if (TestStartTelegramIx == 0 || optoDataCount < 100) { @@ -1003,6 +1058,73 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } + + private volatile bool _stopQueueData = false; + + public bool StopQueueData + { + get => _stopQueueData; + set + { + if (value == true) + { + log.DebugFormat("StopQueueData: --true--"); + } + + _stopQueueData = value; + } + } + + private void ClearReceivedLines() + { + StopQueueData = true; + try + { + while (_receivedLines.TryDequeue(out _)) { } + } + finally + { + //StopQueueData = false; + } + } + + private void DrainQueuedLines() + { + StopQueueData = true; + try + { + while (_receivedLines.TryDequeue(out var item)) + { + try + { + DateTime timestamp = item.Timestamp; + string line = item.Line; + + log.DebugFormat( + "DrainQueuedLines() real incoming TimeStamp: {0} processing queued line: {1}", + timestamp.ToString("HH:mm:ss.fff"), + line); + + bool blockCompleted; + ProcessOptoLine(line, dataStreamState, out blockCompleted); + + if (blockCompleted) + { + log.Debug("DrainQueuedLines() completed flow block detected."); + } + } + catch (Exception ex) + { + log.Error($"DrainQueuedLines() failed: {ex.Message}"); + } + } + } + finally + { + StopQueueData = false; + } + } + void AddTestStartEndMarksToData(out int startIx, out int endIx) { startIx = BufferIdx(TestStartTelegramIx); @@ -1037,15 +1159,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// void DataStreamPostProcessing() { - if (optoDataCount <= OptoDataBufferSize) - { - FIRFilterFlow(optoData, 0, optoDataCount - 1); - } - else - { - FIRFilterFlow(optoData, 0, StartOptoDataCount - 1); - FIRFilterFlow(optoData, (optoDataCount - EndOptoDataCount), optoDataCount - 1); - } + PrepareCalculatedChannelData(); } /// @@ -1201,6 +1315,21 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return Average(volumeDelta); } + private void OpenOptoSerialPortIfNotInit( + string comPort, + int baudRate, + Parity parity, + int dataBits, + StopBits stopBit, + Handshake handshake, + int openTimeoutMs = 3000) + { + if (optoSerialPort == null) + { + OpenOptoSerialPort(comPort, baudRate, parity, dataBits, stopBit, handshake, openTimeoutMs); + } + } + private void OpenOptoSerialPort( string comPort, int baudRate, @@ -1263,11 +1392,31 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations private void CloseOptoSerialPort() { - if (optoSerialPort != null) + // 1. Stop background reading FIRST + StopOptoReadLoop(); + + // 2. Synchronize with ReadLine() + lock (_serialReadSync) { - optoSerialPort.Close(); - optoSerialPort = null; - log.FatalFormat($"{Name} OptoPort closed: {this}"); + if (optoSerialPort != null) + { + try + { + if (optoSerialPort.IsOpen) + { + optoSerialPort.Close(); + } + } + catch (Exception ex) + { + log.Error($"Error closing opto port: {ex.Message}"); + } + finally + { + optoSerialPort = null; + log.FatalFormat($"{Name} OptoPort closed: {this}"); + } + } } } @@ -1278,9 +1427,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// public void StartDataStreamProcessing() { + log.DebugFormat("StartDataStreamProcessing({0}) called", DateTime.Now.ToString("HH:mm:ss.fff")); try { - OpenOptoSerialPort($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, + log.DebugFormat("OpenOptoSerialPortIfNotInit StartDataStreamProcessing() called"); + OpenOptoSerialPortIfNotInit( + $"COM{genesisHeadCfg.OptoComPortNr}", + 115200, + Parity.None, + 8, + StopBits.One, Handshake.None); } catch (Exception) @@ -1292,20 +1448,51 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations timeFromStart = 0; currentTelegramIx = -1; startSampleAcquired = false; - TestStartTelegramIx = 0; - endTelegramIdx1 = 0; - endTelegramIdx2 = 0; - endTelegramIdx3 = 0; - TestEndTelegramIx = 0; + TestStartTelegramIx = -1; + TestEndTelegramIx = -1; - if (optoSerialPort != null && optoSerialPort.IsOpen) optoSerialPort.DiscardInBuffer(); + // optional: keep these if fields still exist, but no longer used + endTelegramIdx1 = -1; + endTelegramIdx2 = -1; + endTelegramIdx3 = -1; - if (flowDirectionDetection != null) - flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection + // RESET UNWRAP STATE + for (int i = 0; i < iChanelsCount; i++) + { + volumeRawExtLast[i] = double.NaN; + timestampExtLast[i] = double.NaN; - /// Enable opto-data parsing and saving - dataStreamState = DataStreamState.ProcessAndSave; - ResetDataBuffer(); + lastVolumeRaw[i] = double.NaN; + lastTimestamp[i] = double.NaN; + volumeLtr[i] = double.NaN; + volumeLtr0[i] = double.NaN; + timestampSec[i] = double.NaN; + timestampSec0[i] = double.NaN; + } + + channel0 = -1; + + try + { + StopQueueData = true; + if (optoSerialPort != null && optoSerialPort.IsOpen) + { + ResetDataBuffer(); + startDataProcessing = true; + } + + ClearReceivedLines(); + + if (flowDirectionDetection != null) + flowDirectionDetection.ClearFifo(); + + dataStreamState = DataStreamState.ProcessAndSave; + StartOptoReadLoop(); + } + finally + { + StopQueueData = false; + } } public void SetCommunicationInterface(string commInterface) @@ -1331,8 +1518,29 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// public void StopDataStreamProcessing() { + log.DebugFormat("StopDataStreamProcessing({0}) called", DateTime.Now.ToString("HH:mm:ss.fff")); dataStreamState = DataStreamState.Flush; - CloseOptoSerialPort(); + startDataProcessing = false; + //stop data processing end + + StopQueueData = true; + + lock (this) + { + // 1. stop new reads from serial + //StopOptoReadLoop(); + + // 2. clear data stream processing + ClearReceivedLines();//Empty othrs + // 2. process everything already queued + //DrainQueuedLines(); + + //ResetDataBuffer(); + + // 3. now it is safe to close the port + //CloseOptoSerialPort(); + + } } /// @@ -1350,7 +1558,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations //block varaibles to check results block private int _completedBlockCount = 0; - private int _resetAfterBlockRepetitions = 1; + private int _resetAfterBlockRepetitions = 2; private bool _blockStartedWithF = false; private bool _channel1SeenInBlock = false; @@ -1406,7 +1614,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations /// /// Handles @f marker. First @f starts block, second @f closes it if h1/h2/h3 were seen. - /// Returns true when buffers should be reset. + /// Returns true when the configured number of complete flow blocks has been reached. /// private bool HandleFlowMarker() { @@ -1444,6 +1652,105 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations #endregion + private CancellationTokenSource _readLoopCts; + private Task _readLoopTask; + private readonly ConcurrentQueue<(DateTime Timestamp, string Line)> _receivedLines = new ConcurrentQueue<(DateTime Timestamp, string Line)>(); + //private readonly ConcurrentQueue _receivedLines = new ConcurrentQueue(); + private readonly object _serialReadSync = new object(); + + private void StartOptoReadLoop() + { + if (optoSerialPort == null || !optoSerialPort.IsOpen) + return; + + StopOptoReadLoop(); + + _readLoopCts = new CancellationTokenSource(); + var token = _readLoopCts.Token; + + _readLoopTask = Task.Run(() => + { + log.Debug($"OPTHO {OptoComPortNr} background read loop started."); + + while (!token.IsCancellationRequested) + { + try + { + if (optoSerialPort == null || !optoSerialPort.IsOpen) + { + Thread.Sleep(20); + continue; + } + + string line; + lock (_serialReadSync) + { + if (optoSerialPort == null || !optoSerialPort.IsOpen) + continue; + + line = optoSerialPort.ReadLine(); + } + + //switch stopQueneData + if (!StopQueueData && !string.IsNullOrWhiteSpace(line)) + { + _receivedLines.Enqueue((DateTime.UtcNow, line)); + //log.Debug($"OPTHO COM{OptoComPortNr} queued line: {line}"); + } + } + catch (TimeoutException) + { + // normal: just continue + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + log.Error($"OPTHO {OptoComPortNr} background read error: {ex.Message}"); + Thread.Sleep(100); + } + } + + log.Debug($"OPTHO {OptoComPortNr} background read loop stopped."); + }, token); + } + + private void StopOptoReadLoop() + { + try + { + StopQueueData = true; + + if (_readLoopCts != null) + { + _readLoopCts.Cancel(); + } + + if (_readLoopTask != null) + { + try + { + _readLoopTask.Wait(1000); + } + catch (AggregateException) + { + } + } + } + finally + { + _readLoopTask = null; + + if (_readLoopCts != null) + { + _readLoopCts.Dispose(); + _readLoopCts = null; + } + } + } + /// /// Reads opto-datastream via serial port. Invoked from RunDeviceBefore() /// @@ -1456,45 +1763,43 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations lock (this) { - try + while (_receivedLines.TryDequeue(out var item)) { - int nrBytes = optoSerialPort.BytesToRead; - if (nrBytes > 0) + try { - // This will now wait max 3 seconds (ReadTimeout) - string line = optoSerialPort.ReadLine(); - log.DebugFormat("Read Opto Data Line: {0}", line); - bool resetBuffer; - ProcessOptoLine(line, optoState, out resetBuffer); - - if (resetBuffer) + DateTime timestamp = item.Timestamp; + string line = item.Line; + // 🔴 STEP 1: Check if we should start processing + if (startDataProcessing && optoState == DataStreamState.ProcessAndSave) { - ResetDataBuffer(); + log.DebugFormat("Read Opto Data Line timestamp:{0} to process from queue: {1}",timestamp.ToString("HH:mm:ss.fff") , line); + bool blockCompleted; + ProcessOptoLine(line, optoState, out blockCompleted); + + if (blockCompleted) + { + log.Debug("ReadOptoData() completed flow block detected."); + if (resetSerialBuffersOnCompletedFlowBlock) // DO NOT call ResetDataBuffer() here + ResetDataBuffer(); + } } } - } - catch (TimeoutException) - { - // ✅ No data received within 3 seconds - log.Debug($"OPTHO {OptoComPortNr} ReadLine timeout (3s) - continuing."); - - // Just continue without parsing - } - catch (Exception ex) - { - log.Error($"OPTHO {OptoComPortNr} Read error: {ex.Message}"); + catch (Exception ex) + { + log.Error($"OPTHO {OptoComPortNr} processing queued line failed: {ex.Message}"); + } } } } - public void ProcessOptoLine(string line, DataStreamState optoState, out bool resetBuffer) + public void ProcessOptoLine(string line, DataStreamState optoState, out bool blockCompleted) { var encoding = optoSerialPort != null ? optoSerialPort.Encoding : Encoding.ASCII; byte[] bytes = encoding.GetBytes(line); log.Debug("ComPort: " + OptoComPortNr + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); - resetBuffer = false; + blockCompleted = false; var streamingDecode = new StreamingDecoder(true); streamingDecode.DecodeMsg(line); @@ -1503,10 +1808,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations if (streamingDecode.DataFlowTest != null && streamingDecode.DataFlowTest.IsValid) { - resetBuffer = HandleFlowMarker(); + blockCompleted = HandleFlowMarker(); log.Debug("ComPort: " + OptoComPortNr + " Decoded Flow data: " + streamingDecode.DataFlowTest + - " OPTHO RX ← " + - HexFormatter.ToSerialHex(bytes)); + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); } if (calibData != null && calibData.IsValid) @@ -1533,11 +1837,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations int iChanel = calibData.Channel - 1; if (iChanel >= 0 && iChanel < iChanelsCount) { - log.Debug("ComPort: " + OptoComPortNr + " Channel: " + iChanel); + log.Debug( + $"Before UpdateFromSmart ch={iChanel + 1}: " + + $"volumeRawExtLast={volumeRawExtLast[iChanel]}, " + + $"timestampExtLast={timestampExtLast[iChanel]}, " + + $"VolumeCm={calibData.VolumeCm}, OverflowVolumeCm={calibData.OverflowVolumeCm}"); + optoData[bufferIx].UpdateFromSmart( calibData, optoDataCount, - Convert.ToSingle(Sequences.ProcessData.RefFlow.Val), + GetReferenceFlowSafe(), ref volumeRawExtLast[iChanel], ref timestampExtLast[iChanel]); @@ -1553,14 +1862,20 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations optoDataCount++; } - else + } + + private float GetReferenceFlowSafe() + { + try { - int iChanel = calibData.Channel - 1; - if (iChanel >= 0 && iChanel < iChanelsCount) - { - flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel); - } + if (Sequences.ProcessData.RefFlow != null) + return Convert.ToSingle(Sequences.ProcessData.RefFlow.Val); } + catch + { + } + + return 0.0f; } @@ -1610,7 +1925,11 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Info($"OPTHO {OptoComPortNr} FLOW Parsed opto data: " + dataFlow + " RX ← " + received); if (HandleFlowMarker()) - ResetDataBuffer(); + { + log.Debug("ReadOptoData() completed flow block detected."); + if (resetSerialBuffersOnCompletedFlowBlock) + ResetDataBuffer(); // no ResetDataBuffer() here + } } } @@ -1672,16 +1991,19 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return received; } + private const bool resetSerialBuffersOnCompletedFlowBlock = true; private void ResetDataBuffer() { - if (optoSerialPort != null) + lock (_serialReadSync) { - optoSerialPort.DiscardInBuffer(); - optoSerialPort.DiscardOutBuffer(); - log.Debug("-- Reaset Data Buffer --"); - return; + if (optoSerialPort != null && optoSerialPort.IsOpen) + { + optoSerialPort.DiscardInBuffer(); + optoSerialPort.DiscardOutBuffer(); + log.Debug("-- Reaset Data Buffer --"); + return; + } } - log.Debug("-- Reaset Data Buffer - no serial port --"); } @@ -1697,11 +2019,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations var readTask = Task.Run(() => { - lock (this) + try { - if (optoSerialPort == null) return string.Empty; - try + lock (_serialReadSync) { + if (optoSerialPort == null || !optoSerialPort.IsOpen) + return string.Empty; + string line = optoSerialPort.ReadLine(); byte[] bytes = optoSerialPort.Encoding.GetBytes(line); string received = HexFormatter.ToSerialHex(bytes); @@ -1709,31 +2033,26 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug("RX ← " + received); return line; } - catch (TimeoutException) - { - // ✅ No data received within 3 seconds - log.Debug($"ReadOptoData() OPTHO {OptoComPortNr} ReadLine timeout (3s) - continuing."); - - // Just continue without parsing - } - catch (Exception ex) - { - log.Error($"ReadOptoData() OPTHO {OptoComPortNr} Read error: {ex.Message}"); - } - - return string.Empty; } + catch (TimeoutException) + { + log.Debug($"ReadOptoData() OPTHO {OptoComPortNr} ReadLine timeout (3s) - continuing."); + } + catch (Exception ex) + { + log.Error($"ReadOptoData() OPTHO {OptoComPortNr} Read error: {ex.Message}"); + } + + return string.Empty; }); var completedTask = await Task.WhenAny(readTask, Task.Delay(timeoutMs)); if (completedTask == readTask) - { - return await readTask; // completed successfully - } + return await readTask; log.Debug("ReadOptoData timeout after " + timeoutMs + " ms"); - return string.Empty; // timeout case + return string.Empty; } public string ReadOptoDataWithTimeout(int timeoutMs = 5000) @@ -2261,10 +2580,11 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return await Task.Run(() => { log.Debug($"Try get End Volume! COM: {this.OptoComPortNr}"); - volumeLtr[channel0] = Double.NaN; + int ch = channel0 >= 0 ? channel0 : 0; + volumeLtr[ch] = Double.NaN; int counter = 0; - while (Double.IsNaN(volumeLtr[channel0]) && counter < 2) + while (Double.IsNaN(volumeLtr[ch]) && counter < 10) { counter++; try @@ -2277,9 +2597,19 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations StreamingDecoder _streamingDecode = new StreamingDecoder(true); _streamingDecode.DecodeMsg(readOptoDataWithTimeout); CalibrationRecord data = _streamingDecode.DataCalib; + if (data == null || !data.IsValid) + continue; + log.Info($"OPTHO {OptoComPortNr} Parsed optho data:" + data); - volumeLtr[channel0] = data.VolumeCm * 1000; - break; + + int dch = data.Channel - 1; + if (dch >= 0 && dch < iChanelsCount) + { + volumeLtr[dch] = data.VolumeCm * 1000; + channel0 = dch; + ch = dch; + break; + } } catch (Exception ex) { @@ -2339,10 +2669,12 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug($"Try get Start Volume! COM: {this.OptoComPortNr}"); Start(); + + int ch = channel0 >= 0 ? channel0 : 0; + volumeLtr0[ch] = Double.NaN; - volumeLtr0[channel0] = Double.NaN; int counter = 0; - while (Double.IsNaN(volumeLtr0[channel0]) && counter < 10) + while (Double.IsNaN(volumeLtr0[ch]) && counter < 10) { counter++; try @@ -2356,8 +2688,18 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations _streamingDecode.DecodeMsg(readOptoDataWithTimeout); CalibrationRecord data = _streamingDecode.DataCalib; log.Info($"OPTHO {OptoComPortNr} Parsed optho data:" + data); - volumeLtr0[channel0] = data.VolumeCm * 1000; - break; + if (data == null || !data.IsValid) + continue; + + int dch = data.Channel - 1; + if (dch >= 0 && dch < iChanelsCount) + { + volumeLtr0[dch] = data.VolumeCm * 1000; + channel0 = dch; + ch = dch; + break; + } + } catch (Exception ex) { @@ -2374,9 +2716,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations log.Debug($"Try get Start Volume! COM: {this.OptoComPortNr}, Volume: {volumeLtr0}"); if (optoSerialPort != null && optoSerialPort.IsOpen) CloseOptoSerialPort(); - if (!Double.IsNaN(volumeLtr0[channel0])) + if (!Double.IsNaN(volumeLtr0[ch])) { - beginWMState = volumeLtr0[channel0]; + beginWMState = volumeLtr0[ch]; ReadPulses(); return beginWMState; } @@ -2675,9 +3017,13 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations private OptoTelegramRaw[][] _rawStartEndByChannel; private OptoTelegramRaw[][] _recalculatedStartEndByChannel; - private OptoTelegramRaw[][] BuildStartEndByChannel(OptoTelegramRaw[] optoData, int optoDataCount) + private OptoTelegramRaw[][] BuildStartEndByChannel( + OptoTelegramRaw[] optoData, + int optoDataCount, + int startIx, + int endIx) { - var grouped = GroupRecordsPerChannel(optoData, 0, optoDataCount - 1, optoDataCount); + var grouped = GroupRecordsPerChannel(optoData, startIx, endIx, optoDataCount); var result = new OptoTelegramRaw[ChannelCount][]; for (int ch = 0; ch < ChannelCount; ch++) @@ -2706,8 +3052,52 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return; } - _rawStartEndByChannel = BuildStartEndByChannel(optoData, optoDataCount); - _recalculatedStartEndByChannel = RecalculateVolumeAndTimeDeltaPerChannel(optoData, optoDataCount); + log.Debug("-- BuildStartEndByChannel --"); + _rawStartEndByChannel = BuildStartEndByChannel(optoData, optoDataCount, TestStartTelegramIx, TestEndTelegramIx); + log.Debug("-- RecalculateVolumeAndTimeDeltaPerChannel --"); + _recalculatedStartEndByChannel = RecalculateVolumeAndTimeDeltaPerChannel(optoData, optoDataCount, TestStartTelegramIx, TestEndTelegramIx); + + log.Debug($"PrepareCalculatedChannelData: optoDataCount={optoDataCount}"); + + // 🔹 RAW DATA LOG + if (_rawStartEndByChannel != null) + { + for (int ch = 0; ch < ChannelCount; ch++) + { + var start = _rawStartEndByChannel[ch]?[0]; + var end = _rawStartEndByChannel[ch]?[1]; + + log.Debug( + $"RAW Ch{ch + 1}: " + + $"Start(V={start?.VolumeRawExt}, T={start?.TimestampExt}) | " + + $"End(V={end?.VolumeRawExt}, T={end?.TimestampExt})"); + } + } + else + { + log.Warn("RAW data is NULL"); + } + + // 🔹 RECALCULATED DATA LOG + if (_recalculatedStartEndByChannel != null) + { + for (int ch = 0; ch < ChannelCount; ch++) + { + var start = _recalculatedStartEndByChannel[ch]?[0]; + var end = _recalculatedStartEndByChannel[ch]?[1]; + + log.Debug( + $"RECALC Ch{ch + 1}: " + + $"Start(V={start?.VolumeRawExt}, T={start?.TimestampExt}) | " + + $"End(V={end?.VolumeRawExt}, T={end?.TimestampExt}) | " + + $"ΔV={(end != null && start != null ? end.VolumeRawExt - start.VolumeRawExt : 0)} | " + + $"ΔT={(end != null && start != null ? end.TimestampExt - start.TimestampExt : 0)}"); + } + } + else + { + log.Warn("RECALCULATED data is NULL"); + } } @@ -2729,18 +3119,34 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations var first = records[0]; var last = records[records.Length - 1]; - result[ch] = ( - ch, - last.TimestampExt - first.TimestampExt, - last.VolumeRawExt - first.VolumeRawExt - ); + double rawDelta = last.TimestampExt - first.TimestampExt; + double rawDeltaVol = last.VolumeRawExt - first.VolumeRawExt; + + log.Debug($"COM{OptoComPortNr} First raw: {first.rawDataToString()}"); + log.Debug($"COM{OptoComPortNr} Last raw: {last.rawDataToString()}"); + + if (rawDeltaVol < 0) + { + log.Error($"Negative delta volume for channel {ch + 1}: {rawDeltaVol}"); + } + + if (rawDelta < 0) + { + log.Error($"Negative delta time for channel {ch + 1}: {rawDelta}"); + } + + result[ch] = (ch, rawDelta, rawDeltaVol); + + log.Debug($" ---- COM{OptoComPortNr} ----"); + log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: First raw: {first.rawDataToString()}"); + log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: Last raw: {last.rawDataToString()}"); + log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: rawΔT={rawDelta} , rawΔVol={rawDeltaVol} "); + log.Debug($" --- ---"); } return result; } - - private OptoTelegramRaw[][] GroupRecordsPerChannel( OptoTelegramRaw[] data, int startIx, @@ -2786,12 +3192,14 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations private OptoTelegramRaw[][] RecalculateVolumeAndTimeDeltaPerChannel( OptoTelegramRaw[] optoData, - int optoDataCount) + int optoDataCount, + int startIx, + int endIx) { var recordByChannel = GroupRecordsPerChannel( optoData, - 0, - optoDataCount - 1, + startIx, + endIx, optoDataCount); var timeDelta = TimeDeltaPerChannel(recordByChannel); diff --git a/TBF/Rig/Sequences/SequenceBase.cs b/TBF/Rig/Sequences/SequenceBase.cs index 55859fe1c..17f972735 100644 --- a/TBF/Rig/Sequences/SequenceBase.cs +++ b/TBF/Rig/Sequences/SequenceBase.cs @@ -678,7 +678,8 @@ namespace TBF.Rig.Sequences /// On error or when STOP pressed /// foreach (var fmPump in PumpsWithFM) fmPump.TurnOff(); - + log.Debug("STOP or ERROR: Pumps with FM stopped!"); + if (inPath != null) { /// diff --git a/TBF/Rig/TestMethods/FlyingStartMassCollection/FlyingStartMassCollectionSeq.cs b/TBF/Rig/TestMethods/FlyingStartMassCollection/FlyingStartMassCollectionSeq.cs index 500086910..47a5c81eb 100644 --- a/TBF/Rig/TestMethods/FlyingStartMassCollection/FlyingStartMassCollectionSeq.cs +++ b/TBF/Rig/TestMethods/FlyingStartMassCollection/FlyingStartMassCollectionSeq.cs @@ -170,8 +170,8 @@ namespace TBF.Rig.TestMethods.FlyingStartMassCollection string testName = Results.Utils.GetTestName(test.Name, test.Repeats, repetitionNr); TestStartTime = DateTime.Now; - bool atleastOneGenesis = false; - atleastOneGenesis = GenesisHeadBatch.Start(sensPath.RegisterReaders, Program.LocalSettings.LastSNTexts); + // bool atleastOneGenesis = false; + // atleastOneGenesis = GenesisHeadBatch.Start(sensPath.RegisterReaders, Program.LocalSettings.LastSNTexts); //------------------------------------------------ Bridge.OnActivity(this, Strings.Checking_tank_capacity); @@ -452,23 +452,23 @@ namespace TBF.Rig.TestMethods.FlyingStartMassCollection } } - if (atleastOneGenesis) - { - log.Info($"Genesis - Starting... Test Name:{test.Name.ToLower()}."); - if (test.Name.ToLower().Contains("calib")) - { - log.Info("Genesis - Calibration starting."); - GenesisHeadBatch.BatchHolder.Value.MetersLogin(); - GenesisHeadBatch.BatchHolder.Value.MetersInitCalibration(); - - } - if (test.Name.ToLower().Contains("init")) - { - log.Info("Genesis - init starting."); - GenesisHeadBatch.BatchHolder.Value.MetersLogin(); - GenesisHeadBatch.BatchHolder.Value.MetersInitMeasurement(); - } - } + // if (atleastOneGenesis) + // { + // log.Info($"Genesis - Starting... Test Name:{test.Name.ToLower()}."); + // if (test.Name.ToLower().Contains("calib")) + // { + // log.Info("Genesis - Calibration starting."); + // GenesisHeadBatch.BatchHolder.Value.MetersLogin(); + // GenesisHeadBatch.BatchHolder.Value.MetersInitCalibration(); + // + // } + // if (test.Name.ToLower().Contains("init")) + // { + // log.Info("Genesis - init starting."); + // GenesisHeadBatch.BatchHolder.Value.MetersLogin(); + // GenesisHeadBatch.BatchHolder.Value.MetersInitMeasurement(); + // } + // } if (drainTheTank) { @@ -610,19 +610,32 @@ namespace TBF.Rig.TestMethods.FlyingStartMassCollection /// Measurement loop end StopRecordingStatistics(); - if (atleastOneGenesis) - { - if (test.Name.ToLower().Contains("calib")) - { - log.Info("Genesis - Calibration stopped."); - GenesisHeadBatch.BatchHolder.Value.MetersStopCalibration(); - } - else - { - log.Info("Genesis - Init measurement stopped."); - GenesisHeadBatch.BatchHolder.Value.MetersStopMeasurement(); - } - } + + + if (sensPath != null && sensPath.RegisterReaders != null) + { + foreach (var rr in sensPath.RegisterReaders) + { + var datastreamRR = rr as ISmartReader; + if (datastreamRR != null) + { + datastreamRR.StopDataStreamProcessing(); + } + } + } + // if (atleastOneGenesis) + // { + // if (test.Name.ToLower().Contains("calib")) + // { + // log.Info("Genesis - Calibration stopped."); + // GenesisHeadBatch.BatchHolder.Value.MetersStopCalibration(); + // } + // else + // { + // log.Info("Genesis - Init measurement stopped."); + // GenesisHeadBatch.BatchHolder.Value.MetersStopMeasurement(); + // } + // } /// /// (Berlin:) Water is stopped immediately after the test and before the 2nd mass measurement in case: diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRawTest.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRawTest.cs index 7b7db3a39..a20383dc5 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRawTest.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/common/OptoTelegramRawTest.cs @@ -12,13 +12,17 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.common private static CalibrationRecord CreateCalibrationRecord( int channel = 1, double volumeCm = 1.234, - double timeS = 12.5) + double overflowVolumeCm = 8.38860799804687, + double timeS = 12.5, + double overflowTimeS = 65536.0) { return new CalibrationRecord { Channel = channel, VolumeCm = volumeCm, - TimeS = timeS + OverflowVolumeCm = overflowVolumeCm, + TimeS = timeS, + OverflowTimeS = overflowTimeS }; } @@ -26,7 +30,12 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.common public void UpdateFromSmart_FirstSample_ShouldInitializeFields() { var telegram = new OptoTelegramRaw(); - var data = CreateCalibrationRecord(channel: 2, volumeCm: 2.5, timeS: 100.0); + var data = CreateCalibrationRecord( + channel: 2, + volumeCm: 2.5, + overflowVolumeCm: 8.38860799804687, + timeS: 100.0, + overflowTimeS: 65536.0); double lastVolume = double.NaN; double lastTimestamp = double.NaN; @@ -54,32 +63,479 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.common Assert.AreEqual(0, telegram.MagneticFieldRaw); } + [TestMethod] + public void UpdateFromSmart_RealGenesisDecodedSample_ShouldMapExpectedValues() + { + var telegram = new OptoTelegramRaw(); + + var data = CreateCalibrationRecord( + channel: 2, + volumeCm: 0.48025591796875, + overflowVolumeCm: 8.38860799804687, + timeS: 62727.8408813477, + overflowTimeS: 65536.0); + + double lastVolume = double.NaN; + double lastTimestamp = double.NaN; + + telegram.UpdateFromSmart(data, counter: 1, refFlow: 0.0f, ref lastVolume, ref lastTimestamp); + + Assert.AreEqual(1, telegram.IChannel()); + Assert.AreEqual(1, telegram.Counter); + Assert.AreEqual(0.0f, telegram.RefFlow); + Assert.AreEqual(OptoTelegramFlags.OK, telegram.Flags); + + Assert.AreEqual(480.25591796875, telegram.VolumeRaw, 1e-9); + Assert.AreEqual(480.25591796875, telegram.VolumeRawExt, 1e-9); + Assert.AreEqual(480.25591796875, lastVolume, 1e-9); + + Assert.AreEqual(62727.8408813477, telegram.Timestamp, 1e-9); + Assert.AreEqual(62727.8408813477, telegram.TimestampExt, 1e-9); + Assert.AreEqual(62727.8408813477, lastTimestamp, 1e-9); + + Assert.AreEqual(0, telegram.FlowRaw); + Assert.AreEqual(0, telegram.CheckSum); + Assert.AreEqual(0, telegram.Impedance); + Assert.AreEqual(0, telegram.EmfRaw); + Assert.AreEqual(0, telegram.MagneticFieldRaw); + } + [TestMethod] public void UpdateFromSmart_WhenVolumeDecreases_ShouldApplyVolumeRollover() { var telegram = new OptoTelegramRaw(); - var data = CreateCalibrationRecord(channel: 1, volumeCm: 1.0, timeS: 10.0); + var overflowVolumeCm = 8.38860799804687; + var overflowRaw = overflowVolumeCm * 1000.0; - double previousExtendedVolume = 5000.0; + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: 1.0, + overflowVolumeCm: overflowVolumeCm, + timeS: 10.0, + overflowTimeS: 65536.0); + + // Must be close enough to overflow so that current=1000 is interpreted as a wrap + double previousExtendedVolume = 8000.0; double previousExtendedTimestamp = 10.0; - telegram.UpdateFromSmart(data, counter: 1, refFlow: 0.5f, ref previousExtendedVolume, ref previousExtendedTimestamp); + telegram.UpdateFromSmart( + data, + counter: 1, + refFlow: 0.5f, + ref previousExtendedVolume, + ref previousExtendedTimestamp); double expectedVolumeRaw = 1000.0; - double expectedExtended = expectedVolumeRaw + StreamingDecoder.DefaultAccuDutOverflowVolumeCm * 1000.0; + double expectedExtended = expectedVolumeRaw + overflowRaw; Assert.AreEqual(expectedVolumeRaw, telegram.VolumeRaw, 1e-9); Assert.AreEqual(expectedExtended, telegram.VolumeRawExt, 1e-6); Assert.AreEqual(expectedExtended, previousExtendedVolume, 1e-6); } + + [TestMethod] + public void UpdateFromSmart_MultipleVolumeRollover_ShouldKeepIncreasingExtendedVolume() + { + var telegram = new OptoTelegramRaw(); + double overflowVolumeCm = 8.38860799804687; + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + // Safe sequence: small forward steps, natural wrap, small forward steps + double[] samples = + { + 7.5, + 8.0, + 0.2, + 1.0, + 1.8, + 2.6, + 3.4, + 4.2, + 5.0, + 5.8, + 6.6, + 7.4, + 8.1, + 0.3, + 1.1 + }; + + double previousExt = double.NaN; + + for (int i = 0; i < samples.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: samples[i], + overflowVolumeCm: overflowVolumeCm, + timeS: i, + overflowTimeS: 65536.0); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + double expectedRaw = samples[i] * 1000.0; + Assert.AreEqual(expectedRaw, telegram.VolumeRaw, 1e-9, $"Raw mismatch at index {i}"); + + if (!double.IsNaN(previousExt)) + { + Assert.IsTrue( + telegram.VolumeRawExt >= previousExt, + $"Volume should not decrease at index {i}. Prev={previousExt}, Current={telegram.VolumeRawExt}"); + } + + previousExt = telegram.VolumeRawExt; + } + } + + [TestMethod] + public void UpdateFromSmart_MultipleVolumeRollover_FirstToLastDeltaShouldBePositive() + { + var telegram = new OptoTelegramRaw(); + double overflowVolumeCm = 8.38860799804687; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + double[] samples = + { + 7.5, 8.0, 0.2, 1.0, 1.8, 2.6, 3.4, 4.2, 5.0, 5.8, 6.6, 7.4, 8.1, 0.3, 1.1 + }; + + double firstExt = double.NaN; + + for (int i = 0; i < samples.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: samples[i], + overflowVolumeCm: overflowVolumeCm, + timeS: i, + overflowTimeS: 65536.0); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + if (i == 0) + firstExt = telegram.VolumeRawExt; + } + + Assert.IsTrue(lastVolumeExt > firstExt, + $"Volume should increase from first to last. First={firstExt}, Last={lastVolumeExt}"); + } + + [TestMethod] + public void UpdateFromSmart_MultipleVolumeRollover_ShouldMatchPhysicalDelta() + { + var telegram = new OptoTelegramRaw(); + double overflowVolumeCm = 8.38860799804687; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + double[] samples = + { + 7.5, + 8.0, + 0.2, + 1.0, + 1.8, + 2.6, + 3.4, + 4.2, + 5.0, + 5.8, + 6.6, + 7.4, + 8.1, + 0.3, + 1.1 + }; + + double firstExt = double.NaN; + double lastExt = double.NaN; + + for (int i = 0; i < samples.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: samples[i], + overflowVolumeCm: overflowVolumeCm, + timeS: i, + overflowTimeS: 65536.0); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + if (i == 0) + firstExt = telegram.VolumeRawExt; + + if (i == samples.Length - 1) + lastExt = telegram.VolumeRawExt; + } + + double start = samples[0]; + double end = samples[samples.Length - 1]; + + int wraps = 0; + for (int i = 1; i < samples.Length; i++) + { + if (samples[i] < samples[i - 1]) + wraps++; + } + + double expectedDeltaCm = wraps * overflowVolumeCm + end - start; + double expectedDeltaRaw = expectedDeltaCm * 1000.0; + double actualDeltaRaw = lastExt - firstExt; + + Assert.AreEqual( + expectedDeltaRaw, + actualDeltaRaw, + 1e-6, + $"Delta mismatch. Wraps={wraps}, Expected={expectedDeltaRaw}, Actual={actualDeltaRaw}"); + } + + [TestMethod] + public void UpdateFromSmart_MultipleTimestampWrap_ShouldMatchPhysicalDelta() + { + var telegram = new OptoTelegramRaw(); + + double overflowTimeS = 65536.0; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + // Safe sequence: small forward steps, natural wrap, small forward steps, second wrap + double[] times = + { + 65530.0, + 65535.0, + 2.0, // wrap 1 + 10.0, + 100.0, + 1000.0, + 10000.0, + 30000.0, + 50000.0, + 65534.0, + 3.0, // wrap 2 + 20.0 + }; + + double firstExt = double.NaN; + double lastExt = double.NaN; + + for (int i = 0; i < times.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: 1.0, + overflowVolumeCm: 8.38860799804687, + timeS: times[i], + overflowTimeS: overflowTimeS); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + if (i == 0) + firstExt = telegram.TimestampExt; + + if (i == times.Length - 1) + lastExt = telegram.TimestampExt; + } + + double start = times[0]; + double end = times[times.Length - 1]; + + int wraps = 0; + for (int i = 1; i < times.Length; i++) + { + if (times[i] < times[i - 1]) + wraps++; + } + + double expectedDelta = wraps * overflowTimeS + end - start; + double actualDelta = lastExt - firstExt; + + Assert.AreEqual( + expectedDelta, + actualDelta, + 1e-6, + $"Timestamp delta mismatch. Wraps={wraps}, Expected={expectedDelta}, Actual={actualDelta}"); + } + + [TestMethod] + public void UpdateFromSmart_MultipleTimestampWrap_ShouldKeepIncreasingExtendedTimestamp() + { + var telegram = new OptoTelegramRaw(); + + double overflowTimeS = 65536.0; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + double[] times = + { + 65530.0, + 65535.0, + 2.0, + 10.0, + 100.0, + 1000.0, + 10000.0, + 30000.0, + 50000.0, + 65534.0, + 3.0, + 20.0 + }; + + double previousExt = double.NaN; + + for (int i = 0; i < times.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: 1.0, + overflowVolumeCm: 8.38860799804687, + timeS: times[i], + overflowTimeS: overflowTimeS); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + if (!double.IsNaN(previousExt)) + { + Assert.IsTrue( + telegram.TimestampExt >= previousExt, + $"Timestamp should not decrease at index {i}. Prev={previousExt}, Current={telegram.TimestampExt}"); + } + + previousExt = telegram.TimestampExt; + } + } + + [TestMethod] + public void UpdateFromSmart_MultipleVolumeRollover_ShouldProduceExpectedExtendedValues() + { + var telegram = new OptoTelegramRaw(); + double overflowVolumeCm = 8.38860799804687; + double overflowRaw = overflowVolumeCm * 1000.0; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + double[] samples = + { + 7.5, + 8.0, + 0.2, + 0.8, + 1.4 + }; + + double[] expectedExt = + { + 7500.0, + 8000.0, + 200.0 + overflowRaw, + 800.0 + overflowRaw, + 1400.0 + overflowRaw + }; + + for (int i = 0; i < samples.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: samples[i], + overflowVolumeCm: overflowVolumeCm, + timeS: i, + overflowTimeS: 65536.0); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + Assert.AreEqual(samples[i] * 1000.0, telegram.VolumeRaw, 1e-9, $"Raw mismatch at index {i}"); + Assert.AreEqual(expectedExt[i], telegram.VolumeRawExt, 1e-6, $"Mismatch at index {i}"); + } + } + + [TestMethod] + public void UpdateFromSmart_TwoVolumeRollovers_ShouldKeepExtendedVolumeIncreasing() + { + var telegram = new OptoTelegramRaw(); + double overflowVolumeCm = 8.38860799804687; + double overflowRaw = overflowVolumeCm * 1000.0; + + double lastVolumeExt = double.NaN; + double lastTimestampExt = double.NaN; + + double[] samples = + { + 6.8, + 8.1, + 0.3, + 1.2, + 2.1, + 3.0, + 4.0, + 7.5, + 8.1, + 0.4, + 1.1 + }; + + double[] expectedExt = + { + 6800.0, + 8100.0, + overflowRaw + 300.0, + overflowRaw + 1200.0, + overflowRaw + 2100.0, + overflowRaw + 3000.0, + overflowRaw + 4000.0, + overflowRaw + 7500.0, + overflowRaw + 8100.0, + 2.0 * overflowRaw + 400.0, + 2.0 * overflowRaw + 1100.0 + }; + + double previous = double.NaN; + + for (int i = 0; i < samples.Length; i++) + { + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: samples[i], + overflowVolumeCm: overflowVolumeCm, + timeS: i, + overflowTimeS: 65536.0); + + telegram.UpdateFromSmart(data, i, 0, ref lastVolumeExt, ref lastTimestampExt); + + Assert.AreEqual(samples[i] * 1000.0, telegram.VolumeRaw, 1e-9, $"Raw mismatch at index {i}"); + Assert.AreEqual(expectedExt[i], telegram.VolumeRawExt, 1e-6, $"Mismatch at index {i}"); + + if (!double.IsNaN(previous)) + { + Assert.IsTrue( + telegram.VolumeRawExt > previous, + $"Extended volume did not increase at index {i}. Prev={previous}, Current={telegram.VolumeRawExt}"); + } + + previous = telegram.VolumeRawExt; + } + } + + [TestMethod] public void UpdateFromSmart_WhenTimestampWraps_ShouldUnwrapForward() { var telegram = new OptoTelegramRaw(); double tsRange = StreamingDecoder.CpuTimeOverflowS; - var data = CreateCalibrationRecord(channel: 1, volumeCm: 1.0, timeS: 1.0); + var data = CreateCalibrationRecord( + channel: 1, + volumeCm: 1.0, + overflowVolumeCm: 8.38860799804687, + timeS: 1.0, + overflowTimeS: tsRange); double previousExtendedVolume = 1000.0; double previousExtendedTimestamp = tsRange - 0.25; diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisReaderTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisReaderTests.cs new file mode 100644 index 000000000..4ba98404a --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisReaderTests.cs @@ -0,0 +1,220 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.GenesisRegReader.common; +using TBF.Rig.RegisterReaders.GenesisRegReader.communication; +using TBF.Rig.RegisterReaders.GenesisRegReader.implementations; +using TBF.Rig.Sequences; +using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common; + +namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations +{ + + + + [TestClass] + public class GenesisReaderTests + { + [TestMethod] + public void RealInput_ShouldCalculate_StartEndVolumes_AndTimes() + { + var reader = new GenesisSmartReader(); + InitializeReaderForTest(reader); + + string[] realInputLines = LoadRealInputLines(); + Assert.IsTrue(realInputLines.Length > 0, "No test input lines were provided."); + + int processedCount = 0; + int firstValidIx = -1; + int lastValidIx = -1; + + foreach (var line in realInputLines) + { + bool blockCompleted; + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out blockCompleted); + + int optoDataCount = GetPrivateField(reader, "optoDataCount"); + if (optoDataCount > processedCount) + { + if (firstValidIx < 0) + firstValidIx = processedCount; + + lastValidIx = optoDataCount - 1; + processedCount = optoDataCount; + } + } + + Assert.IsTrue(processedCount > 0, "No valid calibration telegrams were parsed."); + + reader.TestStartTelegramIx = firstValidIx; + reader.TestEndTelegramIx = lastValidIx; + + object[] markArgs = { 0, 0 }; + InvokePrivate(reader, "AddTestStartEndMarksToData", markArgs); + InvokePrivate(reader, "DataStreamPostProcessing"); + InvokePrivate(reader, "PrepareCalculatedChannelData"); + + // first run: inspect values from debugger/log/output and then replace + Console.WriteLine($"VolumeLtrStart={reader.VolumeLtrStart}"); + Console.WriteLine($"VolumeLtrEnd={reader.VolumeLtrEnd}"); + Console.WriteLine($"TimestampSecStart={reader.TimestampSecStart}"); + Console.WriteLine($"TimestampSecEnd={reader.TimestampSecEnd}"); + + const double expectedVolumeStart = 0.0; // replace with real value + const double expectedVolumeEnd = 0.0; // replace with real value + const double expectedTimeStart = 0.0; // replace with real value + const double expectedTimeEnd = 0.0; // replace with real value + + const double tolerance = 0.000001; + + Assert.AreEqual(expectedVolumeStart, reader.VolumeLtrStart, tolerance, "VolumeLtrStart mismatch"); + Assert.AreEqual(expectedVolumeEnd, reader.VolumeLtrEnd, tolerance, "VolumeLtrEnd mismatch"); + Assert.AreEqual(expectedTimeStart, reader.TimestampSecStart, tolerance, "TimestampSecStart mismatch"); + Assert.AreEqual(expectedTimeEnd, reader.TimestampSecEnd, tolerance, "TimestampSecEnd mismatch"); + } + + [TestMethod] + public void RealInput_ShouldSupport_RolloverNormalization() + { + var reader = new GenesisSmartReader(); + InitializeReaderForTest(reader); + + string[] realInputLines = LoadRealInputLinesWithRollover(); + Assert.IsTrue(realInputLines.Length > 0, "No rollover input lines were provided."); + + foreach (var line in realInputLines) + { + bool blockCompleted; + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out blockCompleted); + } + + int optoDataCount = GetPrivateField(reader, "optoDataCount"); + Assert.IsTrue(optoDataCount > 1, "Need at least 2 valid telegrams."); + + reader.TestStartTelegramIx = 0; + reader.TestEndTelegramIx = optoDataCount - 1; + + object[] markArgs = { 0, 0 }; + InvokePrivate(reader, "AddTestStartEndMarksToData", markArgs); + InvokePrivate(reader, "DataStreamPostProcessing"); + InvokePrivate(reader, "PrepareCalculatedChannelData"); + + Assert.IsTrue(reader.TimestampSecEnd >= reader.TimestampSecStart, + "Normalized end time should be >= start time"); + Assert.IsTrue(reader.VolumeLtrEnd >= reader.VolumeLtrStart, + "Normalized end volume should be >= start volume"); + } + + private static void InitializeReaderForTest(GenesisSmartReader reader) + { + const int channelCount = 3; + + SetPrivateField(reader, "volumeRawExtLast", new double[channelCount]); + SetPrivateField(reader, "timestampExtLast", new double[channelCount]); + + SetPrivateField(reader, "lastTimestamp", new double[channelCount]); + SetPrivateField(reader, "timestampSec", Enumerable.Repeat(double.NaN, channelCount).ToArray()); + SetPrivateField(reader, "timestampSec0", Enumerable.Repeat(double.NaN, channelCount).ToArray()); + + SetPrivateField(reader, "lastVolumeRaw", new double[channelCount]); + SetPrivateField(reader, "volumeLtr", Enumerable.Repeat(double.NaN, channelCount).ToArray()); + SetPrivateField(reader, "volumeLtr0", Enumerable.Repeat(double.NaN, channelCount).ToArray()); + + var optoData = new OptoTelegramRaw[GenesisSmartReader.OptoDataBufferSize]; + for (int i = 0; i < optoData.Length; i++) + optoData[i] = new OptoTelegramRaw(); + + SetPrivateField(reader, "optoData", optoData); + SetPrivateField(reader, "optoDataCount", 0); + SetPrivateField(reader, "toBeFlushed", new OptoTelegramRaw()); + + SetPrivateField(reader, "flowDirectionDetection", new FlowDirectionDetection()); + SetPrivateField(reader, "dataStreamState", DataStreamState.ProcessAndSave); + SetPrivateField(reader, "synchronized", false); + SetPrivateField(reader, "synchronized2", false); + SetPrivateField(reader, "partOfTelegram", string.Empty); + SetPrivateField(reader, "startDataProcessing", true); + + reader.TestStartTelegramIx = 0; + reader.TestEndTelegramIx = 0; + + } + + + private static string[] LoadRealInputLines() + { + return new[] + { + // group 1 + "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", + "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", + "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", + + // group 2 (next real lines from your log) + "@h 1 0 0A1F59C5 00017A44 00115C46 72E1596C 00000400 00001999 7D91B653 7D4F37E7 000191E7 0C 062E4A9D 5332", + "@h 2 0 0A1B1FE9 00017B80 00116B57 72B77428 00000400 0000199B 7DC09689 7B570007 000191E7 0C 062E5327 95BE", + "@h 3 0 0A1DF1D6 00017EA9 00118038 741F80F4 00000400 00001999 7E58A62F 7CC2C607 000191E7 0C 062E5BAF D40F" + }; + } + + private static string[] LoadRealInputLinesWithRollover() + { + return new[] + { + // --- FIRST 3 (before rollover) --- + "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", + "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", + "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", + + // --- LAST 3 (after rollover / later in log) --- + "@h 1 0 00000010 00000020 00000030 00000040 00000400 00000001 00000050 00000060 00000070 0C 00000080 1111", + "@h 2 0 00000011 00000021 00000031 00000041 00000400 00000002 00000051 00000061 00000071 0C 00000081 2222", + "@h 3 0 00000012 00000022 00000032 00000042 00000400 00000003 00000052 00000062 00000072 0C 00000082 3333", + }; + } + + private static void SetPrivateField(object target, string fieldName, object value) + { + var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(field, $"Field '{fieldName}' not found."); + field.SetValue(target, value); + } + + private static T GetPrivateField(object target, string fieldName) + { + var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(field, $"Field '{fieldName}' not found."); + return (T)field.GetValue(target); + } + + private static object InvokePrivate(object target, string methodName, object[] args = null) + { + var methods = target.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .Where(m => m.Name == methodName) + .ToList(); + + Assert.IsTrue(methods.Count > 0, $"Method '{methodName}' not found."); + + var method = methods.First(); + + if (args == null) + return method.Invoke(target, null); + + var parameters = method.GetParameters(); + if (parameters.Length != args.Length) + { + throw new InvalidOperationException( + $"Method '{methodName}' expects {parameters.Length} parameters, but {args.Length} were provided."); + } + + var invokeArgs = new object[args.Length]; + Array.Copy(args, invokeArgs, args.Length); + + return method.Invoke(target, invokeArgs); + } + } + +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs index 7c9302682..0a2a2dccf 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelAveragingTests.cs @@ -46,6 +46,18 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations field.SetValue(null, value); } + private static void InvokePrepareCalculatedChannelData(GenesisSmartReader reader) + { + var method = typeof(GenesisSmartReader).GetMethod( + "PrepareCalculatedChannelData", + BindingFlags.Instance | BindingFlags.NonPublic); + + if (method == null) + throw new MissingMethodException(typeof(GenesisSmartReader).FullName, "PrepareCalculatedChannelData"); + + method.Invoke(reader, null); + } + private static OptoTelegramRaw CreateOptoRecord(int channel, double volumeRawExt, double timestampExt, int counter) { return new OptoTelegramRaw @@ -96,7 +108,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations } [TestMethod] - public void SimulatedInterleaved20x3ChannelStream_ShouldComputeStartAndEndVolumeAndTimeCorrectly() + public void SimulatedInterleaved20x3ChannelStream_ShouldComputeRecalculatedStartAndEndVolumeAndTimeCorrectly() { var reader = CreateReaderWithOptoBuffer(); var optoData = GetPrivateField(reader, "optoData"); @@ -118,15 +130,20 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.TestStartTelegramIx = 2; reader.TestEndTelegramIx = index - 1; + InvokePrepareCalculatedChannelData(reader); + Assert.AreEqual(200.0, reader.VolumeLtrStart, 1e-9); - Assert.AreEqual(217.0, reader.VolumeLtrEnd, 1e-9); + Assert.AreEqual(219.0, reader.VolumeLtrEnd, 1e-9); Assert.AreEqual(1.0, reader.TimestampSecStart, 1e-9); - Assert.AreEqual(18.0, reader.TimestampSecEnd, 1e-9); + Assert.AreEqual(20.0, reader.TimestampSecEnd, 1e-9); + + Assert.AreEqual(200.0, reader.VolumeLtrStartRaw, 1e-9); + Assert.AreEqual(219.0, reader.VolumeLtrEndRaw, 1e-9); } [TestMethod] - public void VolumeLtrEnd_ShouldIgnoreInvalidTrailingRecords_AndUsePreviousValidPerChannelSamples() + public void VolumeLtrEnd_ShouldRecalculateShorterChannelToLongestChannelTimeSpan() { var reader = CreateReaderWithOptoBuffer(); var optoData = GetPrivateField(reader, "optoData"); @@ -145,7 +162,11 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations index++; } - // group 8 ch1 is index 25, group 9 ch1 is index 28 + // channel 1 loses its last two valid records + // valid ch1 becomes: 200..207 with timestamps 1..8 + // longest channel deltaTime is 9 (channels 0 and 2) + // ch1 deltaVolume = 7, deltaTime = 7 => recalculated deltaVolume = 7 * 9 / 7 = 9 + // recalculated end ch1 = 200 + 9 = 209 optoData[25].Flags = OptoTelegramFlags.InvalidTelegram; optoData[28].Flags = OptoTelegramFlags.InvalidTelegram; @@ -153,8 +174,31 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.TestStartTelegramIx = 2; reader.TestEndTelegramIx = index - 1; + InvokePrepareCalculatedChannelData(reader); + Assert.AreEqual(200.0, reader.VolumeLtrStart, 1e-9); - Assert.AreEqual(206.33333333333334, reader.VolumeLtrEnd, 1e-9); + Assert.AreEqual(209.0, reader.VolumeLtrEnd, 1e-9); + + Assert.AreEqual(208.33333333333334, reader.VolumeLtrEndRaw, 1e-9); + + Assert.AreEqual(100.0, reader.VolumeLtrStartCh1, 1e-9); + Assert.AreEqual(200.0, reader.VolumeLtrStartCh2, 1e-9); + Assert.AreEqual(300.0, reader.VolumeLtrStartCh3, 1e-9); + + Assert.AreEqual(109.0, reader.VolumeLtrEndCh1, 1e-9); + Assert.AreEqual(209.0, reader.VolumeLtrEndCh2, 1e-9); + Assert.AreEqual(309.0, reader.VolumeLtrEndCh3, 1e-9); + + Assert.AreEqual(1.0, reader.TimestampSecStart, 1e-9); + Assert.AreEqual(10.0, reader.TimestampSecEnd, 1e-9); + + Assert.AreEqual(1.0, reader.TimestampSecStartCh1, 1e-9); + Assert.AreEqual(1.0, reader.TimestampSecStartCh2, 1e-9); + Assert.AreEqual(1.0, reader.TimestampSecStartCh3, 1e-9); + + Assert.AreEqual(10.0, reader.TimestampSecEndCh1, 1e-9); + Assert.AreEqual(10.0, reader.TimestampSecEndCh2, 1e-9); + Assert.AreEqual(10.0, reader.TimestampSecEndCh3, 1e-9); } [TestMethod] @@ -178,8 +222,12 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.TestStartTelegramIx = 2; reader.TestEndTelegramIx = 8; + InvokePrepareCalculatedChannelData(reader); + Assert.AreEqual(0.0, reader.VolumeLtrStart, 1e-9); Assert.AreEqual(0.0, reader.VolumeLtrEnd, 1e-9); + Assert.AreEqual(0.0, reader.TimestampSecStart, 1e-9); + Assert.AreEqual(0.0, reader.TimestampSecEnd, 1e-9); } [TestMethod] @@ -205,26 +253,66 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.TestStartTelegramIx = 2; reader.TestEndTelegramIx = index - 1; + InvokePrepareCalculatedChannelData(reader); + Assert.AreEqual(100.0, reader.VolumeLtrStartCh1, 1e-9); Assert.AreEqual(200.0, reader.VolumeLtrStartCh2, 1e-9); Assert.AreEqual(300.0, reader.VolumeLtrStartCh3, 1e-9); - Assert.AreEqual(117.0, reader.VolumeLtrEndCh1, 1e-9); - Assert.AreEqual(217.0, reader.VolumeLtrEndCh2, 1e-9); - Assert.AreEqual(317.0, reader.VolumeLtrEndCh3, 1e-9); + Assert.AreEqual(119.0, reader.VolumeLtrEndCh1, 1e-9); + Assert.AreEqual(219.0, reader.VolumeLtrEndCh2, 1e-9); + Assert.AreEqual(319.0, reader.VolumeLtrEndCh3, 1e-9); Assert.AreEqual(1.0, reader.TimestampSecStartCh1, 1e-9); Assert.AreEqual(1.0, reader.TimestampSecStartCh2, 1e-9); Assert.AreEqual(1.0, reader.TimestampSecStartCh3, 1e-9); - Assert.AreEqual(18.0, reader.TimestampSecEndCh1, 1e-9); - Assert.AreEqual(18.0, reader.TimestampSecEndCh2, 1e-9); - Assert.AreEqual(18.0, reader.TimestampSecEndCh3, 1e-9); + Assert.AreEqual(20.0, reader.TimestampSecEndCh1, 1e-9); + Assert.AreEqual(20.0, reader.TimestampSecEndCh2, 1e-9); + Assert.AreEqual(20.0, reader.TimestampSecEndCh3, 1e-9); Assert.AreEqual(200.0, reader.VolumeLtrStart, 1e-9); - Assert.AreEqual(217.0, reader.VolumeLtrEnd, 1e-9); + Assert.AreEqual(219.0, reader.VolumeLtrEnd, 1e-9); Assert.AreEqual(1.0, reader.TimestampSecStart, 1e-9); - Assert.AreEqual(18.0, reader.TimestampSecEnd, 1e-9); + Assert.AreEqual(20.0, reader.TimestampSecEnd, 1e-9); + + Assert.AreEqual(200.0, reader.VolumeLtrStartRaw, 1e-9); + Assert.AreEqual(219.0, reader.VolumeLtrEndRaw, 1e-9); + } + + [TestMethod] + public void VolumeLtrEnd_ShouldAverageOnlyAvailableChannels() + { + var reader = CreateReaderWithOptoBuffer(); + var optoData = GetPrivateField(reader, "optoData"); + + int idx = 0; + + for (int i = 0; i < 5; i++) + { + optoData[idx++] = CreateOptoRecord(0, 10 + i, 100 + i, idx); + optoData[idx++] = CreateOptoRecord(1, 20 + i, 100 + i, idx); + } + + SetPrivateField(reader, "optoDataCount", idx); + reader.TestStartTelegramIx = 1; + reader.TestEndTelegramIx = idx - 1; + + InvokePrepareCalculatedChannelData(reader); + + Assert.AreEqual(15.0, reader.VolumeLtrStart, 1e-9); + Assert.AreEqual(19.0, reader.VolumeLtrEnd, 1e-9); + + Assert.AreEqual(10.0, reader.VolumeLtrStartCh1, 1e-9); + Assert.AreEqual(20.0, reader.VolumeLtrStartCh2, 1e-9); + Assert.AreEqual(0.0, reader.VolumeLtrStartCh3, 1e-9); + + Assert.AreEqual(14.0, reader.VolumeLtrEndCh1, 1e-9); + Assert.AreEqual(24.0, reader.VolumeLtrEndCh2, 1e-9); + Assert.AreEqual(0.0, reader.VolumeLtrEndCh3, 1e-9); + + Assert.AreEqual(100.0, reader.TimestampSecStart, 1e-9); + Assert.AreEqual(104.0, reader.TimestampSecEnd, 1e-9); } } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs index 6db6517e9..01d5f7e74 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderTest.cs @@ -39,6 +39,18 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations return (T)field.GetValue(target); } + private static void InvokePrepareCalculatedChannelData(GenesisSmartReader reader) + { + var method = typeof(GenesisSmartReader).GetMethod( + "PrepareCalculatedChannelData", + BindingFlags.Instance | BindingFlags.NonPublic); + + if (method == null) + throw new MissingMethodException(typeof(GenesisSmartReader).FullName, "PrepareCalculatedChannelData"); + + method.Invoke(reader, null); + } + private static void InitializeThreeChannelState(GenesisSmartReader reader) { SetPrivateField(reader, "volumeRawExtLast", new double[ChannelCount]); @@ -66,7 +78,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.IsTrue(fake.IsOpen); Assert.IsTrue(fake.OpenCalls >= 1); Assert.AreEqual(2, fake.DiscardInCalls); - Assert.AreEqual(1, fake.DiscardOutCalls); + Assert.AreEqual(2, fake.DiscardOutCalls); } [TestMethod] @@ -136,15 +148,9 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations } [DataTestMethod] - [DataRow( - "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", - 0)] - [DataRow( - "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", - 1)] - [DataRow( - "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", - 2)] + [DataRow("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", 0)] + [DataRow("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", 1)] + [DataRow("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", 2)] public void ProcessOptoLine_ShouldUpdateExpectedChannel(string line, int expectedChannelIndex) { var fake = new FakeSerialDriver(); @@ -156,7 +162,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.optoSerialPort = fake; InitializeThreeChannelState(reader); - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool resetDataBuffer); var volumeRawExtLast = GetPrivateField(reader, "volumeRawExtLast"); var timestampExtLast = GetPrivateField(reader, "timestampExtLast"); @@ -195,7 +201,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var ev = reader.Run(); Assert.AreEqual(Event.ReadRegisterDone, ev); - Assert.AreEqual(4.0, reader.WMVolume, 1E-6); // average of (3, 4, 5) + Assert.AreEqual(4.0, reader.WMVolume, 1E-6); Assert.AreEqual(4000, reader.WMPulses); } @@ -214,16 +220,16 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var line = "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD"; - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool resetDataBuffer); int currentTelegramIx = GetPrivateField(reader, "currentTelegramIx"); Assert.AreEqual(0, currentTelegramIx); } [DataTestMethod] - [DataRow( "@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", 0)] - [DataRow( "@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", 1)] - [DataRow( "@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", 2)] + [DataRow("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", 0)] + [DataRow("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", 1)] + [DataRow("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", 2)] public void ProcessOptoLine_ShouldInsertTelegramAndUpdateExpectedChannel(string line, int expectedChannelIndex) { var fake = new FakeSerialDriver(); @@ -233,7 +239,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.Initialize(); reader.optoSerialPort = fake; - reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool ResetDataBuffer); + reader.ProcessOptoLine(line, DataStreamState.ProcessAndSave, out bool resetDataBuffer); var volumeRawExtLast = GetPrivateField(reader, "volumeRawExtLast"); var timestampExtLast = GetPrivateField(reader, "timestampExtLast"); @@ -276,10 +282,8 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations "Inserted telegram TimestampExt should match updated channel cache."); } - - [TestMethod] - public void VolumeLtrStart_And_VolumeLtrEnd_ShouldUsePerChannelAverages() + public void VolumeLtrStart_And_VolumeLtrEnd_ShouldUsePreparedCachedChannelData() { var fake = new FakeSerialDriver(); var reader = new GenesisSmartReader(CreateCfg(), () => fake); @@ -287,14 +291,6 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var optoData = GetPrivateField(reader, "optoData"); - // Build 18 records: 6 groups of channels 0,1,2 - // Start group average = (10 + 20 + 30)/3 = 20 - // Last 5 per channel: - // ch0: 11,12,13,14,15 => avg 13 - // ch1: 21,22,23,24,25 => avg 23 - // ch2: 31,32,33,34,35 => avg 33 - // final average = (13 + 23 + 33)/3 = 23 - int idx = 0; for (int group = 0; group < 6; group++) { @@ -304,11 +300,19 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations } SetPrivateField(reader, "optoDataCount", 18); - reader.TestStartTelegramIx = 2; // first full 3-channel group - reader.TestEndTelegramIx = 17; // last record + reader.TestStartTelegramIx = 1; + reader.TestEndTelegramIx = 17; - Assert.AreEqual(20.0, reader.VolumeLtrStart, 1e-9); - Assert.AreEqual(23.0, reader.VolumeLtrEnd, 1e-9); + InvokePrepareCalculatedChannelData(reader); + + Assert.AreEqual((11.0 + 20.0 + 30.0) / 3.0, reader.VolumeLtrStart, 1e-9); + Assert.AreEqual((15.0 + 25.0 + 35.0) / 3.0, reader.VolumeLtrEnd, 1e-9); + + Assert.AreEqual(100.0, reader.TimestampSecStart, 1e-9); + Assert.AreEqual(105.0, reader.TimestampSecEnd, 1e-9); + + Assert.AreEqual((11.0 + 20.0 + 30.0) / 3.0, reader.VolumeLtrStartRaw, 1e-9); + Assert.AreEqual((15.0 + 25.0 + 35.0) / 3.0, reader.VolumeLtrEndRaw, 1e-9); } private static OptoTelegramRaw CreateOptoRecord(int channel, double volumeRawExt, double timestampExt) @@ -321,7 +325,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations iChannel = channel }; } - + [TestMethod] public void VolumeLtrEnd_ShouldAverageOnlyAvailableChannels() { @@ -333,7 +337,6 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations int idx = 0; - // only channels 0 and 1 for (int i = 0; i < 5; i++) { optoData[idx++] = CreateOptoRecord(0, 10 + i, 100 + i); @@ -344,10 +347,12 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.TestStartTelegramIx = 1; reader.TestEndTelegramIx = idx - 1; - // ch0 avg = 12, ch1 avg = 22 => total avg = 17 - Assert.AreEqual(17.0, reader.VolumeLtrEnd, 1e-9); + InvokePrepareCalculatedChannelData(reader); + + Assert.AreEqual(15.5, reader.VolumeLtrStart, 1e-9); + Assert.AreEqual(19.0, reader.VolumeLtrEnd, 1e-9); } - + [TestMethod] public void ProcessOptoLine_Block_f_h1_h2_h3_f_ShouldResetBuffersOnlyAfterLastF() { @@ -361,30 +366,25 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations bool reset; - // first @f reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); Assert.IsFalse(reset, "Reset must not happen on the first @f."); - // @h 1 reader.ProcessOptoLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331", DataStreamState.ProcessAndSave, out reset); Assert.IsFalse(reset, "Reset must not happen after @h 1."); - // @h 2 reader.ProcessOptoLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD", DataStreamState.ProcessAndSave, out reset); Assert.IsFalse(reset, "Reset must not happen after @h 2."); - // @h 3 reader.ProcessOptoLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E", DataStreamState.ProcessAndSave, out reset); Assert.IsFalse(reset, "Reset must not happen immediately after @h 3."); - // trailing @f reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); Assert.IsTrue(reset, "Reset must happen after trailing @f that closes the h1/h2/h3 block."); } - + [TestMethod] public void ReadOptoData_FullBlock_ShouldDiscardBuffersAfterClosingF() { @@ -399,8 +399,8 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations var reader = new GenesisSmartReader(CreateCfg(), () => fake); reader.Initialize(); - fake.Open(); // important - reader.optoSerialPort = fake; // assign opened fake + fake.Open(); + reader.optoSerialPort = fake; reader.ResetAfterBlockRepetitions = 1; var readMethod = typeof(GenesisSmartReader).GetMethod( @@ -420,7 +420,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.AreEqual(1, fake.DiscardInCalls, "Input buffer should be discarded once after completed block."); Assert.AreEqual(1, fake.DiscardOutCalls, "Output buffer should be discarded once after completed block."); } - + [TestMethod] public void ProcessOptoLine_LastF_AfterH3_ShouldRequestBufferReset() { @@ -431,7 +431,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.Initialize(); reader.optoSerialPort = fake; reader.ResetAfterBlockRepetitions = 1; - + bool reset; reader.ProcessOptoLine("@f AA754B 4D0CEE78 5D89", DataStreamState.ProcessAndSave, out reset); @@ -449,7 +449,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); Assert.IsTrue(reset, "Closing @f after @h 3 must request reset."); } - + [TestMethod] public void ProcessOptoLine_LastF_AfterH3_ShouldNotRequestReset_WhenRepetitionCountIsGreaterThanOne() { @@ -478,7 +478,7 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); Assert.IsFalse(reset, "Reset must not happen after the first completed block when repetition count is 3."); } - + [TestMethod] public void ProcessOptoLine_ShouldResetOnlyAfterThirdCompletedBlock() { @@ -517,9 +517,9 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.IsTrue(reset, "Reset must happen on third full block."); } } - + [TestMethod] - public void ProcessOptoLine_ShouldResetOnlyAfterThirdCompletedBlock_2() + public void ProcessOptoLine_ShouldResetOnlyAfterFifthCompletedBlock() { var fake = new FakeSerialDriver(); fake.Open(); @@ -551,11 +551,10 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations reader.ProcessOptoLine("@f AA7C01 4D0CFE76 B08F", DataStreamState.ProcessAndSave, out reset); if (repetition < 5) - Assert.IsFalse(reset, "Reset must not happen before third full block."); + Assert.IsFalse(reset, "Reset must not happen before fifth full block."); else - Assert.IsTrue(reset, "Reset must happen on third full block."); + Assert.IsTrue(reset, "Reset must happen on fifth full block."); } } - } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderThreadedReadTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderThreadedReadTests.cs new file mode 100644 index 000000000..ba0158fe7 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderThreadedReadTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Reflection; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.GenesisRegReader; +using TBF.Rig.RegisterReaders.GenesisRegReader.common; +using TBF.Rig.RegisterReaders.GenesisRegReader.implementations; + +namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations +{ + [TestClass] + public class GenesisSmartReaderThreadedReadTests + { + private const int ChannelCount = 3; + + private static GenesisCfg CreateCfg() + { + var cfg = new GenesisCfg(null); + cfg.OptoComPortNr = 7; + cfg.RfidComPortNr = 8; + return cfg; + } + + private static void SetPrivateField(object target, string fieldName, object value) + { + var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) + throw new MissingFieldException(target.GetType().FullName, fieldName); + + field.SetValue(target, value); + } + + private static T GetPrivateField(object target, string fieldName) + { + var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) + throw new MissingFieldException(target.GetType().FullName, fieldName); + + return (T)field.GetValue(target); + } + + private static bool WaitUntil(Func condition, int timeoutMs = 1500, int pollMs = 20) + { + var start = Environment.TickCount; + + while (Environment.TickCount - start < timeoutMs) + { + if (condition()) + return true; + + Thread.Sleep(pollMs); + } + + return condition(); + } + + private static void InitializeThreeChannelState(GenesisSmartReader reader) + { + SetPrivateField(reader, "volumeRawExtLast", new double[ChannelCount]); + SetPrivateField(reader, "timestampExtLast", new double[ChannelCount]); + + SetPrivateField(reader, "lastVolumeRaw", new double[ChannelCount]); + SetPrivateField(reader, "lastTimestamp", new double[ChannelCount]); + + SetPrivateField(reader, "volumeLtr", new[] { 0.0, 0.0, 0.0 }); + SetPrivateField(reader, "volumeLtr0", new[] { 0.0, 0.0, 0.0 }); + + SetPrivateField(reader, "timestampSec", new[] { 0.0, 0.0, 0.0 }); + SetPrivateField(reader, "timestampSec0", new[] { 0.0, 0.0, 0.0 }); + } + + [TestMethod] + public void Start_ShouldOpenPort_AndStartBackgroundReadLoop() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + reader.Start(); + + Assert.IsTrue(fake.IsOpen); + Assert.IsTrue(fake.OpenCalls >= 1); + + var readLoopTask = GetPrivateField(reader, "_readLoopTask"); + Assert.IsNotNull(readLoopTask); + } + + [TestMethod] + public void BackgroundReadLoop_ShouldMoveIncomingLinesToQueue() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + InitializeThreeChannelState(reader); + reader.Start(); + + fake.EnqueueLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331"); + + bool queued = WaitUntil(() => + { + var queue = GetPrivateField>(reader, "_receivedLines"); + return !queue.IsEmpty; + }); + + Assert.IsTrue(queued, "Expected background task to enqueue received serial line."); + } + + [TestMethod] + public void RunDeviceBefore_ShouldDrainQueue_AndInsertTelegram() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + InitializeThreeChannelState(reader); + reader.Start(); + + fake.EnqueueLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD"); + + bool queued = WaitUntil(() => + { + var queue = GetPrivateField>(reader, "_receivedLines"); + return !queue.IsEmpty; + }); + + Assert.IsTrue(queued, "Expected line to be queued before processing."); + + reader.RunDeviceBefore(); + + int optoDataCount = GetPrivateField(reader, "optoDataCount"); + var optoData = GetPrivateField(reader, "optoData"); + + Assert.AreEqual(1, optoDataCount); + Assert.AreEqual(1, optoData[0].IChannel()); + } + + [TestMethod] + public void RunDeviceBefore_ShouldDrainAllQueuedLines() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + InitializeThreeChannelState(reader); + reader.Start(); + + fake.EnqueueLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331"); + fake.EnqueueLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD"); + fake.EnqueueLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E"); + + bool queued = WaitUntil(() => + { + var queue = GetPrivateField>(reader, "_receivedLines"); + return !queue.IsEmpty; + }); + + Assert.IsTrue(queued); + + reader.RunDeviceBefore(); + + int optoDataCount = GetPrivateField(reader, "optoDataCount"); + var queueAfter = GetPrivateField>(reader, "_receivedLines"); + + Assert.AreEqual(3, optoDataCount); + Assert.IsTrue(queueAfter.IsEmpty, "Queue should be empty after RunDeviceBefore drains it."); + } + + [TestMethod] + public void StopDataStreamProcessing_ShouldStopReadLoop_AndClosePort() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + reader.Start(); + + Assert.IsTrue(fake.IsOpen); + + reader.StopDataStreamProcessing(); + + Assert.IsFalse(fake.IsOpen); + + var cts = GetPrivateField(reader, "_readLoopCts"); + var task = GetPrivateField(reader, "_readLoopTask"); + + Assert.IsNull(cts); + Assert.IsNull(task); + } + + [TestMethod] + public void IncomingLines_ShouldNotBeProcessedUntilRunDeviceBeforeIsCalled() + { + var fake = new FakeSerialDriver(); + var reader = new GenesisSmartReader(CreateCfg(), () => fake); + + reader.Initialize(); + InitializeThreeChannelState(reader); + reader.Start(); + + fake.EnqueueLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E"); + + bool queued = WaitUntil(() => + { + var queue = GetPrivateField>(reader, "_receivedLines"); + return !queue.IsEmpty; + }); + + Assert.IsTrue(queued); + + int optoDataCountBefore = GetPrivateField(reader, "optoDataCount"); + Assert.AreEqual(0, optoDataCountBefore, "Background thread should only enqueue, not process."); + + reader.RunDeviceBefore(); + + int optoDataCountAfter = GetPrivateField(reader, "optoDataCount"); + Assert.AreEqual(1, optoDataCountAfter); + } + } +} \ No newline at end of file diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index 7dd5a077b..d4ba19ace 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -108,9 +108,11 @@ + +