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 @@ + +