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.
264 lines
14 KiB
C#
264 lines
14 KiB
C#
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";
|
|
}
|
|
}
|