Add overflow volume/timestamp handling and related tests for GenesisSmartReader:

- Extend `CalibrationRecord` constructor to include overflow fields (`OverflowVolumeCm`, `OverflowTimeS`).
- Implement additional unit tests in `OptoTelegramRawTest` to validate multi-rollover scenarios and extended calculations.
- Add `GenesisSmartReaderThreadedReadTests` to cover threaded read-loop functionality and serial processing.
- Enhance logging in `Pump` classes (`TurnOff` methods).
- Update project file to include new test files.
This commit is contained in:
Michal Buzik 2026-04-02 09:25:17 +02:00
parent d5c8fd1c8f
commit 08dff1272b
15 changed files with 1749 additions and 314 deletions

View File

@ -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")]

View File

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

View File

@ -332,6 +332,8 @@ namespace TBF.Rig.Danfoss.VLT2800
Telegram.UpdateTelegramChecksum(msg);
}
SendData(msg);
log.DebugFormat("DONE {0}.TurnOff()", Name);
}

View File

@ -264,6 +264,8 @@ namespace TBF.Rig.Modbus.PumpFM.DanfossVLT
modbus.SendMessage(msg, Name);
}
log.DebugFormat("DONE {0}.TurnOff()", Name);
}

View File

@ -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);
}
/// <summary>

View File

@ -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;
/// <summary>
/// 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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<int>(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<int>(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<T>(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);
}
}
}

View File

@ -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<OptoTelegramRaw[]>(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<OptoTelegramRaw[]>(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<OptoTelegramRaw[]>(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);
}
}
}

View File

@ -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<double[]>(reader, "volumeRawExtLast");
var timestampExtLast = GetPrivateField<double[]>(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<int>(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<double[]>(reader, "volumeRawExtLast");
var timestampExtLast = GetPrivateField<double[]>(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<OptoTelegramRaw[]>(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.");
}
}
}
}

View File

@ -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<T>(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<bool> 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<System.Threading.Tasks.Task>(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<System.Collections.Concurrent.ConcurrentQueue<string>>(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<System.Collections.Concurrent.ConcurrentQueue<string>>(reader, "_receivedLines");
return !queue.IsEmpty;
});
Assert.IsTrue(queued, "Expected line to be queued before processing.");
reader.RunDeviceBefore();
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
var optoData = GetPrivateField<OptoTelegramRaw[]>(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<System.Collections.Concurrent.ConcurrentQueue<string>>(reader, "_receivedLines");
return !queue.IsEmpty;
});
Assert.IsTrue(queued);
reader.RunDeviceBefore();
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
var queueAfter = GetPrivateField<System.Collections.Concurrent.ConcurrentQueue<string>>(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<System.Threading.CancellationTokenSource>(reader, "_readLoopCts");
var task = GetPrivateField<System.Threading.Tasks.Task>(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<System.Collections.Concurrent.ConcurrentQueue<string>>(reader, "_receivedLines");
return !queue.IsEmpty;
});
Assert.IsTrue(queued);
int optoDataCountBefore = GetPrivateField<int>(reader, "optoDataCount");
Assert.AreEqual(0, optoDataCountBefore, "Background thread should only enqueue, not process.");
reader.RunDeviceBefore();
int optoDataCountAfter = GetPrivateField<int>(reader, "optoDataCount");
Assert.AreEqual(1, optoDataCountAfter);
}
}
}

View File

@ -108,9 +108,11 @@
<Compile Include="Rig\Output\FileWriters\Enhanced\WriterTest.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\common\OptoTelegramRawTest.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\FakeSerialDriver.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\GenesisReaderTests.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\GenesisSmartReaderChannelAveragingTests.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\GenesisSmartReaderChannelTests.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\GenesisSmartReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\implementations\GenesisSmartReaderThreadedReadTests.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\diagnosticLed\DiagnosticLedParserTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\hexLogger\HexFormatterTest.cs" />
<Compile Include="Rig\RegisterReaders\iPerlASICReader\communication\C4\IperlHatProtocol\IperlHatFrameBuilderTests.cs" />