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:
@@ -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) =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user