tbf/TBF/Rig/RegisterReaders/AllyReader/AllyMeterReader.cs
Michal Buzik b0083afa10 DataEntry.UNI - timeout, autoclose improve
Introduce configurable timeouts for data entry volume reads in `IRegReaderSmart` and its implementations: added optional timeout parameters and updated default values across associated forms and methods. Updated `EntryFormCfg` to support customizable timeout settings.
2026-08-19 23:47:50 +02:00

798 lines
26 KiB
C#

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
{
public class AllyMeterReader : ComponentBase,
IDevice,
IRegReaderDatastream,
ISessionDataMngmnt,
IOperation,
IRegReaderSmart,
ISmartReader
{
private const int DataEntryCommandTimeoutMs = 5000;
private const int DefaultDataEntryOpticalTimeoutMs = 3000;
private const int MaxStoredSamples = 40000;
private const long RawVolumeModulo = 0x1000000L;
private const long RawVolumeHalfRange = RawVolumeModulo / 2;
private const long RawTimestampModulo = 0x100000000L;
private const long RawTimestampHalfRange = RawTimestampModulo / 2;
private static readonly ILog log = LogManager.GetLogger(typeof(AllyMeterReader));
private readonly object commandSync = new object();
private readonly object opticalSync = new object();
private readonly StringBuilder opticalBuffer = new StringBuilder();
private readonly List<AllyOpticalSample> opticalSamples = new List<AllyOpticalSample>();
private readonly AllyReaderCfg allyCfg;
private IAllyTransport commandTransport;
private AllyCommandService commandService;
private SerialPort opticalPort;
private bool streamEnabled;
private bool operationActive;
private bool hasPreviousRawVolume;
private bool hasPreviousRawTimestamp;
private bool hasTestStartSample;
private uint previousRawVolume;
private uint previousRawTimestamp;
private long extendedRawVolume;
private long extendedRawTimestamp;
private double beginWMState;
private double endWMState;
private double timestampSecStart;
private double timestampSecEnd;
private string lastOpticalLine;
public AllyMeterReader()
{
}
public AllyMeterReader(IComponentCfg cfg)
: base(cfg)
{
allyCfg = cfg as AllyReaderCfg;
}
public override string ToString()
{
return string.Format("{0}({1})", ClassName, Cfg.ToString(-1));
}
public string SerialNumber { get; private set; }
public AllyVersionInfo VersionInfo { get; private set; }
public DateTime? SystemTimeUtc { get; private set; }
public byte? RebootCount { get; private set; }
public double? CalibrationFactorPercent { get; private set; }
public double? ExpectedCalibrationFactorPercent { 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
{
get { return allyCfg == null ? AllyMeterSize.AutoDetect : allyCfg.ConfiguredMeterSize; }
}
public IReadOnlyList<AllyOpticalSample> OpticalSamples
{
get
{
lock (opticalSync)
{
return opticalSamples.ToArray();
}
}
}
public override void Initialize()
{
if (allyCfg == null)
throw new InvalidOperationException("ALLY reader configuration is missing.");
if (allyCfg.CommandComPortNr <= 0 || allyCfg.OptoComPortNr <= 0)
throw new InvalidOperationException("ALLY COM port configuration is invalid.");
PulsesPerLtr = allyCfg.MeterPulsesPerLiter;
QuantityUnits = "l";
log.FatalFormat("{0} initialized: {1}", Name, this);
}
public void RunDeviceBefore()
{
try
{
string text;
lock (opticalSync)
{
if (!streamEnabled || opticalPort == null || !opticalPort.IsOpen)
return;
text = opticalPort.ReadExisting();
}
if (!string.IsNullOrEmpty(text))
ProcessOpticalText(text);
}
catch (Exception ex)
{
CommFailed = true;
log.Error("ALLY optical stream read failed.", ex);
}
}
public void RunDeviceAfter()
{
}
public void StopDevice()
{
StopDataStreamProcessing();
CloseCommandTransport();
}
public void StopDevice2()
{
}
public void StartSession()
{
lock (opticalSync)
{
operationActive = false;
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
}
SerialNumber = string.Empty;
VersionInfo = null;
SystemTimeUtc = null;
RebootCount = null;
CalibrationFactorPercent = null;
ExpectedCalibrationFactorPercent = null;
CommFailed = false;
}
public void SaveMark(object mark)
{
}
public void EndSession()
{
StopDataStreamProcessing();
CloseCommandTransport();
}
public void TestIsGoingToStartSoon(Test test, int repetitionNr)
{
}
public IOperation ReadDatastreamOp()
{
return this;
}
public void Start()
{
lock (opticalSync)
{
operationActive = true;
hasTestStartSample = false;
beginWMState = 0;
endWMState = 0;
timestampSecStart = 0;
timestampSecEnd = 0;
}
StartDataStreamProcessing();
}
public Event Run()
{
return Event.ReadRegisterDone;
}
public void Stop()
{
lock (opticalSync)
{
operationActive = false;
}
StopDataStreamProcessing();
}
public void StartDataStreamProcessing()
{
GetOpticalVolumeLitersPerRawUnit();
lock (opticalSync)
{
if (streamEnabled)
return;
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
if (DebugLevel == DebugMode.Normal)
{
opticalPort = new SerialPort(
"COM" + allyCfg.OptoComPortNr,
allyCfg.OptoBaudRate,
Parity.None,
8,
StopBits.One);
opticalPort.Open();
opticalPort.DiscardInBuffer();
}
streamEnabled = true;
}
}
public void StopDataStreamProcessing()
{
lock (opticalSync)
{
streamEnabled = false;
if (opticalPort == null)
return;
try
{
if (opticalPort.IsOpen)
opticalPort.Close();
}
finally
{
opticalPort.Dispose();
opticalPort = null;
}
}
}
public string ReadOptoData()
{
lock (opticalSync)
{
return lastOpticalLine ?? string.Empty;
}
}
public string ReadSerialNumber(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return SerialNumber = "ALLY-SIMULATED";
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(int timeoutMs = DefaultDataEntryOpticalTimeoutMs)
{
return ReadDataEntryVolume(true, timeoutMs);
}
public Task<double> DataEntry_ReadEndVolume(int timeoutMs = DefaultDataEntryOpticalTimeoutMs)
{
return ReadDataEntryVolume(false, timeoutMs);
}
public AllyVersionInfo ReadVersionAndType(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return VersionInfo = AllyVersionInfo.Parse("SIMULATED,SWM003,SIMULATED");
return ExecuteCommand(service => VersionInfo = service.ReadVersionAndType(timeoutMs));
}
public DateTime ReadSystemTimeUtc(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return (SystemTimeUtc = DateTime.UtcNow).Value;
return ExecuteCommand(service => (SystemTimeUtc = service.ReadSystemTimeUtc(timeoutMs)).Value);
}
public byte ReadRebootCount(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return (RebootCount = 0).Value;
return ExecuteCommand(service => (RebootCount = service.ReadRebootCount(timeoutMs)).Value);
}
public void OpenValve(int timeoutMs)
{
ExecuteCommand(service => service.OpenValve(timeoutMs));
}
public void SetSpreadSpectrum(bool enabled, int timeoutMs)
{
ExecuteCommand(service => service.SetSpreadSpectrum(enabled, timeoutMs));
}
public void SetMeterMode(byte mode, int timeoutMs)
{
ExecuteCommand(service => service.SetMeterMode(mode, timeoutMs));
}
public void SetDiagnosticLed(byte mode, int timeoutMs)
{
ExecuteCommand(service => service.SetDiagnosticLed(mode, timeoutMs));
}
public double ResetCalibrationFactor(int timeoutMs)
{
double factor = GetResetCalibrationFactorPercent();
ExecuteCommand(service => service.SetCalibrationFactorPercent(factor, timeoutMs));
ExpectedCalibrationFactorPercent = factor;
return factor;
}
public double ReadCalibrationFactor(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
{
double simulated = ExpectedCalibrationFactorPercent ?? GetResetCalibrationFactorPercent();
CalibrationFactorPercent = simulated;
return simulated;
}
return ExecuteCommand(
service => (CalibrationFactorPercent =
service.ReadCalibrationFactorPercent(timeoutMs)).Value);
}
public void SetCalibrationFactor(double factorPercent, int timeoutMs)
{
if (!IsCalibrationFactorWithinSpecification(factorPercent))
{
throw new ArgumentOutOfRangeException(
nameof(factorPercent),
"ALLY calibration factor is outside the UI-2093 limit for the configured meter size.");
}
ExecuteCommand(service => service.SetCalibrationFactorPercent(factorPercent, timeoutMs));
ExpectedCalibrationFactorPercent = factorPercent;
}
public void SetMagneticTamperProfile(AllyMagneticTamperProfile profile, int timeoutMs)
{
ExecuteCommand(service => service.SetMagneticTamperProfile(profile, timeoutMs));
}
public void SetDisplayVolume(string eightDigitVolume, int timeoutMs)
{
ExecuteCommand(service => service.SetDisplayVolume(eightDigitVolume, timeoutMs));
}
public void SetLcdTimeout(byte seconds, int timeoutMs)
{
ExecuteCommand(service => service.SetLcdTimeout(seconds, timeoutMs));
}
public void StartOffsetLearning(ushort samples, ushort delaySeconds, int timeoutMs)
{
ExecuteCommand(service => service.StartOffsetLearning(samples, delaySeconds, timeoutMs));
}
public double GetResetCalibrationFactorPercent()
{
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths:
case AllyMeterSize.ThreeQuarterShort:
case AllyMeterSize.ThreeQuarterLong:
return 100D;
case AllyMeterSize.OneInch:
return 260D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. UI-2031 serial-number parsing is required before selecting the reset calibration factor.");
}
}
public bool IsCalibrationFactorWithinSpecification(double value)
{
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths: return value >= 76D && value <= 96D;
case AllyMeterSize.ThreeQuarterShort: return value >= 88D && value <= 108D;
case AllyMeterSize.ThreeQuarterLong: return value >= 89D && value <= 120D;
case AllyMeterSize.OneInch: return value >= 236D && value <= 281D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. Calibration-factor limits cannot be selected safely.");
}
}
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
{
if (string.IsNullOrEmpty(Name))
return 0;
int start = Name.Length;
while (start > 0 && char.IsDigit(Name[start - 1]))
start--;
int value;
return start < Name.Length && int.TryParse(Name.Substring(start), out value) ? value : 0;
}
}
public double PulsesPerLtr { get; set; }
public double LtrsPerPulse { get { return PulsesPerLtr > 0 ? 1D / PulsesPerLtr : 0D; } }
public string QuantityUnits { get; set; }
public int WMPulses { get { return (int)Math.Round(WMVolume * PulsesPerLtr); } }
public int WMRefPulses
{
get
{
return StateMachine.ControlBoardMain == null
? 0
: StateMachine.ControlBoardMain.RefPulses;
}
}
public double WMVolume { get { return Math.Abs(EndWMState - BeginWMState); } }
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, int timeoutMs)
{
return Task.Run(() =>
{
if (DebugLevel != DebugMode.Normal)
{
return Double.NaN;
}
try
{
Start();
int effectiveTimeoutMs = Math.Max(1, timeoutMs);
DateTime deadline = DateTime.UtcNow.AddMilliseconds(effectiveTimeoutMs);
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",
effectiveTimeoutMs,
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)
{
opticalBuffer.Append(text);
while (true)
{
string buffered = opticalBuffer.ToString();
int lineEnd = buffered.IndexOf('\n');
if (lineEnd < 0)
return;
string line = buffered.Substring(0, lineEnd + 1);
opticalBuffer.Remove(0, lineEnd + 1);
lastOpticalLine = line;
AllyOpticalSample sample;
if (!AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample))
continue;
ExtendRawVolume(sample.RawVolume);
ExtendRawTimestamp(sample.RawTimestamp);
sample.ExtendedVolumeLiters =
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
sample.ElapsedSeconds = extendedRawTimestamp / 8192D;
if (opticalSamples.Count == MaxStoredSamples)
opticalSamples.RemoveAt(0);
opticalSamples.Add(sample);
endWMState = sample.ExtendedVolumeLiters;
timestampSecEnd = sample.ElapsedSeconds;
if (operationActive && !hasTestStartSample)
{
hasTestStartSample = true;
beginWMState = sample.ExtendedVolumeLiters;
timestampSecStart = sample.ElapsedSeconds;
}
}
}
}
private void ExtendRawVolume(uint rawVolume)
{
if (!hasPreviousRawVolume)
{
hasPreviousRawVolume = true;
previousRawVolume = rawVolume;
extendedRawVolume = rawVolume;
return;
}
long delta = (long)rawVolume - previousRawVolume;
if (delta < -RawVolumeHalfRange)
delta += RawVolumeModulo;
else if (delta > RawVolumeHalfRange)
delta -= RawVolumeModulo;
extendedRawVolume += delta;
previousRawVolume = rawVolume;
}
private void ExtendRawTimestamp(uint rawTimestamp)
{
if (!hasPreviousRawTimestamp)
{
hasPreviousRawTimestamp = true;
previousRawTimestamp = rawTimestamp;
extendedRawTimestamp = rawTimestamp;
return;
}
long delta = (long)rawTimestamp - previousRawTimestamp;
if (delta < -RawTimestampHalfRange)
delta += RawTimestampModulo;
else if (delta > RawTimestampHalfRange)
delta -= RawTimestampModulo;
extendedRawTimestamp += delta;
previousRawTimestamp = rawTimestamp;
}
private double GetOpticalVolumeLitersPerRawUnit()
{
// The optical format scales volume by flow-tube size: raw / 16000
// for 5/8", 2 * raw / 16000 for 3/4", and 4 * raw / 16000 for 1".
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths: return 1D / 16000D;
case AllyMeterSize.ThreeQuarterShort:
case AllyMeterSize.ThreeQuarterLong: return 2D / 16000D;
case AllyMeterSize.OneInch: return 4D / 16000D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. UI-2031 serial-number parsing is required before decoding optical volume.");
}
}
private void ResetVolumeState()
{
hasPreviousRawVolume = false;
hasPreviousRawTimestamp = false;
hasTestStartSample = false;
previousRawVolume = 0;
previousRawTimestamp = 0;
extendedRawVolume = 0;
extendedRawTimestamp = 0;
beginWMState = 0;
endWMState = 0;
timestampSecStart = 0;
timestampSecEnd = 0;
}
private AllyCommandService GetCommandService()
{
lock (commandSync)
{
if (commandService != null)
return commandService;
commandTransport = new AllySerialTransportBuilder()
.WithPort("COM" + allyCfg.CommandComPortNr)
.WithBaudRate(allyCfg.CommandBaudRate)
.WithDataBits(8)
.WithParity(Parity.None)
.WithStopBits(StopBits.One)
.Build();
commandService = new AllyCommandService(commandTransport);
return commandService;
}
}
private T ExecuteCommand<T>(Func<AllyCommandService, T> command, T simulatedValue = default(T))
{
if (DebugLevel != DebugMode.Normal)
return simulatedValue;
lock (commandSync)
{
try
{
T value = command(GetCommandService());
CommFailed = false;
return value;
}
catch
{
CommFailed = true;
CloseCommandTransport();
throw;
}
}
}
private void ExecuteCommand(Action<AllyCommandService> command)
{
if (DebugLevel != DebugMode.Normal)
return;
ExecuteCommand(
service =>
{
command(service);
return true;
});
}
private void CloseCommandTransport()
{
lock (commandSync)
{
if (commandTransport != null)
commandTransport.Dispose();
commandTransport = null;
commandService = null;
}
}
}
}