diff --git a/TBF/Rig/DataEntry/Uni/CycleBgEnForm.cs b/TBF/Rig/DataEntry/Uni/CycleBgEnForm.cs
index e665a728b..ba259f94b 100644
--- a/TBF/Rig/DataEntry/Uni/CycleBgEnForm.cs
+++ b/TBF/Rig/DataEntry/Uni/CycleBgEnForm.cs
@@ -835,7 +835,7 @@ namespace TBF.Rig.DataEntry.Uni
};
IRegReader[] selectedRegReaders = RegisterReaderSelection.GetSelectedForDataEntry(
- regReaders, log, "serial number");
+ regReaders, WaterMeters, log, "serial number");
BeforeUpdate(selectedRegReaders);
this.DoneUpdateBySerial += (s, eArgs) =>
{
diff --git a/TBF/Rig/DataEntry/Uni/DataEntryWatermark.cs b/TBF/Rig/DataEntry/Uni/DataEntryWatermark.cs
new file mode 100644
index 000000000..50400605b
--- /dev/null
+++ b/TBF/Rig/DataEntry/Uni/DataEntryWatermark.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+
+namespace TBF.Rig.DataEntry.Uni
+{
+ ///
+ /// Displays status text without changing the editable value of a control.
+ ///
+ internal static class DataEntryWatermark
+ {
+ private const int EmSetCueBanner = 0x1501;
+ private const int CbSetCueBanner = 0x1703;
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern IntPtr SendMessage(
+ IntPtr hWnd,
+ int message,
+ IntPtr wParam,
+ [MarshalAs(UnmanagedType.LPWStr)] string lParam);
+
+ public static void Set(Control control, string text)
+ {
+ if (control == null || control.IsDisposed)
+ return;
+
+ int message;
+ if (control is TextBox)
+ message = EmSetCueBanner;
+ else if (control is ComboBox)
+ message = CbSetCueBanner;
+ else
+ return;
+
+ SendMessage(control.Handle, message, new IntPtr(1), text ?? string.Empty);
+ }
+ }
+}
diff --git a/TBF/Rig/DataEntry/Uni/RegisterReaderSelection.cs b/TBF/Rig/DataEntry/Uni/RegisterReaderSelection.cs
new file mode 100644
index 000000000..6099cc574
--- /dev/null
+++ b/TBF/Rig/DataEntry/Uni/RegisterReaderSelection.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using log4net;
+using Results.Entities;
+using TBF.Rig.GenericDevices;
+
+namespace TBF.Rig.DataEntry.Uni
+{
+ ///
+ /// Uses the smart-meter selection for readers that support automatic Data
+ /// Entry and preserves the prechecked water-meter state for other readers.
+ ///
+ internal static class RegisterReaderSelection
+ {
+ public static IRegReader[] GetSelectedForDataEntry(
+ IEnumerable readers,
+ IList waterMeters,
+ ILog log,
+ string operation)
+ {
+ IRegReader[] allSlots = readers == null
+ ? new IRegReader[0]
+ : readers.ToArray();
+ long enabledHeads = Program.LocalSettings.OptoHeadsEnabled;
+
+ IRegReader[] selected = allSlots
+ .Where(reader => IsSelectedForDataEntry(reader, waterMeters, enabledHeads))
+ .ToArray();
+
+ string selectedPositions = string.Join(",", selected.Select(reader => reader.Position));
+ string skippedPositions = string.Join(",", allSlots
+ .Where(reader => reader != null &&
+ !IsSelectedForDataEntry(reader, waterMeters, enabledHeads))
+ .Select(reader => reader.Position));
+
+ log.DebugFormat(
+ "DATA_ENTRY_READER_FILTER Operation={0}, OptoHeadsEnabled=0x{1:X12}, " +
+ "Slots={2}, Readers={3}, Selected={4}, SelectedPositions=[{5}], " +
+ "SkippedPositions=[{6}], SelectionRule=SmartMeterMaskOrPrecheckedWaterMeter",
+ operation,
+ enabledHeads,
+ allSlots.Length,
+ allSlots.Count(reader => reader != null),
+ selected.Length,
+ selectedPositions,
+ skippedPositions);
+
+ foreach (IRegReader reader in allSlots.Where(item => item != null))
+ {
+ log.DebugFormat(
+ "DATA_ENTRY_READER_SLOT Operation={0}, Name={1}, Type={2}, Position={3}, " +
+ "DebugLevel={4}, Selected={5}{6}",
+ operation,
+ reader.Name,
+ reader.GetType().Name,
+ reader.Position,
+ reader.DebugLevel,
+ IsSelectedForDataEntry(reader, waterMeters, enabledHeads),
+ GetSelectionDetails(reader, waterMeters, enabledHeads));
+ }
+
+ return selected;
+ }
+
+ internal static bool IsSelectedForDataEntry(
+ IRegReader reader,
+ IList waterMeters,
+ long enabledHeads)
+ {
+ if (reader == null)
+ return false;
+
+#if IPERL
+ var iperlHead = reader as
+ TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.IperlHead;
+ if (iperlHead != null)
+ {
+ int position0 = iperlHead.Position - 1;
+ if (position0 < 0 || position0 >= 63)
+ return false;
+
+ return !iperlHead.Disabled &&
+ (enabledHeads & (1L << position0)) != 0;
+ }
+#endif
+
+ if (reader is ISmartMeterReader)
+ {
+ int position0 = reader.Position - 1;
+ return position0 >= 0 && position0 < 63 &&
+ (enabledHeads & (1L << position0)) != 0;
+ }
+
+ // Backward compatibility: readers outside the smart-meter family keep the
+ // selection made in the cycle-begin form instead of using the smart mask.
+ return IsPrecheckedWaterMeter(reader, waterMeters);
+ }
+
+ private static bool IsPrecheckedWaterMeter(
+ IRegReader reader,
+ IList waterMeters)
+ {
+ if (reader == null)
+ return false;
+
+ int meterIndex = reader.Position - 1;
+ if (waterMeters == null || meterIndex < 0 || meterIndex >= waterMeters.Count)
+ return true;
+
+ WaterMeter waterMeter = waterMeters[meterIndex];
+ return waterMeter != null && !waterMeter.Disabled;
+ }
+
+ private static string GetSelectionDetails(
+ IRegReader reader,
+ IList waterMeters,
+ long enabledHeads)
+ {
+#if IPERL
+ var iperlHead = reader as
+ TBF.Rig.TestMethods.iPerlCommunication.iPerlHead.IperlHead;
+ if (iperlHead != null)
+ {
+ int position0 = iperlHead.Position - 1;
+ bool maskBit = position0 >= 0 && position0 < 63 &&
+ (enabledHeads & (1L << position0)) != 0;
+ return string.Format(
+ ", IPerlDisabled={0}, SelectionMaskBit={1}, CommFailed={2}, " +
+ "SerialCached={3}",
+ iperlHead.Disabled,
+ maskBit,
+ iperlHead.CommFailed,
+ !string.IsNullOrEmpty(iperlHead.SerialNr));
+ }
+#endif
+ if (reader is ISmartMeterReader)
+ {
+ int position0 = reader.Position - 1;
+ bool maskBit = position0 >= 0 && position0 < 63 &&
+ (enabledHeads & (1L << position0)) != 0;
+ return string.Format(
+ ", SelectionSource=SmartMeterMask, SelectionMaskBit={0}",
+ maskBit);
+ }
+
+ return string.Format(
+ ", SelectionSource=PrecheckedWaterMeter, Prechecked={0}",
+ IsPrecheckedWaterMeter(reader, waterMeters));
+ }
+ }
+}
diff --git a/TBF/Rig/DataEntry/Uni/TestStartEndForm.cs b/TBF/Rig/DataEntry/Uni/TestStartEndForm.cs
index bde2fc778..2fa28d200 100644
--- a/TBF/Rig/DataEntry/Uni/TestStartEndForm.cs
+++ b/TBF/Rig/DataEntry/Uni/TestStartEndForm.cs
@@ -586,7 +586,7 @@ namespace TBF.Rig.DataEntry.Uni
if (bAutoRead)
{
IRegReader[] selectedRegReaders = RegisterReaderSelection.GetSelectedForDataEntry(
- regReaders, log, isEnd ? "end volume" : "begin volume");
+ regReaders, waterMeters, log, isEnd ? "end volume" : "begin volume");
log.Debug($"Reading serial numbers from register readers... IsEnd: {isEnd}");
this.VolumeStartReadbyRegReader += (s, eArgs) =>
{
diff --git a/TBF/Rig/GenericDevices/IRegReaderSmart.cs b/TBF/Rig/GenericDevices/IRegReaderSmart.cs
index 5133990d5..561546eb1 100644
--- a/TBF/Rig/GenericDevices/IRegReaderSmart.cs
+++ b/TBF/Rig/GenericDevices/IRegReaderSmart.cs
@@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace TBF.Rig.GenericDevices
{
- public interface IRegReaderSmart
+ public interface IRegReaderSmart : ISmartMeterReader
{
Task DataEntry_ReadSerialNumber();
Task DataEntry_ReadBeginVolume();
@@ -13,4 +13,4 @@ namespace TBF.Rig.GenericDevices
int MuxBoardNrOrGroup14 { get; }
}
-}
\ No newline at end of file
+}
diff --git a/TBF/Rig/GenericDevices/ISmartMeterReader.cs b/TBF/Rig/GenericDevices/ISmartMeterReader.cs
new file mode 100644
index 000000000..c100be7e0
--- /dev/null
+++ b/TBF/Rig/GenericDevices/ISmartMeterReader.cs
@@ -0,0 +1,10 @@
+namespace TBF.Rig.GenericDevices
+{
+ ///
+ /// Identifies register readers that belong to the smart-meter family and
+ /// therefore use the shared smart-meter position selection.
+ ///
+ public interface ISmartMeterReader
+ {
+ }
+}
diff --git a/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs b/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs
index 43cb37b7a..ef376e5df 100644
--- a/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs
+++ b/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs
@@ -1,13 +1,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.IO.Ports;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
+using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig.RegisterReaders.AllyReader
{
@@ -15,8 +19,12 @@ namespace TBF.Rig.RegisterReaders.AllyReader
IDevice,
IRegReaderDatastream,
ISessionDataMngmnt,
- IOperation
+ IOperation,
+ IRegReaderSmart,
+ ISmartReader
{
+ private const int DataEntryCommandTimeoutMs = 5000;
+ private const int DataEntryOpticalTimeoutMs = 10000;
private const int MaxStoredSamples = 40000;
private const long RawVolumeModulo = 0x1000000L;
private const long RawVolumeHalfRange = RawVolumeModulo / 2;
@@ -69,7 +77,17 @@ namespace TBF.Rig.RegisterReaders.AllyReader
public byte? RebootCount { get; private set; }
public double? CalibrationFactorPercent { get; private set; }
public double? ExpectedCalibrationFactorPercent { get; private set; }
- public bool CommFailed { get; private set; }
+ public bool CommFailed { get; set; }
+ public bool Disabled { get; set; }
+
+ public string SerialNr
+ {
+ get { return SerialNumber ?? string.Empty; }
+ set { SerialNumber = value ?? string.Empty; }
+ }
+
+ public string CommInterface { get { return "Touch-Read"; } }
+ public int RfidComPortNr { get { return allyCfg == null ? 0 : allyCfg.CommandComPortNr; } }
public AllyMeterSize ConfiguredMeterSize
{
@@ -102,12 +120,17 @@ namespace TBF.Rig.RegisterReaders.AllyReader
public void RunDeviceBefore()
{
- if (!streamEnabled || opticalPort == null || !opticalPort.IsOpen)
- return;
-
try
{
- string text = opticalPort.ReadExisting();
+ string text;
+ lock (opticalSync)
+ {
+ if (!streamEnabled || opticalPort == null || !opticalPort.IsOpen)
+ return;
+
+ text = opticalPort.ReadExisting();
+ }
+
if (!string.IsNullOrEmpty(text))
ProcessOpticalText(text);
}
@@ -266,6 +289,36 @@ namespace TBF.Rig.RegisterReaders.AllyReader
return ExecuteCommand(service => SerialNumber = service.ReadSerialNumber(timeoutMs));
}
+ public Task DataEntry_ReadSerialNumber()
+ {
+ if (!string.IsNullOrEmpty(SerialNumber))
+ return Task.FromResult(SerialNumber);
+
+ return Task.Run(() =>
+ {
+ try
+ {
+ return ReadSerialNumber(DataEntryCommandTimeoutMs);
+ }
+ catch (Exception ex)
+ {
+ CommFailed = true;
+ log.ErrorFormat("ALLY Data Entry serial-number read failed: {0}", ex.Message);
+ return null;
+ }
+ });
+ }
+
+ public Task DataEntry_ReadBeginVolume()
+ {
+ return ReadDataEntryVolume(true);
+ }
+
+ public Task DataEntry_ReadEndVolume()
+ {
+ return ReadDataEntryVolume(false);
+ }
+
public AllyVersionInfo ReadVersionAndType(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
@@ -397,6 +450,11 @@ namespace TBF.Rig.RegisterReaders.AllyReader
public RegisterReaderType RegisterReaderType { get { return RegisterReaderType.DataStream; } }
+ // ALLY heads have independent ports. Separate subgroups keep Data Entry reads
+ // deterministic while retaining the common smart-reader scheduling contract.
+ public int Group { get { return 1; } }
+ public int MuxBoardNrOrGroup14 { get { return Position; } }
+
public int Position
{
get
@@ -427,14 +485,134 @@ namespace TBF.Rig.RegisterReaders.AllyReader
}
}
public double WMVolume { get { return Math.Abs(EndWMState - BeginWMState); } }
- public double BeginWMState { get { return beginWMState; } }
- public double EndWMState { get { return endWMState; } }
+ public double BeginWMState
+ {
+ get { return beginWMState; }
+ set { beginWMState = value; }
+ }
+ public double EndWMState
+ {
+ get { return endWMState; }
+ set { endWMState = value; }
+ }
public bool NoSamples { get { return !hasTestStartSample || opticalSamples.Count < 2; } }
public double VolumeLtrStart { get { return beginWMState; } }
public double VolumeLtrEnd { get { return endWMState; } }
public double TimestampSecStart { get { return timestampSecStart; } }
public double TimestampSecEnd { get { return timestampSecEnd; } }
+ public void ResetNfcInterface(bool? nfc_on = null)
+ {
+ log.DebugFormat(
+ "ALLY {0}: ResetNfcInterface({1}) ignored; command interface is fixed to Touch-Read.",
+ Name,
+ nfc_on.HasValue ? nfc_on.Value.ToString() : "null");
+ }
+
+ public void SetNfcInterface()
+ {
+ log.DebugFormat(
+ "ALLY {0}: SetNfcInterface ignored; command interface is fixed to Touch-Read.",
+ Name);
+ }
+
+ public void SetRfidInterface()
+ {
+ log.DebugFormat(
+ "ALLY {0}: SetRfidInterface ignored; command interface is fixed to Touch-Read.",
+ Name);
+ }
+
+ public void SetCommunicationInterface(string commInterface)
+ {
+ if (!string.IsNullOrWhiteSpace(commInterface) &&
+ !string.Equals(commInterface, CommInterface, StringComparison.OrdinalIgnoreCase))
+ {
+ log.WarnFormat(
+ "ALLY {0}: communication interface '{1}' is not supported; using {2}.",
+ Name,
+ commInterface,
+ CommInterface);
+ }
+ }
+
+ public void WriteBinary(BinaryWriter writer)
+ {
+ if (writer == null)
+ throw new ArgumentNullException("writer");
+
+ writer.Write(Disabled);
+ writer.Write(CommFailed);
+ writer.Write(SerialNr);
+ writer.Write(beginWMState);
+ writer.Write(endWMState);
+ }
+
+ public void ReadBinary(BinaryReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException("reader");
+
+ Disabled = reader.ReadBoolean();
+ CommFailed = reader.ReadBoolean();
+ SerialNr = reader.ReadString();
+ beginWMState = reader.ReadDouble();
+ endWMState = reader.ReadDouble();
+ }
+
+ private Task ReadDataEntryVolume(bool isBegin)
+ {
+ return Task.Run(() =>
+ {
+ if (DebugLevel != DebugMode.Normal)
+ {
+ return Double.NaN;
+ }
+
+ try
+ {
+ Start();
+ DateTime deadline = DateTime.UtcNow.AddMilliseconds(DataEntryOpticalTimeoutMs);
+ while (DateTime.UtcNow < deadline)
+ {
+ RunDeviceBefore();
+ lock (opticalSync)
+ {
+ if (hasTestStartSample)
+ {
+ double volume = isBegin ? beginWMState : endWMState;
+ log.DebugFormat(
+ "ALLY Data Entry {0} volume read: {1} l",
+ isBegin ? "begin" : "end", volume);
+ return volume;
+ }
+ }
+
+ Thread.Sleep(20);
+ }
+
+ log.WarnFormat(
+ "ALLY Data Entry {0} volume timeout after {1} ms on COM{2}",
+ isBegin ? "begin" : "end",
+ DataEntryOpticalTimeoutMs,
+ allyCfg.OptoComPortNr);
+ return Double.NaN;
+ }
+ catch (Exception ex)
+ {
+ CommFailed = true;
+ log.ErrorFormat(
+ "ALLY Data Entry {0} volume read failed: {1}",
+ isBegin ? "begin" : "end", ex.Message);
+ return Double.NaN;
+ }
+ finally
+ {
+ Stop();
+ }
+ });
+ }
+
private void ProcessOpticalText(string text)
{
lock (opticalSync)
diff --git a/TBF/Rig/RegisterReaders/IPerlReader/implementations/SmartReader.cs b/TBF/Rig/RegisterReaders/IPerlReader/implementations/SmartReader.cs
index f7b05fcf7..c2bdedf16 100644
--- a/TBF/Rig/RegisterReaders/IPerlReader/implementations/SmartReader.cs
+++ b/TBF/Rig/RegisterReaders/IPerlReader/implementations/SmartReader.cs
@@ -25,7 +25,7 @@ namespace TBF.Rig.RegisterReaders.IPerlReader.implementations
///
/// based on IPerlReader class
///
- public class SmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ISmartReader
+ public class SmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ISmartReader, ISmartMeterReader
{
private static readonly ILog log = LogManager.GetLogger(typeof(SmartReader));
@@ -1667,4 +1667,4 @@ namespace TBF.Rig.RegisterReaders.IPerlReader.implementations
}
}
-}
\ No newline at end of file
+}
diff --git a/TBF/Rig/RegisterReaders/iPerlASICReader/implementations/SmartReader.cs b/TBF/Rig/RegisterReaders/iPerlASICReader/implementations/SmartReader.cs
index bbeb66220..18ea25b47 100644
--- a/TBF/Rig/RegisterReaders/iPerlASICReader/implementations/SmartReader.cs
+++ b/TBF/Rig/RegisterReaders/iPerlASICReader/implementations/SmartReader.cs
@@ -25,7 +25,7 @@ namespace TBF.Rig.RegisterReaders.iPerlASICReader.implementations
///
/// based on IPerlReader class
///
- public class SmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ISmartReader
+ public class SmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ISmartReader, ISmartMeterReader
{
private static readonly ILog log = LogManager.GetLogger(typeof(SmartReader));
@@ -1668,4 +1668,4 @@ namespace TBF.Rig.RegisterReaders.iPerlASICReader.implementations
}
}
-}
\ No newline at end of file
+}
diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj
index 4a1bfa056..771283bd9 100644
--- a/TBF/TBF.csproj
+++ b/TBF/TBF.csproj
@@ -802,6 +802,7 @@
+
diff --git a/TBFTests/Rig/DataEntry/Uni/RegisterReaderSelectionTests.cs b/TBFTests/Rig/DataEntry/Uni/RegisterReaderSelectionTests.cs
new file mode 100644
index 000000000..7f272bcec
--- /dev/null
+++ b/TBFTests/Rig/DataEntry/Uni/RegisterReaderSelectionTests.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using Results.Entities;
+using TBF.Rig.DataEntry.Uni;
+using TBF.Rig.GenericDevices;
+
+namespace TBFTests.Rig.DataEntry.Uni
+{
+ [TestClass]
+ public class RegisterReaderSelectionTests
+ {
+ [TestMethod]
+ public void NonSmartReader_PrecheckedMeter_IsSelectedWithoutSmartMask()
+ {
+ var reader = CreateReader(1);
+ var waterMeters = new List
+ {
+ new WaterMeter { Disabled = false }
+ };
+
+ bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
+ reader.Object, waterMeters, 0L);
+
+ Assert.IsTrue(selected);
+ }
+
+ [TestMethod]
+ public void NonSmartReader_UncheckedMeter_IsNotSelected()
+ {
+ var reader = CreateReader(2);
+ var waterMeters = new List
+ {
+ new WaterMeter { Disabled = false },
+ new WaterMeter { Disabled = true }
+ };
+
+ bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
+ reader.Object, waterMeters, long.MaxValue);
+
+ Assert.IsFalse(selected);
+ }
+
+ [TestMethod]
+ public void NonSmartReader_WithoutWaterMeterContext_PreservesLegacySelection()
+ {
+ var reader = CreateReader(3);
+
+ bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
+ reader.Object, null, 0L);
+
+ Assert.IsTrue(selected);
+ }
+
+ [TestMethod]
+ public void SmartReader_SelectedMaskBit_OverridesUncheckedWaterMeter()
+ {
+ var reader = CreateReader(2, true);
+ var waterMeters = new List
+ {
+ new WaterMeter { Disabled = false },
+ new WaterMeter { Disabled = true }
+ };
+
+ bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
+ reader.Object, waterMeters, 1L << 1);
+
+ Assert.IsTrue(selected);
+ }
+
+ [TestMethod]
+ public void SmartReader_UnselectedMaskBit_IgnoresPrecheckedWaterMeter()
+ {
+ var reader = CreateReader(1, true);
+ var waterMeters = new List
+ {
+ new WaterMeter { Disabled = false }
+ };
+
+ bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
+ reader.Object, waterMeters, 0L);
+
+ Assert.IsFalse(selected);
+ }
+
+ private static Mock CreateReader(int position, bool smart = false)
+ {
+ var reader = new Mock();
+ if (smart)
+ reader.As();
+ reader.SetupGet(item => item.Position).Returns(position);
+ return reader;
+ }
+ }
+}
diff --git a/TBFTests/Rig/DataEntry/Uni/SmartMeterReaderFamilyTests.cs b/TBFTests/Rig/DataEntry/Uni/SmartMeterReaderFamilyTests.cs
new file mode 100644
index 000000000..555cd66de
--- /dev/null
+++ b/TBFTests/Rig/DataEntry/Uni/SmartMeterReaderFamilyTests.cs
@@ -0,0 +1,34 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TBF.Rig.GenericDevices;
+using TBF.Rig.RegisterReaders.AllyReader;
+using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
+using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
+using OldIPerlReader = TBF.Rig.RegisterReaders.IPerlReader.implementations.SmartReader;
+using IPerlAsicReader = TBF.Rig.RegisterReaders.iPerlASICReader.implementations.SmartReader;
+using PoseidonReader = TBF.Rig.RegisterReaders.PoseidonReader.implementations.PoseidonReader;
+
+namespace TBFTests.Rig.DataEntry.Uni
+{
+ [TestClass]
+ public class SmartMeterReaderFamilyTests
+ {
+ [TestMethod]
+ public void SmartMeterFamily_ContainsSupportedReadersOnly()
+ {
+ AssertSmartMeter(typeof(IperlHead));
+ AssertSmartMeter(typeof(GenesisSmartReader));
+ AssertSmartMeter(typeof(AllyMeterReader));
+ AssertSmartMeter(typeof(OldIPerlReader));
+ AssertSmartMeter(typeof(IPerlAsicReader));
+
+ Assert.IsFalse(typeof(ISmartMeterReader).IsAssignableFrom(typeof(PoseidonReader)));
+ }
+
+ private static void AssertSmartMeter(System.Type readerType)
+ {
+ Assert.IsTrue(
+ typeof(ISmartMeterReader).IsAssignableFrom(readerType),
+ readerType.FullName + " must belong to the smart-meter family.");
+ }
+ }
+}
diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs b/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs
index d15d74298..3a895e0b7 100644
--- a/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs
+++ b/TBFTests/Rig/RegisterReaders/AllyReader/AllyMeterReaderTest.cs
@@ -1,9 +1,12 @@
using System;
+using System.IO;
using System.Reflection;
using Common;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.AllyReader;
+using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
@@ -11,6 +14,63 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
[TestSubject(typeof(AllyMeterReader))]
public class AllyMeterReaderTest
{
+ [TestMethod]
+ public void Reader_ImplementsSmartDataEntryContract()
+ {
+ AllyMeterReader reader = CreateReader(AllyMeterSize.FiveEighths);
+
+ Assert.IsInstanceOfType(reader, typeof(IRegReaderSmart));
+ Assert.IsInstanceOfType(reader, typeof(ISmartMeterReader));
+ Assert.IsInstanceOfType(reader, typeof(ISmartReader));
+ }
+
+ [TestMethod]
+ public void SmartReaderContract_MapsAllyCommandInterface()
+ {
+ ISmartReader reader = CreateReader(AllyMeterSize.FiveEighths);
+
+ Assert.AreEqual("Touch-Read", reader.CommInterface);
+ Assert.AreEqual(1, reader.RfidComPortNr);
+
+ reader.ResetNfcInterface();
+ reader.SetNfcInterface();
+ reader.SetRfidInterface();
+ reader.SetCommunicationInterface("RFID");
+ }
+
+ [TestMethod]
+ public void SmartReaderContract_WriteReadBinary_RestoresDataEntryState()
+ {
+ ISmartReader source = CreateReader(AllyMeterSize.FiveEighths);
+ source.Disabled = true;
+ source.CommFailed = true;
+ source.SerialNr = "ALLY-123456";
+ source.BeginWMState = 12.5D;
+ source.EndWMState = 15.75D;
+
+ byte[] data;
+ using (MemoryStream stream = new MemoryStream())
+ using (BinaryWriter writer = new BinaryWriter(stream))
+ {
+ source.WriteBinary(writer);
+ writer.Flush();
+ data = stream.ToArray();
+ }
+
+ ISmartReader restored = CreateReader(AllyMeterSize.FiveEighths);
+ using (MemoryStream stream = new MemoryStream(data))
+ using (BinaryReader reader = new BinaryReader(stream))
+ {
+ restored.ReadBinary(reader);
+ }
+
+ Assert.IsTrue(restored.Disabled);
+ Assert.IsTrue(restored.CommFailed);
+ Assert.AreEqual("ALLY-123456", restored.SerialNr);
+ Assert.AreEqual(12.5D, restored.BeginWMState);
+ Assert.AreEqual(15.75D, restored.EndWMState);
+ }
+
[TestMethod]
public void GetResetCalibrationFactorPercent_AllSupportedSizes_ReturnsUi2093Value()
{
diff --git a/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs
new file mode 100644
index 000000000..f4a3eeddd
--- /dev/null
+++ b/TBFTests/Rig/RegisterReaders/AllyReader/communication/AllyIntegrationTests.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Ports;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using TBF.Rig.RegisterReaders.AllyReader;
+using TBF.Rig.RegisterReaders.AllyReader.Communication;
+
+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.
+ ///
+ [TestClass]
+ [DoNotParallelize]
+ public class AllyIntegrationTests
+ {
+ private const string CommandComPort = "COM3";
+ private const string OpticalComPort = "COM4";
+ private const int CommandBaudRate = 2400;
+ private const int OpticalBaudRate = 9600;
+ private const int ReadTimeoutMs = 5000;
+ private const int OpticalReadSeconds = 10;
+ private const byte ActiveMeterMode = 0x02;
+ private const byte InitialMeterMode = 0x09;
+
+ [TestMethod]
+ [TestCategory("Hardware")]
+ [TestCategory("Serial")]
+ public void Serial_ReadFactoryId_SetActive_ReadOptical_SetInitial()
+ {
+ Console.WriteLine(
+ "ALLY integration test: command={0}/{1}, optical={2}/{3}",
+ CommandComPort,
+ CommandBaudRate,
+ OpticalComPort,
+ OpticalBaudRate);
+
+ using (var commandPort = new SerialPort(
+ CommandComPort,
+ CommandBaudRate,
+ Parity.None,
+ 8,
+ StopBits.One))
+ {
+ commandPort.Handshake = Handshake.None;
+ commandPort.ReadTimeout = ReadTimeoutMs;
+ commandPort.WriteTimeout = ReadTimeoutMs;
+
+ Console.WriteLine("OPEN command port " + CommandComPort);
+ commandPort.Open();
+ bool activeModeCommandAttempted = false;
+ Exception testFailure = null;
+
+ try
+ {
+ byte[] factoryIdRequest = new AllyFrameBuilder()
+ .WithCommand(AllyCommand.ViewFactoryId)
+ .BuildBytes();
+ AllyResponse factoryIdResponse = SendRequest(
+ commandPort,
+ factoryIdRequest,
+ "ViewFactoryId");
+
+ string serialNumber = factoryIdResponse.GetNullTerminatedAscii();
+ Console.WriteLine("PARSE PASS ViewFactoryId: serial number='{0}'", serialNumber);
+ Assert.IsFalse(
+ string.IsNullOrWhiteSpace(serialNumber),
+ "ViewFactoryId returned an empty manufacturing serial number.");
+
+ byte[] activeModeRequest = new AllyFrameBuilder()
+ .WithCommand(AllyCommand.SetMeterMode)
+ .WithByte(ActiveMeterMode)
+ .BuildBytes();
+ activeModeCommandAttempted = true;
+ SendRequest(commandPort, activeModeRequest, "SetMeterMode Active 0x02");
+ Console.WriteLine("STEP PASS Active meter mode 0x02 is acknowledged.");
+
+ int parsedSamples = ReadOpticalSamples();
+ Assert.IsTrue(
+ parsedSamples > 0,
+ "No valid ALLY optical telegram was parsed on " + OpticalComPort + ".");
+ Console.WriteLine("STEP PASS Parsed optical samples: " + parsedSamples);
+ }
+ catch (Exception ex)
+ {
+ testFailure = ex;
+ Console.WriteLine("TEST FAIL: " + ex);
+ throw;
+ }
+ 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)
+ {
+ 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.");
+ }
+ catch (Exception restoreException)
+ {
+ Console.WriteLine("RESTORE FAIL Initial mode 0x09: " + restoreException);
+ if (testFailure == null)
+ throw;
+ }
+ }
+
+ Console.WriteLine("CLOSE command port " + CommandComPort);
+ }
+ }
+
+ Console.WriteLine("ALLY integration test completed.");
+ }
+
+ private static AllyResponse SendRequest(
+ SerialPort port,
+ byte[] request,
+ string commandName)
+ {
+ port.DiscardInBuffer();
+ Console.WriteLine(commandName + " TX -> " + ToHex(request));
+ port.Write(request, 0, request.Length);
+
+ byte[] response;
+ try
+ {
+ response = ReadResponse(port);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(commandName + " RX FAIL: " + ex.Message);
+ throw;
+ }
+
+ Console.WriteLine(commandName + " RX <- " + ToHex(response));
+
+ AllyResponse parsed;
+ try
+ {
+ parsed = new AllyFrameParser().ParseResponse(response);
+ Console.WriteLine(
+ commandName + " PARSE PASS: status=0x" +
+ parsed.Status.ToString("X2") +
+ ", payload=" + ToHex(parsed.Payload));
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(commandName + " PARSE FAIL: " + ex.Message);
+ throw;
+ }
+
+ Assert.IsTrue(
+ parsed.IsSuccess,
+ commandName + " returned status 0x" + parsed.Status.ToString("X2") + ".");
+ return parsed;
+ }
+
+ private static byte[] ReadResponse(SerialPort port)
+ {
+ var response = new List();
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ int value;
+ do
+ {
+ value = ReadByte(port, stopwatch);
+ }
+ while (value != 0x53);
+
+ response.Add((byte)value);
+ response.Add((byte)ReadByte(port, stopwatch));
+ int length = ReadByte(port, stopwatch);
+ if (length < 5)
+ throw new FormatException("ALLY response length is less than five bytes.");
+ response.Add((byte)length);
+
+ while (response.Count < length)
+ response.Add((byte)ReadByte(port, stopwatch));
+ return response.ToArray();
+ }
+
+ private static int ReadByte(SerialPort port, Stopwatch stopwatch)
+ {
+ int remaining = ReadTimeoutMs - (int)stopwatch.ElapsedMilliseconds;
+ if (remaining <= 0)
+ throw new TimeoutException("ALLY response timeout.");
+
+ port.ReadTimeout = remaining;
+ int value = port.ReadByte();
+ if (value < 0)
+ throw new IOException("ALLY command port returned end of stream.");
+ return value;
+ }
+
+ private static int ReadOpticalSamples()
+ {
+ int parsedSamples = 0;
+ using (var opticalPort = new SerialPort(
+ OpticalComPort,
+ OpticalBaudRate,
+ Parity.None,
+ 8,
+ StopBits.One))
+ {
+ opticalPort.Handshake = Handshake.None;
+ opticalPort.ReadTimeout = 1000;
+ opticalPort.NewLine = "\r\n";
+ opticalPort.Encoding = Encoding.ASCII;
+
+ Console.WriteLine("OPEN optical port " + OpticalComPort);
+ opticalPort.Open();
+ opticalPort.DiscardInBuffer();
+
+ DateTime end = DateTime.UtcNow.AddSeconds(OpticalReadSeconds);
+ while (DateTime.UtcNow < end)
+ {
+ try
+ {
+ string rawLine = opticalPort.ReadLine() + "\r\n";
+ Console.WriteLine("OPTO RX <- " + ToHex(Encoding.ASCII.GetBytes(rawLine)));
+ Console.WriteLine("OPTO TEXT <- " + Escape(rawLine));
+
+ AllyOpticalSample sample;
+ if (!AllyOpticalSample.TryParse(rawLine, DateTime.UtcNow, out sample))
+ {
+ Console.WriteLine("OPTO PARSE FAIL");
+ continue;
+ }
+
+ parsedSamples++;
+ Console.WriteLine(
+ "OPTO PARSE PASS: sample={0}, flow={1}, rawVolume={2}, rawTimestamp={3}",
+ parsedSamples,
+ sample.RawFlow,
+ sample.RawVolume,
+ sample.RawTimestamp);
+ }
+ catch (TimeoutException)
+ {
+ Console.WriteLine("OPTO WAIT: no complete line received in the last second.");
+ }
+ }
+
+ Console.WriteLine("CLOSE optical port " + OpticalComPort);
+ }
+
+ return parsedSamples;
+ }
+
+ private static string ToHex(byte[] value)
+ {
+ return value == null || value.Length == 0
+ ? ""
+ : BitConverter.ToString(value).Replace('-', ' ');
+ }
+
+ private static string Escape(string value)
+ {
+ return value.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
+ }
+ }
+}
diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj
index 1fcb52d3c..cbefac8ed 100644
--- a/TBFTests/TBFTests.csproj
+++ b/TBFTests/TBFTests.csproj
@@ -102,6 +102,8 @@
+
+