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