- Enhance `Stop` logs with `registerReader` and `optoSerialPort` details for improved traceability.
3625 lines
126 KiB
C#
3625 lines
126 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Ports;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml.Linq;
|
|
using Common;
|
|
using Config.Entities;
|
|
using log4net;
|
|
using NHibernate;
|
|
using NHibernate.Hql.Ast;
|
|
using Sensus.iPerl.NfcHandler;
|
|
using TBF.Rig.Generic;
|
|
using TBF.Rig.GenericDevices;
|
|
using TBF.Rig.RegisterReaders.GenesisRegReader.common;
|
|
using TBF.Rig.RegisterReaders.GenesisRegReader.communication;
|
|
using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis.DataPackages.MeasurementRecords;
|
|
using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Genesis.Protocols.StreamingProtocol;
|
|
using TBF.Rig.RegisterReaders.GenesisRegReader.communication.Utils;
|
|
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
|
|
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
|
|
|
|
|
|
namespace TBF.Rig.RegisterReaders.GenesisRegReader.implementations
|
|
{
|
|
/// <summary>
|
|
/// based on IPerlReader class
|
|
/// </summary>
|
|
public class GenesisSmartReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation,
|
|
ISmartReader, IRegReaderSmart
|
|
|
|
{
|
|
private static readonly ILog log = LogManager.GetLogger(typeof(GenesisSmartReader));
|
|
private static readonly ILog logStream = LogManager.GetLogger("StreamData");
|
|
|
|
public override string ToString()
|
|
{
|
|
try
|
|
{
|
|
string cfgText;
|
|
|
|
try
|
|
{
|
|
if (genesisHeadCfg != null)
|
|
cfgText = genesisHeadCfg.ToString(-1);
|
|
else if (Cfg != null)
|
|
cfgText = Cfg.ToString();
|
|
else
|
|
cfgText = "<null cfg>";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
cfgText = $"<cfg ToString failed: {ex.Message}>";
|
|
}
|
|
|
|
return $"{GetType().Name}({cfgText})";
|
|
}
|
|
catch
|
|
{
|
|
return GetType().Name;
|
|
}
|
|
}
|
|
|
|
#if TURA_SPECIAL
|
|
public const int OptoDataBufferSize = 250000;
|
|
#else
|
|
public const int OptoDataBufferSize = 40000;
|
|
|
|
/// Opto data count is not limitted by the buffer size
|
|
#endif
|
|
public const string OptoDataDirectory = "C:\\TBF\\ProcessData";
|
|
|
|
public const int StartOptoDataCount = OptoDataBufferSize / 2;
|
|
public const int EndOptoDataCount = OptoDataBufferSize - StartOptoDataCount;
|
|
|
|
public const int
|
|
StartEndFilterSamplesCount2 =
|
|
2; //20 /// StartEndFilterSamplesCount = 2 * StartEndFilterSamplesCount2 + 1
|
|
|
|
public const int FeatureVectorSize = 9;
|
|
|
|
private OptoHeadTest _optoHeadTest;
|
|
|
|
public OptoHeadTest OptoHeadTest
|
|
{
|
|
get
|
|
{
|
|
if (_optoHeadTest == null)
|
|
_optoHeadTest = new OptoHeadTest(this);
|
|
return _optoHeadTest;
|
|
}
|
|
set { _optoHeadTest = value; }
|
|
}
|
|
|
|
|
|
readonly GenesisCfg genesisHeadCfg;
|
|
|
|
string ISmartReader.CommInterface => _commInterface;
|
|
|
|
public int RfidComPortNr => genesisHeadCfg?.RfidComPortNr ?? 0;
|
|
|
|
public bool CommFailed { get; set; }
|
|
public bool Disabled { get; set; }
|
|
|
|
public int OptoComPortNr => genesisHeadCfg?.OptoComPortNr ?? 0;
|
|
|
|
public int MuxBoardNrOrGroup14 => genesisHeadCfg?.MuxBoardNr ?? 0;
|
|
|
|
public int Group => genesisHeadCfg?.Group ?? 0;
|
|
|
|
public MeterType MeterType => genesisHeadCfg?.MeterType ?? MeterType.AutoDetect;
|
|
|
|
public CommunicationInterface CommInterface => genesisHeadCfg?.CommunicationInterface ?? default;
|
|
|
|
public int Position
|
|
{
|
|
get
|
|
{
|
|
int firstDigitPos = Name.IndexOfAny(new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' });
|
|
int position;
|
|
return (firstDigitPos < 0)
|
|
? 0
|
|
: (int.TryParse(Name.Substring(firstDigitPos), out position) ? position : 0);
|
|
}
|
|
}
|
|
|
|
public RegisterReaderType RegisterReaderType
|
|
{
|
|
get { return RegisterReaderType.DataStream; }
|
|
}
|
|
|
|
public double PulsesPerLtr
|
|
{
|
|
get { return 1000.0; }
|
|
set { }
|
|
}
|
|
|
|
public double LtrsPerPulse
|
|
{
|
|
get { return 1 / PulsesPerLtr; }
|
|
}
|
|
|
|
public string QuantityUnits { get; set; }
|
|
|
|
public double CalibTarget => genesisHeadCfg?.ProcParams?.CalibTarget ?? 0.0;
|
|
public int FactorLimitLo => genesisHeadCfg?.ProcParams?.FactorLimitLo ?? 0;
|
|
public int FactorLimitHi => genesisHeadCfg?.ProcParams?.FactorLimitHi ?? 0;
|
|
|
|
|
|
public Counting InitFlowDir
|
|
{
|
|
get
|
|
{
|
|
return (genesisHeadCfg != null && genesisHeadCfg.ProcParams != null)
|
|
? genesisHeadCfg.ProcParams.Counting
|
|
: Counting.Arbitrary;
|
|
}
|
|
}
|
|
|
|
|
|
/// Properties set by the Begin and the End form
|
|
public string SerialNr
|
|
{
|
|
get
|
|
{
|
|
if (ConfigStruct != null)
|
|
return ConfigStruct.GetPcbNrString();
|
|
else if (simulatedPcbNr != null)
|
|
return simulatedPcbNr;
|
|
else
|
|
return string.Empty;
|
|
}
|
|
set { simulatedPcbNr = value; }
|
|
}
|
|
|
|
//public bool Disabled;
|
|
//public bool CommFailed;
|
|
public int ResultCode;
|
|
|
|
|
|
string extraDataPath;
|
|
|
|
public string ExtraDataPath
|
|
{
|
|
get { return extraDataPath; }
|
|
}
|
|
|
|
float[] x;
|
|
|
|
public float[] X
|
|
{
|
|
get { return x; }
|
|
}
|
|
|
|
|
|
private static int iChanelsCount = 3;
|
|
private int firstChanel;
|
|
|
|
/// <summary>
|
|
/// Passed to OptoTelegramRaw.UpdateFromString(...)
|
|
/// </summary>
|
|
double[] volumeRawExtLast;
|
|
|
|
double[] timestampExtLast;
|
|
|
|
FlowDirectionDetection flowDirectionDetection;
|
|
|
|
public bool PositiveCounting;
|
|
|
|
|
|
public ConfigStruct ConfigStruct;
|
|
|
|
/// ConfigStruct of WM obtained or updated by iPerlCommunication
|
|
public CalibrationStruct CalibrationStruct;
|
|
|
|
/// CalibrationStruct of WM obtained or updated by iPerlCommunication
|
|
public CalibrationStructV4 CalibrationStructV4;
|
|
|
|
/// CalibrationStruct of WM obtained or updated by iPerlCommunication
|
|
|
|
public Byte OrigTestModeConfig;
|
|
|
|
/// Written to by StartTestingSealedMeter(), read from by EndTestingSealedMeter()
|
|
|
|
public ushort OrigCalibFactor;
|
|
|
|
public ushort CalibFactor
|
|
{
|
|
get
|
|
{
|
|
return (CalibrationStruct != null)
|
|
? CalibrationStruct.Calibration
|
|
: ((CalibrationStructV4 != null)
|
|
? CalibrationStructV4.Calibration
|
|
: (ushort)0);
|
|
}
|
|
}
|
|
|
|
public ushort OrigCalibFactorLNA;
|
|
|
|
public ushort CalibFactorLNA
|
|
{
|
|
get { return (CalibrationStructV4 != null) ? CalibrationStructV4.CalibrationLNA : (ushort)0; }
|
|
}
|
|
|
|
|
|
public double Q2ErrWOCorrection;
|
|
public int Q2CorrRL;
|
|
public int Q2CorrLR;
|
|
|
|
public double Diff2Hz8Hz;
|
|
public bool Hz2CorrectionDone;
|
|
public int Hz2Correction;
|
|
|
|
public string FWVersion
|
|
{
|
|
get
|
|
{
|
|
return (CalibrationStruct != null)
|
|
? CalibrationStruct.FWVersionStr()
|
|
: ((CalibrationStructV4 != null)
|
|
? CalibrationStructV4.FWVersionStr()
|
|
: string.Empty);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary> Result of the last test used to calculate Q2 correction factors, etc </summary>
|
|
public Results.Entities.MeterTestRslt LastTestResult;
|
|
|
|
public Results.Entities.MeterTestRslt LastTestResult2;
|
|
|
|
|
|
///
|
|
/// Required for IRegisterReader interface
|
|
///
|
|
public int WMPulses
|
|
{
|
|
get { return wmPulses; }
|
|
}
|
|
|
|
public int WMRefPulses
|
|
{
|
|
get { return wmRefPulses; }
|
|
}
|
|
|
|
public double BeginWMState
|
|
{
|
|
get { return ResolveNaNDouble(beginWMState); }
|
|
}
|
|
|
|
double ISmartReader.EndWMState { get; set; }
|
|
double ISmartReader.BeginWMState { get; set; }
|
|
double ICommonRegReader.EndWMState { get; set; }
|
|
double ICommonRegReader.BeginWMState { get; set; }
|
|
|
|
public double EndWMState
|
|
{
|
|
get { return ResolveNaNDouble(endWMState); }
|
|
}
|
|
|
|
public double WMVolume
|
|
{
|
|
get { return ResolveNaNDouble(wmVolume); }
|
|
}
|
|
|
|
public double WMTestTime
|
|
{
|
|
get { return ResolveNaNDouble(wmTestTime); }
|
|
}
|
|
|
|
string simulatedPcbNr = null;
|
|
|
|
double ResolveNaNDouble(double d)
|
|
{
|
|
if (Double.IsNaN(d))
|
|
{
|
|
return 0.0;
|
|
}
|
|
else
|
|
return d;
|
|
}
|
|
|
|
int wmPulses;
|
|
int wmRefPulses;
|
|
double beginWMState;
|
|
double endWMState;
|
|
double wmVolume;
|
|
double wmTestTime;
|
|
|
|
|
|
/// <summary>
|
|
/// New calibration factor calculated from the original factor (argument)
|
|
/// and results of any test(s).
|
|
/// Uses also: this.CalibTarget, this.VolumeLtrStart, this.VolumeLtrEnd
|
|
/// Side effects: this.OrigCalibFactor, this.PositiveCounting
|
|
/// </summary>
|
|
/// <param name="adjustTestResult">Test result for calculations</param>
|
|
/// <param name="originalCalibrationFactor">Original calibration factor</param>
|
|
/// <param name="factorLimitLo">Lower limit for the calibration factor</param>
|
|
/// <param name="factorLimitHi">Upper limit for the calibration factor</param>
|
|
/// <returns>New calibration factor or 0 (= Out of range)</returns>
|
|
public UInt16 CalculateNewCalibFactor(Results.Entities.MeterTestRslt adjustTestResult,
|
|
UInt16 originalCalibrationFactor, UInt16 factorLimitLo, UInt16 factorLimitHi)
|
|
{
|
|
double meterVolume = adjustTestResult.VolumeMeter;
|
|
double targetVolume = adjustTestResult.VolumeRef * (1.0f + CalibTarget / 100.0f);
|
|
|
|
OrigCalibFactor = originalCalibrationFactor;
|
|
|
|
if (meterVolume > 1E-2)
|
|
{
|
|
PositiveCounting = VolumeLtrEnd > VolumeLtrStart;
|
|
|
|
UInt16 newFactor = (UInt16)((double)originalCalibrationFactor * targetVolume / meterVolume + 0.5);
|
|
log.InfoFormat("Calibration factor: orig={0} new={1} V_iperl={2} V_ref={3} V_target={4}",
|
|
originalCalibrationFactor,
|
|
newFactor,
|
|
meterVolume.ToString("F3"),
|
|
adjustTestResult.VolumeRef.ToString("F3"),
|
|
targetVolume.ToString("F3"));
|
|
|
|
|
|
if (newFactor < factorLimitLo || newFactor > factorLimitHi) return 0;
|
|
|
|
return newFactor;
|
|
}
|
|
else
|
|
{
|
|
log.ErrorFormat("Calibration factor: orig={0} new={0} (unchanged!) V_iperl={1}",
|
|
originalCalibrationFactor,
|
|
meterVolume.ToString("F3"));
|
|
return originalCalibrationFactor; /// Too small volume in the denominator -> no correction at all
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Q2 correction factor calculated from the last test (Q2).
|
|
/// This factor should be used only for R800 meters.
|
|
/// </summary>
|
|
/// <param name="q2TestResult">A test result from which to calculate the factor</param>
|
|
/// <param name="nominalFlow">Nominal flow in m3/h</param>
|
|
/// <param name="currentFactor">0 or the current Q2 correction factor when updating the factor</param>
|
|
/// <returns>Calculated Q2 correction factor</returns>
|
|
public double CalculateQ2CorrectionFactor(Results.Entities.MeterTestRslt currentQ2Result, int currentFactor,
|
|
double nominalFlow, double errorTarget = 0)
|
|
{
|
|
double nominalTestFlowLph = Units.ConvertTo(Unit.lph, nominalFlow);
|
|
double volumeRefShiftedToTarget = currentQ2Result.VolumeRef * (1.0 + errorTarget / 100.0);
|
|
double q2adjErrorShiftedToTarget =
|
|
Config.Formulas.ErrorFromVolumes(currentQ2Result.VolumeMeter, volumeRefShiftedToTarget);
|
|
|
|
double A = 16.0 / ScalingFactor(); /// Raw units per ml: DN15=16, DN20=8, DN25=4, DN32=2, DN40=1
|
|
const double B = 8.0; /// Raw units per minute, 8
|
|
const double C = B * 60.0; /// Raw units per hour, 480
|
|
double D = C / A; /// ml correction per hour
|
|
double F = D / (nominalTestFlowLph * 10.0); /// Error corrected with 8 Raw Units per minute [%]
|
|
double G = F / B; /// Error corrected with 1 Raw Units per minute [%]
|
|
|
|
/// Do not change the factor for an invalid measurement (q2adjResult.VolumeMeter == 0)
|
|
double q2CorrectionFactor = (Math.Abs(currentQ2Result.VolumeMeter) <= float.Epsilon)
|
|
? Convert.ToDouble(currentFactor)
|
|
: Convert.ToDouble(currentFactor) - (q2adjErrorShiftedToTarget / G) *
|
|
(volumeRefShiftedToTarget / currentQ2Result.VolumeMeter);
|
|
|
|
log.WarnFormat(
|
|
"CalculateQ2CorrectionFactor() : Pos={0}, PCB#={1}, Error={2}%, Target={3}%, Current factor={4} New factor={5}",
|
|
Name,
|
|
SerialNr,
|
|
currentQ2Result.Error.ToString("F2"),
|
|
errorTarget.ToString("F3"),
|
|
currentFactor.ToString("F1"),
|
|
q2CorrectionFactor.ToString("F1"));
|
|
|
|
return q2CorrectionFactor;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 2 Hz correction factor calculated from two Q3 tests - done at 2Hz and at 8Hz.
|
|
/// This factors should be used only for DN32 and DN40 meters.
|
|
/// </summary>
|
|
/// <param name="resultAt2Hz">Test result @2Hz from which to calculate the factor</param>
|
|
/// <param name="resultAt8Hz">Test result @8Hz from which to calculate the factor</param>
|
|
/// <param name="hz2CorrectionFactor">The calculated Q2 correction factor</param>
|
|
/// <returns>true = OK, false = failed</returns>
|
|
public bool Calculate2HzCorrectionFactor(Results.Entities.MeterTestRslt resultAt2Hz,
|
|
Results.Entities.MeterTestRslt resultAt8Hz,
|
|
out double diff2Hz8Hz, out int hz2CorrectionFactor)
|
|
{
|
|
hz2CorrectionFactor = 0;
|
|
diff2Hz8Hz = 0;
|
|
|
|
if ((resultAt2Hz == null) || (resultAt8Hz == null))
|
|
{
|
|
return false; /// Test result @2Hz and/or @8Hz is missing ==> water meter failed
|
|
}
|
|
|
|
diff2Hz8Hz = resultAt2Hz.Error - resultAt8Hz.Error;
|
|
|
|
if (Math.Abs(diff2Hz8Hz) > 2.5) return false; /// Difference of errors > 2.5 % ==> water meter failed
|
|
|
|
hz2CorrectionFactor = -1 * (int)Math.Round(10 * diff2Hz8Hz);
|
|
|
|
log.WarnFormat("2Hz correction: Pos={0}, PCB#={1}, corrFactor={2}, erro@2Hz={3}%, erro@8Hz={4}%",
|
|
Name,
|
|
SerialNr,
|
|
hz2CorrectionFactor,
|
|
resultAt2Hz.Error.ToString("F2"),
|
|
resultAt8Hz.Error.ToString("F2"));
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Store/update values to be used as a part of the opto-data log file name.
|
|
/// Stop data stream processing and saving, if it is enabled.
|
|
/// </summary>
|
|
/// <param name="test">Currently executed test</param>
|
|
/// <param name="repetitionNr">Currently executed repetition number</param>
|
|
public void TestIsGoingToStartSoon(Test _test, int _repetitionNr)
|
|
{
|
|
|
|
log.Debug($"TestIsGoingToStartSoon({_test.Name}, {_repetitionNr})");
|
|
/// Store/update values to be used as a part of the opto-data log file name
|
|
this.test = _test;
|
|
this.repetitionNr = _repetitionNr;
|
|
|
|
if (IsDataStreamProcessing())
|
|
{
|
|
StopDataStreamProcessing();
|
|
|
|
/// Dummy '#### start test ####' and '#### end of test ####' marks are added
|
|
/// to the raw data file on request of Joern Goege
|
|
if (optoDataCount >= 100 && optoDataCount <= optoData.Length &&
|
|
optoData[40].Flags == OptoTelegramFlags.OK &&
|
|
optoData[optoDataCount - 40].Flags == OptoTelegramFlags.OK)
|
|
{
|
|
optoData[40].Flags = OptoTelegramFlags.OK_TestStart;
|
|
optoData[optoDataCount - 40].Flags = OptoTelegramFlags.OK_TestEnd;
|
|
}
|
|
|
|
DataStreamPostProcessing();
|
|
|
|
string pcbNr = (ConfigStruct != null) ? ConfigStruct.GetPcbNrString() : "UnknownPcbNr";
|
|
string
|
|
wmPosition = Name.Substring(5); /// WMPosition is extracted from a component name in form 'iPerl#'
|
|
if (wmPosition.Length == 1) wmPosition = "0" + wmPosition;
|
|
string cycleStartTime = StateMachine.CycleStartTimeStamp.ToString("HH_mm_ss");
|
|
///
|
|
string relativeDirectory = Path.Combine(StateMachine.CycleStartTimeStamp.ToString("yy"),
|
|
StateMachine.CycleStartTimeStamp.ToString("MM"),
|
|
StateMachine.CycleStartTimeStamp.ToString("dd"));
|
|
string directory = Path.Combine(OptoDataDirectory, relativeDirectory);
|
|
string fileName = string.Format("{0}_{1}_{2}_{3}.txt", pcbNr, wmPosition, "WM", cycleStartTime);
|
|
|
|
if (SaveOptoDataToFile(directory, fileName))
|
|
{
|
|
extraDataPath = Path.Combine(relativeDirectory, fileName);
|
|
}
|
|
}
|
|
|
|
|
|
try
|
|
{
|
|
log.DebugFormat("TestIsGoingToStartSoon - OpenOptoSerialPortIfNotInit called");
|
|
OpenOptoSerialPortIfNotInit ($"COM{genesisHeadCfg.OptoComPortNr}",
|
|
115200,
|
|
Parity.None,
|
|
8,
|
|
StopBits.One,
|
|
Handshake.None);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
///
|
|
Test test;
|
|
|
|
int repetitionNr;
|
|
|
|
|
|
///
|
|
/// Indices to determine centers of start / end samples
|
|
///
|
|
public int TestStartTelegramIx;
|
|
|
|
public int TestEndTelegramIx;
|
|
int endTelegramIdx1;
|
|
int endTelegramIdx2;
|
|
int endTelegramIdx3;
|
|
|
|
int currentTelegramIx;
|
|
bool startSampleAcquired;
|
|
|
|
///
|
|
/// Timestamp from the opto telegram
|
|
///
|
|
private double[] lastTimestamp;
|
|
|
|
private double[] timestampSec;
|
|
private double[] timestampSec0;
|
|
|
|
/// Test start volume for metrology in seconds
|
|
public double TimestampSecStart
|
|
{
|
|
get
|
|
{
|
|
if (TestStartTelegramIx < 0 || TestStartTelegramIx >= optoDataCount)
|
|
return 0;
|
|
|
|
return AverageCachedTime(_recalculatedStartEndByChannel, 0);
|
|
}
|
|
}
|
|
|
|
/// Test end time for metrology in seconds
|
|
|
|
public double TimestampSecEnd
|
|
{
|
|
get
|
|
{
|
|
if (TestEndTelegramIx < 0 || TestEndTelegramIx >= optoDataCount)
|
|
return 0;
|
|
|
|
return AverageCachedTime(_recalculatedStartEndByChannel, 1);
|
|
}
|
|
}
|
|
|
|
///
|
|
public bool NoSamples
|
|
{
|
|
get
|
|
{
|
|
return optoDataCount <= 0 ||
|
|
TestStartTelegramIx < 0 ||
|
|
TestEndTelegramIx < 0 ||
|
|
TestStartTelegramIx >= optoDataCount ||
|
|
TestEndTelegramIx >= optoDataCount ||
|
|
_recalculatedStartEndByChannel == null ||
|
|
TimestampSecEnd < TimestampSecStart;
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Volume of water from the opto telegram
|
|
///
|
|
private double[] lastVolumeRaw;
|
|
|
|
/// Last read raw volume
|
|
private double[] volumeLtr;
|
|
|
|
private double[] volumeLtr0;
|
|
|
|
private int channel0 = -1;
|
|
|
|
|
|
private double Average(double[] data)
|
|
{
|
|
return data.Sum() / data.Length;
|
|
}
|
|
|
|
public double VolumeLtrStartCh1 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 0, 0);
|
|
public double VolumeLtrStartCh2 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 1, 0);
|
|
public double VolumeLtrStartCh3 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 2, 0);
|
|
|
|
public double VolumeLtrEndCh1 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 0, 1);
|
|
public double VolumeLtrEndCh2 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 1, 1);
|
|
public double VolumeLtrEndCh3 => NoSamples ? 0 : GetCachedVolume(_recalculatedStartEndByChannel, 2, 1);
|
|
|
|
public double TimestampSecStartCh1 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 0, 0);
|
|
public double TimestampSecStartCh2 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 1, 0);
|
|
public double TimestampSecStartCh3 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 2, 0);
|
|
|
|
public double TimestampSecEndCh1 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 0, 1);
|
|
public double TimestampSecEndCh2 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 1, 1);
|
|
public double TimestampSecEndCh3 => NoSamples ? 0 : GetCachedTime(_recalculatedStartEndByChannel, 2, 1);
|
|
public double VolumeLtrStartRaw => NoSamples ? 0 : AverageCachedVolume(_rawStartEndByChannel, 0);
|
|
public double VolumeLtrEndRaw => NoSamples ? 0 : AverageCachedVolume(_rawStartEndByChannel, 1);
|
|
|
|
|
|
|
|
/// Test start volume for metrology in liters
|
|
public double VolumeLtrStart
|
|
{
|
|
get
|
|
{
|
|
return VolumeLtrStartRaw;
|
|
}
|
|
}
|
|
|
|
/// Test end volume for metrology in liters
|
|
public double VolumeLtrEnd
|
|
{
|
|
get
|
|
{
|
|
return VolumeLtrEndRaw;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
OptoTelegramRaw[] optoData;
|
|
int optoDataCount;
|
|
|
|
/// Real opto deta count, can be larger then optoData.Length
|
|
///
|
|
OptoTelegramRaw toBeFlushed;
|
|
|
|
///
|
|
/// Opto serial port and worker thread related private variables
|
|
///
|
|
public ISerialDriver optoSerialPort;
|
|
private volatile bool startDataProcessing = false;
|
|
|
|
|
|
/// Start UP flush
|
|
private volatile bool _startupFlushActive;
|
|
private DateTime _startupFlushUntilUtc;
|
|
private int _startupFlushIgnoredLines;
|
|
private DateTime? _startupFlushFirstIgnoredUtc;
|
|
private DateTime? _startupFlushLastIgnoredUtc;
|
|
private readonly object _startupFlushSync = new object();
|
|
|
|
private const int StartupFlushMs = 1500; // or 1000 if you want 1 second
|
|
/// ~ Start UP flush
|
|
|
|
public GenesisSmartReader()
|
|
{
|
|
}
|
|
|
|
public GenesisSmartReader(Generic.IComponentCfg cfg)
|
|
: base(cfg)
|
|
{
|
|
genesisHeadCfg = cfg as GenesisCfg;
|
|
}
|
|
|
|
private readonly Func<ISerialDriver> _serialFactory;
|
|
|
|
public GenesisSmartReader(Generic.IComponentCfg cfg, Func<ISerialDriver> serialFactory = null)
|
|
: base(cfg)
|
|
{
|
|
genesisHeadCfg = cfg as GenesisCfg;
|
|
_serialFactory = serialFactory;
|
|
}
|
|
|
|
|
|
public override void Initialize()
|
|
{
|
|
log.DebugFormat("Initialize() called");
|
|
x = new float[FeatureVectorSize];
|
|
flowDirectionDetection = new FlowDirectionDetection();
|
|
|
|
volumeRawExtLast = new double[iChanelsCount];
|
|
timestampExtLast = new double[iChanelsCount];
|
|
|
|
lastTimestamp = new double[iChanelsCount];
|
|
timestampSec = new double[iChanelsCount];
|
|
timestampSec0 = new double[iChanelsCount];
|
|
|
|
lastVolumeRaw = new double[iChanelsCount]; /// Last read raw volume
|
|
volumeLtr = new double[iChanelsCount];
|
|
volumeLtr0 = new double[iChanelsCount];
|
|
|
|
/// Allocate memory for opto-data from iPerl
|
|
optoData = new OptoTelegramRaw[OptoDataBufferSize];
|
|
for (int i = 0; i < OptoDataBufferSize; i++)
|
|
{
|
|
optoData[i] = new OptoTelegramRaw();
|
|
}
|
|
|
|
toBeFlushed = new OptoTelegramRaw();
|
|
|
|
dataStreamState = DataStreamState.Flush;
|
|
synchronized = false;
|
|
synchronized2 = false;
|
|
partOfTelegram = string.Empty;
|
|
optoSerialPort = null;
|
|
startDataProcessing = false;
|
|
|
|
|
|
log.DebugFormat($"Initialize - OpenOptoSerialPort: {genesisHeadCfg.OptoComPortNr}");
|
|
if (DebugLevel == DebugMode.Normal)
|
|
{
|
|
/// Open serial port: 9600 Bd, 8 data bits, 1 stop bit, no parity
|
|
/// Check whether head is connected, working
|
|
try
|
|
{
|
|
OpenOptoSerialPortIfNotInit($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One,
|
|
Handshake.None);
|
|
ResetDataBuffer();
|
|
CloseOptoSerialPort();
|
|
log.FatalFormat($"{Name} initialized: {this}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.FatalFormat($"{Name} initialization failed: {ex}");
|
|
throw;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.FatalFormat($"{Name} simulated: {this}");
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear data related to a specific water meter
|
|
/// </summary>
|
|
public void StartSession()
|
|
{
|
|
log.DebugFormat("StartSession() called");
|
|
ResultCode = 0;
|
|
|
|
Disabled = false;
|
|
CommFailed = false;
|
|
|
|
ConfigStruct = null;
|
|
CalibrationStruct = null;
|
|
CalibrationStructV4 = null;
|
|
|
|
OrigTestModeConfig = 0;
|
|
|
|
LastTestResult = null;
|
|
LastTestResult2 = null;
|
|
|
|
OrigCalibFactor = 0;
|
|
OrigCalibFactorLNA = 0;
|
|
Q2ErrWOCorrection = 0;
|
|
Q2CorrRL = 0;
|
|
Q2CorrLR = 0;
|
|
|
|
simulatedPcbNr = null;
|
|
|
|
dataStreamState = DataStreamState.Flush;
|
|
|
|
currentFlowDir = InitFlowDir;
|
|
|
|
ResetBlockCountersAndState();
|
|
|
|
startDataProcessing = false;
|
|
//Start connection
|
|
log.DebugFormat($"StartSession - OpenOptoSerialPortIfNotInit: {genesisHeadCfg.OptoComPortNr}");
|
|
if (DebugLevel == DebugMode.Normal)
|
|
{
|
|
/// Open serial port: 9600 Bd, 8 data bits, 1 stop bit, no parity
|
|
/// Check whether head is connected, working
|
|
try
|
|
{
|
|
OpenOptoSerialPortIfNotInit($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One,
|
|
Handshake.None);
|
|
log.FatalFormat($"{Name} StartSession - OpenOptoSerialPortIfNotInit: {genesisHeadCfg.OptoComPortNr}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.FatalFormat($"{Name} initialization failed: {ex}");
|
|
throw;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.FatalFormat($"{Name} simulated: {this}");
|
|
}
|
|
}
|
|
|
|
public void SaveMark(object mark)
|
|
{
|
|
/// TODO
|
|
}
|
|
|
|
public void EndSession()
|
|
{
|
|
StopDataStreamProcessing();
|
|
}
|
|
|
|
|
|
Counting currentFlowDir;
|
|
|
|
///
|
|
// public OptoHeadState CheckFlowDirection()
|
|
// {
|
|
// return (flowDirectionDetection != null) ? flowDirectionDetection.CheckFlowDirection(currentFlowDir, Name) : OptoHeadState.DirNok;
|
|
// }
|
|
// ///
|
|
// public void ChangeFlowDirection()
|
|
// {
|
|
// switch (InitFlowDir)
|
|
// {
|
|
// case Counting.Positive:
|
|
// currentFlowDir = Counting.Negative;
|
|
// break;
|
|
//
|
|
// case Counting.Negative:
|
|
// currentFlowDir = Counting.Positive;
|
|
// break;
|
|
//
|
|
// case Counting.Arbitrary:
|
|
// default:
|
|
// currentFlowDir = Counting.Arbitrary;
|
|
// break;
|
|
// }
|
|
//
|
|
// if (flowDirectionDetection != null) flowDirectionDetection.ClearFifo(); /// Clear FIFO for flow direction detection
|
|
// }
|
|
|
|
|
|
public void RunDeviceBefore()
|
|
{
|
|
//log.DebugFormat("RunDeviceBefore() called");
|
|
if (DebugLevel == DebugMode.Normal)
|
|
{
|
|
try
|
|
{
|
|
//ReadOptoData(dataStreamState);
|
|
// no queue draining here anymore
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugLevel = DebugMode.FailureDuringOperation;
|
|
|
|
log.FatalFormat("Opto-data serial port failure : {0}", e.Message);
|
|
if (e.InnerException != null)
|
|
{
|
|
log.FatalFormat("InnerMessage : {0}", e.InnerException.Message);
|
|
}
|
|
}
|
|
}
|
|
else if (DebugLevel == DebugMode.FailureDuringOperation)
|
|
{
|
|
}
|
|
}
|
|
|
|
public void RunDeviceAfter()
|
|
{
|
|
}
|
|
|
|
public void StopDevice()
|
|
{
|
|
log.DebugFormat("StopDevice() called");
|
|
try
|
|
{
|
|
if (optoSerialPort != null)
|
|
{
|
|
CloseOptoSerialPort();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
public void StopDevice2()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Events: Event.ReadRegisterDone, Event.Error
|
|
/// </summary>
|
|
/// <returns>ReadWaterMeter instance reference casted to IOperaton</returns>
|
|
public IOperation ReadRegisterOp()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear data/counters related to a specific tests
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
ResultCode = 0;
|
|
|
|
volumeLtr = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray();
|
|
volumeLtr0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray();
|
|
timestampSec = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray();
|
|
timestampSec0 = Enumerable.Repeat(Double.NaN, volumeLtr.Length).ToArray();
|
|
|
|
extraDataPath = null;
|
|
|
|
/// Clear the feature vector
|
|
for (int i = 0; i < FeatureVectorSize; i++)
|
|
{
|
|
x[i] = 0;
|
|
}
|
|
}
|
|
|
|
public void TestCompleted()
|
|
{
|
|
/// TODO: Implement
|
|
}
|
|
|
|
|
|
int timeFromStart;
|
|
|
|
/// [s] Time from test start to determine when the test start sample should be taken
|
|
|
|
/// <summary>
|
|
/// Start this operation
|
|
/// </summary>
|
|
public void Start()
|
|
{
|
|
log.DebugFormat("Start: {0:HH:mm:ss.fff}", DateTime.Now);
|
|
lock (_stateSync)
|
|
{
|
|
Clear();
|
|
ResetBlockCountersAndState();
|
|
ReadPulses();
|
|
StartDataStreamProcessing();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Run this operation
|
|
/// </summary>
|
|
/// <returns>eventDone</returns>
|
|
public Event Run()
|
|
{
|
|
lock (_stateSync)
|
|
{
|
|
log.DebugFormat("Run: {0:HH:mm:ss.fff}", DateTime.Now);
|
|
timeFromStart += StateMachine.Period;
|
|
ReadPulses();
|
|
|
|
// Start sample after 1 second
|
|
if (!startSampleAcquired && (timeFromStart >= 2) && (currentTelegramIx >= 0))
|
|
{
|
|
startSampleAcquired = true;
|
|
TestStartTelegramIx = currentTelegramIx;
|
|
|
|
// initialize end at the same point
|
|
TestEndTelegramIx = currentTelegramIx;
|
|
|
|
log.DebugFormat(
|
|
"Test start acquired at ix={0}, timeFromStart={1}",
|
|
TestStartTelegramIx,
|
|
timeFromStart);
|
|
}
|
|
else if (startSampleAcquired && currentTelegramIx >= 0)
|
|
{
|
|
// always keep latest telegram as end
|
|
TestEndTelegramIx = currentTelegramIx;
|
|
}
|
|
}
|
|
|
|
return Event.ReadRegisterDone;
|
|
}
|
|
|
|
private readonly object _stateSync = new object();
|
|
private readonly object _stopSync = new object();
|
|
private volatile bool _isStopping = false;
|
|
/// <summary>
|
|
/// Stop this operation
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
log.DebugFormat("Stop: {0:HH:mm:ss.fff} - dataStreamProcessing() = {1} registerReader = {2}", DateTime.Now, dataStreamState, Name);
|
|
|
|
int startIx = -1;
|
|
int endIx = -1;
|
|
|
|
try
|
|
{
|
|
|
|
lock (_stopSync)
|
|
{
|
|
if (_isStopping)
|
|
return;
|
|
_isStopping = true;
|
|
|
|
lock (_stateSync)
|
|
{
|
|
|
|
|
|
var previousState = dataStreamState;
|
|
|
|
// 1. stop new reads from serial
|
|
StopQueueData = true;
|
|
startDataProcessing = false;
|
|
|
|
StopProcessingLoop();
|
|
ClearReceivedLines(); //Empty othrs
|
|
//ResetDataBuffer();
|
|
CloseOptoSerialPort();
|
|
|
|
// 4. mark state as stopped
|
|
dataStreamState = DataStreamState.Flush;
|
|
// 5. add marks using fully processed data
|
|
AddTestStartEndMarksToData(out startIx, out endIx);
|
|
|
|
if (previousState ==
|
|
DataStreamState
|
|
.CalculateStoredData) //ignore wrong state - should not happen - maybe it is already stopped?
|
|
{
|
|
DataStreamPostProcessing();
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat(
|
|
"Stop: {0:HH:mm:ss.fff} - DataStreamState = {1} opto COM{2} DataStreamPostProcessing NOT Called!",
|
|
DateTime.Now, previousState, optoSerialPort);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
log.WarnFormat("Genesis.Stop( COM{3}) startIx={0} endIx={1} len={2} no raw data file",
|
|
startIx, endIx, optoData.Length, optoSerialPort);
|
|
|
|
if (TestStartTelegramIx == 0 || optoDataCount < 100)
|
|
{
|
|
ResultCode |= (int)Results.Entities.ResultCode.MissingOptoData;
|
|
}
|
|
else if (VolumeLtrEnd == VolumeLtrStart)
|
|
{
|
|
ResultCode |= (int)Results.Entities.ResultCode.OptoDataWithZeroFlow;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (_startupFlushSync)
|
|
{
|
|
_startupFlushActive = false;
|
|
}
|
|
|
|
_isStopping = false;
|
|
}
|
|
}
|
|
|
|
|
|
private CancellationTokenSource _processLoopCts;
|
|
private Task _processLoopTask;
|
|
private readonly AutoResetEvent _queueSignal = new AutoResetEvent(false);
|
|
|
|
private volatile bool _stopQueueData = true;
|
|
|
|
public bool StopQueueData
|
|
{
|
|
get => _stopQueueData;
|
|
set
|
|
{
|
|
if (value == true)
|
|
{
|
|
log.DebugFormat("StopQueueData: --true--");
|
|
}
|
|
|
|
_stopQueueData = value;
|
|
}
|
|
}
|
|
|
|
private void ClearReceivedLines()
|
|
{
|
|
StopQueueData = true;
|
|
try
|
|
{
|
|
while (_receivedLines.TryDequeue(out _)) { }
|
|
}
|
|
finally
|
|
{
|
|
//StopQueueData = false;
|
|
}
|
|
}
|
|
|
|
private void DrainQueuedLines()
|
|
{
|
|
StopQueueData = true;
|
|
try
|
|
{
|
|
while (_receivedLines.TryDequeue(out var item))
|
|
{
|
|
try
|
|
{
|
|
DateTime timestamp = item.Timestamp;
|
|
string line = item.Line;
|
|
|
|
log.DebugFormat(
|
|
"DrainQueuedLines() real incoming TimeStamp: {0} processing queued line: {1}",
|
|
timestamp.ToString("HH:mm:ss.fff"),
|
|
line);
|
|
|
|
bool blockCompleted;
|
|
ProcessOptoLine(line, dataStreamState, out blockCompleted);
|
|
|
|
if (blockCompleted)
|
|
{
|
|
log.Debug("DrainQueuedLines() completed flow block detected.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"DrainQueuedLines() failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
StopQueueData = false;
|
|
}
|
|
}
|
|
|
|
void AddTestStartEndMarksToData(out int startIx, out int endIx)
|
|
{
|
|
startIx = BufferIdx(TestStartTelegramIx);
|
|
if ((startIx > 0) && (startIx < StartOptoDataCount) && (optoData[startIx].Flags == OptoTelegramFlags.OK))
|
|
{
|
|
optoData[startIx].Flags = OptoTelegramFlags.OK_TestStart;
|
|
OptoTelegramRaw.TestStartTimestampDec = optoData[startIx].TimestampDec();
|
|
}
|
|
else
|
|
{
|
|
for (int ix = 0; ix < StartOptoDataCount; ix++)
|
|
{
|
|
if (optoData[ix].Flags == OptoTelegramFlags.OK)
|
|
{
|
|
/// This is the first correct opto-telegram received
|
|
OptoTelegramRaw.TestStartTimestampDec = optoData[ix].TimestampDec();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
endIx = BufferIdx(TestEndTelegramIx);
|
|
if ((endIx > 0) && (optoData[endIx].Flags == OptoTelegramFlags.OK))
|
|
{
|
|
optoData[endIx].Flags = OptoTelegramFlags.OK_TestEnd;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Data stream post processing:
|
|
/// Flow from a reference flowmeter is FIR filtered
|
|
/// </summary>
|
|
void DataStreamPostProcessing()
|
|
{
|
|
PrepareCalculatedChannelData();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine opto data file name: PCB_AA_BB_HH_MI_SS..txt
|
|
/// </summary>
|
|
/// <param name="testName">Test name</param>
|
|
/// <param name="testRepeats">Test repeats count (>= 1)</param>
|
|
/// <param name="repetitionNr">Repetition number (1 .. testRepeats)</param>
|
|
/// <returns></returns>
|
|
string DetermineExtraDataFileName()
|
|
{
|
|
///
|
|
/// Get required pieces of information
|
|
///
|
|
string pcbNr = (ConfigStruct != null) ? ConfigStruct.GetPcbNrString() : "UnknownPcbNr";
|
|
string wmPosition = Name.Substring(5); /// WMPosition is extracted from a component name in form 'iPerl#'
|
|
if (wmPosition.Length == 1) wmPosition = "0" + wmPosition;
|
|
string cycleStartTime = StateMachine.CycleStartTimeStamp.ToString("HH_mm_ss");
|
|
#if ORACLE_DB
|
|
string[] designations =
|
|
string.IsNullOrEmpty(test.RawDataDesignation) ? new string[0] : test.RawDataDesignation.Split(new char[] { '~' });
|
|
int testId = test.RawDataId + (test.Repeats - repetitionNr) * test.RawDataIdRepetMulti;
|
|
string designation = string.IsNullOrEmpty(test.RawDataDesignation)
|
|
? testId.ToString(testId > 0 ? "D2" : "D1") /// Name is generated from Id
|
|
: (designations.Length > repetitionNr - 1) ? designations[repetitionNr - 1] /// Name is from 'RawDataDesignation' parameter
|
|
: string.Format("{0}-{1}", designations[0], repetitionNr); /// Name is form test name and repetition nr.
|
|
#else
|
|
int testId = 0;
|
|
string designation = (test.Repeats == 1) ? test.Name : string.Format("{0}-{1}", test.Name, repetitionNr);
|
|
#endif
|
|
///
|
|
/// Return the file name
|
|
///
|
|
return string.Format("{0}_{1}_{2}_{3}.txt", pcbNr, wmPosition, designation, cycleStartTime);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Save opto data to a file.
|
|
/// </summary>
|
|
bool SaveOptoDataToFile(string directory, string fileName)
|
|
{
|
|
string fullFileName = Path.Combine(directory, fileName);
|
|
log.WarnFormat("Saving {0} raw data to {1}", Name, fullFileName);
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
|
|
double scalFact = ScalingFactor();
|
|
|
|
using (TextWriter optoLogFile = new StreamWriter(fullFileName))
|
|
{
|
|
if (optoDataCount <= OptoDataBufferSize)
|
|
{
|
|
/// Telegrams are stored continuously, save them.
|
|
optoLogFile.WriteLine(optoData[0].ToString(scalFact, null));
|
|
for (int i = 1; i < optoDataCount; i++)
|
|
{
|
|
optoLogFile.WriteLine(optoData[i].ToString(scalFact, optoData[i - 1]));
|
|
}
|
|
}
|
|
else /// if (optoDataCount > MaxOptoDataCount)
|
|
{
|
|
/// Buffer overflow
|
|
/// First part of the buffer is saved as is
|
|
optoLogFile.WriteLine(optoData[0].ToString(scalFact, null));
|
|
for (int i = 1; i < StartOptoDataCount; i++)
|
|
{
|
|
optoLogFile.WriteLine(optoData[i].ToString(scalFact, optoData[i - 1]));
|
|
}
|
|
|
|
optoLogFile.WriteLine(" ...");
|
|
|
|
/// Second part of the buffer is an overflowed circular buffer
|
|
optoLogFile.WriteLine(optoData[BufferIdx(optoDataCount)].ToString(scalFact, null));
|
|
for (int i = optoDataCount - EndOptoDataCount + 1; i < optoDataCount; i++)
|
|
{
|
|
optoLogFile.WriteLine(optoData[BufferIdx(i)]
|
|
.ToString(scalFact, optoData[BufferIdx(i - 1)]));
|
|
}
|
|
}
|
|
|
|
log.WarnFormat("{0} opto data successfully saved: {1} lines", Name, optoDataCount);
|
|
|
|
optoLogFile.Close();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
File.Delete(fullFileName);
|
|
log.ErrorFormat(string.Format("Error writing into file {0}", fullFileName));
|
|
log.ErrorFormat(string.Format("Exception message: {0}", exc.Message));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void ReadPulses()
|
|
{
|
|
if (channel0 == -1)
|
|
return;
|
|
|
|
if (channel0 == -2)
|
|
{
|
|
|
|
beginWMState = 0;
|
|
endWMState = CalculateVolumeByChannels();
|
|
wmVolume = Math.Abs(endWMState - beginWMState);
|
|
wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5);
|
|
log.Debug("wmPulses = " + wmPulses + "wmVolume = " + wmVolume + "PulsesPerLtr = " + PulsesPerLtr + "");
|
|
if (StateMachine.ControlBoardMain != null)
|
|
wmRefPulses = StateMachine.ControlBoardMain.RefPulses;
|
|
else
|
|
wmRefPulses = 0;
|
|
wmTestTime = CalculateTimeByChannels();
|
|
|
|
return;
|
|
}
|
|
|
|
beginWMState = volumeLtr0[channel0];
|
|
endWMState = volumeLtr[channel0];
|
|
wmVolume = Math.Abs(endWMState - beginWMState);
|
|
wmPulses = (int)(wmVolume * (double)PulsesPerLtr + 0.5);
|
|
log.Debug("wmPulses = " + wmPulses + "wmVolume = " + wmVolume + "PulsesPerLtr = " + PulsesPerLtr + "");
|
|
if (StateMachine.ControlBoardMain != null)
|
|
wmRefPulses = StateMachine.ControlBoardMain.RefPulses;
|
|
else
|
|
wmRefPulses = 0;
|
|
wmTestTime = timestampSec[channel0] - timestampSec0[channel0];
|
|
}
|
|
|
|
private double CalculateTimeByChannels()
|
|
{
|
|
double[] timeDelta = new double[iChanelsCount];
|
|
for (int iChanel = 0; iChanel < iChanelsCount; iChanel++)
|
|
{
|
|
timeDelta[iChanel] = this.timestampSec[iChanel] - this.timestampSec0[iChanel];
|
|
}
|
|
|
|
return Average(timeDelta);
|
|
}
|
|
|
|
private double CalculateVolumeByChannels()
|
|
{
|
|
double[] volumeDelta = new double[iChanelsCount];
|
|
for (int iChanel = 0; iChanel < iChanelsCount; iChanel++)
|
|
{
|
|
volumeDelta[iChanel] = this.volumeLtr[iChanel] - this.volumeLtr0[iChanel];
|
|
}
|
|
|
|
return Average(volumeDelta);
|
|
}
|
|
|
|
private void OpenOptoSerialPortIfNotInit(
|
|
string comPort,
|
|
int baudRate,
|
|
Parity parity,
|
|
int dataBits,
|
|
StopBits stopBit,
|
|
Handshake handshake,
|
|
int openTimeoutMs = 3000)
|
|
{
|
|
if (optoSerialPort == null)
|
|
{
|
|
OpenOptoSerialPort(comPort, baudRate, parity, dataBits, stopBit, handshake, openTimeoutMs);
|
|
}
|
|
}
|
|
|
|
private void OpenOptoSerialPort(
|
|
string comPort,
|
|
int baudRate,
|
|
Parity parity,
|
|
int dataBits,
|
|
StopBits stopBit,
|
|
Handshake handshake,
|
|
int openTimeoutMs = 3000)
|
|
{
|
|
if (DebugLevel == DebugMode.FailureDuringOperation) DebugLevel = DebugMode.Normal;
|
|
if (DebugLevel != DebugMode.Normal)
|
|
{
|
|
optoSerialPort = null;
|
|
log.FatalFormat($"{Name} OproPort simulated: {this}");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
CloseOptoSerialPort();
|
|
|
|
log.Info($"Opening serial port {comPort} at {baudRate} baud rate");
|
|
|
|
if (_serialFactory != null)
|
|
{
|
|
optoSerialPort = _serialFactory();
|
|
optoSerialPort.Open();
|
|
if (!optoSerialPort.IsOpen)
|
|
throw new InvalidOperationException("Failed to open injected serial driver.");
|
|
}
|
|
else
|
|
{
|
|
optoSerialPort = new SerialDriverBuilder()
|
|
.WithPort(comPort)
|
|
.WithBaudRate(baudRate)
|
|
.WithParity(parity)
|
|
.WithDataBits(dataBits)
|
|
.WithStopBits(stopBit)
|
|
.WithHandshake(handshake)
|
|
.WithNewLine("\n")
|
|
.WithReadTimeout(5000)
|
|
.WithWriteTimeout(5000)
|
|
.WithDtrEnable(true)
|
|
.WithRtsEnable(true)
|
|
.WithOpenTimeout(openTimeoutMs)
|
|
.WithDiscardInputBufferOnOpen(true)
|
|
.WithDiscardOutputBufferOnOpen(true)
|
|
.BuildAndOpen();
|
|
}
|
|
|
|
log.FatalFormat($"{Name} OptoPort opened: {this}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.FatalFormat($"{Name} OptoPort - error opening port: {this}"
|
|
+ Environment.NewLine + ex.Message);
|
|
//throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close the serial port
|
|
/// StopOptoReadLoop is called inside
|
|
/// </summary>
|
|
private void CloseOptoSerialPort()
|
|
{
|
|
// 1. Stop background reading FIRST
|
|
StopOptoReadLoop();
|
|
|
|
// 2. Synchronize with ReadLine()
|
|
lock (_serialReadSync)
|
|
{
|
|
if (optoSerialPort != null)
|
|
{
|
|
try
|
|
{
|
|
if (optoSerialPort.IsOpen)
|
|
{
|
|
optoSerialPort.Close();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"Error closing opto port: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
optoSerialPort = null;
|
|
log.FatalFormat($"{Name} OptoPort closed: {this}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DataStreamState dataStreamState;
|
|
|
|
/// <summary>
|
|
/// Reset counters / indices / time and start processing and saving datastream data
|
|
/// </summary>
|
|
public void StartDataStreamProcessing()
|
|
{
|
|
log.DebugFormat("StartDataStreamProcessing({0}) called", DateTime.Now.ToString("HH:mm:ss.fff"));
|
|
try
|
|
{
|
|
log.DebugFormat("OpenOptoSerialPortIfNotInit StartDataStreamProcessing() called");
|
|
OpenOptoSerialPortIfNotInit(
|
|
$"COM{genesisHeadCfg.OptoComPortNr}",
|
|
115200,
|
|
Parity.None,
|
|
8,
|
|
StopBits.One,
|
|
Handshake.None);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
/// Reset opto-data, etc.
|
|
optoDataCount = 0;
|
|
timeFromStart = 0;
|
|
currentTelegramIx = -1;
|
|
startSampleAcquired = false;
|
|
TestStartTelegramIx = -1;
|
|
TestEndTelegramIx = -1;
|
|
|
|
// optional: keep these if fields still exist, but no longer used
|
|
endTelegramIdx1 = -1;
|
|
endTelegramIdx2 = -1;
|
|
endTelegramIdx3 = -1;
|
|
|
|
// RESET UNWRAP STATE
|
|
for (int i = 0; i < iChanelsCount; i++)
|
|
{
|
|
volumeRawExtLast[i] = double.NaN;
|
|
timestampExtLast[i] = double.NaN;
|
|
|
|
lastVolumeRaw[i] = double.NaN;
|
|
lastTimestamp[i] = double.NaN;
|
|
volumeLtr[i] = double.NaN;
|
|
volumeLtr0[i] = double.NaN;
|
|
timestampSec[i] = double.NaN;
|
|
timestampSec0[i] = double.NaN;
|
|
}
|
|
|
|
channel0 = -1;
|
|
|
|
try
|
|
{
|
|
StopQueueData = true;
|
|
if (optoSerialPort != null && optoSerialPort.IsOpen)
|
|
{
|
|
ResetDataBuffer();
|
|
}
|
|
|
|
ClearReceivedLines();
|
|
|
|
if (flowDirectionDetection != null)
|
|
flowDirectionDetection.ClearFifo();
|
|
|
|
// start loops first
|
|
dataStreamState = DataStreamState.ProcessAndSave;
|
|
startDataProcessing = true;
|
|
|
|
BeginStartupFlush(1500); // start up flush
|
|
|
|
StartProcessingLoop();
|
|
StartOptoReadLoop();
|
|
}
|
|
finally
|
|
{
|
|
StopQueueData = false;
|
|
}
|
|
}
|
|
|
|
public void SetCommunicationInterface(string commInterface)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when processing and saving datastream data is in progress
|
|
/// </summary>
|
|
bool IsDataStreamProcessing()
|
|
{
|
|
return dataStreamState == DataStreamState.ProcessAndSave;
|
|
}
|
|
|
|
void ISmartReader.SetRfidInterface()
|
|
{
|
|
SetRfidInterface();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stop processing and saving datastream data
|
|
/// </summary>
|
|
public void StopDataStreamProcessing()
|
|
{
|
|
log.DebugFormat("StopDataStreamProcessing({0}) called - IsDataStreamProcessing() = {1}", DateTime.Now.ToString("HH:mm:ss.fff"), IsDataStreamProcessing());
|
|
|
|
StopQueueData = true;
|
|
startDataProcessing = false;
|
|
dataStreamState = DataStreamState.CalculateStoredData;
|
|
|
|
|
|
StopProcessingLoop();
|
|
lock (_queueProcessingLock)
|
|
{
|
|
ClearReceivedLines();
|
|
}
|
|
CloseOptoSerialPort();
|
|
|
|
lock (_startupFlushSync)
|
|
{
|
|
_startupFlushActive = false;
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Variables storing the context of serial port data parsing (ReadOptoSerialPort(...))
|
|
///
|
|
bool synchronized;
|
|
|
|
bool synchronized2;
|
|
string partOfTelegram;
|
|
|
|
private string _commInterface;
|
|
|
|
|
|
#region block variables to check results block
|
|
|
|
//block varaibles to check results block
|
|
private int _completedBlockCount = 0;
|
|
private int _resetAfterBlockRepetitions = 2;
|
|
|
|
private bool _blockStartedWithF = false;
|
|
private bool _channel1SeenInBlock = false;
|
|
private bool _channel2SeenInBlock = false;
|
|
private bool _channel3SeenInBlock = false;
|
|
|
|
public int ResetAfterBlockRepetitions
|
|
{
|
|
get { return _resetAfterBlockRepetitions; }
|
|
set { _resetAfterBlockRepetitions = value < 1 ? 1 : value; }
|
|
}
|
|
|
|
private void ResetBlockState()
|
|
{
|
|
_blockStartedWithF = false;
|
|
_channel1SeenInBlock = false;
|
|
_channel2SeenInBlock = false;
|
|
_channel3SeenInBlock = false;
|
|
}
|
|
|
|
private void ResetBlockCountersAndState()
|
|
{
|
|
_completedBlockCount = 0;
|
|
ResetBlockState();
|
|
}
|
|
|
|
private void MarkCalibrationChannelSeen(int channel)
|
|
{
|
|
if (!_blockStartedWithF)
|
|
return;
|
|
|
|
switch (channel)
|
|
{
|
|
case 1:
|
|
_channel1SeenInBlock = true;
|
|
break;
|
|
case 2:
|
|
_channel2SeenInBlock = true;
|
|
break;
|
|
case 3:
|
|
_channel3SeenInBlock = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private bool HasCompleteHBlock()
|
|
{
|
|
return _blockStartedWithF &&
|
|
_channel1SeenInBlock &&
|
|
_channel2SeenInBlock &&
|
|
_channel3SeenInBlock;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles @f marker. First @f starts block, second @f closes it if h1/h2/h3 were seen.
|
|
/// Returns true when the configured number of complete flow blocks has been reached.
|
|
/// </summary>
|
|
private bool HandleFlowMarker()
|
|
{
|
|
// first @f starts block
|
|
if (!_blockStartedWithF)
|
|
{
|
|
_blockStartedWithF = true;
|
|
_channel1SeenInBlock = false;
|
|
_channel2SeenInBlock = false;
|
|
_channel3SeenInBlock = false;
|
|
return false;
|
|
}
|
|
|
|
// second @f closes block only if all H telegrams were seen
|
|
if (!HasCompleteHBlock())
|
|
{
|
|
// start a fresh block from this @f
|
|
_blockStartedWithF = true;
|
|
_channel1SeenInBlock = false;
|
|
_channel2SeenInBlock = false;
|
|
_channel3SeenInBlock = false;
|
|
return false;
|
|
}
|
|
|
|
_completedBlockCount++;
|
|
|
|
bool shouldReset = _completedBlockCount >= _resetAfterBlockRepetitions;
|
|
if (shouldReset)
|
|
_completedBlockCount = 0;
|
|
|
|
ResetBlockState();
|
|
return shouldReset;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
//----
|
|
// for test only
|
|
internal Action<string> TestLineProcessed;
|
|
internal Func<string, bool> TestProcessLineOverride;
|
|
|
|
internal void TestEnqueueLine(string line, DateTime? timestamp = null)
|
|
{
|
|
_receivedLines.Enqueue((timestamp ?? DateTime.UtcNow, line));
|
|
_queueSignal.Set();
|
|
}
|
|
//----
|
|
|
|
private CancellationTokenSource _readLoopCts;
|
|
private Task _readLoopTask;
|
|
private readonly ConcurrentQueue<(DateTime Timestamp, string Line)> _receivedLines = new ConcurrentQueue<(DateTime Timestamp, string Line)>();
|
|
//private readonly ConcurrentQueue<string> _receivedLines = new ConcurrentQueue<string>();
|
|
private readonly object _serialReadSync = new object();
|
|
|
|
private void StartOptoReadLoop()
|
|
{
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
return;
|
|
|
|
StopOptoReadLoop();
|
|
|
|
_readLoopCts = new CancellationTokenSource();
|
|
var token = _readLoopCts.Token;
|
|
|
|
_readLoopTask = Task.Run(() =>
|
|
{
|
|
log.Debug($"OPTHO {OptoComPortNr} background read loop started.");
|
|
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
EndStartupFlushIfNeeded();
|
|
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
{
|
|
Thread.Sleep(20);
|
|
continue;
|
|
}
|
|
|
|
string line;
|
|
lock (_serialReadSync)
|
|
{
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
continue;
|
|
|
|
line = optoSerialPort.ReadLine();
|
|
}
|
|
|
|
//switch stopQueneData
|
|
if (!StopQueueData && !string.IsNullOrWhiteSpace(line))
|
|
{
|
|
if (ShouldIgnoreLineDuringStartupFlush())
|
|
{
|
|
// old buffered meter data, discard it
|
|
continue;
|
|
}
|
|
|
|
_receivedLines.Enqueue((DateTime.UtcNow, line));
|
|
_queueSignal.Set();
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// normal: just continue
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"OPTHO {OptoComPortNr} background read error: {ex.Message}");
|
|
Thread.Sleep(100);
|
|
}
|
|
}
|
|
|
|
log.Debug($"OPTHO {OptoComPortNr} background read loop stopped.");
|
|
}, token);
|
|
}
|
|
|
|
private void StopOptoReadLoop()
|
|
{
|
|
try
|
|
{
|
|
StopQueueData = true;
|
|
|
|
if (_readLoopCts != null)
|
|
{
|
|
_readLoopCts.Cancel();
|
|
}
|
|
|
|
if (_readLoopTask != null)
|
|
{
|
|
try
|
|
{
|
|
_readLoopTask.Wait(1000);
|
|
}
|
|
catch (AggregateException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_readLoopTask = null;
|
|
|
|
if (_readLoopCts != null)
|
|
{
|
|
_readLoopCts.Dispose();
|
|
_readLoopCts = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void StopProcessingLoop()
|
|
{
|
|
try
|
|
{
|
|
if (_processLoopCts != null)
|
|
_processLoopCts.Cancel();
|
|
|
|
_queueSignal.Set();
|
|
|
|
if (_processLoopTask != null)
|
|
{
|
|
try
|
|
{
|
|
_processLoopTask.Wait(1000);
|
|
}
|
|
catch (AggregateException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_processLoopTask = null;
|
|
|
|
if (_processLoopCts != null)
|
|
{
|
|
_processLoopCts.Dispose();
|
|
_processLoopCts = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void StartProcessingLoop()
|
|
{
|
|
StopProcessingLoop();
|
|
|
|
_processLoopCts = new CancellationTokenSource();
|
|
var token = _processLoopCts.Token;
|
|
|
|
_processLoopTask = Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
log.Debug($"OPTHO {OptoComPortNr} processing loop started.");
|
|
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
|
|
EndStartupFlushIfNeeded();
|
|
|
|
if (_receivedLines.TryDequeue(out var item))
|
|
{
|
|
if (StopQueueData || dataStreamState != DataStreamState.ProcessAndSave || !startDataProcessing)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
if (TestProcessLineOverride != null)
|
|
{
|
|
TestProcessLineOverride(item.Line);
|
|
TestLineProcessed?.Invoke(item.Line);
|
|
continue;
|
|
}
|
|
|
|
bool blockCompleted;
|
|
ProcessOptoLine(item.Line, dataStreamState, out blockCompleted);
|
|
|
|
TestLineProcessed?.Invoke(item.Line);
|
|
|
|
if (blockCompleted)
|
|
{
|
|
log.Debug("Processing loop completed flow block detected.");
|
|
if (resetSerialBuffersOnCompletedFlowBlock)
|
|
ResetDataBuffer();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"Processing loop failed: {ex}");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
_queueSignal.WaitOne(50);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"OPTHO {OptoComPortNr} processing loop error: {ex}");
|
|
Thread.Sleep(50);
|
|
}
|
|
}
|
|
|
|
log.Debug($"OPTHO {OptoComPortNr} processing loop stopped.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"StartProcessingLoop fatal error: {ex}");
|
|
}
|
|
}, token);
|
|
}
|
|
|
|
|
|
private readonly object _queueProcessingLock = new object();
|
|
|
|
/// <summary>
|
|
/// Reads opto-datastream via serial port. Invoked from RunDeviceBefore()
|
|
///
|
|
/// ...
|
|
/// </summary>
|
|
/// <param name="optoState">OptoState.Read or OptoState.Flush</param>
|
|
void ReadOptoData(DataStreamState optoState)
|
|
{
|
|
if (optoSerialPort is null) return;
|
|
|
|
lock (_queueProcessingLock)
|
|
{
|
|
while (_receivedLines.TryDequeue(out var item))
|
|
{
|
|
try
|
|
{
|
|
DateTime timestamp = item.Timestamp;
|
|
string line = item.Line;
|
|
// 🔴 STEP 1: Check if we should start processing
|
|
if (startDataProcessing && optoState == DataStreamState.ProcessAndSave)
|
|
{
|
|
log.DebugFormat("Read Opto Data Line timestamp:{0} to process from queue: {1}",timestamp.ToString("HH:mm:ss.fff") , line);
|
|
bool blockCompleted;
|
|
ProcessOptoLine(line, optoState, out blockCompleted);
|
|
|
|
if (blockCompleted)
|
|
{
|
|
log.Debug("ReadOptoData() completed flow block detected.");
|
|
if (resetSerialBuffersOnCompletedFlowBlock) // DO NOT call ResetDataBuffer() here
|
|
ResetDataBuffer();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"OPTHO {OptoComPortNr} processing queued line failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void ProcessOptoLine(string line, DataStreamState optoState, out bool blockCompleted)
|
|
{
|
|
var encoding = optoSerialPort != null ? optoSerialPort.Encoding : Encoding.ASCII;
|
|
byte[] bytes = encoding.GetBytes(line);
|
|
log.Debug("ComPort: " + OptoComPortNr + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes));
|
|
|
|
blockCompleted = false;
|
|
|
|
var streamingDecode = new StreamingDecoder(true);
|
|
streamingDecode.DecodeMsg(line);
|
|
|
|
CalibrationRecord calibData = streamingDecode.DataCalib;
|
|
|
|
if (streamingDecode.DataFlowTest != null && streamingDecode.DataFlowTest.IsValid)
|
|
{
|
|
blockCompleted = HandleFlowMarker();
|
|
log.Debug("ComPort: " + OptoComPortNr + " Decoded Flow data: " + streamingDecode.DataFlowTest +
|
|
" OPTHO RX ← " + HexFormatter.ToSerialHex(bytes));
|
|
}
|
|
|
|
if (calibData != null && calibData.IsValid)
|
|
{
|
|
log.Debug("ComPort: " + OptoComPortNr + " Decoded Calib: " + calibData + " OPTHO RX ← " +
|
|
HexFormatter.ToSerialHex(bytes));
|
|
|
|
MarkCalibrationChannelSeen(calibData.Channel);
|
|
}
|
|
|
|
if (calibData == null || !calibData.IsValid)
|
|
return;
|
|
|
|
if (optoState == DataStreamState.ProcessAndSave)
|
|
{
|
|
int bufferIx = BufferIdx(optoDataCount);
|
|
|
|
if (synchronized)
|
|
{
|
|
optoData[bufferIx].Counter = optoDataCount;
|
|
optoData[bufferIx].SetFlags(OptoTelegramFlags.SyncError);
|
|
}
|
|
|
|
int iChanel = calibData.Channel - 1;
|
|
if (iChanel >= 0 && iChanel < iChanelsCount)
|
|
{
|
|
log.Debug(
|
|
$"Before UpdateFromSmart ch={iChanel + 1}: " +
|
|
$"volumeRawExtLast={volumeRawExtLast[iChanel]}, " +
|
|
$"timestampExtLast={timestampExtLast[iChanel]}, " +
|
|
$"VolumeCm={calibData.VolumeCm}, OverflowVolumeCm={calibData.OverflowVolumeCm}");
|
|
|
|
optoData[bufferIx].UpdateFromSmart(
|
|
calibData,
|
|
optoDataCount,
|
|
GetReferenceFlowSafe(),
|
|
ref volumeRawExtLast[iChanel],
|
|
ref timestampExtLast[iChanel]);
|
|
|
|
flowDirectionDetection.WriteToFifo(volumeRawExtLast, timestampExtLast, iChanel);
|
|
|
|
OptoTelegramReceived(
|
|
optoDataCount,
|
|
true,
|
|
volumeRawExtLast[iChanel],
|
|
timestampExtLast[iChanel],
|
|
iChanel);
|
|
}
|
|
|
|
optoDataCount++;
|
|
}
|
|
}
|
|
|
|
private float GetReferenceFlowSafe()
|
|
{
|
|
try
|
|
{
|
|
if (Sequences.ProcessData.RefFlow != null)
|
|
return Convert.ToSingle(Sequences.ProcessData.RefFlow.Val);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
|
|
void ISmartReader.ResetNfcInterface(bool? nfc_on)
|
|
{
|
|
ResetNfcInterface(nfc_on);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset buffer - opto serial Read Data Buffer
|
|
/// </summary>
|
|
int lastChannelReadOptoData = -1;
|
|
|
|
private string _rxBuffer = "";
|
|
|
|
public string ReadOptoData()
|
|
{
|
|
if (optoSerialPort is null) return "";
|
|
|
|
string received = ".";
|
|
lock (_stateSync)
|
|
{
|
|
try
|
|
{
|
|
string line = optoSerialPort.ReadLine();
|
|
|
|
var encoding = optoSerialPort?.Encoding ?? Encoding.ASCII;
|
|
byte[] bytes = encoding.GetBytes(line);
|
|
received = HexFormatter.ToSerialHex(bytes);
|
|
log.Debug("RX ← " + received);
|
|
|
|
try
|
|
{
|
|
StreamingDecoder _streamingDecode = new StreamingDecoder(true);
|
|
_streamingDecode.DecodeMsg(line);
|
|
CalibrationRecord data = _streamingDecode.DataCalib;
|
|
if (data != null && data.IsValid)
|
|
{
|
|
log.Info($"OPTHO {OptoComPortNr} DataCalib Parsed opto data: " + data + " RX ← " +
|
|
received);
|
|
MarkCalibrationChannelSeen(data.Channel);
|
|
}
|
|
|
|
FlowTestRecord dataFlow = _streamingDecode.DataFlowTest;
|
|
if (dataFlow != null && dataFlow.IsValid)
|
|
{
|
|
log.Info($"OPTHO {OptoComPortNr} FLOW Parsed opto data: " + dataFlow + " RX ← " + received);
|
|
|
|
if (HandleFlowMarker())
|
|
{
|
|
log.Debug("ReadOptoData() completed flow block detected.");
|
|
if (resetSerialBuffersOnCompletedFlowBlock)
|
|
ResetDataBuffer(); // no ResetDataBuffer() here
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"OPTHO {OptoComPortNr} Read error: {ex.Message}");
|
|
}
|
|
|
|
// string line = optoSerialPort.ReadExisting();
|
|
//
|
|
// if (!string.IsNullOrEmpty(line))
|
|
// {
|
|
// string visual = line.Replace("\r", "\\r").Replace("\n", "\\n");
|
|
//
|
|
// bool hasCR = line.Contains('\r');
|
|
// bool hasLF = line.Contains('\n');
|
|
// bool hasCRLF = line.Contains("\r\n");
|
|
//
|
|
// log.Debug($"RAW: [{visual}]");
|
|
// log.Debug($"CR: {hasCR}, LF: {hasLF}, CRLF: {hasCRLF}");
|
|
//
|
|
// _rxBuffer += line;
|
|
//
|
|
// string[] parts = _rxBuffer.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.None);
|
|
//
|
|
// // incomplete tail
|
|
// if (parts.Length > 0)
|
|
// {
|
|
// _rxBuffer = parts[parts.Length - 1];
|
|
// }
|
|
// else
|
|
// {
|
|
// _rxBuffer = "";
|
|
// }
|
|
// // ~ incomplete tail
|
|
//
|
|
// for (int i = 0; i < parts.Length - 1; i++)
|
|
// {
|
|
// string parsed = parts[i];
|
|
// log.Debug($"PARSED: [{parsed}]");
|
|
//
|
|
// byte[] bytes = optoSerialPort.Encoding.GetBytes(parsed);
|
|
// received = HexFormatter.ToSerialHex(bytes);
|
|
//
|
|
// log.Debug("RX ← " + received);
|
|
// }
|
|
//}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
log.Debug($"ReadOptoData() OPTHO {OptoComPortNr} timeout - continuing.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"ReadOptoData() OPTHO {OptoComPortNr} Read error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return received;
|
|
}
|
|
|
|
private const bool resetSerialBuffersOnCompletedFlowBlock = true;
|
|
private void ResetDataBuffer()
|
|
{
|
|
lock (_serialReadSync)
|
|
{
|
|
if (optoSerialPort != null && optoSerialPort.IsOpen)
|
|
{
|
|
optoSerialPort.DiscardInBuffer();
|
|
optoSerialPort.DiscardOutBuffer();
|
|
log.Debug("-- Reaset Data Buffer --");
|
|
return;
|
|
}
|
|
}
|
|
log.Debug("-- Reaset Data Buffer - no serial port --");
|
|
}
|
|
|
|
void ISmartReader.SetNfcInterface()
|
|
{
|
|
SetNfcInterface();
|
|
}
|
|
|
|
public async Task<string> ReadOptoDataWithTimeoutAsync(int timeoutMs = 5000)
|
|
{
|
|
if (optoSerialPort == null)
|
|
return string.Empty;
|
|
|
|
var readTask = Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
lock (_serialReadSync)
|
|
{
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
return string.Empty;
|
|
|
|
string line = optoSerialPort.ReadLine();
|
|
byte[] bytes = optoSerialPort.Encoding.GetBytes(line);
|
|
string received = HexFormatter.ToSerialHex(bytes);
|
|
|
|
log.Debug("RX ← " + received);
|
|
return line;
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
log.Debug($"ReadOptoData() OPTHO {OptoComPortNr} ReadLine timeout (3s) - continuing.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"ReadOptoData() OPTHO {OptoComPortNr} Read error: {ex.Message}");
|
|
}
|
|
|
|
return string.Empty;
|
|
});
|
|
|
|
var completedTask = await Task.WhenAny(readTask, Task.Delay(timeoutMs));
|
|
|
|
if (completedTask == readTask)
|
|
return await readTask;
|
|
|
|
log.Debug("ReadOptoData timeout after " + timeoutMs + " ms");
|
|
return string.Empty;
|
|
}
|
|
|
|
public string ReadOptoDataWithTimeout(int timeoutMs = 5000)
|
|
{
|
|
try
|
|
{
|
|
return ReadOptoDataWithTimeoutAsync(timeoutMs)
|
|
.GetAwaiter()
|
|
.GetResult();
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
|
|
void OptoTelegramReceived(int currentIx, bool async, double volumeRawExt, double timestampRawExt, int iChanel)
|
|
{
|
|
currentTelegramIx = currentIx;
|
|
|
|
lastVolumeRaw[iChanel] = volumeRawExt;
|
|
lastTimestamp[iChanel] = timestampRawExt;
|
|
|
|
if (channel0 == -1)
|
|
{
|
|
channel0 = iChanel;
|
|
}
|
|
|
|
if (Double.IsNaN(volumeLtr[iChanel]) && Double.IsNaN(volumeLtr0[iChanel]))
|
|
{
|
|
volumeLtr[iChanel] = lastVolumeRaw[iChanel];
|
|
volumeLtr0[iChanel] = volumeLtr[iChanel];
|
|
}
|
|
else
|
|
{
|
|
volumeLtr[iChanel] = lastVolumeRaw[iChanel];
|
|
}
|
|
|
|
if (Double.IsNaN(timestampSec[iChanel]) && Double.IsNaN(timestampSec0[iChanel]))
|
|
{
|
|
timestampSec[iChanel] = lastTimestamp[iChanel];
|
|
timestampSec0[iChanel] = timestampSec[iChanel];
|
|
}
|
|
else
|
|
{
|
|
timestampSec[iChanel] = lastTimestamp[iChanel];
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
///
|
|
/// Called from the state machine when a test is selected and UI needs to be updated.
|
|
/// </summary>
|
|
public void OnOptoReceived(object sender, OptoReceivedEventArgs args)
|
|
{
|
|
if (OptoReceivedHandler == null) return;
|
|
try
|
|
{
|
|
OptoReceivedHandler(sender, args);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
public event EventHandler<OptoReceivedEventArgs> OptoReceivedHandler;
|
|
|
|
|
|
/// <summary>
|
|
/// Compares CalibrationStruct.MeterType with iPerlCfg.MeterType.
|
|
/// iPerlCfg.MeterType == MeterType.AutoDetect disables type checking
|
|
/// CalibrationStruct == null disables type checking ...
|
|
/// ... so that failed RFID communication does not cause type verification failure)
|
|
/// </summary>
|
|
/// <returns>true when type is OK</returns>
|
|
public bool VerifyIPerlType()
|
|
{
|
|
if (genesisHeadCfg.MeterType == MeterType.AutoDetect || CalibrationStruct == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return genesisHeadCfg.MeterType == CalibrationStruct.MeterType;
|
|
}
|
|
|
|
public static double UnitVolume(VolumeUnits units)
|
|
{
|
|
switch (units)
|
|
{
|
|
default:
|
|
case VolumeUnits.m3: return Common.Units.ConvertFrom(Common.Unit.m3, 1.0); /// 1 liter
|
|
case VolumeUnits.UK_gallon:
|
|
return Common.Units.ConvertFrom(Common.Unit.UKgal, 1.0); /// 1 imperial gallon
|
|
case VolumeUnits.US_gallon: return Common.Units.ConvertFrom(Common.Unit.USgal, 1.0); /// 1 US gallon
|
|
}
|
|
}
|
|
|
|
public double ScalingFactor()
|
|
{
|
|
if ((genesisHeadCfg.MeterType == MeterType.AutoDetect) && (CalibrationStruct != null))
|
|
{
|
|
return ScalingFactor(CalibrationStruct.MeterType);
|
|
}
|
|
else if (genesisHeadCfg.MeterType != MeterType.AutoDetect)
|
|
{
|
|
return ScalingFactor(genesisHeadCfg.MeterType);
|
|
}
|
|
else
|
|
{
|
|
return ScalingFactor(MeterType.DN20);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scaling factor:
|
|
/// 0, 1 (DN15, Coax) . . . . 1
|
|
/// 2 (DN20) . . . . . . . . 2
|
|
/// 3 (DN25) . . . . . . . . 4
|
|
/// 4, 5 (DN26, DN32) . . . . 8
|
|
/// 6 (DN40) . . . . . . . . 16
|
|
/// </summary>
|
|
/// <param name="meterType">MeterType (0..6)</param>
|
|
/// <returns>Scaling factor</returns>
|
|
public static double ScalingFactor(MeterType meterType)
|
|
{
|
|
switch (meterType)
|
|
{
|
|
default:
|
|
case MeterType.DN15:
|
|
case MeterType.CoaxManifold:
|
|
return 1.0;
|
|
|
|
case MeterType.DN20:
|
|
return 2.0;
|
|
|
|
case MeterType.DN25:
|
|
return 4.0;
|
|
|
|
case MeterType.DN32:
|
|
return 8.0;
|
|
|
|
case MeterType.DN40:
|
|
return 16.0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate a filtered volume from data stream samples
|
|
/// </summary>
|
|
/// <param name="unwrappedIx">Samples used in calculation are centered around unwrappedIx</param>
|
|
/// <param name="samplesCount2">Count of samples used in calculation is 2 * smaplesCount2 + 1</param>
|
|
/// <returns>Filtered volume</returns>
|
|
double VolumeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, double scalingFactor,
|
|
int samplesCount2 = 0)
|
|
{
|
|
log.Debug("-- Get VolumeFromSamples() --");
|
|
if (samplesCount2 == 0)
|
|
{
|
|
if (unwrappedIx >= optoDataCount)
|
|
{
|
|
log.Debug(
|
|
$"-- FAILED VolumeFromSamples() - unwrappedIx {unwrappedIx} >= optoDataCount{optoDataCount}--");
|
|
return 0;
|
|
}
|
|
|
|
int wrappedIx = BufferIdx(unwrappedIx);
|
|
|
|
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
|
{
|
|
log.Debug($"-- Get VolumeFromSamples() - Quit because:{optoData[wrappedIx].Flags}--");
|
|
return 0;
|
|
}
|
|
|
|
log.Debug($"Valid data VolumeRawExt: {optoData[wrappedIx].VolumeRawExt}");
|
|
return optoData[wrappedIx].VolumeRawExt;
|
|
}
|
|
|
|
|
|
//TODO BUMI - do result as average from data - usually 5 samples
|
|
|
|
if (samplesCount2 < 0) samplesCount2 = 0;
|
|
if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0;
|
|
|
|
|
|
double sum = 0;
|
|
for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
|
|
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
sum += optoData[wrappedIx].VolumeRawExt;
|
|
}
|
|
|
|
return sum / (double)(2 * samplesCount2 + 1);
|
|
//return 0.0000625 * scalingFactor * sum / (double)(2 * samplesCount2 + 1);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate a filtered time from data stream samples
|
|
/// </summary>
|
|
/// <param name="unwrappedIx">Samples used in calculation are centered around unwrappedIx</param>
|
|
/// <param name="samplesCount2">Count of samples used in calculation is 2 * smaplesCount2 + 1</param>
|
|
/// <returns>Filtered time</returns>
|
|
double TimeFromSamples(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx, int samplesCount2 = 0)
|
|
{
|
|
log.Debug("-- Get TimeFromSamples() --");
|
|
if (unwrappedIx >= optoDataCount)
|
|
{
|
|
log.Debug(
|
|
$"-- FAILED TimeFromSamples() - unwrappedIx {unwrappedIx} >= optoDataCount{optoDataCount}--");
|
|
return 0;
|
|
}
|
|
|
|
int wrappedIx = BufferIdx(unwrappedIx);
|
|
|
|
if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
|
optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
|
{
|
|
log.Debug($"-- Get TimeFromSamples() - Quit because:{optoData[wrappedIx].Flags}--");
|
|
return 0;
|
|
}
|
|
|
|
log.Debug($"Valid data TimestampExt: {optoData[wrappedIx].TimestampExt}");
|
|
return optoData[wrappedIx].TimestampExt;
|
|
|
|
// if (samplesCount2 < 0) samplesCount2 = 0;
|
|
// if ((unwrappedIx - samplesCount2) < 0 || (unwrappedIx + samplesCount2) >= optoDataCount) return 0;
|
|
//
|
|
// Int64 sum = 0;
|
|
// for (int i = unwrappedIx - samplesCount2; i <= unwrappedIx + samplesCount2; i++)
|
|
// {
|
|
// int wrappedIx = BufferIdx(i);
|
|
//
|
|
// if (optoData[wrappedIx].Flags != OptoTelegramFlags.OK &&
|
|
// optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestStart &&
|
|
// optoData[wrappedIx].Flags != OptoTelegramFlags.OK_TestEnd)
|
|
// {
|
|
// return 0;
|
|
// }
|
|
//
|
|
// sum += optoData[wrappedIx].TimestampExt;
|
|
// }
|
|
//
|
|
// return sum / (double)(8192 * (2 * samplesCount2 + 1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Filter RefFlow data in an array of OptoTelegramRaw objects by a FIR filter:
|
|
///
|
|
/// kSize = 5, kSize2 = 2
|
|
///
|
|
/// i k
|
|
/// ---------------------------------------------------------------------------
|
|
/// 0 -5 filtered[0] = data[0]
|
|
/// 1 -4 filtered[1] = data[1]
|
|
/// 2 -3 filtered[2] = data[0]*k[0] + ... + data[4]*k[4]
|
|
/// 3 -2 filtered[3] = data[1]*k[0] + ... + data[5]*k[4]
|
|
/// 4 -1 filtered[4] = data[2]*k[0] + ... + data[6]*k[4]
|
|
/// 5 0 data[0] = filtered[0], filtered[0] = data[3]*k[0] + ... + data[7]*k[4]
|
|
/// 6 1 data[1] = filtered[1], filtered[1] = data[4]*k[0] + ... + data[8]*k[4]
|
|
/// 7 ...
|
|
/// </summary>
|
|
/// <param name="optoData">array of OptoTelegramRaw objects</param>
|
|
/// <param name="from">Index of the first optoData item to process</param>
|
|
/// <param name="to">Index of the last optoData item to process</param>
|
|
public static void FIRFilterFlow(OptoTelegramRaw[] optoData, int from, int to)
|
|
{
|
|
float[] kernel = new float[] { 0.1f, 0.2f, 0.4f, 0.2f, 0.1f };
|
|
int kSize = kernel.Length;
|
|
int kSize2 = kernel.Length / 2;
|
|
|
|
float[] filtered = new float[kSize];
|
|
|
|
for (int i = from; i <= to; i++)
|
|
{
|
|
if ((i < from + kSize2) || (i > to - kSize2))
|
|
{
|
|
/// Beginning or end of optoData buffer => Just copy data (=do not filter)
|
|
filtered[i % kSize] = optoData[BufferIdx(i)].RefFlow;
|
|
}
|
|
else
|
|
{
|
|
/// Make a convolution of optoData and the kernel
|
|
float weightedSum = 0;
|
|
for (int j = -kSize2; j <= kSize2; j++)
|
|
weightedSum += optoData[BufferIdx(i + j)].RefFlow * kernel[j + kSize2];
|
|
|
|
filtered[i % kSize] = weightedSum;
|
|
}
|
|
|
|
if (i >= from + kSize)
|
|
{
|
|
/// filtered[] buffer full => copy filtered data to optoData
|
|
optoData[BufferIdx(i - kSize)].RefFlow = filtered[i % kSize];
|
|
}
|
|
}
|
|
|
|
for (int i = to - kSize + 1; i <= to; i++)
|
|
{
|
|
if (i >= 0) optoData[BufferIdx(i)].RefFlow = filtered[i % kSize];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Get index to optoData buffer
|
|
/// </summary>
|
|
/// <param name="index">Original unwrapped index</param>
|
|
/// <returns>Index to the buffer</returns>
|
|
public static int BufferIdx(int index)
|
|
{
|
|
if (index < IperlHead.OptoDataBufferSize)
|
|
{
|
|
return index;
|
|
}
|
|
else
|
|
{
|
|
return IperlHead.StartOptoDataCount +
|
|
(index - IperlHead.OptoDataBufferSize) % IperlHead.EndOptoDataCount;
|
|
}
|
|
}
|
|
|
|
public void WriteBinary(BinaryWriter writer)
|
|
{
|
|
writer.Write(Disabled);
|
|
writer.Write(CommFailed);
|
|
writer.Write(ResultCode);
|
|
writer.Write(PositiveCounting);
|
|
|
|
if (ConfigStruct != null)
|
|
{
|
|
writer.Write(true);
|
|
ConfigStruct.WriteBinary(writer);
|
|
}
|
|
else writer.Write(false);
|
|
|
|
if (CalibrationStruct != null)
|
|
{
|
|
writer.Write(true);
|
|
CalibrationStruct.WriteBinary(writer);
|
|
}
|
|
else writer.Write(false);
|
|
|
|
if (CalibrationStructV4 != null)
|
|
{
|
|
writer.Write(true);
|
|
CalibrationStructV4.WriteBinary(writer);
|
|
}
|
|
else writer.Write(false);
|
|
|
|
writer.Write(OrigCalibFactor);
|
|
writer.Write(OrigCalibFactorLNA);
|
|
writer.Write(Q2ErrWOCorrection);
|
|
writer.Write(Q2CorrRL);
|
|
writer.Write(Q2CorrLR);
|
|
writer.Write(Diff2Hz8Hz);
|
|
writer.Write(Hz2CorrectionDone);
|
|
writer.Write(Hz2Correction);
|
|
|
|
if (LastTestResult != null)
|
|
{
|
|
writer.Write(true);
|
|
LastTestResult.WriteBinary(writer);
|
|
}
|
|
else writer.Write(false);
|
|
|
|
if (LastTestResult2 != null)
|
|
{
|
|
writer.Write(true);
|
|
LastTestResult2.WriteBinary(writer);
|
|
}
|
|
else writer.Write(false);
|
|
}
|
|
|
|
public void ReadBinary(BinaryReader reader)
|
|
{
|
|
Disabled = reader.ReadBoolean();
|
|
CommFailed = reader.ReadBoolean();
|
|
ResultCode = reader.ReadInt32();
|
|
PositiveCounting = reader.ReadBoolean();
|
|
|
|
if (reader.ReadBoolean()) (ConfigStruct = new ConfigStruct()).ReadBinary(reader);
|
|
if (reader.ReadBoolean()) (CalibrationStruct = new CalibrationStruct()).ReadBinary(reader);
|
|
if (reader.ReadBoolean()) (CalibrationStructV4 = new CalibrationStructV4()).ReadBinary(reader);
|
|
|
|
OrigCalibFactor = reader.ReadUInt16();
|
|
OrigCalibFactorLNA = reader.ReadUInt16();
|
|
Q2ErrWOCorrection = reader.ReadDouble();
|
|
Q2CorrRL = reader.ReadInt32();
|
|
Q2CorrLR = reader.ReadInt32();
|
|
Diff2Hz8Hz = reader.ReadDouble();
|
|
Hz2CorrectionDone = reader.ReadBoolean();
|
|
Hz2Correction = reader.ReadInt32();
|
|
|
|
if (reader.ReadBoolean()) (LastTestResult = new Results.Entities.MeterTestRslt()).ReadBinary(reader, null);
|
|
if (reader.ReadBoolean()) (LastTestResult2 = new Results.Entities.MeterTestRslt()).ReadBinary(reader, null);
|
|
}
|
|
|
|
internal void ResetNfcInterface(bool? nfc_on = null)
|
|
{
|
|
if (genesisHeadCfg.HeadCommunicationComPortNr == 0) return;
|
|
|
|
SERIAL_Driver _SERIAL_Driver_Head_Config = new SERIAL_Driver();
|
|
_SERIAL_Driver_Head_Config.OpenConnection($"COM{genesisHeadCfg.HeadCommunicationComPortNr}", 9600, 8,
|
|
Parity.None, StopBits.One);
|
|
|
|
NFCHeadConfig _NFCHead_Config = new NFCHeadConfig(_SERIAL_Driver_Head_Config);
|
|
if (nfc_on == null || nfc_on == false)
|
|
_NFCHead_Config.NFCHeadConfig_SetInterface(false); // set RFID interface
|
|
if (nfc_on == null || nfc_on == true) _NFCHead_Config.NFCHeadConfig_SetInterface(true); // set NFC interface
|
|
_SERIAL_Driver_Head_Config.Close();
|
|
_SERIAL_Driver_Head_Config.Dispose();
|
|
}
|
|
|
|
internal void SetNfcInterface()
|
|
{
|
|
ResetNfcInterface(true);
|
|
}
|
|
|
|
internal void SetRfidInterface()
|
|
{
|
|
ResetNfcInterface(false);
|
|
}
|
|
|
|
internal void SetCommunicationInterface(CommunicationInterface commInterface)
|
|
{
|
|
//using (ISession session = TBF.DB.ConfigDBSessionFactory.OpenSession())
|
|
// Replace the problematic line with the following code to fix the error:
|
|
using (ISession session = TBF.DB.SessionFactories[(int)DBKind.Config].OpenSession())
|
|
using (ITransaction tx = session.BeginTransaction())
|
|
{
|
|
try
|
|
{
|
|
var cmpntEntities = session.QueryOver<Component>()
|
|
.OrderBy(x => x.ItemNr).Asc
|
|
.List<Component>();
|
|
var cmpnt = cmpntEntities.Where(x => x.Name == Name).First();
|
|
if (cmpnt != null)
|
|
{
|
|
XDocument doc = XDocument.Parse(cmpnt.Parameters);
|
|
if (doc != null)
|
|
{
|
|
XElement element = doc.Root.Element("CommunicationInterface");
|
|
if (element != null)
|
|
{
|
|
element.Value = commInterface.ToString();
|
|
cmpnt.Parameters = doc.ToString();
|
|
session.SaveOrUpdate(cmpnt);
|
|
tx.Commit();
|
|
log.FatalFormat($"Set CommunicationInterface {Name} to {commInterface.ToString()}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (tx != null) tx.Rollback();
|
|
log.FatalFormat($"Set CommunicationInterface {Name} error: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public IOperation ReadDatastreamOp()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
|
|
public async Task<string> DataEntry_ReadSerialNumber()
|
|
{
|
|
log.Debug("called DataEntry_ReadSerialNumber()");
|
|
if (!string.IsNullOrEmpty(SerialNr)) return SerialNr;
|
|
|
|
//need to find serial number
|
|
SerialNr = await DataEntry_ReadSerialNumberAsync();
|
|
|
|
return SerialNr;
|
|
}
|
|
|
|
public Task<double> DataEntry_ReadBeginVolume()
|
|
{
|
|
log.Debug("called DataEntry_ReadBeginVolumer()");
|
|
|
|
Task<double> readedVolume = DataEntry_BeginVolumeAsync();
|
|
|
|
return readedVolume;
|
|
}
|
|
|
|
public Task<double> DataEntry_ReadEndVolume()
|
|
{
|
|
log.Debug("called DataEntry_ReadBeginVolumer()");
|
|
|
|
Task<double> readedVolume = DataEntry_EndVolumeAsync();
|
|
|
|
return readedVolume;
|
|
}
|
|
|
|
|
|
public async Task<double> DataEntry_EndVolumeAsync()
|
|
{
|
|
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
{
|
|
StartDataStreamProcessing();
|
|
if (optoSerialPort == null || !optoSerialPort.IsOpen)
|
|
{
|
|
log.Error($"optoSerialPort COM: {this.OptoComPortNr} is not open - DataEntry_EndVolumeAsync()");
|
|
return Double.NaN;
|
|
}
|
|
}
|
|
|
|
return await Task.Run(() =>
|
|
{
|
|
log.Debug($"Try get End Volume! COM: {this.OptoComPortNr}");
|
|
int ch = channel0 >= 0 ? channel0 : 0;
|
|
volumeLtr[ch] = Double.NaN;
|
|
|
|
int counter = 0;
|
|
while (Double.IsNaN(volumeLtr[ch]) && counter < 10)
|
|
{
|
|
counter++;
|
|
try
|
|
{
|
|
string readOptoDataWithTimeout = ReadOptoDataWithTimeout(3000);
|
|
if (!string.IsNullOrEmpty(readOptoDataWithTimeout))
|
|
{
|
|
try
|
|
{
|
|
StreamingDecoder _streamingDecode = new StreamingDecoder(true);
|
|
_streamingDecode.DecodeMsg(readOptoDataWithTimeout);
|
|
CalibrationRecord data = _streamingDecode.DataCalib;
|
|
if (data == null || !data.IsValid)
|
|
continue;
|
|
|
|
log.Info($"OPTHO {OptoComPortNr} Parsed optho data:" + data);
|
|
|
|
int dch = data.Channel - 1;
|
|
if (dch >= 0 && dch < iChanelsCount)
|
|
{
|
|
volumeLtr[dch] = data.VolumeCm * 1000;
|
|
channel0 = dch;
|
|
ch = dch;
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Warn($"Parsing DiagnosticLedState4Data: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
log.Debug($"Try get End Volume! COM: {this.OptoComPortNr}, Volume: {volumeLtr}");
|
|
if (optoSerialPort != null && optoSerialPort.IsOpen) CloseOptoSerialPort();
|
|
|
|
if (!Double.IsNaN(volumeLtr[channel0]))
|
|
{
|
|
endWMState = volumeLtr[channel0];
|
|
if (!Double.IsNaN(beginWMState) && !Double.IsNaN(endWMState))
|
|
{
|
|
//Solve roll over
|
|
if (endWMState < beginWMState)
|
|
{
|
|
log.Debug($"Solve roll over! endWMState: {endWMState} < beginWMState: {beginWMState}");
|
|
const double VOL_RANGE_LITERS = 16777216.0 * 0.00025; // 4,194.304 l
|
|
endWMState += VOL_RANGE_LITERS;
|
|
volumeLtr[channel0] = endWMState;
|
|
ReadPulses();
|
|
log.Debug(
|
|
$"Solve roll over! Upgraded endWMState: {endWMState}, beginWMState: {beginWMState}");
|
|
}
|
|
}
|
|
|
|
return endWMState;
|
|
}
|
|
//}
|
|
|
|
log.Warn("Default NaN value returned! Data Opto stream reading failed!");
|
|
return Double.NaN;
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
|
|
public async Task<double> DataEntry_BeginVolumeAsync()
|
|
{
|
|
if (ConfigStruct == null)
|
|
{
|
|
log.Debug("ConfigStruct is null - created new in ReadSerialNr()");
|
|
ConfigStruct = new ConfigStruct();
|
|
}
|
|
|
|
return await Task.Run(() =>
|
|
{
|
|
|
|
log.Debug($"Try get Start Volume! COM: {this.OptoComPortNr}");
|
|
|
|
Start();
|
|
|
|
int ch = channel0 >= 0 ? channel0 : 0;
|
|
volumeLtr0[ch] = Double.NaN;
|
|
|
|
int counter = 0;
|
|
while (Double.IsNaN(volumeLtr0[ch]) && counter < 10)
|
|
{
|
|
counter++;
|
|
try
|
|
{
|
|
string readOptoDataWithTimeout = ReadOptoDataWithTimeout(5000);
|
|
if (!string.IsNullOrEmpty(readOptoDataWithTimeout))
|
|
{
|
|
try
|
|
{
|
|
StreamingDecoder _streamingDecode = new StreamingDecoder(true);
|
|
_streamingDecode.DecodeMsg(readOptoDataWithTimeout);
|
|
CalibrationRecord data = _streamingDecode.DataCalib;
|
|
log.Info($"OPTHO {OptoComPortNr} Parsed optho data:" + data);
|
|
if (data == null || !data.IsValid)
|
|
continue;
|
|
|
|
int dch = data.Channel - 1;
|
|
if (dch >= 0 && dch < iChanelsCount)
|
|
{
|
|
volumeLtr0[dch] = data.VolumeCm * 1000;
|
|
channel0 = dch;
|
|
ch = dch;
|
|
break;
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Warn($"Parsing DiagnosticLedState4Data: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
log.Debug($"Try get Start Volume! COM: {this.OptoComPortNr}, Volume: {volumeLtr0}");
|
|
if (optoSerialPort != null && optoSerialPort.IsOpen) CloseOptoSerialPort();
|
|
|
|
if (!Double.IsNaN(volumeLtr0[ch]))
|
|
{
|
|
beginWMState = volumeLtr0[ch];
|
|
ReadPulses();
|
|
return beginWMState;
|
|
}
|
|
//}
|
|
|
|
log.Warn("Default NaN value returned! Data Opto stream reading failed!");
|
|
return Double.NaN;
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<string> DataEntry_ReadSerialNumberAsync()
|
|
{
|
|
if (!string.IsNullOrEmpty(SerialNr))
|
|
return SerialNr;
|
|
|
|
if (ConfigStruct == null)
|
|
{
|
|
log.Debug("ConfigStruct is null - created new in ReadSerialNr()");
|
|
ConfigStruct = new ConfigStruct();
|
|
}
|
|
|
|
if (CommFailed || ConfigStruct == null)
|
|
return CommFailed.ToString();
|
|
|
|
return await Task.Run(() =>
|
|
{
|
|
|
|
log.Debug($"Try get ReadSerialNr! COM: {this.RfidComPortNr}");
|
|
if (OptoHeadTest.ReadSerialNr())
|
|
{
|
|
SerialNr = this.ConfigStruct.PCBNumberString;
|
|
log.Debug("ReadSerialNr successful");
|
|
}
|
|
|
|
//optoHeadTest.CloseConnection();
|
|
|
|
return SerialNr;
|
|
});
|
|
}
|
|
|
|
|
|
// private static bool IsValidVolumeRecord(OptoTelegramRaw record)
|
|
// {
|
|
// return record != null &&
|
|
// (record.Flags == OptoTelegramFlags.OK ||
|
|
// record.Flags == OptoTelegramFlags.OK_TestStart ||
|
|
// record.Flags == OptoTelegramFlags.OK_TestEnd) &&
|
|
// record.IChannel() >= 0;
|
|
// }
|
|
|
|
private static int chenelsSwichCount = 3;
|
|
|
|
private double VolumeFromChannelsAtStart(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx)
|
|
{
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return 0;
|
|
|
|
double sum = 0;
|
|
int foundChannels = 0;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
for (int i = unwrappedIx; i >= 0; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.VolumeRawExt;
|
|
foundChannels++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return foundChannels > 0 ? sum / foundChannels : 0;
|
|
}
|
|
|
|
private double VolumeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx,
|
|
int samplesPerChannel)
|
|
{
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return 0;
|
|
|
|
if (samplesPerChannel <= 0)
|
|
samplesPerChannel = 1;
|
|
|
|
double channelSum = 0;
|
|
int channelCount = 0;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
double sum = 0;
|
|
int found = 0;
|
|
|
|
for (int i = unwrappedIx; i >= 0 && found < samplesPerChannel; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.VolumeRawExt;
|
|
found++;
|
|
}
|
|
|
|
if (found > 0)
|
|
{
|
|
channelSum += sum / found;
|
|
channelCount++;
|
|
}
|
|
}
|
|
|
|
return channelCount > 0 ? channelSum / channelCount : 0;
|
|
}
|
|
|
|
private double TimeFromChannelsAtStart(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx)
|
|
{
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return 0;
|
|
|
|
double sum = 0;
|
|
int foundChannels = 0;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
for (int i = unwrappedIx; i >= 0; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.TimestampExt;
|
|
foundChannels++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return foundChannels > 0 ? sum / foundChannels : 0;
|
|
}
|
|
|
|
private double TimeFromChannelsAtEnd(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx,
|
|
int samplesPerChannel)
|
|
{
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return 0;
|
|
|
|
if (samplesPerChannel <= 0)
|
|
samplesPerChannel = 1;
|
|
|
|
double channelSum = 0;
|
|
int channelCount = 0;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
double sum = 0;
|
|
int found = 0;
|
|
|
|
for (int i = unwrappedIx; i >= 0 && found < samplesPerChannel; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.TimestampExt;
|
|
found++;
|
|
}
|
|
|
|
if (found > 0)
|
|
{
|
|
channelSum += sum / found;
|
|
channelCount++;
|
|
}
|
|
}
|
|
|
|
return channelCount > 0 ? channelSum / channelCount : 0;
|
|
}
|
|
|
|
//channels count - do not change!
|
|
private const int ChannelCount = 3;
|
|
|
|
private static bool IsValidVolumeRecord(OptoTelegramRaw record)
|
|
{
|
|
return record != null &&
|
|
(record.Flags == OptoTelegramFlags.OK ||
|
|
record.Flags == OptoTelegramFlags.OK_TestStart ||
|
|
record.Flags == OptoTelegramFlags.OK_TestEnd) &&
|
|
record.IChannel() >= 0 &&
|
|
record.IChannel() < ChannelCount;
|
|
}
|
|
|
|
private static double AverageNullable(double?[] values)
|
|
{
|
|
double sum = 0;
|
|
int count = 0;
|
|
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
if (values[i].HasValue)
|
|
{
|
|
sum += values[i].Value;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count > 0 ? sum / count : 0;
|
|
}
|
|
|
|
|
|
|
|
private double?[] VolumeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx)
|
|
{
|
|
var result = new double?[chenelsSwichCount];
|
|
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return result;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
for (int i = unwrappedIx; i >= 0; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
result[channel] = record.VolumeRawExt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private double?[] VolumeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx,
|
|
int samplesPerChannel)
|
|
{
|
|
var result = new double?[chenelsSwichCount];
|
|
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return result;
|
|
|
|
if (samplesPerChannel <= 0)
|
|
samplesPerChannel = 1;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
double sum = 0;
|
|
int found = 0;
|
|
|
|
for (int i = unwrappedIx; i >= 0 && found < samplesPerChannel; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.VolumeRawExt;
|
|
found++;
|
|
}
|
|
|
|
if (found > 0)
|
|
result[channel] = sum / found;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private OptoTelegramRaw[][] _rawStartEndByChannel;
|
|
private OptoTelegramRaw[][] _recalculatedStartEndByChannel;
|
|
|
|
private OptoTelegramRaw[][] BuildStartEndByChannel(
|
|
OptoTelegramRaw[] optoData,
|
|
int optoDataCount,
|
|
int startIx,
|
|
int endIx)
|
|
{
|
|
var grouped = GroupRecordsPerChannel(optoData, startIx, endIx, optoDataCount);
|
|
var result = new OptoTelegramRaw[ChannelCount][];
|
|
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
result[ch] = new OptoTelegramRaw[2];
|
|
|
|
if (grouped[ch] == null || grouped[ch].Length < 2)
|
|
continue;
|
|
|
|
result[ch][0] = new OptoTelegramRaw();
|
|
result[ch][0].Copy(grouped[ch][0]);
|
|
|
|
result[ch][1] = new OptoTelegramRaw();
|
|
result[ch][1].Copy(grouped[ch][grouped[ch].Length - 1]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void PrepareCalculatedChannelData()
|
|
{
|
|
if (optoData == null || optoDataCount <= 0)
|
|
{
|
|
_rawStartEndByChannel = null;
|
|
_recalculatedStartEndByChannel = null;
|
|
return;
|
|
}
|
|
|
|
log.Debug("-- BuildStartEndByChannel --");
|
|
_rawStartEndByChannel = BuildStartEndByChannel(optoData, optoDataCount, TestStartTelegramIx, TestEndTelegramIx);
|
|
log.Debug("-- RecalculateVolumeAndTimeDeltaPerChannel --");
|
|
_recalculatedStartEndByChannel = RecalculateVolumeAndTimeDeltaPerChannel(optoData, optoDataCount, TestStartTelegramIx, TestEndTelegramIx);
|
|
|
|
log.Debug($"PrepareCalculatedChannelData: optoDataCount={optoDataCount}");
|
|
|
|
// 🔹 RAW DATA LOG
|
|
if (_rawStartEndByChannel != null)
|
|
{
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
var start = _rawStartEndByChannel[ch]?[0];
|
|
var end = _rawStartEndByChannel[ch]?[1];
|
|
|
|
log.Debug(
|
|
$"RAW Ch{ch + 1}: " +
|
|
$"Start(V={start?.VolumeRawExt}, T={start?.TimestampExt}) | " +
|
|
$"End(V={end?.VolumeRawExt}, T={end?.TimestampExt})");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Warn("RAW data is NULL");
|
|
}
|
|
|
|
// 🔹 RECALCULATED DATA LOG
|
|
if (_recalculatedStartEndByChannel != null)
|
|
{
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
var start = _recalculatedStartEndByChannel[ch]?[0];
|
|
var end = _recalculatedStartEndByChannel[ch]?[1];
|
|
|
|
log.Debug(
|
|
$"RECALC Ch{ch + 1}: " +
|
|
$"Start(V={start?.VolumeRawExt}, T={start?.TimestampExt}) | " +
|
|
$"End(V={end?.VolumeRawExt}, T={end?.TimestampExt}) | " +
|
|
$"ΔV={(end != null && start != null ? end.VolumeRawExt - start.VolumeRawExt : 0)} | " +
|
|
$"ΔT={(end != null && start != null ? end.TimestampExt - start.TimestampExt : 0)}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Warn("RECALCULATED data is NULL");
|
|
}
|
|
}
|
|
|
|
|
|
private (int channel, double? deltaTime, double? deltaVolume)[] TimeDeltaPerChannel(
|
|
OptoTelegramRaw[][] grouped)
|
|
{
|
|
var result = new (int channel, double? deltaTime, double? deltaVolume)[ChannelCount];
|
|
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
var records = grouped[ch];
|
|
|
|
if (records == null || records.Length < 2)
|
|
{
|
|
result[ch] = (ch, null, null);
|
|
continue;
|
|
}
|
|
|
|
var first = records[0];
|
|
var last = records[records.Length - 1];
|
|
|
|
double rawDelta = last.TimestampExt - first.TimestampExt;
|
|
double rawDeltaVol = last.VolumeRawExt - first.VolumeRawExt;
|
|
|
|
log.Debug($"COM{OptoComPortNr} First raw: {first.rawDataToString()}");
|
|
log.Debug($"COM{OptoComPortNr} Last raw: {last.rawDataToString()}");
|
|
|
|
if (rawDeltaVol < 0)
|
|
{
|
|
log.Error($"Negative delta volume for channel {ch + 1}: {rawDeltaVol}");
|
|
}
|
|
|
|
if (rawDelta < 0)
|
|
{
|
|
log.Error($"Negative delta time for channel {ch + 1}: {rawDelta}");
|
|
}
|
|
|
|
result[ch] = (ch, rawDelta, rawDeltaVol);
|
|
|
|
log.Debug($" ---- COM{OptoComPortNr} ----");
|
|
log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: First raw: {first.rawDataToString()}");
|
|
log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: Last raw: {last.rawDataToString()}");
|
|
log.Debug($"COM{OptoComPortNr} Ch{ch + 1}: rawΔT={rawDelta} <last.TimestampExt={last.TimestampExt}, first.TimestampExt = {first.TimestampExt}>, rawΔVol={rawDeltaVol} <last.VolumeRawExt={last.VolumeRawExt}, first.VolumeRawExt = {first.VolumeRawExt}> ");
|
|
log.Debug($" --- ---");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private OptoTelegramRaw[][] GroupRecordsPerChannel(
|
|
OptoTelegramRaw[] data,
|
|
int startIx,
|
|
int endIx,
|
|
int dataCount)
|
|
{
|
|
var result = new List<OptoTelegramRaw>[ChannelCount];
|
|
|
|
// init lists
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
result[ch] = new List<OptoTelegramRaw>();
|
|
|
|
if (startIx < 0 || endIx < 0 || startIx >= dataCount || endIx >= dataCount)
|
|
return result.Select(l => l.ToArray()).ToArray();
|
|
|
|
// ensure forward direction
|
|
if (startIx > endIx)
|
|
{
|
|
var tmp = startIx;
|
|
startIx = endIx;
|
|
endIx = tmp;
|
|
}
|
|
|
|
for (int i = startIx; i <= endIx; i++)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = data[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
int ch = record.IChannel();
|
|
|
|
if (ch < 0 || ch >= ChannelCount)
|
|
continue;
|
|
|
|
result[ch].Add(record);
|
|
}
|
|
|
|
// convert List[] → array[]
|
|
return result.Select(l => l.ToArray()).ToArray();
|
|
}
|
|
|
|
private OptoTelegramRaw[][] RecalculateVolumeAndTimeDeltaPerChannel(
|
|
OptoTelegramRaw[] optoData,
|
|
int optoDataCount,
|
|
int startIx,
|
|
int endIx)
|
|
{
|
|
var recordByChannel = GroupRecordsPerChannel(
|
|
optoData,
|
|
startIx,
|
|
endIx,
|
|
optoDataCount);
|
|
|
|
var timeDelta = TimeDeltaPerChannel(recordByChannel);
|
|
|
|
var result = timeDelta
|
|
.Where(x => x.deltaTime.HasValue)
|
|
.OrderByDescending(x => x.deltaTime.Value)
|
|
.FirstOrDefault();
|
|
|
|
int maxChannel = result.channel;
|
|
double? maxValueTime = result.deltaTime;
|
|
|
|
if (maxChannel < 0 || !maxValueTime.HasValue)
|
|
return null;
|
|
|
|
if (recordByChannel[maxChannel] == null || recordByChannel[maxChannel].Length < 2)
|
|
return null;
|
|
|
|
var maxStartRecord = recordByChannel[maxChannel][0];
|
|
var maxEndRecord = recordByChannel[maxChannel][recordByChannel[maxChannel].Length - 1];
|
|
|
|
OptoTelegramRaw[][] recalculatedVariablesByChannel = new OptoTelegramRaw[ChannelCount][];
|
|
for (int channel = 0; channel < ChannelCount; channel++)
|
|
{
|
|
recalculatedVariablesByChannel[channel] = new OptoTelegramRaw[2];
|
|
}
|
|
|
|
for (int channel = 0; channel < ChannelCount; channel++)
|
|
{
|
|
if (recordByChannel[channel] == null || recordByChannel[channel].Length < 2)
|
|
continue;
|
|
|
|
var channelStartRecord = recordByChannel[channel][0];
|
|
var channelEndRecord = recordByChannel[channel][recordByChannel[channel].Length - 1];
|
|
|
|
recalculatedVariablesByChannel[channel][0] = new OptoTelegramRaw();
|
|
recalculatedVariablesByChannel[channel][1] = new OptoTelegramRaw();
|
|
|
|
recalculatedVariablesByChannel[channel][0].Copy(channelStartRecord);
|
|
recalculatedVariablesByChannel[channel][1].Copy(channelEndRecord);
|
|
|
|
recalculatedVariablesByChannel[channel][0].TimestampExt = maxStartRecord.TimestampExt;
|
|
recalculatedVariablesByChannel[channel][1].TimestampExt = maxEndRecord.TimestampExt;
|
|
|
|
var channelDeltaTime = timeDelta[channel].deltaTime;
|
|
var channelDeltaVolume = timeDelta[channel].deltaVolume;
|
|
|
|
if (channelDeltaTime.HasValue &&
|
|
channelDeltaVolume.HasValue &&
|
|
channelDeltaTime.Value != 0)
|
|
{
|
|
double timeCoef = maxValueTime.Value / channelDeltaTime.Value;
|
|
double recalculatedDeltaVolume = channelDeltaVolume.Value * timeCoef;
|
|
|
|
recalculatedVariablesByChannel[channel][1].VolumeRawExt =
|
|
recalculatedVariablesByChannel[channel][0].VolumeRawExt + recalculatedDeltaVolume;
|
|
}
|
|
}
|
|
|
|
return recalculatedVariablesByChannel;
|
|
}
|
|
|
|
private double?[] TimeStartPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx)
|
|
{
|
|
var result = new double?[chenelsSwichCount];
|
|
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return result;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
for (int i = unwrappedIx; i >= 0; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
result[channel] = record.TimestampExt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private double?[] TimeEndPerChannel(OptoTelegramRaw[] optoData, int optoDataCount, int unwrappedIx,
|
|
int samplesPerChannel)
|
|
{
|
|
var result = new double?[chenelsSwichCount];
|
|
|
|
if (unwrappedIx < 0 || unwrappedIx >= optoDataCount)
|
|
return result;
|
|
|
|
if (samplesPerChannel <= 0)
|
|
samplesPerChannel = 1;
|
|
|
|
for (int channel = 0; channel < chenelsSwichCount; channel++)
|
|
{
|
|
double sum = 0;
|
|
int found = 0;
|
|
|
|
for (int i = unwrappedIx; i >= 0 && found < samplesPerChannel; i--)
|
|
{
|
|
int wrappedIx = BufferIdx(i);
|
|
var record = optoData[wrappedIx];
|
|
|
|
if (!IsValidVolumeRecord(record))
|
|
continue;
|
|
|
|
if (record.IChannel() != channel)
|
|
continue;
|
|
|
|
sum += record.TimestampExt;
|
|
found++;
|
|
}
|
|
|
|
if (found > 0)
|
|
result[channel] = sum / found;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private static double GetCachedVolume(OptoTelegramRaw[][] data, int channel, int startEndIndex)
|
|
{
|
|
if (data == null ||
|
|
channel < 0 || channel >= data.Length ||
|
|
data[channel] == null ||
|
|
startEndIndex < 0 || startEndIndex >= data[channel].Length ||
|
|
data[channel][startEndIndex] == null)
|
|
return 0;
|
|
|
|
return data[channel][startEndIndex].VolumeRawExt;
|
|
}
|
|
|
|
private static double GetCachedTime(OptoTelegramRaw[][] data, int channel, int startEndIndex)
|
|
{
|
|
if (data == null ||
|
|
channel < 0 || channel >= data.Length ||
|
|
data[channel] == null ||
|
|
startEndIndex < 0 || startEndIndex >= data[channel].Length ||
|
|
data[channel][startEndIndex] == null)
|
|
return 0;
|
|
|
|
return data[channel][startEndIndex].TimestampExt;
|
|
}
|
|
|
|
private static double AverageCachedVolume(OptoTelegramRaw[][] data, int startEndIndex)
|
|
{
|
|
double sum = 0;
|
|
int count = 0;
|
|
|
|
if (data == null)
|
|
return 0;
|
|
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
if (data[ch] != null &&
|
|
data[ch].Length > startEndIndex &&
|
|
data[ch][startEndIndex] != null)
|
|
{
|
|
sum += data[ch][startEndIndex].VolumeRawExt;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count > 0 ? sum / count : 0;
|
|
}
|
|
|
|
private static double AverageCachedTime(OptoTelegramRaw[][] data, int startEndIndex)
|
|
{
|
|
double sum = 0;
|
|
int count = 0;
|
|
|
|
if (data == null)
|
|
return 0;
|
|
|
|
for (int ch = 0; ch < ChannelCount; ch++)
|
|
{
|
|
if (data[ch] != null &&
|
|
data[ch].Length > startEndIndex &&
|
|
data[ch][startEndIndex] != null)
|
|
{
|
|
sum += data[ch][startEndIndex].TimestampExt;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count > 0 ? sum / count : 0;
|
|
}
|
|
|
|
#region start up flash
|
|
|
|
private void BeginStartupFlush(int durationMs = StartupFlushMs)
|
|
{
|
|
lock (_startupFlushSync)
|
|
{
|
|
_startupFlushIgnoredLines = 0;
|
|
_startupFlushFirstIgnoredUtc = null;
|
|
_startupFlushLastIgnoredUtc = null;
|
|
_startupFlushUntilUtc = DateTime.UtcNow.AddMilliseconds(durationMs);
|
|
_startupFlushActive = true;
|
|
|
|
log.DebugFormat(
|
|
"Startup flush started for {0} ms, until {1:HH:mm:ss.fff}",
|
|
durationMs,
|
|
_startupFlushUntilUtc);
|
|
}
|
|
}
|
|
|
|
private void EndStartupFlushIfNeeded()
|
|
{
|
|
lock (_startupFlushSync)
|
|
{
|
|
if (!_startupFlushActive)
|
|
return;
|
|
|
|
if (DateTime.UtcNow < _startupFlushUntilUtc)
|
|
return;
|
|
|
|
_startupFlushActive = false;
|
|
|
|
log.WarnFormat(
|
|
"Startup flush finished. Ignored {0} incoming opto lines.",
|
|
_startupFlushIgnoredLines);
|
|
}
|
|
}
|
|
|
|
private bool ShouldIgnoreLineDuringStartupFlush()
|
|
{
|
|
lock (_startupFlushSync)
|
|
{
|
|
if (!_startupFlushActive)
|
|
return false;
|
|
|
|
if (DateTime.UtcNow >= _startupFlushUntilUtc)
|
|
{
|
|
_startupFlushActive = false;
|
|
|
|
log.WarnFormat(
|
|
"Startup flush finished. Ignored {0} incoming opto lines. Window: {1:HH:mm:ss.fff} - {2:HH:mm:ss.fff}",
|
|
_startupFlushIgnoredLines,
|
|
_startupFlushFirstIgnoredUtc,
|
|
_startupFlushLastIgnoredUtc);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (_startupFlushFirstIgnoredUtc == null)
|
|
{
|
|
_startupFlushFirstIgnoredUtc = DateTime.UtcNow;
|
|
}
|
|
_startupFlushLastIgnoredUtc = DateTime.UtcNow;
|
|
_startupFlushIgnoredLines++;
|
|
return true;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
}
|
|
} |