Standing Start roll over fix

Add volume rollover handling enhancements and corresponding unit tests:

- Improve rollover logic in `PopulateVolume` for accurate volume compensation.
- Add `TestStartEndFormTest` to validate rollover scenarios with unit tests.
- Refactor and expand handling of initial volume state in `findBeginStateFromForm`.
- Update project files to include new test dependencies and classes.
This commit is contained in:
Michal Buzik 2026-03-01 22:44:33 +01:00
parent 9606ce3588
commit 166cb28d28
4 changed files with 258 additions and 22 deletions

View File

@ -651,7 +651,7 @@ namespace TBF.Rig.DataEntry.Uni
/// </summary>
/// <param name="eArgsReader"></param>
/// <param name="eArgsVolume"></param>
private void PopulateVolume(IRegReader eArgsReader, double eArgsVolume)
public void PopulateVolume(IRegReader eArgsReader, double eArgsVolume)
{
if (regReaders == null || Double.IsNaN(eArgsVolume) || textBoxes == null)
@ -668,27 +668,41 @@ namespace TBF.Rig.DataEntry.Uni
double VolumeRaw = eArgsVolume;
double v = eArgsVolume;
// // ---- normalize to [0, RANGE) in case upstream gives negative values ----
// v = v % VOL_RANGE_LITERS;
// if (v < 0) v += VOL_RANGE_LITERS;
// VolumeRaw = v;
// //Test and compensate roll over
// if (isEnd)
// {
// if (eArgsReader != null)
// {
// double lastMod = eArgsReader.BeginWMState % VOL_RANGE_LITERS;
// if (lastMod < 0) lastMod += VOL_RANGE_LITERS;
//
// double delta = v - lastMod;
//
// // choose the shortest jump across the modulo boundary
// if (delta < -VOL_RANGE_LITERS / 2.0) delta += VOL_RANGE_LITERS;
// else if (delta > VOL_RANGE_LITERS / 2.0) delta -= VOL_RANGE_LITERS;
//
// VolumeRaw = eArgsReader.BeginWMState + delta;
// }
// }
// Test and compensate roll over
if (isEnd)
{
if (eArgsReader != null && (!Double.IsNaN(eArgsReader.BeginWMState)))
{
double lastMod = eArgsReader.BeginWMState % VOL_RANGE_LITERS;
if (lastMod < 0) lastMod += VOL_RANGE_LITERS;
double delta = v - lastMod;
// choose the shortest jump across the modulo boundary
if (delta < -VOL_RANGE_LITERS / 2.0) delta += VOL_RANGE_LITERS;
else if (delta > VOL_RANGE_LITERS / 2.0) delta -= VOL_RANGE_LITERS;
VolumeRaw = eArgsReader.BeginWMState + delta;
log.Debug($"Roll over detected: {eArgsReader.Name} - {eArgsReader.BeginWMState} -> {VolumeRaw}");
}
else
{
//volume from form
Double beginVolume = findBeginStateFromForm();
double lastMod = beginVolume % VOL_RANGE_LITERS;
if (lastMod < 0) lastMod += VOL_RANGE_LITERS;
double delta = v - lastMod;
// choose the shortest jump across the modulo boundary
if (delta < -VOL_RANGE_LITERS / 2.0) delta += VOL_RANGE_LITERS;
else if (delta > VOL_RANGE_LITERS / 2.0) delta -= VOL_RANGE_LITERS;
VolumeRaw = beginVolume + delta;
log.Debug($"Roll over detected: {eArgsReader.Name} - {eArgsReader.BeginWMState} -> {VolumeRaw}");
}
}
for (int k = 0; k < colItems.Count && k < comboRows; k++)
{
@ -722,6 +736,50 @@ namespace TBF.Rig.DataEntry.Uni
}
}
private Double findBeginStateFromForm()
{
List<Ct> ctValues = new List<Ct> { Ct.StartState, Ct.StartStateAux };
int regReadersLength = regReaders.Length;
int comboRows = textBoxes.GetLength(0);
int comboCols = textBoxes.GetLength(1);
for (int k = 0; k < colItems.Count && k < comboRows; k++)
{
Ct current = (Ct)colItems[k].Content;
if (ctValues.Contains(current)) // it define if is start or end
{
for (int ix = 0; ix < wmsCount && ix < comboCols; ix++)
{
int regIndex = ix;
if (regIndex >= 0 && regIndex < regReadersLength)
{
var textBox = textBoxes[k, ix];
if (textBox != null && textBox.Text != null && textBox.Text.Length > 0)
{
try
{
if (VolumeUnit == Unit.None)
VolumeUnit = Unit.l;
double VolumeRaw = Double.Parse(textBox.Text);
Double convertBeginVolumeINLiters = Units.ConvertFrom(VolumeUnit, VolumeRaw);
return convertBeginVolumeINLiters;
}
catch (Exception ex)
{
log.Error($"Error converting volume to {VolumeUnit}: {ex.Message}");
}
}
}
}
}
}
return 0.0D;
}
/// <summary>
/// When one combo box is updated using drop-down menu, all combo boxes
/// are updated by this function.

View File

@ -1731,8 +1731,10 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
//Solve roll over
if (endWMState < beginWMState)
{
log.Debug($"Solve roll over! endWMState: {endWMState} < beginWMState: {beginWMState}");
const double VOL_RANGE_LITERS = 16777216.0 * 0.00025; // 4,194.304 l
endWMState += VOL_RANGE_LITERS;
log.Debug($"Solve roll over! Upgraded endWMState: {endWMState}, beginWMState: {beginWMState}");
}
}
ReadPulses();

View File

@ -0,0 +1,166 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Common;
using Config.Entities;
using JetBrains.Annotations;
using Results.Entities;
using TBF.Rig.DataEntry;
using TBF.Rig.DataEntry.Uni;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
namespace TBFTests.Rig.DataEntry.Uni
{
[TestClass]
[TestSubject(typeof(TestStartEndForm))]
public class TestStartEndFormTest
{
[TestMethod]
public void PopulateVolume_TestRollOver()
{
Exception staEx = null;
var t = new Thread(() =>
{
try
{
// Arrange
double volRange = GetConstDouble(typeof(TestStartEndForm), "VOL_RANGE_LITERS");
// Example: begin = R-10, v=10 => expectedRaw = begin + 20 (shortest jump across modulo)
double begin = volRange - 10.0;
double entered = 10.0;
double expectedRaw = begin + 20.0;
var reader = new FakeRegReader
{
Name = "R1",
BeginWMState = begin
};
var waterMeters = new List<WaterMeter>
{
new WaterMeter { Disabled = false }
};
// Use your DEItem class exactly:
// - Content must be Ct.EndState (because isEnd==true)
// - Action must not be LoadReadOnly, otherwise textbox may be disabled (not critical for setting Text,
// but keep it editable)
IList<DEItem> colItems = new List<DEItem>
{
new DEItem(Ct.EndState, "End", Ac.Clear, 120)
};
// Force isEnd=true (constructor: isEnd = (wmStartStateStr != null))
// Length must match (isCompound?2:1)*waterMeters.Count -> here 1*1 = 1.
string[] wmStartStateStr = new[] { "x" };
var sut = new TestStartEndForm(
waterMeters: waterMeters,
regReaders: new IRegReader[] { reader },
_lineSize: 1,
title: "test",
sz: FontSz.S,
isStartBoxAlwaysEn: false,
isLrOrder: false,
formCloseKeys: "",
colItems: colItems,
isCompound: false,
initialVolumeUnit: Unit.l, // keep conversion identity
isCameraPicture: false,
startImages: Array.Empty<string>(),
endImages: Array.Empty<string>(),
ocrVidi: null,
ocrMessage: "",
streamName: "",
wmStartStateStr: wmStartStateStr,
refVolume: 0,
errLimLo: 0,
errLimHi: 0
);
// Act
sut.PopulateVolume(reader, entered);
// Assert
var tb = GetTextBox(sut, row: 0, col: 0);
Assert.IsNotNull(tb, "Expected TextBox[0,0] to exist.");
Assert.AreEqual(expectedRaw.ToString(), tb.Text);
}
catch (Exception ex)
{
staEx = ex;
}
});
t.SetApartmentState(ApartmentState.STA); // WinForms UI objects => STA is safest
t.Start();
t.Join();
if (staEx != null)
throw new AssertFailedException("PopulateVolume_TestRollOver failed.", staEx);
}
// ----- helpers -----
private static double GetConstDouble(Type type, string fieldName)
{
var f = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (f == null) throw new MissingFieldException(type.FullName, fieldName);
object v = f.IsLiteral ? f.GetRawConstantValue() : f.GetValue(null);
return Convert.ToDouble(v);
}
private static System.Windows.Forms.TextBox GetTextBox(TestStartEndForm sut, int row, int col)
{
var f = typeof(TestStartEndForm).GetField("textBoxes", BindingFlags.Instance | BindingFlags.NonPublic);
if (f == null) throw new MissingFieldException(typeof(TestStartEndForm).FullName, "textBoxes");
var grid = (System.Windows.Forms.TextBox[,])f.GetValue(sut);
return grid[row, col];
}
private sealed class FakeRegReader : IRegReader
{
public string Name { get; set; }
public string ClassName { get; }
public string ParentName { get; }
public DebugMode DebugLevel { get; }
public LogLevel LogLevel { get; }
public IComponentCfg Cfg { get; }
public IList<MeasurementCorrection> Corrections { get; set; }
public IList<Uncertainty> Uncertainties { get; set; }
public void Initialize()
{
throw new NotImplementedException();
}
public void StartChangeHandler()
{
throw new NotImplementedException();
}
public void StopChangeHandler()
{
throw new NotImplementedException();
}
public RegisterReaderType RegisterReaderType { get; }
public int Position { get; }
public double PulsesPerLtr { get; set; }
public double LtrsPerPulse { get; }
public string QuantityUnits { get; set; }
public int WMPulses { get; }
public int WMRefPulses { get; }
public double WMVolume { get; }
public double BeginWMState { get; set; }
public double EndWMState { get; }
}
}
}

View File

@ -84,6 +84,7 @@
<HintPath>..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<Choose>
@ -99,6 +100,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Rig\DataEntry\Uni\IperlHead_test_RollOver_Average.cs" />
<Compile Include="Rig\DataEntry\Uni\PopulateVolumeRolloverTests_Concept.cs" />
<Compile Include="Rig\DataEntry\Uni\TestStartEndFormTest.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" />
@ -111,10 +113,18 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj">
<Project>{c8939821-ba5c-4988-a3d0-bf53b74865c7}</Project>
<Name>Common</Name>
</ProjectReference>
<ProjectReference Include="..\Config\Config.csproj">
<Project>{743df7db-c7b6-42eb-986d-0f485e5588e4}</Project>
<Name>Config</Name>
</ProjectReference>
<ProjectReference Include="..\Results\Results.csproj">
<Project>{9d0dcc88-dc81-47eb-9fdd-4c3907871bfb}</Project>
<Name>Results</Name>
</ProjectReference>
<ProjectReference Include="..\TBF\TBF.csproj">
<Project>{8648FD92-CDA1-4C3A-B5F9-FE547CE1FA48}</Project>
<Name>TBF</Name>