Ally iPerl initial - reader,test method, unit test

Add AllyReader and AllyCalibration support with tests: Introduce new register readers, calibration methods, and comprehensive unit tests for integration and functionality validation. Update project files accordingly.
This commit is contained in:
Michal Buzik 2026-08-18 10:01:02 +02:00
parent c619c7d3b8
commit c71fe75446
37 changed files with 3794 additions and 2 deletions

View File

@ -0,0 +1,618 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBF.Rig.RegisterReaders.AllyReader
{
public class AllyMeterReader : ComponentBase,
IDevice,
IRegReaderDatastream,
ISessionDataMngmnt,
IOperation
{
private const int MaxStoredSamples = 40000;
private const long RawVolumeModulo = 0x1000000L;
private const long RawVolumeHalfRange = RawVolumeModulo / 2;
private const long RawTimestampModulo = 0x100000000L;
private const long RawTimestampHalfRange = RawTimestampModulo / 2;
private static readonly ILog log = LogManager.GetLogger(typeof(AllyMeterReader));
private readonly object commandSync = new object();
private readonly object opticalSync = new object();
private readonly StringBuilder opticalBuffer = new StringBuilder();
private readonly List<AllyOpticalSample> opticalSamples = new List<AllyOpticalSample>();
private readonly AllyReaderCfg allyCfg;
private IAllyTransport commandTransport;
private AllyCommandService commandService;
private SerialPort opticalPort;
private bool streamEnabled;
private bool operationActive;
private bool hasPreviousRawVolume;
private bool hasPreviousRawTimestamp;
private bool hasTestStartSample;
private uint previousRawVolume;
private uint previousRawTimestamp;
private long extendedRawVolume;
private long extendedRawTimestamp;
private double beginWMState;
private double endWMState;
private double timestampSecStart;
private double timestampSecEnd;
private string lastOpticalLine;
public AllyMeterReader()
{
}
public AllyMeterReader(IComponentCfg cfg)
: base(cfg)
{
allyCfg = cfg as AllyReaderCfg;
}
public override string ToString()
{
return string.Format("{0}({1})", ClassName, Cfg.ToString(-1));
}
public string SerialNumber { get; private set; }
public AllyVersionInfo VersionInfo { get; private set; }
public DateTime? SystemTimeUtc { get; private set; }
public byte? RebootCount { get; private set; }
public double? CalibrationFactorPercent { get; private set; }
public double? ExpectedCalibrationFactorPercent { get; private set; }
public bool CommFailed { get; private set; }
public AllyMeterSize ConfiguredMeterSize
{
get { return allyCfg == null ? AllyMeterSize.AutoDetect : allyCfg.ConfiguredMeterSize; }
}
public IReadOnlyList<AllyOpticalSample> OpticalSamples
{
get
{
lock (opticalSync)
{
return opticalSamples.ToArray();
}
}
}
public override void Initialize()
{
if (allyCfg == null)
throw new InvalidOperationException("ALLY reader configuration is missing.");
if (allyCfg.CommandComPortNr <= 0 || allyCfg.OptoComPortNr <= 0)
throw new InvalidOperationException("ALLY COM port configuration is invalid.");
PulsesPerLtr = allyCfg.MeterPulsesPerLiter;
QuantityUnits = "l";
log.FatalFormat("{0} initialized: {1}", Name, this);
}
public void RunDeviceBefore()
{
if (!streamEnabled || opticalPort == null || !opticalPort.IsOpen)
return;
try
{
string text = opticalPort.ReadExisting();
if (!string.IsNullOrEmpty(text))
ProcessOpticalText(text);
}
catch (Exception ex)
{
CommFailed = true;
log.Error("ALLY optical stream read failed.", ex);
}
}
public void RunDeviceAfter()
{
}
public void StopDevice()
{
StopDataStreamProcessing();
CloseCommandTransport();
}
public void StopDevice2()
{
}
public void StartSession()
{
lock (opticalSync)
{
operationActive = false;
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
}
SerialNumber = string.Empty;
VersionInfo = null;
SystemTimeUtc = null;
RebootCount = null;
CalibrationFactorPercent = null;
ExpectedCalibrationFactorPercent = null;
CommFailed = false;
}
public void SaveMark(object mark)
{
}
public void EndSession()
{
StopDataStreamProcessing();
CloseCommandTransport();
}
public void TestIsGoingToStartSoon(Test test, int repetitionNr)
{
}
public IOperation ReadDatastreamOp()
{
return this;
}
public void Start()
{
lock (opticalSync)
{
operationActive = true;
hasTestStartSample = false;
beginWMState = 0;
endWMState = 0;
timestampSecStart = 0;
timestampSecEnd = 0;
}
StartDataStreamProcessing();
}
public Event Run()
{
return Event.ReadRegisterDone;
}
public void Stop()
{
lock (opticalSync)
{
operationActive = false;
}
StopDataStreamProcessing();
}
public void StartDataStreamProcessing()
{
GetOpticalVolumeLitersPerRawUnit();
lock (opticalSync)
{
if (streamEnabled)
return;
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
if (DebugLevel == DebugMode.Normal)
{
opticalPort = new SerialPort(
"COM" + allyCfg.OptoComPortNr,
allyCfg.OptoBaudRate,
Parity.None,
8,
StopBits.One);
opticalPort.Open();
opticalPort.DiscardInBuffer();
}
streamEnabled = true;
}
}
public void StopDataStreamProcessing()
{
lock (opticalSync)
{
streamEnabled = false;
if (opticalPort == null)
return;
try
{
if (opticalPort.IsOpen)
opticalPort.Close();
}
finally
{
opticalPort.Dispose();
opticalPort = null;
}
}
}
public string ReadOptoData()
{
lock (opticalSync)
{
return lastOpticalLine ?? string.Empty;
}
}
public string ReadSerialNumber(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return SerialNumber = "ALLY-SIMULATED";
return ExecuteCommand(service => SerialNumber = service.ReadSerialNumber(timeoutMs));
}
public AllyVersionInfo ReadVersionAndType(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return VersionInfo = AllyVersionInfo.Parse("SIMULATED,SWM003,SIMULATED");
return ExecuteCommand(service => VersionInfo = service.ReadVersionAndType(timeoutMs));
}
public DateTime ReadSystemTimeUtc(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return (SystemTimeUtc = DateTime.UtcNow).Value;
return ExecuteCommand(service => (SystemTimeUtc = service.ReadSystemTimeUtc(timeoutMs)).Value);
}
public byte ReadRebootCount(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return (RebootCount = 0).Value;
return ExecuteCommand(service => (RebootCount = service.ReadRebootCount(timeoutMs)).Value);
}
public void OpenValve(int timeoutMs)
{
ExecuteCommand(service => service.OpenValve(timeoutMs));
}
public void SetSpreadSpectrum(bool enabled, int timeoutMs)
{
ExecuteCommand(service => service.SetSpreadSpectrum(enabled, timeoutMs));
}
public void SetMeterMode(byte mode, int timeoutMs)
{
ExecuteCommand(service => service.SetMeterMode(mode, timeoutMs));
}
public void SetDiagnosticLed(byte mode, int timeoutMs)
{
ExecuteCommand(service => service.SetDiagnosticLed(mode, timeoutMs));
}
public double ResetCalibrationFactor(int timeoutMs)
{
double factor = GetResetCalibrationFactorPercent();
ExecuteCommand(service => service.SetCalibrationFactorPercent(factor, timeoutMs));
ExpectedCalibrationFactorPercent = factor;
return factor;
}
public double ReadCalibrationFactor(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
{
double simulated = ExpectedCalibrationFactorPercent ?? GetResetCalibrationFactorPercent();
CalibrationFactorPercent = simulated;
return simulated;
}
return ExecuteCommand(
service => (CalibrationFactorPercent =
service.ReadCalibrationFactorPercent(timeoutMs)).Value);
}
public void SetCalibrationFactor(double factorPercent, int timeoutMs)
{
if (!IsCalibrationFactorWithinSpecification(factorPercent))
{
throw new ArgumentOutOfRangeException(
nameof(factorPercent),
"ALLY calibration factor is outside the UI-2093 limit for the configured meter size.");
}
ExecuteCommand(service => service.SetCalibrationFactorPercent(factorPercent, timeoutMs));
ExpectedCalibrationFactorPercent = factorPercent;
}
public void SetMagneticTamperProfile(AllyMagneticTamperProfile profile, int timeoutMs)
{
ExecuteCommand(service => service.SetMagneticTamperProfile(profile, timeoutMs));
}
public void SetDisplayVolume(string eightDigitVolume, int timeoutMs)
{
ExecuteCommand(service => service.SetDisplayVolume(eightDigitVolume, timeoutMs));
}
public void SetLcdTimeout(byte seconds, int timeoutMs)
{
ExecuteCommand(service => service.SetLcdTimeout(seconds, timeoutMs));
}
public void StartOffsetLearning(ushort samples, ushort delaySeconds, int timeoutMs)
{
ExecuteCommand(service => service.StartOffsetLearning(samples, delaySeconds, timeoutMs));
}
public double GetResetCalibrationFactorPercent()
{
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths:
case AllyMeterSize.ThreeQuarterShort:
case AllyMeterSize.ThreeQuarterLong:
return 100D;
case AllyMeterSize.OneInch:
return 260D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. UI-2031 serial-number parsing is required before selecting the reset calibration factor.");
}
}
public bool IsCalibrationFactorWithinSpecification(double value)
{
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths: return value >= 76D && value <= 96D;
case AllyMeterSize.ThreeQuarterShort: return value >= 88D && value <= 108D;
case AllyMeterSize.ThreeQuarterLong: return value >= 89D && value <= 120D;
case AllyMeterSize.OneInch: return value >= 236D && value <= 281D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. Calibration-factor limits cannot be selected safely.");
}
}
public RegisterReaderType RegisterReaderType { get { return RegisterReaderType.DataStream; } }
public int Position
{
get
{
if (string.IsNullOrEmpty(Name))
return 0;
int start = Name.Length;
while (start > 0 && char.IsDigit(Name[start - 1]))
start--;
int value;
return start < Name.Length && int.TryParse(Name.Substring(start), out value) ? value : 0;
}
}
public double PulsesPerLtr { get; set; }
public double LtrsPerPulse { get { return PulsesPerLtr > 0 ? 1D / PulsesPerLtr : 0D; } }
public string QuantityUnits { get; set; }
public int WMPulses { get { return (int)Math.Round(WMVolume * PulsesPerLtr); } }
public int WMRefPulses
{
get
{
return StateMachine.ControlBoardMain == null
? 0
: StateMachine.ControlBoardMain.RefPulses;
}
}
public double WMVolume { get { return Math.Abs(EndWMState - BeginWMState); } }
public double BeginWMState { get { return beginWMState; } }
public double EndWMState { get { return endWMState; } }
public bool NoSamples { get { return !hasTestStartSample || opticalSamples.Count < 2; } }
public double VolumeLtrStart { get { return beginWMState; } }
public double VolumeLtrEnd { get { return endWMState; } }
public double TimestampSecStart { get { return timestampSecStart; } }
public double TimestampSecEnd { get { return timestampSecEnd; } }
private void ProcessOpticalText(string text)
{
lock (opticalSync)
{
opticalBuffer.Append(text);
while (true)
{
string buffered = opticalBuffer.ToString();
int lineEnd = buffered.IndexOf('\n');
if (lineEnd < 0)
return;
string line = buffered.Substring(0, lineEnd + 1);
opticalBuffer.Remove(0, lineEnd + 1);
lastOpticalLine = line;
AllyOpticalSample sample;
if (!AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample))
continue;
ExtendRawVolume(sample.RawVolume);
ExtendRawTimestamp(sample.RawTimestamp);
sample.ExtendedVolumeLiters =
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
sample.ElapsedSeconds = extendedRawTimestamp / 8192D;
if (opticalSamples.Count == MaxStoredSamples)
opticalSamples.RemoveAt(0);
opticalSamples.Add(sample);
endWMState = sample.ExtendedVolumeLiters;
timestampSecEnd = sample.ElapsedSeconds;
if (operationActive && !hasTestStartSample)
{
hasTestStartSample = true;
beginWMState = sample.ExtendedVolumeLiters;
timestampSecStart = sample.ElapsedSeconds;
}
}
}
}
private void ExtendRawVolume(uint rawVolume)
{
if (!hasPreviousRawVolume)
{
hasPreviousRawVolume = true;
previousRawVolume = rawVolume;
extendedRawVolume = rawVolume;
return;
}
long delta = (long)rawVolume - previousRawVolume;
if (delta < -RawVolumeHalfRange)
delta += RawVolumeModulo;
else if (delta > RawVolumeHalfRange)
delta -= RawVolumeModulo;
extendedRawVolume += delta;
previousRawVolume = rawVolume;
}
private void ExtendRawTimestamp(uint rawTimestamp)
{
if (!hasPreviousRawTimestamp)
{
hasPreviousRawTimestamp = true;
previousRawTimestamp = rawTimestamp;
extendedRawTimestamp = rawTimestamp;
return;
}
long delta = (long)rawTimestamp - previousRawTimestamp;
if (delta < -RawTimestampHalfRange)
delta += RawTimestampModulo;
else if (delta > RawTimestampHalfRange)
delta -= RawTimestampModulo;
extendedRawTimestamp += delta;
previousRawTimestamp = rawTimestamp;
}
private double GetOpticalVolumeLitersPerRawUnit()
{
// The optical format scales volume by flow-tube size: raw / 16000
// for 5/8", 2 * raw / 16000 for 3/4", and 4 * raw / 16000 for 1".
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths: return 1D / 16000D;
case AllyMeterSize.ThreeQuarterShort:
case AllyMeterSize.ThreeQuarterLong: return 2D / 16000D;
case AllyMeterSize.OneInch: return 4D / 16000D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. UI-2031 serial-number parsing is required before decoding optical volume.");
}
}
private void ResetVolumeState()
{
hasPreviousRawVolume = false;
hasPreviousRawTimestamp = false;
hasTestStartSample = false;
previousRawVolume = 0;
previousRawTimestamp = 0;
extendedRawVolume = 0;
extendedRawTimestamp = 0;
beginWMState = 0;
endWMState = 0;
timestampSecStart = 0;
timestampSecEnd = 0;
}
private AllyCommandService GetCommandService()
{
lock (commandSync)
{
if (commandService != null)
return commandService;
commandTransport = new AllySerialTransportBuilder()
.WithPort("COM" + allyCfg.CommandComPortNr)
.WithBaudRate(allyCfg.CommandBaudRate)
.WithDataBits(8)
.WithParity(Parity.None)
.WithStopBits(StopBits.One)
.Build();
commandService = new AllyCommandService(commandTransport);
return commandService;
}
}
private T ExecuteCommand<T>(Func<AllyCommandService, T> command, T simulatedValue = default(T))
{
if (DebugLevel != DebugMode.Normal)
return simulatedValue;
lock (commandSync)
{
try
{
T value = command(GetCommandService());
CommFailed = false;
return value;
}
catch
{
CommFailed = true;
CloseCommandTransport();
throw;
}
}
}
private void ExecuteCommand(Action<AllyCommandService> command)
{
if (DebugLevel != DebugMode.Normal)
return;
ExecuteCommand(
service =>
{
command(service);
return true;
});
}
private void CloseCommandTransport()
{
lock (commandSync)
{
if (commandTransport != null)
commandTransport.Dispose();
commandTransport = null;
commandService = null;
}
}
}
}

View File

@ -0,0 +1,11 @@
namespace TBF.Rig.RegisterReaders.AllyReader
{
public enum AllyMeterSize
{
AutoDetect,
FiveEighths,
ThreeQuarterShort,
ThreeQuarterLong,
OneInch
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Globalization;
namespace TBF.Rig.RegisterReaders.AllyReader
{
/// <summary>
/// Validated common portion of the 42-byte optical telegram used by the
/// register-reader pattern referenced by UI-2093. ALLY calibration-only
/// fields are intentionally not inferred without UI-1204/UI-1236.
/// </summary>
public sealed class AllyOpticalSample
{
private const int TelegramLength = 42;
public string RawLine { get; private set; }
public DateTime ReceivedAtUtc { get; private set; }
public short RawFlow { get; private set; }
public uint RawVolume { get; private set; }
public uint RawTimestamp { get; private set; }
public double ExtendedVolumeLiters { get; internal set; }
public double ElapsedSeconds { get; internal set; }
private AllyOpticalSample()
{
}
public static bool TryParse(
string line,
DateTime receivedAtUtc,
out AllyOpticalSample sample)
{
sample = null;
if (string.IsNullOrWhiteSpace(line))
return false;
if (line.Length < TelegramLength)
return false;
string telegram = line.Substring(line.Length - TelegramLength, TelegramLength);
if (telegram[6] != '\t' || telegram[11] != '\t' || telegram[16] != '\t' ||
telegram[23] != '\t' || telegram[28] != '\t' || telegram[37] != '\t' ||
telegram[40] != '\r' || telegram[41] != '\n')
{
return false;
}
ushort rawFlowUnsigned;
uint rawVolume;
uint rawTimestamp;
byte checksum;
if (!ushort.TryParse(telegram.Substring(12, 4), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawFlowUnsigned) ||
!uint.TryParse(telegram.Substring(17, 6), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawVolume) ||
!uint.TryParse(telegram.Substring(29, 8), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawTimestamp) ||
!byte.TryParse(telegram.Substring(38, 2), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out checksum))
{
return false;
}
byte calculatedChecksum = 0;
for (int i = 0; i < TelegramLength - 4; i++)
calculatedChecksum += (byte)telegram[i];
if (calculatedChecksum != checksum || rawVolume > 0xFFFFFF)
return false;
sample = new AllyOpticalSample
{
RawLine = telegram,
ReceivedAtUtc = receivedAtUtc,
RawFlow = unchecked((short)rawFlowUnsigned),
RawVolume = rawVolume,
RawTimestamp = rawTimestamp
};
return true;
}
}
}

View File

@ -0,0 +1,191 @@
using System.Collections.Generic;
using System.IO.Ports;
using System.Xml.Serialization;
using Common;
using TBF.Rig.Generic;
namespace TBF.Rig.RegisterReaders.AllyReader
{
public class AllyReaderCfg : ComponentCfgBase, IComponentCfg, IParamsProvider
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(AllyReaderCfg) })[0];
public int CommandComPortNr;
public int CommandBaudRate;
public int OptoComPortNr;
public int OptoBaudRate;
public AllyMeterSize ConfiguredMeterSize;
public double MeterPulsesPerLiter;
private AllyReaderCfg()
{
}
public AllyReaderCfg(IComponentFactory factory)
{
Factory = factory;
Name = "Ally";
ParentName = string.Empty;
InitializeAll();
}
public override XmlSerializer GetSerializer()
{
return Serializer;
}
public IComponentCfgCtrl GetControl(IList<Config.Entities.Component> cmpntEntities)
{
return new Configs.ParamsProvider.ComponentCfgCtrl(this, null);
}
public string ComponentName { get { return Name; } }
public void InitializeAll()
{
CommandComPortNr = 1;
CommandBaudRate = 2400;
OptoComPortNr = 2;
OptoBaudRate = 9600;
ConfiguredMeterSize = AllyMeterSize.AutoDetect;
MeterPulsesPerLiter = 1000D;
}
public int ParamsCount()
{
return 6;
}
public string ParamName(int i)
{
switch (i)
{
case 0: return "Touch-Read COM port";
case 1: return "Touch-Read baud rate";
case 2: return "Optical COM port";
case 3: return "Optical baud rate";
case 4: return "Configured ALLY meter size";
case 5: return "Meter pulses per liter";
default: return string.Empty;
}
}
public ICollection<string> ParamValues(int i)
{
switch (i)
{
case 1:
return new[] { "1200", "2400", "4800", "9600" };
case 3:
return new[] { "9600", "19200", "38400", "57600" };
case 4:
return new[]
{
AllyMeterSize.AutoDetect.ToString(),
AllyMeterSize.FiveEighths.ToString(),
AllyMeterSize.ThreeQuarterShort.ToString(),
AllyMeterSize.ThreeQuarterLong.ToString(),
AllyMeterSize.OneInch.ToString()
};
default:
return null;
}
}
public string ToString(int i)
{
switch (i)
{
case 0: return CommandComPortNr.ToString();
case 1: return CommandBaudRate.ToString();
case 2: return OptoComPortNr.ToString();
case 3: return OptoBaudRate.ToString();
case 4: return ConfiguredMeterSize.ToString();
case 5: return MeterPulsesPerLiter.ToString(System.Globalization.CultureInfo.InvariantCulture);
default:
return string.Format(
"{0}: Touch-Read=COM{1}/{2}, Optical=COM{3}/{4}, Size={5}",
Name,
CommandComPortNr,
CommandBaudRate,
OptoComPortNr,
OptoBaudRate,
ConfiguredMeterSize);
}
}
public CfgUpdateFlags UpdateParam(int i, string strValue)
{
switch (i)
{
case 0: CommandComPortNr = int.Parse(strValue); break;
case 1: CommandBaudRate = int.Parse(strValue); break;
case 2: OptoComPortNr = int.Parse(strValue); break;
case 3: OptoBaudRate = int.Parse(strValue); break;
case 4: ConfiguredMeterSize = (AllyMeterSize)System.Enum.Parse(typeof(AllyMeterSize), strValue); break;
case 5: MeterPulsesPerLiter = Utils.ParseUDouble(strValue); break;
default: return CfgUpdateFlags.None;
}
return CfgUpdateFlags.RestartRqrd;
}
public bool ValidateParam(int i, string strValue, out string message)
{
message = string.Empty;
int integerValue;
double doubleValue;
switch (i)
{
case 0:
case 2:
if (int.TryParse(strValue, out integerValue) && integerValue > 0)
return true;
break;
case 1:
case 3:
if (int.TryParse(strValue, out integerValue) && integerValue > 0)
return true;
break;
case 4:
AllyMeterSize meterSize;
if (System.Enum.TryParse(strValue, out meterSize))
return true;
break;
case 5:
if (double.TryParse(strValue, out doubleValue) && doubleValue > 0)
return true;
break;
default:
message = "Invalid parameter index.";
return false;
}
message = ParamName(i) + " is invalid.";
return false;
}
public bool UpdateEmbeddedDbEntity()
{
return true;
}
public IParamsProvider Clone()
{
return new AllyReaderCfg
{
Name = Name,
ParentName = ParentName,
Factory = Factory,
CommandComPortNr = CommandComPortNr,
CommandBaudRate = CommandBaudRate,
OptoComPortNr = OptoComPortNr,
OptoBaudRate = OptoBaudRate,
ConfiguredMeterSize = ConfiguredMeterSize,
MeterPulsesPerLiter = MeterPulsesPerLiter
};
}
}
}

View File

@ -0,0 +1,208 @@
using System;
using System.Globalization;
using System.Linq;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
/// <summary>
/// ALLY commands required by UI-2093 R9.0. The service deliberately omits
/// iPerl-only Q2, 2 Hz, flip-mode and RFID structure operations.
/// </summary>
public sealed class AllyCommandService
{
private static readonly DateTime SystemTimeEpochUtc =
new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly IAllyTransport transport;
private readonly AllyFrameParser parser = new AllyFrameParser();
public AllyCommandService(IAllyTransport transport)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
}
public string ReadSerialNumber(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithCommand(AllyCommand.ViewFactoryId).BuildBytes(),
timeoutMs);
return response.GetNullTerminatedAscii();
}
public AllyVersionInfo ReadVersionAndType(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithCommand(AllyCommand.ViewVersionAndType).BuildBytes(),
timeoutMs);
return AllyVersionInfo.Parse(response.GetNullTerminatedAscii());
}
public DateTime ReadSystemTimeUtc(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewSystemTime).BuildBytes(),
timeoutMs);
return SystemTimeEpochUtc.AddSeconds(response.GetUInt32LittleEndian());
}
public byte ReadRebootCount(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewRebootCount).BuildBytes(),
timeoutMs);
return response.GetByte();
}
public void OpenValve(int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithCommand(AllyCommand.SetValvePosition)
.WithBytes(0x00, 0x02)
.BuildBytes(),
timeoutMs);
}
public void SetSpreadSpectrum(bool enabled, int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.Configuration)
.WithBytes(0x06, enabled ? (byte)0x00 : (byte)0x01)
.BuildBytes(),
timeoutMs);
}
public void SetMeterMode(byte mode, int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithCommand(AllyCommand.SetMeterMode)
.WithByte(mode)
.BuildBytes(),
timeoutMs);
}
public void SetDiagnosticLed(byte mode, int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
.WithByte(mode)
.BuildBytes(),
timeoutMs);
}
public double ReadCalibrationFactorPercent(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewCalibration).BuildBytes(),
timeoutMs);
return response.GetUInt16LittleEndian() / 40.96D;
}
public void SetCalibrationFactorPercent(double factorPercent, int timeoutMs)
{
if (factorPercent <= 0 || factorPercent > ushort.MaxValue / 40.96D)
throw new ArgumentOutOfRangeException(nameof(factorPercent));
ushort rawValue = checked((ushort)Math.Round(
factorPercent * 40.96D,
MidpointRounding.AwayFromZero));
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetCalibration)
.WithUInt16LittleEndian(rawValue)
.BuildBytes(),
timeoutMs);
}
public void SetMagneticTamperProfile(AllyMagneticTamperProfile profile, int timeoutMs)
{
AllyFrameBuilder builder = new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.Configuration);
// Exact UI-2093 frames. The leading byte selects the RCS profile
// (0x02) or the firmware-default profile (0x05).
if (profile == AllyMagneticTamperProfile.Calibration)
builder.WithBytes(0x02, 0x5A, 0x0C, 0x05, 0x3C, 0x50);
else
builder.WithBytes(0x05, 0x1E, 0x04, 0x05, 0x3C, 0x50);
Send(builder.BuildBytes(), timeoutMs);
}
public void SetDisplayVolume(string eightDigitVolume, int timeoutMs)
{
if (eightDigitVolume == null ||
eightDigitVolume.Length != 8 ||
!eightDigitVolume.All(char.IsDigit))
{
throw new ArgumentException(
"ALLY display volume must contain exactly eight digits.",
nameof(eightDigitVolume));
}
Send(
new AllyFrameBuilder()
.WithCommand(AllyCommand.SetPresetTotal)
.WithNullTerminatedAscii(eightDigitVolume)
.BuildBytes(),
timeoutMs);
}
public void SetLcdTimeout(byte seconds, int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetLcdTimeout)
.WithByte(seconds)
.BuildBytes(),
timeoutMs);
}
public void StartOffsetLearning(ushort samples, ushort startDelaySeconds, int timeoutMs)
{
if (samples == 0)
throw new ArgumentOutOfRangeException(nameof(samples));
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.StartOffsetLearning)
.WithUInt16LittleEndian(samples)
.WithUInt16LittleEndian(startDelaySeconds)
.BuildBytes(),
timeoutMs);
}
private AllyResponse Send(byte[] request, int timeoutMs)
{
try
{
if (!transport.IsOpen)
transport.Open();
byte[] rawResponse = transport.SendAndWait(request, timeoutMs);
AllyResponse response = parser.ParseResponse(rawResponse);
if (!response.IsSuccess)
{
throw new AllyCommunicationException(
"ALLY command failed with status 0x" +
response.Status.ToString("X2", CultureInfo.InvariantCulture) + ".",
response.Status);
}
return response;
}
catch (AllyCommunicationException)
{
throw;
}
catch (Exception ex)
{
throw new AllyCommunicationException("ALLY communication failed.", ex);
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public sealed class AllyFrame
{
public byte Direction { get; private set; }
public byte[] Information { get; private set; }
public AllyFrame(byte direction, byte[] information)
{
Direction = direction;
Information = information ?? Array.Empty<byte>();
}
public byte[] ToArray()
{
int length = 4 + Information.Length;
if (length > byte.MaxValue)
throw new InvalidOperationException("ALLY frame is too long.");
byte[] bytes = new byte[length];
bytes[0] = AllyProtocol.Start;
bytes[1] = Direction;
bytes[2] = (byte)length;
Buffer.BlockCopy(Information, 0, bytes, 3, Information.Length);
bytes[bytes.Length - 1] = AllyProtocol.End;
return bytes;
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
/// <summary>
/// Builds the 0x53 0x57 &lt;length&gt; ... 0x0D request frames used by
/// the ALLY Touch-Read commands in UI-2093.
/// </summary>
public sealed class AllyFrameBuilder
{
private readonly List<byte> information = new List<byte>();
internal AllyFrameBuilder WithCommand(AllyCommand command)
{
EnsureCommandNotSet();
information.Add((byte)command);
return this;
}
internal AllyFrameBuilder WithDeviceCommand(AllyDeviceCommand command)
{
EnsureCommandNotSet();
information.Add(AllyProtocol.DeviceSpecific);
information.Add((byte)command);
return this;
}
public AllyFrameBuilder WithByte(byte value)
{
EnsureCommandSet();
information.Add(value);
return this;
}
public AllyFrameBuilder WithBytes(params byte[] values)
{
EnsureCommandSet();
if (values != null)
information.AddRange(values);
return this;
}
public AllyFrameBuilder WithUInt16LittleEndian(ushort value)
{
EnsureCommandSet();
information.Add((byte)(value & 0xFF));
information.Add((byte)(value >> 8));
return this;
}
public AllyFrameBuilder WithNullTerminatedAscii(string value)
{
EnsureCommandSet();
if (!string.IsNullOrEmpty(value))
information.AddRange(Encoding.ASCII.GetBytes(value));
information.Add(0x00);
return this;
}
public AllyFrame Build()
{
EnsureCommandSet();
return new AllyFrame(AllyProtocol.Write, information.ToArray());
}
public byte[] BuildBytes()
{
return Build().ToArray();
}
private void EnsureCommandSet()
{
if (information.Count == 0)
throw new InvalidOperationException("ALLY command must be set before its payload.");
}
private void EnsureCommandNotSet()
{
if (information.Count != 0)
throw new InvalidOperationException("ALLY frame already contains a command.");
}
}
}

View File

@ -0,0 +1,29 @@
using System;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public sealed class AllyFrameParser
{
public AllyResponse ParseResponse(byte[] data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
if (data.Length < 5)
throw new FormatException("ALLY response is too short.");
if (data[0] != AllyProtocol.Start)
throw new FormatException("ALLY response has an invalid start byte.");
if (data[1] != AllyProtocol.Read)
throw new FormatException("ALLY frame is not a response.");
if (data[2] != data.Length)
throw new FormatException("ALLY response length does not match its length field.");
if (data[data.Length - 1] != AllyProtocol.End)
throw new FormatException("ALLY response has an invalid end byte.");
byte[] payload = new byte[data.Length - 5];
if (payload.Length > 0)
Buffer.BlockCopy(data, 4, payload, 0, payload.Length);
return new AllyResponse(data[3], payload);
}
}
}

View File

@ -0,0 +1,62 @@
using System;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
internal static class AllyProtocol
{
public const byte Start = 0x53;
public const byte Write = 0x57;
public const byte Read = 0x52;
public const byte End = 0x0D;
public const byte CommandCompleted = 0x01;
public const byte DeviceSpecific = 0xFD;
}
internal enum AllyCommand : byte
{
ViewFactoryId = 0x01,
ViewVersionAndType = 0x05,
SetPresetTotal = 0x14,
SetMeterMode = 0x1A,
SetValvePosition = 0x1E
}
internal enum AllyDeviceCommand : byte
{
ViewSystemTime = 0x10,
Configuration = 0x15,
ViewCalibration = 0x53,
SetCalibration = 0x54,
ViewRebootCount = 0x55,
SetDiagnosticLed = 0x60,
SetLcdTimeout = 0x8C,
StartOffsetLearning = 0xD1
}
public enum AllyMagneticTamperProfile
{
Calibration,
FirmwareDefaults
}
public sealed class AllyCommunicationException : Exception
{
public byte? Status { get; private set; }
public AllyCommunicationException(string message)
: base(message)
{
}
public AllyCommunicationException(string message, byte status)
: base(message)
{
Status = status;
}
public AllyCommunicationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.Text;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public sealed class AllyResponse
{
public byte Status { get; private set; }
public byte[] Payload { get; private set; }
public bool IsSuccess { get { return Status == AllyProtocol.CommandCompleted; } }
internal AllyResponse(byte status, byte[] payload)
{
Status = status;
Payload = payload ?? Array.Empty<byte>();
}
public string GetNullTerminatedAscii()
{
int length = Array.IndexOf(Payload, (byte)0x00);
if (length < 0)
length = Payload.Length;
return Encoding.ASCII.GetString(Payload, 0, length).Trim();
}
public byte GetByte()
{
if (Payload.Length != 1)
throw new FormatException("ALLY response does not contain one byte.");
return Payload[0];
}
public ushort GetUInt16LittleEndian()
{
if (Payload.Length != 2)
throw new FormatException("ALLY response does not contain a UInt16 value.");
return (ushort)(Payload[0] | (Payload[1] << 8));
}
public uint GetUInt32LittleEndian()
{
if (Payload.Length != 4)
throw new FormatException("ALLY response does not contain a UInt32 value.");
return (uint)(Payload[0]
| (Payload[1] << 8)
| (Payload[2] << 16)
| (Payload[3] << 24));
}
}
}

View File

@ -0,0 +1,178 @@
using System;
using System.Diagnostics;
using System.IO.Ports;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public sealed class AllySerialTransportBuilder
{
private string portName;
private int baudRate = 2400;
private int dataBits = 8;
private Parity parity = Parity.None;
private StopBits stopBits = StopBits.One;
public AllySerialTransportBuilder WithPort(string value)
{
portName = value;
return this;
}
public AllySerialTransportBuilder WithBaudRate(int value)
{
baudRate = value;
return this;
}
public AllySerialTransportBuilder WithDataBits(int value)
{
dataBits = value;
return this;
}
public AllySerialTransportBuilder WithParity(Parity value)
{
parity = value;
return this;
}
public AllySerialTransportBuilder WithStopBits(StopBits value)
{
stopBits = value;
return this;
}
public AllySerialTransport Build()
{
if (string.IsNullOrWhiteSpace(portName))
throw new InvalidOperationException("ALLY command port is not configured.");
if (baudRate <= 0 || dataBits <= 0)
throw new InvalidOperationException("ALLY serial settings are invalid.");
return new AllySerialTransport(portName, baudRate, dataBits, parity, stopBits);
}
public AllySerialTransport BuildAndConnect()
{
AllySerialTransport transport = Build();
transport.Open();
return transport;
}
}
public sealed class AllySerialTransport : IAllyTransport
{
private readonly object sync = new object();
private readonly string portName;
private readonly int baudRate;
private readonly int dataBits;
private readonly Parity parity;
private readonly StopBits stopBits;
private SerialPort serialPort;
internal AllySerialTransport(
string portName,
int baudRate,
int dataBits,
Parity parity,
StopBits stopBits)
{
this.portName = portName;
this.baudRate = baudRate;
this.dataBits = dataBits;
this.parity = parity;
this.stopBits = stopBits;
}
public bool IsOpen { get { return serialPort != null && serialPort.IsOpen; } }
public void Open()
{
lock (sync)
{
if (IsOpen)
return;
DisposePort();
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.Open();
}
}
public byte[] SendAndWait(byte[] request, int timeoutMs)
{
if (request == null || request.Length == 0)
throw new ArgumentException("ALLY request is empty.", nameof(request));
if (timeoutMs <= 0)
throw new ArgumentOutOfRangeException(nameof(timeoutMs));
lock (sync)
{
if (!IsOpen)
Open();
serialPort.DiscardInBuffer();
serialPort.WriteTimeout = timeoutMs;
serialPort.Write(request, 0, request.Length);
Stopwatch stopwatch = Stopwatch.StartNew();
int first;
do
{
first = ReadByte(stopwatch, timeoutMs);
}
while (first != AllyProtocol.Start);
int direction = ReadByte(stopwatch, timeoutMs);
int length = ReadByte(stopwatch, timeoutMs);
if (length < 5)
throw new FormatException("ALLY response length is invalid.");
byte[] response = new byte[length];
response[0] = (byte)first;
response[1] = (byte)direction;
response[2] = (byte)length;
for (int i = 3; i < response.Length; i++)
response[i] = (byte)ReadByte(stopwatch, timeoutMs);
return response;
}
}
private int ReadByte(Stopwatch stopwatch, int timeoutMs)
{
int remaining = timeoutMs - (int)stopwatch.ElapsedMilliseconds;
if (remaining <= 0)
throw new TimeoutException("ALLY response timeout.");
serialPort.ReadTimeout = remaining;
return serialPort.ReadByte();
}
public void Dispose()
{
lock (sync)
{
DisposePort();
}
}
private void DisposePort()
{
if (serialPort == null)
return;
try
{
if (serialPort.IsOpen)
serialPort.Close();
}
finally
{
serialPort.Dispose();
serialPort = null;
}
}
}
}

View File

@ -0,0 +1,45 @@
using System;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public sealed class AllyVersionInfo
{
public string TouchReadVersion { get; private set; }
public string DeviceType { get; private set; }
public string FirmwareVersion { get; private set; }
public string RawValue { get; private set; }
private AllyVersionInfo()
{
}
public static AllyVersionInfo Parse(string rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
throw new FormatException("ALLY version/type response is empty.");
string[] fields = rawValue.Split(',');
if (fields.Length != 3)
throw new FormatException("ALLY version/type response must contain three comma-separated fields.");
for (int i = 0; i < fields.Length; i++)
fields[i] = fields[i].Trim();
if (Array.Exists(fields, string.IsNullOrWhiteSpace))
throw new FormatException("ALLY version/type response contains an empty field.");
return new AllyVersionInfo
{
TouchReadVersion = fields[0],
DeviceType = fields[1],
FirmwareVersion = fields[2],
RawValue = rawValue.Trim()
};
}
public override string ToString()
{
return RawValue;
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
public interface IAllyTransport : IDisposable
{
bool IsOpen { get; }
void Open();
byte[] SendAndWait(byte[] request, int timeoutMs);
}
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using TBF.Rig.Generic;
namespace TBF.Rig.RegisterReaders.AllyReader
{
public class Factory : IComponentFactory
{
public string ClassName { get { return GetType().Namespace.Substring(8); } }
public override string ToString()
{
return ClassName;
}
public IComponent DummyComponent()
{
return new AllyMeterReader();
}
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components)
{
return new AllyMeterReader(cfg);
}
public IComponentCfg DefaultConfig()
{
return new AllyReaderCfg(this);
}
public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component)
{
return ComponentCfgBase.CreateFromDbEntity(AllyReaderCfg.Serializer, component, this);
}
}
}

View File

@ -0,0 +1,86 @@
namespace TBF.Rig.TestMethods.AllyCalibration
{
public enum AllyCalibrationActivity
{
ReadSerialNumber,
ReadVersionAndType,
ReadSystemTime,
ReadRebootCount,
OpenValve,
DisableSpreadSpectrum,
SetInitialMeterMode,
TurnDiagnosticLedOn,
ResetCalibrationFactor,
ReadCalibrationFactor,
SetCalibrationMagneticTamperProfile,
TurnDiagnosticLedOff,
SetActiveMeterMode,
EnableSpreadSpectrum,
SetDefaultMagneticTamperProfile,
WriteCalibrationFactor,
WriteDisplayVolume,
SetLcdTimeout,
StartOffsetLearning
}
internal static class AllyCalibrationActivityNames
{
public const string ReadSerialNumber = "Read serial number";
public const string ReadVersionAndType = "Read version and type";
public const string ReadSystemTime = "Read system time";
public const string ReadRebootCount = "Read reboot count";
public const string OpenValve = "Open ALLY valve";
public const string DisableSpreadSpectrum = "Disable spread spectrum";
public const string SetInitialMeterMode = "Set initial meter mode";
public const string TurnDiagnosticLedOn = "Turn diagnostic LED on";
public const string ResetCalibrationFactor = "Reset calibration factor";
public const string ReadCalibrationFactor = "Read calibration factor";
public const string SetCalibrationMagneticTamperProfile = "Set calibration magnetic-tamper profile";
public const string TurnDiagnosticLedOff = "Turn diagnostic LED off";
public const string SetActiveMeterMode = "Set active meter mode";
public const string EnableSpreadSpectrum = "Enable spread spectrum";
public const string SetDefaultMagneticTamperProfile = "Set default magnetic-tamper profile";
public const string WriteCalibrationFactor = "Write calibration factor";
public const string WriteDisplayVolume = "Write display volume";
public const string SetLcdTimeout = "Set LCD timeout";
public const string StartOffsetLearning = "Start offset learning";
public static readonly string[] All =
{
ReadSerialNumber,
ReadVersionAndType,
ReadSystemTime,
ReadRebootCount,
OpenValve,
DisableSpreadSpectrum,
SetInitialMeterMode,
TurnDiagnosticLedOn,
ResetCalibrationFactor,
ReadCalibrationFactor,
SetCalibrationMagneticTamperProfile,
TurnDiagnosticLedOff,
SetActiveMeterMode,
EnableSpreadSpectrum,
SetDefaultMagneticTamperProfile,
WriteCalibrationFactor,
WriteDisplayVolume,
SetLcdTimeout,
StartOffsetLearning
};
public static bool TryParse(string value, out AllyCalibrationActivity activity)
{
for (int i = 0; i < All.Length; i++)
{
if (string.Equals(All[i], value, System.StringComparison.OrdinalIgnoreCase))
{
activity = (AllyCalibrationActivity)i;
return true;
}
}
activity = default(AllyCalibrationActivity);
return false;
}
}
}

View File

@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Common;
using Config.Entities;
using Results.Entities;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
using TBF.Rig.Sequences;
using TBF.UiBridge;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class AllyCalibrationSeq : SequenceBase
{
private const byte InitialMeterMode = 0x09;
private const byte ActiveMeterMode = 0x02;
private const byte DiagnosticLedCalibrationMode = 0xC2;
private const string DisplayVolume = "01134010";
private const byte LcdTimeoutSeconds = 30;
private const ushort OffsetLearningSamples = 1000;
private const ushort OffsetLearningDelaySeconds = 1;
public IList<Event> Execute(
Test test,
int repetitionNr,
TestMethod method,
TestMethodCfg cfg,
TestMethodParams parameters)
{
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, Progress.JustStarted));
TestRslt testResult = BatchRslts.GetTestRslt(test.Name, 0);
if (testResult != null)
testResult.StartTime = DateTime.Now;
List<string> messages = new List<string>();
for (int position = 0; position < BatchRslts.WMPositionsCount; position++)
{
Results.Entities.WaterMeter waterMeter = BatchRslts.Batch.WaterMeters[position];
if (waterMeter == null || waterMeter.Disabled)
continue;
MeterTestRslt meterResult = BatchRslts.GetMeterTestRslt(
test.Name,
position,
CompoundMeterId.Single);
AllyMeterReader reader = sensPath != null &&
sensPath.RegisterReaders != null &&
position < sensPath.RegisterReaders.Length
? sensPath.RegisterReaders[position] as AllyMeterReader
: null;
bool passed;
string resultMessage;
if (reader == null)
{
passed = false;
resultMessage = "ALLY" + (position + 1) + ": ALLY reader is not configured.";
}
else
{
passed = ExecuteWithRetries(reader, cfg, parameters, out resultMessage);
resultMessage = reader.Name + ": " + resultMessage;
}
messages.Add(resultMessage);
if (meterResult != null)
{
meterResult.RegReaderType = (int)RegisterReaderType.DataStream;
meterResult.TestDone = true;
meterResult.Passed = passed;
}
}
if (testResult != null)
{
testResult.EndTime = DateTime.Now;
testResult.TestDone = true;
testResult.Remark = string.Join("; ", messages);
}
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, Progress.Completed));
Bridge.OnTestCompleted(this, new TestCompletedEventArgs(test.Name, testResult));
allResults.Info(TestResult2CsvLine(test.Name, 0));
return new List<Event> { Event.Done };
}
private static bool ExecuteWithRetries(
AllyMeterReader reader,
TestMethodCfg cfg,
TestMethodParams parameters,
out string resultMessage)
{
Exception lastException = null;
int attempts = Math.Max(1, cfg.MaxAttempts);
for (int attempt = 1; attempt <= attempts; attempt++)
{
try
{
bool passed = ExecuteOnce(reader, cfg, parameters, out resultMessage);
if (!passed)
return false;
if (attempt > 1)
resultMessage += " (attempt " + attempt + ")";
return true;
}
catch (Exception ex)
{
lastException = ex;
if (attempt < attempts && cfg.DelayBetweenAttemptsMs > 0)
Thread.Sleep(cfg.DelayBetweenAttemptsMs);
}
}
resultMessage = lastException == null
? "ALLY command failed."
: "ALLY command failed after " + attempts + " attempts: " + lastException.Message;
return false;
}
private static bool ExecuteOnce(
AllyMeterReader reader,
TestMethodCfg cfg,
TestMethodParams parameters,
out string resultMessage)
{
AllyCalibrationActivity activity;
if (!AllyCalibrationActivityNames.TryParse(parameters.Activity, out activity))
throw new InvalidOperationException("Unknown ALLY calibration activity: " + parameters.Activity);
int timeout = cfg.CommandTimeoutMs;
switch (activity)
{
case AllyCalibrationActivity.ReadSerialNumber:
string serialNumber = reader.ReadSerialNumber(timeout);
if (string.IsNullOrWhiteSpace(serialNumber))
throw new FormatException("ALLY serial number response is empty.");
resultMessage = "SerialNumber=" + serialNumber;
return true;
case AllyCalibrationActivity.ReadVersionAndType:
AllyVersionInfo version = reader.ReadVersionAndType(timeout);
bool deviceTypeOk = string.Equals(
version.DeviceType,
cfg.ExpectedDeviceType,
StringComparison.OrdinalIgnoreCase);
bool firmwareOk = string.IsNullOrWhiteSpace(cfg.ExpectedFirmwareVersion) ||
string.Equals(
version.FirmwareVersion,
cfg.ExpectedFirmwareVersion,
StringComparison.OrdinalIgnoreCase);
resultMessage = "Version=" + version +
", DeviceType=" + (deviceTypeOk ? "OK" : "NOK") +
", Firmware=" + (firmwareOk ? "OK" : "NOK");
return deviceTypeOk && firmwareOk;
case AllyCalibrationActivity.ReadSystemTime:
DateTime systemTime = reader.ReadSystemTimeUtc(timeout);
resultMessage = "SystemTimeUtc=" + systemTime.ToString("O");
return true;
case AllyCalibrationActivity.ReadRebootCount:
byte rebootCount = reader.ReadRebootCount(timeout);
resultMessage = "RebootCount=" + rebootCount;
return rebootCount == 0;
case AllyCalibrationActivity.OpenValve:
reader.OpenValve(timeout);
resultMessage = "Valve=open";
return true;
case AllyCalibrationActivity.DisableSpreadSpectrum:
reader.SetSpreadSpectrum(false, timeout);
resultMessage = "SpreadSpectrum=disabled";
return true;
case AllyCalibrationActivity.SetInitialMeterMode:
reader.SetMeterMode(InitialMeterMode, timeout);
resultMessage = "MeterMode=0x09";
return true;
case AllyCalibrationActivity.TurnDiagnosticLedOn:
reader.SetDiagnosticLed(DiagnosticLedCalibrationMode, timeout);
resultMessage = "DiagnosticLED=0xC2";
return true;
case AllyCalibrationActivity.ResetCalibrationFactor:
double resetFactor = reader.ResetCalibrationFactor(timeout);
resultMessage = "ResetCalibrationFactor=" + resetFactor.ToString("F2") + "%";
return true;
case AllyCalibrationActivity.ReadCalibrationFactor:
double currentFactor = reader.ReadCalibrationFactor(timeout);
bool factorMatches = !reader.ExpectedCalibrationFactorPercent.HasValue ||
Math.Abs(currentFactor - reader.ExpectedCalibrationFactorPercent.Value) <= 0.05D;
resultMessage = "CalibrationFactor=" + currentFactor.ToString("F3") + "%";
return factorMatches;
case AllyCalibrationActivity.SetCalibrationMagneticTamperProfile:
reader.SetMagneticTamperProfile(AllyMagneticTamperProfile.Calibration, timeout);
resultMessage = "MagneticTamperProfile=calibration";
return true;
case AllyCalibrationActivity.TurnDiagnosticLedOff:
reader.SetDiagnosticLed(0x00, timeout);
resultMessage = "DiagnosticLED=off";
return true;
case AllyCalibrationActivity.SetActiveMeterMode:
reader.SetMeterMode(ActiveMeterMode, timeout);
resultMessage = "MeterMode=active";
return true;
case AllyCalibrationActivity.EnableSpreadSpectrum:
reader.SetSpreadSpectrum(true, timeout);
resultMessage = "SpreadSpectrum=enabled";
return true;
case AllyCalibrationActivity.SetDefaultMagneticTamperProfile:
reader.SetMagneticTamperProfile(AllyMagneticTamperProfile.FirmwareDefaults, timeout);
resultMessage = "MagneticTamperProfile=firmware defaults";
return true;
case AllyCalibrationActivity.WriteCalibrationFactor:
reader.SetCalibrationFactor(parameters.CalibrationFactorPercent, timeout);
resultMessage = "CalibrationFactorWritten=" +
parameters.CalibrationFactorPercent.ToString("F3") + "%";
return true;
case AllyCalibrationActivity.WriteDisplayVolume:
reader.SetDisplayVolume(DisplayVolume, timeout);
resultMessage = "DisplayVolume=" + DisplayVolume;
return true;
case AllyCalibrationActivity.SetLcdTimeout:
reader.SetLcdTimeout(LcdTimeoutSeconds, timeout);
resultMessage = "LcdTimeout=30s";
return true;
case AllyCalibrationActivity.StartOffsetLearning:
reader.StartOffsetLearning(
OffsetLearningSamples,
OffsetLearningDelaySeconds,
timeout);
resultMessage = "OffsetLearning=1000 samples, 1s delay";
return true;
default:
throw new InvalidOperationException("Unsupported ALLY calibration activity.");
}
}
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using TBF.Rig.Generic;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class Factory : IComponentFactory
{
public string ClassName { get { return GetType().Namespace.Substring(8); } }
public IComponent DummyComponent()
{
return new TestMethod();
}
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components)
{
return new TestMethod(cfg);
}
public IComponentCfg DefaultConfig()
{
return new TestMethodCfg(this);
}
public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component)
{
return ComponentCfgBase.CreateFromDbEntity(TestMethodCfg.Serializer, component, this);
}
}
}

View File

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.Sequences;
using TBF.UiBridge;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethod : ComponentBase, ISimultTestMethod, ITestMethodSmart
{
private static readonly ILog log = LogManager.GetLogger(typeof(TestMethod));
private readonly TestMethodCfg allyCfg;
public TestMethod()
{
}
public TestMethod(IComponentCfg cfg)
: base(cfg)
{
allyCfg = cfg as TestMethodCfg;
}
public override string ToString()
{
return Cfg == null ? ClassName : string.Format("{0}({1})", ClassName, Cfg.ToString(-1));
}
public FlowType MethodFlowType { get { return FlowType.volume; } }
public bool SimultWithPrevious
{
get
{
TestMethodParams parameters = allyCfg == null ? null : allyCfg.TestParams as TestMethodParams;
return parameters != null && parameters.SimultWithPrevious;
}
}
public bool SimultWithNext
{
get
{
TestMethodParams parameters = allyCfg == null ? null : allyCfg.TestParams as TestMethodParams;
return parameters != null && parameters.SimultWithNext;
}
}
public bool CanTest(MetersKind meters)
{
return meters == MetersKind.Single;
}
public bool DoTransitions()
{
return false;
}
public bool CheckDeviceCaps(Test test, OutputPath devices, out string message)
{
message = string.Empty;
return true;
}
public override void Initialize()
{
if (allyCfg == null)
throw new InvalidOperationException("ALLY calibration test method configuration is missing.");
log.FatalFormat("{0} initialized: {1}", Name, this);
}
public IList<Event> Execute(Test test, int repetitionNr, bool isLastRepetition)
{
TestMethodParams parameters = allyCfg.TestParams as TestMethodParams;
if (parameters == null)
throw new InvalidOperationException("ALLY calibration test parameters are missing.");
if (DebugLevel == DebugMode.Normal)
return new AllyCalibrationSeq().Execute(test, repetitionNr, this, allyCfg, parameters);
new AllyCalibrationSeq().MakeSimulatedTrivial(test, repetitionNr, test.Part);
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, 1, Progress.Completed));
Bridge.OnTestCompleted(
this,
new TestCompletedEventArgs(
test.Name,
ProcessData.BatchRslts.GetTestRslt(Common.Utils.GetTestName(test.Name, 1, 1), 0)));
return new List<Event> { Event.Done };
}
}
}

View File

@ -0,0 +1,186 @@
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using Common;
using Config.Entities;
using TBF.Rig.Generic;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethodCfg : ComponentCfgBase, IComponentCfg, IParamsProvider
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(TestMethodCfg) })[0];
public int CommandTimeoutMs;
public int MaxAttempts;
public int DelayBetweenAttemptsMs;
public string ExpectedDeviceType;
public string ExpectedFirmwareVersion;
[XmlIgnore]
public ITestParams TestParams { get; set; }
private TestMethodCfg()
{
}
public TestMethodCfg(IComponentFactory factory)
{
Factory = factory;
Name = "AllyCalibrationCommunication";
ParentName = string.Empty;
InitializeAll();
TestParams = CreateTestParamsProvider() as TestMethodParams;
}
public override XmlSerializer GetSerializer()
{
return Serializer;
}
public IComponentCfgCtrl GetControl(IList<Component> cmpntEntities)
{
return new Configs.ParamsProvider.ComponentCfgCtrl(this, null);
}
public override IParamsProvider GetRuntimeTestParamsProvider()
{
return TestParams;
}
public override IParamsProvider CreateTestParamsProvider()
{
return new TestMethodParams(true);
}
public override IParamsProvider GetUITestParamsProvider(Test test)
{
return test.Method == Name ? base.GetUITestParamsProvider(test) : null;
}
public string ComponentName { get { return Name; } }
public void InitializeAll()
{
CommandTimeoutMs = 5000;
MaxAttempts = 4;
DelayBetweenAttemptsMs = 250;
ExpectedDeviceType = "SWM003";
ExpectedFirmwareVersion = string.Empty;
}
public int ParamsCount()
{
return 5;
}
public string ParamName(int i)
{
switch (i)
{
case 0: return "Command timeout [ms]";
case 1: return "Maximum command attempts";
case 2: return "Delay between attempts [ms]";
case 3: return "Expected ALLY device type";
case 4: return "Expected firmware version";
default: return string.Empty;
}
}
public ICollection<string> ParamValues(int i)
{
return null;
}
public string ToString(int i)
{
switch (i)
{
case 0: return CommandTimeoutMs.ToString();
case 1: return MaxAttempts.ToString();
case 2: return DelayBetweenAttemptsMs.ToString();
case 3: return ExpectedDeviceType;
case 4: return ExpectedFirmwareVersion;
default:
return string.Format(
"{0}: timeout={1}ms, attempts={2}, device={3}, firmware={4}",
Name,
CommandTimeoutMs,
MaxAttempts,
ExpectedDeviceType,
string.IsNullOrEmpty(ExpectedFirmwareVersion) ? "not checked" : ExpectedFirmwareVersion);
}
}
public CfgUpdateFlags UpdateParam(int i, string strValue)
{
switch (i)
{
case 0: CommandTimeoutMs = int.Parse(strValue); break;
case 1: MaxAttempts = int.Parse(strValue); break;
case 2: DelayBetweenAttemptsMs = int.Parse(strValue); break;
case 3: ExpectedDeviceType = strValue; break;
case 4: ExpectedFirmwareVersion = strValue; break;
default: return CfgUpdateFlags.None;
}
return CfgUpdateFlags.RestartRqrd;
}
public bool ValidateParam(int i, string strValue, out string message)
{
message = string.Empty;
int value;
switch (i)
{
case 0:
if (int.TryParse(strValue, out value) && value >= 100 && value <= 60000)
return true;
break;
case 1:
if (int.TryParse(strValue, out value) && value >= 1 && value <= 4)
return true;
break;
case 2:
if (int.TryParse(strValue, out value) && value >= 0 && value <= 10000)
return true;
break;
case 3:
if (!string.IsNullOrWhiteSpace(strValue))
return true;
break;
case 4:
return true;
default:
message = "Invalid parameter index.";
return false;
}
message = ParamName(i) + " is invalid.";
return false;
}
public bool UpdateEmbeddedDbEntity()
{
return true;
}
public IParamsProvider Clone()
{
return new TestMethodCfg
{
Name = Name,
ParentName = ParentName,
Factory = Factory,
CommandTimeoutMs = CommandTimeoutMs,
MaxAttempts = MaxAttempts,
DelayBetweenAttemptsMs = DelayBetweenAttemptsMs,
ExpectedDeviceType = ExpectedDeviceType,
ExpectedFirmwareVersion = ExpectedFirmwareVersion,
TestParams = TestParams == null ? null : TestParams.Clone() as ITestParams
};
}
}
}

View File

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
using Common;
using Config.Entities;
using TBF.Rig.Generic;
using TBF.Resources;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethodParams : TestParamsBase, IParamsProvider, ITestParams
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(TestMethodParams) })[0];
public double CalibrationFactorPercent;
public override XmlSerializer GetSerializer()
{
return Serializer;
}
public override void InitializeAll()
{
Activity = AllyCalibrationActivityNames.ReadSerialNumber;
CalibrationFactorPercent = 100D;
SimultWithPrevious = false;
SimultWithNext = false;
}
public override int ParamsCount()
{
return 4;
}
public override string ParamName(int i)
{
switch (i)
{
case 0: return Strings.Activity;
case 1: return "Calibration factor [%]";
case 2: return Strings.Simultaneous_with_previous_step;
case 3: return Strings.Simultaneous_with_next_step;
default: return string.Empty;
}
}
public override ICollection<string> ParamValues(int i)
{
if (i == 0)
return AllyCalibrationActivityNames.All;
if (i == 2 || i == 3)
return new[] { Strings.yes, Strings.no };
return null;
}
public override string ToString(int i)
{
switch (i)
{
case 0: return Activity;
case 1: return CalibrationFactorPercent.ToString(System.Globalization.CultureInfo.InvariantCulture);
case 2: return SimultWithPrevious ? Strings.yes : Strings.no;
case 3: return SimultWithNext ? Strings.yes : Strings.no;
default: return string.Empty;
}
}
public new CfgUpdateFlags UpdateParam(int i, string strValue)
{
switch (i)
{
case 0: Activity = strValue; break;
case 1: CalibrationFactorPercent = Utils.ParseUDouble(strValue); break;
case 2: SimultWithPrevious = string.Equals(strValue, Strings.yes, StringComparison.OrdinalIgnoreCase); break;
case 3: SimultWithNext = string.Equals(strValue, Strings.yes, StringComparison.OrdinalIgnoreCase); break;
default: return CfgUpdateFlags.None;
}
return CfgUpdateFlags.None;
}
public new bool ValidateParam(int i, string strValue, out string message)
{
message = string.Empty;
AllyCalibrationActivity activity;
double factor;
switch (i)
{
case 0:
if (AllyCalibrationActivityNames.TryParse(strValue, out activity))
return true;
break;
case 1:
if (double.TryParse(strValue, out factor) && factor > 0D && factor < 1600D)
return true;
break;
case 2:
case 3:
if (string.Equals(strValue, Strings.yes, StringComparison.OrdinalIgnoreCase) ||
string.Equals(strValue, Strings.no, StringComparison.OrdinalIgnoreCase))
{
return true;
}
break;
default:
message = "Invalid parameter index.";
return false;
}
message = ParamName(i) + " is invalid.";
return false;
}
public new IParamsProvider Clone()
{
return new TestMethodParams
{
Activity = Activity,
CalibrationFactorPercent = CalibrationFactorPercent,
SimultWithPrevious = SimultWithPrevious,
SimultWithNext = SimultWithNext
};
}
public override void UpdateFromDbEntity(ComponentTest dbEntity)
{
if (dbEntity == null)
return;
try
{
TestMethodParams source = Serializer.Deserialize(new StringReader(dbEntity.Parameters)) as TestMethodParams;
testParamsEntity = dbEntity;
componentName = dbEntity.CmpntName;
test = dbEntity.Test;
if (source != null)
{
Activity = source.Activity;
CalibrationFactorPercent = source.CalibrationFactorPercent;
SimultWithPrevious = source.SimultWithPrevious;
SimultWithNext = source.SimultWithNext;
}
}
catch
{
}
}
public TestMethodParams()
{
}
public TestMethodParams(bool initialize)
{
if (initialize)
InitializeAll();
}
}
}

View File

@ -1626,6 +1626,20 @@
<Compile Include="Rig\RegisterReaders\GenesisRegReader\GenesisCfgCtrl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Rig\RegisterReaders\AllyReader\AllyMeterReader.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyMeterSize.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalSample.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfg.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyCommandService.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrame.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrameBuilder.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrameParser.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyProtocol.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyResponse.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllySerialTransport.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyVersionInfo.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\IAllyTransport.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Factory.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\GenesisCfgCtrl.designer.cs">
<DependentUpon>GenesisCfgCtrl.cs</DependentUpon>
</Compile>
@ -1950,6 +1964,12 @@
<DependentUpon>TestMethodCfgCtrl.cs</DependentUpon>
</Compile>
<Compile Include="Rig\TestMethods\FlyingStartMassCollection\Single\TestParams.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyCalibrationActivity.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyCalibrationSeq.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\Factory.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethod.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodCfg.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodParams.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\Factory.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\GenesisCommunicationSeq.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\iPerlCommunicationParams.cs" />

View File

@ -0,0 +1,125 @@
using System;
using System.Reflection;
using Common;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
[TestClass]
[TestSubject(typeof(AllyMeterReader))]
public class AllyMeterReaderTest
{
[TestMethod]
public void GetResetCalibrationFactorPercent_AllSupportedSizes_ReturnsUi2093Value()
{
Assert.AreEqual(100D, CreateReader(AllyMeterSize.FiveEighths).GetResetCalibrationFactorPercent());
Assert.AreEqual(100D, CreateReader(AllyMeterSize.ThreeQuarterShort).GetResetCalibrationFactorPercent());
Assert.AreEqual(100D, CreateReader(AllyMeterSize.ThreeQuarterLong).GetResetCalibrationFactorPercent());
Assert.AreEqual(260D, CreateReader(AllyMeterSize.OneInch).GetResetCalibrationFactorPercent());
}
[TestMethod]
public void IsCalibrationFactorWithinSpecification_BoundariesMatchUi2093()
{
AssertLimits(AllyMeterSize.FiveEighths, 76D, 96D);
AssertLimits(AllyMeterSize.ThreeQuarterShort, 88D, 108D);
AssertLimits(AllyMeterSize.ThreeQuarterLong, 89D, 120D);
AssertLimits(AllyMeterSize.OneInch, 236D, 281D);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void GetResetCalibrationFactorPercent_AutoDetect_ThrowsInvalidOperationException()
{
CreateReader(AllyMeterSize.AutoDetect).GetResetCalibrationFactorPercent();
}
[TestMethod]
public void ProcessOpticalText_VolumeAndTimestampRollover_ProducesContinuousMeasurement()
{
AllyMeterReader reader = CreateReader(AllyMeterSize.FiveEighths);
reader.Initialize();
reader.Start();
string input =
AllyOpticalTelegramFactory.Create(10, 0xFFFFF0, 0xFFFFFF00) +
AllyOpticalTelegramFactory.Create(11, 0x000010, 0x00000100);
InvokeProcessOpticalText(reader, input);
reader.Stop();
Assert.AreEqual(2, reader.OpticalSamples.Count);
Assert.IsFalse(reader.NoSamples);
Assert.AreEqual(32D / 16000D, reader.WMVolume, 1E-9);
Assert.AreEqual(512D / 8192D,
reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
Assert.AreEqual(2, reader.WMPulses);
}
[TestMethod]
public void Position_NameWithNumericSuffix_ReturnsSuffix()
{
AllyReaderCfg config = CreateConfig(AllyMeterSize.FiveEighths);
config.Name = "Ally12";
AllyMeterReader reader = new AllyMeterReader(config);
Assert.AreEqual(12, reader.Position);
}
[TestMethod]
public void SetAndReadCalibrationFactor_SimulatedReader_PreservesExpectedValue()
{
AllyMeterReader reader = CreateReader(AllyMeterSize.FiveEighths);
reader.SetCalibrationFactor(90D, 5000);
double readback = reader.ReadCalibrationFactor(5000);
Assert.AreEqual(90D, reader.ExpectedCalibrationFactorPercent);
Assert.AreEqual(90D, readback);
Assert.AreEqual(90D, reader.CalibrationFactorPercent);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void SetCalibrationFactor_OutsideConfiguredSizeLimit_ThrowsArgumentOutOfRangeException()
{
CreateReader(AllyMeterSize.FiveEighths).SetCalibrationFactor(100D, 5000);
}
private static void AssertLimits(AllyMeterSize meterSize, double lower, double upper)
{
AllyMeterReader reader = CreateReader(meterSize);
Assert.IsTrue(reader.IsCalibrationFactorWithinSpecification(lower));
Assert.IsTrue(reader.IsCalibrationFactorWithinSpecification(upper));
Assert.IsFalse(reader.IsCalibrationFactorWithinSpecification(lower - 0.01D));
Assert.IsFalse(reader.IsCalibrationFactorWithinSpecification(upper + 0.01D));
}
private static AllyMeterReader CreateReader(AllyMeterSize meterSize)
{
return new AllyMeterReader(CreateConfig(meterSize));
}
private static AllyReaderCfg CreateConfig(AllyMeterSize meterSize)
{
return new AllyReaderCfg(
new TBF.Rig.RegisterReaders.AllyReader.Factory())
{
ConfiguredMeterSize = meterSize,
DebugLevel = DebugMode.Simulate
};
}
private static void InvokeProcessOpticalText(AllyMeterReader reader, string text)
{
MethodInfo method = typeof(AllyMeterReader).GetMethod(
"ProcessOpticalText",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(method, "ProcessOpticalText method was not found.");
method.Invoke(reader, new object[] { text });
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
[TestClass]
[TestSubject(typeof(AllyOpticalSample))]
public class AllyOpticalSampleTest
{
[TestMethod]
public void TryParse_ValidTelegram_ParsesFlowVolumeAndTimestamp()
{
string telegram = AllyOpticalTelegramFactory.Create(-2, 0x123456, 0x89ABCDEF);
DateTime receivedAt = new DateTime(2026, 8, 14, 10, 0, 0, DateTimeKind.Utc);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(telegram, receivedAt, out sample);
Assert.IsTrue(result);
Assert.IsNotNull(sample);
Assert.AreEqual((short)-2, sample.RawFlow);
Assert.AreEqual(0x123456U, sample.RawVolume);
Assert.AreEqual(0x89ABCDEFU, sample.RawTimestamp);
Assert.AreEqual(receivedAt, sample.ReceivedAtUtc);
Assert.AreEqual(telegram, sample.RawLine);
}
[TestMethod]
public void TryParse_PrefixedValidTelegram_UsesLastCompleteTelegram()
{
string telegram = AllyOpticalTelegramFactory.Create(1, 2, 3);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse("noise" + telegram, DateTime.UtcNow, out sample);
Assert.IsTrue(result);
Assert.AreEqual(2U, sample.RawVolume);
Assert.AreEqual(3U, sample.RawTimestamp);
Assert.AreEqual(telegram, sample.RawLine);
}
[TestMethod]
public void TryParse_CorruptChecksum_ReturnsFalse()
{
string telegram = AllyOpticalTelegramFactory.Create(1, 2, 3);
string corrupt = (telegram[0] == '0' ? "1" : "0") + telegram.Substring(1);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(corrupt, DateTime.UtcNow, out sample);
Assert.IsFalse(result);
Assert.IsNull(sample);
}
[TestMethod]
public void TryParse_InvalidStructure_ReturnsFalse()
{
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(
"000000 0000 0000 000001 0000 00000001 00\r\n",
DateTime.UtcNow,
out sample);
Assert.IsFalse(result);
Assert.IsNull(sample);
}
}
}

View File

@ -0,0 +1,27 @@
using System.Globalization;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
internal static class AllyOpticalTelegramFactory
{
public static string Create(short rawFlow, uint rawVolume, uint rawTimestamp)
{
string prefix = string.Join("\t", new[]
{
"000000",
"0000",
unchecked((ushort)rawFlow).ToString("X4", CultureInfo.InvariantCulture),
rawVolume.ToString("X6", CultureInfo.InvariantCulture),
"0000",
rawTimestamp.ToString("X8", CultureInfo.InvariantCulture),
string.Empty
});
byte checksum = 0;
for (int i = 0; i < prefix.Length; i++)
checksum += (byte)prefix[i];
return prefix + checksum.ToString("X2", CultureInfo.InvariantCulture) + "\r\n";
}
}
}

View File

@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.IO;
using Common;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
[TestClass]
[TestSubject(typeof(AllyReaderCfg))]
public class AllyReaderCfgTest
{
[TestMethod]
public void DefaultConfig_ContainsExpectedCommunicationSettings()
{
AllyReaderCfg config = CreateConfig();
Assert.AreEqual(1, config.CommandComPortNr);
Assert.AreEqual(2400, config.CommandBaudRate);
Assert.AreEqual(2, config.OptoComPortNr);
Assert.AreEqual(9600, config.OptoBaudRate);
Assert.AreEqual(AllyMeterSize.AutoDetect, config.ConfiguredMeterSize);
Assert.AreEqual(1000D, config.MeterPulsesPerLiter);
Assert.AreEqual(6, config.ParamsCount());
}
[TestMethod]
public void MeterSizeParameter_OffersOnlySupportedAllySizes()
{
ICollection<string> values = CreateConfig().ParamValues(4);
Assert.AreEqual(5, values.Count);
CollectionAssert.Contains((System.Collections.ICollection)values, AllyMeterSize.FiveEighths.ToString());
CollectionAssert.Contains((System.Collections.ICollection)values, AllyMeterSize.OneInch.ToString());
}
[TestMethod]
public void ValidateAndUpdateParameters_ValidValues_UpdateConfiguration()
{
AllyReaderCfg config = CreateConfig();
string message;
Assert.IsTrue(config.ValidateParam(0, "7", out message));
Assert.IsTrue(config.ValidateParam(4, AllyMeterSize.OneInch.ToString(), out message));
Assert.IsTrue(config.ValidateParam(5, "2000", out message));
Assert.IsFalse(config.ValidateParam(4, "Unsupported", out message));
Assert.AreEqual(CfgUpdateFlags.RestartRqrd, config.UpdateParam(0, "7"));
config.UpdateParam(4, AllyMeterSize.OneInch.ToString());
config.UpdateParam(5, "2000");
Assert.AreEqual(7, config.CommandComPortNr);
Assert.AreEqual(AllyMeterSize.OneInch, config.ConfiguredMeterSize);
Assert.AreEqual(2000D, config.MeterPulsesPerLiter);
}
[TestMethod]
public void Serializer_RoundTrip_PreservesReaderSettings()
{
AllyReaderCfg source = CreateConfig();
source.CommandComPortNr = 8;
source.ConfiguredMeterSize = AllyMeterSize.ThreeQuarterLong;
source.MeterPulsesPerLiter = 1234D;
string xml;
using (StringWriter writer = new StringWriter())
{
source.GetSerializer().Serialize(writer, source);
xml = writer.ToString();
}
AllyReaderCfg restored;
using (StringReader reader = new StringReader(xml))
restored = (AllyReaderCfg)AllyReaderCfg.Serializer.Deserialize(reader);
Assert.AreEqual(8, restored.CommandComPortNr);
Assert.AreEqual(AllyMeterSize.ThreeQuarterLong, restored.ConfiguredMeterSize);
Assert.AreEqual(1234D, restored.MeterPulsesPerLiter);
}
private static AllyReaderCfg CreateConfig()
{
return new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory());
}
}
}

View File

@ -0,0 +1,179 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
[TestClass]
[TestSubject(typeof(AllyCommandService))]
public class AllyCommandServiceTest
{
private const int TimeoutMs = 1234;
private FakeAllyTransport transport;
private AllyCommandService service;
[TestInitialize]
public void TestInitialize()
{
transport = new FakeAllyTransport
{
IsOpen = true,
Response = AllyResponseFactory.CreateSuccess()
};
service = new AllyCommandService(transport);
}
[TestMethod]
public void ReadSerialNumber_ValidResponse_ReturnsValueAndBuildsSpecifiedFrame()
{
transport.Response = AllyResponseFactory.CreateAsciiSuccess("100104051");
string result = service.ReadSerialNumber(TimeoutMs);
Assert.AreEqual("100104051", result);
AssertRequest(0x53, 0x57, 0x05, 0x01, 0x0D);
}
[TestMethod]
public void ReadVersionAndType_ValidResponse_ParsesAllFields()
{
transport.Response = AllyResponseFactory.CreateAsciiSuccess("TR9,SWM003,FW9");
AllyVersionInfo result = service.ReadVersionAndType(TimeoutMs);
Assert.AreEqual("TR9", result.TouchReadVersion);
Assert.AreEqual("SWM003", result.DeviceType);
Assert.AreEqual("FW9", result.FirmwareVersion);
AssertRequest(0x53, 0x57, 0x05, 0x05, 0x0D);
}
[TestMethod]
public void ReadSystemTimeAndRebootCount_ValidResponses_ParseLittleEndianPayloads()
{
transport.Response = AllyResponseFactory.CreateSuccess(0x80, 0x51, 0x01, 0x00);
DateTime systemTime = service.ReadSystemTimeUtc(TimeoutMs);
Assert.AreEqual(new DateTime(2000, 1, 2, 0, 0, 0, DateTimeKind.Utc), systemTime);
AssertRequest(0x53, 0x57, 0x06, 0xFD, 0x10, 0x0D);
transport.Response = AllyResponseFactory.CreateSuccess(0x00);
byte rebootCount = service.ReadRebootCount(TimeoutMs);
Assert.AreEqual((byte)0, rebootCount);
AssertRequest(0x53, 0x57, 0x06, 0xFD, 0x55, 0x0D);
}
[TestMethod]
public void SetupCommands_BuildExactUi2093Frames()
{
service.OpenValve(TimeoutMs);
AssertRequest(0x53, 0x57, 0x07, 0x1E, 0x00, 0x02, 0x0D);
service.SetSpreadSpectrum(false, TimeoutMs);
AssertRequest(0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x01, 0x0D);
service.SetSpreadSpectrum(true, TimeoutMs);
AssertRequest(0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x00, 0x0D);
service.SetMeterMode(0x09, TimeoutMs);
AssertRequest(0x53, 0x57, 0x06, 0x1A, 0x09, 0x0D);
service.SetDiagnosticLed(0xC2, TimeoutMs);
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x60, 0xC2, 0x0D);
}
[TestMethod]
public void CalibrationFactor_WriteAndRead_UsesFactorTimes40Point96Encoding()
{
service.SetCalibrationFactorPercent(100D, TimeoutMs);
AssertRequest(0x53, 0x57, 0x08, 0xFD, 0x54, 0x00, 0x10, 0x0D);
transport.Response = AllyResponseFactory.CreateSuccess(0x00, 0x10);
double result = service.ReadCalibrationFactorPercent(TimeoutMs);
Assert.AreEqual(100D, result, 1E-9);
AssertRequest(0x53, 0x57, 0x06, 0xFD, 0x53, 0x0D);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void SetCalibrationFactorPercent_Zero_ThrowsArgumentOutOfRangeException()
{
service.SetCalibrationFactorPercent(0D, TimeoutMs);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void SetDisplayVolume_InvalidDigitCount_ThrowsArgumentException()
{
service.SetDisplayVolume("123", TimeoutMs);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void StartOffsetLearning_ZeroSamples_ThrowsArgumentOutOfRangeException()
{
service.StartOffsetLearning(0, 1, TimeoutMs);
}
[TestMethod]
public void MagneticTamperProfiles_BuildExactUi2093Frames()
{
service.SetMagneticTamperProfile(AllyMagneticTamperProfile.Calibration, TimeoutMs);
AssertRequest(0x53, 0x57, 0x0C, 0xFD, 0x15, 0x02, 0x5A, 0x0C, 0x05, 0x3C, 0x50, 0x0D);
service.SetMagneticTamperProfile(AllyMagneticTamperProfile.FirmwareDefaults, TimeoutMs);
AssertRequest(0x53, 0x57, 0x0C, 0xFD, 0x15, 0x05, 0x1E, 0x04, 0x05, 0x3C, 0x50, 0x0D);
}
[TestMethod]
public void FinalizationCommands_BuildExactUi2093Frames()
{
service.SetDisplayVolume("01134010", TimeoutMs);
AssertRequest(
0x53, 0x57, 0x0E, 0x14,
0x30, 0x31, 0x31, 0x33, 0x34, 0x30, 0x31, 0x30,
0x00, 0x0D);
service.SetLcdTimeout(30, TimeoutMs);
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x8C, 0x1E, 0x0D);
service.StartOffsetLearning(1000, 1, TimeoutMs);
AssertRequest(0x53, 0x57, 0x0A, 0xFD, 0xD1, 0xE8, 0x03, 0x01, 0x00, 0x0D);
}
[TestMethod]
public void Send_ClosedTransport_OpensTransportAndPassesTimeout()
{
transport.IsOpen = false;
service.OpenValve(TimeoutMs);
Assert.AreEqual(1, transport.OpenCallCount);
Assert.AreEqual(1, transport.SendAndWaitCallCount);
Assert.AreEqual(TimeoutMs, transport.LastTimeoutMs);
}
[TestMethod]
public void Send_FailureStatus_ThrowsCommunicationExceptionWithStatus()
{
transport.Response = AllyResponseFactory.CreateFailure(0xFD);
try
{
service.OpenValve(TimeoutMs);
Assert.Fail("Expected AllyCommunicationException.");
}
catch (AllyCommunicationException exception)
{
Assert.AreEqual((byte?)0xFD, exception.Status);
StringAssert.Contains(exception.Message, "0xFD");
}
}
private void AssertRequest(params byte[] expected)
{
CollectionAssert.AreEqual(expected, transport.LastRequest);
Assert.AreEqual(TimeoutMs, transport.LastTimeoutMs);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
[TestClass]
[TestSubject(typeof(AllyFrameParser))]
public class AllyFrameParserTest
{
[TestMethod]
public void ParseResponse_ValidFrame_ReturnsStatusAndPayload()
{
AllyFrameParser parser = new AllyFrameParser();
AllyResponse response = parser.ParseResponse(
new byte[] { 0x53, 0x52, 0x07, 0x01, 0x34, 0x12, 0x0D });
Assert.IsTrue(response.IsSuccess);
Assert.AreEqual((byte)0x01, response.Status);
Assert.AreEqual((ushort)0x1234, response.GetUInt16LittleEndian());
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ParseResponse_Null_ThrowsArgumentNullException()
{
new AllyFrameParser().ParseResponse(null);
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ParseResponse_InvalidLength_ThrowsFormatException()
{
new AllyFrameParser().ParseResponse(
new byte[] { 0x53, 0x52, 0x06, 0x01, 0x0D });
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ParseResponse_InvalidDirection_ThrowsFormatException()
{
new AllyFrameParser().ParseResponse(
new byte[] { 0x53, 0x57, 0x05, 0x01, 0x0D });
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void ParseResponse_InvalidEndByte_ThrowsFormatException()
{
new AllyFrameParser().ParseResponse(
new byte[] { 0x53, 0x52, 0x05, 0x01, 0x00 });
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
[TestClass]
[TestSubject(typeof(AllyFrame))]
public class AllyFrameTest
{
[TestMethod]
public void ToArray_InformationBytes_CalculatesLengthAndAddsEnvelope()
{
AllyFrame frame = new AllyFrame(0x57, new byte[] { 0xFD, 0x54, 0x00, 0x10 });
byte[] result = frame.ToArray();
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x08, 0xFD, 0x54, 0x00, 0x10, 0x0D },
result);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ToArray_InformationExceedsProtocolLength_ThrowsInvalidOperationException()
{
new AllyFrame(0x57, new byte[252]).ToArray();
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void FrameBuilder_PayloadBeforeCommand_ThrowsInvalidOperationException()
{
new AllyFrameBuilder().WithByte(0x01);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
internal static class AllyResponseFactory
{
public static byte[] CreateSuccess(params byte[] payload)
{
return Create(0x01, payload);
}
public static byte[] CreateFailure(byte status, params byte[] payload)
{
return Create(status, payload);
}
public static byte[] CreateAsciiSuccess(string value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
List<byte> payload = new List<byte>(System.Text.Encoding.ASCII.GetBytes(value));
payload.Add(0x00);
return CreateSuccess(payload.ToArray());
}
private static byte[] Create(byte status, params byte[] payload)
{
payload = payload ?? Array.Empty<byte>();
List<byte> response = new List<byte>
{
0x53,
0x52,
0x00,
status
};
response.AddRange(payload);
response.Add(0x0D);
response[2] = checked((byte)response.Count);
return response.ToArray();
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
[TestClass]
[TestSubject(typeof(AllySerialTransportBuilder))]
public class AllySerialTransportBuilderTest
{
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Build_MissingPort_ThrowsInvalidOperationException()
{
new AllySerialTransportBuilder().Build();
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Build_InvalidBaudRate_ThrowsInvalidOperationException()
{
new AllySerialTransportBuilder()
.WithPort("COM1")
.WithBaudRate(0)
.Build();
}
[TestMethod]
public void Build_ValidSettings_CreatesClosedTransportWithoutOpeningHardware()
{
AllySerialTransport transport = new AllySerialTransportBuilder()
.WithPort("COM1")
.WithBaudRate(2400)
.Build();
Assert.IsFalse(transport.IsOpen);
transport.Dispose();
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
[TestClass]
[TestSubject(typeof(AllyVersionInfo))]
public class AllyVersionInfoTest
{
[TestMethod]
public void Parse_ThreeFields_TrimsAndMapsValues()
{
AllyVersionInfo result = AllyVersionInfo.Parse(" TR9 , SWM003 , FW9 ");
Assert.AreEqual("TR9", result.TouchReadVersion);
Assert.AreEqual("SWM003", result.DeviceType);
Assert.AreEqual("FW9", result.FirmwareVersion);
Assert.AreEqual("TR9 , SWM003 , FW9", result.RawValue);
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void Parse_MissingField_ThrowsFormatException()
{
AllyVersionInfo.Parse("TR9,SWM003");
}
[TestMethod]
[ExpectedException(typeof(FormatException))]
public void Parse_EmptyField_ThrowsFormatException()
{
AllyVersionInfo.Parse("TR9,,FW9");
}
}
}

View File

@ -0,0 +1,35 @@
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
internal sealed class FakeAllyTransport : IAllyTransport
{
public bool IsOpen { get; set; }
public int OpenCallCount { get; private set; }
public int SendAndWaitCallCount { get; private set; }
public int DisposeCallCount { get; private set; }
public byte[] Response { get; set; }
public byte[] LastRequest { get; private set; }
public int LastTimeoutMs { get; private set; }
public void Open()
{
OpenCallCount++;
IsOpen = true;
}
public byte[] SendAndWait(byte[] request, int timeoutMs)
{
SendAndWaitCallCount++;
LastRequest = request;
LastTimeoutMs = timeoutMs;
return Response;
}
public void Dispose()
{
DisposeCallCount++;
IsOpen = false;
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using Common;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
{
internal sealed class AllyHardwareIntegrationSettings
{
public bool Enabled { get; private set; }
public int CommandPortNumber { get; private set; }
public int OpticalPortNumber { get; private set; }
public int CommandBaudRate { get; private set; }
public int OpticalBaudRate { get; private set; }
public int CommandTimeoutMs { get; private set; }
public int OpticalCaptureSeconds { get; private set; }
public int ActiveModeSettleMs { get; private set; }
public AllyMeterSize MeterSize { get; private set; }
public string CommandPortName { get { return "COM" + CommandPortNumber; } }
public string OpticalPortName { get { return "COM" + OpticalPortNumber; } }
public static AllyHardwareIntegrationSettings FromEnvironment()
{
return new AllyHardwareIntegrationSettings
{
Enabled = ParseEnabled(Environment.GetEnvironmentVariable("ALLY_HW_TESTS")),
CommandPortNumber = ParsePort("ALLY_COMMAND_PORT", "COM3"),
OpticalPortNumber = ParsePort("ALLY_OPTICAL_PORT", "COM4"),
CommandBaudRate = ParsePositiveInt("ALLY_COMMAND_BAUD", 2400),
OpticalBaudRate = ParsePositiveInt("ALLY_OPTICAL_BAUD", 9600),
CommandTimeoutMs = ParsePositiveInt("ALLY_COMMAND_TIMEOUT_MS", 5000),
OpticalCaptureSeconds = ParsePositiveInt("ALLY_OPTICAL_CAPTURE_SECONDS", 10),
ActiveModeSettleMs = ParsePositiveInt("ALLY_ACTIVE_SETTLE_MS", 1000),
MeterSize = ParseMeterSize(Environment.GetEnvironmentVariable("ALLY_METER_SIZE"))
};
}
public AllyReaderCfg CreateReaderConfig()
{
return new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory())
{
CommandComPortNr = CommandPortNumber,
CommandBaudRate = CommandBaudRate,
OptoComPortNr = OpticalPortNumber,
OptoBaudRate = OpticalBaudRate,
ConfiguredMeterSize = MeterSize,
MeterPulsesPerLiter = 1000D,
DebugLevel = DebugMode.Normal
};
}
private static bool ParseEnabled(string value)
{
return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase);
}
private static int ParsePort(string variableName, string defaultValue)
{
string value = Environment.GetEnvironmentVariable(variableName) ?? defaultValue;
if (value.StartsWith("COM", StringComparison.OrdinalIgnoreCase))
value = value.Substring(3);
int portNumber;
if (!int.TryParse(value, out portNumber) || portNumber <= 0)
throw new InvalidOperationException(variableName + " must contain COM<n> or a positive port number.");
return portNumber;
}
private static int ParsePositiveInt(string variableName, int defaultValue)
{
string value = Environment.GetEnvironmentVariable(variableName);
if (string.IsNullOrWhiteSpace(value))
return defaultValue;
int parsed;
if (!int.TryParse(value, out parsed) || parsed <= 0)
throw new InvalidOperationException(variableName + " must be a positive integer.");
return parsed;
}
private static AllyMeterSize ParseMeterSize(string value)
{
if (string.IsNullOrWhiteSpace(value))
return AllyMeterSize.FiveEighths;
AllyMeterSize result;
if (!Enum.TryParse(value, true, out result) || result == AllyMeterSize.AutoDetect)
{
throw new InvalidOperationException(
"ALLY_METER_SIZE must be a supported explicit size; AutoDetect requires UI-2031 parsing.");
}
return result;
}
}
}

View File

@ -0,0 +1,331 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
{
[TestClass]
[TestCategory("HardwareIntegration")]
[DoNotParallelize]
public class AllyHardwareIntegrationTest
{
private const byte ActiveMeterMode = 0x02;
private const byte InitialMeterMode = 0x09;
private AllyHardwareIntegrationSettings settings;
private AllyIntegrationTestLogger logger;
public TestContext TestContext { get; set; }
[TestInitialize]
public void InitializeTest()
{
settings = AllyHardwareIntegrationSettings.FromEnvironment();
logger = new AllyIntegrationTestLogger(TestContext);
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"Configuration | command={0}/{1}, optical={2}/{3}, size={4}, timeout={5} ms, capture={6} s",
settings.CommandPortName,
settings.CommandBaudRate,
settings.OpticalPortName,
settings.OpticalBaudRate,
settings.MeterSize,
settings.CommandTimeoutMs,
settings.OpticalCaptureSeconds));
if (!settings.Enabled)
{
logger.Log("SKIP | Hardware tests are disabled. Set ALLY_HW_TESTS=1 to enable them.");
Assert.Inconclusive("Set ALLY_HW_TESTS=1 to run ALLY hardware integration tests.");
}
}
[TestCleanup]
public void CleanupTest()
{
if (logger != null)
logger.Dispose();
}
[TestMethod]
public void CommandPort_ReadIdentityAndHealth_LogsParsedResponses()
{
RequirePorts(settings.CommandPortName);
AllyMeterReader reader = CreateReader();
reader.StartSession();
try
{
string serialNumber = logger.Step(
"Read manufacturing serial number",
() => reader.ReadSerialNumber(settings.CommandTimeoutMs));
Assert.IsFalse(string.IsNullOrWhiteSpace(serialNumber), "The manufacturing serial number is empty.");
AllyVersionInfo version = logger.Step(
"Read and parse version/type",
() => reader.ReadVersionAndType(settings.CommandTimeoutMs),
value => string.Format(
CultureInfo.InvariantCulture,
"raw={0}; TouchRead={1}; type={2}; firmware={3}; parse=PASS",
value.RawValue,
value.TouchReadVersion,
value.DeviceType,
value.FirmwareVersion));
Assert.AreEqual("SWM003", version.DeviceType, "The connected device is not an ALLY meter.");
logger.Step("Read meter system time (UTC)", () => reader.ReadSystemTimeUtc(settings.CommandTimeoutMs),
value => value.ToString("O", CultureInfo.InvariantCulture) + "; parse=PASS");
logger.Step("Read reboot count", () => reader.ReadRebootCount(settings.CommandTimeoutMs),
value => value.ToString(CultureInfo.InvariantCulture) + "; parse=PASS");
}
finally
{
reader.EndSession();
logger.Log("SESSION | command port closed");
}
}
[TestMethod]
public void CommandPort_ActiveModeRoundTrip_AlwaysRestoresInitialMode()
{
RequirePorts(settings.CommandPortName);
AllyMeterReader reader = CreateReader();
reader.StartSession();
try
{
logger.Step(
"Set active meter mode 0x02",
() => reader.SetMeterMode(ActiveMeterMode, settings.CommandTimeoutMs));
logger.Log("WAIT | active-mode settling for " + settings.ActiveModeSettleMs + " ms");
Thread.Sleep(settings.ActiveModeSettleMs);
}
finally
{
try
{
logger.Step(
"Restore initial meter mode 0x09",
() => reader.SetMeterMode(InitialMeterMode, settings.CommandTimeoutMs));
}
finally
{
reader.EndSession();
logger.Log("SESSION | command port closed");
}
}
}
[TestMethod]
public void OpticalPort_CaptureAndParseTelegrams_LogsRawAndParseResult()
{
RequirePorts(settings.OpticalPortName);
IList<AllyOpticalSample> samples = CaptureOpticalTelegramsDirectly();
Assert.IsTrue(samples.Count > 0, "No valid ALLY optical telegram was parsed.");
}
[TestMethod]
public void Complex_ReadSerialActivateCaptureParseAndDeactivate_LogsWholeWorkflow()
{
RequirePorts(settings.CommandPortName, settings.OpticalPortName);
AllyMeterReader reader = CreateReader();
bool streamStarted = false;
reader.StartSession();
try
{
string serialNumber = logger.Step(
"Read manufacturing serial number",
() => reader.ReadSerialNumber(settings.CommandTimeoutMs));
Assert.IsFalse(string.IsNullOrWhiteSpace(serialNumber), "The manufacturing serial number is empty.");
logger.Step(
"Set active meter mode 0x02",
() => reader.SetMeterMode(ActiveMeterMode, settings.CommandTimeoutMs));
logger.Log("WAIT | active-mode settling for " + settings.ActiveModeSettleMs + " ms");
Thread.Sleep(settings.ActiveModeSettleMs);
logger.Step("Open optical stream and start measurement", reader.Start);
streamStarted = true;
CaptureThroughReader(reader);
IReadOnlyList<AllyOpticalSample> samples = reader.OpticalSamples;
Assert.IsTrue(samples.Count >= 2, "At least two valid optical samples are required for a measurement.");
Assert.IsFalse(reader.NoSamples, "The reader did not establish a valid optical measurement interval.");
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"MEASUREMENT | PASS | serial={0}, samples={1}, volume={2:R} l, elapsed={3:R} s",
serialNumber,
samples.Count,
reader.WMVolume,
reader.TimestampSecEnd - reader.TimestampSecStart));
}
finally
{
try
{
if (streamStarted)
logger.Step("Stop optical measurement and close COM4", reader.Stop);
}
finally
{
try
{
logger.Step(
"Restore initial meter mode 0x09",
() => reader.SetMeterMode(InitialMeterMode, settings.CommandTimeoutMs));
}
finally
{
reader.EndSession();
logger.Log("SESSION | all ports closed");
}
}
}
}
private AllyMeterReader CreateReader()
{
AllyMeterReader reader = new AllyMeterReader(settings.CreateReaderConfig());
logger.Step("Initialize ALLY reader", reader.Initialize);
return reader;
}
private IList<AllyOpticalSample> CaptureOpticalTelegramsDirectly()
{
List<AllyOpticalSample> samples = new List<AllyOpticalSample>();
StringBuilder buffer = new StringBuilder();
int parsedLines = 0;
int rejectedLines = 0;
using (SerialPort port = new SerialPort(
settings.OpticalPortName,
settings.OpticalBaudRate,
Parity.None,
8,
StopBits.One))
{
logger.Step("Open optical source " + settings.OpticalPortName, port.Open);
try
{
port.DiscardInBuffer();
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < TimeSpan.FromSeconds(settings.OpticalCaptureSeconds))
{
string text = port.ReadExisting();
if (!string.IsNullOrEmpty(text))
buffer.Append(text);
string line;
while (TryTakeLine(buffer, out line))
{
AllyOpticalSample sample;
bool parsed = AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample);
logger.OpticalParse(line, parsed, sample);
if (parsed)
{
parsedLines++;
samples.Add(sample);
}
else
{
rejectedLines++;
}
}
Thread.Sleep(50);
}
}
finally
{
port.Close();
logger.Log("PORT | optical source closed");
}
}
if (buffer.Length > 0)
{
logger.Log("OPTO PARTIAL | trailing incomplete data: " + buffer.ToString()
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"));
}
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"OPTO SUMMARY | valid={0}, rejected={1}, partialChars={2}",
parsedLines,
rejectedLines,
buffer.Length));
return samples;
}
private void CaptureThroughReader(AllyMeterReader reader)
{
int loggedSampleCount = 0;
string lastRawLine = null;
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < TimeSpan.FromSeconds(settings.OpticalCaptureSeconds))
{
reader.RunDeviceBefore();
string rawLine = reader.ReadOptoData();
if (!string.IsNullOrEmpty(rawLine) && rawLine != lastRawLine)
{
AllyOpticalSample parsedSample;
bool parsed = AllyOpticalSample.TryParse(rawLine, DateTime.UtcNow, out parsedSample);
logger.OpticalParse(rawLine, parsed, parsedSample);
lastRawLine = rawLine;
}
IReadOnlyList<AllyOpticalSample> currentSamples = reader.OpticalSamples;
while (loggedSampleCount < currentSamples.Count)
{
AllyOpticalSample sample = currentSamples[loggedSampleCount++];
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"OPTO SAMPLE | index={0}, flow={1}, rawVolume={2}, rawTimestamp={3}, liters={4:R}, seconds={5:R}",
loggedSampleCount,
sample.RawFlow,
sample.RawVolume,
sample.RawTimestamp,
sample.ExtendedVolumeLiters,
sample.ElapsedSeconds));
}
Thread.Sleep(50);
}
logger.Log("OPTO CAPTURE | completed; parsed samples=" + loggedSampleCount);
}
private void RequirePorts(params string[] requiredPorts)
{
string[] available = SerialPort.GetPortNames();
logger.Log("PORTS | available=" + (available.Length == 0 ? "<none>" : string.Join(", ", available)));
string[] missing = requiredPorts
.Where(required => !available.Contains(required, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (missing.Length == 0)
return;
string message = "Required ALLY port(s) are not connected: " + string.Join(", ", missing) + ".";
logger.Log("SKIP | " + message);
Assert.Inconclusive(message);
}
private static bool TryTakeLine(StringBuilder buffer, out string line)
{
string text = buffer.ToString();
int lineEnd = text.IndexOf('\n');
if (lineEnd < 0)
{
line = null;
return false;
}
line = text.Substring(0, lineEnd + 1);
buffer.Remove(0, lineEnd + 1);
return true;
}
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.Globalization;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
{
internal sealed class AllyIntegrationTestLogger : IDisposable
{
private readonly TestContext context;
private readonly StreamWriter writer;
public AllyIntegrationTestLogger(TestContext context)
{
this.context = context;
string directory = string.IsNullOrWhiteSpace(context.ResultsDirectory) ? Path.Combine(Path.GetTempPath(), "TBFTests", "AllyIntegration") : context.ResultsDirectory;
Directory.CreateDirectory(directory);
string testName = SanitizeFileName(context.TestName ?? "AllyIntegration");
string path = Path.Combine( directory, testName + "_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture) + ".log");
writer = new StreamWriter(path, false) { AutoFlush = true };
Log("Log file: " + path);
}
public void Log(string message)
{
string line = string.Format( CultureInfo.InvariantCulture, "{0:O} | {1}", DateTime.Now, message);
context.WriteLine(line);
Console.WriteLine(line);
writer.WriteLine(line);
}
public T Step<T>(string name, Func<T> action, Func<T, string> formatResult = null)
{
Log("STEP START | " + name);
try
{
T result = action();
string response = formatResult == null
? Convert.ToString(result, CultureInfo.InvariantCulture)
: formatResult(result);
Log("STEP PASS | " + name + " | meter response: " + Escape(response));
return result;
}
catch (Exception ex)
{
Log("STEP FAIL | " + name + " | " + ex.GetType().Name + ": " + ex.Message);
throw;
}
}
public void Step(string name, Action action)
{
Step( name, () => { action(); return "ACK"; });
}
public void OpticalParse(string rawLine, bool parsed, AllyOpticalSample sample)
{
Log("OPTO RAW | " + Escape(rawLine));
if (!parsed || sample == null)
{
Log("OPTO PARSE | FAIL");
return;
}
Log(string.Format( CultureInfo.InvariantCulture, "OPTO PARSE | PASS | flow={0}, volume=0x{1:X6} ({1}), timestamp=0x{2:X8} ({2})",
sample.RawFlow,
sample.RawVolume,
sample.RawTimestamp));
}
public void Dispose()
{
writer.Dispose();
}
private static string Escape(string value)
{
if (value == null)
return "<null>";
return value.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
}
private static string SanitizeFileName(string value)
{
foreach (char invalid in Path.GetInvalidFileNameChars())
value = value.Replace(invalid, '_');
return value;
}
}
}

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.TestMethods.AllyCalibration;
namespace TBFTests.Rig.TestMethods.AllyCalibration
{
[TestClass]
[TestSubject(typeof(TestMethodCfg))]
public class TestMethodConfigTest
{
[TestMethod]
public void DefaultConfig_UsesUi2093CommunicationDefaults()
{
TestMethodCfg config = new TestMethodCfg(
new TBF.Rig.TestMethods.AllyCalibration.Factory());
Assert.AreEqual(5000, config.CommandTimeoutMs);
Assert.AreEqual(4, config.MaxAttempts);
Assert.AreEqual(250, config.DelayBetweenAttemptsMs);
Assert.AreEqual("SWM003", config.ExpectedDeviceType);
Assert.AreEqual(string.Empty, config.ExpectedFirmwareVersion);
}
[TestMethod]
public void TestMethodParams_ExposeOnlyAllyActivitiesAndValidateFactor()
{
TestMethodParams parameters = new TestMethodParams(true);
ICollection<string> activities = parameters.ParamValues(0);
Assert.AreEqual(19, activities.Count);
CollectionAssert.Contains((System.Collections.ICollection)activities, "Read serial number");
CollectionAssert.Contains((System.Collections.ICollection)activities, "Start offset learning");
CollectionAssert.DoesNotContain((System.Collections.ICollection)activities, "Q2 correction");
string message;
Assert.IsTrue(parameters.ValidateParam(1, "100", out message));
Assert.IsFalse(parameters.ValidateParam(1, "0", out message));
Assert.IsFalse(parameters.ValidateParam(1, "1600", out message));
}
}
}

View File

@ -136,6 +136,22 @@
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOpTest.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\GenesisHead\GenesisHeadBatchIntegrationTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyMeterReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalSampleTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalTelegramFactory.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyCommandServiceTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyFrameTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyFrameParserTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyResponseFactory.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllySerialTransportBuilderTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyVersionInfoTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\FakeAllyTransport.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyIntegrationTests.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\integration\AllyHardwareIntegrationSettings.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\integration\AllyIntegrationTestLogger.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\integration\AllyHardwareIntegrationTest.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodConfigTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\common\OptoTelegramRawTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\FakeSerialDriver.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\IperlResponseFactory.cs" />