Ally meter integration to test method and unit tests / integration tests supports Ally communication improving
feat(ally): add detailed logging, calibration flow updates, and test communication sequence implementation
This commit is contained in:
parent
0258e54b4e
commit
1c8d01562f
@ -26,10 +26,8 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
private const int DataEntryCommandTimeoutMs = 5000;
|
||||
private const int DefaultDataEntryOpticalTimeoutMs = 3000;
|
||||
private const int MaxStoredSamples = 40000;
|
||||
private const long RawVolumeModulo = 0x1000000L;
|
||||
private const long RawVolumeModulo = 0x100000000L;
|
||||
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();
|
||||
@ -42,14 +40,13 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
private AllyCommandService commandService;
|
||||
private SerialPort opticalPort;
|
||||
private bool streamEnabled;
|
||||
private bool opticalVerificationOutputActive;
|
||||
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 DateTime? firstSampleReceivedAtUtc;
|
||||
private double beginWMState;
|
||||
private double endWMState;
|
||||
private double timestampSecStart;
|
||||
@ -94,6 +91,15 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
get { return allyCfg == null ? AllyMeterSize.AutoDetect : allyCfg.ConfiguredMeterSize; }
|
||||
}
|
||||
|
||||
public bool IsOpticalVolumeConversionConfigured
|
||||
{
|
||||
// The ALLY C6 accumulator is always expressed in quarter millilitres.
|
||||
// Unlike calibration-factor limits, decoding the optical accumulator does
|
||||
// not depend on the nominal meter size. Keeping this true also allows a
|
||||
// bench configured with AutoDetect to persist start/end states.
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public IReadOnlyList<AllyOpticalSample> OpticalSamples
|
||||
{
|
||||
get
|
||||
@ -132,12 +138,25 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
log.DebugFormat(
|
||||
"ALLY_OPTO RX COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
|
||||
allyCfg.OptoComPortNr,
|
||||
text.Length,
|
||||
ToLogText(text),
|
||||
ToHex(text));
|
||||
ProcessOpticalText(text);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CommFailed = true;
|
||||
log.Error("ALLY optical stream read failed.", ex);
|
||||
log.ErrorFormat(
|
||||
"ALLY_OPTO read failed on COM{0} (open={1}, streaming={2}). {3}",
|
||||
allyCfg == null ? 0 : allyCfg.OptoComPortNr,
|
||||
opticalPort != null && opticalPort.IsOpen,
|
||||
streamEnabled,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,30 +244,66 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
|
||||
public void StartDataStreamProcessing()
|
||||
{
|
||||
GetOpticalVolumeLitersPerRawUnit();
|
||||
StartDataStreamProcessing(true);
|
||||
}
|
||||
|
||||
lock (opticalSync)
|
||||
private void StartDataStreamProcessing(bool requireVolumeConversion)
|
||||
{
|
||||
log.InfoFormat(
|
||||
"ALLY_OPTO start requested: COM{0}, {1} Bd, 8N1, meter size={2}, debug={3}",
|
||||
allyCfg == null ? 0 : allyCfg.OptoComPortNr,
|
||||
allyCfg == null ? 0 : allyCfg.OptoBaudRate,
|
||||
ConfiguredMeterSize,
|
||||
DebugLevel);
|
||||
|
||||
try
|
||||
{
|
||||
if (streamEnabled)
|
||||
return;
|
||||
if (requireVolumeConversion)
|
||||
GetOpticalVolumeLitersPerRawUnit();
|
||||
|
||||
opticalSamples.Clear();
|
||||
opticalBuffer.Clear();
|
||||
lastOpticalLine = string.Empty;
|
||||
ResetVolumeState();
|
||||
if (DebugLevel == DebugMode.Normal)
|
||||
lock (opticalSync)
|
||||
{
|
||||
opticalPort = new SerialPort(
|
||||
"COM" + allyCfg.OptoComPortNr,
|
||||
allyCfg.OptoBaudRate,
|
||||
Parity.None,
|
||||
8,
|
||||
StopBits.One);
|
||||
opticalPort.Open();
|
||||
opticalPort.DiscardInBuffer();
|
||||
}
|
||||
if (streamEnabled)
|
||||
{
|
||||
log.Debug("ALLY_OPTO start ignored: stream is already active.");
|
||||
return;
|
||||
}
|
||||
|
||||
streamEnabled = true;
|
||||
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();
|
||||
log.InfoFormat(
|
||||
"ALLY_OPTO opened {0}: baud={1}, dataBits={2}, parity={3}, stopBits={4}",
|
||||
opticalPort.PortName,
|
||||
opticalPort.BaudRate,
|
||||
opticalPort.DataBits,
|
||||
opticalPort.Parity,
|
||||
opticalPort.StopBits);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Info("ALLY_OPTO simulation mode: physical optical COM port was not opened.");
|
||||
}
|
||||
|
||||
streamEnabled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CommFailed = true;
|
||||
log.Error("ALLY_OPTO start failed.", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,12 +313,18 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
{
|
||||
streamEnabled = false;
|
||||
if (opticalPort == null)
|
||||
{
|
||||
log.Debug("ALLY_OPTO stopped: no optical COM port was open.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (opticalPort.IsOpen)
|
||||
{
|
||||
log.InfoFormat("ALLY_OPTO closing {0}.", opticalPort.PortName);
|
||||
opticalPort.Close();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -363,6 +424,46 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
ExecuteCommand(service => service.SetDiagnosticLed(mode, timeoutMs));
|
||||
}
|
||||
|
||||
public bool IsFactorySealed(int timeoutMs)
|
||||
{
|
||||
if (DebugLevel != DebugMode.Normal)
|
||||
return false;
|
||||
|
||||
return ExecuteCommand(service => service.IsFactorySealed(timeoutMs));
|
||||
}
|
||||
|
||||
public void UnsealFactory(int timeoutMs)
|
||||
{
|
||||
ExecuteCommand(service => service.UnsealFactory(timeoutMs));
|
||||
}
|
||||
|
||||
public AllyFactoryUnsealData ReadFactoryUnsealData(int timeoutMs)
|
||||
{
|
||||
if (DebugLevel != DebugMode.Normal)
|
||||
{
|
||||
// Keep the complete test-method workflow executable without a
|
||||
// physical meter. The simulated register is already unsealed.
|
||||
return new AllyFactoryUnsealData(
|
||||
false,
|
||||
"SIMULATED-ALLY",
|
||||
"SIMULATED",
|
||||
"00000000",
|
||||
0U);
|
||||
}
|
||||
|
||||
return ExecuteCommand(service => service.ReadFactoryUnsealData(timeoutMs));
|
||||
}
|
||||
|
||||
public void UnsealFactory(AllyFactoryUnsealData data, int timeoutMs)
|
||||
{
|
||||
ExecuteCommand(service => service.UnsealFactory(data, timeoutMs));
|
||||
}
|
||||
|
||||
public void SealFactory(int timeoutMs)
|
||||
{
|
||||
ExecuteCommand(service => service.SealFactory(timeoutMs));
|
||||
}
|
||||
|
||||
public double ResetCalibrationFactor(int timeoutMs)
|
||||
{
|
||||
double factor = GetResetCalibrationFactorPercent();
|
||||
@ -615,6 +716,18 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
}
|
||||
|
||||
private void ProcessOpticalText(string text)
|
||||
{
|
||||
ProcessOpticalText(text, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
// Kept internal for deterministic MSTest coverage of the host-receipt time
|
||||
// used by ALLY C6 telegrams. C6 has no device timestamp to roll over.
|
||||
internal void ProcessOpticalTextForTest(string text, DateTime receivedAtUtc)
|
||||
{
|
||||
ProcessOpticalText(text, receivedAtUtc);
|
||||
}
|
||||
|
||||
private void ProcessOpticalText(string text, DateTime receivedAtUtc)
|
||||
{
|
||||
lock (opticalSync)
|
||||
{
|
||||
@ -632,32 +745,174 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
lastOpticalLine = line;
|
||||
|
||||
AllyOpticalSample sample;
|
||||
if (!AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample))
|
||||
if (!AllyOpticalSample.TryParse(line, receivedAtUtc, out sample))
|
||||
{
|
||||
if (AllyOpticalSample.IsMetrologyPacket(line))
|
||||
{
|
||||
log.WarnFormat(
|
||||
"ALLY_OPTO rejected C6 metrology telegram on COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
|
||||
allyCfg.OptoComPortNr,
|
||||
line.Length,
|
||||
ToLogText(line),
|
||||
ToHex(line));
|
||||
}
|
||||
else
|
||||
{
|
||||
log.DebugFormat(
|
||||
"ALLY_OPTO ignored non-metrology telegram on COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
|
||||
allyCfg.OptoComPortNr,
|
||||
line.Length,
|
||||
ToLogText(line),
|
||||
ToHex(line));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ExtendRawVolume(sample.RawVolume);
|
||||
ExtendRawTimestamp(sample.RawTimestamp);
|
||||
sample.ExtendedVolumeLiters =
|
||||
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
|
||||
sample.ElapsedSeconds = extendedRawTimestamp / 8192D;
|
||||
if (!firstSampleReceivedAtUtc.HasValue)
|
||||
firstSampleReceivedAtUtc = sample.ReceivedAtUtc;
|
||||
sample.ElapsedSeconds = (sample.ReceivedAtUtc - firstSampleReceivedAtUtc.Value).TotalSeconds;
|
||||
|
||||
if (opticalSamples.Count == MaxStoredSamples)
|
||||
opticalSamples.RemoveAt(0);
|
||||
opticalSamples.Add(sample);
|
||||
|
||||
endWMState = sample.ExtendedVolumeLiters;
|
||||
if (IsOpticalVolumeConversionConfigured)
|
||||
{
|
||||
sample.ExtendedVolumeLiters =
|
||||
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
|
||||
endWMState = sample.ExtendedVolumeLiters;
|
||||
}
|
||||
else
|
||||
{
|
||||
sample.ExtendedVolumeLiters = Double.NaN;
|
||||
}
|
||||
timestampSecEnd = sample.ElapsedSeconds;
|
||||
|
||||
if (operationActive && !hasTestStartSample)
|
||||
if (operationActive && !hasTestStartSample && IsOpticalVolumeConversionConfigured)
|
||||
{
|
||||
hasTestStartSample = true;
|
||||
beginWMState = sample.ExtendedVolumeLiters;
|
||||
timestampSecStart = sample.ElapsedSeconds;
|
||||
}
|
||||
|
||||
log.DebugFormat(
|
||||
"ALLY_OPTO parsed COM{0}: sequence=0x{1:X2}, rawVolume=0x{2:X8}, flow={3}, volume={4:F6} l, elapsed={5:F3} s, emptyPipe={6}, fastHptc={7}",
|
||||
allyCfg.OptoComPortNr,
|
||||
sample.Sequence,
|
||||
sample.RawVolume,
|
||||
sample.RawFlow,
|
||||
sample.ExtendedVolumeLiters,
|
||||
sample.ElapsedSeconds,
|
||||
sample.IsEmptyPipe,
|
||||
sample.IsFastHptc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the meter optical output and then starts COM optical capture.
|
||||
/// C6 volume is decoded in quarter millilitres and is independent of the
|
||||
/// configured nominal meter size.
|
||||
/// </summary>
|
||||
public void StartOpticalVerificationStream(int timeoutMs)
|
||||
{
|
||||
if (IsFactorySealed(timeoutMs))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"ALLY meter is factory sealed. Unseal the meter before starting the optical stream.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ExecuteCommand(service => service.StartOpticalVerificationOutput(timeoutMs));
|
||||
opticalVerificationOutputActive = true;
|
||||
StartDataStreamProcessing(IsOpticalVolumeConversionConfigured);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (opticalVerificationOutputActive)
|
||||
{
|
||||
try
|
||||
{
|
||||
StopOpticalVerificationStream(timeoutMs);
|
||||
}
|
||||
catch (Exception cleanupException)
|
||||
{
|
||||
log.Error("ALLY optical start cleanup failed.", cleanupException);
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production-bench setup operation. It reads the meter-specific factory
|
||||
/// data, opens the factory seal only when needed, and then enables the
|
||||
/// complete optical verification stream.
|
||||
/// </summary>
|
||||
public void UnsealAndStartOpticalVerificationStream(int timeoutMs)
|
||||
{
|
||||
AllyFactoryUnsealData unsealData = ReadFactoryUnsealData(timeoutMs);
|
||||
log.InfoFormat("ALLY_OPTO_SETUP_SEAL_STATE: sealed={0}, factoryId='{1}', programmableText='{2}'",
|
||||
unsealData.IsSealed,
|
||||
unsealData.FactoryId,
|
||||
unsealData.ProgrammableText);
|
||||
|
||||
if (unsealData.IsSealed)
|
||||
{
|
||||
UnsealFactory(unsealData, timeoutMs);
|
||||
if (IsFactorySealed(timeoutMs))
|
||||
throw new InvalidOperationException("ALLY factory seal remained active after the unseal command.");
|
||||
|
||||
log.Info("ALLY_OPTO_SETUP_UNSEALED: factory seal was removed for optical verification.");
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Info("ALLY_OPTO_SETUP_UNSEAL_SKIPPED: meter is already unsealed.");
|
||||
}
|
||||
|
||||
StartOpticalVerificationStream(timeoutMs);
|
||||
log.Info("ALLY_OPTO_SETUP_STARTED: optical verification stream is active.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops COM optical capture and restores LED, meter mode and spread
|
||||
/// spectrum even when the dialog is closed unexpectedly.
|
||||
/// </summary>
|
||||
public void StopOpticalVerificationStream(int timeoutMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (opticalSync)
|
||||
{
|
||||
operationActive = false;
|
||||
}
|
||||
|
||||
if (opticalVerificationOutputActive)
|
||||
ExecuteCommand(service => service.StopOpticalVerificationOutput(timeoutMs));
|
||||
}
|
||||
finally
|
||||
{
|
||||
opticalVerificationOutputActive = false;
|
||||
StopDataStreamProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToHex(string text)
|
||||
{
|
||||
return BitConverter.ToString(Encoding.ASCII.GetBytes(text ?? string.Empty)).Replace("-", " ");
|
||||
}
|
||||
|
||||
private static string ToLogText(string text)
|
||||
{
|
||||
return (text ?? string.Empty)
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private void ExtendRawVolume(uint rawVolume)
|
||||
{
|
||||
if (!hasPreviousRawVolume)
|
||||
@ -678,51 +933,19 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
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.");
|
||||
}
|
||||
// C6 accumulator is expressed in quarter millilitres, independent of tube size.
|
||||
return 1D / 4000D;
|
||||
}
|
||||
|
||||
private void ResetVolumeState()
|
||||
{
|
||||
hasPreviousRawVolume = false;
|
||||
hasPreviousRawTimestamp = false;
|
||||
hasTestStartSample = false;
|
||||
previousRawVolume = 0;
|
||||
previousRawTimestamp = 0;
|
||||
extendedRawVolume = 0;
|
||||
extendedRawTimestamp = 0;
|
||||
firstSampleReceivedAtUtc = null;
|
||||
beginWMState = 0;
|
||||
endWMState = 0;
|
||||
timestampSecStart = 0;
|
||||
|
||||
@ -4,19 +4,51 @@ 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.
|
||||
/// ALLY optical metrology sample. ALLY emits a tab-separated envelope:
|
||||
/// sequence, message type, Base64 binary payload and a four-hex checksum.
|
||||
/// Type C6 contains the 24-byte C2 water-metrology layout.
|
||||
/// </summary>
|
||||
public sealed class AllyOpticalSample
|
||||
{
|
||||
private const int TelegramLength = 42;
|
||||
private const byte MetrologyPacketType = 0xC6;
|
||||
private const int MetrologyPayloadLength = 24;
|
||||
|
||||
public string RawLine { get; private set; }
|
||||
public DateTime ReceivedAtUtc { get; private set; }
|
||||
public byte Sequence { get; private set; }
|
||||
public byte PacketType { get; private set; }
|
||||
public ushort PacketChecksum { get; private set; }
|
||||
public int RawAdc { get; private set; }
|
||||
public short LastField { get; private set; }
|
||||
public short RawFlow { get; private set; }
|
||||
public uint RawVolume { get; private set; }
|
||||
public uint RawTimestamp { get; private set; }
|
||||
public ushort FlipPeriod { get; private set; }
|
||||
public ushort VinfStart { get; private set; }
|
||||
public ushort VinfEnd { get; private set; }
|
||||
public short ElectrodeDelta { get; private set; }
|
||||
public ushort Impedance { get; private set; }
|
||||
public byte FieldDriveTime { get; private set; }
|
||||
public byte Flags { get; private set; }
|
||||
public byte[] ExtensionBytes { get; private set; }
|
||||
|
||||
// C6 has no legacy 8192 Hz meter timestamp. Time is based on receipt.
|
||||
public uint RawTimestamp { get { return 0; } }
|
||||
public bool IsLowFlow { get { return (Flags & 0x01) != 0; } }
|
||||
public bool IsEmptyPipe { get { return (Flags & 0x02) != 0; } }
|
||||
public bool IsFastHptc { get { return (Flags & 0x04) != 0; } }
|
||||
public bool FieldPolarity { get { return (Flags & 0x08) != 0; } }
|
||||
public bool ImpedancePolarity { get { return (Flags & 0x10) != 0; } }
|
||||
|
||||
/// <summary>
|
||||
/// Flow decoded from the C6 payload. The payload stores quarter millilitres per second.
|
||||
/// </summary>
|
||||
public double FlowMillilitersPerSecond { get { return RawFlow / 4D; } }
|
||||
|
||||
/// <summary>
|
||||
/// Accumulated volume decoded from the C6 payload. The payload stores quarter millilitres.
|
||||
/// </summary>
|
||||
public double VolumeLiters { get { return RawVolume / 4000D; } }
|
||||
|
||||
public double ExtendedVolumeLiters { get; internal set; }
|
||||
public double ElapsedSeconds { get; internal set; }
|
||||
|
||||
@ -33,49 +65,77 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return false;
|
||||
|
||||
if (line.Length < TelegramLength)
|
||||
string telegram = line.TrimEnd('\r', '\n');
|
||||
string[] fields = telegram.Split('\t');
|
||||
if (fields.Length != 4)
|
||||
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')
|
||||
byte sequence;
|
||||
byte packetType;
|
||||
ushort checksum;
|
||||
if (!byte.TryParse(fields[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out sequence) ||
|
||||
!byte.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out packetType) ||
|
||||
!ushort.TryParse(fields[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out checksum) ||
|
||||
packetType != MetrologyPacketType)
|
||||
return false;
|
||||
|
||||
byte[] payload;
|
||||
try
|
||||
{
|
||||
payload = Convert.FromBase64String(fields[2]);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
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)
|
||||
if (payload.Length < MetrologyPayloadLength)
|
||||
return false;
|
||||
|
||||
sample = new AllyOpticalSample
|
||||
{
|
||||
RawLine = telegram,
|
||||
RawLine = line,
|
||||
ReceivedAtUtc = receivedAtUtc,
|
||||
RawFlow = unchecked((short)rawFlowUnsigned),
|
||||
RawVolume = rawVolume,
|
||||
RawTimestamp = rawTimestamp
|
||||
Sequence = sequence,
|
||||
PacketType = packetType,
|
||||
PacketChecksum = checksum,
|
||||
RawAdc = BitConverter.ToInt32(payload, 0),
|
||||
LastField = BitConverter.ToInt16(payload, 4),
|
||||
RawFlow = BitConverter.ToInt16(payload, 6),
|
||||
RawVolume = BitConverter.ToUInt32(payload, 8),
|
||||
FlipPeriod = BitConverter.ToUInt16(payload, 12),
|
||||
VinfStart = BitConverter.ToUInt16(payload, 14),
|
||||
VinfEnd = BitConverter.ToUInt16(payload, 16),
|
||||
ElectrodeDelta = BitConverter.ToInt16(payload, 18),
|
||||
Impedance = BitConverter.ToUInt16(payload, 20),
|
||||
FieldDriveTime = payload[22],
|
||||
Flags = payload[23],
|
||||
ExtensionBytes = CopyExtension(payload)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsMetrologyPacket(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return false;
|
||||
|
||||
string[] fields = line.TrimEnd('\r', '\n').Split('\t');
|
||||
byte packetType;
|
||||
return fields.Length == 4 &&
|
||||
byte.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out packetType) &&
|
||||
packetType == MetrologyPacketType;
|
||||
}
|
||||
|
||||
private static byte[] CopyExtension(byte[] payload)
|
||||
{
|
||||
int length = payload.Length - MetrologyPayloadLength;
|
||||
if (length <= 0)
|
||||
return new byte[0];
|
||||
|
||||
byte[] extension = new byte[length];
|
||||
Buffer.BlockCopy(payload, MetrologyPayloadLength, extension, 0, length);
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
partial class AllyReaderManualTestCtrl
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
protected override void Dispose(bool disposing) { if(disposing && components != null) components.Dispose(); base.Dispose(disposing); }
|
||||
protected override void Dispose(bool disposing) { if(disposing) { StopOpticalStream(); if(components != null) components.Dispose(); } base.Dispose(disposing); }
|
||||
private void InitializeComponent()
|
||||
{
|
||||
optoTestGroupBox=new System.Windows.Forms.GroupBox(); optoListBox=new System.Windows.Forms.ListBox(); RfidTestGroupBox=new System.Windows.Forms.GroupBox(); rfidOutputListBox=new System.Windows.Forms.ListBox(); label2=new System.Windows.Forms.Label(); rfidCommandComboBox=new System.Windows.Forms.ComboBox(); commandTestButton=new System.Windows.Forms.Button();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using TBF.Rig.RegisterReaders.AllyReader.Communication;
|
||||
using TBF.Rig.Sequences;
|
||||
|
||||
namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
@ -14,31 +15,99 @@ namespace TBF.Rig.RegisterReaders.AllyReader
|
||||
public AllyReaderManualTestCtrl()
|
||||
{
|
||||
InitializeComponent();
|
||||
rfidCommandComboBox.Items.AddRange(new object[] { "Read serial number", "Read version and type", "Set RFID mode", "Set NFC mode", "Read optical data", "Start optical stream", "Stop optical stream" });
|
||||
rfidCommandComboBox.SelectedIndex = 0;
|
||||
ShowNormalCommands();
|
||||
opticalPollTimer = new Timer { Interval = 250 };
|
||||
opticalPollTimer.Tick += OpticalPollTimer_Tick;
|
||||
}
|
||||
|
||||
public AllyReaderCfg Config { set { config = value; } }
|
||||
public void StopOpticalStream() { opticalPollTimer.Stop(); AllyMeterReader r=FindReader(); if(r!=null) try { r.StopDataStreamProcessing(); } catch(Exception e) { AddOutput(e.Message); } }
|
||||
public void StopOpticalStream()
|
||||
{
|
||||
opticalPollTimer.Stop();
|
||||
AllyMeterReader reader = FindReader();
|
||||
if (reader != null)
|
||||
{
|
||||
try { reader.StopOpticalVerificationStream(CommandTimeoutMs); }
|
||||
catch (Exception exception) { AddOutput("Stop error: " + exception.Message); }
|
||||
}
|
||||
ShowNormalCommands();
|
||||
}
|
||||
private void CommandTestButtonClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
AllyMeterReader r = FindReader(); string cmd = rfidCommandComboBox.SelectedItem as string;
|
||||
if (r == null) { AddOutput("Configured ALLY reader is not initialized on this bench."); return; }
|
||||
try {
|
||||
if (cmd == "Start optical stream") { r.StartDataStreamProcessing(); opticalPollTimer.Start(); AddOutput("Started"); }
|
||||
if (cmd == "Start optical stream")
|
||||
{
|
||||
r.StartOpticalVerificationStream(CommandTimeoutMs);
|
||||
opticalPollTimer.Start();
|
||||
ShowStopOnlyCommand();
|
||||
AddOutput("Optical stream started: valve open, spread spectrum disabled, mode 0x09, LED 0xC2, COM optical capture active.");
|
||||
}
|
||||
else if (cmd == "Stop optical stream") { StopOpticalStream(); AddOutput("Stopped"); }
|
||||
else if (cmd == "Read optical data") AddOpto(r.ReadOptoData());
|
||||
else if (cmd == "Read optical data") { r.RunDeviceBefore(); AddOpto(r.ReadOptoData()); }
|
||||
else if (cmd == "Read serial number") AddOutput(r.ReadSerialNumber(CommandTimeoutMs));
|
||||
else if (cmd == "Read version and type") AddOutput(r.ReadVersionAndType(CommandTimeoutMs).ToString());
|
||||
else if (cmd == "View factory seal") AddOutput(r.IsFactorySealed(CommandTimeoutMs) ? "Factory seal: sealed" : "Factory seal: unsealed");
|
||||
else if (cmd == "Unseal meter") UnsealMeter(r);
|
||||
else if (cmd == "Seal meter") SealMeter(r);
|
||||
else if (cmd == "Set RFID mode") { r.SetRfidInterface(); AddOutput("OK"); }
|
||||
else if (cmd == "Set NFC mode") { r.SetNfcInterface(); AddOutput("OK"); }
|
||||
} catch(Exception x) { AddOutput("Error: " + x.Message); }
|
||||
}
|
||||
private void OpticalPollTimer_Tick(object sender, EventArgs e) { AllyMeterReader r=FindReader(); if(r==null) { opticalPollTimer.Stop(); return; } try { AddOpto(r.ReadOptoData()); } catch(Exception x) { AddOpto("Error: " + x.Message); } }
|
||||
private void UnsealMeter(AllyMeterReader reader)
|
||||
{
|
||||
if (MessageBox.Show("Unseal the ALLY meter? This changes its factory-seal state.", "Unseal meter", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
|
||||
{
|
||||
AddOutput("Unseal cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
AllyFactoryUnsealData data = reader.ReadFactoryUnsealData(CommandTimeoutMs);
|
||||
AddOutput("Factory seal before unseal: " + (data.IsSealed ? "sealed" : "unsealed"));
|
||||
AddOutput("Factory ID: " + data.FactoryId);
|
||||
AddOutput("Programmable text: " + data.ProgrammableText);
|
||||
AddOutput("Reading preset: " + data.ReadingPreset);
|
||||
AddOutput("Seconds active: " + data.SecondsActive);
|
||||
AddOutput("Calculated unseal hash: " + data.CredentialHex);
|
||||
|
||||
if (!data.IsSealed)
|
||||
{
|
||||
AddOutput("Unseal skipped: meter is already unsealed.");
|
||||
return;
|
||||
}
|
||||
|
||||
reader.UnsealFactory(data, CommandTimeoutMs);
|
||||
AddOutput("Unseal response: 0x01 COMPLETE_NO_ERRORS");
|
||||
AddOutput(reader.IsFactorySealed(CommandTimeoutMs) ? "Factory seal is still sealed." : "Factory seal: unsealed");
|
||||
}
|
||||
private void SealMeter(AllyMeterReader reader)
|
||||
{
|
||||
if (MessageBox.Show("Seal the ALLY meter? This protects factory commands and optical-output configuration.", "Seal meter", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
|
||||
{
|
||||
AddOutput("Seal cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
reader.SealFactory(CommandTimeoutMs);
|
||||
AddOutput("Seal response: 0x01 COMPLETE_NO_ERRORS");
|
||||
AddOutput(reader.IsFactorySealed(CommandTimeoutMs) ? "Factory seal: sealed" : "Factory seal was not applied.");
|
||||
}
|
||||
private void OpticalPollTimer_Tick(object sender, EventArgs e) { AllyMeterReader r=FindReader(); if(r==null) { opticalPollTimer.Stop(); return; } try { r.RunDeviceBefore(); AddOpto(r.ReadOptoData()); } catch(Exception x) { AddOpto("Error: " + x.Message); } }
|
||||
private AllyMeterReader FindReader() { return config == null || ProcessData.SmartHeadsUni == null ? null : ProcessData.SmartHeadsUni.OfType<AllyMeterReader>().FirstOrDefault(x => x.Name == config.Name); }
|
||||
private void AddOpto(string text) { if(!string.IsNullOrEmpty(text)) optoListBox.Items.Insert(0,text); }
|
||||
private void AddOutput(string text) { rfidOutputListBox.Items.Insert(0,text ?? string.Empty); }
|
||||
private void ShowNormalCommands()
|
||||
{
|
||||
rfidCommandComboBox.Items.Clear();
|
||||
rfidCommandComboBox.Items.AddRange(new object[] { "Read serial number", "Read version and type", "View factory seal", "Unseal meter", "Seal meter", "Set RFID mode", "Set NFC mode", "Read optical data", "Start optical stream", "Stop optical stream" });
|
||||
rfidCommandComboBox.SelectedIndex = 0;
|
||||
}
|
||||
private void ShowStopOnlyCommand()
|
||||
{
|
||||
rfidCommandComboBox.Items.Clear();
|
||||
rfidCommandComboBox.Items.Add("Stop optical stream");
|
||||
rfidCommandComboBox.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,6 +93,96 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the documented ALLY optical-verification output. Factory
|
||||
/// sealed registers must be explicitly unsealed before this sequence.
|
||||
/// </summary>
|
||||
public void StartOpticalVerificationOutput(int timeoutMs)
|
||||
{
|
||||
if (IsFactorySealed(timeoutMs))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"ALLY meter is factory sealed. Unseal the meter before starting the optical stream.");
|
||||
}
|
||||
|
||||
OpenValve(timeoutMs);
|
||||
SetSpreadSpectrum(false, timeoutMs);
|
||||
SetMeterMode(0x09, timeoutMs);
|
||||
SetDiagnosticLed(0xC2, timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the documented normal ALLY operating state after optical
|
||||
/// verification output has stopped.
|
||||
/// </summary>
|
||||
public void StopOpticalVerificationOutput(int timeoutMs)
|
||||
{
|
||||
SetDiagnosticLed(0x00, timeoutMs);
|
||||
SetMeterMode(0x02, timeoutMs);
|
||||
SetSpreadSpectrum(true, timeoutMs);
|
||||
}
|
||||
|
||||
public bool IsFactorySealed(int timeoutMs)
|
||||
{
|
||||
AllyResponse response = Send(
|
||||
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewFactorySeal).BuildBytes(),
|
||||
timeoutMs);
|
||||
return response.GetByte() != 0x00;
|
||||
}
|
||||
|
||||
public AllyFactoryUnsealData ReadFactoryUnsealData(int timeoutMs)
|
||||
{
|
||||
bool isSealed = IsFactorySealed(timeoutMs);
|
||||
string programmableText = Send(
|
||||
new AllyFrameBuilder().WithCommand(AllyCommand.ViewProgrammableText).BuildBytes(),
|
||||
timeoutMs).GetNullTerminatedAscii();
|
||||
string factoryId = Send(
|
||||
new AllyFrameBuilder().WithCommand(AllyCommand.ViewFactoryId).BuildBytes(),
|
||||
timeoutMs).GetNullTerminatedAscii();
|
||||
string readingPreset = Send(
|
||||
new AllyFrameBuilder().WithCommand(AllyCommand.ViewPresetTotal).BuildBytes(),
|
||||
timeoutMs).GetNullTerminatedAscii();
|
||||
uint secondsActive = Send(
|
||||
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewSecondsActive).BuildBytes(),
|
||||
timeoutMs).GetUInt32LittleEndian();
|
||||
|
||||
return new AllyFactoryUnsealData(
|
||||
isSealed,
|
||||
factoryId,
|
||||
programmableText,
|
||||
readingPreset,
|
||||
secondsActive);
|
||||
}
|
||||
|
||||
public void UnsealFactory(int timeoutMs)
|
||||
{
|
||||
UnsealFactory(ReadFactoryUnsealData(timeoutMs), timeoutMs);
|
||||
}
|
||||
|
||||
public void UnsealFactory(AllyFactoryUnsealData data, int timeoutMs)
|
||||
{
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
Send(
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.SetFactorySeal)
|
||||
.WithByte(0x00)
|
||||
.WithBytes(data.Credential)
|
||||
.BuildBytes(),
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
public void SealFactory(int timeoutMs)
|
||||
{
|
||||
Send(
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.SetFactorySeal)
|
||||
.WithByte(0x01)
|
||||
.BuildBytes(),
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
public double ReadCalibrationFactorPercent(int timeoutMs)
|
||||
{
|
||||
AllyResponse response = Send(
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
{
|
||||
/// <summary>
|
||||
/// Values read immediately before breaking an ALLY factory seal and the
|
||||
/// corresponding, meter-specific eight-byte unseal credential.
|
||||
/// </summary>
|
||||
public sealed class AllyFactoryUnsealData
|
||||
{
|
||||
private const string FactoryFallback = "Factory";
|
||||
|
||||
internal AllyFactoryUnsealData(
|
||||
bool isSealed,
|
||||
string factoryId,
|
||||
string programmableText,
|
||||
string readingPreset,
|
||||
uint secondsActive)
|
||||
{
|
||||
IsSealed = isSealed;
|
||||
FactoryId = factoryId ?? string.Empty;
|
||||
ProgrammableText = programmableText ?? string.Empty;
|
||||
ReadingPreset = readingPreset ?? string.Empty;
|
||||
SecondsActive = secondsActive;
|
||||
Credential = BuildCredential();
|
||||
}
|
||||
|
||||
public bool IsSealed { get; private set; }
|
||||
public string FactoryId { get; private set; }
|
||||
public string ProgrammableText { get; private set; }
|
||||
public string ReadingPreset { get; private set; }
|
||||
public uint SecondsActive { get; private set; }
|
||||
public byte[] Credential { get; private set; }
|
||||
|
||||
public string CredentialHex
|
||||
{
|
||||
get { return BitConverter.ToString(Credential).Replace("-", " "); }
|
||||
}
|
||||
|
||||
private byte[] BuildCredential()
|
||||
{
|
||||
byte[] result = new byte[8];
|
||||
ushort secondsPart = (ushort)((SecondsActive >> 8) & 0xFFFF);
|
||||
ushort factoryIdPart = CalculateIperlCrc16(NormalizeFactoryId(FactoryId));
|
||||
ushort customerTextPart = CalculateCustomerTextPart(ProgrammableText);
|
||||
ushort presetPart = CalculatePresetPart(ReadingPreset);
|
||||
|
||||
WriteUInt16LittleEndian(result, 0, secondsPart);
|
||||
WriteUInt16LittleEndian(result, 2, factoryIdPart);
|
||||
WriteUInt16LittleEndian(result, 4, customerTextPart);
|
||||
WriteUInt16LittleEndian(result, 6, presetPart);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ushort CalculateIperlCrc16(string value)
|
||||
{
|
||||
ushort crc = 0;
|
||||
byte[] data = Encoding.ASCII.GetBytes(value);
|
||||
foreach (byte valueByte in data)
|
||||
{
|
||||
ushort temp = crc;
|
||||
crc = (ushort)(temp >> 8);
|
||||
crc += (ushort)(temp << 8);
|
||||
crc ^= valueByte;
|
||||
|
||||
temp = (ushort)((crc >> 4) & 0x000F);
|
||||
crc ^= temp;
|
||||
temp = (ushort)((crc & 0x000F) << 12);
|
||||
crc ^= temp;
|
||||
temp = (ushort)((crc & 0x00FF) << 5);
|
||||
crc ^= temp;
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static ushort CalculateCustomerTextPart(string value)
|
||||
{
|
||||
byte[] data = Encoding.ASCII.GetBytes(OverlayFactory(value));
|
||||
ushort result = 0;
|
||||
for (int index = 0; index < 4; index++)
|
||||
{
|
||||
byte current = data[index];
|
||||
byte setBits = 0;
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
setBits += (byte)((current >> bit) & 0x01);
|
||||
|
||||
result |= (ushort)(setBits << (index * 4));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ushort CalculatePresetPart(string value)
|
||||
{
|
||||
byte[] data = Encoding.ASCII.GetBytes(OverlayFactory(value));
|
||||
uint selectedCharacters = ((uint)data[2] << 24) |
|
||||
((uint)data[3] << 16) |
|
||||
((uint)data[4] << 8) |
|
||||
data[5];
|
||||
return (ushort)(selectedCharacters % 11);
|
||||
}
|
||||
|
||||
private static string OverlayFactory(string value)
|
||||
{
|
||||
char[] result = FactoryFallback.ToCharArray();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
string trimmed = value.Trim();
|
||||
int count = Math.Min(result.Length, trimmed.Length);
|
||||
for (int index = 0; index < count; index++)
|
||||
result[index] = trimmed[index];
|
||||
}
|
||||
|
||||
return new string(result);
|
||||
}
|
||||
|
||||
private static string NormalizeFactoryId(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? FactoryFallback : value.Trim();
|
||||
}
|
||||
|
||||
private static void WriteUInt16LittleEndian(byte[] target, int offset, ushort value)
|
||||
{
|
||||
target[offset] = (byte)(value & 0xFF);
|
||||
target[offset + 1] = (byte)(value >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,8 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
{
|
||||
ViewFactoryId = 0x01,
|
||||
ViewVersionAndType = 0x05,
|
||||
ViewProgrammableText = 0x07,
|
||||
ViewPresetTotal = 0x13,
|
||||
SetPresetTotal = 0x14,
|
||||
SetMeterMode = 0x1A,
|
||||
SetValvePosition = 0x1E
|
||||
@ -29,6 +31,9 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
SetCalibration = 0x54,
|
||||
ViewRebootCount = 0x55,
|
||||
SetDiagnosticLed = 0x60,
|
||||
ViewSecondsActive = 0x3D,
|
||||
ViewFactorySeal = 0x63,
|
||||
SetFactorySeal = 0x64,
|
||||
SetLcdTimeout = 0x8C,
|
||||
StartOffsetLearning = 0xD1
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Ports;
|
||||
using log4net;
|
||||
|
||||
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
{
|
||||
@ -62,6 +63,7 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
|
||||
public sealed class AllySerialTransport : IAllyTransport
|
||||
{
|
||||
private static readonly ILog log = LogManager.GetLogger(typeof(AllySerialTransport));
|
||||
private readonly object sync = new object();
|
||||
private readonly string portName;
|
||||
private readonly int baudRate;
|
||||
@ -113,33 +115,62 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
||||
|
||||
serialPort.DiscardInBuffer();
|
||||
serialPort.WriteTimeout = timeoutMs;
|
||||
serialPort.Write(request, 0, request.Length);
|
||||
log.DebugFormat("ALLY_CMD TX {0}: {1}", portName, FormatRequestForLog(request));
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
int first;
|
||||
do
|
||||
try
|
||||
{
|
||||
first = ReadByte(stopwatch, 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);
|
||||
|
||||
log.DebugFormat("ALLY_CMD RX {0}: {1}", portName, ToHex(response));
|
||||
return response;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
log.Error("ALLY_CMD failed on " + portName + "; request=" + FormatRequestForLog(request), exception);
|
||||
throw;
|
||||
}
|
||||
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 static string FormatRequestForLog(byte[] request)
|
||||
{
|
||||
// Factory-unseal credentials must not be persisted in the shared TBF log.
|
||||
if (request != null && request.Length == 15 &&
|
||||
request[0] == 0x53 && request[1] == 0x57 && request[2] == 0x0F &&
|
||||
request[3] == 0xFD && request[4] == 0x64 && request[5] == 0x00 && request[14] == 0x0D)
|
||||
{
|
||||
return "53 57 0F FD 64 00 <factory-unseal credential redacted; 8 bytes> 0D";
|
||||
}
|
||||
|
||||
return ToHex(request);
|
||||
}
|
||||
|
||||
private static string ToHex(byte[] data)
|
||||
{
|
||||
return data == null ? "<null>" : BitConverter.ToString(data).Replace("-", " ");
|
||||
}
|
||||
|
||||
private int ReadByte(Stopwatch stopwatch, int timeoutMs)
|
||||
{
|
||||
int remaining = timeoutMs - (int)stopwatch.ElapsedMilliseconds;
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
# ALLY optical-stream procedure
|
||||
|
||||
## Recommended procedure layout
|
||||
|
||||
Use the combined activities at the boundaries of measurement:
|
||||
|
||||
1. `Read serial number` (optional, recommended for traceability)
|
||||
2. `Unseal meter and start optical stream`
|
||||
3. One or more optical measurement activities
|
||||
4. `Stop optical stream`
|
||||
|
||||
The optical measurement must run after the start activity completes and before the stop activity begins.
|
||||
|
||||
## Unseal meter and start optical stream
|
||||
|
||||
This is a production-bench setup operation. It leaves the factory seal **unsealed** and leaves the optical stream **running**.
|
||||
|
||||
| Order | Action | Protocol effect |
|
||||
| --- | --- | --- |
|
||||
| 1 | Read factory unseal data | Reads factory seal state, factory ID, programmable text, reading preset and seconds active. These values produce the meter-specific unseal credential. |
|
||||
| 2 | Unseal only if necessary | Sends Factory Unseal with the calculated credential when the meter is sealed. An already unsealed meter is not changed. |
|
||||
| 3 | Verify factory seal | Confirms that the seal is no longer active. The setup stops on failure. |
|
||||
| 4 | Open ALLY valve | Sends valve state `Open`. |
|
||||
| 5 | Disable Spread Spectrum | Required before enabling the diagnostic optical output. |
|
||||
| 6 | Set initial meter mode | Sets meter mode `0x09`. |
|
||||
| 7 | Turn diagnostic LED on | Sets LED output `0xC2`, enabling the optical verification output. |
|
||||
| 8 | Start optical COM capture | Opens the configured ALLY optical port and starts parsing optical telegrams. |
|
||||
|
||||
### Equivalent manual composition
|
||||
|
||||
The procedure activity is intentionally atomic because the factory unseal credential is meter-specific. The UI manual command **Unseal meter** performs steps 1–3. The rest can be composed in the procedure as:
|
||||
|
||||
1. `Open ALLY valve`
|
||||
2. `Disable spread spectrum`
|
||||
3. `Set initial meter mode`
|
||||
4. `Turn diagnostic LED on`
|
||||
|
||||
These individual activities configure the meter output, but do **not** open the TBF optical COM capture. Use `Unseal meter and start optical stream` when TBF must receive and parse optical data.
|
||||
|
||||
## Stop optical stream
|
||||
|
||||
Use this as the final cleanup activity after all optical measurements. It is idempotent with respect to COM capture: it always closes the local stream even if a previous command failed.
|
||||
|
||||
| Order | Action | Protocol effect |
|
||||
| --- | --- | --- |
|
||||
| 1 | Stop optical processing | Stops the reader's active optical processing loop. |
|
||||
| 2 | Turn diagnostic LED off | Sets LED output `0x00`. |
|
||||
| 3 | Set active meter mode | Sets meter mode `0x02`. |
|
||||
| 4 | Enable Spread Spectrum | Restores normal Spread Spectrum operation. |
|
||||
| 5 | Close optical COM capture | Stops data-stream processing and closes the configured optical serial port, also when an earlier cleanup command failed. |
|
||||
|
||||
The operation does **not** reseal the meter and does not close the valve. Resealing is a separate intentional factory operation.
|
||||
|
||||
### Equivalent manual composition
|
||||
|
||||
The closest procedure-only composition is:
|
||||
|
||||
1. `Turn diagnostic LED off`
|
||||
2. `Set active meter mode`
|
||||
3. `Enable spread spectrum`
|
||||
|
||||
This restores meter settings but it does **not** close TBF optical COM capture. Use `Stop optical stream` whenever the stream was started by `Unseal meter and start optical stream`.
|
||||
|
||||
## Individual activities
|
||||
|
||||
| Activity | What it changes | What it does not do |
|
||||
| --- | --- | --- |
|
||||
| `Open ALLY valve` | Opens the valve. | Does not alter optical output. |
|
||||
| `Disable spread spectrum` | Disables Spread Spectrum. | Does not select mode or LED output. |
|
||||
| `Set initial meter mode` | Sets mode `0x09`. | Does not enable the diagnostic LED or COM capture. |
|
||||
| `Turn diagnostic LED on` | Sets LED `0xC2`. | Does not check/unseal factory seal or open COM capture. |
|
||||
| `Turn diagnostic LED off` | Sets LED `0x00`. | Does not restore mode, spread spectrum or COM capture. |
|
||||
| `Set active meter mode` | Sets mode `0x02`. | Does not turn off LED, enable Spread Spectrum or close COM capture. |
|
||||
| `Enable spread spectrum` | Restores Spread Spectrum. | Does not stop LED or COM capture. |
|
||||
| `Unseal meter and start optical stream` | Complete setup and COM capture. | Does not reseal automatically. |
|
||||
| `Stop optical stream` | Complete optical cleanup and COM close. | Does not reseal or close the valve. |
|
||||
|
||||
## Factory-seal warning
|
||||
|
||||
`Unseal meter and start optical stream` changes factory-seal state when the meter is sealed. Add it only to procedures intended for authorized verification or calibration benches. A normal accuracy test should use a meter already prepared for optical verification.
|
||||
@ -20,7 +20,9 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
WriteCalibrationFactor,
|
||||
WriteDisplayVolume,
|
||||
SetLcdTimeout,
|
||||
StartOffsetLearning
|
||||
StartOffsetLearning,
|
||||
UnsealAndStartOpticalStream,
|
||||
StopOpticalStream
|
||||
}
|
||||
|
||||
internal static class AllyCalibrationActivityNames
|
||||
@ -44,6 +46,8 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
public const string WriteDisplayVolume = "Write display volume";
|
||||
public const string SetLcdTimeout = "Set LCD timeout";
|
||||
public const string StartOffsetLearning = "Start offset learning";
|
||||
public const string UnsealAndStartOpticalStream = "Unseal meter and start optical stream";
|
||||
public const string StopOpticalStream = "Stop optical stream";
|
||||
|
||||
public static readonly string[] All =
|
||||
{
|
||||
@ -65,7 +69,9 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
WriteCalibrationFactor,
|
||||
WriteDisplayVolume,
|
||||
SetLcdTimeout,
|
||||
StartOffsetLearning
|
||||
StartOffsetLearning,
|
||||
UnsealAndStartOpticalStream,
|
||||
StopOpticalStream
|
||||
};
|
||||
|
||||
public static bool TryParse(string value, out AllyCalibrationActivity activity)
|
||||
|
||||
@ -3,7 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Common;
|
||||
using Config.Entities;
|
||||
using log4net;
|
||||
using Results.Entities;
|
||||
using TBF.Rig.GenericDevices;
|
||||
using TBF.Rig.RegisterReaders.AllyReader;
|
||||
using TBF.Rig.RegisterReaders.AllyReader.Communication;
|
||||
using TBF.Rig.Sequences;
|
||||
@ -13,6 +15,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
public class AllyCalibrationSeq : SequenceBase
|
||||
{
|
||||
private static readonly ILog log = LogManager.GetLogger(typeof(AllyCalibrationSeq));
|
||||
private const byte InitialMeterMode = 0x09;
|
||||
private const byte ActiveMeterMode = 0x02;
|
||||
private const byte DiagnosticLedCalibrationMode = 0xC2;
|
||||
@ -28,6 +31,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
TestMethodCfg cfg,
|
||||
TestMethodParams parameters)
|
||||
{
|
||||
log.InfoFormat(
|
||||
"ALLY_CALIBRATION_SEQUENCE_START: test='{0}', metersPath='{1}', activity='{2}', configuredPositions={3}, registerReaders={4}",
|
||||
test == null ? "<null>" : test.Name,
|
||||
test == null ? "<null>" : test.MetersPath,
|
||||
parameters == null ? "<null>" : parameters.Activity,
|
||||
BatchRslts == null ? 0 : BatchRslts.WMPositionsCount,
|
||||
sensPath == null || sensPath.RegisterReaders == null ? 0 : sensPath.RegisterReaders.Length);
|
||||
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, Progress.JustStarted));
|
||||
|
||||
TestRslt testResult = BatchRslts.GetTestRslt(test.Name, 0);
|
||||
@ -40,7 +50,14 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
Results.Entities.WaterMeter waterMeter = BatchRslts.Batch.WaterMeters[position];
|
||||
if (waterMeter == null || waterMeter.Disabled)
|
||||
{
|
||||
log.InfoFormat(
|
||||
"ALLY_CALIBRATION_POSITION_SKIPPED: test='{0}', position={1}, reason={2}",
|
||||
test.Name,
|
||||
position + 1,
|
||||
waterMeter == null ? "water meter is missing" : "water meter is disabled");
|
||||
continue;
|
||||
}
|
||||
|
||||
MeterTestRslt meterResult = BatchRslts.GetMeterTestRslt(
|
||||
test.Name,
|
||||
@ -59,11 +76,34 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
passed = false;
|
||||
resultMessage = "ALLY" + (position + 1) + ": ALLY reader is not configured.";
|
||||
IRegReader configuredReader = sensPath != null &&
|
||||
sensPath.RegisterReaders != null &&
|
||||
position < sensPath.RegisterReaders.Length
|
||||
? sensPath.RegisterReaders[position]
|
||||
: null;
|
||||
log.WarnFormat(
|
||||
"ALLY_CALIBRATION_READER_MISSING: test='{0}', position={1}; configuredReaderName='{2}', configuredReaderType='{3}', expectedType='AllyMeterReader'.",
|
||||
test.Name,
|
||||
position + 1,
|
||||
configuredReader == null ? "<null>" : configuredReader.Name,
|
||||
configuredReader == null ? "<null>" : configuredReader.GetType().FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.InfoFormat(
|
||||
"ALLY_CALIBRATION_POSITION_START: test='{0}', position={1}, reader='{2}', activity='{3}'",
|
||||
test.Name,
|
||||
position + 1,
|
||||
reader.Name,
|
||||
parameters.Activity);
|
||||
passed = ExecuteWithRetries(reader, cfg, parameters, out resultMessage);
|
||||
resultMessage = reader.Name + ": " + resultMessage;
|
||||
log.InfoFormat(
|
||||
"ALLY_CALIBRATION_POSITION_COMPLETED: test='{0}', position={1}, passed={2}, result='{3}'",
|
||||
test.Name,
|
||||
position + 1,
|
||||
passed,
|
||||
resultMessage);
|
||||
}
|
||||
|
||||
messages.Add(resultMessage);
|
||||
@ -90,7 +130,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
return new List<Event> { Event.Done };
|
||||
}
|
||||
|
||||
private static bool ExecuteWithRetries(
|
||||
internal static bool ExecuteWithRetries(
|
||||
AllyMeterReader reader,
|
||||
TestMethodCfg cfg,
|
||||
TestMethodParams parameters,
|
||||
@ -103,6 +143,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
try
|
||||
{
|
||||
log.InfoFormat(
|
||||
"ALLY_CALIBRATION_COMMAND_START: reader='{0}', activity='{1}', attempt={2}/{3}, timeoutMs={4}",
|
||||
reader.Name,
|
||||
parameters.Activity,
|
||||
attempt,
|
||||
attempts,
|
||||
cfg.CommandTimeoutMs);
|
||||
bool passed = ExecuteOnce(reader, cfg, parameters, out resultMessage);
|
||||
if (!passed)
|
||||
return false;
|
||||
@ -113,6 +160,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
log.WarnFormat(
|
||||
"ALLY_CALIBRATION_COMMAND_FAILED: reader='{0}', activity='{1}', attempt={2}/{3}, error='{4}'",
|
||||
reader.Name,
|
||||
parameters.Activity,
|
||||
attempt,
|
||||
attempts,
|
||||
ex.Message);
|
||||
if (attempt < attempts && cfg.DelayBetweenAttemptsMs > 0)
|
||||
Thread.Sleep(cfg.DelayBetweenAttemptsMs);
|
||||
}
|
||||
@ -251,6 +305,16 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
resultMessage = "OffsetLearning=1000 samples, 1s delay";
|
||||
return true;
|
||||
|
||||
case AllyCalibrationActivity.UnsealAndStartOpticalStream:
|
||||
reader.UnsealAndStartOpticalVerificationStream(timeout);
|
||||
resultMessage = "FactorySeal=unsealed; OpticalStream=active (valve=open, spread spectrum=disabled, mode=0x09, LED=0xC2)";
|
||||
return true;
|
||||
|
||||
case AllyCalibrationActivity.StopOpticalStream:
|
||||
reader.StopOpticalVerificationStream(timeout);
|
||||
resultMessage = "OpticalStream=stopped (LED=off, mode=active 0x02, spread spectrum=enabled, COM capture=stopped)";
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException("Unsupported ALLY calibration activity.");
|
||||
}
|
||||
|
||||
75
TBF/Rig/TestMethods/AllyCalibration/AllyCommunicationSeq.cs
Normal file
75
TBF/Rig/TestMethods/AllyCalibration/AllyCommunicationSeq.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
using Common;
|
||||
using Config.Entities;
|
||||
using log4net;
|
||||
using TBF.Rig.Generic;
|
||||
using TBF.Rig.GenericDevices;
|
||||
using TBF.Rig.Sequences;
|
||||
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
|
||||
using TBF.UiBridge;
|
||||
|
||||
namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
/// <summary>
|
||||
/// ALLY equivalent of iPerlCommunicationSeq. Command execution remains in
|
||||
/// AllyTestCorrections; this sequence owns only the modeless dialog lifecycle.
|
||||
/// </summary>
|
||||
public sealed class AllyCommunicationSeq : SequenceBase
|
||||
{
|
||||
private static readonly ILog log = LogManager.GetLogger(typeof(AllyCommunicationSeq));
|
||||
private Form modelessDialog;
|
||||
private volatile bool dialogClosed;
|
||||
|
||||
private delegate void OpenFormDelegate(AllyCommunicationSeq sequence, SmartComponentBase method, Test test, ITestParams parameters);
|
||||
|
||||
private void OpenForm(AllyCommunicationSeq sequence, SmartComponentBase method, Test test, ITestParams parameters)
|
||||
{
|
||||
sequence.modelessDialog = new SmartCommunicationForm(method, test, parameters);
|
||||
sequence.modelessDialog.FormClosed += delegate
|
||||
{
|
||||
sequence.dialogClosed = true;
|
||||
log.Info("ALLY_SMART_COMM_FORM_CLOSED_SIGNAL: FormClosed event received.");
|
||||
};
|
||||
sequence.modelessDialog.Show();
|
||||
}
|
||||
|
||||
public IList<Event> Execute(Test test, int repetitionNr, SmartComponentBase method, ITestParams parameters)
|
||||
{
|
||||
checkUiOp = new Operations.CheckUIOp(true);
|
||||
modelessDialog = null;
|
||||
dialogClosed = false;
|
||||
|
||||
Program.MainWnd.Invoke(new OpenFormDelegate(OpenForm), new object[] { this, method, test, parameters });
|
||||
Bridge.OnActivity(this, TBF.Resources.Strings.Meter_Communication_in_progress);
|
||||
|
||||
bool stopPressed;
|
||||
bool completed;
|
||||
IList<Event> events;
|
||||
State.Create("AllyCommunicationSeq : Wait until the SmartCommunicationForm is closed")
|
||||
.AddOperation(checkUiOp)
|
||||
.EnterState();
|
||||
|
||||
do
|
||||
{
|
||||
events = StateMachine.WaitRunDevsRunOps();
|
||||
stopPressed = TestAndLogUiCmdStop(test, events);
|
||||
completed = dialogClosed ||
|
||||
(modelessDialog is IHasCompleted && ((IHasCompleted)modelessDialog).Completed);
|
||||
} while (!stopPressed && !completed);
|
||||
|
||||
if (stopPressed)
|
||||
{
|
||||
Bridge.OnCloseModelessForm(this, null);
|
||||
return new List<Event> { Event.UiCmdStop };
|
||||
}
|
||||
|
||||
Bridge.OnTestProgress(this, new TestProgressEventArgs(test.Name, Progress.Completed));
|
||||
log.InfoFormat("ALLY_SMART_COMM_SEQUENCE_COMPLETED: test='{0}', formClosed={1}",
|
||||
test == null ? "<null>" : test.Name, dialogClosed);
|
||||
modelessDialog = null;
|
||||
return new List<Event> { Event.Done };
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,14 +6,16 @@ using log4net;
|
||||
using TBF.Rig.Generic;
|
||||
using TBF.Rig.GenericDevices;
|
||||
using TBF.Rig.Sequences;
|
||||
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
|
||||
using TBF.UiBridge;
|
||||
|
||||
namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
public class TestMethod : ComponentBase, ISimultTestMethod, ITestMethodSmart
|
||||
public class TestMethod : SmartComponentBase, ISimultTestMethod, ITestMethodSmart
|
||||
{
|
||||
private static readonly ILog log = LogManager.GetLogger(typeof(TestMethod));
|
||||
private readonly TestMethodCfg allyCfg;
|
||||
private readonly bool[] meterCommunicationMilestones = new bool[32];
|
||||
|
||||
public TestMethod()
|
||||
{
|
||||
@ -72,15 +74,45 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
log.FatalFormat("{0} initialized: {1}", Name, this);
|
||||
}
|
||||
|
||||
public override void MeterCommMilestone(int iItem, bool value)
|
||||
{
|
||||
if (iItem >= 0 && iItem < meterCommunicationMilestones.Length)
|
||||
meterCommunicationMilestones[iItem] = value;
|
||||
}
|
||||
|
||||
public override bool IsMeterCommMilestone(int iItem)
|
||||
{
|
||||
return iItem >= 0 && iItem < meterCommunicationMilestones.Length && meterCommunicationMilestones[iItem];
|
||||
}
|
||||
|
||||
public IList<Event> Execute(Test test, int repetitionNr, bool isLastRepetition)
|
||||
{
|
||||
TestMethodParams parameters = allyCfg.TestParams as TestMethodParams;
|
||||
log.InfoFormat(
|
||||
"ALLY_TEST_EXECUTE: test='{0}', repetition={1}, last={2}, debugMode={3}, activity='{4}', parametersLoaded={5}",
|
||||
test == null ? "<null>" : test.Name,
|
||||
repetitionNr,
|
||||
isLastRepetition,
|
||||
DebugLevel,
|
||||
parameters == null ? "<null>" : parameters.Activity,
|
||||
parameters != null);
|
||||
|
||||
if (parameters == null)
|
||||
{
|
||||
log.ErrorFormat(
|
||||
"ALLY_TEST_NOT_STARTED: test='{0}' cannot execute because the runtime TestParams provider is null. " +
|
||||
"No ALLY command, correction adapter, or SmartCommunicationForm can be started.",
|
||||
test == null ? "<null>" : test.Name);
|
||||
throw new InvalidOperationException("ALLY calibration test parameters are missing.");
|
||||
}
|
||||
|
||||
if (DebugLevel == DebugMode.Normal)
|
||||
return new AllyCalibrationSeq().Execute(test, repetitionNr, this, allyCfg, parameters);
|
||||
{
|
||||
log.Info("ALLY_TEST_ROUTE: opening SmartCommunicationForm through AllyCommunicationSeq and AllyTestCorrections.");
|
||||
return new AllyCommunicationSeq().Execute(test, repetitionNr, this, parameters);
|
||||
}
|
||||
|
||||
log.Info("ALLY_TEST_ROUTE: simulation branch selected; no physical ALLY command will be sent and no Optical heads dialog will be opened.");
|
||||
new AllyCalibrationSeq().MakeSimulatedTrivial(test, repetitionNr, test.Part);
|
||||
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, 1, Progress.Completed));
|
||||
Bridge.OnTestCompleted(
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Xml.Serialization;
|
||||
using Common;
|
||||
using Config.Entities;
|
||||
using TBF.Rig.Generic;
|
||||
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
|
||||
using SmartTestParams = TBF.Rig.Generic.ITestParams;
|
||||
|
||||
namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
public class TestMethodCfg : ComponentCfgBase, IComponentCfg, IParamsProvider
|
||||
// Implement the common smart-meter configuration contract so the ALLY test can
|
||||
// use SmartCommunicationForm in exactly the same way as the ASIC test method.
|
||||
public class TestMethodCfg : ComponentCfgBase, ITestMethodCfg, IParamsProvider
|
||||
{
|
||||
public static readonly XmlSerializer Serializer =
|
||||
XmlSerializer.FromTypes(new[] { typeof(TestMethodCfg) })[0];
|
||||
@ -18,11 +23,31 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
public string ExpectedDeviceType;
|
||||
public string ExpectedFirmwareVersion;
|
||||
|
||||
// The generic SmartCommunicationForm consumes these values through
|
||||
// ITestMethodCfg. ALLY uses one serialized command worker; the fields are
|
||||
// kept separate from the ALLY-specific command timeout above.
|
||||
public int DelayBetweenRetries { get; set; }
|
||||
public int MaxCommRetries { get; set; }
|
||||
public int WaitTimeAfterFailure { get; set; }
|
||||
public int PassThroughWaitTime { get; set; }
|
||||
public int NrThreads { get; set; }
|
||||
public int CommTimeout { get; set; }
|
||||
public int MciTimeoutMs { get; set; }
|
||||
public int BaudRate { get; set; }
|
||||
public int DataBits { get; set; }
|
||||
public Parity ParityBit { get; set; }
|
||||
public StopBits StopBits { get; set; }
|
||||
public bool UseWebService { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string RelativeUrl { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public ITestParams TestParams { get; set; }
|
||||
public SmartTestParams TestParams { get; set; }
|
||||
|
||||
private TestMethodCfg()
|
||||
{
|
||||
InitializeAll();
|
||||
TestParams = new TestMethodParams(true);
|
||||
}
|
||||
|
||||
public TestMethodCfg(IComponentFactory factory)
|
||||
@ -68,6 +93,20 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
DelayBetweenAttemptsMs = 250;
|
||||
ExpectedDeviceType = "SWM003";
|
||||
ExpectedFirmwareVersion = string.Empty;
|
||||
DelayBetweenRetries = 250;
|
||||
MaxCommRetries = 4;
|
||||
WaitTimeAfterFailure = 0;
|
||||
PassThroughWaitTime = 0;
|
||||
NrThreads = 1;
|
||||
CommTimeout = CommandTimeoutMs;
|
||||
MciTimeoutMs = 5000;
|
||||
BaudRate = 2400;
|
||||
DataBits = 8;
|
||||
ParityBit = Parity.None;
|
||||
StopBits = StopBits.One;
|
||||
UseWebService = false;
|
||||
BaseUrl = string.Empty;
|
||||
RelativeUrl = string.Empty;
|
||||
}
|
||||
|
||||
public int ParamsCount()
|
||||
@ -179,7 +218,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
DelayBetweenAttemptsMs = DelayBetweenAttemptsMs,
|
||||
ExpectedDeviceType = ExpectedDeviceType,
|
||||
ExpectedFirmwareVersion = ExpectedFirmwareVersion,
|
||||
TestParams = TestParams == null ? null : TestParams.Clone() as ITestParams
|
||||
TestParams = TestParams == null ? null : TestParams.Clone() as SmartTestParams
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ using TBF.Resources;
|
||||
|
||||
namespace TBF.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
public class TestMethodParams : TestParamsBase, IParamsProvider, ITestParams
|
||||
// Use the same parameter contract as SmartCommunicationForm explicitly.
|
||||
// Config.Entities also exposes an ITestParams name in some builds.
|
||||
public class TestMethodParams : TestParamsBase, IParamsProvider, TBF.Rig.Generic.ITestParams
|
||||
{
|
||||
public static readonly XmlSerializer Serializer =
|
||||
XmlSerializer.FromTypes(new[] { typeof(TestMethodParams) })[0];
|
||||
|
||||
@ -35,7 +35,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
ThreadId,
|
||||
WMNr0,
|
||||
(Ihead != null) ? Ihead.Name : "null",
|
||||
Wm.WMPosition,
|
||||
Wm == null ? "null" : Wm.WMPosition.ToString(),
|
||||
(CommMessage != null) ? CommMessage : "null",
|
||||
CommErr);
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ using TBF.Rig.Generic;
|
||||
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
|
||||
using TBF.Rig.RegisterReaders.iPerlReaderUNI;
|
||||
using TBF.Rig.RegisterReaders.PoseidonReader;
|
||||
using AllyMeterReader = TBF.Rig.RegisterReaders.AllyReader.AllyMeterReader;
|
||||
using IPerlASICSmartReader = TBF.Rig.RegisterReaders.iPerlASICReader.implementations.SmartReader;
|
||||
using TBF.Rig.Sequences;
|
||||
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
|
||||
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
|
||||
@ -98,6 +100,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
public CheckBoxImage[] CheckBoxes { get => this.checkBoxes; }
|
||||
public int[] CkbIndex { get => SmartCommunicationForm.ckbIndex; }
|
||||
public bool[] CkbState { get => SmartCommunicationForm.ckbState; }
|
||||
public IList<int> WaterMeterPositions0 { get => SmartCommunicationForm.waterMeterPositions0; }
|
||||
|
||||
///
|
||||
/// RFID multiplexer PCB / RFID serial port and worker thread related variables
|
||||
@ -110,6 +113,9 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_corrections == null || _corrections.Count == 0)
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrEmpty(SelectedTypeReader) && _corrections.Count > 0)
|
||||
{
|
||||
return _corrections.First();
|
||||
@ -142,10 +148,15 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
{
|
||||
List<ICorrections> correctionsList = new List<ICorrections>();
|
||||
|
||||
foreach (var smartHead in ProcessData.SmartHeadsUni)
|
||||
// In a test use the reader family from the selected Meters Path. The
|
||||
// global list can contain iPerl, ASIC and ALLY readers at the same time.
|
||||
IEnumerable<ISmartReader> smartHeads = ProcessData.RegisterReaders == null
|
||||
? ProcessData.SmartHeadsUni
|
||||
: ProcessData.RegisterReaders.OfType<ISmartReader>();
|
||||
foreach (var smartHead in smartHeads)
|
||||
{
|
||||
try{
|
||||
if (smartHead is IperlHead iperlHead)
|
||||
if (smartHead is IperlHead iperlHead)
|
||||
{
|
||||
if (correctionsList.Any(x => x is IPerlCorrections))
|
||||
continue;
|
||||
@ -153,15 +164,31 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
continue;
|
||||
}
|
||||
|
||||
if (smartHead is SmartReader smartReader)
|
||||
{
|
||||
if (correctionsList.Any(x => x is SmartReader))
|
||||
continue;
|
||||
correctionsList.Add(new PoseidonCorrections(this,log, rfidDataLogger, componentBase, cfg, tests, multiTestParams));
|
||||
continue;
|
||||
}
|
||||
if (smartHead is IPerlASICSmartReader)
|
||||
{
|
||||
if (correctionsList.Any(x => x is IPerlASICCorrections))
|
||||
continue;
|
||||
correctionsList.Add(new IPerlASICCorrections(this, componentBase, cfg, tests, multiTestParams));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception("Unknown smart head type");
|
||||
if (smartHead is AllyMeterReader)
|
||||
{
|
||||
if (correctionsList.Any(x => x is AllyTestCorrections))
|
||||
continue;
|
||||
correctionsList.Add(new AllyTestCorrections(this, componentBase, cfg, tests, multiTestParams));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (smartHead is SmartReader smartReader)
|
||||
{
|
||||
if (correctionsList.Any(x => x is PoseidonCorrections))
|
||||
continue;
|
||||
correctionsList.Add(new PoseidonCorrections(this,log, rfidDataLogger, componentBase, cfg, tests, multiTestParams));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception("Unknown smart head type");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -252,6 +279,29 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
InitializeComponent();
|
||||
|
||||
this.Icon = Properties.Resources.TBF_icon;
|
||||
Shown += (sender, args) => LogFormState("SHOWN");
|
||||
FormClosed += (sender, args) => LogFormState("CLOSED");
|
||||
}
|
||||
|
||||
private void LogFormState(string stage)
|
||||
{
|
||||
string correctionNames = _corrections == null
|
||||
? "<null>"
|
||||
: string.Join(",", _corrections.Select(correction => correction.TypeIdentificatorName()));
|
||||
string positions = waterMeterPositions0 == null
|
||||
? "<null>"
|
||||
: string.Join(",", waterMeterPositions0.Select(position => (position + 1).ToString()));
|
||||
|
||||
log.InfoFormat(
|
||||
"SMART_COMM_FORM_{0}: editMode={1}, visible={2}, activity='{3}', selectedType='{4}', corrections=[{5}], heads={6}, positions=[{7}]",
|
||||
stage,
|
||||
checkBoxesEditMode,
|
||||
Visible,
|
||||
activityLabel == null ? "<uninitialized>" : activityLabel.Text,
|
||||
SelectedTypeReader ?? "<null>",
|
||||
correctionNames,
|
||||
iperlHeads == null ? 0 : iperlHeads.Count,
|
||||
positions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -296,13 +346,21 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
/// </summary>
|
||||
/// <param name="waterMetersCount">Number of text boxes for serial numbers</param>
|
||||
public SmartCommunicationForm(Generic.IComponent componentBase, IList<Test> tests, IList<ITestParams> multiTestParams)
|
||||
: this()
|
||||
: this()
|
||||
{
|
||||
checkBoxesEditMode = false;
|
||||
|
||||
ITestMethodCfg cfg = (componentBase as ITestMethodCfg);
|
||||
ISmartTestMethod smartTestMethod = componentBase as ISmartTestMethod;
|
||||
|
||||
// Resolve the Meters Path before selecting a correction adapter. This is
|
||||
// essential on mixed benches: the ALLY test must not select an iPerl
|
||||
// adapter merely because an iPerl reader exists elsewhere on the bench.
|
||||
if (tests != null && tests.Count > 0)
|
||||
{
|
||||
ProcessData.RegisterReaders = StateMachine.GetMetersPath(tests[0]).RegisterReaders;
|
||||
}
|
||||
|
||||
//TODO get corrections based on defined meter
|
||||
_corrections = GetNewCorrectionList(smartTestMethod, smartTestMethod.TestMethodCfg, tests, multiTestParams);
|
||||
InitializeMeterTypeItems();
|
||||
@ -313,7 +371,6 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
|
||||
if (multiTestParams.Count > 0)
|
||||
{
|
||||
ProcessData.RegisterReaders = StateMachine.GetMetersPath(tests[0]).RegisterReaders;
|
||||
activityLabel.Text = multiTestParams[0].Activity;
|
||||
foreach (var p in multiTestParams)
|
||||
{
|
||||
@ -358,6 +415,12 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
|
||||
private void UpdateHeads()
|
||||
{
|
||||
log.InfoFormat(
|
||||
"SMART_COMM_FORM_UPDATE_HEADS_START: selectedType='{0}', configuredSmartHeads={1}, corrections={2}",
|
||||
SelectedTypeReader ?? "<null>",
|
||||
ProcessData.SmartHeadsUni == null ? 0 : ProcessData.SmartHeadsUni.Count,
|
||||
_corrections == null ? 0 : _corrections.Count);
|
||||
|
||||
if (!(waterMeterPositions0 == null || waterMeterPositions0.Count <= 0)
|
||||
&& labels != null && counters != null && messages != null && checkBoxes != null)
|
||||
{
|
||||
@ -423,6 +486,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
this.ContextMenu = null;
|
||||
}
|
||||
}
|
||||
|
||||
log.InfoFormat(
|
||||
"SMART_COMM_FORM_UPDATE_HEADS_COMPLETED: selectedType='{0}', correction='{1}', displayedHeads={2}, positions=[{3}]",
|
||||
SelectedTypeReader ?? "<null>",
|
||||
corre == null ? "<none>" : corre.TypeIdentificatorName(),
|
||||
iperlHeads.Count,
|
||||
string.Join(",", waterMeterPositions0.Select(position => (position + 1).ToString())));
|
||||
}
|
||||
|
||||
private void InitializeMeterTypeItems()
|
||||
@ -515,7 +585,9 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
|
||||
private void DoOnCommCompleted(object sender, CommCompletedEventArgs data)
|
||||
{
|
||||
log.InfoFormat("SMART_COMM_FORM_COMM_COMPLETED: {0}", data == null ? "<null>" : data.ToString());
|
||||
Correction.DoOnCommCompleted(sender, data, waterMeterPositions0);
|
||||
LogFormState("COMM_TEXT_UPDATED");
|
||||
}
|
||||
|
||||
|
||||
@ -647,6 +719,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
private void SmartCommunicationForm_Load(object sender, EventArgs e)
|
||||
{
|
||||
Localize();
|
||||
LogFormState("LOAD_START");
|
||||
|
||||
if (checkBoxesEditMode)
|
||||
{
|
||||
@ -671,6 +744,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
meterTypeComboBox.Visible = true;
|
||||
}
|
||||
Correction.Load(labels,counters,messages,checkBoxes,ckbIndex,ckbState, iperlHeads,textBoxesCount,checkBoxesEditMode );
|
||||
LogFormState("CONTROLS_LOADED");
|
||||
|
||||
///
|
||||
/// Set location and checkbox states to values stored in local settings
|
||||
@ -691,6 +765,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
SetCheckBoxStates(SmartReaderSelection.GetMask(ls, iperlHeads));
|
||||
}
|
||||
|
||||
int selectedHeads = checkBoxes == null ? 0 : checkBoxes.Take(Math.Min(textBoxesCount, iperlHeads.Count)).Count(checkBox => checkBox.Checked);
|
||||
log.InfoFormat(
|
||||
"SMART_COMM_FORM_SELECTION_APPLIED: selectedHeads={0}/{1}, activity='{2}'",
|
||||
selectedHeads,
|
||||
iperlHeads.Count,
|
||||
activityLabel.Text);
|
||||
|
||||
if (!checkBoxesEditMode)
|
||||
{
|
||||
/// Regular activity (not a checkbox edit mode invoked from TBF menu)
|
||||
@ -772,6 +853,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
|
||||
void DoOnAllCompleted(object sender, AllCompletedEventArgs data)
|
||||
{
|
||||
Text = data.CommMessage;
|
||||
log.InfoFormat("SMART_COMM_FORM_ALL_COMPLETED: message='{0}'", data == null ? "<null>" : data.CommMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Common;
|
||||
using Config.Entities;
|
||||
using log4net;
|
||||
using Results.Entities;
|
||||
using TBF.Rig.Generic;
|
||||
using TBF.Rig.RegisterReaders.AllyReader;
|
||||
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
|
||||
using TBF.Rig.Sequences;
|
||||
using TBF.Rig.TestMethods.AllyCalibration;
|
||||
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
|
||||
using CheckBoxImage = TBF.Boxes.CheckBoxImage;
|
||||
|
||||
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Test-time ALLY adapter for SmartCommunicationForm.
|
||||
///
|
||||
/// AllyCorrections intentionally remains the manual-command adapter. This class
|
||||
/// runs configured ALLY test activities and reports every completed reader back
|
||||
/// to the shared dialog, just like the iPerl ASIC correction adapter does.
|
||||
/// </summary>
|
||||
internal sealed class AllyTestCorrections : ICorrections
|
||||
{
|
||||
private ILog log { get { return ParentFrom == null ? null : ParentFrom.Log; } }
|
||||
private readonly ISmartTestMethod testMethod;
|
||||
private readonly ITestMethodCfg cfg;
|
||||
private readonly IList<Test> tests;
|
||||
private readonly IList<ITestParams> multiTestParams;
|
||||
private readonly IList<Thread> workerThreads = new List<Thread>();
|
||||
private volatile bool stopWorkerThreads;
|
||||
private int completedActivities;
|
||||
private int expectedActivities;
|
||||
|
||||
public AllyTestCorrections(SmartCommunicationForm parent, ISmartTestMethod testMethod,
|
||||
ITestMethodCfg cfg, IList<Test> tests, IList<ITestParams> multiTestParams)
|
||||
{
|
||||
ParentFrom = parent;
|
||||
this.testMethod = testMethod;
|
||||
this.cfg = cfg;
|
||||
this.tests = tests ?? new List<Test>();
|
||||
this.multiTestParams = multiTestParams ?? new List<ITestParams>();
|
||||
}
|
||||
|
||||
public string TypeIdentificatorName() { return "ALLY"; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public int StartTimeSec { get; set; }
|
||||
public SmartCommunicationForm ParentFrom { get; set; }
|
||||
public ITestMethodCfg Cfg { get { return cfg; } set { } }
|
||||
public IList<ITestParams> MultiTestParams { get { return multiTestParams; } }
|
||||
public IList<Test> Tests { get { return tests; } set { } }
|
||||
public IList<ISmartReader> iperlHeads { get { return ParentFrom == null ? null : ParentFrom.Heads; } }
|
||||
|
||||
public void PrepareForTestsActivities(int waterMetersCount)
|
||||
{
|
||||
StartTime = DateTime.Now;
|
||||
StartTimeSec = StateMachine.Time;
|
||||
stopWorkerThreads = false;
|
||||
completedActivities = 0;
|
||||
expectedActivities = Math.Max(0, waterMetersCount) * Math.Max(1, multiTestParams.Count);
|
||||
workerThreads.Clear();
|
||||
|
||||
// ALLY command communication is serialized. A single command port must
|
||||
// not be accessed concurrently even when more than one meter is shown.
|
||||
workerThreads.Add(new Thread(Worker) { IsBackground = true, Name = "ALLY SmartCommunication worker" });
|
||||
ParentFrom.Log.InfoFormat(
|
||||
"ALLY_TEST_CORRECTIONS_PREPARE: readers={0}, activities={1}, expectedCallbacks={2}, workers=1",
|
||||
waterMetersCount, multiTestParams.Count, expectedActivities);
|
||||
}
|
||||
|
||||
public void Worker(object threadData)
|
||||
{
|
||||
int threadId = (threadData as TBF.Boxes.IntBox) == null ? 0 : (threadData as TBF.Boxes.IntBox).Val;
|
||||
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_WORKER_START: thread={0}, readers={1}, activities={2}",
|
||||
threadId, iperlHeads == null ? 0 : iperlHeads.Count, multiTestParams.Count);
|
||||
|
||||
for (int activityIndex = 0; activityIndex < multiTestParams.Count && !stopWorkerThreads; activityIndex++)
|
||||
{
|
||||
Test currentTest = activityIndex < tests.Count ? tests[activityIndex] : null;
|
||||
TestMethodParams parameters = multiTestParams[activityIndex] as TestMethodParams;
|
||||
bool allPassed = true;
|
||||
int processedReaders = 0;
|
||||
if (parameters == null)
|
||||
{
|
||||
ParentFrom.Log.ErrorFormat("ALLY_TEST_CORRECTIONS_INVALID_PARAMS: activityIndex={0}", activityIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
TBF.UiBridge.Bridge.OnTestProgress(this,
|
||||
new TBF.UiBridge.TestProgressEventArgs(currentTest, Progress.JustStarted));
|
||||
|
||||
for (int localIndex = 0; localIndex < iperlHeads.Count && !stopWorkerThreads; localIndex++)
|
||||
{
|
||||
AllyMeterReader reader = iperlHeads[localIndex] as AllyMeterReader;
|
||||
int originalIndex = GetOriginalIndex(localIndex);
|
||||
WaterMeter waterMeter = GetWaterMeter(originalIndex);
|
||||
CommErr error = CommErr.None;
|
||||
string message;
|
||||
bool passed = false;
|
||||
|
||||
if (reader == null)
|
||||
{
|
||||
error = CommErr.WrongIPerlType;
|
||||
message = "ALLY reader is not configured.";
|
||||
}
|
||||
else if (IsDisabled(localIndex, reader, waterMeter))
|
||||
{
|
||||
error = CommErr.HeadDisabledByUser;
|
||||
message = "Disabled by user";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
ParentFrom.Log.InfoFormat(
|
||||
"ALLY_TEST_CORRECTIONS_COMMAND_START: test='{0}', position={1}, reader='{2}', activity='{3}'",
|
||||
currentTest == null ? "<null>" : currentTest.Name,
|
||||
originalIndex + 1,
|
||||
reader.Name,
|
||||
parameters.Activity);
|
||||
passed = AllyCalibrationSeq.ExecuteWithRetries(reader, cfg as TestMethodCfg, parameters, out message);
|
||||
message = reader.Name + ": " + message;
|
||||
if (!passed)
|
||||
error = CommErr.CommFailed;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
error = CommErr.CommFailed;
|
||||
message = reader.Name + ": " + exception.Message;
|
||||
ParentFrom.Log.Error("ALLY_TEST_CORRECTIONS_COMMAND_FAILED", exception);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateResult(currentTest, originalIndex, waterMeter, reader, parameters.Activity, passed);
|
||||
processedReaders++;
|
||||
allPassed &= passed;
|
||||
ParentFrom.Log.InfoFormat(
|
||||
"ALLY_TEST_CORRECTIONS_COMMAND_COMPLETED: test='{0}', position={1}, passed={2}, error={3}, result='{4}'",
|
||||
currentTest == null ? "<null>" : currentTest.Name,
|
||||
originalIndex + 1,
|
||||
passed,
|
||||
error,
|
||||
message);
|
||||
SmartCommunicationForm.OnCommCompleted(this,
|
||||
new CommCompletedEventArgs(threadId, localIndex, reader, waterMeter, message, error));
|
||||
}
|
||||
|
||||
CompleteTest(currentTest, processedReaders, allPassed);
|
||||
}
|
||||
|
||||
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_WORKER_COMPLETED: thread={0}", threadId);
|
||||
}
|
||||
|
||||
public bool WorkerActivity(string currentActivity, ISmartReader iHead, WaterMeter wm, Test currentTest,
|
||||
int wmNr0, ref CommErr error, ref string resultStr, bool[] ckbState, int threadID, int currentActivityStep)
|
||||
{
|
||||
AllyMeterReader reader = iHead as AllyMeterReader;
|
||||
TestMethodParams parameters = currentActivityStep < multiTestParams.Count
|
||||
? multiTestParams[currentActivityStep] as TestMethodParams
|
||||
: null;
|
||||
if (reader == null || parameters == null)
|
||||
{
|
||||
error = CommErr.WrongArguments;
|
||||
resultStr = "ALLY reader or activity is missing.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool passed = AllyCalibrationSeq.ExecuteWithRetries(reader, cfg as TestMethodCfg, parameters, out resultStr);
|
||||
error = passed ? CommErr.None : CommErr.CommFailed;
|
||||
return passed;
|
||||
}
|
||||
|
||||
public void ProcessResultOfWorkerActivity(int iMultiTestParamsItem, string currentActivity, int currentGroup,
|
||||
ISmartReader iHead, WaterMeter wm, int wmNr0, CommErr error, string resultStr, bool[] ckbState, int threadID)
|
||||
{
|
||||
SmartCommunicationForm.OnCommCompleted(this,
|
||||
new CommCompletedEventArgs(threadID, wmNr0, iHead, wm, resultStr, error));
|
||||
}
|
||||
|
||||
public void StopWorkerThreads(bool stopAllThreads) { stopWorkerThreads = stopAllThreads; }
|
||||
public bool GetStopWorkerThreads() { return stopWorkerThreads; }
|
||||
public IList<Thread> GetAllThreads() { return workerThreads; }
|
||||
public ICorrections GetNewCorrection() { return this; }
|
||||
public void GetGroup() { }
|
||||
public ContextMenu GetContextMenu() { return new ContextMenu(); }
|
||||
|
||||
public void Load(Label[] labels, PictureBox[] counters, TextBox[] messages, CheckBoxImage[] checkBoxes, int[] ckbIndex,
|
||||
bool[] ckbState, IList<ISmartReader> heads, int textBoxesCount, bool checkBoxesEditMode)
|
||||
{
|
||||
for (int index = 0; index < textBoxesCount; index++)
|
||||
{
|
||||
bool hasHead = heads != null && index < heads.Count && heads[index] is AllyMeterReader;
|
||||
labels[index].Visible = counters[index].Visible = messages[index].Visible = checkBoxes[index].Visible = hasHead;
|
||||
if (!hasHead)
|
||||
continue;
|
||||
|
||||
bool disabled = heads[index].Disabled;
|
||||
checkBoxes[index].Enabled = checkBoxes[index].Checked = ckbState[index] = !disabled;
|
||||
messages[index].Text = disabled ? "Disabled by user" : "---";
|
||||
counters[index].BackColor = disabled
|
||||
? iPerlCommunicationConstants.DisabledColor
|
||||
: iPerlCommunicationConstants.OptoNokColor;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetHeadsCount() { return iperlHeads == null ? 0 : iperlHeads.Count; }
|
||||
public void StartDataStreamProcessingForActiveMeters(int iMultiTestParamsItem) { }
|
||||
|
||||
public void DoOnCommCompleted(object sender, CommCompletedEventArgs data, IList<int> waterMeterPositions0)
|
||||
{
|
||||
if (data == null || data.WMNr0 < 0 || data.WMNr0 >= ParentFrom.Messages.Length)
|
||||
return;
|
||||
|
||||
ParentFrom.Messages[data.WMNr0].Text = data.CommMessage;
|
||||
ParentFrom.Counters[data.WMNr0].BackColor = data.CommErr == CommErr.None
|
||||
? iPerlCommunicationConstants.OptoAndDirOKColor
|
||||
: iPerlCommunicationConstants.OptoNokColor;
|
||||
|
||||
if (++completedActivities < expectedActivities || ParentFrom.IsDisposed)
|
||||
return;
|
||||
|
||||
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_ALL_COMPLETED: callbacks={0}", completedActivities);
|
||||
ParentFrom.NormalClose();
|
||||
}
|
||||
|
||||
public void NormalClose(IList<int> waterMeterPositions0) { StopWorkerThreads(true); }
|
||||
public bool IsFamilyOfSmartReader(ISmartReader smartHead) { return smartHead is AllyMeterReader; }
|
||||
|
||||
private bool IsDisabled(int localIndex, AllyMeterReader reader, WaterMeter waterMeter)
|
||||
{
|
||||
return (ParentFrom.CkbState != null && localIndex < ParentFrom.CkbState.Length && !ParentFrom.CkbState[localIndex]) ||
|
||||
reader.Disabled ||
|
||||
(waterMeter != null && waterMeter.Disabled);
|
||||
}
|
||||
|
||||
private int GetOriginalIndex(int localIndex)
|
||||
{
|
||||
IList<int> positions = ParentFrom.WaterMeterPositions0;
|
||||
return positions != null && localIndex < positions.Count ? positions[localIndex] : localIndex;
|
||||
}
|
||||
|
||||
private static WaterMeter GetWaterMeter(int originalIndex)
|
||||
{
|
||||
return ProcessData.BatchRslts != null && ProcessData.BatchRslts.Batch != null &&
|
||||
ProcessData.BatchRslts.Batch.WaterMeters != null &&
|
||||
originalIndex >= 0 && originalIndex < ProcessData.BatchRslts.Batch.WaterMeters.Count
|
||||
? ProcessData.BatchRslts.Batch.WaterMeters[originalIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
private void UpdateResult(Test test, int originalIndex, WaterMeter waterMeter, AllyMeterReader reader,
|
||||
string activity, bool passed)
|
||||
{
|
||||
if (test == null || ProcessData.BatchRslts == null)
|
||||
return;
|
||||
|
||||
MeterTestRslt result = ProcessData.BatchRslts.GetMeterTestRslt(test.Name, originalIndex, CompoundMeterId.Single);
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
result.RegReaderType = (int)RegisterReaderType.DataStream;
|
||||
result.TestDone = true;
|
||||
result.Passed = passed;
|
||||
|
||||
// Follow the established iPerl/ASIC behavior: the physical reader owns
|
||||
// the received value, while the displayed and persisted s/n belongs to
|
||||
// the WaterMeter associated with this MeterTestRslt.
|
||||
AllyCalibrationActivity parsedActivity;
|
||||
if (passed && reader != null && waterMeter != null &&
|
||||
AllyCalibrationActivityNames.TryParse(activity, out parsedActivity) &&
|
||||
parsedActivity == AllyCalibrationActivity.ReadSerialNumber &&
|
||||
!string.IsNullOrWhiteSpace(reader.SerialNr))
|
||||
{
|
||||
// The MeterTestRslt inverse reference can be null until NHibernate
|
||||
// flushes it. The batch WaterMeter is the authoritative object used
|
||||
// by the Results grid, therefore write to it directly.
|
||||
waterMeter.SerialNr = reader.SerialNr;
|
||||
if (result.WaterMeter != null)
|
||||
result.WaterMeter.SerialNr = reader.SerialNr;
|
||||
log.InfoFormat(
|
||||
"ALLY_TEST_SERIAL_NUMBER_MAPPED: test='{0}', position={1}, serialNumber='{2}'",
|
||||
test.Name,
|
||||
originalIndex + 1,
|
||||
reader.SerialNr);
|
||||
}
|
||||
|
||||
log.InfoFormat("ALLY_TEST_RESULT_UPDATED: test='{0}', position={1}, passed={2}, testDone={3}",
|
||||
test.Name, originalIndex + 1, result.Passed, result.TestDone);
|
||||
}
|
||||
|
||||
private void CompleteTest(Test test, int processedReaders, bool allPassed)
|
||||
{
|
||||
if (test == null || ProcessData.BatchRslts == null)
|
||||
return;
|
||||
|
||||
TestRslt testResult = ProcessData.BatchRslts.GetTestRslt(test.Name, 0);
|
||||
if (testResult == null)
|
||||
return;
|
||||
|
||||
if (testResult.StartTime == DateTime.MinValue)
|
||||
testResult.StartTime = DateTime.Now;
|
||||
testResult.EndTime = DateTime.Now;
|
||||
testResult.TestDone = true;
|
||||
testResult.Remark = processedReaders == 0
|
||||
? "No ALLY reader is configured for this Meters Path."
|
||||
: allPassed ? "ALLY communication completed." : "ALLY communication failed.";
|
||||
|
||||
log.InfoFormat("ALLY_TEST_RESULT_COMPLETED: test='{0}', readers={1}, resultDone={2}, remark='{3}'",
|
||||
test.Name, processedReaders, testResult.TestDone, testResult.Remark);
|
||||
|
||||
TBF.UiBridge.Bridge.OnTestProgress(this,
|
||||
new TBF.UiBridge.TestProgressEventArgs(test, Progress.Completed));
|
||||
TBF.UiBridge.Bridge.OnTestCompleted(this,
|
||||
new TBF.UiBridge.TestCompletedEventArgs(test.Name, testResult));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,12 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
|
||||
ParentFrom.ActivityLabel.Text = item.Text;
|
||||
string command = item.Tag.ToString();
|
||||
IList<ISmartReader> heads = ParentFrom.Heads;
|
||||
ParentFrom.Log.InfoFormat(
|
||||
"SMART_COMM_MANUAL_COMMAND_START: family='{0}', command='{1}', caption='{2}', heads={3}",
|
||||
TypeIdentificatorName(),
|
||||
command,
|
||||
item.Text,
|
||||
heads == null ? 0 : heads.Count);
|
||||
List<Task<string>> operations = heads
|
||||
.Select(head => head as TReader)
|
||||
.Select(reader => reader == null
|
||||
@ -69,7 +75,15 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
|
||||
|
||||
string[] results = await Task.WhenAll(operations);
|
||||
for (int index = 0; index < results.Length && index < ParentFrom.Messages.Length; index++)
|
||||
{
|
||||
ParentFrom.Messages[index].Text = results[index];
|
||||
ParentFrom.Log.InfoFormat(
|
||||
"SMART_COMM_MANUAL_COMMAND_RESULT: family='{0}', command='{1}', displayedRow={2}, result='{3}'",
|
||||
TypeIdentificatorName(),
|
||||
command,
|
||||
index + 1,
|
||||
results[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private string ExecuteSafely(TReader reader, string command)
|
||||
|
||||
@ -1646,6 +1646,7 @@
|
||||
<DependentUpon>AllyReaderManualTestCtrl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyCommandService.cs" />
|
||||
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFactoryUnsealData.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" />
|
||||
@ -1982,6 +1983,7 @@
|
||||
<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\AllyCommunicationSeq.cs" />
|
||||
<Compile Include="Rig\TestMethods\AllyCalibration\Factory.cs" />
|
||||
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethod.cs" />
|
||||
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodCfg.cs" />
|
||||
@ -2574,6 +2576,7 @@
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\ManualSmartCorrectionsBase.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\GenesisCorrections.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\AllyCorrections.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\AllyTestCorrections.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlASICCorrections.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlASICOpticalHeadTest.cs" />
|
||||
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlCorrections.cs" />
|
||||
@ -4548,6 +4551,7 @@
|
||||
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-cooling.png" />
|
||||
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-heating.png" />
|
||||
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-idle.png" />
|
||||
<Content Include="Rig\TestMethods\AllyCalibration\ALLY_OPTICAL_STREAM_SEQUENCE.md" />
|
||||
<Content Include="SampleConfig\config.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Common;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@ -97,24 +96,75 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessOpticalText_VolumeAndTimestampRollover_ProducesContinuousMeasurement()
|
||||
public void ProcessOpticalText_32BitVolumeRollover_ProducesContinuousMeasurementUsingHostElapsedTime()
|
||||
{
|
||||
AllyMeterReader reader = CreateReader(AllyMeterSize.FiveEighths);
|
||||
reader.Initialize();
|
||||
reader.Start();
|
||||
|
||||
string input =
|
||||
AllyOpticalTelegramFactory.Create(10, 0xFFFFF0, 0xFFFFFF00) +
|
||||
AllyOpticalTelegramFactory.Create(11, 0x000010, 0x00000100);
|
||||
InvokeProcessOpticalText(reader, input);
|
||||
AllyOpticalTelegramFactory.CreateC6(10, 0, 0, 10, 0xFFFFFFF0, 0, 0, 0, 0, 0, 0, 0) +
|
||||
AllyOpticalTelegramFactory.CreateC6(11, 0, 0, 11, 0x00000010, 0, 0, 0, 0, 0, 0, 0);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
input,
|
||||
new DateTime(2026, 9, 16, 8, 0, 0, DateTimeKind.Utc));
|
||||
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);
|
||||
Assert.AreEqual(32D / 4000D, reader.WMVolume, 1E-9);
|
||||
// ALLY C6 does not carry an ASIC timestamp; elapsed time is based on the host receipt time.
|
||||
Assert.IsTrue(reader.TimestampSecEnd >= reader.TimestampSecStart);
|
||||
Assert.AreEqual(8, reader.WMPulses);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessOpticalText_AutoDetect_TransfersDecodedStartAndEndVolumes()
|
||||
{
|
||||
AllyMeterReader reader = CreateReader(AllyMeterSize.AutoDetect);
|
||||
reader.Initialize();
|
||||
reader.Start();
|
||||
|
||||
DateTime start = new DateTime(2026, 9, 15, 10, 40, 0, DateTimeKind.Utc);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
AllyOpticalTelegramFactory.CreateC6(1, 0, 0, 8, 100U, 0, 0, 0, 0, 0, 0, 0),
|
||||
start);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
AllyOpticalTelegramFactory.CreateC6(2, 0, 0, 8, 140U, 0, 0, 0, 0, 0, 0, 0),
|
||||
start.AddSeconds(3));
|
||||
reader.Stop();
|
||||
|
||||
Assert.IsTrue(reader.IsOpticalVolumeConversionConfigured);
|
||||
Assert.IsFalse(reader.NoSamples);
|
||||
Assert.AreEqual(100D / 4000D, reader.BeginWMState, 1E-12);
|
||||
Assert.AreEqual(140D / 4000D, reader.EndWMState, 1E-12);
|
||||
Assert.AreEqual(40D / 4000D, reader.WMVolume, 1E-12);
|
||||
Assert.AreEqual(3D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-12);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ProcessOpticalText_VolumeAndSequenceRollover_KeepMeasurementAndHostTimeContinuous()
|
||||
{
|
||||
AllyMeterReader reader = CreateReader(AllyMeterSize.AutoDetect);
|
||||
reader.Initialize();
|
||||
reader.Start();
|
||||
|
||||
DateTime beforeMidnight = new DateTime(2026, 12, 31, 23, 59, 59, 900, DateTimeKind.Utc);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
AllyOpticalTelegramFactory.CreateC6(0xFF, 0, 0, 4, 0xFFFFFFFEU, 0, 0, 0, 0, 0, 0, 0),
|
||||
beforeMidnight);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
AllyOpticalTelegramFactory.CreateC6(0x00, 0, 0, 4, 0x00000002U, 0, 0, 0, 0, 0, 0, 0),
|
||||
beforeMidnight.AddMilliseconds(200));
|
||||
reader.Stop();
|
||||
|
||||
Assert.AreEqual((byte)0xFF, reader.OpticalSamples[0].Sequence);
|
||||
Assert.AreEqual((byte)0x00, reader.OpticalSamples[1].Sequence);
|
||||
// The delta is calculated from two ~1,073,741 l double values after
|
||||
// 32-bit rollover. Allow binary floating-point cancellation far below
|
||||
// a single C6 quarter-millilitre unit (0.00025 l).
|
||||
Assert.AreEqual(4D / 4000D, reader.WMVolume, 1E-9);
|
||||
Assert.AreEqual(0.2D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -172,14 +222,5 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Common;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using TBF.Rig.RegisterReaders.AllyReader;
|
||||
|
||||
namespace TBFTests.Rig.RegisterReaders.AllyReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Regression coverage for C6 telegrams captured from an ALLY optical sensor.
|
||||
/// The test output deliberately documents the decoded values and their units.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
[TestSubject(typeof(AllyOpticalSample))]
|
||||
public class AllyOpticalRealExamplesTest
|
||||
{
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_RealC6Examples_ParsesValuesAndDocumentsUnits()
|
||||
{
|
||||
AllyOpticalSample first = Parse("17\tC6\taQcAABgQAABgEgAA8AAAALbo//8A6AMAAA==\t8513\r\n");
|
||||
Assert.AreEqual((byte)0x17, first.Sequence);
|
||||
Assert.AreEqual(1897, first.RawAdc);
|
||||
Assert.AreEqual((short)4120, first.LastField);
|
||||
Assert.AreEqual((short)0, first.RawFlow);
|
||||
Assert.AreEqual(0D, first.FlowMillilitersPerSecond, 1E-12);
|
||||
Assert.AreEqual(4704U, first.RawVolume);
|
||||
Assert.AreEqual(1.176D, first.VolumeLiters, 1E-12);
|
||||
Assert.AreEqual((ushort)240, first.FlipPeriod);
|
||||
Assert.AreEqual((ushort)0, first.VinfStart);
|
||||
Assert.AreEqual((ushort)59574, first.VinfEnd);
|
||||
Assert.AreEqual((short)-1, first.ElectrodeDelta);
|
||||
Assert.AreEqual((ushort)59392, first.Impedance);
|
||||
Assert.AreEqual((byte)3, first.FieldDriveTime);
|
||||
Assert.AreEqual((byte)0, first.Flags);
|
||||
Assert.IsFalse(first.IsEmptyPipe);
|
||||
Assert.IsFalse(first.IsFastHptc);
|
||||
Assert.AreEqual(0x8513, first.PacketChecksum);
|
||||
CollectionAssert.AreEqual(new byte[] { 0 }, first.ExtensionBytes);
|
||||
|
||||
AllyOpticalSample second = Parse("13\tC6\tSAoAAJ4OAABOEgAA8AAAAMby//8A6AMAAA==\t4A31\r\n");
|
||||
Assert.AreEqual((byte)0x13, second.Sequence);
|
||||
Assert.AreEqual(2632, second.RawAdc);
|
||||
Assert.AreEqual((short)3742, second.LastField);
|
||||
Assert.AreEqual((short)0, second.RawFlow);
|
||||
Assert.AreEqual(0D, second.FlowMillilitersPerSecond, 1E-12);
|
||||
Assert.AreEqual(4686U, second.RawVolume);
|
||||
Assert.AreEqual(1.1715D, second.VolumeLiters, 1E-12);
|
||||
Assert.AreEqual((ushort)240, second.FlipPeriod);
|
||||
Assert.AreEqual((ushort)62150, second.VinfEnd);
|
||||
Assert.AreEqual((short)-1, second.ElectrodeDelta);
|
||||
Assert.AreEqual((ushort)59392, second.Impedance);
|
||||
Assert.AreEqual((byte)3, second.FieldDriveTime);
|
||||
Assert.AreEqual((byte)0, second.Flags);
|
||||
Assert.AreEqual(0x4A31, second.PacketChecksum);
|
||||
CollectionAssert.AreEqual(new byte[] { 0 }, second.ExtensionBytes);
|
||||
|
||||
WriteDecodedValues("example 1", first);
|
||||
WriteDecodedValues("example 2", second);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Decode_TenC6Samples_AcrossSequenceTimeAndVolumeRollover_ReportsStartEndAndDelta()
|
||||
{
|
||||
AllyReaderCfg cfg = new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory())
|
||||
{
|
||||
DebugLevel = DebugMode.Simulate,
|
||||
ConfiguredMeterSize = AllyMeterSize.AutoDetect,
|
||||
Name = "AllyRolloverTest"
|
||||
};
|
||||
AllyMeterReader reader = new AllyMeterReader(cfg);
|
||||
reader.Initialize();
|
||||
reader.Start();
|
||||
|
||||
// ALLY C6 has no device timestamp. The time rollover therefore means
|
||||
// the host receipt time crossing midnight, while the 32-bit C6 volume
|
||||
// accumulator wraps from FFFFFFFF to 00000000.
|
||||
DateTime firstReceivedAt = new DateTime(2026, 12, 31, 23, 59, 57, 750, DateTimeKind.Utc);
|
||||
const uint firstRawVolume = 0xFFFFFFF0U;
|
||||
const uint rawIncrement = 4U; // one millilitre per sample
|
||||
|
||||
for (int index = 0; index < 10; index++)
|
||||
{
|
||||
uint rawVolume = unchecked(firstRawVolume + rawIncrement * (uint)index);
|
||||
byte sequence = unchecked((byte)(0xF8 + index));
|
||||
DateTime receivedAt = firstReceivedAt.AddMilliseconds(500 * index);
|
||||
string telegram = AllyOpticalTelegramFactory.CreateC6(
|
||||
sequence, 1000 + index, 0, 4, rawVolume, 240, 0, 0, -1, 59392, 3, 0);
|
||||
|
||||
reader.ProcessOpticalTextForTest(telegram, receivedAt);
|
||||
AllyOpticalSample decoded = reader.OpticalSamples[index];
|
||||
WriteDecodedValues("rollover sample " + (index + 1), decoded);
|
||||
}
|
||||
|
||||
reader.Stop();
|
||||
|
||||
Assert.AreEqual(10, reader.OpticalSamples.Count);
|
||||
Assert.AreEqual((byte)0xF8, reader.OpticalSamples[0].Sequence);
|
||||
Assert.AreEqual((byte)0x01, reader.OpticalSamples[9].Sequence);
|
||||
Assert.AreEqual(firstRawVolume, reader.OpticalSamples[0].RawVolume);
|
||||
Assert.AreEqual(0x00000014U, reader.OpticalSamples[9].RawVolume);
|
||||
Assert.AreEqual(9D * rawIncrement / 4000D, reader.WMVolume, 1E-9);
|
||||
Assert.AreEqual(4.5D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
|
||||
Assert.IsTrue(reader.OpticalSamples[5].ReceivedAtUtc.Date > firstReceivedAt.Date);
|
||||
|
||||
if (TestContext != null)
|
||||
{
|
||||
TestContext.WriteLine(
|
||||
"ALLY rollover result | samples={0}; start={1:F9} l; end={2:F9} l; delta={3:F9} l; elapsed={4:F3} s",
|
||||
reader.OpticalSamples.Count,
|
||||
reader.BeginWMState,
|
||||
reader.EndWMState,
|
||||
reader.WMVolume,
|
||||
reader.TimestampSecEnd - reader.TimestampSecStart);
|
||||
}
|
||||
}
|
||||
|
||||
private static AllyOpticalSample Parse(string telegram)
|
||||
{
|
||||
AllyOpticalSample sample;
|
||||
bool parsed = AllyOpticalSample.TryParse(
|
||||
telegram,
|
||||
new DateTime(2026, 9, 10, 8, 55, 52, DateTimeKind.Utc),
|
||||
out sample);
|
||||
Assert.IsTrue(parsed, "The captured C6 optical telegram must parse.");
|
||||
Assert.IsNotNull(sample);
|
||||
return sample;
|
||||
}
|
||||
|
||||
private void WriteDecodedValues(string name, AllyOpticalSample sample)
|
||||
{
|
||||
string output = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"ALLY C6 {0} | sequence=0x{1:X2}; ADC={2} raw counts; field={3} raw counts; flow={4} quarter-mL/s ({5:F3} mL/s); accumulator={6} quarter-mL ({7:F6} l); flipPeriod={8} raw ticks; VinfStart={9} raw; VinfEnd={10} raw; electrodeDelta={11} mV; impedance={12} raw; fieldDriveTime={13} us; flags=0x{14:X2}; emptyPipe={15}; fastHptc={16}; checksum=0x{17:X4} (preserved, not CRC-validated); extension={18}",
|
||||
name,
|
||||
sample.Sequence,
|
||||
sample.RawAdc,
|
||||
sample.LastField,
|
||||
sample.RawFlow,
|
||||
sample.FlowMillilitersPerSecond,
|
||||
sample.RawVolume,
|
||||
sample.VolumeLiters,
|
||||
sample.FlipPeriod,
|
||||
sample.VinfStart,
|
||||
sample.VinfEnd,
|
||||
sample.ElectrodeDelta,
|
||||
sample.Impedance,
|
||||
sample.FieldDriveTime,
|
||||
sample.Flags,
|
||||
sample.IsEmptyPipe,
|
||||
sample.IsFastHptc,
|
||||
sample.PacketChecksum,
|
||||
BitConverter.ToString(sample.ExtensionBytes));
|
||||
|
||||
if (TestContext != null)
|
||||
TestContext.WriteLine(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,56 +10,59 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
|
||||
public class AllyOpticalSampleTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void TryParse_ValidTelegram_ParsesFlowVolumeAndTimestamp()
|
||||
public void TryParse_RealC6Telegram_ParsesAllyMetrologyFields()
|
||||
{
|
||||
string telegram = AllyOpticalTelegramFactory.Create(-2, 0x123456, 0x89ABCDEF);
|
||||
DateTime receivedAt = new DateTime(2026, 8, 14, 10, 0, 0, DateTimeKind.Utc);
|
||||
const string telegram = "17\tC6\taQcAABgQAABgEgAA8AAAALbo//8A6AMAAA==\t8513\r\n";
|
||||
DateTime receivedAt = new DateTime(2026, 9, 10, 8, 55, 52, 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((byte)0x17, sample.Sequence);
|
||||
Assert.AreEqual((byte)0xC6, sample.PacketType);
|
||||
Assert.AreEqual(0x8513, sample.PacketChecksum);
|
||||
Assert.AreEqual(1897, sample.RawAdc);
|
||||
Assert.AreEqual((short)0x1018, sample.LastField);
|
||||
Assert.AreEqual(0x1260U, sample.RawVolume);
|
||||
Assert.AreEqual((ushort)0x00F0, sample.FlipPeriod);
|
||||
Assert.AreEqual((short)-1, sample.ElectrodeDelta);
|
||||
Assert.AreEqual(1, sample.ExtensionBytes.Length);
|
||||
Assert.AreEqual(receivedAt, sample.ReceivedAtUtc);
|
||||
Assert.AreEqual(telegram, sample.RawLine);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_PrefixedValidTelegram_UsesLastCompleteTelegram()
|
||||
public void TryParse_C6Telegram_ParsesFlags()
|
||||
{
|
||||
string telegram = AllyOpticalTelegramFactory.Create(1, 2, 3);
|
||||
string telegram = AllyOpticalTelegramFactory.CreateC6(1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0x07);
|
||||
|
||||
AllyOpticalSample sample;
|
||||
bool result = AllyOpticalSample.TryParse("noise" + telegram, DateTime.UtcNow, out sample);
|
||||
bool result = AllyOpticalSample.TryParse(telegram, DateTime.UtcNow, out sample);
|
||||
|
||||
Assert.IsTrue(result);
|
||||
Assert.AreEqual(2U, sample.RawVolume);
|
||||
Assert.AreEqual(3U, sample.RawTimestamp);
|
||||
Assert.AreEqual(telegram, sample.RawLine);
|
||||
Assert.IsTrue(sample.IsLowFlow);
|
||||
Assert.IsTrue(sample.IsEmptyPipe);
|
||||
Assert.IsTrue(sample.IsFastHptc);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_CorruptChecksum_ReturnsFalse()
|
||||
public void TryParse_NonMetrologyPacket_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);
|
||||
bool result = AllyOpticalSample.TryParse("12\tC2\tOwIAAFAiAAAAAAAAACBYDgwOAACXylAS\t7947\r\n", DateTime.UtcNow, out sample);
|
||||
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsNull(sample);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TryParse_InvalidStructure_ReturnsFalse()
|
||||
public void TryParse_InvalidBase64_ReturnsFalse()
|
||||
{
|
||||
AllyOpticalSample sample;
|
||||
bool result = AllyOpticalSample.TryParse(
|
||||
"000000 0000 0000 000001 0000 00000001 00\r\n",
|
||||
"17\tC6\tinvalid\t8513\r\n",
|
||||
DateTime.UtcNow,
|
||||
out sample);
|
||||
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
using System.Globalization;
|
||||
using System;
|
||||
|
||||
namespace TBFTests.Rig.RegisterReaders.AllyReader
|
||||
{
|
||||
internal static class AllyOpticalTelegramFactory
|
||||
{
|
||||
public static string Create(short rawFlow, uint rawVolume, uint rawTimestamp)
|
||||
public static string CreateC6(byte sequence, int rawAdc, short lastField, short rawFlow, uint rawVolume,
|
||||
ushort flipPeriod, ushort vinfStart, ushort vinfEnd, short electrodeDelta, ushort impedance,
|
||||
byte fieldDriveTime, byte flags, byte extension = 0)
|
||||
{
|
||||
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";
|
||||
byte[] payload = new byte[25];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(rawAdc), 0, payload, 0, 4);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(lastField), 0, payload, 4, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(rawFlow), 0, payload, 6, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(rawVolume), 0, payload, 8, 4);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(flipPeriod), 0, payload, 12, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(vinfStart), 0, payload, 14, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(vinfEnd), 0, payload, 16, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(electrodeDelta), 0, payload, 18, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(impedance), 0, payload, 20, 2);
|
||||
payload[22] = fieldDriveTime;
|
||||
payload[23] = flags;
|
||||
payload[24] = extension;
|
||||
return sequence.ToString("X2") + "\tC6\t" + Convert.ToBase64String(payload) + "\t0000\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +81,93 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x60, 0xC2, 0x0D);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OpticalVerificationOutput_UnsealedMeter_UsesDocumentedStartAndStopSequence()
|
||||
{
|
||||
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0x00));
|
||||
|
||||
service.StartOpticalVerificationOutput(TimeoutMs);
|
||||
|
||||
Assert.AreEqual(5, transport.Requests.Count);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D }, transport.Requests[0]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0x1E, 0x00, 0x02, 0x0D }, transport.Requests[1]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x01, 0x0D }, transport.Requests[2]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0x1A, 0x09, 0x0D }, transport.Requests[3]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0xFD, 0x60, 0xC2, 0x0D }, transport.Requests[4]);
|
||||
|
||||
transport.Requests.Clear();
|
||||
service.StopOpticalVerificationOutput(TimeoutMs);
|
||||
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0xFD, 0x60, 0x00, 0x0D }, transport.Requests[0]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0x1A, 0x02, 0x0D }, transport.Requests[1]);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x00, 0x0D }, transport.Requests[2]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OpticalVerificationOutput_SealedMeter_StopsBeforeAnyStateChangingCommand()
|
||||
{
|
||||
transport.Response = AllyResponseFactory.CreateSuccess(0x01);
|
||||
|
||||
try
|
||||
{
|
||||
service.StartOpticalVerificationOutput(TimeoutMs);
|
||||
Assert.Fail("Expected the factory-seal guard to reject optical output activation.");
|
||||
}
|
||||
catch (InvalidOperationException exception)
|
||||
{
|
||||
StringAssert.Contains(exception.Message, "factory sealed");
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, transport.Requests.Count);
|
||||
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D }, transport.Requests[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FactorySealCommands_ReadMeterSpecificInputsAndBuildProvidedVerificationBenchFrame()
|
||||
{
|
||||
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0x01));
|
||||
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("Customer Text 123456"));
|
||||
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("RB252601C144"));
|
||||
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("00001000"));
|
||||
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0xD0, 0xC5, 0x5D, 0x00));
|
||||
|
||||
AllyFactoryUnsealData data = service.ReadFactoryUnsealData(TimeoutMs);
|
||||
|
||||
Assert.IsTrue(data.IsSealed);
|
||||
Assert.AreEqual("RB252601C144", data.FactoryId);
|
||||
Assert.AreEqual("Customer Text 123456", data.ProgrammableText);
|
||||
Assert.AreEqual("00001000", data.ReadingPreset);
|
||||
Assert.AreEqual((uint)6145488, data.SecondsActive);
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0xC5, 0x5D, 0xF1, 0xF8, 0x53, 0x45, 0x09, 0x00 },
|
||||
data.Credential);
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D },
|
||||
transport.Requests[0]);
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x53, 0x57, 0x05, 0x07, 0x0D },
|
||||
transport.Requests[1]);
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x53, 0x57, 0x05, 0x01, 0x0D },
|
||||
transport.Requests[2]);
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x53, 0x57, 0x05, 0x13, 0x0D },
|
||||
transport.Requests[3]);
|
||||
CollectionAssert.AreEqual(
|
||||
new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x3D, 0x0D },
|
||||
transport.Requests[4]);
|
||||
|
||||
service.UnsealFactory(data, TimeoutMs);
|
||||
AssertRequest(
|
||||
0x53, 0x57, 0x0F, 0xFD, 0x64,
|
||||
0x00, 0xC5, 0x5D, 0xF1, 0xF8, 0x53, 0x45, 0x09, 0x00,
|
||||
0x0D);
|
||||
|
||||
service.SealFactory(TimeoutMs);
|
||||
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x64, 0x01, 0x0D);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalibrationFactor_WriteAndRead_UsesFactorTimes40Point96Encoding()
|
||||
{
|
||||
|
||||
@ -12,25 +12,27 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
{
|
||||
/// <summary>
|
||||
/// Direct hardware smoke test modelled after IperlHatIntegrationTests.
|
||||
/// COM3 is the ALLY Touch-Read connection and COM4 is the optical source.
|
||||
/// COM12 is the ALLY Touch-Read connection and COM13 is the optical source.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
[TestCategory("HardwareIntegration")]
|
||||
[DoNotParallelize]
|
||||
public class AllyIntegrationTests
|
||||
{
|
||||
private const string CommandComPort = "COM3";
|
||||
private const string OpticalComPort = "COM4";
|
||||
private const string CommandComPort = "COM12";
|
||||
private const string OpticalComPort = "COM13";
|
||||
private const int CommandBaudRate = 2400;
|
||||
private const int OpticalBaudRate = 9600;
|
||||
private const int OpticalBaudRate = 38400;
|
||||
private const int ReadTimeoutMs = 5000;
|
||||
private const int OpticalReadSeconds = 10;
|
||||
private const byte ActiveMeterMode = 0x02;
|
||||
private const byte InitialMeterMode = 0x09;
|
||||
private const byte DiagnosticLedCalibrationMode = 0xC2;
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Hardware")]
|
||||
[TestCategory("Serial")]
|
||||
public void Serial_ReadFactoryId_SetActive_ReadOptical_SetInitial()
|
||||
public void Integration_Serial_ReadFactoryId_PrepareOptical_ReadOptical_SetActive()
|
||||
{
|
||||
Console.WriteLine(
|
||||
"ALLY integration test: command={0}/{1}, optical={2}/{3}",
|
||||
@ -52,7 +54,9 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
|
||||
Console.WriteLine("OPEN command port " + CommandComPort);
|
||||
commandPort.Open();
|
||||
bool activeModeCommandAttempted = false;
|
||||
bool initialModeCommandAttempted = false;
|
||||
bool diagnosticLedCommandAttempted = false;
|
||||
bool spreadSpectrumDisableAttempted = false;
|
||||
Exception testFailure = null;
|
||||
|
||||
try
|
||||
@ -71,13 +75,38 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
string.IsNullOrWhiteSpace(serialNumber),
|
||||
"ViewFactoryId returned an empty manufacturing serial number.");
|
||||
|
||||
byte[] activeModeRequest = new AllyFrameBuilder()
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithCommand(AllyCommand.SetValvePosition)
|
||||
.WithBytes(0x00, 0x02)
|
||||
.BuildBytes(),
|
||||
"Open valve for calibration");
|
||||
|
||||
spreadSpectrumDisableAttempted = true;
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.Configuration)
|
||||
.WithBytes(0x06, 0x01)
|
||||
.BuildBytes(),
|
||||
"Disable Spread Spectrum");
|
||||
|
||||
byte[] initialModeRequest = new AllyFrameBuilder()
|
||||
.WithCommand(AllyCommand.SetMeterMode)
|
||||
.WithByte(ActiveMeterMode)
|
||||
.WithByte(InitialMeterMode)
|
||||
.BuildBytes();
|
||||
activeModeCommandAttempted = true;
|
||||
SendRequest(commandPort, activeModeRequest, "SetMeterMode Active 0x02");
|
||||
Console.WriteLine("STEP PASS Active meter mode 0x02 is acknowledged.");
|
||||
initialModeCommandAttempted = true;
|
||||
SendRequest(commandPort, initialModeRequest, "SetMeterMode Initial calibration 0x09");
|
||||
|
||||
diagnosticLedCommandAttempted = true;
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
|
||||
.WithByte(DiagnosticLedCalibrationMode)
|
||||
.BuildBytes(),
|
||||
"Set diagnostic LED calibration 0xC2");
|
||||
|
||||
int parsedSamples = ReadOpticalSamples();
|
||||
Assert.IsTrue(
|
||||
@ -93,22 +122,44 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Attempt the safe state even when the active command timed out: the
|
||||
// meter may have accepted it while its response was lost.
|
||||
if (activeModeCommandAttempted && commandPort.IsOpen)
|
||||
if (commandPort.IsOpen)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] initialModeRequest = new AllyFrameBuilder()
|
||||
.WithCommand(AllyCommand.SetMeterMode)
|
||||
.WithByte(InitialMeterMode)
|
||||
.BuildBytes();
|
||||
SendRequest(commandPort, initialModeRequest, "SetMeterMode Initial 0x09");
|
||||
Console.WriteLine("STEP PASS Initial meter mode 0x09 is acknowledged.");
|
||||
if (diagnosticLedCommandAttempted)
|
||||
{
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
|
||||
.WithByte(0x00)
|
||||
.BuildBytes(),
|
||||
"Set diagnostic LED off 0x00");
|
||||
}
|
||||
if (initialModeCommandAttempted)
|
||||
{
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithCommand(AllyCommand.SetMeterMode)
|
||||
.WithByte(ActiveMeterMode)
|
||||
.BuildBytes(),
|
||||
"SetMeterMode Active 0x02");
|
||||
}
|
||||
if (spreadSpectrumDisableAttempted)
|
||||
{
|
||||
SendRequest(
|
||||
commandPort,
|
||||
new AllyFrameBuilder()
|
||||
.WithDeviceCommand(AllyDeviceCommand.Configuration)
|
||||
.WithBytes(0x06, 0x00)
|
||||
.BuildBytes(),
|
||||
"Enable Spread Spectrum");
|
||||
}
|
||||
}
|
||||
catch (Exception restoreException)
|
||||
{
|
||||
Console.WriteLine("RESTORE FAIL Initial mode 0x09: " + restoreException);
|
||||
Console.WriteLine("RESTORE FAIL calibration cleanup: " + restoreException);
|
||||
if (testFailure == null)
|
||||
throw;
|
||||
}
|
||||
@ -238,11 +289,14 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
|
||||
parsedSamples++;
|
||||
Console.WriteLine(
|
||||
"OPTO PARSE PASS: sample={0}, flow={1}, rawVolume={2}, rawTimestamp={3}",
|
||||
"OPTO PARSE PASS: sample={0}, type=0x{1:X2}, sequence=0x{2:X2}, flow={3}, rawVolume={4}, emptyPipe={5}, fastHptc={6}",
|
||||
parsedSamples,
|
||||
sample.PacketType,
|
||||
sample.Sequence,
|
||||
sample.RawFlow,
|
||||
sample.RawVolume,
|
||||
sample.RawTimestamp);
|
||||
sample.IsEmptyPipe,
|
||||
sample.IsFastHptc);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using TBF.Rig.RegisterReaders.AllyReader.Communication;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
{
|
||||
@ -11,6 +12,13 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
public byte[] Response { get; set; }
|
||||
public byte[] LastRequest { get; private set; }
|
||||
public int LastTimeoutMs { get; private set; }
|
||||
public IList<byte[]> Requests { get; private set; } = new List<byte[]>();
|
||||
private readonly Queue<byte[]> responses = new Queue<byte[]>();
|
||||
|
||||
public void QueueResponse(byte[] response)
|
||||
{
|
||||
responses.Enqueue(response);
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
@ -23,7 +31,8 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
|
||||
SendAndWaitCallCount++;
|
||||
LastRequest = request;
|
||||
LastTimeoutMs = timeoutMs;
|
||||
return Response;
|
||||
Requests.Add(request);
|
||||
return responses.Count > 0 ? responses.Dequeue() : Response;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@ -15,6 +15,7 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
public int OpticalCaptureSeconds { get; private set; }
|
||||
public int ActiveModeSettleMs { get; private set; }
|
||||
public AllyMeterSize MeterSize { get; private set; }
|
||||
public bool UnsealMeterBeforeOpticalTest { get; private set; }
|
||||
|
||||
public string CommandPortName { get { return "COM" + CommandPortNumber; } }
|
||||
public string OpticalPortName { get { return "COM" + OpticalPortNumber; } }
|
||||
@ -24,13 +25,14 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
return new AllyHardwareIntegrationSettings
|
||||
{
|
||||
Enabled = ParseEnabled(Environment.GetEnvironmentVariable("ALLY_HW_TESTS")),
|
||||
CommandPortNumber = ParsePort("ALLY_COMMAND_PORT", "COM3"),
|
||||
OpticalPortNumber = ParsePort("ALLY_OPTICAL_PORT", "COM4"),
|
||||
CommandPortNumber = ParsePort("ALLY_COMMAND_PORT", "COM12"),
|
||||
OpticalPortNumber = ParsePort("ALLY_OPTICAL_PORT", "COM13"),
|
||||
CommandBaudRate = ParsePositiveInt("ALLY_COMMAND_BAUD", 2400),
|
||||
OpticalBaudRate = ParsePositiveInt("ALLY_OPTICAL_BAUD", 9600),
|
||||
OpticalBaudRate = ParsePositiveInt("ALLY_OPTICAL_BAUD", 38400),
|
||||
CommandTimeoutMs = ParsePositiveInt("ALLY_COMMAND_TIMEOUT_MS", 5000),
|
||||
OpticalCaptureSeconds = ParsePositiveInt("ALLY_OPTICAL_CAPTURE_SECONDS", 10),
|
||||
ActiveModeSettleMs = ParsePositiveInt("ALLY_ACTIVE_SETTLE_MS", 1000),
|
||||
UnsealMeterBeforeOpticalTest = ParseEnabled(Environment.GetEnvironmentVariable("ALLY_UNSEAL_METER")),
|
||||
MeterSize = ParseMeterSize(Environment.GetEnvironmentVariable("ALLY_METER_SIZE"))
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ 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;
|
||||
@ -17,9 +16,6 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
[DoNotParallelize]
|
||||
public class AllyHardwareIntegrationTest
|
||||
{
|
||||
private const byte ActiveMeterMode = 0x02;
|
||||
private const byte InitialMeterMode = 0x09;
|
||||
|
||||
private AllyHardwareIntegrationSettings settings;
|
||||
private AllyIntegrationTestLogger logger;
|
||||
|
||||
@ -32,14 +28,15 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
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",
|
||||
"Configuration | command={0}/{1}, optical={2}/{3}, size={4}, timeout={5} ms, capture={6} s, unseal={7}",
|
||||
settings.CommandPortName,
|
||||
settings.CommandBaudRate,
|
||||
settings.OpticalPortName,
|
||||
settings.OpticalBaudRate,
|
||||
settings.MeterSize,
|
||||
settings.CommandTimeoutMs,
|
||||
settings.OpticalCaptureSeconds));
|
||||
settings.OpticalCaptureSeconds,
|
||||
settings.UnsealMeterBeforeOpticalTest));
|
||||
|
||||
if (!settings.Enabled)
|
||||
{
|
||||
@ -78,7 +75,10 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
value.TouchReadVersion,
|
||||
value.DeviceType,
|
||||
value.FirmwareVersion));
|
||||
Assert.AreEqual("SWM003", version.DeviceType, "The connected device is not an ALLY meter.");
|
||||
Assert.AreEqual<string>(
|
||||
"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");
|
||||
@ -92,46 +92,8 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
}
|
||||
}
|
||||
|
||||
[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();
|
||||
@ -139,21 +101,50 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
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);
|
||||
ReadSerialNumberAndFirmwareRevision(reader);
|
||||
EnsureMeterIsUnsealed(reader);
|
||||
logger.Step("Start ALLY optical verification stream and open " + settings.OpticalPortName,
|
||||
() => reader.StartOpticalVerificationStream(settings.CommandTimeoutMs));
|
||||
streamStarted = true;
|
||||
CaptureThroughReader(reader);
|
||||
|
||||
IReadOnlyList<AllyOpticalSample> samples = reader.OpticalSamples;
|
||||
Assert.IsTrue(samples.Count > 0, "No valid ALLY optical telegram was parsed.");
|
||||
logger.Log("OPTO CAPTURE | PASS | parsed samples=" + samples.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (streamStarted)
|
||||
logger.Step("Stop ALLY optical verification stream and close " + settings.OpticalPortName,
|
||||
() => reader.StopOpticalVerificationStream(settings.CommandTimeoutMs));
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.EndSession();
|
||||
logger.Log("SESSION | optical port closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Complex_ReadSerialCaptureParseAndStop_LogsWholeWorkflow()
|
||||
{
|
||||
RequirePorts(settings.CommandPortName, settings.OpticalPortName);
|
||||
AllyMeterReader reader = CreateReader();
|
||||
bool streamStarted = false;
|
||||
reader.StartSession();
|
||||
try
|
||||
{
|
||||
string serialNumber = ReadSerialNumberAndFirmwareRevision(reader);
|
||||
EnsureMeterIsUnsealed(reader);
|
||||
logger.Step("Start ALLY optical verification stream and open " + settings.OpticalPortName,
|
||||
() => reader.StartOpticalVerificationStream(settings.CommandTimeoutMs));
|
||||
streamStarted = true;
|
||||
logger.Step("Start optical measurement interval", reader.Start);
|
||||
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.");
|
||||
@ -170,21 +161,13 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
try
|
||||
{
|
||||
if (streamStarted)
|
||||
logger.Step("Stop optical measurement and close COM4", reader.Stop);
|
||||
logger.Step("Stop ALLY optical verification stream and close " + settings.OpticalPortName,
|
||||
() => reader.StopOpticalVerificationStream(settings.CommandTimeoutMs));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Step(
|
||||
"Restore initial meter mode 0x09",
|
||||
() => reader.SetMeterMode(InitialMeterMode, settings.CommandTimeoutMs));
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.EndSession();
|
||||
logger.Log("SESSION | all ports closed");
|
||||
}
|
||||
reader.EndSession();
|
||||
logger.Log("SESSION | all ports closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,69 +179,62 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
return reader;
|
||||
}
|
||||
|
||||
private IList<AllyOpticalSample> CaptureOpticalTelegramsDirectly()
|
||||
private string ReadSerialNumberAndFirmwareRevision(AllyMeterReader reader)
|
||||
{
|
||||
List<AllyOpticalSample> samples = new List<AllyOpticalSample>();
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
int parsedLines = 0;
|
||||
int rejectedLines = 0;
|
||||
string serialNumber = logger.Step(
|
||||
"Read manufacturing serial number",
|
||||
() => reader.ReadSerialNumber(settings.CommandTimeoutMs));
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(serialNumber), "The manufacturing serial number is empty.");
|
||||
|
||||
using (SerialPort port = new SerialPort(
|
||||
settings.OpticalPortName,
|
||||
settings.OpticalBaudRate,
|
||||
Parity.None,
|
||||
8,
|
||||
StopBits.One))
|
||||
logger.Step(
|
||||
"Read firmware revision",
|
||||
() => reader.ReadVersionAndType(settings.CommandTimeoutMs),
|
||||
value => string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"raw={0}; type={1}; firmware={2}; parse=PASS",
|
||||
value.RawValue,
|
||||
value.DeviceType,
|
||||
value.FirmwareVersion));
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
private void EnsureMeterIsUnsealed(AllyMeterReader reader)
|
||||
{
|
||||
bool isSealed = logger.Step("View factory seal", () => reader.IsFactorySealed(settings.CommandTimeoutMs),
|
||||
value => value ? "sealed" : "unsealed");
|
||||
if (!isSealed)
|
||||
{
|
||||
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");
|
||||
}
|
||||
logger.Log("FACTORY SEAL | meter is already unsealed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.Length > 0)
|
||||
if (!settings.UnsealMeterBeforeOpticalTest)
|
||||
{
|
||||
logger.Log("OPTO PARTIAL | trailing incomplete data: " + buffer.ToString()
|
||||
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"));
|
||||
const string message =
|
||||
"ALLY meter is factory sealed. Set ALLY_UNSEAL_METER=1 to explicitly authorize " +
|
||||
"the integration test to unseal this meter, then rerun the test.";
|
||||
logger.Log("BLOCKED | " + message);
|
||||
Assert.Inconclusive(message);
|
||||
return;
|
||||
}
|
||||
logger.Log(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"OPTO SUMMARY | valid={0}, rejected={1}, partialChars={2}",
|
||||
parsedLines,
|
||||
rejectedLines,
|
||||
buffer.Length));
|
||||
return samples;
|
||||
|
||||
AllyFactoryUnsealData data = logger.Step(
|
||||
"Read ALLY factory-unseal inputs",
|
||||
() => reader.ReadFactoryUnsealData(settings.CommandTimeoutMs),
|
||||
value => string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"factoryId={0}; programmableText={1}; readingPreset={2}; secondsActive={3}; credential=<redacted>",
|
||||
value.FactoryId,
|
||||
value.ProgrammableText,
|
||||
value.ReadingPreset,
|
||||
value.SecondsActive));
|
||||
|
||||
logger.Step("Unseal ALLY meter", () => reader.UnsealFactory(data, settings.CommandTimeoutMs));
|
||||
bool isSealedAfterUnseal = logger.Step("Verify factory seal after unseal",
|
||||
() => reader.IsFactorySealed(settings.CommandTimeoutMs),
|
||||
value => value ? "sealed" : "unsealed");
|
||||
Assert.IsFalse(isSealedAfterUnseal, "ALLY unseal command completed but the meter still reports sealed.");
|
||||
logger.Log("FACTORY SEAL | unseal completed and verified.");
|
||||
}
|
||||
|
||||
private void CaptureThroughReader(AllyMeterReader reader)
|
||||
@ -275,7 +251,10 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
{
|
||||
AllyOpticalSample parsedSample;
|
||||
bool parsed = AllyOpticalSample.TryParse(rawLine, DateTime.UtcNow, out parsedSample);
|
||||
logger.OpticalParse(rawLine, parsed, parsedSample);
|
||||
if (parsed || AllyOpticalSample.IsMetrologyPacket(rawLine))
|
||||
logger.OpticalParse(rawLine, parsed, parsedSample);
|
||||
else
|
||||
logger.Log("OPTO IGNORE | non-metrology telegram | " + rawLine.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"));
|
||||
lastRawLine = rawLine;
|
||||
}
|
||||
|
||||
@ -285,13 +264,28 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
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}",
|
||||
"OPTO SAMPLE | index={0}; type=0x{1:X2}; sequence=0x{2:X2}; ADC={3} raw counts; field={4} raw counts; flow={5} quarter-mL/s ({6:F3} mL/s); accumulator={7} quarter-mL ({8:F6} l); resultVolume={9:R} l; elapsed={10:F3} s; flipPeriod={11} raw ticks; VinfStart={12} raw; VinfEnd={13} raw; electrodeDelta={14} mV; impedance={15} raw; fieldDriveTime={16} us; flags=0x{17:X2}; emptyPipe={18}; fastHptc={19}; checksum=0x{20:X4} (not CRC-validated)",
|
||||
loggedSampleCount,
|
||||
sample.PacketType,
|
||||
sample.Sequence,
|
||||
sample.RawAdc,
|
||||
sample.LastField,
|
||||
sample.RawFlow,
|
||||
sample.FlowMillilitersPerSecond,
|
||||
sample.RawVolume,
|
||||
sample.RawTimestamp,
|
||||
sample.VolumeLiters,
|
||||
sample.ExtendedVolumeLiters,
|
||||
sample.ElapsedSeconds));
|
||||
sample.ElapsedSeconds,
|
||||
sample.FlipPeriod,
|
||||
sample.VinfStart,
|
||||
sample.VinfEnd,
|
||||
sample.ElectrodeDelta,
|
||||
sample.Impedance,
|
||||
sample.FieldDriveTime,
|
||||
sample.Flags,
|
||||
sample.IsEmptyPipe,
|
||||
sample.IsFastHptc,
|
||||
sample.PacketChecksum));
|
||||
}
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
@ -313,19 +307,5 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,10 +64,25 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
|
||||
return;
|
||||
}
|
||||
|
||||
Log(string.Format( CultureInfo.InvariantCulture, "OPTO PARSE | PASS | flow={0}, volume=0x{1:X6} ({1}), timestamp=0x{2:X8} ({2})",
|
||||
Log(string.Format( CultureInfo.InvariantCulture, "OPTO PARSE | PASS | type=0x{0:X2}, sequence=0x{1:X2}; ADC={2} raw counts; field={3} raw counts; flow={4} quarter-mL/s ({5:F3} mL/s); accumulator={6} quarter-mL ({7:F6} l); flipPeriod={8} raw ticks; VinfStart={9} raw; VinfEnd={10} raw; electrodeDelta={11} mV; impedance={12} raw; fieldDriveTime={13} us; flags=0x{14:X2}; emptyPipe={15}; fastHptc={16}; checksum=0x{17:X4} (not CRC-validated)",
|
||||
sample.PacketType,
|
||||
sample.Sequence,
|
||||
sample.RawAdc,
|
||||
sample.LastField,
|
||||
sample.RawFlow,
|
||||
sample.FlowMillilitersPerSecond,
|
||||
sample.RawVolume,
|
||||
sample.RawTimestamp));
|
||||
sample.VolumeLiters,
|
||||
sample.FlipPeriod,
|
||||
sample.VinfStart,
|
||||
sample.VinfEnd,
|
||||
sample.ElectrodeDelta,
|
||||
sample.Impedance,
|
||||
sample.FieldDriveTime,
|
||||
sample.Flags,
|
||||
sample.IsEmptyPipe,
|
||||
sample.IsFastHptc,
|
||||
sample.PacketChecksum));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using TBF.Rig.RegisterReaders.AllyReader;
|
||||
using TBF.Rig.TestMethods.AllyCalibration;
|
||||
|
||||
namespace TBFTests.Rig.TestMethods.AllyCalibration
|
||||
{
|
||||
/// <summary>
|
||||
/// Component-level integration coverage of Read PCB, start optical output,
|
||||
/// C6 parsing and cleanup without serial hardware.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
[TestSubject(typeof(AllyCalibrationSeq))]
|
||||
public class AllyOpticalWorkflowIntegrationTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ConfiguredWorkflow_FakeAllyReader_ReadPcbStartReadOpticalAndStop_TransfersMeterStates()
|
||||
{
|
||||
AllyMeterReader reader = CreateFakeReader();
|
||||
TestMethodCfg cfg = new TestMethodCfg(new TBF.Rig.TestMethods.AllyCalibration.Factory());
|
||||
TestMethodParams parameters = new TestMethodParams(true);
|
||||
string message;
|
||||
|
||||
parameters.Activity = AllyCalibrationActivityNames.ReadSerialNumber;
|
||||
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
|
||||
Assert.AreEqual("ALLY-SIMULATED", reader.SerialNr);
|
||||
|
||||
parameters.Activity = AllyCalibrationActivityNames.UnsealAndStartOpticalStream;
|
||||
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
|
||||
|
||||
reader.Start();
|
||||
DateTime start = new DateTime(2026, 9, 15, 10, 40, 0, DateTimeKind.Utc);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
TBFTests.Rig.RegisterReaders.AllyReader.AllyOpticalTelegramFactory.CreateC6(0x10, 0, 0, 12, 8000U, 0, 0, 0, 0, 0, 0, 0),
|
||||
start);
|
||||
reader.ProcessOpticalTextForTest(
|
||||
TBFTests.Rig.RegisterReaders.AllyReader.AllyOpticalTelegramFactory.CreateC6(0x12, 0, 0, 12, 8120U, 0, 0, 0, 0, 0, 0, 0),
|
||||
start.AddSeconds(5));
|
||||
reader.Stop();
|
||||
|
||||
parameters.Activity = AllyCalibrationActivityNames.StopOpticalStream;
|
||||
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
|
||||
|
||||
Assert.IsFalse(reader.NoSamples);
|
||||
Assert.AreEqual(2D, reader.BeginWMState, 1E-12);
|
||||
Assert.AreEqual(2.03D, reader.EndWMState, 1E-12);
|
||||
Assert.AreEqual(0.03D, reader.WMVolume, 1E-12);
|
||||
Assert.AreEqual(5D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-12);
|
||||
}
|
||||
|
||||
private static AllyMeterReader CreateFakeReader()
|
||||
{
|
||||
AllyReaderCfg cfg = new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory())
|
||||
{
|
||||
DebugLevel = Common.DebugMode.Simulate,
|
||||
ConfiguredMeterSize = AllyMeterSize.AutoDetect,
|
||||
Name = "FakeAlly1"
|
||||
};
|
||||
AllyMeterReader reader = new AllyMeterReader(cfg);
|
||||
reader.Initialize();
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -28,8 +28,10 @@ namespace TBFTests.Rig.TestMethods.AllyCalibration
|
||||
TestMethodParams parameters = new TestMethodParams(true);
|
||||
ICollection<string> activities = parameters.ParamValues(0);
|
||||
|
||||
Assert.AreEqual(19, activities.Count);
|
||||
Assert.AreEqual(21, activities.Count);
|
||||
CollectionAssert.Contains((System.Collections.ICollection)activities, "Read serial number");
|
||||
CollectionAssert.Contains((System.Collections.ICollection)activities, "Unseal meter and start optical stream");
|
||||
CollectionAssert.Contains((System.Collections.ICollection)activities, "Stop optical stream");
|
||||
CollectionAssert.Contains((System.Collections.ICollection)activities, "Start offset learning");
|
||||
CollectionAssert.DoesNotContain((System.Collections.ICollection)activities, "Q2 correction");
|
||||
|
||||
|
||||
@ -144,6 +144,7 @@
|
||||
<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\AllyOpticalRealExamplesTest.cs" />
|
||||
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalTelegramFactory.cs" />
|
||||
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgTest.cs" />
|
||||
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyCommandServiceTest.cs" />
|
||||
@ -158,6 +159,7 @@
|
||||
<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\AllyCalibration\AllyOpticalWorkflowIntegrationTest.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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user