From a5eabc2322cf9493add55ddad571f18cb4bc8e08 Mon Sep 17 00:00:00 2001 From: Thomas Wiedebusch Date: Thu, 15 May 2025 15:43:07 +0200 Subject: [PATCH] ProductionUiCordonel: - manually merged some modules from main EOL_2.6.20 --- Common/.vs/config/applicationhost.config | 2 +- .../Ports/SerialPorts/BaseSerialPort.cs | 70 +- .../Ports/SerialPorts/BaseSerialPort.cs.bak | 693 +++++++++++++ .../Genesis/GenesisCore/RegisterRestorer.cs | 9 + .../GenesisCore/RegisterRestorer.cs.bak | 104 +- ...erRegisters.csproj.CoreCompileInputs.cache | 2 +- .../ProductionUiCordonel/App.config | 10 +- .../EolProcesses/CheckRadio.cs | 18 +- .../EolProcesses/CheckRadio.cs.bak | 829 +++++++++++++++ .../ProductionProcesses/ProcessController.cs | 5 +- .../ProcessController.cs.bak | 857 ++++++++++++++++ .../ProductionWindow.xaml.cs | 14 + .../ProductionWindow.xaml.cs.bak | 963 ++++++++++++++++++ 13 files changed, 3496 insertions(+), 80 deletions(-) create mode 100644 Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs.bak create mode 100644 Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs.bak create mode 100644 Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs.bak create mode 100644 Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs.bak diff --git a/Common/.vs/config/applicationhost.config b/Common/.vs/config/applicationhost.config index a894c3cf..883679a3 100644 --- a/Common/.vs/config/applicationhost.config +++ b/Common/.vs/config/applicationhost.config @@ -155,7 +155,7 @@ - + diff --git a/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs b/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs index 13ae7987..6185afae 100644 --- a/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs +++ b/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs @@ -27,7 +27,7 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts /// /// The port is not assigned. /// - public const String PortNotAssigned = "NA"; + public const String PortNotAssigned = "NA"; /// public virtual event EventHandler OnRawRecordSendOut; @@ -193,6 +193,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts /// /// Dispose serial port /// + /// + /// - Dispose procedure changed. + /// public void Dispose() { try @@ -206,6 +209,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts //Run thread again to notice CancellationToken has changed _onSyncReceiveThread.Set(); + + // dispose directly called from here to overcome glitches caused by USB to serial interface + _serialPort.Dispose(); } catch (Exception ex) @@ -419,6 +425,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts /// The IrDA sniffer showed these on the communication line, but they are incompletely received. /// Assuming the DiscardInBuffer may be delayed, so the incoming data will be scrapped. /// + /// + /// - Dispose procedure changed. + /// private void ReadingThreadLoop() { //put the receive thread to JoinWaitSleep to avoid reading and logger output for timeout on @@ -448,8 +457,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts if (SyncMarkRecord.SkipDecoding != dataSyncMarkThisRun) { OnRawRecordReceived?.Invoke(this, new StringPortDataEventArgs(rxStringRecord, - _receiveTimeStampPc, dataSyncMarkThisRun)); + _receiveTimeStampPc, dataSyncMarkThisRun)); } + if (RecordStreamingRawData) { AsciiDataLogger.Trace($"{rxStringRecord}"); @@ -503,12 +513,14 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts if (PortSettingsForTransmitProtocol.DoubleSyncByte) { if (rxByte.Equals(PortSettingsForTransmitProtocol.ProtSyncByte) && - rxByteRecord[rxByteRecord.Count - 1].Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) + rxByteRecord[rxByteRecord.Count - 1] + .Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) { //skips this byte and read the next one rxByte = (Byte)_serialPort.ReadByte(); } } + //add byte to receive result buffer rxByteRecord.Add(rxByte); @@ -552,7 +564,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts } } catch (ThreadAbortException) - { throw; } + { + _byteDataLogger.Info($"{Ident} Thread abort exception fired!"); + } catch (TimeoutException) { //ATTENTION: This "_serialPort.DiscardInBuffer()" caused a lot of trouble as it flushes a few incoming @@ -575,12 +589,12 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts if (_receiveThread.ThreadState == ThreadState.Aborted || _receiveThread.ThreadState == ThreadState.AbortRequested) { - _byteDataLogger.Warn(ex, $"{ Ident} ThreadState is Aborted or AbortRequested but an " + - "error occurred while reading records from serial port"); + _byteDataLogger.Warn(ex, $"{Ident} ThreadState is Aborted or AbortRequested but an " + + "error occurred while reading records from serial port"); } else { - _byteDataLogger.Error(ex, $"{ Ident} Error while reading records from serial port"); + _byteDataLogger.Error(ex, $"{Ident} Error while reading records from serial port"); } } finally @@ -598,31 +612,33 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts } } } - }//while (!_tokenReadData.IsCancellationRequested) - }// over hole reading thread loop + } //while (!_tokenReadData.IsCancellationRequested) + } // over hole reading thread loop catch (ThreadAbortException) - { } + { + _byteDataLogger.Info($"{Ident} Thread abort exception fired!"); + } catch (Exception ex) { _byteDataLogger.Error($"{Ident} {ex.Message}"); } - finally - { - if (_serialPort.IsOpen) - { - _serialPort.Close(); - if (_serialPort.IsOpen) - { - _byteDataLogger.Info($"{Ident} Port close delay!"); - Thread.Sleep(50); - _serialPort.Close(); - } - _serialPort.Dispose(); - _byteDataLogger.Info($"{Ident} Port is closed"); - - - } - } + //finally + //{ + // //var retries = 5; + // //while (_serialPort != null && _serialPort.IsOpen && retries-- > 0) + // //{ + // // _serialPort.Close(); + // // if (_serialPort.IsOpen) + // // { + // // _byteDataLogger.Info($"{Ident} Port closing delay!"); + // // Thread.Sleep(500); + // // } + // //} + // //if (_serialPort != null && _serialPort.IsOpen) + // // _byteDataLogger.Info($"{Ident} Port unable to close"); + // //else + // // _byteDataLogger.Info($"{Ident} Port is closed"); + //} } /// diff --git a/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs.bak b/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs.bak new file mode 100644 index 00000000..13ae7987 --- /dev/null +++ b/Common/Hardware/Interfaces/Ports/SerialPorts/BaseSerialPort.cs.bak @@ -0,0 +1,693 @@ +using NLog; +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Threading; +using Xylem.Common.CommonCore.Consts; +using Xylem.Common.CommonCore.ThreadWatcher; +using Xylem.Common.Hardware.Interfaces.Ports.PortCore; +using Xylem.Common.Hardware.Interfaces.Ports.PortCore.EventArguments; +using Xylem.Common.Utils.Logging; + +namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts +{ + /// + /// All Ports must use BasePort as an base class + /// its support some wrapping event/function handling + /// + public abstract class BaseSerialPort : IPort, IDisposable + { + + private ILogger _byteDataLogger; + internal ILogger AsciiDataLogger; + + /// + public virtual event EventHandler OnRawRecordReceived; + + /// + /// The port is not assigned. + /// + public const String PortNotAssigned = "NA"; + + /// + public virtual event EventHandler OnRawRecordSendOut; + + //initially do not signal event + private readonly AutoResetEvent _onSyncReceiveThread = new AutoResetEvent(false); + private readonly Thread _receiveThread; + private readonly CancellationTokenSource _receiveToken = new CancellationTokenSource(); + + /// + /// Delay between bytes if receiving has started in milliseconds + /// + private const Int32 InterByteReadDelayMs = 50; + + /// + /// internal SerialPort class + /// + private readonly SerialPort _serialPort; + + + /// + /// Activate raw record recording + /// + public Boolean RecordStreamingRawData = false; + //public Boolean RecordStreamingRawData + //{ + // get => _recordStreamingRawData; + // set => _recordStreamingRawData = value; + //} + + /// + /// date and time of incoming first date interrupt on serial IO to set the + /// PC time-stamp to the record + /// + private DateTimeOffset _receiveTimeStampPc; + + /// + /// Individual port settings depending on transmit protocol + /// + protected TransmitPortSettings PortSettingsForTransmitProtocol; + + /// + public String GetPortName() + { + return _serialPort.PortName; + } + + /// + /// store a identification of a port, this contains the Slot, Port, Protocol and Type + /// e.g. "Slot:1, Port:COM4, Protocol:Request, Type:IrDA -" + /// + public String Ident; + + /// + /// ctor for SerialComPort with and full constructed properties + /// + /// set port as unique + /// Class for communication, all parameters for serial communication needs to be set + /// Class for store all parameters for serial communication needs to be set came from transmit protocol + protected BaseSerialPort(String ident, SerialPort serialPort, TransmitPortSettings setting) + { + Ident = ident; + + //communication port settings + _serialPort = serialPort; + _serialPort.BaudRate = (Int32)setting.BaudRate; + + PortSettingsForTransmitProtocol = setting; + + //assign thread to loop + _receiveThread = new Thread(ReadingThreadLoop) { Name = $"{Ident} Reading thread" }; + + AsciiDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "LedRawData", "TargetLedRawDataBase", "LedRawDataBase"); + _byteDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "UartSerialPort", "COMBase", "BaseSerialPort"); + } + + + public void RefreshLogger() + { + + AsciiDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "LedRawData", "TargetLedRawDataBase", "LedRawDataBase"); + _byteDataLogger = NLogHelper.CreateOrGetMultiLogger(Ident, "", "UartSerialPort", "COMBase", "BaseSerialPort"); + + + } + + + + /// + /// received bytes in buffer of SerialComPort, calls base function + /// + /// number of bytes in Rx buffer + protected Int32 BytesToRead() + { + try + { + return _serialPort.BytesToRead; + } + catch (Exception ex) + { + _byteDataLogger.Error(ex); + return 0; + + } + + } + + /// + /// read single byte from Rx buffer of SerialComPort, calls base function + /// + /// + protected Int32 ReadByte() + { + + try + { + return _serialPort.ReadByte(); + } + catch (Exception ex) + { + _byteDataLogger.Error(ex); + return 0; + + } + } + + /// + public void PortWrite(Byte[] record) + { + try + { + SpecificPortWrite(record); + } + catch (Exception ex) + { + throw new SystemException($"{Ident} Communication error while sending data to port", ex); + } + } + + /// + /// + /// flush Rx and Tx buffer of SerialComPort, calls base functions + /// + public void Clear() + { + try + { + if (!_serialPort.IsOpen) + { + return; + } + + _serialPort.DiscardInBuffer(); + _serialPort.DiscardOutBuffer(); + } + catch (Exception ex) + { + _byteDataLogger.Error(ex); + + } + } + + /// + /// Dispose serial port + /// + public void Dispose() + { + try + { + + //remove registration for receive delegate + _serialPort.DataReceived -= PhysicalDataReceived; + + //Cancel send and receive tokens + _receiveToken.Cancel(); + + //Run thread again to notice CancellationToken has changed + _onSyncReceiveThread.Set(); + + } + catch (Exception ex) + { + _byteDataLogger.Error(ex); + + } + + } + + + /// + public void Open() + { + try + { + + //set receive interrupt threshold for immediate execution + _serialPort.ReceivedBytesThreshold = 1; + _serialPort.DataReceived += PhysicalDataReceived; + + //start reading thread, will be immediately put to waitSleepJoin in ReadingThreadLoop + //to avoid side effects with RFID communication + + ThreadWatcher.Instance.Start(_receiveThread); + + if (!_serialPort.IsOpen) + { + _serialPort.Open(); + if (!_serialPort.IsOpen) + { + _byteDataLogger.Error($"{Ident} Port cannot be opened"); + throw new ApplicationException($"{Ident} Port cannot be opened"); + } + _byteDataLogger.Info($"{Ident} Port is opened"); + } + else + { + _byteDataLogger.Warn($"{Ident} Port was already opened"); + } + } + catch (Exception ex) + { + _byteDataLogger.Error($"{Ident} {ex.Message}"); + throw new ApplicationException($"{Ident} {ex.Message}"); + } + } + /// + /// Getting the base port + /// + protected SerialPort GetBaseComport() + { + return _serialPort; + } + + /// + /// Fill FIFO with received data + /// + /// + /// - Initial + /// + /// + /// - Time stamp added + /// + /// + /// - Timeout moved from to PhysicalDataReceived to avoid + /// of serial port while waiting on first incoming + /// data. + /// + /// + /// - Inter byte read delay used instead of response delay!!! + /// + + /// + /// + public virtual void PhysicalDataReceived(Object sender, SerialDataReceivedEventArgs e) + { + if (!_receiveToken.IsCancellationRequested) + { + //remove event delegate to avoid repeated execution during one record + _serialPort.DataReceived -= PhysicalDataReceived; + + //put the actual time stamp to this record being able to assign it correctly + //even if the decoding is delayed. This time stamp will be used to do the first + //synchronization at start and stop of the measurement. Therefore the serial buffer + //has to be flushed in advance to avoid wrong time stamp to "old" records + _receiveTimeStampPc = DateTimeOffset.UtcNow; + + //start timeout for hanging read communication called inter byte delay + _serialPort.ReadTimeout = InterByteReadDelayMs; + // _serialPort.ReadTimeout = PortSettingsForTransmitProtocol.ResponseTimeoutMs; + + //wake up the reading threat from JoinWaitSleep + _onSyncReceiveThread.Set(); + } + } + + /// + /// + /// + private SyncMarkRecord _syncMarkRecord; + + /// + public void SynchronizeReceiveBuffer(SyncMarkRecord syncMarkRecord) + { + try + { + + //not in test bench situation only for temp logging + if (syncMarkRecord == SyncMarkRecord.FlushBuffer) + { + FlushBuffer(); + return; + } + //DecodeEveryPackage is ongoing and SkipDecoding is requested = do nothing + if (_syncMarkRecord == SyncMarkRecord.DecodeEveryPackage && + syncMarkRecord == SyncMarkRecord.SkipDecoding) + { + return; + } + //deny intermediate record when a start or end record is requested + if (syncMarkRecord == SyncMarkRecord.DecodeIntermediate && + (_syncMarkRecord == SyncMarkRecord.SyncEnd || + _syncMarkRecord == SyncMarkRecord.SyncStart)) + { + return; + } + //being able to detect the first incoming record after a sync is requested, the buffer is going to be flushed + //if the measurement will be started or stopped + if (syncMarkRecord == SyncMarkRecord.SyncEnd || + syncMarkRecord == SyncMarkRecord.SyncStart || + syncMarkRecord == SyncMarkRecord.DecodeIntermediate) + { + FlushBuffer(); + } + + _byteDataLogger.Info($"{Ident} Mark next incoming record as {syncMarkRecord} (was {_syncMarkRecord})"); + _syncMarkRecord = syncMarkRecord; + + + + } + catch (Exception ex) + { + _byteDataLogger.Error($"{Ident} {ex.Message}"); + } + } + + private void FlushBuffer() + { + //check if flushing is allowed + if (null != PortSettingsForTransmitProtocol.ReceiveBufferFlushThreshold) + { + //flush buffer above threshold to get an accurate actual record with next record coming in + if (_serialPort.IsOpen && _serialPort.BytesToRead > PortSettingsForTransmitProtocol.ReceiveBufferFlushThreshold) + { + _byteDataLogger.Warn($"{Ident} Flushed receive buffer({_serialPort.BytesToRead}Byte)"); + _serialPort.DiscardInBuffer(); + } + } + } + + /// + /// Reading thread loop getting data from serial port + /// + /// + /// - Initial + /// + /// + /// - Reading will be executed and repeated until buffer is empty or timeout + /// + /// + /// - Receive byte protocol more dynamically on position of length information in record + /// and additional length, + /// - decoding of data controlled by dataSyncMarker!= SyncMarkRecord.SkipDecoding to speed up recording + /// + /// + /// - Directly invoked onRawRecord Received event, + /// - Doubling of sync byte implemented + /// + /// + /// - Timeout moved from ReadingThreadLoop to to avoid + /// of serial port while waiting on first incoming + /// data and flush receive buffer at timeout to force task to enter JoinWaitSleep state. + /// + /// + /// - Hide data in logging for e.g. passwords + /// + /// + /// - Raw record recording for missing sync-byte issues. + /// + /// + /// - Try to recover raw record on missing SYNC byte by adding the SYNC upfront and sending + /// the record to the decoding thread which will detect if just the SYNC byte had been + /// missed, then the CRC will match and the record can be decoded. + /// + /// + /// - Improved recovering of data sets at byte records with preceding SYNC byte: + /// - used payload length == 0 to skip decoding, it makes no sense to decode something where + /// the payload is empty, + /// - wait a certain time to complete incoming data. + /// + /// + /// - Inter byte read delay used instead of response delay. + /// + /// + /// - Flush buffer if received length is 0. + /// + /// + /// - Flush buffer (DiscardInBuffer) removed as some bytes are missing from time to time. + /// The IrDA sniffer showed these on the communication line, but they are incompletely received. + /// Assuming the DiscardInBuffer may be delayed, so the incoming data will be scrapped. + /// + private void ReadingThreadLoop() + { + //put the receive thread to JoinWaitSleep to avoid reading and logger output for timeout on + //RFID communication, because RFID will handle the physical receive by itself + //here the assignment of the _synReadingThreadEvent to the reading thread is being done + _onSyncReceiveThread.WaitOne(); + + try + { + while (!_receiveToken.IsCancellationRequested) + { + try + { + //remind data marker for this Thread execution time slice + var dataSyncMarkThisRun = _syncMarkRecord; + + //read line of ASCII indicated by syncByte == null + if (null == PortSettingsForTransmitProtocol.ProtSyncByte) + { + //exit is timeout from serialPort or received line + var rxStringRecord = _serialPort.ReadLine(); + + if (!string.IsNullOrEmpty(rxStringRecord)) + { + //decode optional at synchronized data SyncStart, SyncEnd or DecodeIntermediate + //todo remove after air problem + if (SyncMarkRecord.SkipDecoding != dataSyncMarkThisRun) + { + OnRawRecordReceived?.Invoke(this, new StringPortDataEventArgs(rxStringRecord, + _receiveTimeStampPc, dataSyncMarkThisRun)); + } + if (RecordStreamingRawData) + { + AsciiDataLogger.Trace($"{rxStringRecord}"); + } + } + } + //byte record with preceding SYNC byte + else + { + //normally decoding is required + dataSyncMarkThisRun = SyncMarkRecord.DecodeEveryPackage; + + //this is the record for decoding + var rxByteRecord = new List(); + + //this is the informational record for logging on undetected SYNC byte + var rxRawRecord = new List(); + + //each byte protocol has to start with a syncByte, this has to be detected first + Byte rxByte; + do + { + rxByte = (Byte)_serialPort.ReadByte(); + //record raw data stream for output on missing sync-byte to investigate this issue + rxRawRecord.Add(rxByte); + } while (_serialPort.BytesToRead > 0 && PortSettingsForTransmitProtocol.ProtSyncByte != rxByte); + + // if syncByte has not been detected skip read loop and wait for next incoming record + if (PortSettingsForTransmitProtocol.ProtSyncByte == rxByte) + { + //save received SYNC byte + rxByteRecord.Add(rxByte); + + //to assemble the length it has to be extracted first from the record at given index, + //the payload length index cannot be 0 because at this position is always the SYNC byte, + //the overall length includes the SYNC byte! + var length = PortSettingsForTransmitProtocol.ProtLengthIndex + 1; + + //if the protLengthIndex is null the protAddLength equals the entire record length + if (null == PortSettingsForTransmitProtocol.ProtLengthIndex) + { + length = PortSettingsForTransmitProtocol.ProtAddLength; + } + + //read until length index to extract the length from the record + //the length index cannot be at position 0, because this is always the syncByte + do + { + rxByte = (Byte)_serialPort.ReadByte(); + //read the next byte if doubled sync byte detected and required by transmit protocol settings + if (PortSettingsForTransmitProtocol.DoubleSyncByte) + { + if (rxByte.Equals(PortSettingsForTransmitProtocol.ProtSyncByte) && + rxByteRecord[rxByteRecord.Count - 1].Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) + { + //skips this byte and read the next one + rxByte = (Byte)_serialPort.ReadByte(); + } + } + //add byte to receive result buffer + rxByteRecord.Add(rxByte); + + //capture the length at given index and readjust the record length + if (null != PortSettingsForTransmitProtocol.ProtLengthIndex + && rxByteRecord.Count - 1 == PortSettingsForTransmitProtocol.ProtLengthIndex) + { + //if the received length indicates empty payload, then nothing is to decode + if (rxByte == 0) + { + //exit this loop + dataSyncMarkThisRun = SyncMarkRecord.SkipDecoding; + FlushBuffer(); + } + else + { + //build the new length to continue this receive loop + length = (UInt16)(PortSettingsForTransmitProtocol.ProtAddLength + rxByte); + } + } + + //wait a certain time to let the data stream coming in + if (length - rxByteRecord.Count - 1 > _serialPort.BytesToRead) + { + Thread.Sleep(2); + } + + } while (rxByteRecord.Count < length && dataSyncMarkThisRun != SyncMarkRecord.SkipDecoding); + + //all data received or skip decoding marked, skip decoding will clear the response timeout + //these protocols have always to be decoded because the base is a request protocol + OnRawRecordReceived?.Invoke(this, new ListBytePortDataEventArgs(rxByteRecord, + _receiveTimeStampPc, dataSyncMarkThisRun)); + } + else + { + //output actual byte and recorded raw data stream to investigate missed sync-byte + _byteDataLogger.Warn($"{Ident} Response SYNC Byte missing. Raw Byte Received: " + + $"{BitConverter.ToString(rxRawRecord.ToArray())}"); + } + } + } + catch (ThreadAbortException) + { throw; } + catch (TimeoutException) + { + //ATTENTION: This "_serialPort.DiscardInBuffer()" caused a lot of trouble as it flushes a few incoming + //bytes and therefore destroys the already started data stream. Here it had been left in to indicate + //this critical issue! + /*-------------------------------DO NOT ACTIVATE-----------------------------------------------------*/ + //flush receive buffer at timeout to force task to enter JoinWaitSleep state in finally + //if (_serialPort.IsOpen) + //{ + // _serialPort.DiscardInBuffer(); + //} + /*---------------------------------------------------------------------------------------------------*/ + + _byteDataLogger.Trace($"{Ident} Read timeout({InterByteReadDelayMs}ms)"); + //_byteDataLogger.Trace($"{Ident} Read timeout({PortSettingsForTransmitProtocol.ResponseTimeoutMs}ms)"); + + } + catch (Exception ex) + { + if (_receiveThread.ThreadState == ThreadState.Aborted + || _receiveThread.ThreadState == ThreadState.AbortRequested) + { + _byteDataLogger.Warn(ex, $"{ Ident} ThreadState is Aborted or AbortRequested but an " + + "error occurred while reading records from serial port"); + } + else + { + _byteDataLogger.Error(ex, $"{ Ident} Error while reading records from serial port"); + } + } + finally + { + if (_serialPort.IsOpen && !_receiveToken.IsCancellationRequested) + { + //it makes no sense to put the thread to sleep if there is something to read + if (0 == _serialPort.BytesToRead) + { + //restore event delegate to activate handle for incoming records + _serialPort.DataReceived += PhysicalDataReceived; + + //put this thread (receive thread) to JainWaitSleep + _onSyncReceiveThread.WaitOne(); + } + } + } + }//while (!_tokenReadData.IsCancellationRequested) + }// over hole reading thread loop + catch (ThreadAbortException) + { } + catch (Exception ex) + { + _byteDataLogger.Error($"{Ident} {ex.Message}"); + } + finally + { + if (_serialPort.IsOpen) + { + _serialPort.Close(); + if (_serialPort.IsOpen) + { + _byteDataLogger.Info($"{Ident} Port close delay!"); + Thread.Sleep(50); + _serialPort.Close(); + } + _serialPort.Dispose(); + _byteDataLogger.Info($"{Ident} Port is closed"); + + + } + } + } + + /// + public Boolean IsOpen() + { + return _serialPort != null && _serialPort.IsOpen; + } + + /// + /// Serial port specific write routine + /// + /// + /// + /// - Hide data in logging for e.g. passwords + /// + protected virtual void SpecificPortWrite(Byte[] txBuffer) + { + PhysicalWrite(txBuffer); + } + + /// + /// Physically sending to the serial port + /// + /// + /// - Initial + /// + /// + /// - Doubling of sync byte in protocol behind sync byte itself implemented + /// + /// + /// - Flush buffer as MOXA sometimes takes two messages and combines these to one! + /// As the Genesis needs a separated wake-up message with a following delay before the + /// real payload message, this causes a lot of trouble. + /// + protected void PhysicalWrite(Byte[] txBuffer) + { + try + { + //don't double the sync byte itself + var txByteList = new List { txBuffer[0] }; + for (var byteCtr = 1; byteCtr < txBuffer.Length; byteCtr++) + { + txByteList.Add(txBuffer[byteCtr]); + + //write the doubled sync byte again if required by transmit protocol settings + if (PortSettingsForTransmitProtocol.DoubleSyncByte) + { + if (txBuffer[byteCtr].Equals(PortSettingsForTransmitProtocol.ProtSyncByte)) + { + txByteList.Add(txBuffer[byteCtr]); + } + } + } + + _serialPort.Write(txByteList.ToArray(), 0, txByteList.Count); + + // flush the buffer for MOXA, to avoid two subsequent communications assembled to one communication! + _serialPort.BaseStream.Flush(); + + OnRawRecordSendOut?.Invoke(this, new ListBytePortDataEventArgs(txByteList, DateTimeOffset.UtcNow)); + } + catch (Exception ex) + { + _byteDataLogger.Error($"{Ident} {ex.Message}"); + } + } + } +} diff --git a/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs b/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs index d303d837..5e25bb8b 100644 --- a/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs +++ b/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs @@ -857,11 +857,20 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - CancellationToken. /// + /// + /// - Temporary excluded un-comparable registers from write as this make no sense to write them blind being + /// unable to read the correct value back. + /// private StatusReturn WriteRegisterParameterSet(List recoveryRegistersDb, Int32 recRegistersCount, MeterRegisters meterRegisters, CancellationToken cancellationToken) { foreach (var recReg in recoveryRegistersDb) { + //TODO THW Check + // Temporary excluded un-comparable registers from write as this make no sense to write them blind being + // unable to read the correct value back. + if (_excludedFromCompareParameters.Any( regName => recReg.RegisterIdent.Equals(regName))) + continue; _actualRegisterCtr++; var overallProcessCtrPercent = 100.0 * _actualRegisterCtr / recRegistersCount; diff --git a/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs.bak b/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs.bak index 5a31d27f..d303d837 100644 --- a/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs.bak +++ b/Common/Hardware/WaterMeter/Genesis/GenesisCore/RegisterRestorer.cs.bak @@ -146,10 +146,9 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// private static readonly List _finalizeProgrammingAll = new List { + // Writing 1 will force a log flush new RecoveryRegisterItem("LOGGER_TriggerLogFlush", new Byte[] { 0x01 }), - // set counter to 0 as this is a value how often the radio detected a reset - new RecoveryRegisterItem("PERIODICLOG_ResetCounter", new Byte[] { 0x00 }), - // trigger idle 0 releases the display to normal operation + // Trigger idle 0 releases the display to normal operation new RecoveryRegisterItem("GENESISFLOW_TriggerIdle", new Byte[] { 0x00 }) }; @@ -159,12 +158,16 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// private static readonly List _finalizeProgrammingEmea = new List { + // Set counter to 0 as this is a value how often the radio detected a reset + new RecoveryRegisterItem("SENSUSRADIO_ResetCounter", new Byte[] { 0x00 }), // System state 0xFF will force a storage of all parameters and reset, followed by system state 0x02 // for customer mode, the reset needs only a few milliseconds // Attention logout-delay-login required after this step! new RecoveryRegisterItem("SENSUSRADIO_SystemState", new Byte[] { 0xFF }), - // seal the display to lock against changes + // Seal the display to lock against changes new RecoveryRegisterItem("GENESISFLOW_SealDisplay", new Byte[] { 0x01 }), + // Set counter to 0 as this is a value how often the radio detected a reset + new RecoveryRegisterItem("PERIODICLOG_ResetCounter", new Byte[] { 0x00 }), }; /// @@ -172,8 +175,11 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// private static readonly List _finalizeProgrammingEol = new List { + // Writing 1 to this register clears the volume accumulator new RecoveryRegisterItem("GENESISFLOW_ResetAccumulators", new Byte[] { 0x01 }), + // Writing 1 to this register erases the RMA area new RecoveryRegisterItem("NFC_EraseRMA", new Byte[] { 0x01 }), + // Set counter to 0 as this is a value how often the customer app detected a reset new RecoveryRegisterItem("CUSTOMER_RebootCount", new Byte[] { 0x00 }) }; @@ -182,11 +188,6 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// private Int32 _actualRegisterCtr; - /// - /// Stop register access - /// - public Boolean StopRegisterAccess; - /// /// Genesis meter object /// @@ -573,6 +574,8 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore List recoveryRegistersDb, out String errorMsg) { errorMsg = ""; + if (string.IsNullOrEmpty(pcbId)) + return StatusReturn.Failed; try { var url = $"{ServiceUrls.GenesisFinalCheckServiceUrl()}GetProgrammingParameters?PcbID={pcbId}"; @@ -620,14 +623,18 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// list of registers to update from database without special sequence /// + /// /// true if setup succeeded /// /// - Initial. /// - public StatusReturn EolProgrammingRegisters(List recoveryRegistersDb) + /// + /// - CancellationToken. + /// + public StatusReturn EolProgrammingRegisters(List recoveryRegistersDb, CancellationToken cancellationToken) { // mark EOL as true to force accu reset - return WriteRegisters(recoveryRegistersDb, true); + return WriteRegisters(recoveryRegistersDb, cancellationToken, true); } /// @@ -668,6 +675,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// list of registers to update from database without special sequence /// + /// /// EOL (end of line) programming with accu reset /// true if restoring succeeded /// @@ -722,7 +730,11 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Exported write routines to . /// - public StatusReturn WriteRegisters(List recoveryRegistersDb, Boolean eolProgramming = false) + /// + /// - CancellationToken. + /// + public StatusReturn WriteRegisters(List recoveryRegistersDb, CancellationToken cancellationToken, + Boolean eolProgramming = false) { if (_currentGenesis == null || string.IsNullOrEmpty(_currentGenesis.PcbId) || @@ -740,7 +752,6 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore RecoveryRegisters = recoveryRegistersDb; } - StopRegisterAccess = false; _actualRegisterCtr = 0; // Build string list for debug @@ -787,7 +798,8 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(Resources.StrRegisterRecoveryPreparation, actualProcessMessage: Resources.StrRegisterRecoveryPreparation)); - if (StatusReturn.Failed == WriteRegisterParameterSet(_prepareProgramming, recRegistersCount, meterRegisters)) + if (StatusReturn.Failed == WriteRegisterParameterSet(_prepareProgramming, recRegistersCount, meterRegisters, + cancellationToken)) { // the logout will force the radio app to activate the settings _currentGenesis?.Logout(); @@ -798,7 +810,8 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(Resources.StrRegisterRecoveryExec, actualProcessMessage: Resources.StrRegisterRecoveryExec)); - if (StatusReturn.Failed == WriteRegisterParameterSet(recoveryRegistersDb, recRegistersCount, meterRegisters)) + if (StatusReturn.Failed == WriteRegisterParameterSet(recoveryRegistersDb, recRegistersCount, meterRegisters, + cancellationToken)) { // the logout will force the radio app to activate the settings _currentGenesis?.Logout(); @@ -809,7 +822,8 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs(Resources.StrRegisterRecoveryFinalization, actualProcessMessage: Resources.StrRegisterRecoveryFinalization)); - if (StatusReturn.Failed == WriteRegisterParameterSet(finalizeProgrammingAll, recRegistersCount, meterRegisters)) + if (StatusReturn.Failed == WriteRegisterParameterSet(finalizeProgrammingAll, recRegistersCount, meterRegisters, + cancellationToken)) { // the logout will force the radio app to activate the settings _currentGenesis?.Logout(); @@ -835,12 +849,16 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// /// + /// /// /// /// - Initial imported from . /// + /// + /// - CancellationToken. + /// private StatusReturn WriteRegisterParameterSet(List recoveryRegistersDb, Int32 recRegistersCount, - MeterRegisters meterRegisters) + MeterRegisters meterRegisters, CancellationToken cancellationToken) { foreach (var recReg in recoveryRegistersDb) { @@ -913,7 +931,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore overallProcessCtrPercent, actualProcessMsg)); } - if (StopRegisterAccess) + if (cancellationToken.IsCancellationRequested) break; } @@ -926,6 +944,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// - The _restoreRegisters has to be filled with all registers being able to restore (RW), /// - The _finalReadRegisters has to be filled. /// + /// /// true if equal /// /// - Initial. @@ -963,13 +982,15 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Hash password and encryption key with SHA256. /// - public Boolean CompareRegisters() + /// + /// - CancellationToken. + /// + public Boolean CompareRegisters(CancellationToken cancellationToken) { if (_pcbIdFinalRegisterRead != _pcbIdWriteRegisters || RecoveryRegisters == null) return false; - StopRegisterAccess = false; _actualRegisterCtr = 0; var retVal = true; @@ -1130,7 +1151,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore } } - if (StopRegisterAccess) + if (cancellationToken.IsCancellationRequested) break; } catch (Exception ex) @@ -1145,6 +1166,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// Read the registers after the update for comparision. Build the post update registers here, /// because an update may have changed the registers (new or removed registers). /// + /// /// true if registers could be read /// /// - Initial. @@ -1174,7 +1196,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Check readBack value for null. /// - public Boolean FinalReadRegisters() + /// + /// - CancellationToken. + /// + public Boolean FinalReadRegisters(CancellationToken cancellationToken) { if (_currentGenesis == null) return false; @@ -1197,7 +1222,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore _finalRegisterList.Sort(); // executes login read logout - retVal = ReadRegistersSet(ref _finalRegisters); + retVal = ReadRegistersSet(ref _finalRegisters, cancellationToken); // backup read data to RecoveryRegisters if (RecoveryRegisters != null && _finalRegisters != null) @@ -1239,6 +1264,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// The defined registers are based on the "configuration.json" and the installed application with /// a specific version. /// + /// /// true if registers could be read /// /// - Initial. @@ -1263,7 +1289,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Register list changed from RegisterDefinition to RecoveryRegisterItem list. /// - public Boolean InitialReadRegisters() + /// + /// - CancellationToken. + /// + public Boolean InitialReadRegisters(CancellationToken cancellationToken) { if (_currentGenesis == null) return false; @@ -1289,7 +1318,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore _initialRegisterList = _initialRegisters.Select(register => register.RegisterIdent).ToList(); _initialRegisterList.Sort(); - return ReadRegistersSet(ref _initialRegisters); + return ReadRegistersSet(ref _initialRegisters, cancellationToken); } /// @@ -1344,6 +1373,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// The defined registers are based on the "configuration.json" and the installed application with a specific /// version. /// + /// /// true if registers could be read /// /// - Initial. @@ -1351,7 +1381,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Register list changed from RegisterDefinition to RecoveryRegisterItem list. /// - public Boolean ReadRegisters() + /// + /// - CancellationToken. + /// + public Boolean ReadRegisters(CancellationToken cancellationToken) { if (_currentGenesis == null) return false; @@ -1366,17 +1399,21 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore OnProcessUpdate?.Invoke(this, new ProcessExecEventArgs($@"{Resources.StrReadRegister}", 0, Resources.StrReadRegistersBeforeUpdate)); - return ReadRegistersSet(ref registers); + return ReadRegistersSet(ref registers, cancellationToken); } /// /// Read predefined registers from RecoveryRegisters. /// + /// /// true if registers could be read /// /// - Initial. /// - public Boolean ReadPresetRegisters() + /// + /// - CancellationToken. + /// + public Boolean ReadPresetRegisters(CancellationToken cancellationToken) { if (_currentGenesis == null || RecoveryRegisters == null || RecoveryRegisters.Count == 0) return false; @@ -1387,7 +1424,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore _pcbIdWriteRegisters = _currentGenesis.PcbId; _pcbIdFinalRegisterRead = _currentGenesis.PcbId; - return ReadRegistersSet(ref RecoveryRegisters); + return ReadRegistersSet(ref RecoveryRegisters, cancellationToken); } /// @@ -1399,6 +1436,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// Executes login, read cycle and logout. /// /// registers to read + /// /// true if registers could be read /// /// - Initial. @@ -1428,11 +1466,13 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore /// /// - Register list changed from RegisterDefinition to RecoveryRegisterItem list. /// - private Boolean ReadRegistersSet(ref List registers) + /// + /// - CancellationToken. + /// + private Boolean ReadRegistersSet(ref List registers, CancellationToken cancellationToken) { if (_currentGenesis == null) return false; - StopRegisterAccess = false; _actualRegisterCtr = 0; // read values from Genesis @@ -1501,7 +1541,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore overallProcessCtrPercent, actualProcessMsg)); } - if (StopRegisterAccess) + if (cancellationToken.IsCancellationRequested) break; } catch (Exception ex) diff --git a/Common/Hardware/WaterMeter/WaterMeterCore/WaterMeterRegisters/obj/Debug/WaterMeterRegisters.csproj.CoreCompileInputs.cache b/Common/Hardware/WaterMeter/WaterMeterCore/WaterMeterRegisters/obj/Debug/WaterMeterRegisters.csproj.CoreCompileInputs.cache index 4163759b..9389613c 100644 --- a/Common/Hardware/WaterMeter/WaterMeterCore/WaterMeterRegisters/obj/Debug/WaterMeterRegisters.csproj.CoreCompileInputs.cache +++ b/Common/Hardware/WaterMeter/WaterMeterCore/WaterMeterRegisters/obj/Debug/WaterMeterRegisters.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -8cb55c312de586b683d72cf58c13e2801d10e14b767777d98338d3e1473a1213 +7df2ad6be70922b931820861acc675522243ee78 diff --git a/Common/Production/ProductionUiCordonel/App.config b/Common/Production/ProductionUiCordonel/App.config index 2eb4ac66..728a665b 100644 --- a/Common/Production/ProductionUiCordonel/App.config +++ b/Common/Production/ProductionUiCordonel/App.config @@ -34,11 +34,11 @@ - - - - - + + + + + diff --git a/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs b/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs index ac3db873..d3f53451 100644 --- a/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs +++ b/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs @@ -176,6 +176,9 @@ namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EolPr /// /// - CancellationToken. /// + /// + /// - Removed change and read back of MainAlarmMask and ExtendedAlarmMask. + /// public override StatusReturn ExecuteProcess() { // This will force to reset all values, Sentinel and Prepare shipping will fail based on EOL Progress @@ -304,27 +307,16 @@ namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EolPr DisableAllButtons(); }// !skipRadioCheck - // reset all radio alarms - WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_MainAlarmMask", - new Byte[] { 0 }, ProgrammingSource.ProgramInternal)); - WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_ExtendedAlarmMask", - new Byte[] { 0 }, ProgrammingSource.ProgramInternal)); // set system state back to shipping mode WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_SystemState", new Byte[] { 0xFF }, ProgrammingSource.ProgramInternal)); ActualProcessProgress++; - // check radio is in customer mode 0x02 and alarm masks are 0 - var mainAlarmMask = - RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_MainAlarmMask")); - var extendedAlarmMask = - RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_ExtendedAlarmMask")); _finalRadioSystemState = RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_SystemState")); ActualProcessProgress++; - if (// Radio mode and alarm masks NOT okay forces always an error - // radio system state 0x02 shipping mode, 0x01 production mode, 0x0C transmission and data logger active - _finalRadioSystemState != 0x02 || mainAlarmMask != 0 || extendedAlarmMask != 0 + if (// radio system state 0x02 shipping mode, 0x01 production mode, 0x0C transmission and data logger active + _finalRadioSystemState != 0x02 // If radio check is enabled the signal level has to be in range and the test has to be finished || (!skipRadioCheck && (!_successSignalLevel || !_sirtComOkay || !_testFinished))) { diff --git a/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs.bak b/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs.bak new file mode 100644 index 00000000..ac3db873 --- /dev/null +++ b/Common/Production/ProductionUiCordonel/ProductionProcesses/EolProcesses/CheckRadio.cs.bak @@ -0,0 +1,829 @@ +using System; +using System.Configuration; +using System.Linq; +using System.Net; +using System.Threading; +using System.Windows.Threading; +using LaaPackages.Features.Cordonel.Models; +using Newtonsoft.Json.Linq; +using SIRTCOM; +using Xylem.Common.CommonCore.Configuration; +using Xylem.Common.CommonCore.Consts; +using Xylem.Common.Hardware.WaterMeter.Genesis.Registers; +using Xylem.Common.Logic.ProductionOrderCore; +using Xylem.Common.Logic.SoftwareAccessHelper; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.Enum; +using Xylem.Common.Production.ProductionUiCordonel.Properties; +using Xylem.Common.Production.ProductionUiCordonel.UserControls; +using Button = System.Windows.Controls.Button; +// ReSharper disable UnusedAutoPropertyAccessor.Local + +namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EolProcesses +{ + public class CheckRadio : BaseProductionProcess + { + /// + /// Maximum of expected processing steps for this process to feed the GUI progress bar. + /// Use the sleep 250 to create x seconds timeout; + /// + private const Int32 TimeoutLoop_ms = 250; + private const Int32 Timeout_s = 10; + private const Int32 MaxActualProgressSteps = 1000 / TimeoutLoop_ms * Timeout_s; + + /// + /// Remind the ID of the radio result as feedback from the DB. + /// + private Int64 _radioResultCheckId; + + /// + /// Final state of radio system. + /// + private Int32 _finalRadioSystemState; + /// + /// Reminder for specific user control + /// + private UcRadioCheck _checkRadioUserControl; + + /// + /// Signal level successfully tested + /// + private Boolean _successSignalLevel; + + /// + /// Radio encryption key converted to string + /// + private String _radioEncryptionKey; + + /// + /// Test finished can be good or abort by done button to exit this production process + /// + private Boolean _testFinished; + + /// + /// SIRT communication okay + /// + private Boolean _sirtComOkay; + + private DataContainer _dataContainer; + private Statemashine _stateMachine; + + /// + /// Minimum PAM signal level of SIRT got from DB + /// + private Byte? _rssiPamSirtDbMin; + /// + /// Maximum PAM signal level of SIRT got from DB + /// + private Byte? _rssiPamSirtDbMax; + /// + /// Minimum SEMI signal level of Cordonel response got from DB + /// + private Byte? _rssiSemiCordonelDbMin; + /// + /// Maximum SEMI signal level of Cordonel response got from DB + /// + private Byte? _rssiSemiCordonelDbMax; + + /// + /// Measured from SIRT the signal strength of the Cordonel SEMI answer + /// + private Byte? _rssiSemiCordonelMeasuredBySirt; + + /// + /// Measured from Cordonel the signal strength of the SIRT PAM received + /// + private Byte? _rssiPamSirtMeasuredByCordonel; + + /// + /// Number of the SIRT box as these are evaluated for their signal strength + /// + private Byte _sirtBoxNr; + + /// + /// Comport number of SIRT + /// + private Byte _sirtComport; + + /// + /// Radio address Cordonel + /// + private UInt32 _radioAddressCordonel; + + /// + /// Ctor + /// + public CheckRadio(String name) : base(name) + { + } + + /// + protected override void InitUserControl() + { + UserControl = new UcRadioCheck(); + _checkRadioUserControl = (UcRadioCheck)UserControl; + _checkRadioUserControl.OnResetRequested += delegate + { + Retry(); + }; + //_checkRadioUserControl.OnDoneRequested += delegate + //{ + // SaveRssiLevelsToDb(); + //}; + } + + /// + /// Check of radio signal strength, skips process on special permission or if NA region + /// + /// + /// + /// - Initial + /// + /// + /// - Skip process on special permission or if NA region, + /// - Remove userControl from main screen if skipped. + /// + /// + /// - Fire SingleProcessState event with argument. + /// + /// + /// - Removed signals for and as + /// these will be automatically set based on return value in + /// . + /// + /// + /// - Assignment of stateMachine and dataContainer. + /// + /// + /// - Get encryption key moved to process to speed up + /// this process as this process needs to be run exclusively. + /// + /// + /// - EOL progress signaled. + /// + /// + /// - Logout at finalization, + /// - Log all write accesses. + /// + /// + /// - SkipRadioCheck not nullable. + /// + /// + /// - SetTestInit as bool. + /// + /// + /// - Separate TestInit and SirtInit (only if test shouldn't be skipped). + /// + /// + /// - CancellationToken. + /// + public override StatusReturn ExecuteProcess() + { + // This will force to reset all values, Sentinel and Prepare shipping will fail based on EOL Progress + if (Meter == null || ProductionRequirements == null || + (!Meter.Region.Equals("NA") && RadioEncryptionKey?.Count == 0)) + { + return StatusReturn.Failed; + } + + // Setup all parameters being able to execute initially or repeat the test + TestInit(); + + // Check first if radio is installed, + // North America Region(NA) doesn't have a Sensus radio application installed + if (Meter.Region.Equals("NA")) + { + EolProgress.FinalRadioSystemStateCheck = EOLStatus.NA; + LogNewStatusMsg(Resources.StrSkippedMsgRadioCheckRegion); + PrepareExit(); + return StatusReturn.Skipped; + } + + // Starting point of time-consuming operations + SignalRunningState(MaxActualProgressSteps); + + // If requirement forces skipping but radio is installed, it should at least be set to customer mode and checked for + // it. + // Reminder for skip radio check forced by requirements + var skipRadioCheck = ProductionRequirements.SkipRadioCheck; + + try + { + if (!Meter.IsLoggedOn) + { + Meter.Login(); + if (!Meter.IsLoggedOn) + { + return SetTestFailed(Resources.StrErrorMsgCordonelLogin); + } + } + + // Standard run for radio signal test (RSSI) + if (!skipRadioCheck) + { + if (UserControl == null) + { + return SetTestFailed(Resources.StrErrorMsgRadioCheck); + } + + // Initialize the SIRT + if (!SirtInit()) + { + return SetTestFailed(Resources.StrErrorMsgSirt); + } + + //FrequencyIndicator + var freq = RegisterConverter.ByteArrayToValue( + Meter.ReadRegister("SENSUSRADIO_FrequencyIndicator")); + _radioAddressCordonel = + RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_RadioAddress")); + ActualProcessProgress++; + + var meterRawEncryptionKey = Meter.ReadRegister("SENSUSRADIO_EncryptionKey", 16); + var meterEncryptionKey = ""; + + // convert byte array to string + if (meterRawEncryptionKey != null) + { + meterEncryptionKey = BitConverter.ToString(meterRawEncryptionKey.ToArray()).Replace("-", ""); + } + + if (RadioEncryptionKey != null) + { + _radioEncryptionKey = RadioEncryptionKey[0].ToUpper(); + } + + // Compare the meterEncryptionKey read out from meter with the database RadioEncryptionKey + // got with the MeterProcessState/api/FinalCheck/GetEncryptionKey + if (meterRawEncryptionKey == null || RadioEncryptionKey == null || + string.IsNullOrEmpty(meterEncryptionKey) || string.IsNullOrEmpty(_radioEncryptionKey) || + !_radioEncryptionKey.Equals(meterEncryptionKey)) + { + return SetTestFailed(Resources.StrErrorMsgRadioKey); + } + + SuccessMsgDispatcher(Resources.StrSuccessMsgDbToMeterEncryptionKey); + ActualProcessProgress++; + + _sirtComport = Convert.ToByte(ConfigurationManager.AppSettings[$"SirtComport{freq}"]); + _sirtBoxNr = Convert.ToByte(ConfigurationManager.AppSettings["SirtBoxNr"]); + var ucRadio = (UcRadioCheck)UserControl; + ucRadio.SetHeadline($"{Resources.StrStateRadioCheck} BOX: {_sirtBoxNr}"); + LogNewStatusMsg($"{Resources.StrStateRadioCheck} Port: COM{_sirtComport}, BOX: {_sirtBoxNr}, " + + $"f: {freq} MHz"); + + // Tell the radio that something is in the slot which will impact the radio transmission + WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_IrDAModulePresent", + new Byte[] { 1 }, ProgrammingSource.ProgramInternal)); + + ActualProcessProgress++; + + // Clear the measurement as on a retry started from main control the old values will be displayed + ClearRssiMeasurement(); + // kick off RSSI test + StartRssiMeasurement(); + + // Exit if meanwhile cancellation is required + if (CancellationToken.IsCancellationRequested) + CancellationProcedure(); + + var exitTime = DateTime.Now + TimeSpan.FromSeconds(Timeout_s); + var actualTime = DateTime.Now; + // wait on result or timeout or stop request from main window + while (!_testFinished && exitTime > actualTime && SingleProcessState != SingleProcessState.Idle) + { + // Exit if meanwhile cancellation is required + if (CancellationToken.IsCancellationRequested) + CancellationProcedure(); + + Thread.Sleep(TimeoutLoop_ms); + actualTime = DateTime.Now; + ActualProcessProgress++; + } + + // lock all buttons as process is finished + DisableAllButtons(); + }// !skipRadioCheck + + // reset all radio alarms + WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_MainAlarmMask", + new Byte[] { 0 }, ProgrammingSource.ProgramInternal)); + WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_ExtendedAlarmMask", + new Byte[] { 0 }, ProgrammingSource.ProgramInternal)); + // set system state back to shipping mode + WriteRegisterLogAndProcessCtr(new ProgrammingParameters("SENSUSRADIO_SystemState", + new Byte[] { 0xFF }, ProgrammingSource.ProgramInternal)); + ActualProcessProgress++; + + // check radio is in customer mode 0x02 and alarm masks are 0 + var mainAlarmMask = + RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_MainAlarmMask")); + var extendedAlarmMask = + RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_ExtendedAlarmMask")); + _finalRadioSystemState = RegisterConverter.ByteArrayToValue(Meter.ReadRegister("SENSUSRADIO_SystemState")); + ActualProcessProgress++; + + if (// Radio mode and alarm masks NOT okay forces always an error + // radio system state 0x02 shipping mode, 0x01 production mode, 0x0C transmission and data logger active + _finalRadioSystemState != 0x02 || mainAlarmMask != 0 || extendedAlarmMask != 0 + // If radio check is enabled the signal level has to be in range and the test has to be finished + || (!skipRadioCheck && (!_successSignalLevel || !_sirtComOkay || !_testFinished))) + { + return SetTestFailed(!_sirtComOkay ? Resources.StrErrorMsgSirt : Resources.StrErrorMsgRadioCheck); + } + + SuccessMsgDispatcher(Resources.StrSuccessMsgRadioInCustomerMode); + SuccessMsgDispatcher(Resources.StrSuccessMsgRadioAlarmsReset); + + // Skip radio check by requirement + if (skipRadioCheck) + { + EolProgress.FinalRadioSystemStateCheck = EOLStatus.NA; + WarningMsgDispatcher(Resources.StrSkippedMsgRadioCheckSpecialPermission); + PrepareExit(); + return StatusReturn.Skipped; + } + return SetTestSucceeded(); + + } + catch (Exception ex) + { + Meter?.WriteLog(ex.Message); + return SetTestFailed(Resources.StrErrorMsgRadioCheck); + } + } + + /// + /// Common for test Init + /// + private void TestInit() + { + // Uncheck executed process to leave the result open needed for retries + EolProgress.RadioCheckId = null; + EolProgress.FinalRadioSystemState = null; + EolProgress.FinalRadioSystemStateCheck = null; + // Value not saved as test hasn't been executed + _radioResultCheckId = 0; + _finalRadioSystemState = 0; + + _sirtComOkay = false; + _testFinished = false; + _successSignalLevel = false; + } + + /// + /// Initialize the SIRT + /// + /// + private Boolean SirtInit() + { + try + { + // Reset needed for repeated run + _stateMachine?.Stop(); + _stateMachine?.ClearBUPRadioAdresses(); + _stateMachine?.ClearPam(); + _stateMachine = null; + _dataContainer = null; + _stateMachine = new Statemashine(); + _dataContainer = new DataContainer(); + + } + catch (Exception ex) + { + Meter?.WriteLog(ex.Message); + return false; + } + + return true; + } + + + /// + /// Common to prepare exit for all status returns + /// + private void PrepareExit() + { + EolProgress.RadioCheckId = _radioResultCheckId; + EolProgress.FinalRadioSystemState = _finalRadioSystemState; + + // Essential to logout from meter at any exit + Meter?.Logout(); + + _stateMachine?.Stop(); + _stateMachine = null; + _dataContainer = null; + } + + /// + /// Common for test failed + /// + /// error message to dispatch and log + /// + private StatusReturn SetTestFailed(String errorMsg) + { + EolProgress.FinalRadioSystemStateCheck = EOLStatus.FAIL; + ErrorMsgDispatcher(errorMsg); + PrepareExit(); + return StatusReturn.Failed; + } + + /// + /// Common for test succeeded + /// + /// + private StatusReturn SetTestSucceeded() + { + EolProgress.FinalRadioSystemStateCheck = EOLStatus.OK; + PrepareExit(); + return StatusReturn.Okay; + } + + /// + /// Disable all buttons of user control. + /// + /// + /// + /// - Initial + /// + private void DisableAllButtons() + { + //EnableBtn(_checkRadioUserControl.btnSkip, false); + EnableBtn(_checkRadioUserControl.btnRetry, false); + } + + private void EnableBtn(Button btn, Boolean enable) + { + btn.Dispatcher.Invoke(DispatcherPriority.Send, + new Action(() => + { + btn.IsEnabled = enable; + } + )); + } + + private void UpdateUc() + { + _checkRadioUserControl.SetUi(new UcRadioCheck.UiContainer(_rssiPamSirtDbMin, _rssiSemiCordonelDbMin, + _rssiPamSirtMeasuredByCordonel, _rssiSemiCordonelMeasuredBySirt, _rssiPamSirtDbMax, _rssiSemiCordonelDbMax)); + } + + /// + /// Check of radio signal strength. + /// + /// + /// + /// - Initial + /// + private void StartRssiMeasurement() + { + // disable all buttons during ongoing measurement process + DisableAllButtons(); + UpdateUc(); + + try + { + ActualProcessProgress++; + GetRssiMinMaxValuesFromDb(_sirtBoxNr, Meter.OrderNumber); + SendPamListenSemi((Int32)_radioAddressCordonel, _sirtComport, _radioEncryptionKey); + UpdateUc(); + ActualProcessProgress++; + } + catch (Exception ex) + { + _testFinished = true; + Meter?.WriteLog(ex.Message); + LogNewStatusMsg(Resources.StrErrorMsgRadioCheck); + } + } + + /// + /// Clear RSSI values for display of unknown values but leave limits active as + /// those are already loaded from DB and will not change during retry. Therefore, + /// the process can be optimized to avoid recurrent DB access! + /// + /// + /// + /// - Initial + /// + private void ClearRssiMeasurement() + { + _sirtComOkay = false; + _rssiPamSirtMeasuredByCordonel = null; + _rssiSemiCordonelMeasuredBySirt = null; + } + + /// + /// Retry collection of RSSI values (entire process with sending PAM and receiving SEMI). + /// + /// + /// + /// - Initial + /// + private void Retry() + { + // reset process counter to start from scratch + ActualProcessProgress = 0; + if (_stateMachine != null) + { + _stateMachine.ClearPam(); + _stateMachine.Stop(); + } + ClearRssiMeasurement(); + UpdateUc(); + SendPamListenSemi((Int32)_radioAddressCordonel, _sirtComport, _radioEncryptionKey); + } + + /// + /// ATTENTION: + /// As those definitions are directly used as DB definitions for pushing the result: + /// + /// LEAVE THOSE NAMING AS DEFINED HERE! + /// + /// + private class DataContainer + { + public Int32 RadioAdress + { + get; set; + } + public Byte RSSI_PAM + { + get; set; + } + public Byte RSSI_SEMI + { + get; set; + } + public Int32 LimitId + { + get; set; + } + public Byte Result + { + get; set; + } + } + + + /// + /// Save RSSI values to DB. + /// + /// + /// + /// - Initial + /// + private void SaveRssiLevelsToDb() + { + try + { + if (_dataContainer != null) + { + _dataContainer.RadioAdress = (Int32)_radioAddressCordonel; // 0-4294967295 + var url = $"{ServiceUrls.GenesisRadioCheckServiceUrl()}SaveRSSI"; + + var response = LocalWebRequest.PostRequestAsync(url, 30000, out var httpStatus, _dataContainer); + if (!string.IsNullOrEmpty(response) && httpStatus == HttpStatusCode.OK) + { + var jsonObj = JObject.Parse(response); + _radioResultCheckId = Convert.ToInt32(jsonObj.SelectToken("radiocheckid")); + } + + SuccessMsgDispatcher(Resources.StrSuccessMsgSetRadioRssiLimitsToDb); + ActualProcessProgress++; + } + else + { + ErrorMsgDispatcher(Resources.StrErrorMsgSetRadioRssiLimitsToDb); + } + } + catch (Exception ex) + { + Meter?.WriteLog(ex.Message); + ErrorMsgDispatcher(Resources.StrErrorMsgSetRadioRssiLimitsToDb); + } + finally + { + _testFinished = true; + } + } + + /// + /// Get RSSI values from DB. + /// + /// + /// + /// - Initial + /// + private void GetRssiMinMaxValuesFromDb(Byte BoxNr, Int32 productionOrderNo) + { + try + { + if (_dataContainer != null) + { + var url = $"{ServiceUrls.GenesisRadioCheckServiceUrl()}GetLimits/{BoxNr}/{productionOrderNo}"; + var body = LocalWebRequest.GetRequest(url, 300000, out var httpStatus); + if (httpStatus != HttpStatusCode.OK || string.IsNullOrEmpty(body)) + { + ErrorMsgDispatcher(Resources.StrErrorMsgGetRadioRssiLimitsFromDb); + return; + } + var o = JObject.Parse(body); + + _dataContainer.LimitId = Convert.ToInt32(o.SelectToken("LimitId")?.ToString()); + _rssiPamSirtDbMin = Convert.ToByte(o.SelectToken("RSSI_PAM_Min")?.ToString()); + _rssiPamSirtDbMax = Convert.ToByte(o.SelectToken("RSSI_PAM_Max")?.ToString()); + _rssiSemiCordonelDbMin = Convert.ToByte(o.SelectToken("RSSI_SEMI_Min")?.ToString()); + _rssiSemiCordonelDbMax = Convert.ToByte(o.SelectToken("RSSI_SEMI_Max")?.ToString()); + SuccessMsgDispatcher(Resources.StrSuccessMsgGetRadioRssiLimitsFromDb + + $": ID: ({_dataContainer.LimitId}), " + + $"SIRT Min: ({_rssiPamSirtDbMin}), " + + $"SIRT Max: ({_rssiPamSirtDbMax}), " + + $"Cordonel Min: ({_rssiSemiCordonelDbMin}), " + + $"Cordonel Max: ({_rssiSemiCordonelDbMax})"); + ActualProcessProgress++; + } + } + catch (Exception ex) + { + Meter?.WriteLog(ex.Message); + ErrorMsgDispatcher(Resources.StrErrorMsgGetRadioRssiLimitsFromDb); + } + } + + /// + /// Send PAM to force a BUP and a SEMI and start the RSSI observer to measure the signal strength. + /// + /// + /// + /// - Initial + /// + /// + /// - Optimized based on new documentations. + /// + private void SendPamListenSemi(Int32 radioAddress, Int32 byteComPort, String strKey) + { + if (_dataContainer == null || _stateMachine == null) + return; + + _rssiPamSirtMeasuredByCordonel = null; + _rssiSemiCordonelMeasuredBySirt = null; + + Byte P81_1 = 0; + + _dataContainer.RadioAdress = radioAddress; + + _stateMachine.initialise((Byte)byteComPort); + var date = DateTime.Now.Date; + var loggingPath = $"C:\\GenesisLog\\{date:yyyy-MM-dd}\\ProductionUiCordonel\\{Meter.PcbId}_RadioCheck.log"; + _stateMachine.StartLoggin(loggingPath); + + LogNewStatusMsg($"{Resources.StrRadioMsgSendPam}: {radioAddress.ToString()}"); + + // Initialize the state machine of the SIRT dll + _stateMachine.Stop(); + _stateMachine.ClearPam(); + _stateMachine.Timeout_s = 30; + + // Dummy PAM 00 with radio encryption key, SIRT dll takes the key for the response and the + // address for the request and response, CommandHex 00 is data size of 0, so it makes no sense + // to encrypt the 00 PAM, but the key will be used in the SEMI response + _stateMachine.SendPAM(radioAddress.ToString(), "00", "", P81_1, 1, 0, strKey, "0", true); + ActualProcessProgress++; + + // SystemState 0x55 will force meter immediately to send BUP, internal the system state remains + // on 0x02. This has to be sent after PAM! + Meter.WriteRegister("SENSUSRADIO_SystemState", 0x55); + + // Start polling + _stateMachine.OnStateChanged += StateMachineOnStateChange_Handler; + _stateMachine.Start(); + + ActualProcessProgress++; + } + + /// + /// Get RSSI (Received Signal Strength Indicator) values from SIRT (Sensus Interface Radio Tool). + /// Set the + /// + /// + /// + /// - Initial + /// + /// + /// - Initial + /// + private void GotRssiCallback(Byte rssiSemiCordonel, Byte rssiPamSirt) + { + if (_dataContainer == null) + return; + + // RSSI PAM sent by SIRT and measured by Cordonel + LogNewStatusMsg($"{Resources.StrMeasuredMsgRadioRssiSirt}: ({rssiPamSirt})"); + // RSSI SEMI sent by Cordonel and measured by SIRT + LogNewStatusMsg($"{Resources.StrMeasuredMsgRadioRssiCordonel}: ({rssiSemiCordonel})"); + + _rssiPamSirtMeasuredByCordonel = rssiPamSirt; + _rssiSemiCordonelMeasuredBySirt = rssiSemiCordonel; + _dataContainer.RSSI_PAM = rssiPamSirt; + _dataContainer.RSSI_SEMI = rssiSemiCordonel; + + UpdateUc(); + + // threshold check for failed test + if (_rssiPamSirtMeasuredByCordonel < _rssiPamSirtDbMin || + _rssiPamSirtMeasuredByCordonel > _rssiPamSirtDbMax || + _rssiSemiCordonelMeasuredBySirt < _rssiSemiCordonelDbMin || + _rssiSemiCordonelMeasuredBySirt > _rssiSemiCordonelDbMax) + { + _dataContainer.Result = 0; + // done can finish the test which is equal to abort on error + //EnableBtn(_checkRadioUserControl.btnSkip, true); + // reset can repeat the test as often as wanted + EnableBtn(_checkRadioUserControl.btnRetry, true); + _successSignalLevel = false; + } + else + { + // on success there is no choice to made, it is finished + _dataContainer.Result = 1; + _successSignalLevel = true; + + // safe those results to DB for logging + SaveRssiLevelsToDb(); + } + } + + /// + /// State machine state change handler. + /// + /// + /// + /// - Initial + /// + private void StateMachineOnStateChange_Handler(Object sender, MyEventArgs e) + { + if (_stateMachine == null) + return; + try + { + // Reading back sent PAM from SIRT to Cordonel + var pam = _stateMachine.GetPAM(1); + + if (pam != null) + { + if (pam.State == PAM.PAMStates.Timeout) + { + LogNewStatusMsg($"Timeout {pam.GetTime()} s"); + } + + if (pam.CountSemi > 0) + { + // SEMI received from Cordonel + _stateMachine.Stop(); + _stateMachine.ClearPam(); + + var hexBytesSemi = pam.Semi.HexData.Replace(" ", ""); + var semiBytes = new Byte[hexBytesSemi.Length / 2]; + + for (var i = 0; i < hexBytesSemi.Length; i += 2) + semiBytes[i / 2] = Convert.ToByte(hexBytesSemi.Substring(i, 2), 16); + + // SEMI Status okay if 0, if SIRT dll returns unequal to 0 the encryption key is wrong + // If 0 the encryption key is correct + if (semiBytes[19] == 0) + { + // RSSI SEMI sent by Cordonel and measured by SIRT (signal level of Cordonel in byte 2) + var rssiSemiMeasuredBySirt = semiBytes[2]; + + // RSSI PAM sent by SIRT and measured by Cordonel (signal level of SIRT packet to SEMI in byte 20) + var rssiPamMeasuredByCordonel = semiBytes[20]; + + GotRssiCallback(rssiSemiMeasuredBySirt, rssiPamMeasuredByCordonel); + _sirtComOkay = true; + } + else + { + // SEMI Status not 0 is internal error which should be logged + LogNewStatusMsg(pam.Semi.GetPamErrorMessage()); + _sirtComOkay = false; + } + } + else + { + LogNewStatusMsg($"BUPS: ({pam.CountBup})"); + _sirtComOkay = false; + } + }//pam not null + else + { + _stateMachine.Stop(); + _stateMachine.ClearPam(); + } + } + catch (Exception ex) + { + Meter?.WriteLog(ex.Message); + } + } + } +} diff --git a/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs b/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs index 40b7fb09..ad66190a 100644 --- a/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs +++ b/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs @@ -453,11 +453,14 @@ namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses /// /// - Handle genesis meter is null as new meter. /// + /// + /// - Check _genesisMeter not null before equality with old pcbid. + /// private Boolean CheckForNewMeter() { if (_genesisMeter == null || (!string.IsNullOrEmpty(_lastPcbId) && - !_genesisMeter.PcbId.Equals(_lastPcbId))) + _genesisMeter?.PcbId != null && !_genesisMeter.PcbId.Equals(_lastPcbId))) { // This loop will be entered after the meter has changed, but not on the initial meter // after program start. diff --git a/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs.bak b/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs.bak new file mode 100644 index 00000000..40b7fb09 --- /dev/null +++ b/Common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs.bak @@ -0,0 +1,857 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using LaaPackages.Features.Cordonel.Models; +using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore; +using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd; +using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisStatus; +using Xylem.Common.Hardware.WaterMeter.WaterMeterCore; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.Enum; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EventArgs; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.GenericProcesses; +using Timer = System.Threading.Timer; + +namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses +{ + /// + /// Implementation for all methods needed to drive the state machine of the process controller + /// + public class ProcessController + { + #region ------------------------------------------ Variables -------------------------------------------------- + // actual genesis meter + private IGenesisMeter _genesisMeter; + // Cordonel requirements for production + private CordonelRequirements _cordonelRequirements; + // EOL progress states for production + private EOLProgressModel _cordonelEolProgress; + // last pcbId for check of changed genesisMeter + private String _lastPcbId; + // batch just used for setup, as only one meter will be assigned + private MeterBatch _meterBatch; + // state of the state machine + private ProcessState _stateMachineState; + // locker to avoid repeated state execution + private ProcessState _lockedProcessState; + // sender of state change event to evaluate in e.g. error process + private IProductionProcess _stateChangeRequestProcess; + // List of production processes + public List Processes + { + get; + private set; + } + + // Container for production process states + private IProcessStateDef _processStateDef; + + // Container for production status information, a list is used to keep the content over all classes of production + // processes as a reference seems not to return their value (always reset to the initial value) + private GenesisStatus _productionStatus; + + // register restorer + private RegisterRestorer _registerRestorer; + + // sensus radio encryption key + private List _radioEncryptionKey; + + // Password container with all information, a list is used to keep the content over all classes of production + // processes as a reference seems not to return their value (always reset to the initial value) + private MeterPwdDb _pwdContainer; + + // reminder for slot + private readonly Int32 _slot; + + /// + /// Change the overview table of the GUI to the state of the individual process + /// + public event EventHandler OnSingleProcessStateChanged; + + /// + /// The overall process chain changed needed to signal to the GUI + /// - Can be forced by the state machine to invoke, + /// - Will be used via the OverallProcessStateChanged_Handler by the individual processes to return the next + /// required sate after break, error or completion to change the state of the state machine. + /// + public event EventHandler OnOverallProcessStateChanged; + + /// + /// Request for logging to GUI output and log-file + /// + public event EventHandler OnProcessLogRequest; + + /// + /// Keep the progress bars of the GUI up to date + /// + public event EventHandler OnProcessProgressChanged; + + // cancellation token + private CancellationTokenSource _cancellationTokenSource; + private CancellationToken _cancellationToken; + + // cyclic timer for state machine call + private Timer _processCtrlTimer; + private const Int32 ProcessTimerCycleMs = 250; + + /// + /// Avoid waiting for a single process completion. + /// + public const IProductionProcess DoNotWaitForSingleProcessCompletion = null; + + /// + /// Kick off a parallel execution of the state machine. + /// + public const Boolean ExecuteInParallel = true; + + #endregion --------------------------------------- Variables -------------------------------------------------- + #region ------------------------------------------ Class ------------------------------------------------------ + + /// + /// Ctor: + /// - Setup process timer for cyclic call of state machine, + /// - Create and setup Cordonel. + /// + /// physical slot for this meter + /// + /// + /// - Initial + /// + /// + /// - Always set to production mode to load the registers for this FW and not the latest, + /// - Always RegisterWatchService on, + /// - Always MinMaxCHeck on. + /// + public ProcessController(Int32 slot, IProcessStateDef processSateDef) + { + _processCtrlTimer = new Timer(TmrProcessCtrlCycle_Elapsed, null, ProcessTimerCycleMs, ProcessTimerCycleMs); + _slot = slot; + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; + _processStateDef = processSateDef; + InitNewMeter(); + InitProcesses(); + // program start with idle process + _stateMachineState = ProcessState.Idle; + } + + /// + /// Initialization of new meter + /// + /// + /// - Initial + /// + /// + /// - Initialize processes to build a list of all production processes. + /// + /// + /// - Removed list of failed production processes + /// + private void InitNewMeter() + { + _genesisMeter = new GenesisMeter(); + _genesisMeter.SetupFromConfigFile(_slot); + // Important setup to overwrite setting from "Setup" in GTB + _genesisMeter.Configuration.ProductionMode = true; + _genesisMeter.Configuration.UseRegisterWatchService = true; + _genesisMeter.Configuration.UseMinMaxCheck = true; + // Meter batch even if only one meter in use as the meter batch routines can be used + _meterBatch = new MeterBatch(); + _meterBatch.AddMeter(_genesisMeter); + // Set lock process to unreachable state delimiter to force initial entry of state machine + _lockedProcessState = ProcessState.StateListDelimiter; + // Create a register restorer + _registerRestorer = new RegisterRestorer(_genesisMeter); + _radioEncryptionKey = new List(); + // Containers for production status and standard or special requirement for production + _productionStatus = new GenesisStatus(); + _cordonelRequirements = new CordonelRequirements(); + // Password container as reminder for repeated test on identical device + _pwdContainer = new MeterPwdDb(); + // EOL progress to feed the sentinel + _cordonelEolProgress = new EOLProgressModel(); + } + + /// + /// Removal of old meter + /// + private void RemoveMeter() + { + _genesisMeter?.ClearPassword(); + // meter batch dispose disposes all IMeters + _meterBatch?.Dispose(); + _meterBatch = null; + _genesisMeter = null; + _registerRestorer = null; + _radioEncryptionKey = null; + _productionStatus = null; + _cordonelRequirements = null; + _pwdContainer = null; + _cordonelEolProgress = null; + } + + /// + /// Dispose. + /// + /// + /// - Initial + /// + public void Dispose() + { + _processCtrlTimer?.Dispose(); + _processCtrlTimer = null; + _processStateDef = null; + KillProcesses(); + RemoveMeter(); + } + + #endregion --------------------------------------- Class ------------------------------------------------------ + #region ------------------------------------------ Events ----------------------------------------------------- + /// + /// Single process state or process list changed event, + /// Use event to force special procedure like uninstalling events + /// + /// + /// + /// + /// - Initial + /// + /// + /// - Remove process events if process is finished. + /// + /// + /// - Remove process events if process is finished or skipped. + /// + private void SingleProcessStateChanged_Handler(Object sender, SingleProcessStateArgs e) + { + IProductionProcess pp = null; + if (sender is IProductionProcess process) + { + pp = process; + } + // uninstall events + switch (e.SingleProcessState) + { + // after execution independent of the result, the events coming from the process + // are needed to be removed + case SingleProcessState.Succeeded: + case SingleProcessState.Skipped: + case SingleProcessState.Abort: + case SingleProcessState.Error: + RemoveProcessEvents(pp); + break; + + case SingleProcessState.Retry: + case SingleProcessState.Waiting: + case SingleProcessState.Running: + break; + } + // signal state change to GUI + OnSingleProcessStateChanged?.Invoke(sender, e); + } + + private void ProcessProgress_Handler(Object sender, ProcessProgressArgs e) + { + OnProcessProgressChanged?.Invoke(sender, e); + } + + private void ProcessLog_Handler(Object sender, ProcessLogArgs e) + { + OnProcessLogRequest?.Invoke(sender, e); + } + /// + /// Handler to set a new state for the state machine regarding the entire final-process + /// + /// + /// + /// + /// - Initial + /// + private void OverallProcessStateChanged_Handler(Object sender, ProcessStateArgs e) + { + // remind caller of the state change request to act on e.g. error processing + _stateChangeRequestProcess = (IProductionProcess)sender; + + // received new status from external + if (e.State != null) + { + _stateMachineState = (ProcessState)e.State; + } + } + + /// + /// Process timer + /// + /// + /// + /// - Initial + /// + private void TmrProcessCtrlCycle_Elapsed(Object state) + { + StateMachine(); + } + + #endregion --------------------------------------- Events ----------------------------------------------------- + #region ------------------------------------------ State Machine ---------------------------------------------- + + /// + /// State-machine of ProcessController which serves all production processes. + /// This-state machine executes all states defined in the "GenericStateMachineDef" and the individual + /// production-process specific "XxxStateMachineDef". + /// + /// A few states with special functions will be caught in advance in this StateMachine + /// + /// + /// - Initial + /// + /// + /// - Generic implementation + /// + private void StateMachine() + {// avoid repeated execution of the state machine on unchanged state + if (_stateMachineState == _lockedProcessState) + { + Thread.Sleep(ProcessTimerCycleMs >> 1); + } + else + { + try + { + // remind backup state to avoid repeated execution and side effects + _lockedProcessState = _stateMachineState; + + // signal overall process progress to GUI based on ready processes + CalcAndPushOverAllProgress(); + + // signal state change to logger + OnOverallProcessStateChanged?.Invoke(this, new ProcessStateArgs(_stateMachineState)); + + // get the processStateStruct of the actual set stateMachineSTate + var processStateStruct = _processStateDef.GetProcessStateStructOfState(_stateMachineState); + + switch (_stateMachineState) + { + case ProcessState.Idle: + // This state needs an external state change like user input to go ahead + break; + + case ProcessState.DetectCordonel: + // Wait for cancellation + if (_cancellationToken.IsCancellationRequested) + { + Thread.Sleep(500); + // Reset the cancellation request + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; + } + + // Set up the counter for maximum overall process progress bar in GUI + CalcAndPushMaxOverAllProcessSteps(); + + // Detection of Cordonel has to be completed before assigning next state, so leave to automatic + // to change the state after successfully execution. The detection is the base for all others. + StartProcess(processStateStruct); + break; + + case ProcessState.CheckNewMeter: + // Detection of Cordonel has to be completed before assigning next state. It has to be checked if + // the meter changed to clear all collected contents of previous run. To UPDATE ALL INFORMATION after + // new meter assignment the "DetectCordonel" has to be repeated to return here "Old Meter" and then + // enter the "ConnectCordonel"! + _stateMachineState = CheckForNewMeter() ? ProcessState.DetectCordonel : ProcessState.ConnectCordonel; + break; + + + case ProcessState.RepeatFailedTests: + _stateMachineState = RepeatFailedTests(); + break; + + case ProcessState.AbortTest: + AbortProcesses(); + _stateMachineState = ProcessState.Idle; + break; + + case ProcessState.Stop: + KillProcesses(); + _stateMachineState = ProcessState.Idle; + break; + + case ProcessState.Error: + // Common error handling routine + ProcessErrorHandler(_stateChangeRequestProcess); + // Start the error handler with its user feedback request. It has to be started + // always even if the abortion of all processes is required! The error handler + // will change the exit state based on the user input. + StartProcess(processStateStruct); + break; + + case ProcessState.StateListDelimiter: + _stateMachineState = ProcessState.Idle; + break; + + default: + // This is the call to the processes for a specific test + StartProcess(processStateStruct); + if (processStateStruct.KickOffParallelState != null) + { + _stateMachineState = (ProcessState)processStateStruct.KickOffParallelState; + } + break; + } + } + catch (ThreadAbortException ex) + { + _genesisMeter?.WriteLog(ex.Message); + _stateMachineState = ProcessState.Idle; + } + catch (Exception ex) + { + _genesisMeter?.WriteLog(ex.Message); + _stateMachineState = ProcessState.Error; + } + } + }// state locked against repeated execution + + + #endregion --------------------------------------- State Machine ---------------------------------------------- + #region ------------------------------------------ Tools ------------------------------------------------------ + /// + /// Returns the currentGenesis information for display + /// + /// + /// + /// - Initial + /// + public CordonelRequirements GetProductionRequirements() + { + return _cordonelRequirements; + } + /// + /// Returns the currentGenesis information for display + /// + /// + /// + /// - Initial + /// + public IGenesisMeter GetGenesisMeter() + { + return _genesisMeter; + } + /// + /// Clears all collected data on new genesisMeter and reminds the detected for next + /// detection and run of this comparison. + /// + /// true for new meter + /// + /// - Initial + /// + /// + /// - The StartProcessSequence has already removed all processes from list to have a clean start, + /// avoid it here again, because the detect is now in the list! + /// + /// + /// - Handle genesis meter is null as new meter. + /// + private Boolean CheckForNewMeter() + { + if (_genesisMeter == null || + (!string.IsNullOrEmpty(_lastPcbId) && + !_genesisMeter.PcbId.Equals(_lastPcbId))) + { + // This loop will be entered after the meter has changed, but not on the initial meter + // after program start. + KillProcesses(); + RemoveMeter(); + InitNewMeter(); + InitProcesses(); + // Remind actual genesis + _lastPcbId = _genesisMeter?.PcbId; + + // New meter detected + return true; + } + + // Remind actual genesis + _lastPcbId = _genesisMeter?.PcbId; + + // Old meter + return false; + } + + /// + /// Restart final test on first failed or aborted test. + /// + /// + /// - Initial + /// + /// + /// - Removed list of failed production processes and search in Processes + /// + /// + /// - Leave skipped processes as is. + /// + private ProcessState RepeatFailedTests() + { + var state = ProcessState.Idle; + + // Search for first test in processes list which failed or aborted due to final test in + // parallel processing forced abort of active processes + foreach (var p in Processes) + { + if (p.SingleProcessState == SingleProcessState.Error || + p.SingleProcessState == SingleProcessState.Abort || + p.SingleProcessState == SingleProcessState.Retry) + { + // set the state to the first entry to kick off the state machine here + state = _processStateDef.GetStateOfProcess(p); + break; + } + } + // Put all processes which are not successfully completed to init (Waiting for execution) + foreach (var pp in Processes.Where(pp => pp.SingleProcessState != SingleProcessState.Succeeded && + pp.SingleProcessState != SingleProcessState.Skipped)) + { + pp.IdleProcess(); + } + + return state; + } + /// + /// Error handler + /// + /// + /// - Initial + /// + /// + /// - Activated check for sequences in production process for ProductionProcessCheckFinalParametrization + /// to re-start with ProductionProcessExecFinalParametrization. + /// + /// + /// - Removed list of failed production processes + /// + /// + /// - Kick off exec parametrization if check after reboot failed. + /// + private void ProcessErrorHandler(IProductionProcess process) + { + if (process == null) + return; + // Special action if parametrization check failed to repeat it + if (ProcessState.CheckFinalParametrization == _processStateDef.GetStateOfProcess(process)) + { + // Signal state change to retry sequence exec config, store config, reboot, check config + var pp = _processStateDef.GetProcessOfState(ProcessState.ExecFinalParametrization); + pp.RetryProcess(); + pp = _processStateDef.GetProcessOfState(ProcessState.ExecStoreConfiguration); + pp.RetryProcess(); + pp = _processStateDef.GetProcessOfState(ProcessState.RebootCordonel); + pp.RetryProcess(); + pp = _processStateDef.GetProcessOfState(ProcessState.CheckFinalParametrization); + pp.RetryProcess(); + } + + // Error signal needed to abort to all processes + AbortProcesses(); + } + + /// + /// Calculate the maximum of all processes expected to execute and signal to GUI. + /// + private void CalcAndPushMaxOverAllProcessSteps() + { + if (_processStateDef == null) + return; + + var _maxOverallProcessesSteps = _processStateDef.GetNumberOfInitializedProcesses(); + OnProcessProgressChanged?.Invoke(this, new ProcessProgressArgs(_maxOverallProcessesSteps, + ProcessProgressArgs.ProcessProgressType.MaxOverallProcessSteps)); + } + + + /// + /// Calculate all processes which are completed or skipped and signal to GUI. + /// + private void CalcAndPushOverAllProgress() + { + if (Processes == null) + return; + + // check the readiness of all processes in the loop + var _overallReadyProcesses = 0; + foreach (var p in Processes) + { + if (p.SingleProcessState == SingleProcessState.Succeeded || + p.SingleProcessState == SingleProcessState.Skipped) + _overallReadyProcesses++; + } + + OnProcessProgressChanged?.Invoke(this, new ProcessProgressArgs(_overallReadyProcesses, + ProcessProgressArgs.ProcessProgressType.OverallProcessesSteps)); + } + + /// + /// Initialize all processes which are connected to a production process. + /// + /// + /// - Initial + /// + private void InitProcesses() + { + if (Processes == null) + Processes = new List(); + + // get all processes relevant for EOL from table which has to be sorted + var processes = _processStateDef.GetAllEolRelevantProcesses(); + + foreach (var process in processes) + { + // process is standby for execution after idle, this overrides the eventually abort state + process.IdleProcess(); + + // add process to list to feed the process window in main screen + if (Processes.All(pp => pp.ProcessName != process.ProcessName)) + { + Processes.Add(process); + } + } + } + + /// + /// Production process kick off: + /// - Sets single process state to waiting within the process init, + /// - Adds new process to process list, + /// - Optional waits for single process to complete before process is going to be started, + /// - Optional waits for all previously assigned and started processes to complete before process is going to + /// be started, + /// - Assigns slot, productionStatus, programmingParameters and genesisMeter to process, + /// - Adds event handlers for process information, + /// - Starts process. + /// + /// + /// actual state + /// + /// - Initial + /// + /// + /// - Skipped process due to one of the previous processes failed + /// + /// + /// - Restructured to avoid process start if processes to wait for are not completed + /// + /// + /// - Changed from programming parameters to register restorer + /// + /// + /// - Changed setup process to start process being able to create a process list in advance. + /// + /// + /// - Changed setup process to start process being able to create a process list in advance. + /// + /// + /// - Feed with processStateStruct. + /// + protected void StartProcess(ProcessStateStruct processStateStruct) + { + // deny access if uninitialized + if (Processes == null || processStateStruct.ProductionProcess == null) + return; + + // do not start a new process if any error or abort request is signaled + if (AnyProcessRequiresAbortion() && !processStateStruct.StartAlways ) + { + return; + } + + // process is waiting for execution after init, this overrides the eventually abort state + processStateStruct.ProductionProcess.InitProcess(); + + // wait for a single process to complete before start of new depending on process + if (processStateStruct.WaitForSingleProcess != null) + { + // each process will have its own timer until a timeout will change the state + while (Processes.Any(pp => !AnyProcessRequiresAbortion() + && _processStateDef.GetStateOfProcess(pp) == processStateStruct.WaitForSingleProcess + && WaitForProcess(pp))) + { + Thread.Sleep(50); + } + } + + // wait for all initialized processes to complete before start the new process + if (processStateStruct.WaitForAllProcesses) + { + // each process will have its own timer until a timeout will change the state, + // avoid checking the new process, which is not started due to this waiting loop + while (Processes.Any(pp => pp.ProcessName != processStateStruct.ProductionProcess.ProcessName + && WaitForProcess(pp) + && !AnyProcessRequiresAbortion())) + { + Thread.Sleep(50); + } + } + + // if any process exits meanwhile with an error or abort request avoid a start of this process + if (( !processStateStruct.StartAlways && AnyProcessRequiresAbortion()) + || processStateStruct.ProductionProcess.SingleProcessState == SingleProcessState.Idle + || processStateStruct.ProductionProcess.SingleProcessState == SingleProcessState.Abort) + { + processStateStruct.ProductionProcess.AbortProcess(); + return; + } + + // References to commonly used production process objects + processStateStruct.ProductionProcess.Meter = _genesisMeter; + processStateStruct.ProductionProcess.RegisterRestorer = _registerRestorer; + processStateStruct.ProductionProcess.RadioEncryptionKey = _radioEncryptionKey; + processStateStruct.ProductionProcess.PwdContainer = _pwdContainer; + processStateStruct.ProductionProcess.ProductionStatus = _productionStatus; + processStateStruct.ProductionProcess.ProductionRequirements = _cordonelRequirements; + processStateStruct.ProductionProcess.EolProgress = _cordonelEolProgress; + processStateStruct.ProductionProcess.CancellationToken = _cancellationToken; + + // kick off process execution, the breakExitState will not be used + processStateStruct.ProductionProcess.StartProcess(processStateStruct.NextStateOnSuccess, + processStateStruct.ErrorExitState, processStateStruct.BreakExitState); + // connect process events to actual process + AddProcessEvents(processStateStruct.ProductionProcess); + } + + /// + /// Check all processes for error or abort request + /// + /// true if abort required + /// + /// - Cancellation token. + /// + private Boolean AnyProcessRequiresAbortion() + { + return Processes.Any(pp => pp.SingleProcessState == SingleProcessState.Error || + pp.SingleProcessState == SingleProcessState.Abort || + _cancellationToken.IsCancellationRequested); + } + /// + /// Check process if in status waiting or running (incomplete) + /// + /// production process + /// true if process to wait for execution or exit running state + private Boolean WaitForProcess(IProductionProcess pp) + { + return pp.SingleProcessState == SingleProcessState.Waiting || + pp.SingleProcessState == SingleProcessState.Running; + } + /// + /// Install process events + /// + /// + private void AddProcessEvents(IProductionProcess process) + { + if (process == null) + return; + + process.OnProcessProgressChanged += ProcessProgress_Handler; + process.OnProcessLogRequest += ProcessLog_Handler; + process.OnSingleProcessStateChanged += SingleProcessStateChanged_Handler; + process.OnOverallProcessStateChanged += OverallProcessStateChanged_Handler; + } + + /// + /// Remove process events + /// + /// + private void RemoveProcessEvents(IProductionProcess process) + { + if (process == null) + return; + + process.OnProcessProgressChanged -= ProcessProgress_Handler; + process.OnProcessLogRequest -= ProcessLog_Handler; + process.OnSingleProcessStateChanged -= SingleProcessStateChanged_Handler; + process.OnOverallProcessStateChanged -= OverallProcessStateChanged_Handler; + } + + /// + /// Get the actual process state of the state machine + /// + /// actual process state + /// + /// - Initial + /// + private ProcessState GetProcessState() + { + return _stateMachineState; + } + + /// + /// Set the new process state of the state machine + /// + /// true, if process state is set + /// + /// - Initial + /// + public Boolean SetProcessState(ProcessState newProcessState) + { + if (newProcessState != _stateMachineState) + { + _stateMachineState = newProcessState; + return true; + } + + return false; + } + + /// + /// Remove all processes from the list: + /// - Signal change to GUI, + /// - Remove process events all, + /// - Set state to abort. + /// + /// + /// - Initial + /// + /// + /// - Abort register access. + /// + /// + /// - Cancellation token source. + /// + private void KillProcesses() + { + _cancellationTokenSource?.Cancel(); + Thread.Sleep(1000); + + if (Processes == null || Processes.Count == 0) + return; + + // put all processes to idle and remove events of these processes + foreach (var pp in Processes) + { + pp.IdleProcess(); + } + } + + /// + /// Abort all processes if one previous process failed, but leave the list intact. + /// Leave all events for those which are already running intact to force a clean stop. + /// + /// + /// - Initial + /// + private void AbortProcesses() + { + if (Processes == null || Processes.Count == 0) + return; + + // Skip all processes which are not succeeded, erroneous or skipped to leave the status intact + // and request abort for all others + foreach (var pp in Processes.Where(pp => pp.SingleProcessState != SingleProcessState.Succeeded && + pp.SingleProcessState != SingleProcessState.Error && + pp.SingleProcessState != SingleProcessState.Skipped && + pp.SingleProcessState != SingleProcessState.Retry)) + { + pp.AbortProcess(); + } + } + #endregion --------------------------------------- Tools ------------------------------------------------------ + } +} diff --git a/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs b/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs index 81001603..774a2546 100644 --- a/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs +++ b/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs @@ -803,6 +803,18 @@ namespace Xylem.Common.Production.ProductionUiCordonel // remove last userControl dpSubProcess.Children.Clear(); rtbLog.Document.Blocks.Clear(); + + // clear all visual fields as it may be a new pcbId + UpdateContentControl(lblOrderNumberValue, "?"); + UpdateContentControl(lblRadioAddressValue, ""); + UpdateContentControl(lblRadioAddressText, ""); + UpdateContentControl(lblFwValue, "?"); + UpdateContentControl(lblMeterSizeValue, "?"); + UpdateContentControl(lblLutValue, "?"); + UpdateContentControl(lblSerialNumberValue, "?"); + UpdateContentControl(lblRegionValue, "?"); + UpdateUi(lblPcbIdValue, "?"); + _processCtrl?.SetProcessState(ProcessState.DetectCordonel); } @@ -883,7 +895,9 @@ namespace Xylem.Common.Production.ProductionUiCordonel // Returns the compressed file for error report printout File.WriteAllText(fileName, compressedSb.ToString()); if (printReport) + { Process.Start(fileName); + } } } diff --git a/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs.bak b/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs.bak new file mode 100644 index 00000000..81001603 --- /dev/null +++ b/Common/Production/ProductionUiCordonel/ProductionWindow.xaml.cs.bak @@ -0,0 +1,963 @@ +using System; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Threading; +using LaaPackages.Features.Cordonel.Models; +using Newtonsoft.Json; +using NLog; +using Xylem.Common.CommonCore.Configuration; +using Xylem.Common.CommonCore.Consts; +using Xylem.Common.Hardware.Interfaces.Ports.PortCore; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.Enum; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EolProcesses; +using Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EventArgs; +using Xylem.Common.Utils.Logging; +using Xylem.Common.Utils.UserAccessCtrl; +using Control = System.Windows.Forms.Control; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; + +namespace Xylem.Common.Production.ProductionUiCordonel +{ + /// + /// Interaction logic for FinalTest.xaml + /// + public sealed partial class ProductionWindow + { + #region variables and properties + private readonly ILogger _logger; + // remind manually changed culture setting + private static readonly CultureInfo _cultureInfo = Thread.CurrentThread.CurrentCulture; + private static readonly AssemblyName _assemblyName = Assembly.GetExecutingAssembly().GetName(); + private static readonly String _assemblyExecRootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + private static readonly Version _version = _assemblyName.Version; + private readonly String _strSoftwareNameVersion; + private RegisterLDAPUser _registerForm; + + private Int32 Slot + { + get; set; + } + + // maximum processing steps for calculation of progress bar + private Int32 _maxOverallProcessSteps; + private Int32 _maxSingleProcessSteps; + private ProcessController _processCtrl; + private DateTimeOffset _startTime; + private DispatcherTimer _timer; + private String _lastLoggingTextToAvoidRepetition; + private CordonelRequirements _productionRequirements; + private UserAccessWindow _userAccessWindow; + private IProcessStateDef _processStateDef; + + #endregion + + public ProductionWindow(String softwareName, String projectName, IProcessStateDef processStateDef) + { + _logger = NLogHelper.CreateOrGetLogger(projectName); + _processStateDef = processStateDef; + String msg; + try + { + // returns true in DEBUG configuration + if (!Logic.SoftwareAccessHelper.Access.HasAccess(_assemblyName)) + { + msg = $"{Properties.Resources.StrStartMessageSwLicenseExpired} {_version}"; + _logger.Error(msg); + MessageBox.Show(msg, Properties.Resources.StrProcessStateError, + MessageBoxButton.OK, MessageBoxImage.Error); + Close(); + return; + } + } + catch (Exception e) + { + msg = Properties.Resources.StrErrorMsgMissingNetworkConnetction + " " + e.Message; + _logger.Error(msg); + MessageBox.Show(msg, + Properties.Resources.StrProcessStateError, + MessageBoxButton.OK, MessageBoxImage.Error); + Close(); + return; + } + + InitializeComponent(); + // Log version to files for debug purposes + _strSoftwareNameVersion = $"{softwareName} {_version}"; + _logger.Info(_strSoftwareNameVersion); + + Title = _strSoftwareNameVersion; + + //static labels + UpdateContentControl(lblSerialNumberTxt, Properties.Resources.StrLblSerialNumberTxt + ":"); + UpdateContentControl(lblRadioAddressText, Properties.Resources.StrLblRadioAddressTxt + ":"); + UpdateContentControl(lblOrderNumberText, Properties.Resources.StrLblOrderNumberTxt + ":"); + UpdateContentControl(lblMeterSizeText, Properties.Resources.StrLblMeterSizeTxt + ":"); + UpdateContentControl(lblSingleProcessText, Properties.Resources.StrLblSingleProcessText + ":"); + UpdateContentControl(lblTotalProcessText, Properties.Resources.StrLblTotalProcessText + ":"); + optionsMenu.Header = Properties.Resources.StrMenuOptionsText; + optionsMenuChangePassword.Header = Properties.Resources.StrMenuChangePasswordText; + hlpMenu.Header = Properties.Resources.StrMenuHelpText; + UpdateUi(lblApprovalText, ""); + UpdateUi(lblTimeText, Properties.Resources.StrLblTimeText + ":"); + + // buttons + UpdateContentControl(btnStart, Properties.Resources.StrBtnStart); + UpdateContentControl(btnStop, Properties.Resources.StrBtnStop); + + InitializeFormState(); + + } + + private ApplicationSettings Settings + { + get; set; + } + + /// + /// Initialize the FinalTest GUI: + /// - Read the configuration file for slot and port assignments and settings, + /// - Dispatch slots to slot combobox, + /// - Read setting from "ProductionUiConfig.json" file for GUI and slot setup, + /// - Create final process controller and install events, + /// - Create a dispatch timer to display the elapsed time of the process. + /// + /// + /// + private void Window_Initialized(Object sender, EventArgs e) + { + var configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + nameof(Hardware.WaterMeter.Genesis), ProgramConfig.SerialConfigFileName); + + if (!File.Exists(configFile)) + { + var msg = Properties.Resources.StrErrorMsgMissingConfigurationFile; + MessageBox.Show(msg, + Properties.Resources.StrProcessStateError, + MessageBoxButton.OK, MessageBoxImage.Error); + _logger.Error(msg); + throw new ApplicationException(msg); + } + var tr = new StreamReader(configFile); + var meterConfigList = JsonConvert.DeserializeObject(tr.ReadToEnd()); + + cbxSlotBox.Items.Clear(); + if (meterConfigList != null) + { + foreach (var item in meterConfigList.OrderBy(s => s.Slot)) + { + cbxSlotBox.Items.Add(item.Slot); + } + } + + cbxSlotBox.SelectedIndex = 0; + Settings = new ApplicationSettings("ProductionUiConfig.json"); + + UseSetting(true); + + if (cbxSlotBox.SelectedItem == null || string.IsNullOrEmpty(cbxSlotBox.SelectedItem.ToString()) || + !int.TryParse(cbxSlotBox.SelectedItem.ToString(), out var slot)) + { + Slot = 1; + } + else + { + Slot = slot; + } + + try + { + _processCtrl = new ProcessController(Slot, _processStateDef); + } + catch (Exception ex) + { + _logger.Error(ex.Message); + MessageBox.Show(ex.Message, + Properties.Resources.StrProcessStateError, + MessageBoxButton.OK, MessageBoxImage.Error); + throw new ApplicationException(ex.Message); + } + + //install the process changed event handler for single processes + _processCtrl.OnSingleProcessStateChanged += SingleProcessStateChanged_Handler; + _processCtrl.OnProcessLogRequest += ProcessLog_Handler; + _processCtrl.OnProcessProgressChanged += ProcessProgress_Handler; + _processCtrl.OnOverallProcessStateChanged += ProcessStateChange_Handler; + + _timer = new DispatcherTimer(); + _timer.Tick += TmrProgressUpdate_Tick; + _timer.Interval = TimeSpan.FromMilliseconds(500); + + UpdateStatusGrid(); + } + + private void Window_Closing(Object sender, CancelEventArgs e) + { + _userAccessWindow?.Close(); + _processCtrl.OnSingleProcessStateChanged -= SingleProcessStateChanged_Handler; + _processCtrl.OnProcessLogRequest -= ProcessLog_Handler; + _processCtrl.OnProcessProgressChanged -= ProcessProgress_Handler; + _processCtrl.OnOverallProcessStateChanged -= ProcessStateChange_Handler; + _userAccessWindow = null; + + _processCtrl.Dispose(); + _processCtrl = null; + _timer.Stop(); + _timer.Tick -= TmrProgressUpdate_Tick; + _timer = null; + _registerForm = null; + } + private void UseSetting(Boolean setSlot = false) + { + //tblkAutodetectState.Text = "Off"; + //if (Settings.AutoDetect) + //{ + // tblkAutodetectState.Text = "On"; + //} + if (setSlot) + { + cbxSlotBox.SelectedValue = Settings.Slot; + } + } + + private void InitializeFormState() + { + if (Software.IsRegistrationPending) + { + _registerForm = new RegisterLDAPUser(); + _registerForm.OnCancellation += OnUserAccessControlCancellation; + UiElmEnable(optionsMenu, false, false); + } + else + { + UiElmEnable(optionsMenu, true); + UiElmEnable(optionsMenuChangePassword, Software.IsAutenticated); + UiElmEnable(optionsMenuLogout, Software.IsAutenticated); + UiElmEnable(optionsMenuLogin, !Software.IsAutenticated); + UiElmEnable(btnStart, Software.IsAutenticated); + UiElmEnable(btnStop, false); + UpdateContentControl(lblOperator, + Software.IsAutenticated + ? $"{Properties.Resources.StrLblOperatorNameText}: {Software.UserName}" + : $"{Properties.Resources.StrLblOperatorNameText}: ?"); + } + } + + /// + /// Shows the user access window with login form or password change form. + /// + /// + /// Windows forms control + /// + /// + /// - Initial. + /// + private void ShowUserAccessWindow(Boolean visible = true, Control control = null, String headerTxt = "") + { + if (visible && control != null) + { + UiElmEnable(optionsMenu, false); + + if (_userAccessWindow == null) + _userAccessWindow = new UserAccessWindow(control, headerTxt); + // View the user access window as container for the windows forms password change and login + _userAccessWindow.OnCloseWindow += OnUserAccessWindowClosed_Handler; + _userAccessWindow.ShowDialog(); + } + else + { + if (_userAccessWindow != null) + { + _userAccessWindow.OnCloseWindow -= OnUserAccessWindowClosed_Handler; + try + { + _userAccessWindow.Close(); + } + catch (Exception) + { + //ignore + } + _userAccessWindow = null; + } + + if (_registerForm != null) + _registerForm.OnCancellation -= OnUserAccessControlCancellation; + + UiElmEnable(optionsMenu, true); + + } + } + + /// + /// Close event handler of the user access window with login form or password change form. + /// + /// + /// + /// + /// - Initial. + /// + private void OnUserAccessWindowClosed_Handler(Object sender, EventArgs eventArgs) + { + ShowUserAccessWindow(false); + InitializeFormState(); + } + + private void UpdateUi(ContentControl c, String content, Brush color = null) + { + if (color == null) + color = Brushes.Black; + + c.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + c.Content = content; + c.Foreground = color; + })); + } + + private void UpdateUi(TextBox c, String content, Brush color = null) + { + if (color == null) + color = Brushes.Black; + + c.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + c.Text = content; + c.Foreground = color; + })); + } + private void UpdateUi(TextBlock c, String content, Brush color = null) + { + if (color == null) + color = Brushes.Black; + + c.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + c.Text = content; + c.Foreground = color; + })); + } + + /// + /// Update the elapsed time as information + /// + /// information attached to process depending on region + /// + /// - Initial + /// + private void SetTimeDisplay() + { + var time = DateTimeOffset.UtcNow; + var timeSpan = time - _startTime; + UpdateContentControl(lblTimeValue, $@"{(UInt32)timeSpan.TotalMinutes}:{timeSpan.Seconds:00}"); + } + + /// + /// Common routine for state change: + /// - Log state change, + /// - Execute stop procedure on error. + /// + /// + /// + /// + /// - Initial + /// + /// + /// - Extended. + /// + /// + /// - Extended. + /// + private void ProcessStateChange_Handler(Object sender, ProcessStateArgs e) + { + _logger.Debug($"Slot:{Slot} - Final test state: {e.State}"); + + // Nothing to display + if (e.State == ProcessState.Idle) + { + // Update progress bars + ProcessProgress_Handler(this, new ProcessProgressArgs(0, + ProcessProgressArgs.ProcessProgressType.OverallProcessesSteps)); + ProcessProgress_Handler(this, new ProcessProgressArgs(0, + ProcessProgressArgs.ProcessProgressType.ActualProcessSteps)); + } + // Stop all procedures + if (e.State == ProcessState.Stop) + { + _timer?.Stop(); + UiElmEnable(btnStart, true); + UiElmEnable(btnStop, false); + // update progress bars + ProcessProgress_Handler(this, new ProcessProgressArgs(0, + ProcessProgressArgs.ProcessProgressType.OverallProcessesSteps)); + ProcessProgress_Handler(this, new ProcessProgressArgs(0, + ProcessProgressArgs.ProcessProgressType.ActualProcessSteps)); + // remove user control + SetNewUserControl(null); + } + // Repeat failed tests + if (e.State == ProcessState.RepeatFailedTests) + { + UiElmEnable(btnStart, false); + UiElmEnable(btnStop, true); + _timer?.Start(); + } + // Stop time for manual operation and visual inspection as this is not part of the configuration + if (e.State == ProcessState.CheckOrderNumber) + { + _timer?.Stop(); + } + // Prepare shipping + if (e.State == ProcessState.PrepareShippingMode) + { + _timer?.Stop(); + UiElmEnable(btnStart, false); + UiElmEnable(btnStop, false); + } + // Ready for shipping + if (e.State == ProcessState.ReadyForShipping) + { + // Create extended report including all steps and each register write, read and comparison + CreateFile(Properties.Resources.StrInfoSuccessPapers); + BtnStop_Click(this, null); + } + // Print return report + if (e.State == ProcessState.PrintReturnReport) + { + PrintErrorNote(); + BtnStop_Click(this, null); + } + // Print success report + if (e.State == ProcessState.PrintSuccessReport) + { + PrintSuccessNote(); + BtnStop_Click(this, null); + } + } + + /// + /// Enable UI elements + /// + /// + /// + /// + private void UiElmEnable(UIElement elm, Boolean isEnabled, Boolean isVisible = true) + { + elm.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + elm.Visibility = isVisible ? Visibility.Visible : Visibility.Hidden; + elm.IsEnabled = isEnabled; + + })); + } + + /// + /// Fill logging window with information + /// + /// + /// + /// + /// - Initial + /// + /// + /// - Changed to rich text edit with colored output. + /// + /// + /// - Avoid output of repeated text or useless empty text. + /// + private void ProcessLog_Handler(Object sender, ProcessLogArgs e) + { + rtbLog.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + if (e != null) + { + try + { + // remove unacceptable spacing between the lines + rtbLog.Document.LineHeight = 1; + + var color = e.Color ?? Brushes.Black; + var text = e.Data; + if (!string.IsNullOrEmpty(text) && !text.Equals(_lastLoggingTextToAvoidRepetition)) + { + var tr = new TextRange(rtbLog.Document.ContentEnd, rtbLog.Document.ContentEnd) + { + Text = text + }; + tr.ApplyPropertyValue(TextElement.ForegroundProperty, color); + rtbLog.ScrollToEnd(); + rtbLog.AppendText(Environment.NewLine); + } + + _lastLoggingTextToAvoidRepetition = text; + } + catch (FormatException) + { + + } + } + } + )); + } + + /// + /// Set values for progress bar, maximum and actual for single process and overall process. + /// - Limit values to 0.0..100.0. + /// + /// + /// + /// + /// - Initial + /// + /// + /// - Introduced multi line text with center alignment. + /// + private void ProcessProgress_Handler(Object sender, ProcessProgressArgs e) + { + if (e.ProgressType == ProcessProgressArgs.ProcessProgressType.MaxOverallProcessSteps) + { + _maxOverallProcessSteps = e.Progress; + } + else if (e.ProgressType == ProcessProgressArgs.ProcessProgressType.MaxActualProcessSteps) + { + _maxSingleProcessSteps = e.Progress; + } + if (e.ProgressType == ProcessProgressArgs.ProcessProgressType.OverallProcessesSteps) + { + pbTotalProgress.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + // limit output to progress bar to 0..100 as integer for process bar + var progress = _maxOverallProcessSteps <= 0 ? 0.0 : e.Progress * 100.0 / _maxOverallProcessSteps; + progress = progress > 100.0 ? 100.0 : progress < 0.0 ? 0.0 : progress; + pbTotalProgress.Value = (Int32)progress; + UpdateContentControl(lblTotalProgressValue, $@"{progress:##0.0} %"); + } + )); + } + else if (e.ProgressType == ProcessProgressArgs.ProcessProgressType.ActualProcessSteps) + { + pbSubProgress.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + // limit output to progress bar to 0..100 as integer for process bar + var progress = _maxSingleProcessSteps <= 0 ? 0.0 : e.Progress * 100.0 / _maxSingleProcessSteps; + progress = progress > 100.0 ? 100.0 : progress < 0.0 ? 0.0 : progress; + pbSubProgress.Value = (Int32)progress; + UpdateContentControl(lblSubProgressValue, $@"{progress:##0.0} %"); + } + )); + } + } + + /// + /// Output message to control without access violation. + /// + /// + /// + /// - Initial + /// + /// + /// - Introduced multi line text with center alignment. + /// + private static void UpdateContentControl(ContentControl ctl, String text) + { + ctl.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + ctl.Content = new TextBlock() + { + Text = text, + TextWrapping = TextWrapping.Wrap, + TextAlignment = TextAlignment.Center + }; + } + )); + } + + /// + /// Status grid update for all single processes + /// + private void UpdateStatusGrid() + { + var dt = new DataTable(); + dt.Columns.Add(Properties.Resources.StrDataTableColumnProcess); + dt.Columns.Add(Properties.Resources.StrDataTableColumnState); + var i = 0; + if (_processCtrl?.Processes == null) + return; + foreach (var item in _processCtrl.Processes) + { + dt.Rows.Add(); + dt.Rows[i][0] = item.ProcessName; + dt.Rows[i][1] = ProductionProcessStateHelper.GetTextFromProductionProcessState(item.SingleProcessState); + i++; + } + + dgTotalProcess.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + dgTotalProcess.DataContext = dt; + } + )); + } + + /// + /// Dispatch new user control to main screen + /// + /// + private void SetNewUserControl(UserControl uc) + { + dgTotalProcess.Dispatcher.Invoke(DispatcherPriority.Normal, + new Action(() => + { + // remove last userControl + dpSubProcess.Children.Clear(); + if (uc != null) + { + dpSubProcess.Children.Add(uc); + DockPanel.SetDock(uc, Dock.Top); // & Dock.Left); + } + } + )); + } + + #region TimerControls + /// + /// Handle timing depending updates. + /// + /// + /// + /// - Initial + /// + private void TmrProgressUpdate_Tick(Object sender, EventArgs e) + { + SetTimeDisplay(); + } + #endregion + + /// + /// Update of status grid with information returned by user control. + /// + /// + /// + /// + /// - Initial + /// + /// + /// - Activate new user control on start of process. + /// + private void SingleProcessStateChanged_Handler(Object sender, SingleProcessStateArgs e) + { + if (sender is IProductionProcess productionProcess) + { + if (e.SingleProcessState == SingleProcessState.Running) + { + // Set new user control + SetNewUserControl(productionProcess.GetUserControl()); + } + else + { + // remove user control on idle, skipped, abort or error + SetNewUserControl(null); + } + } + + UpdateStatusGrid(); + UpdateGuiWithProductInfos(); + } + + private void OnUserAccessControlCancellation(Control control) + { + OnUserAccessWindowClosed_Handler(this, null); + } + + + /// + /// Set labels of device information and order number + /// + /// + /// - Initial + /// + /// + /// - Additional info of Cordonel. + /// + private void UpdateGuiWithProductInfos() + { + var genesisMeter = _processCtrl?.GetGenesisMeter(); + _productionRequirements = _processCtrl?.GetProductionRequirements(); + + if (genesisMeter == null) + return; + + UpdateContentControl(lblOrderNumberValue, + genesisMeter.OrderNumber != 0 ? genesisMeter.OrderNumber.ToString() : "?"); + + UpdateContentControl(lblRadioAddressValue, + genesisMeter.RadioAddress != 0 ? genesisMeter.RadioAddress.ToString() : ""); + UpdateContentControl(lblRadioAddressText, + genesisMeter.RadioAddress != 0 ? Properties.Resources.StrLblRadioAddressTxt + ":" : ""); + + UpdateContentControl(lblFwValue, + !string.IsNullOrEmpty(genesisMeter.FwVersion) ? genesisMeter.FwVersion : "?"); + + UpdateContentControl(lblMeterSizeValue, + !string.IsNullOrEmpty(genesisMeter.MeterSize) ? genesisMeter.MeterSize : "?"); + + UpdateContentControl(lblLutValue, + !string.IsNullOrEmpty(genesisMeter.LutCrc) ? genesisMeter.LutCrc : "?"); + + + UpdateContentControl(lblSerialNumberValue, + !string.IsNullOrEmpty(genesisMeter.CustomerSerialNumber) ? genesisMeter.CustomerSerialNumber : "?"); + + UpdateContentControl(lblRegionValue, + !string.IsNullOrEmpty(genesisMeter.Region) ? genesisMeter.Region : "?"); + + UpdateUi(lblPcbIdValue, !string.IsNullOrEmpty(genesisMeter.PcbId) ? genesisMeter.PcbId : "?"); + + var msg = ""; + var color = Brushes.Black; + if (_productionRequirements?.IsSpecialActive != null && (Boolean)_productionRequirements.IsSpecialActive && + _productionRequirements?.IsSpecialApproved != null && (Boolean)_productionRequirements.IsSpecialApproved) + { + msg = $"{Properties.Resources.StrLblSpecialRequirement} - Id.Ver: {_productionRequirements.SpecialId}." + + $"{_productionRequirements.SpecialVersion}"; + color = Brushes.Red; + } + else if (_productionRequirements?.IsStandardActive != null && (Boolean)_productionRequirements.IsStandardActive && + _productionRequirements?.IsStandardApproved != null && (Boolean)_productionRequirements.IsStandardApproved) + { + msg = $"{Properties.Resources.StrLblStandardRequirement} - Id.Ver: {_productionRequirements.StandardId}." + + $"{_productionRequirements.StandardVersion}"; + } + + UpdateUi(lblApprovalText, msg, color); + } + + //private void BtnSwitchAutoDetect_Click(Object sender, RoutedEventArgs e) + //{ + // //Settings.AutoDetect = !Settings.AutoDetect; + // //StoreSetting(); + // //UseSetting(); + //} + + private void OnLogout_Click(Object sender, EventArgs e) + { + Software.Logout(); + InitializeFormState(); + } + + private void OnLogin_Click(Object sender, EventArgs args) + { + ShowUserAccessWindow(true, new LoginForm(OnUserAccessControlCancellation), + Properties.Resources.StrLoginMsg); + } + + private void OnChangePassword_Click(Object _, EventArgs _1) + { + ShowUserAccessWindow(true, new ChangePassword(OnUserAccessControlCancellation), + Properties.Resources.StrChangePasswordMsg); + } + + private void CbxSlotBox_SelectionChanged(Object sender, SelectionChangedEventArgs e) + { + + } + + private void StoreSetting() + { + Settings.Update("ProductionUiConfig.json"); + } + + //private void BtnConnect_Click(Object sender, RoutedEventArgs e) + //{ + // Settings.Slot = (Int32)cbxSlotBox.SelectedValue; + // StoreSetting(); + //} + + private void BtnStart_Click(Object sender, RoutedEventArgs e) + { + UiElmEnable(btnStart, false); + UiElmEnable(btnStop, true); + + _startTime = DateTimeOffset.UtcNow; + _timer?.Start(); + + // remove last userControl + dpSubProcess.Children.Clear(); + rtbLog.Document.Blocks.Clear(); + _processCtrl?.SetProcessState(ProcessState.DetectCordonel); + } + + private void PrintErrorNote() + { + // Safe report to file + CreateFile(Properties.Resources.StrInstructionReturnPapers, true, true); + } + + private void PrintSuccessNote() + { + // Safe report to file + CreateFile(Properties.Resources.StrInfoSuccessPapers, true, true); + } + + /// + /// Logging a file with all process information, building a compressed report for printout. + /// + /// + /// - Reduced printout. + /// + private void CreateFile(String headerInfo, Boolean shortReport = false, Boolean printReport = false) + { + var sb = new StringBuilder(); + var genesisMeter = _processCtrl.GetGenesisMeter(); + + sb.AppendLine("========================================================================"); + sb.AppendLine(headerInfo); + sb.AppendLine("========================================================================"); + sb.AppendLine(_strSoftwareNameVersion); + sb.AppendLine("------------------------------------------------------------------------"); + sb.AppendLine($"{Properties.Resources.StrDateTime}: {DateTime.Now}"); + sb.AppendLine("------------------------------------------------------------------------"); + sb.AppendLine($"PcbId: {genesisMeter.PcbId}"); + sb.AppendLine($"{Properties.Resources.StrPrintOrderNumberTxt}: {genesisMeter.OrderNumber}"); + sb.AppendLine($"{Properties.Resources.StrLblSerialNumberTxt}: {genesisMeter.CustomerSerialNumber}"); + sb.AppendLine($"{Properties.Resources.StrLblOperatorNameText}: {Software.UserName}"); + sb.AppendLine("------------------------------------------------------------------------"); + + //TODO THW take the log path from setup (done with GTB) + var loggingPath = $"C:\\GenesisLog\\{DateTime.Now:yyyy-MM-dd}\\ProductionUiCordonel\\"; + var fileName = $"{loggingPath}{DateTime.Now.ToString("yyyyMMddHHmmss")}_ExtendedReport_{genesisMeter.PcbId}.txt"; + + // Copy the header to the compressed string builder + var compressedSb = new StringBuilder(); + if (shortReport) + { + compressedSb.Append(sb); + } + + var rtbText = new TextRange( + rtbLog.Document.ContentStart, + rtbLog.Document.ContentEnd + ); + sb.Append($"{rtbText.Text}"); + // Write the complete report to the file + File.WriteAllText(fileName, sb.ToString()); + + // Reduce sb content to compress it for printout + if (shortReport) + { + + fileName = $"{loggingPath}{DateTime.Now.ToString("yyyyMMddHHmmss")}" + + $"_ShortReport_{genesisMeter.PcbId}.txt"; + var sr = new StringReader(rtbText.Text); + var srLine = sr.ReadLine(); + while (srLine != null) + { + if (srLine.Contains(Properties.Resources.StrProcessStateError) || + srLine.Contains(Properties.Resources.StrProcessStateDone) || + srLine.Contains(Properties.Resources.StrProcessStateWarning) || + srLine.Contains(Properties.Resources.StrProcessStateSkipped) || + srLine.Contains(Properties.Resources.StrWarningMsgSpecialRequirementActive)) + compressedSb.AppendLine(srLine); + srLine = sr.ReadLine(); + } + + // Returns the compressed file for error report printout + File.WriteAllText(fileName, compressedSb.ToString()); + if (printReport) + Process.Start(fileName); + } + } + + /// + /// Stop button as user interaction to kill the finalization process + /// + /// + /// + private void BtnStop_Click(Object sender, RoutedEventArgs e) + { + //set the process state to the state machine + _processCtrl?.SetProcessState(ProcessState.Stop); + } + + /// + /// Open requirement in Web-Browser. + /// + /// + /// + private void LblApprovalText_Click(Object sender, System.Windows.Input.MouseButtonEventArgs e) + { + var requirementId = 0; + var requirementWebPageLink = ServiceUrls.InspectCordonelRequirementInfoServiceUrl(); + if (_productionRequirements?.SpecialId != null && + _productionRequirements?.IsSpecialApproved != null && + _productionRequirements.SpecialId != 0) + { + requirementId = (Int32)_productionRequirements.SpecialId; + requirementWebPageLink += $"SpecialRequirements#{requirementId}"; + } + else if (_productionRequirements?.StandardId != null && + _productionRequirements?.IsStandardApproved != null && + _productionRequirements.StandardId != 0) + { + requirementId = (Int32)_productionRequirements.StandardId; + requirementWebPageLink += $"StandardRequirements#{requirementId}"; + } + + if (requirementId != 0) + { + Process.Start(requirementWebPageLink); + } + } + + private void HlpMenu_Click(Object sender, RoutedEventArgs e) + { + try + { + var filename = ""; + if (Thread.CurrentThread.CurrentCulture.Name.Contains("de")) + { + filename = Path.Combine(_assemblyExecRootPath, "Docu", @"Handbuch Cordonel Versandfertigmachen (VFM).pdf"); + } + else if (Thread.CurrentThread.CurrentCulture.Name.Contains("en")) + { + filename = Path.Combine(_assemblyExecRootPath, "Docu", @"Manual Cordonel End of Line Test (EOL).pdf"); + } + + if (!string.IsNullOrEmpty(filename)) + { + Process.Start(filename); + } + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + + private void Window_Loaded(Object sender, RoutedEventArgs e) + { + if (!Software.IsAutenticated) + OnLogin_Click(this, null); + + } + } +}