common/Production/ProductionUiCordonel/ProductionProcesses/ProcessController.cs
2026-04-23 17:50:07 +02:00

1073 lines
49 KiB
C#

using LaaPackages.Features.Cordonel.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Threading;
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.Genesis.Registers.DataTypes;
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.Properties;
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 _lockedStateMachineState;
// 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> OnProcessStateChanged;
/// <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> OnStateMachineStateChanged;
/// <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;
/// <summary>
/// Dispatch a message to the GUI
/// </summary>
public event EventHandler<GuiMessageArgs> OnGuiMessageDispatcher;
// cancellation token
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
// cyclic timer for state machine call
private Timer _processCtrlTimer;
private const Int32 ProcessTimerCycleMs = 250;
// Delay timer between finish exec-parametrization and 'StoreConfiguration' as 'PERIODICLOG' doesn't
// have 'StoreConfiguration' and needs at least 20 s to update contents before 'StoreConfiguration'
// can be dispatched to 'SENSUSRADIO'.
private DispatcherTimer _paramExeFinishedToStoreConfigDelayTimer;
// Cycle time in milliseconds
private const Int32 ParamExeFinishedToStoreConfigDelayCycleMs = 1000;
// Time to keep in seconds
private const Int32 ParamExeFinishedToStoreConfigTimeS = 22;
private Boolean _paramExeFinishedToStoreConfigDelayTimerExceeded;
private DateTimeOffset _paramExeFinishedToStoreConfigDelayTimerStartTime;
/// <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>
/// <remarks date="2025-Nov-20" author="Thomas Wiedebusch">
/// - _paramExeFinishedToStoreConfigurationDelayTimer.
/// </remarks>
public ProcessController(Int32 slot, IProcessStateDef processSateDef)
{
// Timer for cyclic call of state machine
_processCtrlTimer = new Timer(TmrProcessCtrlCycle_Elapsed, null, ProcessTimerCycleMs, ProcessTimerCycleMs);
// Delay timer between final-param-exec finished and start of 'StoreConfiguration'
_paramExeFinishedToStoreConfigDelayTimer = new DispatcherTimer();
_paramExeFinishedToStoreConfigDelayTimer.Tick += TmrParamExeFinishedToStoreConfigCycle_Tick;
_paramExeFinishedToStoreConfigDelayTimer.Interval = TimeSpan.FromMilliseconds(ParamExeFinishedToStoreConfigDelayCycleMs);
_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
_lockedStateMachineState = 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;
_paramExeFinishedToStoreConfigDelayTimer?.Stop();
_paramExeFinishedToStoreConfigDelayTimer = 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>
/// <remarks date="2025-Nov-20" author="Thomas Wiedebusch">
/// - Init and start the delay timer between finish exec-parametrization and 'StoreConfiguration
/// to all apps as 'PERIODICLOG' doesn't have 'StoreConfiguration' and needs at least 20 s to
/// update contents before 'SENSUSRADIO' tries to synchronize all settings.
/// </remarks>
private void ProcessStateChanged_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;
}
// Delay timer init and start if parametrization exec finished but only for 'EMEA' region
if (pp != null &&
pp.ProcessName != null &&
pp.Meter != null &&
pp.Meter.Region.Equals("EMEA") &&
pp.ProcessName.Equals(Resources.StrStateFinalParametrization) &&
e.SingleProcessState == SingleProcessState.Succeeded)
{
var teaTime = new TimeT();
_paramExeFinishedToStoreConfigDelayTimerExceeded = false;
_paramExeFinishedToStoreConfigDelayTimerStartTime = teaTime.DateTimeUtc;
_paramExeFinishedToStoreConfigDelayTimer.Start();
var msg = $"{Resources.StrMsgRebootDelayTimerStarted} {ParamExeFinishedToStoreConfigTimeS} s" +
$" - {teaTime}";
OnProcessLogRequest?.Invoke(sender, new ProcessLogArgs(msg));
}
// signal state change to GUI
OnProcessStateChanged?.Invoke(sender, e);
}
private void ProcessProgress_Handler(Object sender, ProcessProgressArgs e)
{
OnProcessProgressChanged?.Invoke(sender, e);
}
private void GuiMessageDispatcher_Handler(Object sender, GuiMessageArgs e)
{
OnGuiMessageDispatcher?.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 ForceStateMachineStateChange_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();
}
/// <summary>
/// Delay timer between parametrization exec finished and store config to support the 'PERIODICLOG' update
/// which needs at least 20 s after writing to update the internal contents. Not holding this time will cause
/// to take the value before the writing access as 'SENSUSRADIO' will synchronize all parameters if it receives
/// the 'StoreConfiguration' command.
/// </summary>
/// <param name="state"></param>
/// <param name="e"></param>
/// <remarks date="2025-Nov-20" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void TmrParamExeFinishedToStoreConfigCycle_Tick(Object state, System.EventArgs e)
{
var teaTime = new TimeT();
var timeSpan = teaTime.DateTimeUtc - _paramExeFinishedToStoreConfigDelayTimerStartTime;
if (timeSpan.Seconds >= ParamExeFinishedToStoreConfigTimeS)
{
_paramExeFinishedToStoreConfigDelayTimerExceeded = true;
_paramExeFinishedToStoreConfigDelayTimer.Stop();
var msg = $"{Resources.StrMsgRebootDelayTimerExceeded} {ParamExeFinishedToStoreConfigTimeS} s - {teaTime}";
OnProcessLogRequest?.Invoke(this, new ProcessLogArgs(msg));
}
}
#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 == _lockedStateMachineState)
{
Thread.Sleep(ProcessTimerCycleMs >> 1);
}
else
{
// ATTENTION: Has to be the very first step here!
// Remind backup state to avoid repeated execution and side effects
_lockedStateMachineState = _stateMachineState;
try
{
// Signal overall process progress to GUI based on ready processes
CalcAndPushOverAllProgress();
// Signal state change to logger
OnStateMachineStateChanged?.Invoke(this, new ProcessStateArgs(_stateMachineState));
if (_processStateDef != null)
{
// Check if state is the first in the list of production processes to reset cancellationToken and
// calculate the over all process steps to feed the progress bars.
var startProcessState = _processStateDef.GetStateOfFirstProcess();
if (startProcessState == _stateMachineState)
{
// Kill all processes at start
KillProcesses();
// Avoid immediately cancellation for first process of the process chain
ResetCancellationToken();
// Set up the counter for maximum overall process progress bar in GUI
CalcAndPushMaxOverAllProcessSteps();
}
// Get the processStateStruct of this new required stateMachineState
var processStateStruct = _processStateDef.GetProcessStateStructOfState(_stateMachineState);
switch (_stateMachineState)
{
case ProcessState.Idle:
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"!
if (processStateStruct.NextStateOnSuccess != null)
{
_stateMachineState = CheckForNewMeter()
? (ProcessState)processStateStruct.NextStateOnSuccess
: processStateStruct.ErrorExitState;
}
else
{
_stateMachineState = ProcessState.Error;
}
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>
/// Common routine to reset the cancellation token on restart or new meter
/// </summary>
/// <remarks date="2025-Jul-07" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2025-Aug-1" author="Thomas Wiedebusch">
/// - Rest all cancellation tokens in processes.
/// </remarks>
private void ResetCancellationToken()
{
// This state needs an external state change like user input to go ahead
// Remove cancellation token if idle reached for clean start
if (_cancellationToken.IsCancellationRequested)
{
// Reset the cancellation request
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
foreach (var pp in Processes)
{
pp.CancellationToken = _cancellationToken;
}
}
}
/// <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>
/// <remarks date="2025-Mai-12" author="Thomas Wiedebusch">
/// - Check _genesisMeter not null before equality with old pcbid.
/// </remarks>
/// <remarks date="2025-Jun-07" author="Thomas Wiedebusch">
/// - ResetCancellationToken.
/// </remarks>
/// <remarks date="2025-Jun-08" author="Thomas Wiedebusch">
/// - Remind order number,
/// - avoid to kill the order number scan and avoid to reset this production state to idle.
/// </remarks>
private Boolean CheckForNewMeter()
{
if (_genesisMeter == null ||
(!string.IsNullOrEmpty(_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.
var pp = _processStateDef.GetProcessOfState(ProcessState.DetectCordonel);
var ppNo = _processStateDef.GetProductionProcessNoOfProcess(pp);
// Remind order number to restore it for new genesis for assembly line, where the order
// number scan is the first step
var orderNoBackup = _genesisMeter?.OrderNumber;
KillProcesses(ppNo);
RemoveMeter();
InitNewMeter();
InitProcesses(ppNo);
ResetCancellationToken();
// Remind actual genesis for next call
_lastPcbId = _genesisMeter?.PcbId;
// Restore production order number for recurrent call with identical order but different
// new meter
if (_genesisMeter != null) _genesisMeter.OrderNumber = orderNoBackup ?? 0;
// 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. The production processes are sorted,
// so the first can start the process chain again.
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.CheckRadio);
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>
/// <param name="productionProcessNo">An optional number to start the killing</param>
/// <remarks date="2024-Jul-15" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
/// <remarks date="2025-Jul-08" author="Thomas Wiedebusch">
/// - Leave preceding processes to the input process number alive.
/// </remarks>
private void InitProcesses(UInt32? productionProcessNo = null)
{
if (Processes == null)
Processes = new List<IProductionProcess>();
// Get all processes relevant for production from table which has to be sorted
var processes = _processStateDef.GetAllProductionProcesses();
foreach (var process in processes)
{
// Without defined start process all will be reset to idle
if (productionProcessNo == null)
process.IdleProcess();
// All processes from and behind this process will be killed to kep the preceding as is
else if (_processStateDef.GetProductionProcessNoOfProcess(process) >= productionProcessNo)
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>
/// <remarks date="2025-Aug-11" author="Thomas Wiedebusch">
/// - Removed cancellationToken assignment.
/// </remarks>
/// <remarks date="2025-Nov-20" author="Thomas Wiedebusch">
/// Delay timer between finish exec-parametrization and 'StoreConfiguration' as 'PERIODICLOG' doesn't
/// have 'StoreConfiguration' and needs at least 20 s to update contents before 'StoreConfiguration'
/// can be dispatched to 'SENSUSRADIO'.
/// </remarks>
/// <remarks date="2025-Nov-21" author="Thomas Wiedebusch">
/// Abort enabled during waiting loop.
/// </remarks>
/// <remarks date="2026-Feb-12" author="Thomas Wiedebusch">
/// Skip the process if already succeeded and kick off the next process if succeeded.
/// </remarks>
private 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;
}
// Skip the process if already succeeded and kick off the 'NextStateOnSuccess' but avoid some hidden states
if (processStateStruct.ProcessState != ProcessState.Error &&
processStateStruct.ProcessState != ProcessState.CreateEolProgress &&
processStateStruct.ProductionProcess.SingleProcessState == SingleProcessState.Succeeded &&
processStateStruct.NextStateOnSuccess != null)
{
_stateMachineState = (ProcessState)processStateStruct.NextStateOnSuccess;
return;
}
// Process is waiting for execution after init, this overrides the eventually abort state
processStateStruct.ProductionProcess.InitProcess();
// Wait for '_paramExeFinishedToRebootDelayTimerExceeded' before 'StoreConfiguration' to all apps
while (processStateStruct.ProcessState == ProcessState.ExecStoreConfiguration &&
_paramExeFinishedToStoreConfigDelayTimer != null &&
_paramExeFinishedToStoreConfigDelayTimer.IsEnabled &&
!_paramExeFinishedToStoreConfigDelayTimerExceeded &&
!AnyProcessRequiresAbortion())
{
var teaTime = new TimeT();
var timeSpan = teaTime.DateTimeUtc - _paramExeFinishedToStoreConfigDelayTimerStartTime;
var msg = $"{Resources.StrMsgRebootDelayTimerActive} " +
$"{timeSpan.Seconds}/{ParamExeFinishedToStoreConfigTimeS} s - {teaTime}";
OnProcessLogRequest?.Invoke(this, new ProcessLogArgs(msg));
OnProcessProgressChanged?.Invoke(this, new ProcessProgressArgs(ParamExeFinishedToStoreConfigTimeS,
ProcessProgressArgs.ProcessProgressType.MaxActualProcessSteps));
OnProcessProgressChanged?.Invoke(this, new ProcessProgressArgs(timeSpan.Seconds,
ProcessProgressArgs.ProcessProgressType.ActualProcessSteps));
Thread.Sleep(1000);
}
// 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;
// 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.OnGuiMessageDispatcher += GuiMessageDispatcher_Handler;
process.OnProcessProgressChanged += ProcessProgress_Handler;
process.OnProcessLogRequest += ProcessLog_Handler;
process.OnProcessStateChanged += ProcessStateChanged_Handler;
process.OnForceStateMachineStateChange += ForceStateMachineStateChange_Handler;
}
/// <summary>
/// Remove process events
/// </summary>
/// <param name="process"></param>
private void RemoveProcessEvents(IProductionProcess process)
{
if (process == null)
return;
process.OnGuiMessageDispatcher -= GuiMessageDispatcher_Handler;
process.OnProcessProgressChanged -= ProcessProgress_Handler;
process.OnProcessLogRequest -= ProcessLog_Handler;
process.OnProcessStateChanged -= ProcessStateChanged_Handler;
process.OnForceStateMachineStateChange -= ForceStateMachineStateChange_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>
public 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>
/// Forces the state machine to start the first production process
/// </summary>
/// <returns>true, if process state is set</returns>
/// <remarks date="2025-Jun-30" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
public Boolean StartFirstProcess()
{
var newProcessState = _processStateDef.GetStateOfFirstProcess();
return SetProcessState(newProcessState);
}
/// <summary>
/// Remove all processes from the <see cref="Processes"/> list:
/// - Signal change to GUI,
/// - Remove process events all,
/// - Set state to idle.
/// </summary>
/// <param name="productionProcessNo">An optional number to start the killing</param>
/// <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>
/// <remarks date="2025-Jul-08" author="Thomas Wiedebusch">
/// - Kill processes from the start process as all others are already finished.
/// </remarks>
private void KillProcesses(UInt32? productionProcessNo = null)
{
_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)
{
// Without defined start process all will be killed
if (productionProcessNo == null)
pp.IdleProcess();
// All processes from and behind this process will be killed to kep the preceding as is
else if (_processStateDef.GetProductionProcessNoOfProcess(pp) >= productionProcessNo)
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 ------------------------------------------------------
}
}