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:
parent
6c3c6cd0ef
commit
d12ee008db
@ -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) =>
|
||||
{
|
||||
|
||||
38
TBF/Rig/DataEntry/Uni/DataEntryWatermark.cs
Normal file
38
TBF/Rig/DataEntry/Uni/DataEntryWatermark.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
TBF/Rig/DataEntry/Uni/RegisterReaderSelection.cs
Normal file
152
TBF/Rig/DataEntry/Uni/RegisterReaderSelection.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) =>
|
||||
{
|
||||
|
||||
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
TBF/Rig/GenericDevices/ISmartMeterReader.cs
Normal file
10
TBF/Rig/GenericDevices/ISmartMeterReader.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
95
TBFTests/Rig/DataEntry/Uni/RegisterReaderSelectionTests.cs
Normal file
95
TBFTests/Rig/DataEntry/Uni/RegisterReaderSelectionTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
TBFTests/Rig/DataEntry/Uni/SmartMeterReaderFamilyTests.cs
Normal file
34
TBFTests/Rig/DataEntry/Uni/SmartMeterReaderFamilyTests.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user