diff --git a/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs b/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs index 48c9cbef0..f52352d7d 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs @@ -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 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); } } } + /// + /// 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. + /// + 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; + } + } + + /// + /// 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. + /// + 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."); + } + + /// + /// Stops COM optical capture and restores LED, meter mode and spread + /// spectrum even when the dialog is closed unexpectedly. + /// + 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; diff --git a/TBF/Rig/RegisterReaders/AllyReader/AllyOpticalSample.cs b/TBF/Rig/RegisterReaders/AllyReader/AllyOpticalSample.cs index 36ce9f0bd..a96481c0d 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/AllyOpticalSample.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/AllyOpticalSample.cs @@ -4,19 +4,51 @@ using System.Globalization; namespace TBF.Rig.RegisterReaders.AllyReader { /// - /// 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. /// 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; } } + + /// + /// Flow decoded from the C6 payload. The payload stores quarter millilitres per second. + /// + public double FlowMillilitersPerSecond { get { return RawFlow / 4D; } } + + /// + /// Accumulated volume decoded from the C6 payload. The payload stores quarter millilitres. + /// + 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; + } } } diff --git a/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.Designer.cs b/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.Designer.cs index 106bc24a8..cac759325 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.Designer.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.Designer.cs @@ -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(); diff --git a/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.cs b/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.cs index ac0123258..be2120e00 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/AllyReaderManualTestCtrl.cs @@ -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().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; + } } } diff --git a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyCommandService.cs b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyCommandService.cs index fa31e7d42..19199bf9f 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyCommandService.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyCommandService.cs @@ -93,6 +93,96 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication timeoutMs); } + /// + /// Enables the documented ALLY optical-verification output. Factory + /// sealed registers must be explicitly unsealed before this sequence. + /// + 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); + } + + /// + /// Restores the documented normal ALLY operating state after optical + /// verification output has stopped. + /// + 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( diff --git a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyFactoryUnsealData.cs b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyFactoryUnsealData.cs new file mode 100644 index 000000000..3214c9030 --- /dev/null +++ b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyFactoryUnsealData.cs @@ -0,0 +1,130 @@ +using System; +using System.Text; + +namespace TBF.Rig.RegisterReaders.AllyReader.Communication +{ + /// + /// Values read immediately before breaking an ALLY factory seal and the + /// corresponding, meter-specific eight-byte unseal credential. + /// + 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); + } + } +} diff --git a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyProtocol.cs b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyProtocol.cs index 4c91412c5..360fdc40d 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyProtocol.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyProtocol.cs @@ -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 } diff --git a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllySerialTransport.cs b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllySerialTransport.cs index adc445594..4f8e7deca 100644 --- a/TBF/Rig/RegisterReaders/AllyReader/Communication/AllySerialTransport.cs +++ b/TBF/Rig/RegisterReaders/AllyReader/Communication/AllySerialTransport.cs @@ -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 0D"; + } + + return ToHex(request); + } + + private static string ToHex(byte[] data) + { + return data == null ? "" : BitConverter.ToString(data).Replace("-", " "); + } + private int ReadByte(Stopwatch stopwatch, int timeoutMs) { int remaining = timeoutMs - (int)stopwatch.ElapsedMilliseconds; diff --git a/TBF/Rig/TestMethods/AllyCalibration/ALLY_OPTICAL_STREAM_SEQUENCE.md b/TBF/Rig/TestMethods/AllyCalibration/ALLY_OPTICAL_STREAM_SEQUENCE.md new file mode 100644 index 000000000..970d9806e --- /dev/null +++ b/TBF/Rig/TestMethods/AllyCalibration/ALLY_OPTICAL_STREAM_SEQUENCE.md @@ -0,0 +1,80 @@ +# ALLY optical-stream procedure + +## Recommended procedure layout + +Use the combined activities at the boundaries of measurement: + +1. `Read serial number` (optional, recommended for traceability) +2. `Unseal meter and start optical stream` +3. One or more optical measurement activities +4. `Stop optical stream` + +The optical measurement must run after the start activity completes and before the stop activity begins. + +## Unseal meter and start optical stream + +This is a production-bench setup operation. It leaves the factory seal **unsealed** and leaves the optical stream **running**. + +| Order | Action | Protocol effect | +| --- | --- | --- | +| 1 | Read factory unseal data | Reads factory seal state, factory ID, programmable text, reading preset and seconds active. These values produce the meter-specific unseal credential. | +| 2 | Unseal only if necessary | Sends Factory Unseal with the calculated credential when the meter is sealed. An already unsealed meter is not changed. | +| 3 | Verify factory seal | Confirms that the seal is no longer active. The setup stops on failure. | +| 4 | Open ALLY valve | Sends valve state `Open`. | +| 5 | Disable Spread Spectrum | Required before enabling the diagnostic optical output. | +| 6 | Set initial meter mode | Sets meter mode `0x09`. | +| 7 | Turn diagnostic LED on | Sets LED output `0xC2`, enabling the optical verification output. | +| 8 | Start optical COM capture | Opens the configured ALLY optical port and starts parsing optical telegrams. | + +### Equivalent manual composition + +The procedure activity is intentionally atomic because the factory unseal credential is meter-specific. The UI manual command **Unseal meter** performs steps 1–3. The rest can be composed in the procedure as: + +1. `Open ALLY valve` +2. `Disable spread spectrum` +3. `Set initial meter mode` +4. `Turn diagnostic LED on` + +These individual activities configure the meter output, but do **not** open the TBF optical COM capture. Use `Unseal meter and start optical stream` when TBF must receive and parse optical data. + +## Stop optical stream + +Use this as the final cleanup activity after all optical measurements. It is idempotent with respect to COM capture: it always closes the local stream even if a previous command failed. + +| Order | Action | Protocol effect | +| --- | --- | --- | +| 1 | Stop optical processing | Stops the reader's active optical processing loop. | +| 2 | Turn diagnostic LED off | Sets LED output `0x00`. | +| 3 | Set active meter mode | Sets meter mode `0x02`. | +| 4 | Enable Spread Spectrum | Restores normal Spread Spectrum operation. | +| 5 | Close optical COM capture | Stops data-stream processing and closes the configured optical serial port, also when an earlier cleanup command failed. | + +The operation does **not** reseal the meter and does not close the valve. Resealing is a separate intentional factory operation. + +### Equivalent manual composition + +The closest procedure-only composition is: + +1. `Turn diagnostic LED off` +2. `Set active meter mode` +3. `Enable spread spectrum` + +This restores meter settings but it does **not** close TBF optical COM capture. Use `Stop optical stream` whenever the stream was started by `Unseal meter and start optical stream`. + +## Individual activities + +| Activity | What it changes | What it does not do | +| --- | --- | --- | +| `Open ALLY valve` | Opens the valve. | Does not alter optical output. | +| `Disable spread spectrum` | Disables Spread Spectrum. | Does not select mode or LED output. | +| `Set initial meter mode` | Sets mode `0x09`. | Does not enable the diagnostic LED or COM capture. | +| `Turn diagnostic LED on` | Sets LED `0xC2`. | Does not check/unseal factory seal or open COM capture. | +| `Turn diagnostic LED off` | Sets LED `0x00`. | Does not restore mode, spread spectrum or COM capture. | +| `Set active meter mode` | Sets mode `0x02`. | Does not turn off LED, enable Spread Spectrum or close COM capture. | +| `Enable spread spectrum` | Restores Spread Spectrum. | Does not stop LED or COM capture. | +| `Unseal meter and start optical stream` | Complete setup and COM capture. | Does not reseal automatically. | +| `Stop optical stream` | Complete optical cleanup and COM close. | Does not reseal or close the valve. | + +## Factory-seal warning + +`Unseal meter and start optical stream` changes factory-seal state when the meter is sealed. Add it only to procedures intended for authorized verification or calibration benches. A normal accuracy test should use a meter already prepared for optical verification. diff --git a/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationActivity.cs b/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationActivity.cs index 49440b4a7..fddddcac3 100644 --- a/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationActivity.cs +++ b/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationActivity.cs @@ -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) diff --git a/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationSeq.cs b/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationSeq.cs index 87e0c8e15..902782d14 100644 --- a/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationSeq.cs +++ b/TBF/Rig/TestMethods/AllyCalibration/AllyCalibrationSeq.cs @@ -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 ? "" : test.Name, + test == null ? "" : test.MetersPath, + parameters == 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 ? "" : configuredReader.Name, + configuredReader == 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.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."); } diff --git a/TBF/Rig/TestMethods/AllyCalibration/AllyCommunicationSeq.cs b/TBF/Rig/TestMethods/AllyCalibration/AllyCommunicationSeq.cs new file mode 100644 index 000000000..e1ca341e7 --- /dev/null +++ b/TBF/Rig/TestMethods/AllyCalibration/AllyCommunicationSeq.cs @@ -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 +{ + /// + /// ALLY equivalent of iPerlCommunicationSeq. Command execution remains in + /// AllyTestCorrections; this sequence owns only the modeless dialog lifecycle. + /// + 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 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 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.UiCmdStop }; + } + + Bridge.OnTestProgress(this, new TestProgressEventArgs(test.Name, Progress.Completed)); + log.InfoFormat("ALLY_SMART_COMM_SEQUENCE_COMPLETED: test='{0}', formClosed={1}", + test == null ? "" : test.Name, dialogClosed); + modelessDialog = null; + return new List { Event.Done }; + } + } +} diff --git a/TBF/Rig/TestMethods/AllyCalibration/TestMethod.cs b/TBF/Rig/TestMethods/AllyCalibration/TestMethod.cs index 9cc10f371..6acd6164d 100644 --- a/TBF/Rig/TestMethods/AllyCalibration/TestMethod.cs +++ b/TBF/Rig/TestMethods/AllyCalibration/TestMethod.cs @@ -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 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 ? "" : test.Name, + repetitionNr, + isLastRepetition, + DebugLevel, + parameters == 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 ? "" : 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( diff --git a/TBF/Rig/TestMethods/AllyCalibration/TestMethodCfg.cs b/TBF/Rig/TestMethods/AllyCalibration/TestMethodCfg.cs index 42db4e76b..22cd1ecc5 100644 --- a/TBF/Rig/TestMethods/AllyCalibration/TestMethodCfg.cs +++ b/TBF/Rig/TestMethods/AllyCalibration/TestMethodCfg.cs @@ -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 }; } } diff --git a/TBF/Rig/TestMethods/AllyCalibration/TestMethodParams.cs b/TBF/Rig/TestMethods/AllyCalibration/TestMethodParams.cs index bbb9502a0..6ea4a3c44 100644 --- a/TBF/Rig/TestMethods/AllyCalibration/TestMethodParams.cs +++ b/TBF/Rig/TestMethods/AllyCalibration/TestMethodParams.cs @@ -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]; diff --git a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/CommCompletedEventArgs.cs b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/CommCompletedEventArgs.cs index 45cc4398e..dd266d32d 100644 --- a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/CommCompletedEventArgs.cs +++ b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/CommCompletedEventArgs.cs @@ -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); } diff --git a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartCommunicationForm.cs b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartCommunicationForm.cs index b29b3f795..5a5661195 100644 --- a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartCommunicationForm.cs +++ b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/SmartCommunicationForm.cs @@ -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 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 correctionsList = new List(); - 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 smartHeads = ProcessData.RegisterReaders == null + ? ProcessData.SmartHeadsUni + : ProcessData.RegisterReaders.OfType(); + 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 + ? "" + : string.Join(",", _corrections.Select(correction => correction.TypeIdentificatorName())); + string positions = waterMeterPositions0 == 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 ? "" : activityLabel.Text, + SelectedTypeReader ?? "", + correctionNames, + iperlHeads == null ? 0 : iperlHeads.Count, + positions); } /// @@ -296,13 +346,21 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication /// /// Number of text boxes for serial numbers public SmartCommunicationForm(Generic.IComponent componentBase, IList tests, IList 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 ?? "", + 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 ?? "", + corre == null ? "" : 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 ? "" : 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 ? "" : data.CommMessage); } diff --git a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/AllyTestCorrections.cs b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/AllyTestCorrections.cs new file mode 100644 index 000000000..980efd36c --- /dev/null +++ b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/AllyTestCorrections.cs @@ -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 +{ + /// + /// 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. + /// + 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 tests; + private readonly IList multiTestParams; + private readonly IList workerThreads = new List(); + private volatile bool stopWorkerThreads; + private int completedActivities; + private int expectedActivities; + + public AllyTestCorrections(SmartCommunicationForm parent, ISmartTestMethod testMethod, + ITestMethodCfg cfg, IList tests, IList multiTestParams) + { + ParentFrom = parent; + this.testMethod = testMethod; + this.cfg = cfg; + this.tests = tests ?? new List(); + this.multiTestParams = multiTestParams ?? new List(); + } + + 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 MultiTestParams { get { return multiTestParams; } } + public IList Tests { get { return tests; } set { } } + public IList 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 ? "" : 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 ? "" : 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 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 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 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 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 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)); + } + } +} diff --git a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/ManualSmartCorrectionsBase.cs b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/ManualSmartCorrectionsBase.cs index f96352bf3..430782a06 100644 --- a/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/ManualSmartCorrectionsBase.cs +++ b/TBF/Rig/Uni/SharedDialogs/SmartMetersCommunication/implementations/ManualSmartCorrectionsBase.cs @@ -60,6 +60,12 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations ParentFrom.ActivityLabel.Text = item.Text; string command = item.Tag.ToString(); IList 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> 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) diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index 7a71a773d..8e4effe28 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1646,6 +1646,7 @@ AllyReaderManualTestCtrl.cs + @@ -1982,6 +1983,7 @@ + @@ -2574,6 +2576,7 @@ + @@ -4548,6 +4551,7 @@ + Always diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs index 3a895e0b7..ce8e98c05 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs @@ -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 }); - } } } diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalRealExamplesTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalRealExamplesTest.cs new file mode 100644 index 000000000..f2d83b098 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalRealExamplesTest.cs @@ -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 +{ + /// + /// Regression coverage for C6 telegrams captured from an ALLY optical sensor. + /// The test output deliberately documents the decoded values and their units. + /// + [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); + } + } +} diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalSampleTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalSampleTest.cs index 7e4ddecf6..301fcb676 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalSampleTest.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalSampleTest.cs @@ -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); diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalTelegramFactory.cs b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalTelegramFactory.cs index 2f634ccfb..e4d39040b 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalTelegramFactory.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/AllyOpticalTelegramFactory.cs @@ -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"; } } } diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyCommandServiceTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyCommandServiceTest.cs index e12d12efb..53a12c7cb 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyCommandServiceTest.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyCommandServiceTest.cs @@ -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() { diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs index f4a3eeddd..2c5ed6ec9 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs @@ -12,25 +12,27 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication { /// /// 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. /// [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) { diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/communication/FakeAllyTransport.cs b/TBFTests/Rig/RegisterReaders/AllyReader/communication/FakeAllyTransport.cs index fe6c832fb..90f249503 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/communication/FakeAllyTransport.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/communication/FakeAllyTransport.cs @@ -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 Requests { get; private set; } = new List(); + private readonly Queue responses = new Queue(); + + 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() diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationSettings.cs b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationSettings.cs index 2449777ad..4bf2664b4 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationSettings.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationSettings.cs @@ -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")) }; } diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationTest.cs index b6159c9ac..1dbe22b3c 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationTest.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyHardwareIntegrationTest.cs @@ -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( + "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 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 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 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 CaptureOpticalTelegramsDirectly() + private string ReadSerialNumberAndFirmwareRevision(AllyMeterReader reader) { - List samples = new List(); - 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=", + 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; - } } } diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyIntegrationTestLogger.cs b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyIntegrationTestLogger.cs index 7afb0aa6f..3a21a33e1 100644 --- a/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyIntegrationTestLogger.cs +++ b/TBFTests/Rig/RegisterReaders/AllyReader/integration/AllyIntegrationTestLogger.cs @@ -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() diff --git a/TBFTests/Rig/TestMethods/AllyCalibration/AllyOpticalWorkflowIntegrationTest.cs b/TBFTests/Rig/TestMethods/AllyCalibration/AllyOpticalWorkflowIntegrationTest.cs new file mode 100644 index 000000000..7d3ad37c8 --- /dev/null +++ b/TBFTests/Rig/TestMethods/AllyCalibration/AllyOpticalWorkflowIntegrationTest.cs @@ -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 +{ + /// + /// Component-level integration coverage of Read PCB, start optical output, + /// C6 parsing and cleanup without serial hardware. + /// + [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; + } + } +} diff --git a/TBFTests/Rig/TestMethods/AllyCalibration/TestMethodConfigTest.cs b/TBFTests/Rig/TestMethods/AllyCalibration/TestMethodConfigTest.cs index 2124bcdca..d98826380 100644 --- a/TBFTests/Rig/TestMethods/AllyCalibration/TestMethodConfigTest.cs +++ b/TBFTests/Rig/TestMethods/AllyCalibration/TestMethodConfigTest.cs @@ -28,8 +28,10 @@ namespace TBFTests.Rig.TestMethods.AllyCalibration TestMethodParams parameters = new TestMethodParams(true); ICollection 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"); diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index 7dd3ba129..b551c6485 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -144,6 +144,7 @@ + @@ -158,6 +159,7 @@ +