ProductionUiCordonel: - manually merged some modules from main EOL_2.6.20

This commit is contained in:
Thomas Wiedebusch
2025-05-15 15:43:07 +02:00
parent 66f457dc0b
commit a5eabc2322
13 changed files with 3496 additions and 80 deletions
+1 -1
View File
@@ -155,7 +155,7 @@
</site>
<site name="MeterProcessState" id="2">
<application path="/" applicationPool="Clr4IntegratedAppPool">
<virtualDirectory path="/" physicalPath="C:\Users\SZLATEV\source\repos\laa_production\Common\Service\MeterProcessState" />
<virtualDirectory path="/" physicalPath="D:\Projekte\SENSUS_GitLab\LAA_PRODUCTION\Common\Service\MeterProcessState" />
</application>
<bindings>
<binding protocol="http" bindingInformation="*:56011:localhost" />
@@ -193,6 +193,9 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts
/// <summary>
/// Dispose serial port
/// </summary>
/// <remarks date="2025-Mai-12..14" author="T.Wiedebusch">
/// - Dispose procedure changed.
/// </remarks>
public void Dispose()
{
try
@@ -207,6 +210,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.
/// </remarks>
/// <remarks date="2025-Mai-12..14" author="T.Wiedebusch">
/// - Dispose procedure changed.
/// </remarks>
private void ReadingThreadLoop()
{
//put the receive thread to JoinWaitSleep to avoid reading and logger output for timeout on
@@ -450,6 +459,7 @@ namespace Xylem.Common.Hardware.Interfaces.Ports.SerialPorts
OnRawRecordReceived?.Invoke(this, new StringPortDataEventArgs(rxStringRecord,
_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 " +
_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");
//}
}
/// <inheritdoc />
@@ -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
{
/// <summary>
/// All Ports must use BasePort as an base class
/// its support some wrapping event/function handling
/// </summary>
public abstract class BaseSerialPort : IPort, IDisposable
{
private ILogger _byteDataLogger;
internal ILogger AsciiDataLogger;
/// <inheritdoc />
public virtual event EventHandler<BasePortDataEventArgs> OnRawRecordReceived;
/// <summary>
/// The port is not assigned.
/// </summary>
public const String PortNotAssigned = "NA";
/// <inheritdoc />
public virtual event EventHandler<BasePortDataEventArgs> OnRawRecordSendOut;
//initially do not signal event
private readonly AutoResetEvent _onSyncReceiveThread = new AutoResetEvent(false);
private readonly Thread _receiveThread;
private readonly CancellationTokenSource _receiveToken = new CancellationTokenSource();
/// <summary>
/// Delay between bytes if receiving has started in milliseconds
/// </summary>
private const Int32 InterByteReadDelayMs = 50;
/// <summary>
/// internal SerialPort class
/// </summary>
private readonly SerialPort _serialPort;
/// <summary>
/// Activate raw record recording
/// </summary>
public Boolean RecordStreamingRawData = false;
//public Boolean RecordStreamingRawData
//{
// get => _recordStreamingRawData;
// set => _recordStreamingRawData = value;
//}
/// <summary>
/// date and time of incoming first date interrupt on serial IO to set the
/// PC time-stamp to the record
/// </summary>
private DateTimeOffset _receiveTimeStampPc;
/// <summary>
/// Individual port settings depending on transmit protocol
/// </summary>
protected TransmitPortSettings PortSettingsForTransmitProtocol;
/// <inheritdoc />
public String GetPortName()
{
return _serialPort.PortName;
}
/// <summary>
/// store a identification of a port, this contains the Slot, Port, Protocol and Type
/// e.g. "Slot:1, Port:COM4, Protocol:Request, Type:IrDA -"
/// </summary>
public String Ident;
/// <summary>
/// ctor for SerialComPort with and full constructed <see cref="T:System.IO.Ports.SerialPort" /> properties
/// </summary>
/// <param name="ident">set port as unique</param>
/// <param name="serialPort">Class for communication, all parameters for serial communication needs to be set</param>
/// <param name="setting">Class for store all parameters for serial communication needs to be set came from transmit protocol</param>
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");
}
/// <summary>
/// received bytes in buffer of SerialComPort, calls base function
/// </summary>
/// <returns>number of bytes in Rx buffer</returns>
protected Int32 BytesToRead()
{
try
{
return _serialPort.BytesToRead;
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
return 0;
}
}
/// <summary>
/// read single byte from Rx buffer of SerialComPort, calls base function
/// </summary>
/// <returns></returns>
protected Int32 ReadByte()
{
try
{
return _serialPort.ReadByte();
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
return 0;
}
}
/// <inheritdoc />
public void PortWrite(Byte[] record)
{
try
{
SpecificPortWrite(record);
}
catch (Exception ex)
{
throw new SystemException($"{Ident} Communication error while sending data to port", ex);
}
}
/// <inheritdoc />
/// <summary>
/// flush Rx and Tx buffer of SerialComPort, calls base functions
/// </summary>
public void Clear()
{
try
{
if (!_serialPort.IsOpen)
{
return;
}
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
}
catch (Exception ex)
{
_byteDataLogger.Error(ex);
}
}
/// <summary>
/// Dispose serial port
/// </summary>
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);
}
}
/// <inheritdoc />
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}");
}
}
/// <summary>
/// Getting the base port
/// </summary>
protected SerialPort GetBaseComport()
{
return _serialPort;
}
/// <summary>
/// Fill FIFO with received data
/// </summary>
/// <remarks date="2018-Feb-16" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Feb-21" author="T.Wiedebusch">
/// - Time stamp added
/// </remarks>
/// <remarks date="2018-Dec-14" author="T.Wiedebusch">
/// - Timeout moved from <see cref="ReadingThreadLoop"/> to PhysicalDataReceived to avoid
/// <see cref="TimeoutException"/> of serial port while waiting on first incoming
/// data.
/// </remarks>
/// <remarks date="2019-Dec-12" author="T.Wiedebusch">
/// - Inter byte read delay used instead of response delay!!!
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
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();
}
}
/// <summary>
/// <see cref="SyncMarkRecord" />
/// </summary>
private SyncMarkRecord _syncMarkRecord;
/// <inheritdoc />
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();
}
}
}
/// <summary>
/// Reading thread loop getting data from serial port
/// </summary>
/// <remarks date="2018-Feb-16" author="T.Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Mar-08" author="T.Wiedebusch">
/// - Reading will be executed and repeated until buffer is empty or timeout
/// </remarks>
/// <remarks date="2018-Mar-09" author="T.Wiedebusch">
/// - 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
/// </remarks>
/// <remarks date="2018-Oct-23" author="T.Wiedebusch">
/// - Directly invoked onRawRecord Received event,
/// - Doubling of sync byte implemented
/// </remarks>
/// <remarks date="2018-Dec-14" author="T.Wiedebusch">
/// - Timeout moved from ReadingThreadLoop to <see cref="PhysicalDataReceived"/> to avoid
/// <see cref="TimeoutException"/> of serial port while waiting on first incoming
/// data and flush receive buffer at timeout to force task to enter JoinWaitSleep state.
/// </remarks>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
/// <remarks date="2019-Dec-03" author="T.Wiedebusch">
/// - Raw record recording for missing sync-byte issues.
/// </remarks>
/// <remarks date="2019-Dec-04" author="T.Wiedebusch">
/// - 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.
/// </remarks>
/// <remarks date="2019-Dec-05" author="T.Wiedebusch">
/// - 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.
/// </remarks>
/// <remarks date="2019-Dec-12" author="T.Wiedebusch">
/// - Inter byte read delay used instead of response delay.
/// </remarks>
/// <remarks date="2019-Dec-14" author="T.Wiedebusch">
/// - Flush buffer if received length is 0.
/// </remarks>
/// <remarks date="2022-Jul-15" author="T.Wiedebusch">
/// - 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.
/// </remarks>
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<Byte>();
//this is the informational record for logging on undetected SYNC byte
var rxRawRecord = new List<Byte>();
//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");
}
}
}
/// <inheritdoc />
public Boolean IsOpen()
{
return _serialPort != null && _serialPort.IsOpen;
}
/// <summary>
/// Serial port specific write routine
/// </summary>
/// <param name="txBuffer"></param>
/// <remarks date="2019-Apr-11" author="T.Wiedebusch">
/// - Hide data in logging for e.g. passwords
/// </remarks>
protected virtual void SpecificPortWrite(Byte[] txBuffer)
{
PhysicalWrite(txBuffer);
}
/// <summary>
/// Physically sending to the serial port
/// </summary>
/// <remarks date="2017" author="R.Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2018-Oct-23" author="T.Wiedebusch">
/// - Doubling of sync byte in protocol behind sync byte itself implemented
/// </remarks>
/// <remarks date="2022-Feb-25" author="T.Wiedebusch, R.Drabesch">
/// - 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.
/// </remarks>
protected void PhysicalWrite(Byte[] txBuffer)
{
try
{
//don't double the sync byte itself
var txByteList = new List<Byte> { 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}");
}
}
}
}
@@ -857,11 +857,20 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
/// <remarks date="2025-Mai-07" author="Thomas Wiedebusch">
/// - Temporary excluded un-comparable registers from write as this make no sense to write them blind being
/// unable to read the correct value back.
/// </remarks>
private StatusReturn WriteRegisterParameterSet(List<RecoveryRegisterItem> 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;
@@ -146,10 +146,9 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// </summary>
private static readonly List<RecoveryRegisterItem> _finalizeProgrammingAll = new List<RecoveryRegisterItem>
{
// 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
/// </summary>
private static readonly List<RecoveryRegisterItem> _finalizeProgrammingEmea = new List<RecoveryRegisterItem>
{
// 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 }),
};
/// <summary>
@@ -172,8 +175,11 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// </summary>
private static readonly List<RecoveryRegisterItem> _finalizeProgrammingEol = new List<RecoveryRegisterItem>
{
// 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
/// </summary>
private Int32 _actualRegisterCtr;
/// <summary>
/// Stop register access
/// </summary>
public Boolean StopRegisterAccess;
/// <summary>
/// Genesis meter object
/// </summary>
@@ -573,6 +574,8 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
List<RecoveryRegisterItem> 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
/// </summary>
/// <param name="recoveryRegistersDb">list of registers to update from database without special sequence
/// </param>
/// <param name="cancellationToken"></param>
/// <returns>true if setup succeeded</returns>
/// <remarks date="2024-Jan-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public StatusReturn EolProgrammingRegisters(List<RecoveryRegisterItem> recoveryRegistersDb)
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
public StatusReturn EolProgrammingRegisters(List<RecoveryRegisterItem> recoveryRegistersDb, CancellationToken cancellationToken)
{
// mark EOL as true to force accu reset
return WriteRegisters(recoveryRegistersDb, true);
return WriteRegisters(recoveryRegistersDb, cancellationToken, true);
}
/// <summary>
@@ -668,6 +675,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// </summary>
/// <param name="recoveryRegistersDb">list of registers to update from database without special sequence
/// </param>
/// <param name="cancellationToken"></param>
/// <param name="eolProgramming">EOL (end of line) programming with accu reset</param>
/// <returns>true if restoring succeeded</returns>
/// <remarks date="2023-Oct-09" author="Thomas Wiedebusch">
@@ -722,7 +730,11 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2025-Feb-14" author="Thomas Wiedebusch">
/// - Exported write routines to <see cref="WriteRegisterParameterSet"/>.
/// </remarks>
public StatusReturn WriteRegisters(List<RecoveryRegisterItem> recoveryRegistersDb, Boolean eolProgramming = false)
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
public StatusReturn WriteRegisters(List<RecoveryRegisterItem> 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
/// <param name="recoveryRegistersDb"></param>
/// <param name="recRegistersCount"></param>
/// <param name="meterRegisters"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <remarks date="2025-Feb-14" author="Thomas Wiedebusch">
/// - Initial imported from <see cref="WriteRegisters"/>.
/// </remarks>
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
private StatusReturn WriteRegisterParameterSet(List<RecoveryRegisterItem> 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.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>true if equal</returns>
/// <remarks date="2023-Nov-20" author="Thomas Wiedebusch">
/// - Initial.
@@ -963,13 +982,15 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2024-Mar-20" author="Thomas Wiedebusch">
/// - Hash password and encryption key with SHA256.
/// </remarks>
public Boolean CompareRegisters()
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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).
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>true if registers could be read</returns>
/// <remarks date="2020-Dec-08" author="Thomas Wiedebusch">
/// - Initial.
@@ -1174,7 +1196,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2024-Dec-12" author="Thomas Wiedebusch">
/// - Check readBack value for null.
/// </remarks>
public Boolean FinalReadRegisters()
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>true if registers could be read</returns>
/// <remarks date="2020-Dec-08" author="Thomas Wiedebusch">
/// - Initial.
@@ -1263,7 +1289,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2024-Feb-19" author="Thomas Wiedebusch">
/// - Register list changed from RegisterDefinition to RecoveryRegisterItem list.
/// </remarks>
public Boolean InitialReadRegisters()
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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);
}
/// <summary>
@@ -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.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>true if registers could be read</returns>
/// <remarks date="2023-Oct-16" author="Thomas Wiedebusch">
/// - Initial.
@@ -1351,7 +1381,10 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2024-Feb-19" author="Thomas Wiedebusch">
/// - Register list changed from RegisterDefinition to RecoveryRegisterItem list.
/// </remarks>
public Boolean ReadRegisters()
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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);
}
/// <summary>
/// Read predefined registers from RecoveryRegisters.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>true if registers could be read</returns>
/// <remarks date="2024-Sep-25" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public Boolean ReadPresetRegisters()
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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);
}
/// <summary>
@@ -1399,6 +1436,7 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// Executes login, read cycle and logout.
/// </summary>
/// <param name="registers">registers to read</param>
/// <param name="cancellationToken"></param>
/// <returns>true if registers could be read</returns>
/// <remarks date="2020-Dec-17" author="Thomas Wiedebusch">
/// - Initial.
@@ -1428,11 +1466,13 @@ namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
/// <remarks date="2024-Feb-19" author="Thomas Wiedebusch">
/// - Register list changed from RegisterDefinition to RecoveryRegisterItem list.
/// </remarks>
private Boolean ReadRegistersSet(ref List<RecoveryRegisterItem> registers)
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
private Boolean ReadRegistersSet(ref List<RecoveryRegisterItem> 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)
@@ -1 +1 @@
8cb55c312de586b683d72cf58c13e2801d10e14b767777d98338d3e1473a1213
7df2ad6be70922b931820861acc675522243ee78
@@ -34,11 +34,11 @@
<appSettings>
<add key="Mode" value="2" /> <!--1 = EOL ; 2 = Picking-->
<add key="Mode" value="1" /> <!--1 = EOL ; 2 = Picking-->
<add key="SirtComport433" value="14" /> <!--Individual for Thomas-->
<add key="SirtComport868" value="15" /> <!--Individual for Thomas-->
<add key="SirtBoxNr" value="2" /> <!--Roland and Thomas, box for development, box at shipping station is 1-->
<add key="StationID" value="2" /> <!--Roland and Thomas, shipping station is 1-->
<add key="StationID" value="1" /> <!--Roland and Thomas, shipping station is 1-->
</appSettings>
@@ -176,6 +176,9 @@ namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses.EolPr
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
/// <remarks date="2025-Mai-07" author="Thomas Wiedebusch">
/// - Removed change and read back of MainAlarmMask and ExtendedAlarmMask.
/// </remarks>
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<Byte>(Meter.ReadRegister("SENSUSRADIO_MainAlarmMask"));
var extendedAlarmMask =
RegisterConverter.ByteArrayToValue<Byte>(Meter.ReadRegister("SENSUSRADIO_ExtendedAlarmMask"));
_finalRadioSystemState = RegisterConverter.ByteArrayToValue<Byte>(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)))
{
@@ -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
{
/// <summary>
/// Maximum of expected processing steps for this process to feed the GUI progress bar.
/// Use the sleep 250 to create x seconds timeout;
/// </summary>
private const Int32 TimeoutLoop_ms = 250;
private const Int32 Timeout_s = 10;
private const Int32 MaxActualProgressSteps = 1000 / TimeoutLoop_ms * Timeout_s;
/// <summary>
/// Remind the ID of the radio result as feedback from the DB.
/// </summary>
private Int64 _radioResultCheckId;
/// <summary>
/// Final state of radio system.
/// </summary>
private Int32 _finalRadioSystemState;
/// <summary>
/// Reminder for specific user control
/// </summary>
private UcRadioCheck _checkRadioUserControl;
/// <summary>
/// Signal level successfully tested
/// </summary>
private Boolean _successSignalLevel;
/// <summary>
/// Radio encryption key converted to string
/// </summary>
private String _radioEncryptionKey;
/// <summary>
/// Test finished can be good or abort by done button to exit this production process
/// </summary>
private Boolean _testFinished;
/// <summary>
/// SIRT communication okay
/// </summary>
private Boolean _sirtComOkay;
private DataContainer _dataContainer;
private Statemashine _stateMachine;
/// <summary>
/// Minimum PAM signal level of SIRT got from DB
/// </summary>
private Byte? _rssiPamSirtDbMin;
/// <summary>
/// Maximum PAM signal level of SIRT got from DB
/// </summary>
private Byte? _rssiPamSirtDbMax;
/// <summary>
/// Minimum SEMI signal level of Cordonel response got from DB
/// </summary>
private Byte? _rssiSemiCordonelDbMin;
/// <summary>
/// Maximum SEMI signal level of Cordonel response got from DB
/// </summary>
private Byte? _rssiSemiCordonelDbMax;
/// <summary>
/// Measured from SIRT the signal strength of the Cordonel SEMI answer
/// </summary>
private Byte? _rssiSemiCordonelMeasuredBySirt;
/// <summary>
/// Measured from Cordonel the signal strength of the SIRT PAM received
/// </summary>
private Byte? _rssiPamSirtMeasuredByCordonel;
/// <summary>
/// Number of the SIRT box as these are evaluated for their signal strength
/// </summary>
private Byte _sirtBoxNr;
/// <summary>
/// Comport number of SIRT
/// </summary>
private Byte _sirtComport;
/// <summary>
/// Radio address Cordonel
/// </summary>
private UInt32 _radioAddressCordonel;
/// <summary>
/// Ctor
/// </summary>
public CheckRadio(String name) : base(name)
{
}
/// <inheritdoc/>
protected override void InitUserControl()
{
UserControl = new UcRadioCheck();
_checkRadioUserControl = (UcRadioCheck)UserControl;
_checkRadioUserControl.OnResetRequested += delegate
{
Retry();
};
//_checkRadioUserControl.OnDoneRequested += delegate
//{
// SaveRssiLevelsToDb();
//};
}
/// <summary>
/// Check of radio signal strength, skips process on special permission or if NA region
/// </summary>
/// <returns></returns>
/// <remarks date="2023-Jun-22" author="Thomas Wiedebusch/Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2023-Jun-27" author="Thomas Wiedebusch">
/// - Skip process on special permission or if NA region,
/// - Remove userControl from main screen if skipped.
/// </remarks>
/// <remarks date="2023-Jun-29" author="Thomas Wiedebusch">
/// - Fire SingleProcessState event with argument.
/// </remarks>
/// <remarks date="2023-Jul-07" author="Thomas Wiedebusch">
/// - Removed signals for <see cref="SingleProcessState.Error"/> and <see cref="SingleProcessState.Succeeded"/> as
/// these will be automatically set based on return value in
/// <see cref="BaseProductionProcess.FinalizeProcess"/>.
/// </remarks>
/// <remarks date="2023-Jul-11" author="Thomas Wiedebusch">
/// - Assignment of stateMachine and dataContainer.
/// </remarks>
/// <remarks date="2024-Jul-20..22" author="Thomas Wiedebusch">
/// - Get encryption key moved to process <see cref="ExecFinalParametrization"/> to speed up
/// this process as this process needs to be run exclusively.
/// </remarks>
/// <remarks date="2024-Sep-05" author="Thomas Wiedebusch">
/// - EOL progress signaled.
/// </remarks>
/// <remarks date="2024-Sep-09" author="Thomas Wiedebusch">
/// - Logout at finalization,
/// - Log all write accesses.
/// </remarks>
/// <remarks date="2025-Jan-21" author="Thomas Wiedebusch">
/// - SkipRadioCheck not nullable.
/// </remarks>
/// <remarks date="2025-Feb-11" author="Thomas Wiedebusch">
/// - SetTestInit as bool.
/// </remarks>
/// <remarks date="2025-Feb-20" author="Thomas Wiedebusch">
/// - Separate TestInit and SirtInit (only if test shouldn't be skipped).
/// </remarks>
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - CancellationToken.
/// </remarks>
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<UInt32>(
Meter.ReadRegister("SENSUSRADIO_FrequencyIndicator"));
_radioAddressCordonel =
RegisterConverter.ByteArrayToValue<UInt32>(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<Byte>(Meter.ReadRegister("SENSUSRADIO_MainAlarmMask"));
var extendedAlarmMask =
RegisterConverter.ByteArrayToValue<Byte>(Meter.ReadRegister("SENSUSRADIO_ExtendedAlarmMask"));
_finalRadioSystemState = RegisterConverter.ByteArrayToValue<Byte>(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);
}
}
/// <summary>
/// Common for test Init
/// </summary>
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;
}
/// <summary>
/// Initialize the SIRT
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Common to prepare exit for all status returns
/// </summary>
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;
}
/// <summary>
/// Common for test failed
/// </summary>
/// <param name="errorMsg">error message to dispatch and log</param>
/// <remarks><see cref="StatusReturn.Failed"/></remarks>
private StatusReturn SetTestFailed(String errorMsg)
{
EolProgress.FinalRadioSystemStateCheck = EOLStatus.FAIL;
ErrorMsgDispatcher(errorMsg);
PrepareExit();
return StatusReturn.Failed;
}
/// <summary>
/// Common for test succeeded
/// </summary>
/// <remarks><see cref="StatusReturn.Okay"/></remarks>
private StatusReturn SetTestSucceeded()
{
EolProgress.FinalRadioSystemStateCheck = EOLStatus.OK;
PrepareExit();
return StatusReturn.Okay;
}
/// <summary>
/// Disable all buttons of user control.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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));
}
/// <summary>
/// Check of radio signal strength.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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);
}
}
/// <summary>
/// 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!
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
private void ClearRssiMeasurement()
{
_sirtComOkay = false;
_rssiPamSirtMeasuredByCordonel = null;
_rssiSemiCordonelMeasuredBySirt = null;
}
/// <summary>
/// Retry collection of RSSI values (entire process with sending PAM and receiving SEMI).
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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);
}
/// <summary>
/// ATTENTION:
/// As those definitions are directly used as DB definitions for pushing the result:
///
/// LEAVE THOSE NAMING AS DEFINED HERE!
///
/// </summary>
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;
}
}
/// <summary>
/// Save RSSI values to DB.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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;
}
}
/// <summary>
/// Get RSSI values from DB.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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);
}
}
/// <summary>
/// Send PAM to force a BUP and a SEMI and start the RSSI observer to measure the signal strength.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2023-07-11" author="Thomas Wiedebusch">
/// - Optimized based on new documentations.
/// </remarks>
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++;
}
/// <summary>
/// Get RSSI (Received Signal Strength Indicator) values from SIRT (Sensus Interface Radio Tool).
/// Set the <see cref="_successSignalLevel"/>
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-09-17" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
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();
}
}
/// <summary>
/// State machine state change handler.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
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);
}
}
}
}
@@ -453,11 +453,14 @@ namespace Xylem.Common.Production.ProductionUiCordonel.ProductionProcesses
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Handle genesis meter is null as new meter.
/// </remarks>
/// <remarks date="2025-Mai-12" author="Thomas Wiedebusch">
/// - Check _genesisMeter not null before equality with old pcbid.
/// </remarks>
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.
@@ -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
{
/// <summary>
/// Implementation for all methods needed to drive the state machine of the process controller
/// </summary>
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<IProductionProcess> 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<String> _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;
/// <summary>
/// Change the overview table of the GUI to the state of the individual process
/// </summary>
public event EventHandler<SingleProcessStateArgs> OnSingleProcessStateChanged;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<ProcessStateArgs> OnOverallProcessStateChanged;
/// <summary>
/// Request for logging to GUI output and log-file
/// </summary>
public event EventHandler<ProcessLogArgs> OnProcessLogRequest;
/// <summary>
/// Keep the progress bars of the GUI up to date
/// </summary>
public event EventHandler<ProcessProgressArgs> OnProcessProgressChanged;
// cancellation token
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
// cyclic timer for state machine call
private Timer _processCtrlTimer;
private const Int32 ProcessTimerCycleMs = 250;
/// <summary>
/// Avoid waiting for a single process completion.
/// </summary>
public const IProductionProcess DoNotWaitForSingleProcessCompletion = null;
/// <summary>
/// Kick off a parallel execution of the state machine.
/// </summary>
public const Boolean ExecuteInParallel = true;
#endregion --------------------------------------- Variables --------------------------------------------------
#region ------------------------------------------ Class ------------------------------------------------------
/// <summary>
/// Ctor:
/// - Setup process timer for cyclic call of state machine,
/// - Create and setup Cordonel.
/// </summary>
/// <param name="slot">physical slot for this meter</param>
/// <param name="processSateDef"></param>
/// <remarks date="2023-Feb-14" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2023-Jul-04" author="Thomas Wiedebusch">
/// - Always set to production mode to load the registers for this FW and not the latest,
/// - Always RegisterWatchService on,
/// - Always MinMaxCHeck on.
/// </remarks>
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;
}
/// <summary>
/// Initialization of new meter
/// </summary>
/// <remarks date="2023-Feb-14" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Initialize processes to build a list of all production processes.
/// </remarks>
/// <remarks date="2024-Sep-09" author="Thomas Wiedebusch">
/// - Removed list of failed production processes
/// </remarks>
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<String>();
// 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();
}
/// <summary>
/// Removal of old meter
/// </summary>
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;
}
/// <summary>
/// Dispose.
/// </summary>
/// <remarks date="2023-Feb-14" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public void Dispose()
{
_processCtrlTimer?.Dispose();
_processCtrlTimer = null;
_processStateDef = null;
KillProcesses();
RemoveMeter();
}
#endregion --------------------------------------- Class ------------------------------------------------------
#region ------------------------------------------ Events -----------------------------------------------------
/// <summary>
/// Single process state or process list changed event,
/// Use event to force special procedure like uninstalling events
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2023-Feb-14" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2023-Jun-29" author="Thomas Wiedebusch">
/// - Remove process events if process is finished.
/// </remarks>
/// <remarks date="2023-Jul-17" author="Thomas Wiedebusch">
/// - Remove process events if process is finished or skipped.
/// </remarks>
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);
}
/// <summary>
/// Handler to set a new state for the state machine regarding the entire final-process
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2023-Mar-03" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
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;
}
}
/// <summary>
/// Process timer
/// </summary>
/// <param name="state"></param>
/// <remarks date="2023-Jan-31" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void TmrProcessCtrlCycle_Elapsed(Object state)
{
StateMachine();
}
#endregion --------------------------------------- Events -----------------------------------------------------
#region ------------------------------------------ State Machine ----------------------------------------------
/// <summary>
/// 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
/// </summary>
/// <remarks date="2023-Jan-17" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2025-Apr-15" author="Thomas Wiedebusch">
/// - Generic implementation
/// </remarks>
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 ------------------------------------------------------
/// <summary>
/// Returns the currentGenesis information for display
/// </summary>
/// <returns></returns>
/// <remarks date="2023-Feb-27" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public CordonelRequirements GetProductionRequirements()
{
return _cordonelRequirements;
}
/// <summary>
/// Returns the currentGenesis information for display
/// </summary>
/// <returns></returns>
/// <remarks date="2023-Feb-27" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public IGenesisMeter GetGenesisMeter()
{
return _genesisMeter;
}
/// <summary>
/// Clears all collected data on new genesisMeter and reminds the detected for next
/// detection and run of this comparison.
/// </summary>
/// <returns>true for new meter</returns>
/// <remarks date="2023-Jun-28" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2023-Jul-17" author="Thomas Wiedebusch">
/// - 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!
/// </remarks>
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Handle genesis meter is null as new meter.
/// </remarks>
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;
}
/// <summary>
/// Restart final test on first failed or aborted test.
/// </summary>
/// <remarks date="2023-Jul-18" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Sep-09" author="Thomas Wiedebusch">
/// - Removed list of failed production processes and search in Processes
/// </remarks>
/// <remarks date="2024-Oct-21" author="Thomas Wiedebusch">
/// - Leave skipped processes as is.
/// </remarks>
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;
}
/// <summary>
/// Error handler
/// </summary>
/// <remarks date="2023-Feb-15" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Feb-07" author="Thomas Wiedebusch">
/// - Activated check for sequences in production process for ProductionProcessCheckFinalParametrization
/// to re-start with ProductionProcessExecFinalParametrization.
/// </remarks>
/// <remarks date="2024-Sep-09" author="Thomas Wiedebusch">
/// - Removed list of failed production processes
/// </remarks>
/// <remarks date="2025-Jan-28" author="Thomas Wiedebusch">
/// - Kick off exec parametrization if check after reboot failed.
/// </remarks>
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();
}
/// <summary>
/// Calculate the maximum of all processes expected to execute and signal to GUI.
/// </summary>
private void CalcAndPushMaxOverAllProcessSteps()
{
if (_processStateDef == null)
return;
var _maxOverallProcessesSteps = _processStateDef.GetNumberOfInitializedProcesses();
OnProcessProgressChanged?.Invoke(this, new ProcessProgressArgs(_maxOverallProcessesSteps,
ProcessProgressArgs.ProcessProgressType.MaxOverallProcessSteps));
}
/// <summary>
/// Calculate all processes which are completed or skipped and signal to GUI.
/// </summary>
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));
}
/// <summary>
/// Initialize all processes which are connected to a production process.
/// </summary>
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void InitProcesses()
{
if (Processes == null)
Processes = new List<IProductionProcess>();
// 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);
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="processStateStruct"></param>
/// <returns>actual state</returns>
/// <remarks date="2023-Jan-31" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2023-Jun-21" author="Thomas Wiedebusch">
/// - Skipped process due to one of the previous processes failed
/// </remarks>
/// <remarks date="2023-Jul-17" author="Thomas Wiedebusch">
/// - Restructured to avoid process start if processes to wait for are not completed
/// </remarks>
/// <remarks date="2023-Dec-15" author="Thomas Wiedebusch">
/// - Changed from programming parameters to register restorer
/// </remarks>
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Changed setup process to start process being able to create a process list in advance.
/// </remarks>
/// <remarks date="2024-Sep-01" author="Thomas Wiedebusch">
/// - Changed setup process to start process being able to create a process list in advance.
/// </remarks>
/// <remarks date="2025-Apr-15" author="Thomas Wiedebusch">
/// - Feed with processStateStruct.
/// </remarks>
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);
}
/// <summary>
/// Check all processes for error or abort request
/// </summary>
/// <returns>true if abort required</returns>
/// <remarks date="2025-Apr-10" author="Thomas Wiedebusch">
/// - Cancellation token.
/// </remarks>
private Boolean AnyProcessRequiresAbortion()
{
return Processes.Any(pp => pp.SingleProcessState == SingleProcessState.Error ||
pp.SingleProcessState == SingleProcessState.Abort ||
_cancellationToken.IsCancellationRequested);
}
/// <summary>
/// Check process if in status waiting or running (incomplete)
/// </summary>
/// <param name="pp">production process</param>
/// <returns>true if process to wait for execution or exit running state</returns>
private Boolean WaitForProcess(IProductionProcess pp)
{
return pp.SingleProcessState == SingleProcessState.Waiting ||
pp.SingleProcessState == SingleProcessState.Running;
}
/// <summary>
/// Install process events
/// </summary>
/// <param name="process"></param>
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;
}
/// <summary>
/// Remove process events
/// </summary>
/// <param name="process"></param>
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;
}
/// <summary>
/// Get the actual process state of the state machine
/// </summary>
/// <returns>actual process state</returns>
/// <remarks date="2023-Jan-17" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private ProcessState GetProcessState()
{
return _stateMachineState;
}
/// <summary>
/// Set the new process state of the state machine
/// </summary>
/// <returns>true, if process state is set</returns>
/// <remarks date="2023-Jan-17" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public Boolean SetProcessState(ProcessState newProcessState)
{
if (newProcessState != _stateMachineState)
{
_stateMachineState = newProcessState;
return true;
}
return false;
}
/// <summary>
/// Remove all processes from the <see cref="Processes"/> list:
/// - Signal change to GUI,
/// - Remove process events all,
/// - Set state to abort.
/// </summary>
/// <remarks date="2023-Jul-11" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2025-Feb-13" author="Thomas Wiedebusch">
/// - Abort register access.
/// </remarks>
/// <remarks date="2025-Apr-07" author="Thomas Wiedebusch">
/// - Cancellation token source.
/// </remarks>
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();
}
}
/// <summary>
/// Abort all processes if one previous process failed, but leave the <see cref="Processes"/> list intact.
/// Leave all events for those which are already running intact to force a clean stop.
/// </summary>
/// <remarks date="2023-Jun-21" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
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 ------------------------------------------------------
}
}
@@ -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,9 +895,11 @@ namespace Xylem.Common.Production.ProductionUiCordonel
// Returns the compressed file for error report printout
File.WriteAllText(fileName, compressedSb.ToString());
if (printReport)
{
Process.Start(fileName);
}
}
}
/// <summary>
/// Stop button as user interaction to kill the finalization process
@@ -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
{
/// <summary>
/// Interaction logic for FinalTest.xaml
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<SlotConfig[]>(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}: ?");
}
}
/// <summary>
/// Shows the user access window with login form or password change form.
/// </summary>
/// <param name="visible"></param>
/// <param name="control">Windows forms control</param>
/// <param name="headerTxt"></param>
/// <remarks date="2025-Mar-25" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
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);
}
}
/// <summary>
/// Close event handler of the user access window with login form or password change form.
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
/// <remarks date="2025-Mar-25" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
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;
}));
}
/// <summary>
/// Update the elapsed time as information
/// </summary>
/// <returns>information attached to process depending on region</returns>
/// <remarks date="2024-Jul-11" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void SetTimeDisplay()
{
var time = DateTimeOffset.UtcNow;
var timeSpan = time - _startTime;
UpdateContentControl(lblTimeValue, $@"{(UInt32)timeSpan.TotalMinutes}:{timeSpan.Seconds:00}");
}
/// <summary>
/// Common routine for state change:
/// - Log state change,
/// - Execute stop procedure on error.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2020-Dec-12" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-Aug-30" author="Thomas Wiedebusch">
/// - Extended.
/// </remarks>
/// <remarks date="2025-Apr-10" author="Thomas Wiedebusch">
/// - Extended.
/// </remarks>
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);
}
}
/// <summary>
/// Enable UI elements
/// </summary>
/// <param name="elm"></param>
/// <param name="isEnabled"></param>
/// <param name="isVisible"></param>
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;
}));
}
/// <summary>
/// Fill logging window with information
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2023-07-06" author="Thomas Wiedebusch">
/// - Changed to rich text edit with colored output.
/// </remarks>
/// <remarks date="2024-08-28" author="Thomas Wiedebusch">
/// - Avoid output of repeated text or useless empty text.
/// </remarks>
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)
{
}
}
}
));
}
/// <summary>
/// Set values for progress bar, maximum and actual for single process and overall process.
/// - Limit values to 0.0..100.0.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-07-18" author="Thomas Wiedebusch">
/// - Introduced multi line text with center alignment.
/// </remarks>
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} %");
}
));
}
}
/// <summary>
/// Output message to control without access violation.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2023-07-11" author="Thomas Wiedebusch">
/// - Introduced multi line text with center alignment.
/// </remarks>
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
};
}
));
}
/// <summary>
/// Status grid update for all single processes
/// </summary>
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;
}
));
}
/// <summary>
/// Dispatch new user control to main screen
/// </summary>
/// <param name="uc"></param>
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
/// <summary>
/// Handle timing depending updates.
/// </summary>
/// <returns></returns>
/// <remarks date="2024-07-11" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void TmrProgressUpdate_Tick(Object sender, EventArgs e)
{
SetTimeDisplay();
}
#endregion
/// <summary>
/// Update of status grid with information returned by user control.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-08-28" author="Thomas Wiedebusch">
/// - Activate new user control on start of process.
/// </remarks>
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);
}
/// <summary>
/// Set labels of device information and order number
/// </summary>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2024-07-15" author="Thomas Wiedebusch">
/// - Additional info of Cordonel.
/// </remarks>
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);
}
/// <summary>
/// Logging a file with all process information, building a compressed report for printout.
/// </summary>
/// <remarks date="2025-Apr-10" author="Thomas Wiedebusch">
/// - Reduced printout.
/// </remarks>
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);
}
}
/// <summary>
/// Stop button as user interaction to kill the finalization process
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void BtnStop_Click(Object sender, RoutedEventArgs e)
{
//set the process state to the state machine
_processCtrl?.SetProcessState(ProcessState.Stop);
}
/// <summary>
/// Open requirement in Web-Browser.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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);
}
}
}