Add ReadStableMassOp unit test and enhance MettlerToledo Scale support

- Introduced `ReadStableMassOpTest` to validate mass reading operations.
- Refactored `Scale` to support `ISerialPort` for improved testability.
- Added `SerialPortDevice` and `SerialPortDeviceFake` implementations, with `GetComponent` overload in `Factory`.
- Enhanced `StateMachine` with `InitializeBoardEtc_Fake` for custom setups.
- Updated project files to include new classes.
This commit is contained in:
Michal Buzik 2025-11-19 16:28:23 +01:00
parent 6311df49c3
commit c7cba20de0
9 changed files with 315 additions and 2 deletions

View File

@ -14,6 +14,8 @@ namespace TBF.Rig.Scales.MettlerToledo
public IComponent DummyComponent() { return new Scale(); }
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components) { return new Scale(cfg); }
// wrapping to use in unit tests
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components, ISerialPort serialPort) { return new Scale(cfg, serialPort); }
public IComponentCfg DefaultConfig() { return new ScaleCfg("WT", this); }

View File

@ -0,0 +1,14 @@
using System.IO.Ports;
namespace TBF.Rig.Scales.MettlerToledo
{
public interface ISerialPort
{
void Open();
void Close();
void Write(string text);
string ReadExisting();
bool IsOpen { get; }
Handshake Handshake { get; set; }
}
}

View File

@ -50,7 +50,7 @@ namespace TBF.Rig.Scales.MettlerToledo
///
public IDrawingItem DrawingItem { get { return scaleCfg as IDrawingItem; } }
protected SerialPort serialPort;
protected ISerialPort serialPort;
protected StringBuilder stringBuilder;
/// <summary>The state of the mass measurement</summary>
@ -93,6 +93,13 @@ namespace TBF.Rig.Scales.MettlerToledo
{
scaleCfg = cfg as ScaleCfg;
}
public Scale(Generic.IComponentCfg cfg, ISerialPort serialPort)
: base(cfg)
{
scaleCfg = cfg as ScaleCfg;
this.serialPort = serialPort;
}
public override void Initialize()
{
@ -111,7 +118,8 @@ namespace TBF.Rig.Scales.MettlerToledo
if (scaleCfg.DebugLevel == DebugMode.Normal)
{
string comPortName = "COM" + scaleCfg.ComPortNr.ToString();
serialPort = new SerialPort(comPortName, scaleCfg.BaudRate, scaleCfg.Parity, scaleCfg.DataBits, scaleCfg.StopBits);
serialPort ??= new SerialPortDevice(comPortName, scaleCfg.BaudRate, scaleCfg.Parity, scaleCfg.DataBits,
scaleCfg.StopBits);
serialPort.Handshake = scaleCfg.Handshake;
serialPort.Open();
log.FatalFormat("{0} - Device successfully initialized", Name);

View File

@ -0,0 +1,13 @@
using System.IO.Ports;
namespace TBF.Rig.Scales.MettlerToledo
{
public class SerialPortDevice : SerialPort, ISerialPort
{
public SerialPortDevice(string comPortName, int scaleCfgBaudRate, Parity scaleCfgParity, int scaleCfgDataBits, StopBits scaleCfgStopBits)
: base(comPortName, scaleCfgBaudRate, scaleCfgParity, scaleCfgDataBits, scaleCfgStopBits)
{
}
}
}

View File

@ -0,0 +1,84 @@
using System.IO.Ports;
using log4net;
using log4net.Core;
using log4net.Repository.Hierarchy;
using NHibernate;
namespace TBF.Rig.Scales.MettlerToledo
{
public class SerialPortDeviceFake : ISerialPort
{
// write dirrectly into cmd
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private bool bIsOpen = false;
public void Open()
{
log.Debug( "SerialPortDeviceFake.Open()");
bIsOpen = true;
}
public void Close()
{
log.Debug( "SerialPortDeviceFake.Close()");
bIsOpen = false;
}
private string lastWrite = "";
private string lastRead = "";
public string LastWrite { get => lastWrite; set => lastWrite = value;}
public string LastRead { get => lastRead; set => lastRead = value;}
public void Write(string text)
{
log.DebugFormat("SerialPortDeviceFake.Write({0})", text);
lastWrite = text;
}
private string forcedText = null;
public void ForceReturnAnswer(string text)
{
forcedText = text;
}
public string ReadExisting()
{
string result = "", end = "\r\n";
if (forcedText != null)
{
result = forcedText;
forcedText = null;
return result + end;
}
if (!string.IsNullOrEmpty(lastWrite))
{
if (lastWrite.Contains("S"+end))
{
result = "S D 170.725 kg"; // correct stable scale mass answer
}
else if (lastWrite.Contains("R"))
{
result = "RD";
}
//store result
if (!string.IsNullOrEmpty(result))
{
result = result + end;
lastRead = result;
}
}
return result;
}
public bool IsOpen
{
get { return bIsOpen; }
}
Handshake handshakeVal = Handshake.None;
public Handshake Handshake { get => handshakeVal; set => handshakeVal = value; }
}
}

View File

@ -323,6 +323,22 @@ namespace TBF.Rig
}
}
[Obsolete("Use InitializeBoardEtc() instead.")]
public static void InitializeBoardEtc_Fake(IList<IComponent> componentList)
{
components = componentList;
// base find components - find control board!, ...
foreach (var cmpnt in components)
{
if (cmpnt is ControlBoard.IControlBoard) ControlBoardMain = cmpnt as ControlBoard.IControlBoard;
if (cmpnt is IBenchInfo) ProcessData.BenchInfo = cmpnt as IBenchInfo;
if (cmpnt is IErrorFlags) ProcessData.ErrorFlagsComp = cmpnt as IErrorFlags;
if (cmpnt is IStatisticsMonitoring)
ProcessData.StatisticsMonitoringComp = cmpnt as IStatisticsMonitoring;
}
}
public static string CurrentlyInitializedComponentName;
///

View File

@ -1406,11 +1406,14 @@
<Compile Include="Rig\RegisterReaders\StandingStartStop\Factory.cs" />
<Compile Include="Rig\RegisterReaders\StandingStartStop\RRProcParams.cs" />
<Compile Include="Rig\RegValvePosition.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ISerialPort.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ScaleCfg.cs" />
<Compile Include="Rig\Scales\MettlerToledo\Scale.cs" />
<Compile Include="Rig\Scales\MettlerToledo\Factory.cs" />
<Compile Include="Rig\Scales\MettlerToledo\GetSerNumOp.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOp.cs" />
<Compile Include="Rig\Scales\MettlerToledo\SerialPortDevice.cs" />
<Compile Include="Rig\Scales\MettlerToledo\SerialPortDeviceFake.cs" />
<Compile Include="Rig\Scales\MettlerToledo\SetUnitsOp.cs" />
<Compile Include="Rig\Scales\MettlerToledo\TaringOp.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ZeroOp.cs" />

View File

@ -0,0 +1,168 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Common;
using Config.Entities;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using TBF.Boxes;
using TBF.Rig;
using TBF.Rig.BuiltIn.Valve;
using TBF.Rig.ControlBoard.Uni;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.Scales.MettlerToledo;
using Factory = TBF.Rig.Scales.MettlerToledo.Factory;
namespace TBFTests.Rig.Scales.MettlerToledo
{
[TestClass]
[TestSubject(typeof(ReadStableMassOp))]
public class ReadStableMassOpTest
{
[TestMethod]
public void Run_ReadStableMassOp()
{
//
var mockControlBoard = new Mock<TBF.Rig.ControlBoard.IControlBoard>();
mockControlBoard.Setup(v => v.Name).Returns("UniCB");
IList<IComponent> componentsBoard = new List<IComponent>() { mockControlBoard.Object};
StateMachine.InitializeBoardEtc_Fake(componentsBoard);
Factory factory = new Factory();
ScaleCfg cfg = new ScaleCfg("Scale0", factory);
cfg.Evaporation = "Evaporation0";
//
var mockValve = new Mock<IValve>();
mockValve.Setup(v => v.Name).Returns(cfg.DrainValve);
//
var mockEvaporation = new Mock<IEvaporation>();
mockEvaporation.Setup(v => v.Name).Returns(cfg.Evaporation);
//components
IComponent valveObject = mockValve.Object;
IComponent evaporationObject = mockEvaporation.Object;
IList<IComponent> components = new List<IComponent>() { valveObject, mockControlBoard.Object, evaporationObject };
StateMachine.InitializeBoardEtc_Fake(components);
ValveCfg valveCfg = new ValveCfg(new ValveFactory());
var valveFactory = new ValveFactory();
IComponent iComponent = valveFactory.GetComponent(valveCfg,components);
SerialPortDeviceFake serialPort = new SerialPortDeviceFake();
Scale scale = factory.GetComponent(cfg,null, serialPort) as Scale;
scale.DebugLevel = DebugMode.Normal;
scale.Initialize();
DoubleBox finalMass = new DoubleBox();
ReadStableMassOp readStableMassOp = new ReadStableMassOp(scale, ref finalMass, 2, MassMethod.Scale, 4, 0.05);
//////////////////////////////////
/// Start
readStableMassOp.Start();
/// Run
bool stopLoop = false;
while(!stopLoop)
{
Event run = readStableMassOp.Run();
if (run == Event.Abort || run == Event.ScaleDone)
{
stopLoop = true;
break;
}
if (StateMachine.Time >= 2 && StateMachine.Time < 3) // delay
{
//must be send command to stabilisate mass
Assert.IsTrue(scale.Activity == Activity.RunningOperation);
Assert.IsTrue(serialPort.IsOpen);
Assert.IsTrue(serialPort.LastWrite.Contains("S\r\n"));
Assert.IsTrue(string.IsNullOrEmpty(serialPort.LastRead));
}
//time gap between measurements - 60 sec
if (StateMachine.Time == 61) //valid measurement
{
//FIRST measurement
Assert.IsTrue(scale.Activity == Activity.RunningOperation);
//try simulate immediate measurement
scale.Activity = Activity.ImmediateMeasurement;
scale.RunDeviceBefore();
Assert.IsTrue(scale.Activity == Activity.Idle);
Assert.IsTrue(scale.MsrmntState == MsrmntState.Valid);
Assert.IsTrue(scale.Mass-0.1 < 170.725 && scale.Mass+0.1 > 170.724); //value from fake serial port
}
if (StateMachine.Time == 62) //valid measurement
{
//SECOND measurement - forced answer
scale.Activity = Activity.ImmediateMeasurement;
serialPort.ForceReturnAnswer("S S 0.000 kg");
scale.RunDeviceBefore();
Assert.IsTrue(scale.Activity == Activity.Idle);
Assert.IsTrue(scale.MsrmntState == MsrmntState.Valid);
Assert.IsTrue(scale.Mass-0.1 < 0 && scale.Mass+0.1 > 0);
}
////////// ERROR CASES
if (StateMachine.Time >= 63 && StateMachine.Time < 65) // 2x invalid parameter S_I
{
//FAILED in customer measurement - forced answer
scale.Activity = Activity.ImmediateMeasurement;
serialPort.ForceReturnAnswer("S I"); //not ready scale
scale.RunDeviceBefore();
Assert.IsTrue(scale.MsrmntState == MsrmntState.Failed);
Assert.IsTrue(scale.Activity == Activity.Idle);
}
// SET Correct answer
if (StateMachine.Time >= 66 && StateMachine.Time < 70) //valid parametrs - IF 4 times valid measurement finish and calculate average final mass
{
//FAILED in customer measurement - forced answer
scale.Activity = Activity.ImmediateMeasurement;
serialPort.ForceReturnAnswer("S S 120.56 kg"); //not ready scale
scale.RunDeviceBefore();
Assert.IsTrue(scale.MsrmntState == MsrmntState.Valid);
Assert.IsTrue(scale.Activity == Activity.Idle);
}
////////// ~ ERROR CASES
if (StateMachine.Time >= 70)
{
Assert.Fail( "Timeout");
return;
}
Thread.Sleep(10);
StateMachine.Time += 1;
}
/// Stop
readStableMassOp.Stop();
Assert.IsTrue(scale.Activity == Activity.Idle);
Assert.IsTrue(scale.MsrmntState == MsrmntState.Valid);
Assert.IsTrue(finalMass.Val-0.1 < 60.28 && finalMass.Val+0.1 > 60.28);
//////////////////////////////////////
}
}
}

View File

@ -106,6 +106,7 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOpTest.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
@ -124,6 +125,10 @@
<Project>{9d0dcc88-dc81-47eb-9fdd-4c3907871bfb}</Project>
<Name>Results</Name>
</ProjectReference>
<ProjectReference Include="..\SchematicDrawing\SchematicDrawing.csproj">
<Project>{0f79ca69-9dbc-41f3-a6fc-5a2937365343}</Project>
<Name>SchematicDrawing</Name>
</ProjectReference>
<ProjectReference Include="..\TBF\TBF.csproj">
<Project>{8648FD92-CDA1-4C3A-B5F9-FE547CE1FA48}</Project>
<Name>TBF</Name>