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:
Michal Buzik 2026-09-16 09:30:50 +02:00
parent 0258e54b4e
commit 1c8d01562f
33 changed files with 2129 additions and 381 deletions

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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();

View File

@ -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;
}
}
}

View File

@ -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(

View File

@ -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);
}
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -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 13. 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.

View File

@ -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)

View File

@ -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.");
}

View 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 };
}
}
}

View File

@ -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(

View File

@ -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
};
}
}

View File

@ -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];

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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));
}
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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 });
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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";
}
}
}

View File

@ -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()
{

View File

@ -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)
{

View File

@ -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()

View File

@ -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"))
};
}

View File

@ -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;
}
}
}

View File

@ -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()

View File

@ -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;
}
}
}

View File

@ -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");

View File

@ -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" />