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.
This commit is contained in:
Michal Buzik 2026-09-16 10:23:04 +02:00
parent 684513ee59
commit b8f6af7112
5 changed files with 394 additions and 14 deletions

View File

@ -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);
}
/// <param name="test">The preceding Q3 measurement test whose result receives the factors.</param>
/// <param name="validationTest">The current Write Q3 Calibration activity; its error limits validate the factors.</param>
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<string> 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
}
}
}
}

View File

@ -3928,6 +3928,9 @@ namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations
}
#endregion
/// <summary>Compatibility limit used when the Write Q3 Calibration activity has no error limits configured.</summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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 ===");

View File

@ -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))
{

View File

@ -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<T>(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<int>(reader, "optoDataCount"));
var rows = ReadField<OptoTelegramRaw[]>(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<int>(reader, "optoDataCount"));
reader.ProcessOptoLine(Golden, DataStreamState.ProcessAndSave, out complete);
Assert.AreEqual(2, ReadField<int>(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<int>(reader, "optoDataCount"));
Assert.IsFalse(complete);
}
private const string Golden = "@h 1 0 10000000 FFFFFF00 00000400 00100000 00000400 00008000 00400000 00800000 FFFFF000 0C 00018000 700D";
}
}

View File

@ -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.");
}
}
}
}