aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/MachineStudio/Modules/Tango.MachineStudio.RML/Properties/AssemblyInfo.cs
blob: 062f0ace5080ffd1472a98b23653a40e530054cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;

[assembly: AssemblyTitle("Tango - Machine Studio RML Module")]
[assembly: AssemblyVersion("2.0.11.1737")]

[assembly: ComVisible(false)]

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
                                     //(used if a resource is not found in the page,
                                     // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
                                              //(used if a resource is not found in the page,
                                              // app, or any theme specific resource dictionaries)
)]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tango.PMR;
using Tango.PMR.Diagnostics;
using Tango.Transport;
using Tango.Transport.Transporters;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Threading;
using Tango.PMR.Common;
using Tango.PMR.Printing;
using System.Reactive.Subjects;
using Tango.PMR.Debugging;
using Tango.Logging;
using Tango.Settings;
using System.IO;
using Tango.BL.Entities;
using Tango.PMR.Hardware;
using Google.Protobuf;
using Tango.PMR.Connection;
using Tango.BL.Enumerations;
using Tango.PMR.Stubs;
using System.Threading;
using Tango.Integration.Storage;
using Ionic.Zip;
using Tango.Core.Threading;
using Tango.PMR.IO;
using Tango.Integration.Upgrade;
using Tango.PMR.FirmwareUpgrade;
using Tango.Integration.Logging;
using Tango.Integration.JobRuns;
using Tango.FirmwareUpdateLib.WPF;
using Tango.FirmwareUpdateLib;
using Tango.Core.ExtensionMethods;
using Tango.ColorConversion;
using Tango.Integration.Emergency;
using Tango.PMR.MachineStatus;
using Newtonsoft.Json;
using Tango.PMR.Integration;
using System.Globalization;
using Tango.PMR.Power;
using Tango.PMR.ThreadLoading;
using Tango.BL.DTO;
using Tango.PMR.IFS;
using System.Runtime.CompilerServices;

namespace Tango.Integration.Operation
{
    /// <summary>
    /// Represents the Tango machine operator default implementation.
    /// </summary>
    /// <seealso cref="Tango.Transport.Transporters.BasicTransporter" />
    /// <seealso cref="Tango.Integration.Operation.IMachineOperator" />
    public class MachineOperator : BasicTransporter, IMachineOperator
    {
        public const String FIRMWARE_UPGRADE_FOLDER_NAME = "UpgradePackage";
        public const String FIRMWARE_UPGRADE_CONFIG_FILE_NAME = "package.cfg";
        public const String JOB_DESCRIPTION_FILE_NAME = "job_segments.jdf";

        public const int MAX_DISPENSER_NANOLITER = 130000000;
        public const double MAX_MIDTANK_LITERS = 1.8;
        public const double EMPTY_MIDTANK_LITERS = 0.2;
        public const double LOW_MIDTANK_LITERS = 0.3;
        public const double OVERALL_TEMPERATURE_OK = 35;
        public const double OVERALL_TEMPERATURE_WARNING = 35;
        public const double OVERALL_TEMPERATURE_ERROR = 40;

        private bool _diagnosticsSent;
        private bool _eventsSent;
        private bool _debugSent;
        private bool _machineStatusSent;
        private bool _inkFillingStatusSent;
        private bool _threadLoadingSent;
        private static RunningJobStatus _last_job_status;
        private bool _isPowerDownRequestInProgress;
        private bool _isHeadCleaningInProgress;
        private List<BL.ValueObjects.JobRunLiquidQuantity> _lastJobLiquidQuantities;
        private DateTime _diagnosticsTime;
        private MachineStatus _machineStatusBeforeJobStart;
        private Configuration _machineConfiguration;

        private DateTime _jobStartDate;
        private DateTime? _jobUploadingStartDate;
        private DateTime? _jobHeatingStartDate;
        private DateTime? _jobActualStartDate;
        private List<Event> _emulatedEvents;
        private List<BitResultComposition> _bitResults;
        private JobSpoolType _currentSpoolType;

        public static String EmbeddedLogsFolder { get; private set; }
        public static String EmbeddedLogsTag { get; private set; }
        public static SessionFileLogger SessionLogger { get; set; }
        public static String CachedJobOperationFile { get; set; }

        #region Classes

        private class RequiredLiquid
        {
            public IdsPack IdsPack { get; set; }
            public int Quantity { get; set; }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes the <see cref="MachineOperator"/> class.
        /// </summary>
        static MachineOperator()
        {
            if (EmbeddedLogManager == null)
            {
                EmbeddedLogManager = new LogManager();

                EmbeddedLogsTag = "Embedded";
                EmbeddedLogsFolder = Path.Combine(Path.GetDirectoryName(SettingsManager.Default.Folder), "Logs", Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName), "Embedded");
                Directory.CreateDirectory(EmbeddedLogsFolder);
                FileLogger fileLogger = new FileLogger(EmbeddedLogsFolder, EmbeddedLogsTag) { Enabled = true };
                EmbeddedLogManager.RegisterLogger(fileLogger);
            }

            if (SessionLogger == null)
            {
                SessionLogger = new SessionFileLogger();
                LogManager.Default.RegisterLogger(SessionLogger);
            }


            CachedJobOperationFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Twine", "Tango", "Job Resume", Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName), "CachedJobOperation.cache");
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MachineOperator"/> class.
        /// </summary>
        public MachineOperator() : base()
        {
            _bitResults = new List<BitResultComposition>();
            _emulatedEvents = new List<Event>();
            ComponentName = $"Machine Operator {_component_counter++}";
            DeviceInformation = new DeviceInformation();
            MachineEventsStateProvider = new DefaultMachineEventsStateProvider();
            JobRunsLogger = new BasicJobRunsLogger(this);
            JobRunsLogger.Start();
            EnableEventsNotification = true;
            EnableMachineStatusUpdates = true;
            EnableInkFillingStatus = true;
            EnableJobResume = true;
            LogEmbeddedDebuggingToFile = true;
            FirmwareUpgradeMode = FirmwareUpgradeModes.DFU | FirmwareUpgradeModes.TFP_PACKAGE;
            GradientGenerationConfiguration = new DefaultGradientGenerationConfiguration();
            EmergencyNotificationProvider = new UsbEmergencyNotificationProvider("COM1");
            EnableJobLiquidQuantityValidation = true;
            FailsWithAdapter = true;
            IsSpoolReplaced = true;
            ContinuousRequestTimeout = TimeSpan.FromSeconds(2);
            ResetInkFllingStatus();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MachineOperator"/> class.
        /// </summary>
        /// <param name="adapter">The transport adapter.</param>
        public MachineOperator(ITransportAdapter adapter) : this()
        {
            Adapter = adapter;
        }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when the machine <see cref="Status" /> has changed.
        /// </summary>
        public event EventHandler<MachineStatuses> StatusChanged;

        /// <summary>
        /// Occurs when there is new diagnostics data available.
        /// </summary>
        public event EventHandler<StartDiagnosticsResponse> DiagnosticsDataAvailable;

        /// <summary>
        /// Occurs when an events notification has been received from the embedded device.
        /// </summary>
        public event EventHandler<StartEventsNotificationResponse> EventsNotification;

        /// <summary>
        /// Occurs when a new debug log is available.
        /// </summary>
        public event EventHandler<StartDebugLogResponse> DebugLogAvailable;

        /// <summary>
        /// Occurs when machine embedded device status has changed.
        /// </summary>
        public event EventHandler<MachineStatus> MachineStatusChanged;

        /// <summary>
        /// Occurs when a new cartridge validation request has been received.
        /// </summary>
        public event EventHandler<CartridgeValidationEventArgs> CartridgeValidationRequestReceived;

        /// <summary>
        /// Reports about the job printing preparation progress.
        /// </summary>
        public event EventHandler<PreparingJobProgressEventArgs> PreparingJobProgress;

        /// <summary>
        /// Occurs when a printing process has started.
        /// </summary>
        public event EventHandler<PrintingEventArgs> PrintingStarted;

        /// <summary>
        /// Occurs when a printing process has completed.
        /// </summary>
        public event EventHandler<PrintingEventArgs> PrintingCompleted;

        /// <summary>
        /// Occurs when a printing process has failed.
        /// </summary>
        public event EventHandler<PrintingFailedEventArgs> PrintingFailed;

        /// <summary>
        /// Occurs when a printing process has been aborted.
        /// </summary>
        public event EventHandler<PrintingEventArgs> PrintingAborted;

        /// <summary>
        /// Occurs when a printing process has ended.
        /// </summary>
        public event EventHandler<PrintingEventArgs> PrintingEnded;

        /// <summary>
        /// Occurs when the machine operator has detected that a job is in progress after connecting to the machine.
        /// </summary>
        public event EventHandler<ResumingJobEventArgs> ResumingJob;

        /// <summary>
        /// Occurs when the machine was connected and device has reported IsAfterReset.
        /// </summary>
        public event EventHandler FirmwareStarted;

        /// <summary>
        /// Occurs when power down has started.
        /// </summary>
        public event EventHandler<PowerDownStartedEventArgs> PowerDownStarted;

        /// <summary>
        /// Occurs when the thread loading status has changed.
        /// </summary>
        public event EventHandler<StartThreadLoadingResponse> ThreadLoadingStatusChanged;

        /// <summary>
        /// Occurs when a thread loading confirmation is required.
        /// </summary>
        public event EventHandler<ThreadLoadingConfirmationRequiredEventArgs> ThreadLoadingConfirmationRequired;

        /// <summary>
        /// Occurs when thread loading has completed.
        /// </summary>
        public event EventHandler<StartThreadLoadingResponse> ThreadLoadingCompleted;

        /// <summary>
        /// Occurs when thread loading has failed.
        /// </summary>
        public event EventHandler<StartThreadLoadingResponse> ThreadLoadingFailed;

        /// <summary>
        /// Occurs when the power up sequence has started.
        /// </summary>
        public event EventHandler<StartPowerUpResponse> PowerUpStarted;

        /// <summary>
        /// Occurs when the power up sequence progress has changed.
        /// </summary>
        public event EventHandler<StartPowerUpResponse> PowerUpProgress;

        /// <summary>
        /// Occurs when power up sequence has completed successfully.
        /// </summary>
        public event EventHandler<StartPowerUpResponse> PowerUpCompleted;

        /// <summary>
        /// Occurs when power up sequence has failed.
        /// </summary>
        public event EventHandler<StartPowerUpResponse> PowerUpFailed;

        /// <summary>
        /// Occurs when power up sequence has ended. Could be due to no response to the request!
        /// </summary>
        public event EventHandler PowerUpEnded;

        /// <summary>
        /// Occurs when a head cleaning job has ended.
        /// </summary>
        public event EventHandler<HeadCleaningEndedEventArgs> HeadCleaningEnded;

        /// <summary>
        /// Occurs when the ink filling status has changed.
        /// </summary>
        public event EventHandler<InkFillingStatusChangedEventArgs> InkFillingStatusChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether to create a new designated session log file each successful connection.
        /// This log file will contain standard logs that have occurred between the last connection and disconnection states.
        /// </summary>
        public static bool EnableSessionLogFile
        {
            get { return SessionLogger.Enabled; }
            set
            {
                SessionLogger.Enabled = value;
            }
        }

        /// <summary>
        /// Gets or sets the job handling mode.
        /// </summary>
        public JobHandlerModes JobHandlingMode { get; set; }

        /// <summary>
        /// Gets or sets the job upload strategy.
        /// </summary>
        public JobUploadStrategy JobUploadStrategy { get; set; }

        /// <summary>
        /// Gets or sets the job number of units duplication method.
        /// </summary>
        public JobUnitsMethods JobUnitsMethod { get; set; }

        /// <summary>
        /// Gets or sets the way of calculating how much liquid was spent during the job.
        /// </summary>
        public JobLiquidQuantityCalculationMode JobLiquidQuantityCalculationMode { get; set; }

        private MachineStatuses _status;
        /// <summary>
        /// Gets the current machine status.
        /// </summary>
        public MachineStatuses Status
        {
            get { return _status; }
            protected set
            {
                if (_status != value)
                {
                    _status = value;
                    RaisePropertyChangedAuto();
                    OnStatusChanged(value);
                    RaisePropertyChanged(nameof(IsPrinting));
                    RaisePropertyChanged(nameof(CanPrint));
                    RaisePropertyChanged(nameof(IsConnected));
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the machine is connected and status is not disconnected.
        /// </summary>
        public bool IsConnected
        {
            get { return State == TransportComponentState.Connected && Status != MachineStatuses.Disconnected; }
        }

        private MachineStatus _machineStatus;
        /// <summary>
        /// Gets the machine embedded device status.
        /// </summary>
        public MachineStatus MachineStatus
        {
            get { return _machineStatus; }
            private set { _machineStatus = value; RaisePropertyChangedAuto(); }
        }

        private InkFillingStatus _inkFillingStatus;
        /// <summary>
        /// Gets or sets the ink filling status.
        /// </summary>
        public InkFillingStatus InkFillingStatus
        {
            get { return _inkFillingStatus; }
            private set { _inkFillingStatus = value; RaisePropertyChangedAuto(); }
        }

        private StartThreadLoadingResponse _threadLoadingStatus;
        /// <summary>
        /// Gets the current thread loading status.
        /// </summary>
        public StartThreadLoadingResponse ThreadLoadingStatus
        {
            get { return _threadLoadingStatus; }
            private set { _threadLoadingStatus = value; RaisePropertyChangedAuto(); }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to enable liquid quantity validation before starting the job.
        /// The validation is done using the reported <see cref="MachineStatus" />.
        /// </summary>
        public bool EnableJobLiquidQuantityValidation { get; set; }

        /// <summary>
        /// Gets or sets the firmware upgrade mode.
        /// </summary>
        public FirmwareUpgradeModes FirmwareUpgradeMode { get; set; }

        /// <summary>
        /// Gets a value indicating whether this instance is printing.
        /// </summary>
        public bool IsPrinting
        {
            get
            {
                return Status == MachineStatuses.Printing || Status == MachineStatuses.GettingReady;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance can print.
        /// </summary>
        public bool CanPrint
        {
            get
            {
                return Status == MachineStatuses.ReadyToDye || Status == MachineStatuses.PowerUp || Status == MachineStatuses.Standby;
            }
        }

        private Job _runningJob;
        /// <summary>
        /// Gets the running job.
        /// </summary>
        public Job RunningJob
        {
            get { return _runningJob; }
            set { _runningJob = value; RaisePropertyChangedAuto(); }
        }

        private RunningJobStatus _runningJobStatus;
        /// <summary>
        /// Gets the running job status.
        /// </summary>
        public RunningJobStatus RunningJobStatus
        {
            get { return _runningJobStatus; }
            set { _runningJobStatus = value; RaisePropertyChangedAuto(); }
        }

        /// <summary>
        /// Gets the embedded device log manager.
        /// </summary>
        public static LogManager EmbeddedLogManager { get; private set; }

        private bool _enableDiagnostics;
        /// <summary>
        /// Gets or sets a value indicating whether direct the embedded device to send diagnostics messages.
        /// </summary>
        public bool EnableDiagnostics
        {
            get { return _enableDiagnostics; }
            set
            {
                if (_enableDiagnostics != value)
                {
                    _enableDiagnostics = value;
                    RaisePropertyChangedAuto();
                    OnEnableDiagnosticsChanged(value);
                }
            }
        }

        private bool _enableEventsNotification;
        /// <summary>
        /// Gets or sets a value indicating whether direct the embedded device to send events notification messages.
        /// </summary>
        public bool EnableEventsNotification
        {
            get { return _enableEventsNotification; }
            set
            {
                if (_enableEventsNotification != value)
                {
                    _enableEventsNotification = value;
                    RaisePropertyChangedAuto();
                    OnEnableEventsNotification(value);
                }
            }
        }

        private bool _enableEmbeddedDebugging;
        /// <summary>
        /// Gets or sets a value indicating whether to allow incoming debugging messages.
        /// </summary>
        /// <exception cref="System.NotImplementedException">
        /// </exception>
        public bool EnableEmbeddedDebugging
        {
            get
            {
                return _enableEmbeddedDebugging;
            }
            set
            {
                if (_enableEmbeddedDebugging != value)
                {
                    _enableEmbeddedDebugging = value;
                    RaisePropertyChangedAuto();
                    OnEnableEmbeddedDebuggingChanged(value);
                }
            }
        }

        private bool _enableMachineStatusUpdates;
        /// <summary>
        /// Gets or sets a value indicating whether to direct the embedded device to update about status changes.
        /// </summary>
        public bool EnableMachineStatusUpdates
        {
            get { return _enableMachineStatusUpdates; }
            set
            {
                if (_enableMachineStatusUpdates != value)
                {
                    _enableMachineStatusUpdates = value;
                    RaisePropertyChangedAuto();
                    OnEnableMachineStatusUpdatesChanged(value);
                }
            }
        }

        private bool _enableInkFillingStatus;
        public bool EnableInkFillingStatus
        {
            get { return _enableInkFillingStatus; }
            set
            {
                if (_enableInkFillingStatus != value)
                {
                    _enableInkFillingStatus = value;
                    RaisePropertyChangedAuto();
                    OnEnableInkFillingStatus(value);
                }
            }
        }

        private bool _enableAutomaticThreadLoading;
        /// <summary>
        /// Gets or sets a value indicating whether to enable automatic thread loading support.
        /// </summary>
        public bool EnableAutomaticThreadLoading
        {
            get { return _enableAutomaticThreadLoading; }
            set
            {
                _enableAutomaticThreadLoading = value;
                RaisePropertyChangedAuto();
                OnEnableAutomaticThreadLoadingChanged(value);
            }
        }

        private bool _enableJobResume;
        /// <summary>
        /// Gets or sets a value indicating whether to check whether a job is in progress after connection was successful.
        /// </summary>
        public bool EnableJobResume
        {
            get
            {
                return _enableJobResume;
            }
            set
            {
                _enableJobResume = value; RaisePropertyChangedAuto();
            }
        }

        private bool _logEmbeddedDebuggingToFile;
        /// <summary>
        /// Gets or sets a value indicating whether to automatically save incoming log data from the embedded device.
        /// </summary>
        public bool LogEmbeddedDebuggingToFile
        {
            get { return _logEmbeddedDebuggingToFile; }
            set
            {
                _logEmbeddedDebuggingToFile = value; RaisePropertyChangedAuto();
            }
        }

        private bool _enablePowerUpSequence;
        /// <summary>
        /// Gets or sets a value indicating whether to enable the power sequence tracking.
        /// </summary>
        public bool EnablePowerUpSequence
        {
            get { return _enablePowerUpSequence; }
            set { _enablePowerUpSequence = value; RaisePropertyChangedAuto(); }
        }

        /// <summary>
        /// Gets or sets the machine events state provider used to get notifications about current machine events and errors.
        /// </summary>
        public IMachineEventsStateProvider MachineEventsStateProvider { get; set; }

        /// <summary>
        /// Gets or sets the job runs logger.
        /// </summary>
        public IJobRunsLogger JobRunsLogger { get; set; }

        /// <summary>
        /// Gets the last process parameters table sent to the embedded device.
        /// </summary>
        public ProcessParametersTable CurrentProcessParameters { get; private set; }

        /// <summary>
        /// Gets the last hardware configuration sent to the embedded device.
        /// </summary>
        public HardwareConfiguration CurrentHardwareConfiguration { get; private set; }

        private DeviceInformation _deviceInformation;
        /// <summary>
        /// Gets or sets the embedded device information.
        /// </summary>
        public DeviceInformation DeviceInformation
        {
            get { return _deviceInformation; }
            set { _deviceInformation = value; RaisePropertyChangedAuto(); }
        }

        private IGradientGenerationConfiguration _gradientGenerationConfiguration;
        /// <summary>
        /// Gets or sets the gradients generation configuration.
        /// </summary>
        public IGradientGenerationConfiguration GradientGenerationConfiguration
        {
            get { return _gradientGenerationConfiguration; }
            set { _gradientGenerationConfiguration = value; RaisePropertyChangedAuto(); }
        }

        /// <summary>
        /// Gets or sets the emergency notification provider.
        /// </summary>
        public IEmergencyNotificationProvider EmergencyNotificationProvider { get; set; }

        /// <summary>
        /// Gets or sets the general continuous request timeout.
        /// </summary>
        public TimeSpan ContinuousRequestTimeout { get; set; }

        /// <summary>
        /// Gets a value indicating whether the spool was replaced after the last job.
        /// </summary>
        public bool IsSpoolReplaced { get; private set; }

        #endregion

        #region Virtual Methods

        /// <summary>
        /// Called when the enable diagnostics property has been changed
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual async void OnEnableDiagnosticsChanged(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_diagnosticsSent)
            {
                var request = new StartDiagnosticsRequest();

                bool responseLogged = false;
                _diagnosticsSent = true;

                LogManager.Log($"Sending '{nameof(StartDiagnosticsRequest)}'...");

                SendContinuousRequest<StartDiagnosticsRequest, StartDiagnosticsResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = false }).ObserveOn(new NewThreadScheduler()).Subscribe(
                    (response) =>
                    {
                        if (!responseLogged)
                        {
                            _diagnosticsTime = DateTime.Now;
                            responseLogged = true;
                        }
                        else
                        {
                            _diagnosticsTime = _diagnosticsTime.Add(TimeSpan.FromMilliseconds(response.Message.ElapsedMilli));
                        }

                        response.Message.DateTime = _diagnosticsTime.ToString("MM/dd/yyyy HH:mm:ss.fff");

                        OnDiagnosticsDataAvailable(response);
                    },
                    (ex) =>
                    {
                        _diagnosticsSent = false;
                    },
                    () =>
                    {
                        _diagnosticsSent = false;
                        LogManager.Log("Diagnostics response completed!?", LogCategory.Warning);
                    });
            }
            else if (_diagnosticsSent)
            {
                _diagnosticsSent = false;

                if (State == TransportComponentState.Connected)
                {
                    var req = new StopDiagnosticsRequest();

                    try
                    {
                        var res = await SendRequest<StopDiagnosticsRequest, StopDiagnosticsResponse>(req, new TransportRequestConfig() { ShouldLog = true });
                    }
                    catch { }
                }
            }
        }

        /// <summary>
        /// Called when the enable events property has been changed.
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual async void OnEnableEventsNotification(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_eventsSent)
            {
                var request = new StartEventsNotificationRequest();

                bool responseLogged = false;
                _eventsSent = true;

                SendContinuousRequest<StartEventsNotificationRequest, StartEventsNotificationResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe(
                    (response) =>
                    {
                        OnEventsNotification(response);

                        if (!responseLogged)
                        {
                            responseLogged = true;
                        }
                    },
                    (ex) =>
                    {
                        _eventsSent = false;
                    },
                    () =>
                    {
                        _eventsSent = false;
                        LogManager.Log("Events Notification response completed!?", LogCategory.Warning);
                    });
            }
            else if (_eventsSent)
            {
                _eventsSent = false;

                if (State == TransportComponentState.Connected)
                {
                    var req = new StopEventsNotificationRequest();

                    try
                    {
                        var res = await SendRequest<StopEventsNotificationRequest, StopEventsNotificationResponse>(req, new TransportRequestConfig() { ShouldLog = true });
                    }
                    catch { }
                }
            }
        }

        /// <summary>
        /// Called when the enable embedded debugging has been changed
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual async void OnEnableEmbeddedDebuggingChanged(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_debugSent)
            {
                var request = new StartDebugLogRequest();

                bool responseLogged = false;
                _debugSent = true;

                SendContinuousRequest<StartDebugLogRequest, StartDebugLogResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = true }).ObserveOn(new NewThreadScheduler())
                    .Subscribe
                    (
                    (response) =>
                    {
                        if (!responseLogged)
                        {
                            responseLogged = true;
                        }

                        OnDebugLogAvailable(response);
                    },
                    (ex) =>
                    {
                        _debugSent = false;
                    },
                    () =>
                    {
                        _debugSent = false;
                    });
            }
            else if (_debugSent)
            {
                _debugSent = false;

                if (State == TransportComponentState.Connected)
                {
                    var req = new StopDebugLogRequest();

                    try
                    {
                        var res = await SendRequest<StopDebugLogRequest, StopDebugLogResponse>(req, new TransportRequestConfig() { ShouldLog = true });
                    }
                    catch { }
                }
            }
        }

        /// <summary>
        /// Called when the enable machine status updates has been changed.
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual async void OnEnableMachineStatusUpdatesChanged(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_machineStatusSent)
            {
                var request = new StartMachineStatusUpdateRequest();

                bool responseLogged = false;
                _machineStatusSent = true;

                SendContinuousRequest<StartMachineStatusUpdateRequest, StartMachineStatusUpdateResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe(
                    (response) =>
                    {
                        OnMachineStatusChanged(response);

                        if (!responseLogged)
                        {
                            responseLogged = true;
                        }
                    },
                    (ex) =>
                    {
                        _machineStatusSent = false;
                    },
                    () =>
                    {
                        _machineStatusSent = false;
                        LogManager.Log("Machine status update response completed!?", LogCategory.Warning);
                    });
            }
            else if (_machineStatusSent)
            {
                _machineStatusSent = false;

                if (State == TransportComponentState.Connected)
                {
                    var req = new StopMachineStatusUpdateRequest();

                    try
                    {
                        var res = await SendRequest<StopMachineStatusUpdateRequest, StopMachineStatusUpdateResponse>(req, new TransportRequestConfig() { ShouldLog = true });
                    }
                    catch { }
                }
            }
        }

        /// <summary>
        /// Called when the enable ink filling status has been changed.
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual void OnEnableInkFillingStatus(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_inkFillingStatusSent)
            {
                var request = new StartInkFillingStatusRequest();

                bool responseLogged = false;
                _inkFillingStatusSent = true;

                SendContinuousRequest<StartInkFillingStatusRequest, StartInkFillingStatusResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe(
                    (response) =>
                    {
                        OnInkFillingStatusChanged(response);

                        if (!responseLogged)
                        {
                            responseLogged = true;
                        }
                    },
                    (ex) =>
                    {
                        _inkFillingStatusSent = false;
                    },
                    () =>
                    {
                        _inkFillingStatusSent = false;
                        LogManager.Log("Ink filling status response completed!?", LogCategory.Warning);
                    });
            }
            else if (_inkFillingStatusSent)
            {
                _inkFillingStatusSent = false;
            }
        }

        /// <summary>
        /// Called when the enable automatic thread loading has been changed
        /// </summary>
        /// <param name="value">if set to <c>true</c> [value].</param>
        protected virtual async void OnEnableAutomaticThreadLoadingChanged(bool value)
        {
            if (value && State == TransportComponentState.Connected && !_threadLoadingSent)
            {
                var request = new StartThreadLoadingRequest();

                bool responseLogged = false;
                _threadLoadingSent = true;

                SendContinuousRequest<StartThreadLoadingRequest, StartThreadLoadingResponse>(request, new TransportContinuousRequestConfig() { ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe(
                    (response) =>
                    {
                        OnThreadLoadingStatusChanged(response);

                        if (!responseLogged)
                        {
                            responseLogged = true;
                        }
                    },
                    (ex) =>
                    {
                        _threadLoadingSent = false;
                    },
                    () =>
                    {
                        _threadLoadingSent = false;
                        LogManager.Log("Thread loading response completed!?", LogCategory.Warning);
                    });
            }
            else if (_threadLoadingSent)
            {
                _threadLoadingSent = false;

                if (State == TransportComponentState.Connected)
                {
                    var req = new StopThreadLoadingRequest();

                    try
                    {
                        var res = await SendRequest<StopThreadLoadingRequest, StopThreadLoadingResponse>(req, new TransportRequestConfig() { ShouldLog = true });
                    }
                    catch { }
                }
            }
        }

        /// <summary>
        /// Invokes the <see cref="DiagnosticsDataAvailable"/> event.
        /// </summary>
        /// <param name="data">The sensors data.</param>
        protected virtual void OnDiagnosticsDataAvailable(StartDiagnosticsResponse data)
        {
            DiagnosticsDataAvailable?.Invoke(this, data);
        }

        /// <summary>
        /// Called when events notification message has been received.
        /// </summary>
        /// <param name="response">The response.</param>
        protected virtual void OnEventsNotification(StartEventsNotificationResponse response)
        {
            if (MachineEventsStateProvider != null)
            {
                var events = response.Events;

                foreach (var emulated in _emulatedEvents)
                {
                    if (!events.Any(x => x.Type == emulated.Type))
                    {
                        events.Add(emulated);
                    }
                }

                MachineEventsStateProvider.ApplyEvents(events);
            }

            EventsNotification?.Invoke(this, response);
        }

        /// <summary>
        /// Invokes the <see cref="DebugLogAvailable"/> event.
        /// </summary>
        /// <param name="data">The sensors data.</param>
        protected virtual void OnDebugLogAvailable(StartDebugLogResponse data)
        {
            if (LogEmbeddedDebuggingToFile && EmbeddedLogManager != null)
            {
                EmbeddedLogManager.Log(new EmbeddedLogItem(data));
            }

            DebugLogAvailable?.Invoke(this, data);
        }

        /// <summary>
        /// Called when the machine status has been updated.
        /// </summary>
        /// <param name="response">The response.</param>
        protected virtual void OnMachineStatusChanged(StartMachineStatusUpdateResponse response)
        {
            if (response.Status == null) return;

            bool changed = (MachineStatus == null || response.Status.State != MachineStatus.State);

            MachineStatus = response.Status;
            MachineStatusChanged?.Invoke(this, MachineStatus);

            if (changed)
            {
                OnMachineStateChanged(MachineStatus.State);
            }

            if (MachineStatus.SpoolState == SpoolState.Absent)
            {
                IsSpoolReplaced = true;
            }
        }

        /// <summary>
        /// Called when ink filling status has been changed.
        /// </summary>
        /// <param name="response">The response.</param>
        protected virtual void OnInkFillingStatusChanged(StartInkFillingStatusResponse response)
        {
            if (response.Status == null || response.Status.CartridgesStatuses == null || response.Status.CartridgesStatuses.Count == 0) return;

            int index = -1;
            bool raiseChange = false;

            foreach (var remoteCartridge in response.Status.CartridgesStatuses)
            {
                index++;

                if (remoteCartridge.Cartridge == null)
                {
                    LogManager.Log($"Remote cartridge arrived with null cartridge at position [{index}] and will be ignored.", LogCategory.Error);
                    continue;
                }

                var localCartridge = InkFillingStatus.CartridgesStatuses.SingleOrDefault(x => x.Cartridge.Index == remoteCartridge.Cartridge.Index && x.Cartridge.Slot == remoteCartridge.Cartridge.Slot);

                if (localCartridge != null)
                {
                    if (localCartridge.State != remoteCartridge.State)
                    {
                        localCartridge.State = remoteCartridge.State;
                        LogManager.Log($"{localCartridge.Cartridge.Slot} Cartridge '{localCartridge.Cartridge.Index}' state changed: '{localCartridge.State}' => '{remoteCartridge.State}'.");
                    }

                    if (remoteCartridge.Cartridge.Tag != null)
                    {
                        LogManager.Log($"{localCartridge.Cartridge.Slot} Cartridge '{localCartridge.Cartridge.Index}' Tag arrived:\n{remoteCartridge.Cartridge.Tag.ToJsonString()}");
                    }

                    localCartridge.Message = remoteCartridge.Message;
                    localCartridge.ProgressPercentage = remoteCartridge.ProgressPercentage;

                    raiseChange = true;
                }
                else
                {
                    LogManager.Log($"Could not locate local cartridge with slot '{remoteCartridge.Cartridge.Slot}' and index '{remoteCartridge.Cartridge.Index}'.", LogCategory.Error);
                }
            }

            if (raiseChange)
            {
                RaisePropertyChanged(nameof(InkFillingStatus));
                InkFillingStatusChanged?.Invoke(this, new InkFillingStatusChangedEventArgs() { Status = InkFillingStatus });
            }
        }

        /// <summary>
        /// Called when the machine state has been changed.
        /// </summary>
        /// <param name="state">The state.</param>
        protected async virtual void OnMachineStateChanged(MachineState state)
        {
            LogManager.Log($"Machine State Changed: {state}.");

            if (IsPrinting)
            {
                LogManager.Log($"Machine state change will not affect the machine operator status as it is now in a '{Status}' status.", LogCategory.Warning);
                return;
            }

            switch (state)
            {
                case MachineState.PowerUp:
                    UpdateStatus(MachineStatuses.PowerUp);
                    break;
                //case MachineState.PreparingJob:
                //    Status = MachineStatuses.GettingReady;
                //    break;
                case MachineState.Ready:
                    UpdateStatus(MachineStatuses.ReadyToDye);
                    break;
                case MachineState.Sleep:
                    UpdateStatus(MachineStatuses.Standby);
                    break;
                case MachineState.PowerOff:
                    UpdateStatus(MachineStatuses.ShuttingDown);
                    if (!_isPowerDownRequestInProgress)
                    {
                        try
                        {
                            await PowerDown();
                        }
                        catch { }
                    }
                    break;
                case MachineState.Error:
                    //Status = MachineStatuses.Error;
                    break;
            }
        }

        /// <summary>
        /// Called when the thread loading status has been changed.
        /// </summary>
        /// <param name="response">The response.</param>
        protected virtual void OnThreadLoadingStatusChanged(StartThreadLoadingResponse response)
        {
            bool changed = (ThreadLoadingStatus == null || response.State != ThreadLoadingStatus.State || response.ErrorReason != ThreadLoadingStatus.ErrorReason);

            if (changed)
            {
                ThreadLoadingStatus = response;
                ThreadLoadingStatusChanged?.Invoke(this, response);

                LogManager.Log($"Thread Loading Status Changed: {ThreadLoadingStatus.State}.");

                switch (ThreadLoadingStatus.State)
                {
                    case ThreadLoadingState.ReadyForLoading:

                        LogManager.Log("Thread loading is ready for loading. Invoking confirmation event...");

                        ThreadLoadingConfirmationRequired?.Invoke(this, new ThreadLoadingConfirmationRequiredEventArgs((processTable) =>
                        {
                            //Confirm Action
                            try
                            {
                                var process = processTable.ToProcessParametersPMR();
                                LogManager.Log($"Thread loading confirmation received with process parameters:\n{process.ToJsonString()}");
                                LogManager.Log("Sending continue thread loading request...");
                                var r = SendRequest<ContinueThreadLoadingRequest, ContinueThreadLoadingResponse>(new ContinueThreadLoadingRequest()
                                {
                                    ProcessParameters = process,
                                }, new TransportRequestConfig() { ShouldLog = true }).Result;
                            }
                            catch (Exception ex)
                            {
                                LogManager.Log(ex, "Error confirming thread loading sequence.");
                            }
                        })
                        {
                            Status = ThreadLoadingStatus,
                        });
                        break;
                    case ThreadLoadingState.Completed:
                        ThreadLoadingCompleted?.Invoke(this, ThreadLoadingStatus);
                        break;
                    case ThreadLoadingState.FinalizationError:
                    case ThreadLoadingState.PreparationError:
                        ThreadLoadingFailed?.Invoke(this, ThreadLoadingStatus);
                        break;
                }
            }
        }

        /// <summary>
        /// Called when a new request has been received.
        /// </summary>
        /// <param name="container">The request.</param>
        protected override void OnRequestReceived(RequestReceivedEventArgs e)
        {
            base.OnRequestReceived(e);

            if (e.Handled) return;

            var container = e.Container;

            if (container.Type == MessageType.CartridgeValidationRequest)
            {
                e.Handled = true;
                OnCartridgeValidationRequestReceived(container.Token, MessageFactory.ExtractMessageFromContainer<CartridgeValidationRequest>(container));
            }
            else if (container.Type == MessageType.UpdateStatusRequest)
            {
                e.Handled = true;
                OnUpdateStatusRequestReceived(container.Token, MessageFactory.ExtractMessageFromContainer<UpdateStatusRequest>(container));
            }
        }

        /// <summary>
        /// Called when the machine status has been changed
        /// </summary>
        /// <param name="status">The status.</param>
        protected virtual void OnStatusChanged(MachineStatuses status)
        {
            StatusChanged?.Invoke(this, status);
        }

        /// <summary>
        /// Called when the cartridge validation request has been received.
        /// </summary>
        /// <param name="request">The request.</param>
        protected virtual void OnCartridgeValidationRequestReceived(String token, CartridgeValidationRequest request)
        {
            if (request.Action == CartridgeAction.Inserted)
            {
                CartridgeValidationEventArgs e = new CartridgeValidationEventArgs(request, (index) =>
                {
                    //Approve
                    SendResponse<CartridgeValidationResponse>(new CartridgeValidationResponse()
                    {
                        IsValid = true,
                        Index = index,
                    }, token).Wait();

                }, () =>
                {
                    //Decline
                    SendResponse<CartridgeValidationResponse>(new CartridgeValidationResponse()
                    {

                    }, token).Wait();

                });

                CartridgeValidationRequestReceived?.Invoke(this, e);
            }
        }

        /// <summary>
        /// Called when the update status request has been received.
        /// </summary>
        /// <param name="token">The token.</param>
        /// <param name="request">The update status request.</param>
        protected virtual void OnUpdateStatusRequestReceived(string token, UpdateStatusRequest request)
        {
            try
            {
                UpdateStatus((MachineStatuses)request.Status);
            }
            catch (Exception ex)
            {
                LogManager.Log(ex);
            }

            try
            {
                SendResponse<UpdateStatusResponse>(new UpdateStatusResponse(), token);
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, "Error sending UpdateStatus response.");
            }
        }

        /// <summary>
        /// Called when the printing has been started.
        /// </summary>
        /// <param name="handler">The handler.</param>
        /// <param name="job">The job.</param>
        protected virtual void OnPrintingStarted(JobHandler handler, Job job, bool isResumed = false)
        {
            LogManager.Log("Raising printing started event...");

            PrintingStarted?.Invoke(this, new PrintingEventArgs(handler, job)
            {
                StartDate = _jobStartDate,
                IsResumed = isResumed
            });
        }

        /// <summary>
        /// Called when the printing has been completed.
        /// </summary>
        /// <param name="handler">The handler.</param>
        /// <param name="job">The job.</param>
        protected virtual void OnPrintingCompleted(JobHandler handler, Job job)
        {
            LogManager.Log("Raising printing completed event...");

            PrintingCompleted?.Invoke(this, new PrintingEventArgs(handler, job)
            {
                LiquidQuantities = _lastJobLiquidQuantities.ToList(),
                StartDate = _jobStartDate,
                UploadingStartTime = _jobUploadingStartDate,
                HeatingStartTime = _jobHeatingStartDate,
                ActualStartTime = _jobActualStartDate,
            });

            OnPrintingEnded(handler, job);
        }

        /// <summary>
        /// Called when the printing has been failed.
        /// </summary>
        /// <param name="handler">The handler.</param>
        /// <param name="job">The job.</param>
        /// <param name="exception">The exception.</param>
        protected virtual void OnPrintingFailed(JobHandler handler, Job job, Exception exception)
        {
            LogManager.Log("Raising printing failed event...");

            PrintingFailed?.Invoke(this, new PrintingFailedEventArgs(handler, job, exception)
            {
                LiquidQuantities = _lastJobLiquidQuantities.ToList(),
                StartDate = _jobStartDate,
                UploadingStartTime = _jobUploadingStartDate,
                HeatingStartTime = _jobHeatingStartDate,
                ActualStartTime = _jobActualStartDate,
            });
            OnPrintingEnded(handler, job);
        }

        /// <summary>
        /// Called when the printing has been aborted.
        /// </summary>
        /// <param name="handler">The handler.</param>
        /// <param name="job">The job.</param>
        protected virtual void OnPrintingAborted(JobHandler handler, Job job)
        {
            LogManager.Log("Raising printing aborted event...");

            PrintingAborted?.Invoke(this, new PrintingEventArgs(handler, job)
            {
                LiquidQuantities = _lastJobLiquidQuantities.ToList(),
                StartDate = _jobStartDate,
                UploadingStartTime = _jobUploadingStartDate,
                HeatingStartTime = _jobHeatingStartDate,
                ActualStartTime = _jobActualStartDate,
            });
            OnPrintingEnded(handler, job);
        }

        /// <summary>
        /// Called when the printing has been ended.
        /// </summary>
        /// <param name="handler">The handler.</param>
        /// <param name="job">The job.</param>
        protected virtual void OnPrintingEnded(JobHandler handler, Job job)
        {
            IsSpoolReplaced = false;

            LogManager.Log("Raising printing ended event...");

            PrintingEnded?.Invoke(this, new PrintingEventArgs(handler, job)
            {
                LiquidQuantities = _lastJobLiquidQuantities.ToList(),
                StartDate = _jobStartDate,
                UploadingStartTime = _jobUploadingStartDate,
                HeatingStartTime = _jobHeatingStartDate,
                ActualStartTime = _jobActualStartDate,
            });
        }

        protected virtual void OnHeadCleaningEnded(HeadCleaningHandler handler, JobRunStatus status)
        {
            SaveLastJobLiquidQuantities(null, null, null, null);

            HeadCleaningEnded?.Invoke(this, new HeadCleaningEndedEventArgs()
            {
                StartDate = _jobStartDate,
                Length = handler.Status.Total,
                EndPosition = handler.Status.Progress,
                Status = status,
                LiquidQuantities = _lastJobLiquidQuantities.ToList(),
            });
        }

        #endregion

        #region Override Methods

        /// <summary>
        /// Called when the component state has changed.
        /// </summary>
        /// <param name="state">The state.</param>
        protected override void OnStateChanged(TransportComponentState state)
        {
            base.OnStateChanged(state);

            if (state != TransportComponentState.Connected)
            {
                _diagnosticsSent = false;
                _debugSent = false;
                _eventsSent = false;
                _machineStatusSent = false;

                if (Status != MachineStatuses.Disconnected)
                {
                    UpdateStatus(MachineStatuses.Disconnected);
                    ResetEvents();
                    ResetInkFllingStatus();
                }
            }
        }

        private void ResetEvents()
        {
            if (MachineEventsStateProvider != null)
            {
                LogManager.Log("Resetting active events...");
                _emulatedEvents.Clear();
                MachineEventsStateProvider.Reset();
            }
        }

        /// <summary>
        /// Disconnects the machine operator and the underlying transporter.
        /// </summary>
        /// <returns></returns>
        public async override Task Disconnect()
        {
            if (Status == MachineStatuses.Upgrading) return;

            UpdateStatus(MachineStatuses.Disconnected);

            if (MachineStatus != null)
            {
                MachineStatus.State = MachineState.Ready;
            }

            SessionLogger.EndSession();

            if (State == TransportComponentState.Connected)
            {
                DisconnectRequest request = new DisconnectRequest();

                try
                {
                    var response = await SendRequest<DisconnectRequest, DisconnectResponse>(request, new TransportRequestConfig() { ShouldLog = true });

                    UpdateStatus(MachineStatuses.Disconnected);
                }
                catch { }
            }

            ResetEvents();
            ResetInkFllingStatus();

            await base.Disconnect();
        }

        /// <summary>
        /// Connects the transport component.
        /// </summary>
        /// <returns></returns>
        public async override Task Connect()
        {
            var keep_alive = UseKeepAlive;
            UseKeepAlive = false;

            if (Status != MachineStatuses.Upgrading)
            {
                await base.Connect();
            }

            if (State == TransportComponentState.Connected)
            {
                ConnectRequest request = new ConnectRequest()
                {
                    Password = "1234",
                    UnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    SpoolType = _currentSpoolType
                };

                try
                {
                    var response = await SendRequest<ConnectRequest, ConnectResponse>(request, new TransportRequestConfig() { ShouldLog = true });

                    SessionLogger.CreateSession();

                    _isPowerDownRequestInProgress = false;

                    DeviceInformation = response.Message.DeviceInformation;

                    if (Status != MachineStatuses.Upgrading)
                    {
                        UpdateStatus(MachineStatuses.ReadyToDye);
                    }

                    _diagnosticsSent = false;
                    _eventsSent = false;
                    _debugSent = false;
                    _machineStatusSent = false;

                    _bitResults = null;

                    OnEnableDiagnosticsChanged(EnableDiagnostics);
                    OnEnableEmbeddedDebuggingChanged(EnableEmbeddedDebugging);
                    OnEnableEventsNotification(EnableEventsNotification);
                    OnEnableMachineStatusUpdatesChanged(EnableMachineStatusUpdates);
                    OnEnableAutomaticThreadLoadingChanged(EnableAutomaticThreadLoading);
                    OnEnableInkFillingStatus(EnableInkFillingStatus);

                    if (EnablePowerUpSequence)
                    {
                        TrackPowerUpSequence();
                    }

                    if (EnableJobResume)
                    {
                        ResumeJob();
                    }

                    if (response.Message.IsAfterReset)
                    {
                        FirmwareStarted?.Invoke(this, new EventArgs());
                    }
                }
                catch (Exception ex)
                {
                    SessionLogger.EndSession();
                    await base.Disconnect();
                    throw ex;
                }
                finally
                {
                    UseKeepAlive = keep_alive;
                }
            }
        }

        #endregion

        #region Private Methods

        private void UpdateStatus(MachineStatuses status, [CallerMemberName] string caller = null, [CallerFilePath] string file = null, [CallerLineNumber] int lineNumber = 0)
        {
            if (Status != status)
            {
                Status = status;
                LogManager.Log($"Machine operator status changed: {status}", caller, file, lineNumber);
            }
        }

        private void ResetInkFllingStatus()
        {
            if (InkFillingStatus == null)
            {
                var status = new InkFillingStatus();

                for (int i = 0; i < 8; i++)
                {
                    status.CartridgesStatuses.Add(new CartridgeStatus()
                    {
                        Cartridge = new Cartridge()
                        {
                            Index = i,
                            Slot = CartridgeSlot.Ink,
                        },
                        State = CartridgeState.Absent
                    });
                }

                status.CartridgesStatuses.Add(new CartridgeStatus()
                {
                    Cartridge = new Cartridge() { Index = 0, Slot = CartridgeSlot.WasteMiddle },
                    State = CartridgeState.Absent
                });

                status.CartridgesStatuses.Add(new CartridgeStatus()
                {
                    Cartridge = new Cartridge() { Index = 1, Slot = CartridgeSlot.WasteLower },
                    State = CartridgeState.Absent
                });

                InkFillingStatus = status;
            }
            else
            {
                foreach (var cartridge in InkFillingStatus.CartridgesStatuses)
                {
                    cartridge.ProgressPercentage = 0;
                    cartridge.Message = String.Empty;
                    cartridge.State = CartridgeState.Absent;
                }
            }

            InkFillingStatusChanged?.Invoke(this, new InkFillingStatusChangedEventArgs() { Status = InkFillingStatus });
        }

        private void SaveCachedJobOperation(Job job)
        {
            try
            {
                LogManager.Log("Caching current job operation...");
                CachedJobOperation cache = new CachedJobOperation();
                cache.JobDTO = JobDTO.FromObservable(job);
                cache.MachineStatus = MachineStatus;
                cache.ProcessParametersDTO = ProcessParametersTableDTO.FromObservable(CurrentProcessParameters);
                cache.MachineConfigurationDTO = ConfigurationDTO.FromObservable(job.Machine.Configuration);
                var json = JsonConvert.SerializeObject(cache);
                Directory.CreateDirectory(Path.GetDirectoryName(CachedJobOperationFile));
                File.WriteAllText(CachedJobOperationFile, json);
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, "Error caching job operation for job resume.");
            }
        }

        private CachedJobOperation LoadCachedJobOperation()
        {
            try
            {
                LogManager.Log("Loading last cached job operation...");
                String json = File.ReadAllText(CachedJobOperationFile);
                CachedJobOperation cache = JsonConvert.DeserializeObject<CachedJobOperation>(json);
                return cache;
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, "Error loading cache of last job operation for job resume.");
                return null;
            }
        }

        private void TrackPowerUpSequence()
        {
            LogManager.Log("Starting power up sequence tracking...");

            bool started = false;
            bool completed = false;
            PowerUpState lastState = PowerUpState.None;

            SendContinuousRequest<StartPowerUpRequest, StartPowerUpResponse>(new StartPowerUpRequest(), new TransportContinuousRequestConfig()
            {
                ShouldLog = true,
                Timeout = TimeSpan.FromSeconds(5)
            }).Subscribe((response) =>
            {
                if (!started)
                {
                    started = true;
                    PowerUpStarted?.Invoke(this, response);
                }

                PowerUpProgress?.Invoke(this, response);

                var state = response.Message.State;

                if (state != lastState)
                {
                    LogManager.Log($"Power up sequence state changed to '{state}'...");

                    switch (state)
                    {
                        case PowerUpState.Error:
                            completed = true;
                            LogManager.Log($"Power up sequence failed with state '{state}'. ({response.Message.Message})");
                            PowerUpFailed?.Invoke(this, response);
                            PowerUpEnded?.Invoke(this, new EventArgs());
                            break;
                        case PowerUpState.Cancelled:
                            completed = true;
                            LogManager.Log($"Power up sequence canceled with state '{state}'. ({response.Message.Message})");
                            PowerUpEnded?.Invoke(this, new EventArgs());
                            break;
                        case PowerUpState.MachineReadyToDye:
                            completed = true;
                            LogManager.Log($"Power up sequence completed successfully with state '{state}'. ({response.Message.Message})");
                            PowerUpCompleted?.Invoke(this, response);
                            PowerUpEnded?.Invoke(this, new EventArgs());
                            break;
                    }

                    lastState = state;
                }

            }, (ex) =>
            {
                if (!completed)
                {
                    completed = true;
                    LogManager.Log(ex, "Power up sequence tracking failed.");
                    PowerUpEnded?.Invoke(this, new EventArgs());
                }
            }, () =>
            {
                if (!completed)
                {
                    completed = true;
                    PowerUpEnded?.Invoke(this, new EventArgs());
                }
            });
        }

        private async void ResumeJob()
        {
            LogManager.Log("Checking if a job is in progress...");

            try
            {
                var res = await SendRequest<CurrentJobRequest, CurrentJobResponse>(new CurrentJobRequest(), new TransportRequestConfig() { ShouldLog = true });

                if (res.Message.IsJobInProgress)
                {
                    LogManager.Log("Job is in progress. Trying to resume job...");
                    CachedJobOperation cache = LoadCachedJobOperation();

                    if (cache == null)
                    {
                        LogManager.Log("Cannot resume current job with no cached operation.", LogCategory.Error);
                        return;
                    }

                    Job job = null;
                    Configuration configuration = null;
                    ProcessParametersTable processParameters = null;

                    try
                    {
                        processParameters = cache.ProcessParametersDTO.ToObservable();
                        job = cache.JobDTO.ToObservable();
                        configuration = cache.MachineConfigurationDTO.ToObservable();
                        CurrentProcessParameters = processParameters;
                    }
                    catch (Exception ex)
                    {
                        LogManager.Log(ex, "Error deserializing cache job operation. Aborting resume.");
                        return;
                    }

                    JobTicket jobTicket = res.Message.JobTicket;

                    ResumingJobEventArgs args = new ResumingJobEventArgs(() =>
                    {
                        RunningJob = null;
                        RunningJobStatus = null;

                        var request = new ResumeCurrentJobRequest();

                        JobHandler handler = null;

                        handler = new JobHandler(async () =>
                        {
                            try
                            {
                                if (handler.CanCancel)
                                {
                                    handler.CanCancel = false;
                                    handler.IsCanceled = true;
                                    LogManager.Log("Aborting current job...");
                                    var result = await SendRequest<AbortJobRequest, AbortJobResponse>(new AbortJobRequest(), new TransportRequestConfig() { ShouldLog = true });
                                    SaveLastJobLiquidQuantities(job, configuration, processParameters, handler);
                                    OnPrintingAborted(handler, job);
                                    handler.RaiseCanceled();
                                    if (Status != MachineStatuses.Disconnected)
                                    {
                                        UpdateStatus(MachineStatuses.ReadyToDye);
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                handler.CanCancel = true;
                                LogManager.Log(ex, "Failed to cancel job.");
                            }
                        }, job, jobTicket, processParameters, JobHandlingMode);

                        handler.StatusChanged += (x, s) =>
                        {
                            RunningJobStatus = s;
                        };

                        if (MachineStatus != null)
                        {
                            _machineStatusBeforeJobStart = MachineStatus.Clone();
                        }
                        else
                        {
                            _machineStatusBeforeJobStart = cache.MachineStatus.Clone();
                        }

                        _jobStartDate = DateTime.UtcNow;
                        _jobUploadingStartDate = _jobStartDate;
                        _jobHeatingStartDate = _jobStartDate;
                        _jobActualStartDate = null;

                        bool responseLogged = false;
                        bool completed = false;

                        Thread.Sleep(500); //Just wait maybe Shlomo is getting this message to fast after restart ?

                        SendContinuousRequest<ResumeCurrentJobRequest, ResumeCurrentJobResponse>(request, new TransportContinuousRequestConfig() { ContinuousTimeout = TimeSpan.FromSeconds(10), ShouldLog = true }).Subscribe((response) =>
                        {
                            if (!completed)
                            {
                                handler.RaiseStatusReceived(response.Message.Status);
                                _last_job_status = handler.Status;

                                if (response.Message.Status.Progress > 0)
                                {
                                    if (_jobActualStartDate == null)
                                    {
                                        _jobActualStartDate = DateTime.UtcNow;
                                    }
                                }

                                if (!responseLogged)
                                {
                                    UpdateStatus(MachineStatuses.GettingReady);
                                    responseLogged = true;
                                    RunningJob = job;
                                    OnPrintingStarted(handler, job, true);
                                }

                                if (JobHandlingMode == JobHandlerModes.SettingUp)
                                {
                                    if (response.Message.Status.Progress > CurrentProcessParameters.DryerBufferLengthMeters)
                                    {
                                        if (!completed)
                                        {
                                            UpdateStatus(MachineStatuses.Printing);
                                        }
                                    }
                                }
                                else
                                {
                                    if (response.Message.Status.Progress > 0)
                                    {
                                        if (!completed)
                                        {
                                            UpdateStatus(MachineStatuses.Printing);
                                        }
                                    }
                                }
                            }
                        }, (ex) =>
                        {
                            if (!completed)
                            {
                                completed = true;

                                if (Status != MachineStatuses.Disconnected)
                                {
                                    UpdateStatus(MachineStatuses.ReadyToDye);
                                }

                                if (!handler.IsCanceled)
                                {
                                    SaveLastJobLiquidQuantities(job, configuration, processParameters, handler);

                                    Exception finalException = ex;

                                    if (ex is ContinuousResponseAbortedException continuousException)
                                    {
                                        finalException = new ContinuousResponseAbortedException($"Job aborted by the embedded device ({continuousException.Container.ErrorMessage}).");
                                    }

                                    OnPrintingFailed(handler, job, finalException);
                                    handler.RaiseFailed(finalException);
                                }
                            }
                        }, () =>
                        {
                            if (!completed)
                            {
                                completed = true;
                                UpdateStatus(MachineStatuses.ReadyToDye);
                                SaveLastJobLiquidQuantities(job, configuration, processParameters, handler);
                                OnPrintingCompleted(handler, job);
                                handler.RaiseCompleted();
                            }
                        });

                        return handler;
                    });

                    args.JobGuid = jobTicket.Guid;
                    ResumingJob?.Invoke(this, args);
                }
            }
            catch (Exception ex)
            {
                LogManager.Log(ex);
            }
        }

        /// <summary>
        /// Creates a PMR job segment.
        /// </summary>
        /// <param name="segment">The segment.</param>
        /// <returns></returns>
        private JobSegment CreatePMRJobSegment(Segment segment, Job job, ProcessParametersTable processParameters)
        {
            LogManager.Log($"Converting segment {segment.SegmentIndex} to PMR segment...");

            JobSegment jobSegment = new JobSegment();
            jobSegment.Length = segment.LengthWithFactor;
            jobSegment.Name = segment.Name;

            var stops = segment.BrushStops.ToList();

            if (GradientGenerationConfiguration != null && GradientGenerationConfiguration.IsEnabled && segment.BrushStops.Count > 1)
            {
                LogManager.Log($"Generate segment {segment.SegmentIndex} gradient...");
                try
                {
                    stops = GradientGenerationConfiguration.Generate(segment, job, processParameters, (e) =>
                    {
                        PreparingJobProgress?.Invoke(this, e);
                    });
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException($"Error occurred while trying to generate a gradient.\n{ex.Message}");
                }
                LogManager.Log($"Gradient generated.");

                PreparingJobProgress?.Invoke(this, new PreparingJobProgressEventArgs()
                {
                    Job = job,
                    Total = job.Segments.Sum(x => x.Length),
                    Progress = job.Segments.Sum(x => x.Length),
                });
            }

            foreach (var stop in stops)
            {
                JobBrushStop jobStop = new JobBrushStop();
                jobStop.Index = stop.StopIndex;
                jobStop.OffsetPercent = stop.OffsetPercent;
                jobStop.OffsetMeters = stop.OffsetMeters;

                if (stop.LiquidVolumes == null)
                {
                    stop.SetLiquidVolumes(job.Machine.Configuration, job.Rml, processParameters);
                }

                foreach (var liquidVolume in stop.LiquidVolumes)
                {
                    JobDispenser dispenser = new JobDispenser();
                    dispenser.Index = liquidVolume.IdsPack.PackIndex;
                    dispenser.Volume = liquidVolume.Volume;
                    dispenser.DispenserLiquidType = (DispenserLiquidType)liquidVolume.IdsPack.LiquidType.Code;
                    dispenser.DispenserStepDivision = (DispenserStepDivision)liquidVolume.DispenserStepDivision;

                    if (liquidVolume.DispenserStepDivision != BL.Dispensing.DispenserStepDivisions.Auto)
                    {
                        dispenser.NanoliterPerPulse = liquidVolume.NanoliterPerStep;
                    }
                    else
                    {
                        dispenser.NanoliterPerPulse = liquidVolume.IdsPack.Dispenser.NlPerPulse;
                    }

                    dispenser.LiquidMaxNanoliterPerCentimeter = liquidVolume.LiquidMaxNanoliterPerCentimeter;
                    dispenser.NanoliterPerCentimeter = liquidVolume.NanoliterPerCentimeter;
                    dispenser.NanolitterPerSecond = liquidVolume.NanoliterPerSecond;
                    dispenser.PulsePerSecond = liquidVolume.PulsePerSecond;

                    jobStop.Dispensers.Add(dispenser);
                }

                jobSegment.BrushStops.Add(jobStop);
            }

            return jobSegment;
        }

        private void ContinueSingleSpoolJob(Segment segment, Job job, ProcessParametersTable processParameters, JobHandler handler)
        {
            JobRequest request = new JobRequest();

            JobTicket ticket = new JobTicket();
            ticket.Guid = handler.Job.Guid;
            ticket.EnableInterSegment = job.EnableInterSegment;
            ticket.InterSegmentLength = job.InterSegmentLength;
            ticket.Length = segment.Length;
            ticket.WindingMethod = (JobWindingMethod)job.WindingMethod.Code;
            ticket.Spool = new JobSpool();

            job.SpoolType.MapPrimitivesTo(ticket.Spool);
            ticket.Spool.JobSpoolType = (JobSpoolType)job.SpoolType.Code;

            ProcessParameters process = new ProcessParameters();
            processParameters.MapPrimitivesTo(process);
            ticket.ProcessParameters = process;

            ticket.Segments.Add(CreatePMRJobSegment(segment, job, processParameters));

            request.JobTicket = ticket;

            bool responseLogged = false;

            var previous_segments_length = job.Segments.Where(x => x.SegmentIndex < segment.SegmentIndex).Sum(x => x.Length);

            SendContinuousRequest<JobRequest, JobResponse>(request, new TransportContinuousRequestConfig() { ContinuousTimeout = ContinuousRequestTimeout, ShouldLog = true }).Subscribe((response) =>
            {
                response.Message.Status.Progress += previous_segments_length;

                handler.RaiseStatusReceived(response.Message.Status);

                if (!responseLogged && segment == job.OrderedSegments.First())
                {
                    responseLogged = true;
                    UpdateStatus(MachineStatuses.Printing);
                    RunningJob = handler.Job;
                    OnPrintingStarted(handler, handler.Job);
                }

            }, (ex) =>
            {
                if (!(ex is ContinuousResponseAbortedException))
                {
                    UpdateStatus(MachineStatuses.ReadyToDye);

                    if (!handler.IsCanceled)
                    {
                        OnPrintingFailed(handler, handler.Job, ex);
                        handler.RaiseFailed(ex);
                    }
                }
                else
                {
                    UpdateStatus(MachineStatuses.ReadyToDye);
                }
            }, () =>
            {
                if (segment == job.OrderedSegments.Last())
                {
                    UpdateStatus(MachineStatuses.ReadyToDye);
                    OnPrintingCompleted(handler, handler.Job);
                    handler.RaiseCompleted();
                }
                else
                {
                    handler.RaiseSpoolChangeRequired(() =>
                    {
                        ContinueSingleSpoolJob(segment.GetNextSegment(), job, processParameters, handler);
                    }, () =>
                    {
                        OnPrintingAborted(handler, handler.Job);
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        handler.RaiseCanceled();
                    });
                }
            });
        }

        private List<RequiredLiquid> ValidateJobLiquidQuantity(Job job, ProcessParametersTable processParameters, Configuration configuration)
        {
            LogManager.Log("Validating job liquid quantities using integral...");

            Dictionary<int, double> liquidQuantities = new Dictionary<int, double>();
            List<RequiredLiquid> requiredLiquids = new List<RequiredLiquid>();

            foreach (var pack in configuration.NoneEmptyIdsPacks.OrderBy(x => x.PackIndex))
            {
                liquidQuantities.Add(pack.PackIndex, 0);
            }

            int resolution = GradientGenerationConfiguration.ResolutionCM;

            for (int i = 0; i < Math.Max(job.NumberOfUnits, 1); i++)
            {
                for (int segmentIndex = 0; segmentIndex < job.Segments.Count; segmentIndex++)
                {
                    var segment = job.Segments[segmentIndex];
                    var segment_length_cm = segment.Length * 100d;

                    List<BrushStop> orderedBrushCollection = segment.BrushStops.OrderBy(x => x.OffsetMeters).ToList();

                    int solid_gradient_oeff = orderedBrushCollection.Count == 1 ? 1 : 2;
                    double prev_offset_cm = 0;

                    for (int brushIndex = 0; brushIndex < orderedBrushCollection.Count; brushIndex++)
                    {
                        var brush = orderedBrushCollection[brushIndex];
                        double brush_length_centimeters = 0d;
                        double brush_offset_cm = 0;

                        if ((brushIndex + 1) < orderedBrushCollection.Count)
                        {
                            brush_offset_cm = (brush.OffsetMeters * 100d);
                            double next_brush_offset_cm = (orderedBrushCollection[brushIndex + 1].OffsetMeters * 100d);
                            brush_length_centimeters = ((next_brush_offset_cm - brush_offset_cm) + (brush_offset_cm - prev_offset_cm));

                            if (brushIndex == 0)
                            {
                                // add a resolution step for first brush
                                brush_length_centimeters += resolution;
                            }
                        }
                        else//last brush or solid brush
                        {
                            brush_length_centimeters = (segment_length_cm - prev_offset_cm);
                            if (orderedBrushCollection.Count > 1)
                            {
                                // add a resolution for last brush , not solid brush
                                brush_length_centimeters -= resolution;
                            }
                        }

                        prev_offset_cm = brush_offset_cm;

                        foreach (var liquidVolumes in brush.LiquidVolumes)
                        {
                            liquidQuantities[liquidVolumes.IdsPack.PackIndex] += liquidVolumes.NanoliterPerCentimeter * (brush_length_centimeters / solid_gradient_oeff);
                        }
                    }
                }
            }

            if (MachineStatus != null)
            {
                var exception = new InsufficientLiquidQuantityException($"Insufficient liquids level.");

                bool shouldThrow = false;

                foreach (var liquidQuantity in liquidQuantities)
                {
                    int index = liquidQuantity.Key;
                    var packLevel = MachineStatus.IDSPacksLevels.SingleOrDefault(x => x.Index == index);
                    var idsPack = configuration.NoneEmptyIdsPacks.SingleOrDefault(x => x.PackIndex == index);

                    if (packLevel != null)
                    {
                        var idsLevel = new InsufficientLiquidQuantityException.IDSPackLevel()
                        {
                            IdsPack = idsPack,
                            Current = packLevel.DispenserLevel,
                            Required = (int)liquidQuantities[index],
                            Maximum = MAX_DISPENSER_NANOLITER,
                        };

                        requiredLiquids.Add(new RequiredLiquid()
                        {
                            IdsPack = idsPack,
                            Quantity = (int)liquidQuantities[index]
                        });

                        LogManager.Log($"Required {idsLevel.IdsPack.LiquidType.Type}: {idsLevel.Required}, Current: {idsLevel.Current}");

                        if (idsLevel.Required > idsLevel.Current)
                        {
                            shouldThrow = true;
                            string display_value = (((double)(idsLevel.Required - idsLevel.Current) / 1000000)).ToString("N2", CultureInfo.InvariantCulture);
                            idsLevel.Message = $"Missing {display_value} CC to complete the job.";

                            if (idsLevel.Required > idsLevel.Maximum)
                            {
                                display_value = (((double)(idsLevel.Required - idsLevel.Maximum)) / 1000000).ToString("N2", CultureInfo.InvariantCulture);
                                idsLevel.Message = $"Required ink exceeds the maximum capacity of the dispenser by {display_value} CC. Please reduce the segment length.";
                            }
                        }

                        exception.IdsPackLevels.Add(idsLevel);
                    }
                    else
                    {
                        LogManager.Log($"Could not validate required liquid quantity for job. Missing IDS Pack level at index {index}.", LogCategory.Warning);
                    }
                }

                if (shouldThrow)
                {
                    LogManager.Log("Liquid quantity validation failed due to insufficient quantity. Throwing exception...");

                    exception.IdsPackLevels = exception.IdsPackLevels.OrderBy(x => x.IdsPack.PackIndex).ToList();

                    throw LogManager.Log(exception, JsonConvert.SerializeObject(exception.IdsPackLevels.Select(x => new
                    {
                        Liquid = x.IdsPack.LiquidType.Name,
                        x.Required,
                        x.Current
                    }).ToList()));
                }
            }
            else
            {
                LogManager.Log("Could not validate required liquid quantity for job. No machine status received", LogCategory.Warning);
            }

            return requiredLiquids;
        }

        private List<RequiredLiquid> ValidateJobLiquidQuantity(JobTicket ticket, ProcessParametersTable processParameters, Configuration configuration)
        {
            LogManager.Log("Validating job liquid quantity using job ticket...");

            Dictionary<int, double> liquidQuantities = new Dictionary<int, double>();
            List<RequiredLiquid> requiredLiquids = new List<RequiredLiquid>();

            foreach (var pack in configuration.NoneEmptyIdsPacks.OrderBy(x => x.PackIndex))
            {
                liquidQuantities.Add(pack.PackIndex, 0);
            }

            for (int segmentIndex = 0; segmentIndex < ticket.Segments.Count; segmentIndex++)
            {
                var segment = ticket.Segments[segmentIndex];
                var segment_length_cm = segment.Length * 100d;

                var stop_count = segment.BrushStops.Count - (segment.BrushStops.Count == 1 ? 0 : 1);
                var stop_length_centimeters = segment_length_cm / stop_count;

                for (int stopIndex = 0; stopIndex < stop_count; stopIndex++)
                {
                    var stop = segment.BrushStops[stopIndex];

                    foreach (var dispenser in stop.Dispensers)
                    {
                        liquidQuantities[dispenser.Index] += dispenser.NanoliterPerCentimeter * stop_length_centimeters;
                    }
                }
            }

            foreach (var key in liquidQuantities.Select(x => x.Key).ToList())
            {
                liquidQuantities[key] *= Math.Max(ticket.NumberOfUnits, 1);
            }

            if (MachineStatus != null)
            {
                var exception = new InsufficientLiquidQuantityException($"Insufficient liquids level.");

                bool shouldThrow = false;

                foreach (var liquidQuantity in liquidQuantities)
                {
                    int index = liquidQuantity.Key;
                    var packLevel = MachineStatus.IDSPacksLevels.SingleOrDefault(x => x.Index == index);
                    var idsPack = configuration.NoneEmptyIdsPacks.SingleOrDefault(x => x.PackIndex == index);

                    if (packLevel != null)
                    {
                        var idsLevel = new InsufficientLiquidQuantityException.IDSPackLevel()
                        {
                            IdsPack = idsPack,
                            Current = packLevel.DispenserLevel,
                            Required = (int)liquidQuantities[index],
                            Maximum = MAX_DISPENSER_NANOLITER,
                        };

                        requiredLiquids.Add(new RequiredLiquid()
                        {
                            IdsPack = idsPack,
                            Quantity = (int)liquidQuantities[index]
                        });

                        LogManager.Log($"Required {idsLevel.IdsPack.LiquidType.Type}: {idsLevel.Required}, Current: {idsLevel.Current}");

                        if (idsLevel.Required > idsLevel.Current)
                        {
                            shouldThrow = true;

                            string display_value = (((double)(idsLevel.Required - idsLevel.Current) / 1000000)).ToString("N2", CultureInfo.InvariantCulture);
                            idsLevel.Message = $"Missing {display_value} CC to complete the job.";

                            if (idsLevel.Required > idsLevel.Maximum)
                            {
                                display_value = (((double)(idsLevel.Required - idsLevel.Maximum)) / 1000000).ToString("N2", CultureInfo.InvariantCulture);
                                idsLevel.Message = $"Required ink exceeds the maximum capacity of the dispenser by {display_value} CC. Please reduce the segment length.";
                            }
                        }

                        exception.IdsPackLevels.Add(idsLevel);
                    }
                    else
                    {
                        LogManager.Log($"Could not validate required liquid quantity for job. Missing IDS Pack level at index {index}.", LogCategory.Warning);
                    }
                }


                if (shouldThrow)
                {
                    LogManager.Log("Liquid quantity validation failed due to insufficient quantity. Throwing exception...");

                    exception.IdsPackLevels = exception.IdsPackLevels.OrderBy(x => x.IdsPack.PackIndex).ToList();

                    throw LogManager.Log(exception, JsonConvert.SerializeObject(exception.IdsPackLevels.Select(x => new
                    {
                        Liquid = x.IdsPack.LiquidType.Name,
                        x.Required,
                        x.Current
                    }).ToList()));
                }
            }
            else
            {
                LogManager.Log("Could not validate required liquid quantity for job. No machine status received", LogCategory.Warning);
            }

            return requiredLiquids;
        }

        /// <summary>
        /// Assign the liquid quantities spent by the last job using the job and the handler last status.
        /// </summary>
        /// <param name="job">The job.</param>
        /// <param name="configuration">The configuration.</param>
        /// <param name="handler">The handler.</param>
        private void SaveLastJobLiquidQuantities(Job job, Configuration configuration, ProcessParametersTable processParameters, JobHandler handler)
        {
            LogManager.Log($"Calculating job run liquid quantities using '{JobLiquidQuantityCalculationMode}' method...");

            if (configuration == null)
            {
                configuration = _machineConfiguration;
            }

            try
            {
                _lastJobLiquidQuantities = new List<BL.ValueObjects.JobRunLiquidQuantity>();

                if (JobLiquidQuantityCalculationMode == JobLiquidQuantityCalculationMode.MachineStatus)
                {
                    foreach (var pack in configuration.NoneEmptyIdsPacks.ToList())
                    {
                        var packLevelAfter = MachineStatus.IDSPacksLevels.SingleOrDefault(x => x.Index == pack.PackIndex);
                        var packLevelBefore = _machineStatusBeforeJobStart.IDSPacksLevels.SingleOrDefault(x => x.Index == pack.PackIndex);

                        if (packLevelAfter != null && packLevelBefore != null)
                        {
                            if (packLevelAfter.DispenserLevel > packLevelBefore.DispenserLevel)
                            {
                                LogManager.Log($"Invalid '{pack.LiquidType.Name}' dispenser level calculated: {packLevelBefore.DispenserLevel} - {packLevelAfter.DispenserLevel} = {packLevelBefore.DispenserLevel - packLevelAfter.DispenserLevel}. Ignoring...");
                                continue;
                            }

                            _lastJobLiquidQuantities.Add(new BL.ValueObjects.JobRunLiquidQuantity()
                            {
                                LiquidType = pack.LiquidType.Type,
                                Quantity = packLevelBefore.DispenserLevel - packLevelAfter.DispenserLevel,
                            });
                        }
                    }
                }
                else
                {
                    _lastJobLiquidQuantities = CreateJobRunLiquidQuantities(job, configuration, processParameters, handler.Status.Progress, handler.Status.TotalProgress);
                }

                LogManager.Log($"Job run liquid quantities calculation completed:\n{_lastJobLiquidQuantities.ToJsonString()}");
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, LogCategory.Critical, "Error calculating and saving last job run liquid quantities.");
            }
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Creates the job run liquid quantities.
        /// </summary>
        /// <param name="job">The job.</param>
        /// <param name="configuration">The configuration.</param>
        /// <param name="processParameters">The process parameters.</param>
        /// <param name="position">The position.</param>
        /// <param name="length">The length.</param>
        /// <param name="gradientResolution">The gradient resolution.</param>
        /// <returns></returns>
        public static List<BL.ValueObjects.JobRunLiquidQuantity> CreateJobRunLiquidQuantities(Job job, Configuration configuration, ProcessParametersTable processParameters, double position, double length)
        {
            var units = Math.Max(job.NumberOfUnits, 1);

            var effectiveSegments = new List<Segment>();
            for (int i = 0; i < units; i++)
            {
                if (i > 0 && job.EnableInterSegment)
                {
                    effectiveSegments.Add(Job.CreateInterSegment(job.InterSegmentLength));
                }

                foreach (var segment in job.EffectiveSegments)
                {
                    effectiveSegments.Add(segment.Clone(job));
                }
            }

            effectiveSegments.Add(Job.CreateInterSegment(processParameters.DryerBufferLengthMeters));

            double total = length;
            double position_cm = position * 100d;
            double total_length = 0;

            Dictionary<int, double> liquidQuantities = new Dictionary<int, double>();

            foreach (var pack in configuration.NoneEmptyIdsPacks.OrderBy(x => x.PackIndex))
            {
                liquidQuantities.Add(pack.PackIndex, 0);
            }

            bool stop_calc = false;

            for (int segmentIndex = 0; segmentIndex < effectiveSegments.Count && !stop_calc; segmentIndex++)
            {
                var segment = effectiveSegments[segmentIndex];
                var segment_length_cm = segment.Length * 100d;

                List<BrushStop> orderedBrushCollection = segment.BrushStops.OrderBy(x => x.OffsetMeters).ToList();

                int solid_gradient_oeff = orderedBrushCollection.Count == 1 ? 1 : 2;
                double prev_offset_cm = 0;
                double delta_brushLenghtToStopPosition = 0d;

                double position_interval_centimeters = 0d;//interval for calculation where the stop occurred
                for (int brushIndex = 0; brushIndex < orderedBrushCollection.Count && !stop_calc; brushIndex++)
                {
                    var brush = orderedBrushCollection[brushIndex];
                    double brush_length_centimeters = 0d;
                    double brush_offset_cm = 0;

                    if ((brushIndex + 1) < orderedBrushCollection.Count)
                    {
                        brush_offset_cm = (brush.OffsetMeters * 100d);
                        double next_brush_offset_cm = (orderedBrushCollection[brushIndex + 1].OffsetMeters * 100d);
                        brush_length_centimeters = (next_brush_offset_cm - prev_offset_cm);
                        double brush_length_centimeters_before_calc = brush_length_centimeters;

                        if (delta_brushLenghtToStopPosition > 0)//calculate second brush
                        {
                            brush_length_centimeters = ((position_interval_centimeters - delta_brushLenghtToStopPosition) * (position_interval_centimeters - delta_brushLenghtToStopPosition)) / position_interval_centimeters;
                            stop_calc = true;
                        }
                        else if (total_length + prev_offset_cm + brush_length_centimeters > position_cm)//calculate first brush
                        {
                            position_interval_centimeters = brush_length_centimeters;
                            delta_brushLenghtToStopPosition = (total_length + prev_offset_cm + brush_length_centimeters) - position_cm;
                            brush_length_centimeters = brush_length_centimeters - (delta_brushLenghtToStopPosition * delta_brushLenghtToStopPosition / brush_length_centimeters);
                        }
                    }
                    else//last brush or solid brush
                    {
                        brush_length_centimeters = (segment_length_cm - prev_offset_cm);
                        if (delta_brushLenghtToStopPosition > 0)//second  brush
                        {
                            brush_length_centimeters = ((position_interval_centimeters - delta_brushLenghtToStopPosition) * (position_interval_centimeters - delta_brushLenghtToStopPosition)) / position_interval_centimeters;
                            stop_calc = true;
                        }
                        else if (orderedBrushCollection.Count == 1 && (total_length + segment_length_cm) > position_cm)// solid brush
                        {
                            brush_length_centimeters = position_cm - total_length;
                            stop_calc = true;
                        }
                    }

                    prev_offset_cm = brush_offset_cm;

                    if (brush.LiquidVolumes != null)
                    {
                        foreach (var liquidVolumes in brush.LiquidVolumes)
                        {
                            liquidQuantities[liquidVolumes.IdsPack.PackIndex] += liquidVolumes.NanoliterPerCentimeter * (brush_length_centimeters / solid_gradient_oeff);
                        }
                    }
                }
                total_length += segment_length_cm;
            }

            List<BL.ValueObjects.JobRunLiquidQuantity> quantities = new List<BL.ValueObjects.JobRunLiquidQuantity>();

            foreach (var liquidQuantity in liquidQuantities)
            {
                int index = liquidQuantity.Key;
                var idsPack = configuration.NoneEmptyIdsPacks.SingleOrDefault(x => x.PackIndex == index);

                if (idsPack != null)
                {
                    quantities.Add(new BL.ValueObjects.JobRunLiquidQuantity()
                    {
                        LiquidType = idsPack.LiquidType.Type,
                        Quantity = (int)liquidQuantities[index],
                    });
                }
            }

            return quantities;
        }


        #endregion

        #region Public Methods

        /// <summary>
        /// Prints the specified job.
        /// The process parameters table will be calculated using color conversion gamut region.
        /// This method cannot accept brush stops with 'Volume' as color space.
        /// </summary>
        /// <param name="job">The job.</param>
        /// <param name="config">Optional job configuration.</param>
        /// <returns></returns>
        public async Task<JobHandler> Print(Job job, AdditionalJobConfiguration config = null)
        {
            ProcessParametersTable processParameters = null;

            if (config == null) config = new AdditionalJobConfiguration();

            await Task.Factory.StartNew(() =>
            {
                IColorConverter converter = new DefaultColorConverter();

                if (job.Rml == null)
                {
                    throw new NullReferenceException("Job RML is null");
                }

                var processGroup = job.Rml.ProcessParametersTablesGroups.FirstOrDefault(x => x.Active);

                if (processGroup == null)
                {
                    throw new NullReferenceException("Could not locate an active process parameters tables group for RML " + job.Rml.Name);
                }

                try
                {
                    bool useLightInks = config.UseLightInks;
                    if (job.OrderedSegmentsWithGroups.Count > 1 && !job.EnableInterSegment) useLightInks = false;

                    processParameters = converter.GetRecommendedProcessParameters(job, useLightInks);
                }
                catch (Exception ex)
                {
                    throw LogManager.Log(new InvalidOperationException($"An error occurred while trying to resolve the recommended process parameters.\n{ex.Message}"));
                }

                if (processParameters == null)
                {
                    throw new NullReferenceException("Could not locate any process parameters table in group " + processGroup.Name + " for RML " + job.Rml.Name);
                }

                try
                {
                    foreach (var stop in job.OrderedSegmentsWithGroups.SelectMany(x => x.BrushStops).ToList())
                    {
                        stop.SetLiquidVolumes(job.Machine.Configuration, job.Rml, processParameters);
                    }
                }
                catch (Exception ex)
                {
                    throw LogManager.Log(new InvalidOperationException($"An error occurred while trying to apply final liquid volumes.\n{ex.Message}"));
                }
            });

            return await Print(job, processParameters, config);
        }

        /// <summary>
        /// Prints the specified job using the specified job parameters.
        /// </summary>
        /// <param name="job">The job.</param>
        /// <param name="processParameters">Process parameters table</param>
        /// <returns></returns>
        public Task<JobHandler> Print(Job job, ProcessParametersTable processParameters, AdditionalJobConfiguration config = null)
        {
            return Task.Factory.StartNew(() =>
            {
                if (config == null) config = new AdditionalJobConfiguration();

                if (!CanPrint)
                {
                    throw new InvalidOperationException("Could not print while status = " + Status);
                }

                _jobStartDate = DateTime.UtcNow;

                LogManager.Log($"Executing job '{job.Name}'...");

                if (MachineStatus == null)
                {
                    LogManager.Log("Aborting job execution. No machine status received yet.");
                    throw new InvalidOperationException("Cannot execute a job before at least one machine status has been received.");
                }

                _lastJobLiquidQuantities = new List<BL.ValueObjects.JobRunLiquidQuantity>();
                _jobUploadingStartDate = null;
                _jobHeatingStartDate = null;
                _jobActualStartDate = null;

                RunningJob = null;
                RunningJobStatus = null;

                if (job.NumberOfUnits < 1)
                {
                    job.NumberOfUnits = 1;
                }

                var originalJob = job;
                var clonedJob = job.Clone();
                clonedJob.Guid = job.Guid;
                clonedJob.Name = job.Name;

                job = job.Clone();
                job.Guid = originalJob.Guid;
                job.Name = originalJob.Name;

                var jobSegments = job.OrderedSegmentsWithGroups;

                //Color Conversion
                if (config.UseColorConversion)
                {
                    IColorConverter converter = new DefaultColorConverter();

                    bool useLightInks = config.UseLightInks;

                    //Use light inks only if one segment or inter segment is enabled.
                    if (job.OrderedSegmentsWithGroups.Count > 1 && !job.EnableInterSegment) useLightInks = false;

                    foreach (var segment in jobSegments)
                    {
                        foreach (var stop in segment.BrushStops)
                        {
                            try
                            {
                                var output = converter.Convert(stop, false, useLightInks && segment.BrushStops.Count == 1); //Use light inks only if this is a solid segment.
                                output.ApplyOnBrushStopLiquidVolumes(stop, processParameters);
                            }
                            catch (Exception ex)
                            {
                                throw new InvalidOperationException($"Error processing the coordinates of stop '{stop.StopIndex}' of segment '{stop.Segment.SegmentIndex}'.", ex);
                            }
                        }
                    }
                }

                //Lubrication
                if (job.EnableLubrication)
                {
                    if (config.UseLubricantVolume)
                    {
                        LogManager.Log($"Job custom lubrication is enabled. Settings all brush stops to {config.LubricationVolume}% lubricant.");

                        foreach (var stop in jobSegments.SelectMany(x => x.BrushStops).ToList())
                        {
                            var lubricantVolume = stop.LiquidVolumes.SingleOrDefault(x => x.IdsPack != null && x.IdsPack.LiquidType != null && x.LiquidType == LiquidTypes.Lubricant);

                            if (lubricantVolume != null)
                            {
                                lubricantVolume.Volume = config.LubricationVolume;
                            }
                        }
                    }
                    else
                    {
                        LogManager.Log($"Job auto lubrication is enabled. Settings all none Volume brush stops to 100% lubricant.");

                        foreach (var stop in jobSegments.SelectMany(x => x.BrushStops).Where(x => x.BrushColorSpace != ColorSpaces.Volume).ToList())
                        {
                            var lubricantVolume = stop.LiquidVolumes.SingleOrDefault(x => x.IdsPack != null && x.IdsPack.LiquidType != null && x.LiquidType == LiquidTypes.Lubricant);

                            if (lubricantVolume != null)
                            {
                                lubricantVolume.Volume = 100;
                            }
                        }
                    }
                }
                else
                {
                    LogManager.Log("Job lubrication is disabled.");
                }

                //Modify transparent/white brush stops. (Transparent/white stops should be all zeros and 100% TI)
                LogManager.Log("Modifying all 'white' brush stops...");
                foreach (var stop in job.OrderedSegmentsWithGroups.SelectMany(x => x.BrushStops).Where(x => x.IsTransparent || x.IsWhite).ToList())
                {
                    foreach (var liquidVolume in stop.LiquidVolumes.Where(x => x.LiquidType != LiquidTypes.TransparentInk && x.LiquidType != LiquidTypes.Lubricant).ToList())
                    {
                        liquidVolume.Volume = 0;
                    }

                    var tiLiquid = stop.LiquidVolumes.SingleOrDefault(x => x.IdsPack != null && x.IdsPack.LiquidType != null && x.LiquidType == LiquidTypes.TransparentInk);

                    if (tiLiquid != null)
                    {
                        tiLiquid.Volume = 100;
                    }
                }

                var segments = job.OrderedSegmentsWithGroups.ToList();

                List<RequiredLiquid> requiredLiquids = null;

                //Validate liquid quantities
                if (EnableJobLiquidQuantityValidation)
                {
                    if (!originalJob.Rml.UseColorLibGradients) //Validate liquid quantities when ColorLib generate gradient is disabled
                    {
                        requiredLiquids = ValidateJobLiquidQuantity(job, processParameters, job.Machine.Configuration);
                    }
                    else //Validate liquid quantities when ColorLib generate gradient is enabled
                    {
                        JobTicket t = new JobTicket();
                        t.NumberOfUnits = (uint)originalJob.NumberOfUnits;

                        foreach (var segment in segments)
                        {
                            if (segment is Segment simpleSegment)
                            {
                                t.Segments.Add(CreatePMRJobSegment(simpleSegment, originalJob, processParameters));
                            }
                            else if (segment is SegmentsGroup group)
                            {
                                List<JobSegment> groupSegments = new List<JobSegment>();

                                foreach (var innerSegment in group.OrderedSegments)
                                {
                                    groupSegments.Add(CreatePMRJobSegment(innerSegment, originalJob, processParameters));
                                }

                                for (int i = 0; i < group.Repeats; i++)
                                {
                                    t.Segments.AddRange(groupSegments.ToList());
                                }
                            }
                        }

                        requiredLiquids = ValidateJobLiquidQuantity(t, processParameters, job.Machine.Configuration);
                    }
                }
                else
                {
                    LogManager.Log("Liquid quantity validation is disabled. Skipping...");
                }

                CurrentProcessParameters = processParameters;

                JobRequest request = new JobRequest();

                var jobForJobRun = job.Clone();
                jobForJobRun.Guid = job.Guid;
                jobForJobRun.Name = job.Name;
                jobForJobRun.ID = job.ID;

                int max = job.OrderedSegmentsWithGroups.Last().SegmentIndex + 1;

                for (int i = 0; i < job.NumberOfUnits - 1; i++)
                {
                    foreach (var s in segments)
                    {
                        var cloned = s.Clone(job);
                        cloned.SegmentIndex = max++;

                        if (cloned is Segment simpleSegment)
                        {
                            job.Segments.Add(simpleSegment);
                        }
                        else if (cloned is SegmentsGroup g)
                        {
                            job.SegmentsGroups.Add(g);
                        }
                    }
                }

                JobTicket ticket = new JobTicket();
                ticket.Guid = originalJob.Guid;
                ticket.EnableInterSegment = job.EnableInterSegment;
                ticket.InterSegmentLength = Math.Max(job.InterSegmentLength, 1);
                ticket.EnableLubrication = job.EnableLubrication;
                ticket.Length = job.Length;
                ticket.WindingMethod = (JobWindingMethod)job.WindingMethod.Code;
                ticket.UploadStrategy = JobUploadStrategy;

                if (JobUnitsMethod == JobUnitsMethods.Device)
                {
                    ticket.NumberOfUnits = (uint)Math.Max(job.NumberOfUnits, 1);
                }

                //Spool parameters
                ticket.Spool = new JobSpool();
                job.SpoolType.MapPropertiesTo(ticket.Spool);
                ticket.Spool.JobSpoolType = (JobSpoolType)job.SpoolType.Code;

                //Override spool parameters from RML Spool calibration
                var rmlSpool = job.Rml.RmlsSpools.FirstOrDefault(x => x.SpoolType.Guid == job.SpoolType.Guid);
                if (rmlSpool != null)
                {
                    ticket.Spool.RotationsPerPassage = rmlSpool.RotationsPerPassage != null ? rmlSpool.RotationsPerPassage.Value : ticket.Spool.RotationsPerPassage;
                    ticket.Spool.Length = rmlSpool.Length != null ? rmlSpool.Length.Value : ticket.Spool.Length;
                    ticket.Spool.BackingRate = rmlSpool.BackingRate != null ? rmlSpool.BackingRate.Value : ticket.Spool.BackingRate;
                    ticket.Spool.BottomBackingRate = rmlSpool.BottomBackingRate != null ? rmlSpool.BottomBackingRate.Value : ticket.Spool.BottomBackingRate;
                    ticket.Spool.BtsrSpoolTension = rmlSpool.BtsrSpoolTension != null ? rmlSpool.BtsrSpoolTension.Value : ticket.Spool.BtsrSpoolTension;
                    ticket.Spool.StartOffsetPulses = rmlSpool.StartOffsetPulses != null ? rmlSpool.StartOffsetPulses.Value : ticket.Spool.StartOffsetPulses;
                    ticket.Spool.SegmentOffsetPulses = rmlSpool.SegmentOffsetPulses != null ? rmlSpool.SegmentOffsetPulses.Value : ticket.Spool.SegmentOffsetPulses;
                }

                //Override spool parameters from Machine Spool calibration
                var machineSpool = job.Machine.Spools.FirstOrDefault(x => x.SpoolType.Guid == job.SpoolType.Guid);
                if (machineSpool != null)
                {
                    ticket.Spool.LimitSwitchStartPointOffset = machineSpool.LimitSwitchStartPointOffset != null ? machineSpool.LimitSwitchStartPointOffset.Value : ticket.Spool.LimitSwitchStartPointOffset;
                    //ticket.Spool.StartOffsetPulses = machineSpool.StartOffsetPulses != null ? machineSpool.StartOffsetPulses.Value : ticket.Spool.StartOffsetPulses;
                    //ticket.Spool.BackingRate = machineSpool.BackingRate != null ? machineSpool.BackingRate.Value : ticket.Spool.BackingRate;
                    //ticket.Spool.SegmentOffsetPulses = machineSpool.SegmentOffsetPulses != null ? machineSpool.SegmentOffsetPulses.Value : ticket.Spool.SegmentOffsetPulses;
                    //ticket.Spool.BottomBackingRate = machineSpool.BottomBackingRate != null ? machineSpool.BottomBackingRate.Value : ticket.Spool.BottomBackingRate;
                }

                //Thread Parameters
                ticket.ThreadParameters = new ThreadParameters();
                job.Rml.MapPrimitivesTo(ticket.ThreadParameters);

                ProcessParameters process = new ProcessParameters();
                processParameters.MapPrimitivesTo(process);
                ticket.ProcessParameters = process;

                //Head Cleaning Parameters
                ticket.HeadCleaningParameters = new HeadCleaningParameters();
                ticket.HeadCleaningParameters.CleanerFlow = job.Rml.CleanerFlow;
                ticket.HeadCleaningParameters.ArcHeadCleaningMotorSpeed = job.Rml.ArcHeadCleaningMotorSpeed;

                //BTSR Parameters
                ticket.BtsrParameters = new PMR.BTSR.BtsrParameters();
                ticket.BtsrParameters.BtsrApplicationType = job.Rml.BtsrApplicationType != null ? (PMR.BTSR.BtsrApplicationType)job.Rml.BtsrApplicationType.Code : PMR.BTSR.BtsrApplicationType.Seamless;
                ticket.BtsrParameters.BtsrYarnType = job.Rml.BtsrYarnType != null ? (PMR.BTSR.BtsrYarnType)job.Rml.BtsrYarnType.Code : PMR.BTSR.BtsrYarnType.AllYarn3;
                ticket.BtsrParameters.TensionError = (float)job.Rml.BtsrTensionError;

                JobHandler handler = null;
                StorageFileHandler fileUploadHandler = null;

                bool requestSent = false;

                handler = new JobHandler(async () =>
                {
                    try
                    {
                        if (handler.CanCancel)
                        {
                            handler.CanCancel = false;
                            handler.IsCanceled = true;
                            LogManager.Log("Aborting current job...");
                            LogManager.Log($"Aborting current gradient generation...");
                            GradientGenerationConfiguration.AbortCurrentGeneration();

                            if (fileUploadHandler != null)
                            {
                                LogManager.Log("Job is currently uploading. Aborting file upload...");
                                await fileUploadHandler.Cancel();
                                fileUploadHandler = null;
                                LogManager.Log("Job upload canceled.");
                                OnPrintingAborted(handler, jobForJobRun);
                                handler.RaiseCanceled();
                                if (Status != MachineStatuses.Disconnected)
                                {
                                    UpdateStatus(MachineStatuses.ReadyToDye);
                                }
                            }
                            else
                            {
                                if (requestSent)
                                {
                                    var result = await SendRequest<AbortJobRequest, AbortJobResponse>(new AbortJobRequest(), new TransportRequestConfig() { ShouldLog = true });
                                }

                                SaveLastJobLiquidQuantities(clonedJob, originalJob.Machine.Configuration, processParameters, handler);
                                OnPrintingAborted(handler, jobForJobRun);
                                handler.RaiseCanceled();
                                if (Status != MachineStatuses.Disconnected)
                                {
                                    UpdateStatus(MachineStatuses.ReadyToDye);
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        handler.CanCancel = true;
                        LogManager.Log(ex, "Failed to cancel job.");
                    }
                }, clonedJob, ticket, processParameters, JobHandlingMode);

                handler.StatusChanged += (x, s) =>
                {
                    RunningJobStatus = s;
                };

                if (!job.IsAllSegmentsPerSpool)
                {
                    ContinueSingleSpoolJob(job.OrderedSegments.First(), job, processParameters, handler);
                    return handler;
                }

                ThreadFactory.StartNew(async () =>
                {
                    if (handler.IsCanceled)
                    {
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        return;
                    }

                    UpdateStatus(MachineStatuses.GettingReady);
                    RunningJob = clonedJob;
                    OnPrintingStarted(handler, clonedJob);

                    Thread.Sleep(100);

                    handler.RaiseStatusReceived(new JobStatus()
                    {
                        CurrentSegmentIndex = 0,
                        Progress = 0,
                        Message = "Preparing Job...",
                    });

                    foreach (var segment in segments)
                    {
                        try
                        {
                            if (segment is Segment simpleSegment)
                            {
                                ticket.Segments.Add(CreatePMRJobSegment(simpleSegment, originalJob, processParameters));
                            }
                            else if (segment is SegmentsGroup group)
                            {
                                List<JobSegment> groupSegments = new List<JobSegment>();

                                foreach (var innerSegment in group.OrderedSegments)
                                {
                                    groupSegments.Add(CreatePMRJobSegment(innerSegment, originalJob, processParameters));
                                }

                                for (int i = 0; i < group.Repeats; i++)
                                {
                                    ticket.Segments.AddRange(groupSegments.ToList());
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            handler.RaiseFailed(ex);
                            UpdateStatus(MachineStatuses.ReadyToDye);
                            return;
                        }

                        if (handler.IsCanceled)
                        {
                            UpdateStatus(MachineStatuses.ReadyToDye);
                            return;
                        }
                    }

                    //Log Job Outline (Only first and last brush stops if gradient).
                    var ticketToLog = ticket.Clone();
                    ticketToLog.Segments.Clear();

                    foreach (var seg in ticket.Segments)
                    {
                        JobSegment segmentToLog = new JobSegment();

                        segmentToLog.Length = seg.Length;
                        segmentToLog.BrushStops.Add(seg.BrushStops.First());

                        if (seg.BrushStops.Count > 1)
                        {
                            segmentToLog.BrushStops.Add(seg.BrushStops.Last());
                        }

                        ticketToLog.Segments.Add(segmentToLog);
                    }

                    if (!job.EnableInterSegment)
                    {
                        NormalizeJobTicket(ticketToLog, processParameters);
                    }

                    LogManager.Log($"Job outline for '{job.Name}':\n{ticketToLog.ToJsonString()}");

                    if (handler.IsCanceled)
                    {
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        return;
                    }

                    var segs = new List<JobSegment>();

                    if (JobUnitsMethod == JobUnitsMethods.Operator)
                    {
                        for (int i = 0; i < job.NumberOfUnits; i++)
                        {
                            foreach (var s in ticket.Segments)
                            {
                                var cloned = s.Clone();
                                segs.Add(cloned);
                            }
                        }
                    }
                    else
                    {
                        foreach (var s in ticket.Segments)
                        {
                            var cloned = s.Clone();
                            segs.Add(cloned);
                        }
                    }

                    if (segs.Count > 0)
                    {
                        ticket.Segments.Clear();
                        ticket.Segments.AddRange(segs);
                    }

                    if (!job.EnableInterSegment)
                    {
                        NormalizeJobTicket(ticket, processParameters);
                    }

                    request.JobTicket = ticket.Clone();

                    LogManager.Log($"Job upload method is set to {JobUploadStrategy}...");

                    if (handler.IsCanceled)
                    {
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        return;
                    }

                    var oldKeepAlive = UseKeepAlive;

                    if (requiredLiquids != null)
                    {
                        JobPrepareRequest prepareRequest = new JobPrepareRequest();
                        prepareRequest.ProcessParameters = ticket.ProcessParameters;

                        foreach (var requiredLiquid in requiredLiquids)
                        {
                            JobPrepareDispenser prepareDispenser = new JobPrepareDispenser();
                            prepareDispenser.DispenserLiquidType = (DispenserLiquidType)requiredLiquid.IdsPack.LiquidType.Type;
                            prepareDispenser.Index = requiredLiquid.IdsPack.PackIndex;
                            prepareDispenser.TotalNanoliter = requiredLiquid.Quantity;
                            prepareDispenser.Active = requiredLiquid.Quantity > 0;

                            prepareRequest.Dispensers.Add(prepareDispenser);
                        }

                        try
                        {
                            var response = await SendRequest<JobPrepareRequest, JobPrepareResponse>(prepareRequest, new TransportRequestConfig()
                            {
                                ShouldLog = true,
                                Timeout = TimeSpan.FromSeconds(10)
                            });
                        }
                        catch (ResponseErrorException ex)
                        {
                            LogManager.Log(ex, "Error sending job preparation request. Aborting job...");
                            UseKeepAlive = oldKeepAlive;
                            UpdateStatus(MachineStatuses.ReadyToDye);
                            OnPrintingFailed(handler, jobForJobRun, ex);
                            handler.RaiseFailed(ex);
                            return;
                        }
                        catch (Exception ex)
                        {
                            LogManager.Log(ex, "Error sending job preparation request.");
                        }
                    }

                    if (JobUploadStrategy == JobUploadStrategy.JobDescriptionFile)
                    {
                        LogManager.Log("Generating job description file...");

                        try
                        {
                            request.JobTicket.Segments.Clear();

                            JobDescriptionFile jobDescriptionFile = new JobDescriptionFile(ticket.Segments);
                            MemoryStream ms = jobDescriptionFile.ToStream();

                            handler.RaiseStatusReceived(new JobStatus()
                            {
                                CurrentSegmentIndex = 0,
                                Progress = 0,
                                Message = "Uploading job description file...",
                            });

                            if (handler.IsCanceled)
                            {
                                UpdateStatus(MachineStatuses.ReadyToDye);
                                return;
                            }

                            LogManager.Log("Creating storage API manager...");
                            var storage = CreateStorageManager();

                            if (handler.IsCanceled)
                            {
                                UpdateStatus(MachineStatuses.ReadyToDye);
                                return;
                            }

                            //Suppress keep alive while job uploads.
                            //storage.SuppressKeepAliveWhileFileUploads = true;
                            UseKeepAlive = false; //This is a work around for Shlomo not managing to keep alive while parsing the file.

                            LogManager.Log("Getting storage drive information...");
                            var storageInfo = await storage.GetStorageDrive();
                            LogManager.Log("Getting root folder information...");
                            var root_folder = await storage.GetRootFolder();

                            var existing_item = root_folder.Items.SingleOrDefault(x => x.Name == JOB_DESCRIPTION_FILE_NAME);
                            if (existing_item != null)
                            {
                                LogManager.Log("Removing previous job description file...");
                                await storage.DeleteItem(existing_item);
                            }

                            String job_file_path = Path.Combine(storageInfo.Root, JOB_DESCRIPTION_FILE_NAME);

                            LogManager.Log($"Uploading job description file '{job_file_path}' of size: {ms.Length} bytes...");

                            TaskCompletionSource<object> uploadCompletion = new TaskCompletionSource<object>();
                            _jobUploadingStartDate = DateTime.UtcNow;
                            fileUploadHandler = await storage.UploadFile(job_file_path, ms);
                            bool uploadCanceled = false;
                            Exception uploadException = null;
                            fileUploadHandler.Canceled += (_, __) =>
                            {
                                uploadCanceled = true;
                                uploadCompletion.SetResult(true);
                            };
                            fileUploadHandler.Completed += (_, __) =>
                            {
                                uploadCompletion.SetResult(true);
                            };
                            fileUploadHandler.Failed += (_, e) =>
                            {
                                uploadCompletion.SetException(e);
                            };

                            try
                            {
                                await uploadCompletion.Task;
                            }
                            catch (Exception ue)
                            {
                                if (uploadException != null)
                                {
                                    throw uploadException;
                                }
                                else
                                {
                                    throw ue;
                                }
                            }
                            finally
                            {
                                try
                                {
                                    fileUploadHandler = null;
                                    ms.Dispose();
                                }
                                catch { }
                            }

                            if (uploadCanceled)
                            {
                                return;
                            }
                            else
                            {
                                LogManager.Log("Job upload completed successfully.");
                            }

                            request.JobTicket.JobDescriptionFile = job_file_path;
                        }
                        catch (Exception ex)
                        {
                            UseKeepAlive = oldKeepAlive;
                            UpdateStatus(MachineStatuses.ReadyToDye);
                            OnPrintingFailed(handler, jobForJobRun, ex);
                            handler.RaiseFailed(ex);
                            return;
                        }
                    }
                    else
                    {
                        _jobUploadingStartDate = DateTime.UtcNow;
                    }

                    if (handler.IsCanceled)
                    {
                        UseKeepAlive = oldKeepAlive;
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        return;
                    }

                    _machineStatusBeforeJobStart = MachineStatus.Clone();

                    SaveCachedJobOperation(clonedJob); //Cache job and machine status for job resume!

                    bool responseLogged = false;
                    bool completed = false; //Use this in case Shlomo is sending progress after completion.

                    _jobHeatingStartDate = DateTime.UtcNow;

                    if (handler.IsCanceled)
                    {
                        UseKeepAlive = oldKeepAlive;
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        return;
                    }

                    SendContinuousRequest<JobRequest, JobResponse>(request, new TransportContinuousRequestConfig() { Timeout = TimeSpan.FromSeconds(10), ContinuousTimeout = TimeSpan.FromSeconds(10), ShouldLog = true }).Subscribe((response) =>
                    {
                        if (!completed)
                        {
                            handler.RaiseStatusReceived(response.Message.Status);
                            _last_job_status = handler.Status;

                            if (response.Message.Status.Progress > 0)
                            {
                                if (oldKeepAlive != UseKeepAlive)
                                {
                                    UseKeepAlive = oldKeepAlive;
                                }

                                if (_jobActualStartDate == null)
                                {
                                    _jobActualStartDate = DateTime.UtcNow;
                                }
                            }

                            if (!responseLogged)
                            {
                                requestSent = true;
                                responseLogged = true;
                            }

                            if (JobHandlingMode == JobHandlerModes.SettingUp)
                            {
                                if (response.Message.Status.Progress > processParameters.DryerBufferLengthMeters)
                                {
                                    if (!completed)
                                    {
                                        UpdateStatus(MachineStatuses.Printing);
                                    }
                                }
                            }
                            else
                            {
                                if (response.Message.Status.Progress > 0)
                                {
                                    if (!completed)
                                    {
                                        UpdateStatus(MachineStatuses.Printing);
                                    }
                                }
                            }
                        }

                    }, (ex) =>
                    {
                        if (!completed)
                        {
                            completed = true;

                            UseKeepAlive = oldKeepAlive;

                            if (Status != MachineStatuses.Disconnected)
                            {
                                UpdateStatus(MachineStatuses.ReadyToDye);
                            }

                            if (!handler.IsCanceled)
                            {
                                SaveLastJobLiquidQuantities(originalJob, originalJob.Machine.Configuration, processParameters, handler);

                                Exception finalException = ex;

                                if (ex is ContinuousResponseAbortedException continuousException)
                                {
                                    finalException = new ContinuousResponseAbortedException($"Job aborted by the embedded device ({continuousException.Container.ErrorMessage}).");
                                }

                                OnPrintingFailed(handler, jobForJobRun, finalException);
                                handler.RaiseFailed(finalException);
                            }
                        }
                    }, () =>
                    {
                        if (!completed)
                        {
                            completed = true;

                            UseKeepAlive = oldKeepAlive;

                            UpdateStatus(MachineStatuses.ReadyToDye);
                            SaveLastJobLiquidQuantities(clonedJob, originalJob.Machine.Configuration, processParameters, handler);
                            OnPrintingCompleted(handler, jobForJobRun);
                            handler.RaiseCompleted();
                        }
                    });
                });

                return handler;

            });
        }

        private void NormalizeJobTicket(JobTicket ticket, ProcessParametersTable processParameters)
        {
            var maxNanoStop = ticket.Segments.SelectMany(x => x.BrushStops).OrderBy(x => x.GetTotalNanoliterPerCentimeter()).Last();
            double maxNanoliter = maxNanoStop.GetTotalNanoliterPerCentimeter();

            LogManager.Log($"Normalizing brush stops TI quantities by {maxNanoliter} nanoliters...");

            foreach (var stop in ticket.Segments.SelectMany(x => x.BrushStops).Where(x => x != maxNanoStop))
            {
                stop.NormalizeStop(maxNanoliter, processParameters.MinInkUptake, processParameters.DyeingSpeed);
            }
        }

        /// <summary>
        /// Uploads the specified process parameters to the embedded device.
        /// </summary>
        /// <param name="processParameters">The process parameters.</param>
        /// <returns></returns>
        public async Task<UploadProcessParametersResponse> UploadProcessParameters(ProcessParametersTable processParameters)
        {
            UploadProcessParametersRequest request = new UploadProcessParametersRequest();
            request.ProcessParameters = new ProcessParameters();
            processParameters.MapPrimitivesTo(request.ProcessParameters);

            UploadProcessParametersResponse response = null;

            try
            {
                CurrentProcessParameters = processParameters;
                response = await SendRequest<UploadProcessParametersRequest, UploadProcessParametersResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Uploads the specified hardware configuration to the embedded device.
        /// </summary>
        /// <param name="hardwareVersion">Machine version.</param>
        /// <param name="configuration">Machine configuration.</param>
        /// <returns></returns>
        public async Task<UploadHardwareConfigurationResponse> UploadHardwareConfiguration(HardwareVersion hardwareVersion, Configuration configuration)
        {
            _machineConfiguration = configuration;

            LogManager.Log("Uploading hardware configuration...");

            try
            {
                hardwareVersion = configuration.GetHardwareConfiguration().Merge(hardwareVersion);
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, "Error merging hardware configuration to hardware version.");
            }

            HardwareConfiguration hardwareConfiguration = new HardwareConfiguration();

            foreach (var dancer in hardwareVersion.HardwareDancers.Where(x => x.Active))
            {
                PMR.Hardware.HardwareDancer item = new PMR.Hardware.HardwareDancer();
                dancer.MapPrimitivesTo(item);
                item.HardwareDancerType = (PMR.Hardware.HardwareDancerType)dancer.HardwareDancerType.Code;
                hardwareConfiguration.Dancers.Add(item);
            }

            foreach (var motor in hardwareVersion.HardwareMotors.Where(x => x.Active))
            {
                PMR.Hardware.HardwareMotor item = new PMR.Hardware.HardwareMotor();
                motor.MapPrimitivesTo(item);
                item.HardwareMotorType = (PMR.Hardware.HardwareMotorType)motor.HardwareMotorType.Code;
                hardwareConfiguration.Motors.Add(item);
            }

            foreach (var pid in hardwareVersion.HardwarePidControls.Where(x => x.Active))
            {
                PMR.Hardware.HardwarePidControl item = new PMR.Hardware.HardwarePidControl();
                pid.MapPrimitivesTo(item);
                item.HardwarePidControlType = (PMR.Hardware.HardwarePidControlType)pid.HardwarePidControlType.Code;
                hardwareConfiguration.PidControls.Add(item);
            }

            foreach (var winder in hardwareVersion.HardwareWinders.Where(x => x.Active))
            {
                PMR.Hardware.HardwareWinder item = new PMR.Hardware.HardwareWinder();
                winder.MapPrimitivesTo(item);
                item.HardwareWinderType = (PMR.Hardware.HardwareWinderType)winder.HardwareWinderType.Code;
                hardwareConfiguration.Winders.Add(item);
            }

            foreach (var sensor in hardwareVersion.HardwareSpeedSensors.Where(x => x.Active))
            {
                PMR.Hardware.HardwareSpeedSensor item = new PMR.Hardware.HardwareSpeedSensor();
                sensor.MapPrimitivesTo(item);
                item.HardwareSpeedSensorType = (PMR.Hardware.HardwareSpeedSensorType)sensor.HardwareSpeedSensorType.Code;
                hardwareConfiguration.SpeedSensors.Add(item);
            }

            foreach (var blower in hardwareVersion.HardwareBlowers.Where(x => x.Active))
            {
                PMR.Hardware.HardwareBlower item = new PMR.Hardware.HardwareBlower();
                blower.MapPrimitivesTo(item);
                item.HardwareBlowerType = (PMR.Hardware.HardwareBlowerType)blower.HardwareBlowerType.Code;
                hardwareConfiguration.Blowers.Add(item);
            }

            foreach (var breakSensor in hardwareVersion.HardwareBreakSensors.Where(x => x.Active))
            {
                PMR.Hardware.HardwareBreakSensor item = new PMR.Hardware.HardwareBreakSensor();
                breakSensor.MapPrimitivesTo(item);
                item.HardwareBreakSensorType = (PMR.Hardware.HardwareBreakSensorType)breakSensor.HardwareBreakSensorType.Code;
                hardwareConfiguration.BreakSensors.Add(item);
            }

            foreach (var idsPack in configuration.NoneEmptyIdsPacks.OrderBy(x => x.PackIndex))
            {
                PMR.Hardware.HardwareDispenser item = new PMR.Hardware.HardwareDispenser();
                item.Capacity = idsPack.Dispenser.DispenserType.Capacity;
                item.HardwareDispenserType = (PMR.Hardware.HardwareDispenserType)idsPack.Dispenser.DispenserType.Code;
                item.Index = idsPack.PackIndex;
                item.NlPerPulse = idsPack.Dispenser.NlPerPulse;
                hardwareConfiguration.Dispensers.Add(item);
            }

            UploadHardwareConfigurationRequest request = new UploadHardwareConfigurationRequest();
            request.HardwareConfiguration = hardwareConfiguration;

            UploadHardwareConfigurationResponse response = null;

            try
            {
                LogManager.Log("Checking if firmware version < 1.6...");

                if (DeviceInformation != null && DeviceInformation.Version != null)
                {
                    Version firmwareVersion = Version.Parse(DeviceInformation.Version);

                    if (firmwareVersion < new Version("1.6.0.0") && firmwareVersion > new Version("1.0.0.0"))
                    {
                        LogManager.Log("Firmware version is lower than '1.6'. Skipping hardware configuration upload...");
                        return new UploadHardwareConfigurationResponse();
                    }
                }
                else
                {
                    LogManager.Log("No device information found. Continuing...", LogCategory.Warning);
                }
            }
            catch (Exception ex)
            {
                LogManager.Log(ex, "Error occurred while checking for firmware version. Continuing...");
            }

            try
            {
                LogManager.Log($"Sending '{nameof(UploadHardwareConfigurationRequest)}'...");
                LogManager.Log($"{nameof(UploadHardwareConfigurationRequest)} request:\n{request.HardwareConfiguration.ToJsonString()}", LogCategory.Debug);

                CurrentHardwareConfiguration = hardwareConfiguration;
                response = await SendRequest<UploadHardwareConfigurationRequest, UploadHardwareConfigurationResponse>(request, new TransportRequestConfig() { ShouldLog = false });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Starts jogging the specified motor.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<MotorJoggingResponse> StartMotorJogging(MotorJoggingRequest request)
        {
            MotorJoggingResponse response = null;

            try
            {
                response = await SendRequest<MotorJoggingRequest, MotorJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Stops jogging the specified motor.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<MotorAbortJoggingResponse> StopMotorJogging(MotorAbortJoggingRequest request)
        {
            return await SendRequest<MotorAbortJoggingRequest, MotorAbortJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Starts homing the specified motor.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public IObservable<MotorHomingResponse> StartMotorHoming(MotorHomingRequest request)
        {
            return SendContinuousRequest<MotorHomingRequest, MotorHomingResponse>(request, new TransportContinuousRequestConfig() { ContinuousTimeout = ContinuousRequestTimeout, ShouldLog = true }).Select(x => x.Message);
        }

        /// <summary>
        /// Stops homing the specified motor.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<MotorAbortHomingResponse> StopMotorHoming(MotorAbortHomingRequest request)
        {
            return await SendRequest<MotorAbortHomingRequest, MotorAbortHomingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Starts jogging the specified dispenser.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<DispenserJoggingResponse> StartDispenserJogging(DispenserJoggingRequest request)
        {
            return await SendRequest<DispenserJoggingRequest, DispenserJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Stops jogging the specified dispenser.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<DispenserAbortJoggingResponse> StopDispenserJogging(DispenserAbortJoggingRequest request)
        {
            return await SendRequest<DispenserAbortJoggingRequest, DispenserAbortJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Starts homing the specified dispenser.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public IObservable<DispenserHomingResponse> StartDispenserHoming(DispenserHomingRequest request)
        {
            return SendContinuousRequest<DispenserHomingRequest, DispenserHomingResponse>(request, new TransportContinuousRequestConfig() { ContinuousTimeout = ContinuousRequestTimeout, ShouldLog = true }).Select(x => x.Message);
        }

        /// <summary>
        /// Stops homing the specified dispenser.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<DispenserAbortHomingResponse> StopDispenserHoming(DispenserAbortHomingRequest request)
        {
            return await SendRequest<DispenserAbortHomingRequest, DispenserAbortHomingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Turn on/off the specified digital output pin.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<SetDigitalOutResponse> SetDigitalOut(SetDigitalOutRequest request)
        {
            return await SendRequest<SetDigitalOutRequest, SetDigitalOutResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Starts jogging the thread motion system.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<ThreadJoggingResponse> StartThreadJogging(ThreadJoggingRequest request)
        {
            return await SendRequest<ThreadJoggingRequest, ThreadJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Stops jogging the thread motion system.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<ThreadAbortJoggingResponse> StopThreadJogging(ThreadAbortJoggingRequest request)
        {
            return await SendRequest<ThreadAbortJoggingRequest, ThreadAbortJoggingResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Sets the specified component value.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        public async Task<SetComponentValueResponse> SetComponentValue(SetComponentValueRequest request)
        {
            return await SendRequest<SetComponentValueRequest, SetComponentValueResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Sets the state of the specified heater type.
        /// </summary>
        /// <param name="heater">The heater.</param>
        /// <param name="setPoint">Set point temperature.</param>
        /// <returns></returns>
        public async Task<SetHeaterStateResponse> SetHeaterState(HeaterType heater, double setPoint)
        {
            SetHeaterStateResponse response = null;
            SetHeaterStateRequest request = new SetHeaterStateRequest()
            {
                HeaterType = heater,
                SetPoint = setPoint,
                IsActive = true,
            };

            try
            {
                response = await SendRequest<SetHeaterStateRequest, SetHeaterStateResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Sets the state of the specified blower.
        /// </summary>
        /// <param name="blower">The blower.</param>
        /// <param name="isActive">Blower on/off.</param>
        /// <param name="voltage">The voltage in millivolts.</param>
        /// <returns></returns>
        public async Task<SetBlowerStateResponse> SetBlowerState(PMR.Hardware.HardwareBlowerType blower, bool isActive, double voltage)
        {
            SetBlowerStateResponse response = null;
            SetBlowerStateRequest request = new SetBlowerStateRequest()
            {
                BlowerType = blower,
                Voltage = voltage,
                IsActive = isActive,
            };

            try
            {
                response = await SendRequest<SetBlowerStateRequest, SetBlowerStateResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Sets the state of the specified valve type.
        /// </summary>
        /// <param name="valve">The valve.</param>
        /// <param name="state">Valve state.</param>
        /// <returns></returns>
        public async Task<SetValveStateResponse> SetValveState(ValveType valve, ValveStateCode state)
        {
            SetValveStateResponse response = null;
            SetValveStateRequest request = new SetValveStateRequest()
            {
                ValveType = valve,
                State = state,
            };

            try
            {
                response = await SendRequest<SetValveStateRequest, SetValveStateResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Resolves the specified event type.
        /// </summary>
        /// <param name="eventType">Type of the event.</param>
        /// <returns></returns>
        public async Task<ResolveEventResponse> ResolveEvent(PMR.Diagnostics.EventType eventType)
        {
            ResolveEventRequest request = new ResolveEventRequest() { Type = eventType };
            return await SendRequest<ResolveEventRequest, ResolveEventResponse>(request, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Resets the embedded device.
        /// </summary>
        /// <returns></returns>
        public async Task<StubFpgaWriteRegResponse> Reset()
        {
            StubFpgaWriteRegResponse response = null;
            StubFpgaWriteRegRequest request = null;

            try
            {
                request = new StubFpgaWriteRegRequest()
                {
                    Address = 0x60000800 | 0x3D0,
                    Value = 0x0
                };

                response = await SendRequest<StubFpgaWriteRegRequest, StubFpgaWriteRegResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            Thread.Sleep(1000);

            try
            {
                request = new StubFpgaWriteRegRequest()
                {
                    Address = 0x60000800 | 0x3D0,
                    Value = 0x1
                };

                response = await SendRequest<StubFpgaWriteRegRequest, StubFpgaWriteRegResponse>(request, new TransportRequestConfig() { ShouldLog = true });
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return response;
        }

        /// <summary>
        /// Directs the embedded device to switch to stand-by mode.
        /// </summary>
        /// <returns></returns>
        public async Task StandBy()
        {
            LogManager.Log("Switching to stand-by mode...");
            await SendRequest<StandByRequest, StandByResponse>(new StandByRequest(), new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Resets the device through the DFU channel.
        /// </summary>
        /// <returns></returns>
        public Task ResetDFU()
        {
            return Task.Factory.StartNew(() =>
            {
                LogManager.Log("Performing device reset through DFU...");

                //LogManager.Log("Disconnecting Operator...");
                //Disconnect().Wait();
                //LogManager.Log("Operator disconnected.");

                FirmwareUpdateManager updateManager = new FirmwareUpdateManager();

                LogManager.Log("Initializing DFU API...");
                updateManager.Initialize();

                LogManager.Log("Enumerating DFU devices...");
                var device = updateManager.GetAvailableDevices(false).Where(x => !x.DeviceName.Contains("In-Circuit Debug Interface")).FirstOrDefault();
                if (device != null)
                {
                    LogManager.Log($"DFU device found: '{device.DeviceName}'.");
                    LogManager.Log("Switching to DFU mode...");
                    device.SwitchToDFUMode();
                    Thread.Sleep(6000);

                    LogManager.Log("Reattaching to DFU device...");
                    device = updateManager.GetAvailableDevices(false).Where(x => !x.DeviceName.Contains("In-Circuit Debug Interface")).FirstOrDefault();

                    if (device != null)
                    {
                        LogManager.Log("Resetting device...");
                        device.Reset();
                        Thread.Sleep(1000);
                        LogManager.Log("Reset completed.");
                    }
                    else
                    {
                        throw LogManager.Log(new Exception("DFU device not found."));
                    }
                }
                else
                {
                    throw LogManager.Log(new Exception("DFU device not found."));
                }
            });
        }

        /// <summary>
        /// Creates a storage manager for managing the machine file system.
        /// </summary>
        /// <returns></returns>
        public StorageManager CreateStorageManager()
        {
            return new StorageManager(this);
        }

        /// <summary>
        /// Upgrades the firmware.
        /// </summary>
        /// <param name="tfpStream">The TFP stream (Tango Firmware Package File).</param>
        /// <param name="isEmulated">Specify whether the connected machine is emulated and to skip the actual DFU interface.</param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        public virtual async Task<FirmwareUpgradeHandler> UpgradeFirmware(Stream tfpStream, bool isEmulated = false)
        {
            bool cancel = false;
            ZipFile zip = null;
            Action abortAction = null;

            var upgradeHandler = new FirmwareUpgradeHandler(() =>
            {
                cancel = true;

                abortAction?.Invoke();
            });

            try
            {
                LogManager.Log("Starting firmware upgrade...");
                LogManager.Log($"Firmware upgrade flags: {String.Join(", ", FirmwareUpgradeMode.GetFlags<FirmwareUpgradeModes>().Select(x => x.ToString()))}");

                if (!CanPrint)
                {
                    throw LogManager.Log(new InvalidOperationException($"Could not perform firmware upgrade while operator status is '{Status}'."));
                }

                LogManager.Log("Extracting tfp package...");
                var package_info = await GetFirmwarePackageInfo(tfpStream);
                tfpStream.Position = 0;

                LogManager.Log("Validating TFP package...");
                package_info.Validate();

                LogManager.Log("Reading zip stream...");
                zip = ZipFile.Read(tfpStream);

                double packageUploadTotal = 0;

                try
                {
                    packageUploadTotal = zip.Entries.Sum(x => x.UncompressedSize);
                }
                catch (Exception ex)
                {
                    LogManager.Log(ex, "Error calculating total package upload bytes.");
                }

                LogManager.Log("Creating storage manager...");
                var storage = CreateStorageManager();
                LogManager.Log("Getting storage drive information...");
                var drive = await storage.GetStorageDrive();
                LogManager.Log($"Storage drive info:\n{drive.ToJsonString()}");
                LogManager.Log("Getting root folder...");
                var root = await storage.GetRootFolder();
                LogManager.Log($"Root folder: '{root.Path}'");
                var existing_folder = root.Items.SingleOrDefault(x => x.Name == FIRMWARE_UPGRADE_FOLDER_NAME);
                if (existing_folder != null)
                {
                    LogManager.Log("Root folder exists. Deleting...");
                    await storage.DeleteItem(existing_folder);
                }

                String package_folder = Path.Combine(drive.Root, FIRMWARE_UPGRADE_FOLDER_NAME);

                LogManager.Log($"Creating new folder: '{package_folder}'.");
                await storage.CreateFolder(package_folder);

                List<StorageFileHandler> handlers = new List<StorageFileHandler>();
                List<ZipEntry> entries = zip.Entries.ToList();
                List<Stream> streams = new List<Stream>();

                LogManager.Log("Disabling keep alive...");
                var keepAlive = UseKeepAlive;
                UseKeepAlive = false;

                Action upgradeDFU = null;
                Action uploadNext = null;
                Action validate = null;
                Action activate = null;
                Action postActivation = null;

                UpdateStatus(MachineStatuses.Upgrading);

                abortAction = new Action(() =>
                {
                    UpdateStatus(MachineStatuses.ReadyToDye);
                });

                upgradeDFU = new Action(() =>
                {
                    try
                    {
                        if (FirmwareUpgradeMode.HasFlag(FirmwareUpgradeModes.DFU))
                        {
                            if (package_info.ContainsMcu())
                            {
                                LogManager.Log("DFU enabled. Starting upgrade via DFU...");
                                LogManager.Log("Extracting MCU file...");
                                ZipEntry mcuEntry = null;
                                try
                                {
                                    mcuEntry = entries.Single(x => x.FileName == package_info.FileDescriptors.Single(y => y.Destination == VersionFileDestination.Mcu).FileName);
                                    entries.Remove(mcuEntry);
                                }
                                catch (Exception ex)
                                {
                                    LogManager.Log(ex, "Error extracting MCU file from package.");
                                    upgradeHandler.RaiseFailed(new IOException("Error retrieving MCU file from package.", ex));
                                    return;
                                }

                                MemoryStream ms = new MemoryStream();
                                mcuEntry.Extract(ms);
                                ms.Position = 0;
                                byte[] data = ms.ToArray();
                                ms.Dispose();

                                FirmwareUpgradeManager upgradeManager = new FirmwareUpgradeManager();
                                upgradeManager.UpgradeProgress += (sender, e) =>
                                {
                                    upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Upgrading, e.State.ToDescription(), false, e.Total, e.Progress);
                                };

                                LogManager.Log("Disconnecting adapter...");
                                Adapter.Disconnect().Wait();

                                ResetEvents();
                                ResetInkFllingStatus();

                                try
                                {
                                    if (!isEmulated)
                                    {
                                        LogManager.Log("Upgrading...");
                                        upgradeManager.PerformUpgrade(data).Wait();
                                    }
                                    else
                                    {
                                        LogManager.Log("Upgrading (emulated)...");
                                        Thread.Sleep(3000);
                                    }
                                }
                                catch (Exception ex)
                                {
                                    LogManager.Log("Firmware upgrade failed while doing DFU upload. We need to destroy the whole transport layer.", LogCategory.Error);
                                    UpdateStatus(MachineStatuses.Disconnected);
                                    upgradeHandler.RaiseFailed(ex);
                                    OnFailed(ex);
                                    return;
                                }

                                LogManager.Log("Waiting for the device...");
                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Upgrading, "Waiting for the device...");
                                Thread.Sleep(5000);

                                LogManager.Log("Reconnecting adapter...");
                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Upgrading, "Connecting...");
                                Adapter.Connect().Wait();

                                Connect().Wait();

                                LogManager.Log("Connected...");
                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Upgrading, "Connected.");
                                Thread.Sleep(2000);

                                LogManager.Log("Waiting...");
                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Upgrading, "Waiting...");
                                Thread.Sleep(2000);

                                UpdateStatus(MachineStatuses.Upgrading);
                            }
                            else
                            {
                                LogManager.Log("DFU is enabled but no MCU file was found on the package. Skipping...");
                            }
                        }

                        //Upload tfp package only if specified in flag && package info contains more files other than the mcu bin file.
                        if (FirmwareUpgradeMode.HasFlag(FirmwareUpgradeModes.TFP_PACKAGE))
                        {
                            if (package_info.ContainsNoneMcu())
                            {
                                LogManager.Log("TFP package is enabled. Starting upload...");
                                uploadNext();
                            }
                            else
                            {
                                LogManager.Log("TFP package is enabled but no other files other than the MCU file were found on the package. Skipping...");
                                postActivation();
                            }
                        }
                        else
                        {
                            postActivation();
                        }
                    }
                    catch (Exception ex)
                    {
                        UpdateStatus(MachineStatuses.ReadyToDye);
                        upgradeHandler.RaiseFailed(ex);
                        return;
                    }
                });

                uploadNext = new Action(() =>
                {
                    if (entries.Count > 0)
                    {
                        try
                        {
                            var entry = entries.First();
                            entries.Remove(entry);

                            LogManager.Log($"Uploading file '{entry.FileName}'...");

                            var reader = entry.OpenReader();
                            streams.Add(reader);

                            var handler = storage.UploadFile(Path.Combine(package_folder, entry.FileName), reader).Result;
                            handlers.Add(handler);
                            handler.Canceled += (_, __) => { upgradeHandler.RaiseCanceled(); cancel = true; abortAction(); };
                            handler.Completed += (_, __) => uploadNext();
                            handler.Failed += (_, failedEx) => { upgradeHandler.RaiseFailed(failedEx); cancel = true; abortAction(); };
                            handler.Progress += (_, e) =>
                            {
                                if (cancel)
                                {
                                    handler.Cancel();
                                    return;
                                }

                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Uploading, $"Uploading '{entry.FileName}'...", false, packageUploadTotal, upgradeHandler.Current + e.Delta);
                            };
                        }
                        catch (Exception ex)
                        {
                            abortAction();
                            upgradeHandler.RaiseFailed(ex);
                        }
                    }
                    else
                    {
                        validate();
                    }
                });

                validate = new Action(() =>
                {
                    try
                    {
                        LogManager.Log("Validating version...");
                        streams.ForEach(x => x.Dispose());
                        upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Validating, "Validating version...");
                        var validateRequest = new ValidateVersionRequest();
                        validateRequest.Path = package_folder;
                        var validateResponse = SendRequest<ValidateVersionRequest, ValidateVersionResponse>(validateRequest, new TransportRequestConfig() { Timeout = TimeSpan.FromSeconds(10), ShouldLog = true }).Result;
                        activate();
                    }
                    catch (Exception ex)
                    {
                        upgradeHandler.RaiseFailed(ex);
                    }
                });

                activate = new Action(() =>
                {
                    try
                    {
                        TaskCompletionSource<object> activationCompletion = new TaskCompletionSource<object>();

                        bool completed = false;

                        LogManager.Log("Activating version...");

                        upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Activating, "Activating version...");

                        var activateRequest = new ActivateVersionRequest();
                        activateRequest.Path = package_folder;

                        SendContinuousRequest<ActivateVersionRequest, ActivateVersionResponse>(activateRequest, new TransportContinuousRequestConfig() { Timeout = TimeSpan.FromSeconds(10), ContinuousTimeout = TimeSpan.FromSeconds(10), ShouldLog = true })
                        .Subscribe((response) =>
                        {
                            if (!completed && response.Message.Progress > 0)
                            {
                                upgradeHandler.RaiseProgress(FirmwareUpgradeStatus.Activating, "Activating version...", false, response.Message.Total, response.Message.Progress);
                            }
                        }, (ex) =>
                        {
                            if (!completed)
                            {
                                completed = true;
                                activationCompletion.SetException(ex);
                            }
                        }, () =>
                        {
                            if (!completed)
                            {
                                completed = true;
                                activationCompletion.SetResult(true);
                            }
                        });

                        var result = activationCompletion.Task.GetAwaiter().GetResult();

                        postActivation();
                    }
                    catch (Exception ex)
                    {
                        upgradeHandler.RaiseFailed(ex);
                    }
                });

                postActivation = new Action(() =>
                {
                    LogManager.Log("Firmware upgrade completed.");
                    upgradeHandler.RaiseCompleted();
                    UpdateStatus(MachineStatuses.ReadyToDye);
                    LogManager.Log("Enabling keep alive...");
                    UseKeepAlive = keepAlive;
                });

                ThreadFactory.StartNew(() =>
                {
                    upgradeDFU();
                });

                return upgradeHandler;
            }
            catch (Exception)
            {
                if (zip != null)
                {
                    zip.Dispose();
                }

                throw;
            }
        }

        /// <summary>
        /// Validates the firmware package integrity.
        /// </summary>
        /// <param name="tfpStream">The TFP (Tango Firmware Package File) stream.</param>
        /// <returns></returns>
        public Task<VersionPackageDescriptor> GetFirmwarePackageInfo(Stream tfpStream)
        {
            return Task.Factory.StartNew<VersionPackageDescriptor>(() =>
            {
                using (ZipFile zip = ZipFile.Read(tfpStream))
                {
                    var reader = zip.Entries.SingleOrDefault(x => x.FileName == FIRMWARE_UPGRADE_CONFIG_FILE_NAME).OpenReader();
                    var info = VersionPackageDescriptor.Parser.ParseFrom(reader);
                    reader.Close();
                    reader.Dispose();
                    return info;
                }
            });
        }

        /// <summary>
        /// Directs the embedded device to validate the last uploaded firmware package.
        /// </summary>
        /// <returns></returns>
        public async Task ValidateFirmwareVersion(String path)
        {
            var validateRequest = new ValidateVersionRequest();
            validateRequest.Path = path;
            await SendRequest<ValidateVersionRequest, ValidateVersionResponse>(validateRequest, new TransportRequestConfig() { Timeout = TimeSpan.FromSeconds(10), ShouldLog = true });
        }

        /// <summary>
        /// Directs the embedded device to validate the last uploaded firmware package.
        /// </summary>
        /// <returns></returns>
        public async Task ActivateFirmwareVersion(String path)
        {
            var activateRequest = new ActivateVersionRequest();
            activateRequest.Path = path;
            await SendRequest<ActivateVersionRequest, ActivateVersionResponse>(activateRequest, new TransportRequestConfig() { Timeout = TimeSpan.FromSeconds(10), ShouldLog = true });
        }

        /// <summary>
        /// Turns off the machine.
        /// </summary>
        /// <returns></returns>
        public Task<PowerDownHandler> PowerDown()
        {
            if (_isPowerDownRequestInProgress)
            {
                throw new InvalidOperationException("Machine power down is already in progress.");
            }

            _isPowerDownRequestInProgress = true;

            PowerDownHandler handler = new PowerDownHandler(new Task(() =>
            {
                _isPowerDownRequestInProgress = false;
                Thread.Sleep(2000);
                var r = SendRequest<AbortPowerDownRequest, AbortPowerDownResponse>(new AbortPowerDownRequest(), new TransportRequestConfig() { ShouldLog = true }).Result;
            }));

            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(100);

                bool firstResponse = true;

                SendContinuousRequest<StartPowerDownRequest, StartPowerDownResponse>(new StartPowerDownRequest(), new TransportContinuousRequestConfig() { ContinuousTimeout = TimeSpan.FromSeconds(2), ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe((response) =>
                {
                    if (firstResponse)
                    {
                        firstResponse = false;
                        UpdateStatus(MachineStatuses.ShuttingDown);
                    }

                    handler.RaiseStatusChanged(response);
                }, (ex) =>
                {
                    if (_isPowerDownRequestInProgress)
                    {
                        _isPowerDownRequestInProgress = false;
                        LogManager.Log(ex, "Power down error.");
                        handler.RaiseFailed(ex);
                    }
                }, () =>
                {
                    if (_isPowerDownRequestInProgress)
                    {
                        _isPowerDownRequestInProgress = false;
                        handler.RaiseCompleted();
                    }
                });
            });

            PowerDownStarted?.Invoke(this, new PowerDownStartedEventArgs()
            {
                Handler = handler,
            });

            return Task.FromResult(handler);
        }

        /// <summary>
        /// Turns off the machine.
        /// </summary>
        /// <returns></returns>
        public Task<HeadCleaningHandler> PerformHeadCleaning(bool longCleaning)
        {
            if (_isHeadCleaningInProgress)
            {
                throw new InvalidOperationException("Head cleaning is already in progress.");
            }

            if (!CanPrint)
            {
                throw new InvalidOperationException($"Cannot perform head cleaning while machine status is '{Status}'.");
            }

            _isHeadCleaningInProgress = true;
            bool _completed = false;

            HeadCleaningHandler handler = null;
            handler = new HeadCleaningHandler(() =>
            {
                _isHeadCleaningInProgress = false;
                Thread.Sleep(1000);

                if (!_completed)
                {
                    _completed = true;
                    OnHeadCleaningEnded(handler, JobRunStatus.Aborted);
                }

                var r = SendRequest<AbortHeadCleaningRequest, AbortHeadCleaningResponse>(new AbortHeadCleaningRequest(), new TransportRequestConfig() { ShouldLog = true }).Result;
            });

            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(100);

                _lastJobLiquidQuantities = new List<BL.ValueObjects.JobRunLiquidQuantity>();
                _machineStatusBeforeJobStart = MachineStatus.Clone();

                bool firstResponse = true;
                _jobStartDate = DateTime.UtcNow;

                SendContinuousRequest<StartHeadCleaningRequest, StartHeadCleaningResponse>(new StartHeadCleaningRequest() { IsLongJob = longCleaning }, new TransportContinuousRequestConfig() { ContinuousTimeout = TimeSpan.FromSeconds(5), ShouldLog = true }).ObserveOn(new NewThreadScheduler()).Subscribe((response) =>
                {
                    if (firstResponse)
                    {
                        firstResponse = false;
                    }

                    handler.RaiseStatusChanged(response);
                }, (ex) =>
                {
                    if (!(ex is ContinuousResponseAbortedException))
                    {
                        _isHeadCleaningInProgress = false;
                        LogManager.Log(ex, "Head cleaning error.");
                        handler.RaiseFailed(ex);
                    }

                    if (!_completed)
                    {
                        _completed = true;
                        OnHeadCleaningEnded(handler, JobRunStatus.Failed);
                    }
                }, () =>
                {
                    _isHeadCleaningInProgress = false;
                    handler.RaiseCompleted();

                    if (!_completed)
                    {
                        _completed = true;
                        OnHeadCleaningEnded(handler, JobRunStatus.Completed);
                    }
                });
            });

            return Task.FromResult(handler);
        }

        /// <summary>
        /// Starts the automatic thread loading process.
        /// </summary>
        /// <returns></returns>
        public async Task StartThreadLoading()
        {
            var response = await SendRequest<TryThreadLoadingRequest, TryThreadLoadingResponse>(new TryThreadLoadingRequest());
        }

        /// <summary>
        /// Continues the current thread loading.
        /// </summary>
        /// <param name="processParameters">The process parameters.</param>
        /// <returns></returns>
        public async Task ContinueThreadLoading(ProcessParametersTable processParameters)
        {
            var process = processParameters.ToProcessParametersPMR();
            var r = await SendRequest<ContinueThreadLoadingRequest, ContinueThreadLoadingResponse>(new ContinueThreadLoadingRequest()
            {
                ProcessParameters = process,
            }, new TransportRequestConfig() { ShouldLog = true });
        }

        /// <summary>
        /// Attempts to jog the thread in order to check whether there are no thread breaking issues.
        /// </summary>
        /// <returns></returns>
        public async Task AttemptThreadJogging()
        {
            var r = await SendRequest<AttemptThreadJoggingRequest, AttemptThreadJoggingResponse>(new AttemptThreadJoggingRequest()
            {

            }, new TransportRequestConfig() { ShouldLog = true, Timeout = TimeSpan.FromSeconds(20) });
        }

        /// <summary>
        /// Emulates a hardware event that will last for the specified timeout.
        /// </summary>
        /// <param name="ev">Type of the event.</param>
        /// <param name="timeout">The timeout.</param>
        public async void PushEmulatedEvent(Event ev, TimeSpan timeout)
        {
            if (!_emulatedEvents.Exists(x => x.Type == ev.Type))
            {
                _emulatedEvents.Add(ev);
                await Task.Delay(timeout);
                _emulatedEvents.Remove(ev);
            }
        }

        /// <summary>
        /// Gets the last machine built-in test results.
        /// </summary>
        /// <returns></returns>
        public async Task<List<BitResultComposition>> GetBitResults(List<BL.Entities.BitType> bitTypes)
        {
            if (_bitResults == null)
            {
                var response = await SendRequest<BitResultsRequest, BitResultsResponse>(new BitResultsRequest(), new TransportRequestConfig()
                {
                    ShouldLog = true,
                    Timeout = TimeSpan.FromMinutes(5)
                });

                var compositions = new List<BitResultComposition>();

                foreach (var bitType in bitTypes)
                {
                    BitResultComposition composition = new BitResultComposition();
                    composition.BitType = bitType;
                    composition.BitResult = new BitResult() { BitType = (PMR.Diagnostics.BitType)bitType.Code, Description = "Skipped" };
                    compositions.Add(composition);
                }

                foreach (var bitResult in response.Message.Results)
                {
                    var composition = compositions.SingleOrDefault(x => x.BitType.Code == bitResult.BitType.ToInt32());

                    if (composition != null)
                    {
                        composition.BitResult = bitResult;
                    }
                }

                compositions = compositions.OrderBy(x => x.Priority).ToList();

                _bitResults = compositions;
            }

            return _bitResults;
        }

        /// <summary>
        /// Notifies the remote machine about spool type change.
        /// If the machine is not connected the spool type will be added to the connection request.
        /// </summary>
        /// <param name="spoolType">Type of the spool.</param>
        public async Task SetSpoolType(JobSpoolType spoolType)
        {
            _currentSpoolType = spoolType;

            if (IsConnected)
            {
                LogManager.Log($"Changing spool type to: '{spoolType}'.");

                try
                {
                    await SendRequest<SpoolTypeChangedRequest, SpoolTypeChangedResponse>(new SpoolTypeChangedRequest()
                    {
                        SpoolType = spoolType
                    });
                }
                catch (Exception ex)
                {
                    LogManager.Log(ex, "Error changing the spool type on the machine.");
                }
            }
        }

        #endregion
    }
}