From b8f6af7112b0a56922b4e1fa54c7b67fb94f1e84 Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Wed, 16 Sep 2026 10:23:04 +0200 Subject: [PATCH] Fix point 4. - take % validation from procedure to Q3 Calib factor CH1-3 verification Implement error limit configuration for Q3 calibration factors. - Add default and configurable error limits for Q3 calibration validation. - Introduce `CalculateQ3CalibrationWithErrorLimits` for flexible validation handling. - Overhaul Q3 workflow to respect configured limits, with fallback to defaults. - Extend tests for calibration, including boundary and invalid inputs. --- .../communication/OptoHeadTest.cs | 32 ++- .../implementations/GenesisSmartReader.cs | 73 ++++- .../GenesisCommunicationForm.cs | 4 +- .../GenesisReadDataRegressionTests.cs | 263 ++++++++++++++++++ .../GenesisSmartReader_Q3Test.cs | 36 ++- 5 files changed, 394 insertions(+), 14 deletions(-) create mode 100644 TBFTests/Rig/RegisterReaders/GenesisRegReader/GenesisReadDataRegressionTests.cs diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/communication/OptoHeadTest.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/communication/OptoHeadTest.cs index 4665222bd..b47ab3e3c 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/communication/OptoHeadTest.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/communication/OptoHeadTest.cs @@ -1407,11 +1407,22 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication } public string WriteQ3Calibration(TestMethodCfg cfg, Test test, WaterMeter wm) + { + return WriteQ3Calibration(cfg, test, wm, test); + } + + /// The preceding Q3 measurement test whose result receives the factors. + /// The current Write Q3 Calibration activity; its error limits validate the factors. + public string WriteQ3Calibration(TestMethodCfg cfg, Test test, WaterMeter wm, Test validationTest) { if (genesisHead != null && genesisHead.DebugLevel == DebugMode.Simulate) { if (test == null || wm == null) return "Missing Genesis meter or test"; - if (!genesisHead.CalculateQ3Calibration() || !genesisHead.Q3CalibValid) + double errorLimitLo; + double errorLimitHi; + GenesisSmartReader.TryGetEffectiveQ3CalibrationErrorLimits(validationTest == null ? 0 : validationTest.ErrLimLo, + validationTest == null ? 0 : validationTest.ErrLimHi, out errorLimitLo, out errorLimitHi); + if (!genesisHead.CalculateQ3CalibrationWithErrorLimits(errorLimitLo, errorLimitHi) || !genesisHead.Q3CalibValid) return "Q3 simulation requires valid measurement data"; var result = ProcessData.BatchRslts.GetTestRslt(test.Name, test.Part); StoreCalibrationValuesResults(result, genesisHead, wm); @@ -1434,7 +1445,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication return "Missing Test"; } - return Task.Run(() => WriteQ3Calibration_Async(genesisHead, cfg, test, wm)) + return Task.Run(() => WriteQ3Calibration_Async(genesisHead, cfg, test, wm, validationTest)) .GetAwaiter() .GetResult(); } @@ -1443,7 +1454,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication private async Task WriteQ3Calibration_Async(GenesisSmartReader genesisSmartReader, TestMethodCfg cfg, - Test test, WaterMeter wm) + Test test, WaterMeter wm, Test validationTest) { try { @@ -1489,7 +1500,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication CancellationToken token = default; - bool areInitialisedData = genesisSmartReader.CalculateQ3Calibration(); + Test effectiveValidationTest = validationTest ?? test; + double errorLimitLo; + double errorLimitHi; + bool procedureLimitsConfigured = GenesisSmartReader.TryGetEffectiveQ3CalibrationErrorLimits( + effectiveValidationTest == null ? 0 : effectiveValidationTest.ErrLimLo, + effectiveValidationTest == null ? 0 : effectiveValidationTest.ErrLimHi, + out errorLimitLo, out errorLimitHi); + log.Info($"WriteQ3Calibration_Async( Slot: {genesisSmartReader.GetSlotNr}) - Q3 factor validation limits: {errorLimitLo} .. {errorLimitHi}% ({(procedureLimitsConfigured ? "Write Q3 Calibration activity" : "default")})"); + + bool areInitialisedData = genesisSmartReader.CalculateQ3CalibrationWithErrorLimits(errorLimitLo, errorLimitHi); if (!areInitialisedData) { log.Error("WriteQ3Calibration_Async() - CalculateQ3Calibration Initialised Data not valid"); @@ -1501,7 +1521,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication if (!genesisSmartReader.Q3CalibValid) { log.Error("WriteQ3Calibration_Async() - Q3Channel not valid"); - return "Q3Channel not valid"; + return $"Q3Channel not valid ({errorLimitLo} .. {errorLimitHi}%)"; } //Get Activity Status @@ -2056,4 +2076,4 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication } } -} \ No newline at end of file +} diff --git a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs index 76e905cd5..a19b5ac23 100644 --- a/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs +++ b/TBF/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader.cs @@ -3928,6 +3928,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } #endregion + /// Compatibility limit used when the Write Q3 Calibration activity has no error limits configured. + public const double DefaultQ3CalibrationFactorErrorLimit = 5.0; + private double[] q3CalibInitial = {Double.NaN,Double.NaN,Double.NaN}; private bool[] isChQ3CalibValid = { false,false,false}; private double[] q3CalibCh = {Double.NaN,Double.NaN,Double.NaN}; @@ -3979,6 +3982,17 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations public bool CalculateQ3Calibration() + { + return CalculateQ3CalibrationWithErrorLimits(-DefaultQ3CalibrationFactorErrorLimit, + DefaultQ3CalibrationFactorErrorLimit); + } + + /// + /// Calculates Q3 factors using the limits configured on the procedure activity that writes them. + /// A pair for which the high limit is not greater than the low limit (the normal unset 0 / 0 + /// value) falls back to the historical +/- 5 % validation. + /// + public bool CalculateQ3CalibrationWithErrorLimits(double errorLimitLo, double errorLimitHi) { if (Double.IsNaN(refVolume) || Double.IsInfinity(refVolume) || refVolume <= 0 || Double.IsNaN(refTime) || Double.IsInfinity(refTime) || refTime <= 0) { @@ -3986,13 +4000,24 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations return false; } - CalculateQ3Calibration( refVolume, refTime); + CalculateQ3Calibration(refVolume, refTime, errorLimitLo, errorLimitHi); return true; } public void CalculateQ3Calibration(double refVolume, double refTime) { - GetQ3Calibration(refVolume, refTime, q3CalibInitial, ref isChQ3CalibValid,ref q3DiffPercentageCalibCh, ref q3CalibCh); + CalculateQ3Calibration(refVolume, refTime, -DefaultQ3CalibrationFactorErrorLimit, + DefaultQ3CalibrationFactorErrorLimit); + } + + public void CalculateQ3Calibration(double refVolume, double refTime, double errorLimitLo, double errorLimitHi) + { + double effectiveErrorLimitLo; + double effectiveErrorLimitHi; + TryGetEffectiveQ3CalibrationErrorLimits(errorLimitLo, errorLimitHi, + out effectiveErrorLimitLo, out effectiveErrorLimitHi); + GetQ3Calibration(refVolume, refTime, q3CalibInitial, effectiveErrorLimitLo, + effectiveErrorLimitHi, ref isChQ3CalibValid, ref q3DiffPercentageCalibCh, ref q3CalibCh); } public void GetQ3Calibration(double refVolume, double refTime, double[] initCalibFactor, ref bool[] isChQ3CalibValid, ref double[] q3CalibCh) @@ -4002,6 +4027,36 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } public void GetQ3Calibration(double refVolume, double refTime, double[] initCalibFactor, ref bool[] isChQ3CalibValid, ref double[] calibDiffPercent, ref double[] q3CalibCh) + { + GetQ3Calibration(refVolume, refTime, initCalibFactor, + -DefaultQ3CalibrationFactorErrorLimit, DefaultQ3CalibrationFactorErrorLimit, + ref isChQ3CalibValid, ref calibDiffPercent, ref q3CalibCh); + } + + /// + /// Resolves the error limits used to validate a calculated Q3 factor. TBF stores an unset + /// limit pair as equal values (normally 0 / 0), so only an ordered finite pair is considered set. + /// + public static bool TryGetEffectiveQ3CalibrationErrorLimits(double errorLimitLo, double errorLimitHi, + out double effectiveErrorLimitLo, out double effectiveErrorLimitHi) + { + if (!Double.IsNaN(errorLimitLo) && !Double.IsInfinity(errorLimitLo) && + !Double.IsNaN(errorLimitHi) && !Double.IsInfinity(errorLimitHi) && + errorLimitHi > errorLimitLo) + { + effectiveErrorLimitLo = errorLimitLo; + effectiveErrorLimitHi = errorLimitHi; + return true; + } + + effectiveErrorLimitLo = -DefaultQ3CalibrationFactorErrorLimit; + effectiveErrorLimitHi = DefaultQ3CalibrationFactorErrorLimit; + return false; + } + + public void GetQ3Calibration(double refVolume, double refTime, double[] initCalibFactor, + double errorLimitLo, double errorLimitHi, ref bool[] isChQ3CalibValid, + ref double[] calibDiffPercent, ref double[] q3CalibCh) { log.Debug("=== Q3 CALIBRATION START ==="); @@ -4021,7 +4076,7 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } } - log.Debug($"Inputs: refVolume={refVolume}, refTime={refTime}, initCalibFactors={string.Join(",", initCalibFactor)}"); + log.Debug($"Inputs: refVolume={refVolume}, refTime={refTime}, initCalibFactors={string.Join(",", initCalibFactor)}, errorLimits={errorLimitLo}..{errorLimitHi}%"); if (_rawStartEndByChannel == null) { @@ -4110,10 +4165,16 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations } q3CalibCh[iChannel] = (refVolume / recalculatedDeltaVolume) * initCalibFactor[iChannel]; - double diffPercent = Math.Abs((initCalibFactor[iChannel] - q3CalibCh[iChannel] ) / initCalibFactor[iChannel]) * 100.0; - isChQ3CalibValid[iChannel] = diffPercent <= 5.0 && !double.IsNaN(q3CalibCh[iChannel]) && !double.IsInfinity(q3CalibCh[iChannel]) && q3CalibCh[iChannel] >= 1 && q3CalibCh[iChannel] <= ushort.MaxValue; + // Procedure error limits are an ordered range, e.g. -2 .. +2, so validation + // must retain the direction of the factor change. Keep the persisted difference + // absolute for compatibility with the existing calibration-result fields. + double signedDiffPercent = ((q3CalibCh[iChannel] - initCalibFactor[iChannel]) / initCalibFactor[iChannel]) * 100.0; + double diffPercent = Math.Abs(signedDiffPercent); + isChQ3CalibValid[iChannel] = signedDiffPercent >= errorLimitLo && signedDiffPercent <= errorLimitHi && + !double.IsNaN(q3CalibCh[iChannel]) && !double.IsInfinity(q3CalibCh[iChannel]) && + q3CalibCh[iChannel] >= 1 && q3CalibCh[iChannel] <= ushort.MaxValue; calibDiffPercent[iChannel] = diffPercent; - log.Debug($"Calculated Q3Calib Ch[{iChannel}] ={q3CalibCh[iChannel]} DiffPercent={diffPercent}% isValid[{isChQ3CalibValid[iChannel]}] IninitCalibFactor={initCalibFactor}"); + log.Debug($"Calculated Q3Calib Ch[{iChannel}] ={q3CalibCh[iChannel]} SignedDiffPercent={signedDiffPercent}% DiffPercent={diffPercent}% isValid[{isChQ3CalibValid[iChannel]}] IninitCalibFactor={initCalibFactor}"); } log.Debug("=== Q3 CALIBRATION END ==="); diff --git a/TBF/Rig/TestMethods/GenesisCommunication/GenesisCommunicationForm.cs b/TBF/Rig/TestMethods/GenesisCommunication/GenesisCommunicationForm.cs index b73f9f08f..05d7215f3 100644 --- a/TBF/Rig/TestMethods/GenesisCommunication/GenesisCommunicationForm.cs +++ b/TBF/Rig/TestMethods/GenesisCommunication/GenesisCommunicationForm.cs @@ -1088,7 +1088,9 @@ namespace TBF.Rig.TestMethods.GenesisCommunication BeforeTest = currentTest; } - var gciFullLoginResult = iHead.OptoHeadTest.WriteQ3Calibration(cfg, BeforeTest, wm); + // Keep the Q3 measurement result on the preceding test, but validate against the + // limits configured on the current "Write Q3 Calibration Slot" activity. + var gciFullLoginResult = iHead.OptoHeadTest.WriteQ3Calibration(cfg, BeforeTest, wm, currentTest); if (!string.IsNullOrEmpty(gciFullLoginResult) && gciFullLoginResult.Equals(TBF.Rig.RegisterReaders.GenesisRegReader.communication.OptoHeadTest.ResultOk)) { diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/GenesisReadDataRegressionTests.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/GenesisReadDataRegressionTests.cs new file mode 100644 index 000000000..8506df51c --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/GenesisReadDataRegressionTests.cs @@ -0,0 +1,263 @@ +using System; +using System.Globalization; +using System.Text; +using System.Linq; +using System.Reflection; +using TBF.Rig.RegisterReaders.GenesisRegReader.implementations; +using TBF.Rig.RegisterReaders.GenesisRegReader.communication; +using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis; +using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis.Protocols.StreamingProtocol; +using TBF.Rig.RegisterReaders.GenesisRegReader.common; + +namespace TBFTests.Rig.RegisterReaders.GenesisRegReader +{ + // Golden frames have CRCs generated independently with Python binascii.crc_hqx. + // No ports, databases, GCI engine or production-code changes are required. + [TestClass] + [TestCategory("GenesisReadRegression")] + public class GenesisReadDataRegressionTests + { + [DataTestMethod] + [DataRow("@h 1 0 10000000 FFFFFF00 00000400 00100000 00000000 00008000 00400000 00800000 FFFFF000 0C 00018000 8D2C", 1, 1024, 0.001024)] + [DataRow("@h 1 0 10000000 FFFFFF00 00000400 00100000 00000200 00008000 00400000 00800000 FFFFF000 0C 00018000 7BAC", 1, 512, 0.002048)] + [DataRow("@h 1 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 700D", 1, 1024, 0.001024)] + [DataRow("@h 1 0 10000000 FFFFFF00 00000400 00100000 00000800 00008000 00400000 00800000 FFFFF000 0C 00018000 674F", 1, 2048, 0.000512)] + [DataRow("@h 2 0 10000000 FFFFFF00 00000400 00100000 00000000 00008000 00400000 00800000 FFFFF000 0C 00018000 3F43", 2, 1024, 0.001024)] + [DataRow("@h 2 0 10000000 FFFFFF00 00000400 00100000 00000200 00008000 00400000 00800000 FFFFF000 0C 00018000 C9C3", 2, 512, 0.002048)] + [DataRow("@h 2 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 C262", 2, 1024, 0.001024)] + [DataRow("@h 2 0 10000000 FFFFFF00 00000400 00100000 00000800 00008000 00400000 00800000 FFFFF000 0C 00018000 D520", 2, 2048, 0.000512)] + [DataRow("@h 3 0 10000000 FFFFFF00 00000400 00100000 00000000 00008000 00400000 00800000 FFFFF000 0C 00018000 A179", 3, 1024, 0.001024)] + [DataRow("@h 3 0 10000000 FFFFFF00 00000400 00100000 00000200 00008000 00400000 00800000 FFFFF000 0C 00018000 57F9", 3, 512, 0.002048)] + [DataRow("@h 3 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 5C58", 3, 1024, 0.001024)] + [DataRow("@h 3 0 10000000 FFFFFF00 00000400 00100000 00000800 00008000 00400000 00800000 FFFFF000 0C 00018000 4B1A", 3, 2048, 0.000512)] + public void ProtocolH_GoldenFramesPreserveChannelUnitsAndScale(string frame, int channel, int scale, double volume) + { + var decoder = new StreamingDecoder(); + Assert.IsTrue(decoder.DecodeMsg(frame)); + var data = decoder.DataCalib; + Assert.IsNotNull(data); + Assert.IsTrue(data.IsValid); + Assert.AreEqual(channel, data.Channel); + Assert.AreEqual(scale, data.VolumeScaleRawPerMl); + Assert.AreEqual(volume, data.VolumeCm, 1e-12); + Assert.AreEqual(1048576d, data.AccuVolumeRaw); + Assert.AreEqual(1024d, data.DeltaVolumeRaw); + Assert.AreEqual(volume / 1024, data.DeltaVolumeQm, 1e-15); + Assert.AreEqual(1.5, data.TimeS, 1e-12); + Assert.AreEqual(.5, data.SampleIntervalS, 1e-12); + Assert.AreEqual(.001, data.AmplitudeUpV, 1e-12); + Assert.AreEqual(.002, data.AmplitudeDownV, 1e-12); + Assert.AreEqual(-1d, data.TemperatureDegC, 1e-12); + Assert.AreEqual(65536d, data.OverflowTimeS); + Assert.IsNull(decoder.DataFlowTest); + double previousVolume = double.NaN, previousTime = double.NaN; + var telegram = new OptoTelegramRaw(); + telegram.UpdateFromSmart(data, 17, 2.5f, ref previousVolume, ref previousTime); + Assert.AreEqual(channel - 1, telegram.iChannel); + Assert.AreEqual(volume * 1000, telegram.VolumeRawExt, 1e-9); + Assert.AreEqual(1.5, telegram.TimestampExt, 1e-12); + Assert.AreEqual(17, telegram.Counter); + Assert.AreEqual(2.5f, telegram.RefFlow); + } + + [DataTestMethod] + [DataRow("@h 1 0 00000000 00000000 00000400 00100000 00000400 00008000 00400000 00800000 00000000 0C 00018000 0A39", 0, 0, 0)] + [DataRow("@h 1 0 7FFFFFFF 7FFFFFFF 00000400 00100000 00000400 00008000 00400000 00800000 7FFFFFFF 0C 00018000 89B0", 2147483647, 2147483647, 2147483647)] + [DataRow("@h 1 0 80000000 80000000 00000400 00100000 00000400 00008000 00400000 00800000 80000000 0C 00018000 3E06", -2147483648, -2147483648, -2147483648)] + [DataRow("@h 1 0 FFFFFFFF FFFFFFFF 00000400 00100000 00000400 00008000 00400000 00800000 FFFFFFFF 0C 00018000 6870", -1, -1, -1)] + public void ProtocolH_SignedFieldsPreserveTwosComplement(string frame, int total, int delta, int temperature) + { + var decoder = new StreamingDecoder(); + Assert.IsTrue(decoder.DecodeMsg(frame)); + var data = decoder.DataCalib; + Assert.AreEqual(total, data.RawTotalTimeOfFlight); + Assert.AreEqual(delta, data.RawDeltaTimeOfFlight); + Assert.AreEqual(total / 274877906944d, data.TotalTimeOfFlightS, 1e-15); + Assert.AreEqual(delta / 274877906944d, data.DeltaTimeOfFlightS, 1e-15); + Assert.AreEqual(temperature / 4096d, data.TemperatureDegC, 1e-10); + } + + [DataTestMethod] + [DataRow("@f 00000000 00000000 F603", 0.0, 0.0)] + [DataRow("@f 7FFFFFFF FFFFFFFF 3996", 2147.483647, 65535.99998474121)] + [DataRow("@f FFFFFFFF 00010000 21AD", -1e-06, 1.0)] + [DataRow("@f 80000000 00008000 0541", -2147.483648, 0.5)] + public void ProtocolF_PreservesSignedVolumeAndUnsignedTime(string frame, double volume, double time) + { + var decoder = new StreamingDecoder(); + Assert.IsTrue(decoder.DecodeMsg(frame)); + Assert.IsNotNull(decoder.DataFlowTest); + Assert.IsTrue(decoder.DataFlowTest.IsValid); + Assert.AreEqual(volume, decoder.DataFlowTest.VolumeCm, 1e-9); + Assert.AreEqual(time, decoder.DataFlowTest.TimeS, 1e-12); + Assert.IsNull(decoder.DataCalib); + } + + [DataTestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow("garbage")] + [DataRow("@h")] + [DataRow("@h 1")] + [DataRow("@h 1 invalid")] + [DataRow("@f 00000001 00010000 ZZZZ")] + [DataRow("@h\t1\t0\t10000000\tFFFFFF00\t00000400\t00100000\t00000400\t00008000\t00400000\t00800000\tFFFFF000\t0C\t00018000\t700D")] + [DataRow("@h 1 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 700D ")] + [DataRow("@he 1 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 700D")] + public void InvalidOrUnsupportedInputProducesNoMeasurement(string frame) + { + var decoder = new StreamingDecoder(); + decoder.DecodeMsg(frame); + Assert.IsNull(decoder.DataCalib); + Assert.IsNull(decoder.DataFlowTest); + } + + [DataTestMethod] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + [DataRow(5)] + [DataRow(6)] + [DataRow(7)] + [DataRow(8)] + [DataRow(9)] + [DataRow(10)] + [DataRow(11)] + [DataRow(12)] + [DataRow(13)] + public void AlteringAnyCalibrationFieldWithoutUpdatingCrcRejectsRecord(int field) + { + var words = Golden.Split(' '); + words[field] = words[field] == "0" ? "1" : "0"; + var decoder = new StreamingDecoder(); + Assert.IsFalse(decoder.DecodeMsg(string.Join(" ", words))); + Assert.IsNull(decoder.DataCalib); + } + + [DataTestMethod] + [DataRow("en-US")] + [DataRow("sk-SK")] + [DataRow("de-DE")] + public void HexDecodingIsIndependentOfCulture(string culture) + { + var previous = System.Threading.Thread.CurrentThread.CurrentCulture; + try + { + System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); + var decoder = new StreamingDecoder(); + Assert.IsTrue(decoder.DecodeMsg(Golden)); + Assert.AreEqual(.001024, decoder.DataCalib.VolumeCm, 1e-12); + Assert.AreEqual(-1d, decoder.DataCalib.TemperatureDegC); + } + finally { System.Threading.Thread.CurrentThread.CurrentCulture = previous; } + } + + [TestMethod] + public void DiagnosticModeRetainsBadCrcButMarksRecordInvalid() + { + var corrupted = Golden.Substring(0, Golden.Length - 4) + "0000"; + var decoder = new StreamingDecoder(false); + Assert.IsFalse(decoder.DecodeMsg(corrupted)); + Assert.IsNotNull(decoder.DataCalib); + Assert.IsFalse(decoder.DataCalib.IsValid); + Assert.AreEqual(.001024, decoder.DataCalib.VolumeCm, 1e-12); + } + + [TestMethod] + public void CrcMatchesIndependentCcittFalseCheckVector() + { + Assert.AreEqual((ushort)0x29B1, Crc16Ccitt.CalculateMsb1021(Encoding.ASCII.GetBytes("123456789"))); + Assert.AreEqual((ushort)0xFFFF, Crc16Ccitt.CalculateMsb1021(new byte[0])); + } + 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.StopQueueData = false; + + reader.TestStartTelegramIx = 0; + reader.TestEndTelegramIx = 0; + } + + private static void SetPrivateField(object target, string name, object value) + { + var field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(field, name); + field.SetValue(target, value); + } + private static T ReadField(object target, string name) + { + return (T)target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(target); + } + + [TestMethod] + public void ProcessOptoLinePreservesPayloadAndCounterThroughReaderPipeline() + { + var reader = new GenesisSmartReader(new TBF.Rig.RegisterReaders.GenesisRegReader.GenesisCfg(new TBF.Rig.RegisterReaders.GenesisRegReader.Factory())); + InitializeReaderForTest(reader); + bool complete; + reader.ProcessOptoLine(Golden, DataStreamState.ProcessAndSave, out complete); + Assert.IsFalse(complete); + Assert.AreEqual(1, ReadField(reader, "optoDataCount")); + var rows = ReadField(reader, "optoData"); + Assert.AreEqual(0, rows[0].iChannel); + Assert.AreEqual(1.024, rows[0].VolumeRawExt, 1e-9); + Assert.AreEqual(1.5, rows[0].TimestampExt, 1e-12); + Assert.AreEqual(0, rows[0].Counter); + reader.ProcessOptoLine("invalid telegram", DataStreamState.ProcessAndSave, out complete); + Assert.AreEqual(1, ReadField(reader, "optoDataCount")); + reader.ProcessOptoLine(Golden, DataStreamState.ProcessAndSave, out complete); + Assert.AreEqual(2, ReadField(reader, "optoDataCount")); + Assert.AreEqual(1, rows[1].Counter); + Assert.AreEqual(rows[0].VolumeRawExt, rows[1].VolumeRawExt, 1e-9); + } + + [DataTestMethod] + [DataRow("stop")] + [DataRow("queue")] + [DataRow("processing")] + public void ReaderStopGatesPreventMeasurementsFromBeingAppended(string gate) + { + var reader = new GenesisSmartReader(new TBF.Rig.RegisterReaders.GenesisRegReader.GenesisCfg(new TBF.Rig.RegisterReaders.GenesisRegReader.Factory())); + InitializeReaderForTest(reader); + if (gate == "stop") SetPrivateField(reader, "_isStopping", true); + if (gate == "queue") reader.StopQueueData = true; + if (gate == "processing") SetPrivateField(reader, "startDataProcessing", false); + bool complete; + reader.ProcessOptoLine(Golden, DataStreamState.ProcessAndSave, out complete); + Assert.AreEqual(0, ReadField(reader, "optoDataCount")); + Assert.IsFalse(complete); + } + + private const string Golden = "@h 1 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 700D"; + } +} diff --git a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader_Q3Test.cs b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader_Q3Test.cs index 71830e2be..39b263501 100644 --- a/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader_Q3Test.cs +++ b/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader_Q3Test.cs @@ -236,6 +236,40 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.AreEqual(15625.0, calib[1], 0.001); Assert.AreEqual(15625.0, calib[2], 0.001); } + + [TestMethod] + public void GetQ3Calibration_ShouldUseWriteActivityLimits_AndFallbackWhenTheyAreUnset() + { + double limitLo; + double limitHi; + Assert.IsFalse(GenesisSmartReader.TryGetEffectiveQ3CalibrationErrorLimits(0.0, 0.0, out limitLo, out limitHi)); + Assert.AreEqual(-5.0, limitLo); + Assert.AreEqual(5.0, limitHi); + Assert.IsTrue(GenesisSmartReader.TryGetEffectiveQ3CalibrationErrorLimits(-2.0, 2.0, out limitLo, out limitHi)); + Assert.AreEqual(-2.0, limitLo); + Assert.AreEqual(2.0, limitHi); + + var sut = new GenesisSmartReader(); + // At refTime 120 s, this gives a calculated factor 3 % above the initial factor. + var raw = CreateKnownStartEndData( + (100.0, 100.0 + (200.0 / 1.03 / 12.0), 10.0, 20.0), + (200.0, 200.0 + (200.0 / 1.03 / 12.0), 10.0, 20.0), + (300.0, 300.0 + (200.0 / 1.03 / 12.0), 10.0, 20.0)); + SetPrivateField(sut, "_rawStartEndByChannel", raw); + SetPrivateField(sut, "_recalculatedStartEndByChannel", raw); + SetPrivateField(sut, "optoDataCount", 2); + sut.TestStartTelegramIx = 0; + sut.TestEndTelegramIx = 1; + + var valid = new bool[3]; + var differences = new double[3]; + var factors = new double[3]; + sut.GetQ3Calibration(200.0, 120.0, ValidInitFactors, -2.0, 2.0, + ref valid, ref differences, ref factors); + + CollectionAssert.AreEqual(new[] { false, false, false }, valid); + Assert.IsTrue(differences.All(x => x > 2.9 && x < 3.1)); + } [TestMethod] public void CalculateQ3Calibration_ShouldComputeExpectedChannelValues_FromSimulationData() @@ -424,4 +458,4 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations Assert.Fail($"Field '{fieldName}' not found."); } } -} \ No newline at end of file +}