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 { /// /// based on IPerlReader class /// 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 = ""; } catch (Exception ex) { cfgText = $""; } 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 bool EnableShowChanels => genesisHeadCfg?.EnableShowChanels ?? false; 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; /// /// Passed to OptoTelegramRaw.UpdateFromString(...) /// 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); } } /// Result of the last test used to calculate Q2 correction factors, etc 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; /// /// 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 /// /// Test result for calculations /// Original calibration factor /// Lower limit for the calibration factor /// Upper limit for the calibration factor /// New calibration factor or 0 (= Out of range) 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 } } /// /// Q2 correction factor calculated from the last test (Q2). /// This factor should be used only for R800 meters. /// /// A test result from which to calculate the factor /// Nominal flow in m3/h /// 0 or the current Q2 correction factor when updating the factor /// Calculated Q2 correction factor 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; } /// /// 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. /// /// Test result @2Hz from which to calculate the factor /// Test result @8Hz from which to calculate the factor /// The calculated Q2 correction factor /// true = OK, false = failed 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; } /// /// 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. /// /// Currently executed test /// Currently executed repetition number 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); } } if (DebugLevel == DebugMode.Normal) { /// Check whether head is connected, working try { OpenOptoSerialPortIfNotInit($"COM{genesisHeadCfg.OptoComPortNr}", 115200, Parity.None, 8, StopBits.One, Handshake.None); log.FatalFormat($"{Name} TestIsGoingToStartSoon - OpenOptoSerialPortIfNotInit: {genesisHeadCfg.OptoComPortNr}"); } catch (Exception ex) { log.FatalFormat($"TestIsGoingToStartSoon - {Name} initialization failed: {ex}"); } } else { log.FatalFormat($"{Name} simulated: {this}"); } } /// 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 int StartupFlushMs { get { return genesisHeadCfg?.IBeginDataFlush ?? 2500; } // or 1000 if you want 1 second - preset 2500ms } /// ~ Start UP flush public GenesisSmartReader() { } public GenesisSmartReader(Generic.IComponentCfg cfg) : base(cfg) { genesisHeadCfg = cfg as GenesisCfg; } private readonly Func _serialFactory; public GenesisSmartReader(Generic.IComponentCfg cfg, Func 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}"); } } /// /// Clear data related to a specific water meter /// 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) { //New request - let dissable connection untill need in testing or come call Soon will start test } 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() { } /// /// Events: Event.ReadRegisterDone, Event.Error /// /// ReadWaterMeter instance reference casted to IOperaton public IOperation ReadRegisterOp() { return this; } /// /// Clear data/counters related to a specific tests /// Reset - what user sees /// 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 /// /// Reset - what algorithm remembers private void Reset() { optoDataCount = 0; if (optoData == null || optoData.Length != OptoDataBufferSize) { optoData = new OptoTelegramRaw[OptoDataBufferSize]; } for (int i = 0; i < optoData.Length; i++) { if (optoData[i] == null) optoData[i] = new OptoTelegramRaw(); } Array.Clear(volumeRawExtLast, 0, volumeRawExtLast.Length); Array.Clear(timestampExtLast, 0, timestampExtLast.Length); Array.Clear(lastVolumeRaw, 0, lastVolumeRaw.Length); Array.Clear(lastTimestamp, 0, lastTimestamp.Length); for (int i = 0; i < volumeLtr.Length; i++) { volumeLtr[i] = double.NaN; volumeLtr0[i] = double.NaN; timestampSec[i] = double.NaN; timestampSec0[i] = double.NaN; } TestStartTelegramIx = -1; TestEndTelegramIx = -1; currentTelegramIx = -1; startSampleAcquired = false; timeFromStart = 0; channel0 = -1; ResultCode = 0; ClearReceivedLines(); log.Debug("Reset for new test"); } /// /// Start this operation /// public void Start() { var stopTask = _backgroundStopTask; if (stopTask != null && !stopTask.IsCompleted) { log.Debug($"Start() waiting for previous background stop of {Name}"); stopTask.Wait(); } log.DebugFormat("Start: {0:HH:mm:ss.fff} COM{1} ", DateTime.Now, OptoComPortNr); //RequestImmediateStop(); // ensure fully frozen lock (_stateSync) { Reset(); // clean state Clear(); ResetBlockCountersAndState(); ReadPulses(); StartDataStreamProcessing(); } } /// /// Run this operation /// /// eventDone public Event Run() { lock (_stateSync) { log.DebugFormat("Run: {0:HH:mm:ss.fff}", DateTime.Now); timeFromStart += StateMachine.Period; ReadPulses(); // Start sample after 3 second if (!startSampleAcquired && (timeFromStart >= 3) && (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; private Task _backgroundStopTask = Task.CompletedTask; /// /// Stop this operation /// public void Stop() { DataStreamState previousState; lock (_stopSync) { if (_isStopping) return; _isStopping = true; } previousState = RequestImmediateStopInternal(DataStreamState.Flush); log.DebugFormat( "Stop: {0:HH:mm:ss.fff} - previousState={1}, registerReader={2}", DateTime.Now, previousState, Name); // If we need final calculation, do synchronous stop if (previousState == DataStreamState.CalculateStoredData) { StopInternal(doPostProcessing: true); return; } // Otherwise run cleanup in background and return immediately _backgroundStopTask = Task.Run(() => StopInternal(doPostProcessing: false)); } private void StopInternal(bool doPostProcessing) { log.DebugFormat( "Stop: TASK {0:HH:mm:ss.fff} - dataStreamProcessing() = {1} registerReader = {2}", DateTime.Now, dataStreamState, Name); int startIx = -1; int endIx = -1; try { // stop workers first StopProcessingLoop(); CloseOptoSerialPort(); // now clear leftovers ClearReceivedLines(); if (doPostProcessing) { lock (_stateSync) { AddTestStartEndMarksToData(out startIx, out endIx); DataStreamPostProcessing(); log.WarnFormat( "Genesis.Stop( COM{3}) startIx={0} endIx={1} len={2}", startIx, endIx, optoData.Length, OptoComPortNr); if (TestStartTelegramIx == 0 || optoDataCount < 100) { ResultCode |= (int)Results.Entities.ResultCode.MissingOptoData; } else if (VolumeLtrEnd == VolumeLtrStart) { ResultCode |= (int)Results.Entities.ResultCode.OptoDataWithZeroFlow; } } } else { log.DebugFormat( "StopInternal background cleanup finished for {0}, COM{1}. No post-processing.", Name, OptoComPortNr); } } catch (Exception ex) { log.Error($"StopInternal failed for {Name}: {ex}"); } finally { lock (_startupFlushSync) { _startupFlushActive = false; } lock (_stopSync) { _isStopping = false; } } log.DebugFormat( "Stop: TASK Finished {0:HH:mm:ss.fff} - dataStreamProcessing() = {1} registerReader = {2}", DateTime.Now, dataStreamState, Name); } private DataStreamState RequestImmediateStopInternal(DataStreamState newState) { lock (_stopSync) { var previousState = dataStreamState; StopQueueData = true; startDataProcessing = false; if (newState != null) { dataStreamState = newState; } _queueSignal.Set(); log.DebugFormat( "RequestImmediateStop: {0:HH:mm:ss.fff} - previousState={1}, registerReader = {2}", DateTime.Now, previousState, Name); return previousState; } } public void RequestImmediateStop() { //No change state for this call - now in Stop is used RequestImmediateStopInternal(dataStreamState); } 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; } } /// /// Data stream post processing: /// Flow from a reference flowmeter is FIR filtered /// void DataStreamPostProcessing() { PrepareCalculatedChannelData(); try { log.DebugFormat("DataStreamPostProcessing() - harcoded call GetQ3Calibration(200.0, 120.0, 15625.0);"); GetQ3Calibration(200.0, 120.0, 15625.0); } catch (Exception ex) { log.Error($"DataStreamPostProcessing -- Q3 CALIBRATION -- failed: {ex}"); } } /// /// Determine opto data file name: PCB_AA_BB_HH_MI_SS..txt /// /// Test name /// Test repeats count (>= 1) /// Repetition number (1 .. testRepeats) /// 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); } /// /// Save opto data to a file. /// 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; } } /// /// Close the serial port /// StopOptoReadLoop is called inside /// private void CloseOptoSerialPort() { // 1. Stop background reading FIRST StopOptoReadLoop(); // 2. Synchronize with ReadLine() lock (_serialReadSync) { if (optoSerialPort != null) { try { if (optoSerialPort.IsOpen) { ISerialDriver portRef; lock (_serialReadSync) { portRef = optoSerialPort; optoSerialPort = null; } portRef?.Close(); } } catch (Exception ex) { log.Error($"Error closing opto port: {ex.Message}"); } finally { optoSerialPort = null; log.FatalFormat($"{Name} OptoPort closed: {this}"); } } } } DataStreamState dataStreamState; /// /// Reset counters / indices / time and start processing and saving datastream data /// 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(StartupFlushMs); // start up flush 2500ms StartProcessingLoop(); StartOptoReadLoop(); } finally { StopQueueData = false; } } public void SetCommunicationInterface(string commInterface) { throw new NotImplementedException(); } /// /// Returns true when processing and saving datastream data is in progress /// bool IsDataStreamProcessing() { return dataStreamState == DataStreamState.ProcessAndSave; } void ISmartReader.SetRfidInterface() { SetRfidInterface(); } /// /// Stop processing and saving datastream data /// 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; } /// /// 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. /// 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 TestLineProcessed; internal Func 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 _receivedLines = new ConcurrentQueue(); 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; ISerialDriver portRef; lock (_serialReadSync) { portRef = optoSerialPort; } if (portRef == null || !portRef.IsOpen) continue; line = portRef.ReadLine(); //switch stopQueneData if (!StopQueueData && !string.IsNullOrWhiteSpace(line)) { if (_isStopping || StopQueueData || !startDataProcessing || dataStreamState != DataStreamState.ProcessAndSave) continue; if (ShouldIgnoreLineDuringStartupFlush()) { // old buffered meter data, discard it continue; } if (_isStopping || StopQueueData || !startDataProcessing || dataStreamState != DataStreamState.ProcessAndSave) 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(); /// /// Reads opto-datastream via serial port. Invoked from RunDeviceBefore() /// /// ... /// /// OptoState.Read or OptoState.Flush 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) { blockCompleted = false; if (_isStopping || StopQueueData || !startDataProcessing || dataStreamState != DataStreamState.ProcessAndSave) return; var encoding = optoSerialPort?.Encoding ?? Encoding.ASCII; byte[] bytes = encoding.GetBytes(line); log.Debug("ComPort: " + OptoComPortNr + " OPTHO RX ← " + HexFormatter.ToSerialHex(bytes)); 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 (optoData == null) throw new InvalidOperationException($"{Name}: optoData is null"); if (bufferIx < 0 || bufferIx >= optoData.Length) throw new IndexOutOfRangeException($"{Name}: invalid bufferIx={bufferIx}, len={optoData.Length}, optoDataCount={optoDataCount}"); if (optoData[bufferIx] == null) { log.ErrorFormat("{0}: optoData[{1}] was null, recreating.", Name, bufferIx); optoData[bufferIx] = new OptoTelegramRaw(); } 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); } /// /// Reset buffer - opto serial Read Data Buffer /// 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 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]; } } /// /// /// Called from the state machine when a test is selected and UI needs to be updated. /// public void OnOptoReceived(object sender, OptoReceivedEventArgs args) { if (OptoReceivedHandler == null) return; try { OptoReceivedHandler(sender, args); } catch (Exception) { } } public event EventHandler OptoReceivedHandler; /// /// 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) /// /// true when type is OK 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); } } /// /// Scaling factor: /// 0, 1 (DN15, Coax) . . . . 1 /// 2 (DN20) . . . . . . . . 2 /// 3 (DN25) . . . . . . . . 4 /// 4, 5 (DN26, DN32) . . . . 8 /// 6 (DN40) . . . . . . . . 16 /// /// MeterType (0..6) /// Scaling factor 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; } } /// /// Calculate a filtered volume from data stream samples /// /// Samples used in calculation are centered around unwrappedIx /// Count of samples used in calculation is 2 * smaplesCount2 + 1 /// Filtered volume 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); } /// /// Calculate a filtered time from data stream samples /// /// Samples used in calculation are centered around unwrappedIx /// Count of samples used in calculation is 2 * smaplesCount2 + 1 /// Filtered time 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)); } /// /// 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 ... /// /// array of OptoTelegramRaw objects /// Index of the first optoData item to process /// Index of the last optoData item to process 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]; } } /// /// Get index to optoData buffer /// /// Original unwrapped index /// Index to the buffer 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() .OrderBy(x => x.ItemNr).Asc .List(); 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 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 DataEntry_ReadBeginVolume() { log.Debug("called DataEntry_ReadBeginVolumer()"); Task readedVolume = DataEntry_BeginVolumeAsync(); return readedVolume; } public Task DataEntry_ReadEndVolume() { log.Debug("called DataEntry_ReadBeginVolumer()"); Task readedVolume = DataEntry_EndVolumeAsync(); return readedVolume; } public async Task 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 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 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; } internal void PrepareCalculatedChannelDataForTest(bool simulate) { if (simulate) { var fake = GenerateFakeStartEndByChannel(); _rawStartEndByChannel = fake; _recalculatedStartEndByChannel = CloneStartEndByChannel(fake); optoDataCount = 2; TestStartTelegramIx = 0; TestEndTelegramIx = 1; return; } PrepareCalculatedChannelData(); } internal void PrepareCalculatedChannelDataSimulationForTest() { var fake = GenerateFakeStartEndByChannel(); _rawStartEndByChannel = fake; _recalculatedStartEndByChannel = CloneStartEndByChannel(fake); optoDataCount = 2; TestStartTelegramIx = 0; TestEndTelegramIx = 1; } private OptoTelegramRaw[][] GenerateFakeStartEndByChannel() { var result = new OptoTelegramRaw[ChannelCount][]; double baseStartTime = 10.0; double baseEndTime = 20.0; double baseStartVolume = 100.0; double baseEndVolume = 150.0; for (int ch = 0; ch < ChannelCount; ch++) { result[ch] = new OptoTelegramRaw[2]; var start = new OptoTelegramRaw(); var end = new OptoTelegramRaw(); // slightly different values per channel start.TimestampExt = baseStartTime; end.TimestampExt = baseEndTime; start.VolumeRawExt = baseStartVolume + (ch * 5.0); end.VolumeRawExt = baseEndVolume + (ch * 7.0); start.iChannel = ch + 1; end.iChannel = ch + 1; result[ch][0] = start; result[ch][1] = end; } return result; } private OptoTelegramRaw[][] CloneStartEndByChannel(OptoTelegramRaw[][] source) { if (source == null) return null; var copy = new OptoTelegramRaw[ChannelCount][]; for (int ch = 0; ch < ChannelCount; ch++) { if (source[ch] == null) continue; copy[ch] = new OptoTelegramRaw[source[ch].Length]; for (int i = 0; i < source[ch].Length; i++) { if (source[ch][i] == null) continue; copy[ch][i] = new OptoTelegramRaw(); copy[ch][i].Copy(source[ch][i]); } } return copy; } private void PrepareCalculatedChannelData() { if (this.DebugLevel == DebugMode.Simulate) { var fake = GenerateFakeStartEndByChannel(); _rawStartEndByChannel = fake; _recalculatedStartEndByChannel = CloneStartEndByChannel(fake); // make NoSamples return false optoDataCount = 2; TestStartTelegramIx = 0; TestEndTelegramIx = 1; return; } 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} , rawΔVol={rawDeltaVol} "); log.Debug($" --- ---"); } return result; } private OptoTelegramRaw[][] GroupRecordsPerChannel( OptoTelegramRaw[] data, int startIx, int endIx, int dataCount) { var result = new List[ChannelCount]; // init lists for (int ch = 0; ch < ChannelCount; ch++) result[ch] = new List(); 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 = 2500) { 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 private bool isQ3CalibValid = false; private double q3Calib = 0; public bool Q3CalibValid { get => isQ3CalibValid; } public double Q3CalibValue { get => q3Calib; } public void GetQ3Calibration(double refVolume, double refTime, double initCalibFactor) { log.Debug("=== Q3 CALIBRATION START ==="); isQ3CalibValid = false; q3Calib = 0.0; log.Debug($"Inputs: refVolume={refVolume}, refTime={refTime}, initCalibFactor={initCalibFactor}"); if (_rawStartEndByChannel == null) { log.Debug("EXIT: _rawStartEndByChannel is NULL"); return; } if (NoSamples) { log.Debug("EXIT: NoSamples == true"); return; } if (refVolume <= 0 || refTime <= 0 || initCalibFactor <= 0) { log.Debug("EXIT: Invalid input values (<= 0)"); return; } OptoTelegramRaw[][] recalculatedVariablesByChannel = new OptoTelegramRaw[ChannelCount][]; for (int channel = 0; channel < ChannelCount; channel++) { recalculatedVariablesByChannel[channel] = new OptoTelegramRaw[2]; } log.Debug($"ChannelCount={ChannelCount}"); for (int iChannel = 0; iChannel < ChannelCount; iChannel++) { var channelData = _rawStartEndByChannel[iChannel]; if (channelData == null || channelData.Length < 2) { log.Debug($"Channel {iChannel}: SKIPPED (no data)"); continue; } var start = channelData[0]; var end = channelData[1]; if (start == null || end == null) { log.Debug($"Channel {iChannel}: SKIPPED (null records)"); continue; } recalculatedVariablesByChannel[iChannel][0] = new OptoTelegramRaw(); recalculatedVariablesByChannel[iChannel][1] = new OptoTelegramRaw(); recalculatedVariablesByChannel[iChannel][0].Copy(start); recalculatedVariablesByChannel[iChannel][1].Copy(end); double deltaTime = end.TimestampExt - start.TimestampExt; double deltaVolume = end.VolumeRawExt - start.VolumeRawExt; log.Debug($"Channel {iChannel}:"); log.Debug($" Start: T={start.TimestampExt}, V={start.VolumeRawExt}"); log.Debug($" End: T={end.TimestampExt}, V={end.VolumeRawExt}"); log.Debug($" Delta: dT={deltaTime}, dV={deltaVolume}"); if (deltaTime > 0) { double timeCoef = refTime / deltaTime; double recalculatedDeltaVolume = deltaVolume * timeCoef; recalculatedVariablesByChannel[iChannel][1].VolumeRawExt = recalculatedVariablesByChannel[iChannel][0].VolumeRawExt + recalculatedDeltaVolume; log.Debug($" Recalc: coef={timeCoef}, new dV={recalculatedDeltaVolume}"); } else { log.Debug($" WARNING: deltaTime <= 0 → no recalculation"); } recalculatedVariablesByChannel[iChannel][0].TimestampExt = 0; recalculatedVariablesByChannel[iChannel][1].TimestampExt = refTime; } double scaling = 15625.0; log.Debug($"Scaling={scaling}"); if (scaling <= 0) { log.Debug("EXIT: scaling <= 0"); return; } double avgRawVolume = AverageCachedVolume(recalculatedVariablesByChannel, 0); log.Debug($"AverageCachedVolume={avgRawVolume}"); if (avgRawVolume <= 0) { log.Debug("EXIT: avgRawVolume <= 0"); return; } double measuredVolume = avgRawVolume / scaling; log.Debug($"MeasuredVolume={measuredVolume}"); if (measuredVolume <= 0) { log.Debug("EXIT: measuredVolume <= 0"); return; } q3Calib = (refVolume / measuredVolume) * initCalibFactor; log.Debug($"Q3Calib (raw)={q3Calib}"); double diffPercent = Math.Abs((q3Calib - initCalibFactor) / initCalibFactor) * 100.0; log.Debug($"DiffPercent={diffPercent}%"); isQ3CalibValid = diffPercent <= 5.0; log.Debug($"Validation: {(isQ3CalibValid ? "VALID" : "INVALID")}"); if (!isQ3CalibValid) { q3Calib = 0.0; log.Debug("Q3Calib reset to 0 due to invalid result"); } log.Debug("=== Q3 CALIBRATION END ==="); } } }