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:
2026-08-19 14:33:06 +02:00
parent 6c3c6cd0ef
commit d12ee008db
15 changed files with 857 additions and 16 deletions
+1 -1
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) =>
{
@@ -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));
}
}
}
+1 -1
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) =>
{
+2 -2
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; }
}
}
}
@@ -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
}
}
}
}
+1
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" />