FIX UNI Data Entry - Smart Reader + missing files

Implement smart meter interface enhancements for AllyReader: Add `ISmartMeterReader` interface, update `RegisterReaderSelection` logic, integrate smart-meter position selection, and expand unit test coverage.
This commit is contained in:
Michal Buzik 2026-08-19 14:33:06 +02:00
parent 6c3c6cd0ef
commit d12ee008db
15 changed files with 857 additions and 16 deletions

View File

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

View File

@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TBF.Rig.DataEntry.Uni
{
/// <summary>
/// Displays status text without changing the editable value of a control.
/// </summary>
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Uses the smart-meter selection for readers that support automatic Data
/// Entry and preserves the prechecked water-meter state for other readers.
/// </summary>
internal static class RegisterReaderSelection
{
public static IRegReader[] GetSelectedForDataEntry(
IEnumerable<IRegReader> readers,
IList<WaterMeter> 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<WaterMeter> 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<WaterMeter> 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<WaterMeter> 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));
}
}
}

View File

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

View File

@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace TBF.Rig.GenericDevices
{
public interface IRegReaderSmart
public interface IRegReaderSmart : ISmartMeterReader
{
Task<string> DataEntry_ReadSerialNumber();
Task<double> DataEntry_ReadBeginVolume();
@ -13,4 +13,4 @@ namespace TBF.Rig.GenericDevices
int MuxBoardNrOrGroup14 { get; }
}
}
}

View File

@ -0,0 +1,10 @@
namespace TBF.Rig.GenericDevices
{
/// <summary>
/// Identifies register readers that belong to the smart-meter family and
/// therefore use the shared smart-meter position selection.
/// </summary>
public interface ISmartMeterReader
{
}
}

View File

@ -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<string> 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<double> DataEntry_ReadBeginVolume()
{
return ReadDataEntryVolume(true);
}
public Task<double> 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<double> 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)

View File

@ -25,7 +25,7 @@ namespace TBF.Rig.RegisterReaders.IPerlReader.implementations
/// <summary>
/// based on IPerlReader class
/// </summary>
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
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace TBF.Rig.RegisterReaders.iPerlASICReader.implementations
/// <summary>
/// based on IPerlReader class
/// </summary>
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
}
}
}
}

View File

@ -802,6 +802,7 @@
<Compile Include="Rig\GenericDevices\IReceivesDataFromRadio.cs" />
<Compile Include="Rig\GenericDevices\IRegReaderPulses.cs" />
<Compile Include="Rig\GenericDevices\IRegReaderSmart.cs" />
<Compile Include="Rig\GenericDevices\ISmartMeterReader.cs" />
<Compile Include="Rig\GenericDevices\IRestAPIAdapter.cs" />
<Compile Include="Rig\GenericDevices\IResultsProcessor.cs" />
<Compile Include="Rig\GenericDevices\ISessionDataMngmnt.cs" />

View File

@ -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<WaterMeter>
{
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<WaterMeter>
{
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<WaterMeter>
{
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<WaterMeter>
{
new WaterMeter { Disabled = false }
};
bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
reader.Object, waterMeters, 0L);
Assert.IsFalse(selected);
}
private static Mock<IRegReader> CreateReader(int position, bool smart = false)
{
var reader = new Mock<IRegReader>();
if (smart)
reader.As<ISmartMeterReader>();
reader.SetupGet(item => item.Position).Returns(position);
return reader;
}
}
}

View File

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

View File

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

View File

@ -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
{
/// <summary>
/// Direct hardware smoke test modelled after IperlHatIntegrationTests.
/// COM3 is the ALLY Touch-Read connection and COM4 is the optical source.
/// </summary>
[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<byte>();
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
? "<empty>"
: BitConverter.ToString(value).Replace('-', ' ');
}
private static string Escape(string value)
{
return value.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t");
}
}
}

View File

@ -102,6 +102,8 @@
</Choose>
<ItemGroup>
<Compile Include="Entities\MeasurementCorrectionTest.cs" />
<Compile Include="Rig\DataEntry\Uni\RegisterReaderSelectionTests.cs" />
<Compile Include="Rig\DataEntry\Uni\SmartMeterReaderFamilyTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Rig\Network\Camera\CJMS11\CameraTest.cs" />
<Compile Include="Rig\Network\Camera\KeyenceIV3G120\CameraTest.cs" />