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
@@ -34,11 +34,11 @@
<appSettings>
<add key="Mode" value="2" /> <!--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="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="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,7 +895,9 @@ namespace Xylem.Common.Production.ProductionUiCordonel
// Returns the compressed file for error report printout
File.WriteAllText(fileName, compressedSb.ToString());
if (printReport)
{
Process.Start(fileName);
}
}
}
@@ -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);
}
}
}