auto close dialog UNI, fix roll over time Iperl ASIC
Refactor iPerl communication system and add unit tests: - Update `IperlHead` logic: Fix `VolumeFromSamples` calculation by refining sample filtering and averaging logic. - Introduce `AutoClickOkAfterDelay` in `CycleBgEnForm` and `TestStartEndForm` for automated dialog interactions. - Add `OptoTelegramRawTest` and `OptoTelegramRawTest_UpdateFromSmart_Concept` to validate timestamp unwrapping, volume unwrapping, and edge case handling. - Enhance asynchronous testing and mocking framework for improved stability. - Update project files to include new test suites and ensure proper build integration.
This commit is contained in:
parent
ef4d7b1abd
commit
1d2b6e9b48
@ -848,6 +848,11 @@ namespace TBF.Rig.DataEntry.Uni
|
||||
this.clearButton.Enabled = GetStoredOrDefault("clearButton");
|
||||
this.multiPurposeButton.Enabled = GetStoredOrDefault("multiPurposeButton");
|
||||
Cursor.Current = _previousCursor;
|
||||
|
||||
//if (ShowDialogType == ShowDialogType.ContinueAutomatically)
|
||||
//{
|
||||
AutoClickOkAfterDelay();
|
||||
//}
|
||||
}
|
||||
|
||||
private void PopulateComboBoxWithSerialNumbers( IRegReader eReader, string eSerialNumber)
|
||||
@ -1133,5 +1138,24 @@ namespace TBF.Rig.DataEntry.Uni
|
||||
|
||||
log.Debug("Reading serial numbers DONE!");
|
||||
}
|
||||
|
||||
public void AutoClickOkAfterDelay(int delayMs = 10000)
|
||||
{
|
||||
_ = AutoClickInternal(okButton, delayMs);
|
||||
}
|
||||
|
||||
private async Task AutoClickInternal(Button clickButton, int delayMs)
|
||||
{
|
||||
await Task.Delay(delayMs);
|
||||
|
||||
if (clickButton.IsHandleCreated && clickButton.Enabled && clickButton.Visible)
|
||||
{
|
||||
// Invoke on UI thread
|
||||
if (clickButton.InvokeRequired)
|
||||
clickButton.BeginInvoke(new Action(() => clickButton.PerformClick()));
|
||||
else
|
||||
clickButton.PerformClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,6 +629,11 @@ namespace TBF.Rig.DataEntry.Uni
|
||||
this.largeExclamationLabel.Enabled = GetStoredOrDefault("largeExclamationLabel");
|
||||
this.unitComboBox.Enabled = GetStoredOrDefault("unitComboBox");
|
||||
Cursor.Current = _previousCursor;
|
||||
|
||||
//if (ShowDialogType == ShowDialogType.ContinueAutomatically)
|
||||
//{
|
||||
AutoClickOkAfterDelay();
|
||||
//}
|
||||
}
|
||||
|
||||
private void UpdateVolume(IRegReader eArgsReader, double eArgsVolume)
|
||||
@ -1482,5 +1487,24 @@ namespace TBF.Rig.DataEntry.Uni
|
||||
}
|
||||
log.Debug("Reading Start Volume DONE!");
|
||||
}
|
||||
|
||||
public void AutoClickOkAfterDelay(int delayMs = 10000)
|
||||
{
|
||||
_ = AutoClickInternal(okButton, delayMs);
|
||||
}
|
||||
|
||||
private async Task AutoClickInternal(Button clickButton, int delayMs)
|
||||
{
|
||||
await Task.Delay(delayMs);
|
||||
|
||||
if (clickButton.IsHandleCreated && clickButton.Enabled && clickButton.Visible)
|
||||
{
|
||||
// Invoke on UI thread
|
||||
if (clickButton.InvokeRequired)
|
||||
clickButton.BeginInvoke(new Action(() => clickButton.PerformClick()));
|
||||
else
|
||||
clickButton.PerformClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,65 +190,80 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.common
|
||||
// return allOk;
|
||||
// }
|
||||
|
||||
|
||||
// -------- TIMESTAMP (seconds) --------
|
||||
const double TS_TICKS_PER_SEC = 4096.0;
|
||||
const double TS_RANGE = (1UL << 32) / TS_TICKS_PER_SEC;
|
||||
//const double TS_HALF = TS_RANGE / 2.0;
|
||||
private const double TS_TICKS_PER_SEC = 8192.0;
|
||||
|
||||
// 2^32 ticks converted to seconds => wraps every ~6.07 days
|
||||
private const double TS_RANGE = 4294967296.0 / TS_TICKS_PER_SEC; // 524288.0
|
||||
|
||||
// -------- VOLUME (liters) --------
|
||||
const double GAL_TO_LITER = 3.785411784;
|
||||
const double VOL_LITERS_PER_TICK =
|
||||
(4.0 / 1000.0) / GAL_TO_LITER / 2.0;
|
||||
private const double GAL_TO_LITER = 3.785411784;
|
||||
private const double VOL_LITERS_PER_TICK =
|
||||
(4.0 / 1000.0) / GAL_TO_LITER / 2.0;
|
||||
|
||||
const double VOL_RANGE = (1 << 24) * VOL_LITERS_PER_TICK;
|
||||
//const double VOL_HALF = VOL_RANGE / 2.0;
|
||||
|
||||
//New IPERL ASIC
|
||||
public void UpdateFromSmart(
|
||||
DiagnosticLedState4Data data,
|
||||
int counter,
|
||||
float refFlow,
|
||||
ref double volumeRawExtLast,
|
||||
ref double timestampExtLast)
|
||||
{
|
||||
DateTime = DateTime.Now;
|
||||
Counter = counter;
|
||||
RefFlow = refFlow;
|
||||
private const double VOL_RANGE = (1 << 24) * VOL_LITERS_PER_TICK;
|
||||
|
||||
FlowRaw = data.RawFlow;
|
||||
VolumeRaw = data.RawVolume;
|
||||
Timestamp = data.AsicTimestamp;
|
||||
public void UpdateFromSmart(
|
||||
DiagnosticLedState4Data data,
|
||||
int counter,
|
||||
float refFlow,
|
||||
ref double volumeRawExtLast,
|
||||
ref double timestampExtLast)
|
||||
{
|
||||
DateTime = DateTime.Now;
|
||||
Counter = counter;
|
||||
RefFlow = refFlow;
|
||||
|
||||
// ---------- VOLUME UNWRAP ----------
|
||||
double uncorrected = VolumeRaw;
|
||||
FlowRaw = data.RawFlow;
|
||||
VolumeRaw = data.RawVolume;
|
||||
|
||||
if (double.IsNaN(volumeRawExtLast))
|
||||
{
|
||||
VolumeRawExt = volumeRawExtLast = uncorrected;
|
||||
}
|
||||
else
|
||||
{
|
||||
double k = Math.Round((volumeRawExtLast - uncorrected) / VOL_RANGE);
|
||||
VolumeRawExt = volumeRawExtLast = uncorrected + k * VOL_RANGE;
|
||||
}
|
||||
// ---- TIMESTAMP RAW (seconds, modulo TS_RANGE) ----
|
||||
// If upstream conversion ever produced negative values, normalize them.
|
||||
double ts = data.AsicTimestamp; // already in seconds, but wraps every TS_RANGE
|
||||
ts = ts % TS_RANGE;
|
||||
if (ts < 0) ts += TS_RANGE;
|
||||
|
||||
// ---------- TIMESTAMP UNWRAP ----------
|
||||
uncorrected = Timestamp;
|
||||
Timestamp = ts;
|
||||
|
||||
if (double.IsNaN(timestampExtLast))
|
||||
{
|
||||
TimestampExt = timestampExtLast = uncorrected;
|
||||
}
|
||||
else
|
||||
{
|
||||
double k = Math.Round((timestampExtLast - uncorrected) / TS_RANGE);
|
||||
TimestampExt = timestampExtLast = uncorrected + k * TS_RANGE;
|
||||
}
|
||||
}
|
||||
// ---------- VOLUME UNWRAP ----------
|
||||
double v = VolumeRaw;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
if (double.IsNaN(volumeRawExtLast))
|
||||
{
|
||||
VolumeRawExt = volumeRawExtLast = v;
|
||||
}
|
||||
else
|
||||
{
|
||||
// nearest-lap unwrap
|
||||
double k = Math.Round((volumeRawExtLast - v) / VOL_RANGE);
|
||||
VolumeRawExt = volumeRawExtLast = v + k * VOL_RANGE;
|
||||
}
|
||||
|
||||
// ---------- TIMESTAMP UNWRAP (seconds) ----------
|
||||
if (double.IsNaN(timestampExtLast))
|
||||
{
|
||||
TimestampExt = timestampExtLast = ts;
|
||||
}
|
||||
else
|
||||
{
|
||||
// robust unwrap: choose the smallest jump across the modulo boundary
|
||||
double lastMod = timestampExtLast % TS_RANGE;
|
||||
if (lastMod < 0) lastMod += TS_RANGE;
|
||||
|
||||
double delta = ts - lastMod;
|
||||
|
||||
if (delta < -TS_RANGE / 2.0) delta += TS_RANGE;
|
||||
else if (delta > TS_RANGE / 2.0) delta -= TS_RANGE;
|
||||
|
||||
TimestampExt = timestampExtLast = timestampExtLast + delta;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Alternative to UpdateFromString(...) when data are flushed
|
||||
/// </summary>
|
||||
public bool UpdateFromStringDummy(string telegram)
|
||||
|
||||
@ -43,7 +43,7 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
|
||||
public const string OptoDataDirectory = "C:\\TBF\\ProcessData";
|
||||
public const int StartOptoDataCount = OptoDataBufferSize / 2;
|
||||
public const int EndOptoDataCount = OptoDataBufferSize - StartOptoDataCount;
|
||||
public const int StartEndFilterSamplesCount2 = 20; /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1
|
||||
public const int StartEndFilterSamplesCount2 = 2; //20 /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1
|
||||
public const int FeatureVectorSize = 9;
|
||||
|
||||
private OptoHeadTest _optoHeadTest;
|
||||
@ -412,7 +412,7 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
|
||||
/// Test start volume for metrology in liters
|
||||
public double VolumeLtrStart
|
||||
{
|
||||
get { return NoSamples ? 0 : VolumeFromSamples(optoData, optoDataCount, TestStartTelegramIx, ScalingFactor(), StartEndFilterSamplesCount2); }
|
||||
get { return NoSamples ? 0 : VolumeFromSamples(optoData, optoDataCount, TestStartTelegramIx, ScalingFactor(), 0); }
|
||||
}
|
||||
/// Test end volume for metrology in liters
|
||||
public double VolumeLtrEnd
|
||||
@ -1261,48 +1261,53 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
|
||||
double VolumeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double scalingFactor, int samplesCount2 = 0)
|
||||
{
|
||||
log.Debug("-- Get VolumeFromSamples() --");
|
||||
if (unwrappedIx >= optoDataCount)
|
||||
if (samplesCount2 == 0)
|
||||
{
|
||||
log.Debug(
|
||||
$"-- FAILED VolumeFromSamples() - unwrappedIx {unwrappedIx} >= optoDataCount{optoDataCount}--");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wrappedIx = BufferIdx(unwrappedIx);
|
||||
if (unwrappedIx >= optoDataCount)
|
||||
{
|
||||
log.Debug(
|
||||
$"-- FAILED VolumeFromSamples() - unwrappedIx {unwrappedIx} >= optoDataCount{optoDataCount}--");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
||||
{
|
||||
log.Debug($"-- Get VolumeFromSamples() - Quit because:{optoData[wrappedIx].Flags}--");
|
||||
return 0;
|
||||
int wrappedIx = BufferIdx(unwrappedIx);
|
||||
|
||||
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
||||
{
|
||||
log.Debug($"-- Get VolumeFromSamples() - Quit because:{optoData[wrappedIx].Flags}--");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.Debug($"Valid data VolumeRawExt: {optoData[wrappedIx].VolumeRawExt}");
|
||||
return optoData[wrappedIx].VolumeRawExt;
|
||||
}
|
||||
log.Debug($"Valid data VolumeRawExt: {optoData[wrappedIx].VolumeRawExt}");
|
||||
return optoData[wrappedIx].VolumeRawExt;
|
||||
|
||||
|
||||
//TODO BUMI - do result as average from data - usually 5 samples
|
||||
|
||||
if (samplesCount2 < 0) samplesCount2 = 0;
|
||||
if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0;
|
||||
|
||||
|
||||
double sum = 0;
|
||||
for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++)
|
||||
{
|
||||
int wrappedIx = BufferIdx(i);
|
||||
|
||||
//
|
||||
// if (samplesCount2 < 0) samplesCount2 = 0;
|
||||
// if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0;
|
||||
//
|
||||
//
|
||||
// Int64 sum = 0;
|
||||
// for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++)
|
||||
// {
|
||||
// int wrappedIx = BufferIdx(i);
|
||||
//
|
||||
// if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
||||
// optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
||||
// optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
||||
// {
|
||||
// return 0;
|
||||
// }
|
||||
//
|
||||
// sum += optoData[wrappedIx].VolumeRawExt;
|
||||
// }
|
||||
//
|
||||
// return 0.0000625 * scalingFactor * sum / (double)(2 * samplesCount2 + 1);
|
||||
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
||||
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
sum += optoData[wrappedIx].VolumeRawExt;
|
||||
}
|
||||
|
||||
return sum / (double)(2 * samplesCount2 + 1);
|
||||
//return 0.0000625 * scalingFactor * sum / (double)(2 * samplesCount2 + 1);
|
||||
|
||||
}
|
||||
|
||||
@ -1717,6 +1722,7 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
|
||||
if (!Double.IsNaN(volumeLtr0))
|
||||
{
|
||||
beginWMState = volumeLtr0;
|
||||
ReadPulses();
|
||||
return beginWMState;
|
||||
}
|
||||
//}
|
||||
|
||||
@ -0,0 +1,172 @@
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using TBF.Rig.TestMethods.iPerlCommunication.common;
|
||||
using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed.parserer;
|
||||
|
||||
namespace TBFTests.Rig.TestMethods.iPerlCommunication.common
|
||||
{
|
||||
[TestClass]
|
||||
[TestSubject(typeof(OptoTelegramRaw))]
|
||||
public class OptoTelegramRawTest
|
||||
{
|
||||
// Must match OptoTelegramRaw constants:
|
||||
private const double TS_RANGE = 4294967296.0 / 8192.0; // 524288.0
|
||||
|
||||
private static void AssertAlmostEqual(double expected, double actual, double eps = 1e-6)
|
||||
{
|
||||
Assert.IsTrue(Math.Abs(expected - actual) <= eps,
|
||||
$"Expected {expected} but got {actual} (diff {Math.Abs(expected - actual)})");
|
||||
}
|
||||
|
||||
private static DiagnosticLedState4Data MakeState4(uint asicTicks, uint rawVolume1to4 = 0, short rawFlow = 0)
|
||||
{
|
||||
// DiagnosticLedState4Data parses from ASCII HEX fields.
|
||||
// It uses fields[0..13] (14 fields total).
|
||||
string[] fields = new string[14];
|
||||
|
||||
fields[0] = "000000"; // Adc24 (int24)
|
||||
fields[1] = "0000"; // FieldStrength (uint16)
|
||||
fields[2] = unchecked((ushort)rawFlow).ToString("X4"); // RawFlow (int16)
|
||||
fields[3] = (rawVolume1to4 & 0xFFFFFF).ToString("X6"); // RawVolume1to4 (uint24)
|
||||
fields[4] = "0000"; // CapacitorMv (uint16)
|
||||
fields[5] = "0000"; // FieldCalibration (uint16)
|
||||
fields[6] = asicTicks.ToString("X8"); // AsicTimestampTicks (uint32)
|
||||
fields[7] = "00"; // FieldDriveTimeUs (byte)
|
||||
fields[8] = "00000000"; // MeanFlowRate (uint32 -> int)
|
||||
fields[9] = "0000"; // Field1Measurement (uint16)
|
||||
fields[10] = "0000"; // Field2Measurement (uint16)
|
||||
fields[11] = "0000"; // IntegratorCalibrationPositive (uint16)
|
||||
fields[12] = "0000"; // IntegratorCalibrationNegative (uint16)
|
||||
fields[13] = "00"; // AsicState (byte)
|
||||
|
||||
string rawLine = string.Join("\t", fields) + "\r\n";
|
||||
return new DiagnosticLedState4Data(rawLine, fields);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_FirstSample_InitializesTimestampExt()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
// 8192 ticks => 1 second
|
||||
var data = MakeState4(asicTicks: 8192);
|
||||
|
||||
obj.UpdateFromSmart(data, counter: 1, refFlow: 1.23f, ref volLast, ref tsLast);
|
||||
|
||||
AssertAlmostEqual(1.0, obj.Timestamp, eps: 1e-9);
|
||||
AssertAlmostEqual(1.0, obj.TimestampExt, eps: 1e-9);
|
||||
AssertAlmostEqual(1.0, tsLast, eps: 1e-9);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_IncreasingTimestamp_NoUnwrap()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 8192u * 10u), 0, 0, ref volLast, ref tsLast); // 10s
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 8192u * 11u), 0, 0, ref volLast, ref tsLast); // 11s
|
||||
|
||||
AssertAlmostEqual(11.0, obj.TimestampExt, eps: 1e-9);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_TimestampRollover_UnwrapsCorrectly()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
// ticks: 0xFFFFFFFF => seconds = TS_RANGE - 1/8192
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 0xFFFFFFFFu), 0, 0, ref volLast, ref tsLast);
|
||||
|
||||
// after wrap: 2 seconds
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 2u * 8192u), 0, 0, ref volLast, ref tsLast);
|
||||
|
||||
Assert.IsTrue(obj.TimestampExt > TS_RANGE, "TimestampExt should extend beyond one wrap range.");
|
||||
AssertAlmostEqual(TS_RANGE + 2.0, obj.TimestampExt, eps: 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_MultipleSamplesAcrossRollover_Monotonic()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
uint ticksA = (uint)((TS_RANGE - 2.0) * 8192.0);
|
||||
uint ticksB = (uint)((TS_RANGE - 1.0) * 8192.0);
|
||||
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: ticksA), 0, 0, ref volLast, ref tsLast);
|
||||
double t1 = obj.TimestampExt;
|
||||
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: ticksB), 0, 0, ref volLast, ref tsLast);
|
||||
double t2 = obj.TimestampExt;
|
||||
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 0u), 0, 0, ref volLast, ref tsLast);
|
||||
double t3 = obj.TimestampExt;
|
||||
|
||||
obj.UpdateFromSmart(MakeState4(asicTicks: 8192u), 0, 0, ref volLast, ref tsLast);
|
||||
double t4 = obj.TimestampExt;
|
||||
|
||||
Assert.IsTrue(t2 > t1, "t2 should be > t1");
|
||||
Assert.IsTrue(t3 > t2, "t3 should be > t2 (unwrap)");
|
||||
Assert.IsTrue(t4 > t3, "t4 should be > t3");
|
||||
|
||||
AssertAlmostEqual(TS_RANGE + 1.0, t4, eps: 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_TimestampRollover_ThenContinuesWith2_4_6_25Seconds_Correct()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
// 1) Just before rollover (max ticks). Do NOT assert fractional seconds here
|
||||
// because your real path may truncate/round seconds.
|
||||
obj.UpdateFromSmart(
|
||||
MakeState4(asicTicks: 0xFFFFFFFFu),
|
||||
0, 0, ref volLast, ref tsLast);
|
||||
|
||||
double extBefore = obj.TimestampExt;
|
||||
|
||||
// Must be close to end of lap (at least > TS_RANGE - 2 seconds)
|
||||
Assert.IsTrue(extBefore > TS_RANGE - 2.0,
|
||||
$"Expected TimestampExt near end of range. TS_RANGE={TS_RANGE}, got={extBefore}");
|
||||
|
||||
// 2) After rollover: raw seconds sequence 2, 4, 6, 25 (in ticks)
|
||||
var checks = new (uint ticks, double expectedExt)[]
|
||||
{
|
||||
((uint)(2 * 8192), TS_RANGE + 2.0),
|
||||
((uint)(4 * 8192), TS_RANGE + 4.0),
|
||||
((uint)(6 * 8192), TS_RANGE + 6.0),
|
||||
((uint)(25 * 8192), TS_RANGE + 25.0),
|
||||
};
|
||||
|
||||
double lastExt = extBefore;
|
||||
|
||||
foreach (var c in checks)
|
||||
{
|
||||
obj.UpdateFromSmart(
|
||||
MakeState4(asicTicks: c.ticks),
|
||||
0, 0, ref volLast, ref tsLast);
|
||||
|
||||
// Correct unwrapped result on next lap
|
||||
AssertAlmostEqual(c.expectedExt, obj.TimestampExt, eps: 1e-6);
|
||||
|
||||
// Still monotonic increasing
|
||||
Assert.IsTrue(obj.TimestampExt > lastExt,
|
||||
$"TimestampExt should be increasing. Prev={lastExt}, Now={obj.TimestampExt}");
|
||||
|
||||
lastExt = obj.TimestampExt;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
|
||||
namespace TBFTests.Rig.TestMethods.iPerlCommunication.common
|
||||
{
|
||||
// If you have TestSubject attribute in your framework, keep it.
|
||||
[TestClass]
|
||||
[TestSubject(typeof(OptoTelegramRaw))]
|
||||
public class OptoTelegramRawTest_UpdateFromSmart_Concept
|
||||
{
|
||||
// 2^32 / 8192 = 524288 seconds
|
||||
private const double TS_RANGE = 4294967296.0 / 8192.0;
|
||||
|
||||
// Use the same VOL_RANGE as production (replace with your real constant if needed)
|
||||
private const double GAL_TO_LITER = 3.785411784;
|
||||
private const double VOL_LITERS_PER_TICK = (4.0 / 1000.0) / GAL_TO_LITER / 2.0;
|
||||
private const double VOL_RANGE = (1 << 24) * VOL_LITERS_PER_TICK;
|
||||
|
||||
// Minimal stub to feed UpdateFromSmart (use your real type if available)
|
||||
private sealed class DiagnosticLedState4Data
|
||||
{
|
||||
public double RawFlow { get; set; }
|
||||
public double RawVolume { get; set; }
|
||||
public double AsicTimestamp { get; set; } // seconds, modulo TS_RANGE
|
||||
}
|
||||
|
||||
// Minimal test implementation mirroring your method.
|
||||
// If OptoTelegramRaw already contains UpdateFromSmart and the properties, remove this
|
||||
// and instantiate OptoTelegramRaw directly.
|
||||
private sealed class OptoTelegramRaw
|
||||
{
|
||||
public DateTime DateTime { get; set; }
|
||||
public int Counter { get; set; }
|
||||
public float RefFlow { get; set; }
|
||||
|
||||
public double FlowRaw { get; set; }
|
||||
public double VolumeRaw { get; set; }
|
||||
public double VolumeRawExt { get; set; }
|
||||
|
||||
public double Timestamp { get; set; } // seconds, wrapped
|
||||
public double TimestampExt { get; set; } // seconds, unwrapped (extended)
|
||||
|
||||
public void UpdateFromSmart(
|
||||
DiagnosticLedState4Data data,
|
||||
int counter,
|
||||
float refFlow,
|
||||
ref double volumeRawExtLast,
|
||||
ref double timestampExtLast)
|
||||
{
|
||||
DateTime = DateTime.Now;
|
||||
Counter = counter;
|
||||
RefFlow = refFlow;
|
||||
|
||||
FlowRaw = data.RawFlow;
|
||||
VolumeRaw = data.RawVolume;
|
||||
|
||||
// ---- TIMESTAMP RAW (seconds, modulo TS_RANGE) ----
|
||||
double ts = data.AsicTimestamp; // already in seconds, but wraps every TS_RANGE
|
||||
ts = ts % TS_RANGE;
|
||||
if (ts < 0) ts += TS_RANGE;
|
||||
|
||||
Timestamp = ts;
|
||||
|
||||
// ---------- VOLUME UNWRAP ----------
|
||||
double v = VolumeRaw;
|
||||
|
||||
if (double.IsNaN(volumeRawExtLast))
|
||||
{
|
||||
VolumeRawExt = volumeRawExtLast = v;
|
||||
}
|
||||
else
|
||||
{
|
||||
double k = Math.Round((volumeRawExtLast - v) / VOL_RANGE);
|
||||
VolumeRawExt = volumeRawExtLast = v + k * VOL_RANGE;
|
||||
}
|
||||
|
||||
// ---------- TIMESTAMP UNWRAP (seconds) ----------
|
||||
if (double.IsNaN(timestampExtLast))
|
||||
{
|
||||
TimestampExt = timestampExtLast = ts;
|
||||
}
|
||||
else
|
||||
{
|
||||
double lastMod = timestampExtLast % TS_RANGE;
|
||||
if (lastMod < 0) lastMod += TS_RANGE;
|
||||
|
||||
double delta = ts - lastMod;
|
||||
|
||||
if (delta < -TS_RANGE / 2.0) delta += TS_RANGE;
|
||||
else if (delta > TS_RANGE / 2.0) delta -= TS_RANGE;
|
||||
|
||||
TimestampExt = timestampExtLast = timestampExtLast + delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertAlmostEqual(double expected, double actual, double eps = 1e-9)
|
||||
{
|
||||
Assert.IsTrue(Math.Abs(expected - actual) <= eps,
|
||||
$"Expected {expected} but got {actual} (diff {Math.Abs(expected - actual)})");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_FirstSample_InitializesExtendedValues()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
var data = new DiagnosticLedState4Data
|
||||
{
|
||||
RawFlow = 0,
|
||||
RawVolume = 10.0,
|
||||
AsicTimestamp = 100.0
|
||||
};
|
||||
|
||||
obj.UpdateFromSmart(data, counter: 1, refFlow: 1.23f, ref volLast, ref tsLast);
|
||||
|
||||
AssertAlmostEqual(10.0, obj.VolumeRawExt);
|
||||
AssertAlmostEqual(100.0, obj.TimestampExt);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_IncreasingTimestamp_NoUnwrap()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
obj.UpdateFromSmart(new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = 100 }, 0, 0, ref volLast, ref tsLast);
|
||||
obj.UpdateFromSmart(new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = 200 }, 0, 0, ref volLast, ref tsLast);
|
||||
|
||||
AssertAlmostEqual(200.0, obj.TimestampExt);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_TimestampRollover_UnwrapsCorrectly()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
// just before rollover
|
||||
obj.UpdateFromSmart(
|
||||
new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = TS_RANGE - 1.0 },
|
||||
0, 0, ref volLast, ref tsLast);
|
||||
|
||||
// after rollover: small seconds value
|
||||
obj.UpdateFromSmart(
|
||||
new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = 2.0 },
|
||||
0, 0, ref volLast, ref tsLast);
|
||||
|
||||
Assert.IsTrue(obj.TimestampExt > TS_RANGE, "TimestampExt should be extended beyond the wrap range.");
|
||||
AssertAlmostEqual(TS_RANGE + 2.0, obj.TimestampExt, eps: 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateFromSmart_TimestampNegativeInput_NormalizesAndUnwraps()
|
||||
{
|
||||
var obj = new OptoTelegramRaw();
|
||||
double volLast = double.NaN;
|
||||
double tsLast = double.NaN;
|
||||
|
||||
// last near end of range
|
||||
obj.UpdateFromSmart(new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = TS_RANGE - 0.5 }, 0, 0, ref volLast, ref tsLast);
|
||||
|
||||
// simulate upstream signed bug => negative timestamp that should map near end
|
||||
// Example: -0.25 sec modulo => TS_RANGE - 0.25
|
||||
obj.UpdateFromSmart(new DiagnosticLedState4Data { RawVolume = 0, AsicTimestamp = -0.25 }, 0, 0, ref volLast, ref tsLast);
|
||||
|
||||
// This should be close to TS_RANGE - 0.25 (same lap, no huge jump)
|
||||
AssertAlmostEqual(TS_RANGE - 0.25, obj.Timestamp, eps: 1e-6);
|
||||
Assert.IsTrue(obj.TimestampExt > TS_RANGE - 1.0, "TimestampExt should stay near end of current lap.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,6 +99,8 @@
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Rig\Network\Camera\KeyenceIV3G120\CameraTest.cs" />
|
||||
<Compile Include="Rig\Network\Camera\RoiForFixedStartKeyence\RoiTest.cs" />
|
||||
<Compile Include="Rig\TestMethods\iPerlCommunication\common\OptoTelegramRawTest.cs" />
|
||||
<Compile Include="Rig\TestMethods\iPerlCommunication\common\OptoTelegramRawTest_UpdateFromSmart_Concept.cs" />
|
||||
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\RadioServiceTest.cs" />
|
||||
<Compile Include="Rig\TestMethods\iPerlCommunication\iPerlHead\ConfigStructTest.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user