File indexing completed on 2024-04-28 03:43:14

0001 /*  Ekos state machine for the Capture module
0002     SPDX-FileCopyrightText: 2022 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "capturemodulestate.h"
0008 #include "ekos/manager/meridianflipstate.h"
0009 #include "ekos/capture/sequencejob.h"
0010 #include "ekos/capture/sequencequeue.h"
0011 #include "ekos/capture/refocusstate.h"
0012 #include "fitsviewer/fitsdata.h"
0013 
0014 #include "ksnotification.h"
0015 #include <ekos_capture_debug.h>
0016 
0017 #define GD_TIMER_TIMEOUT    60000
0018 
0019 namespace Ekos
0020 {
0021 CaptureModuleState::CaptureModuleState(QObject *parent): QObject{parent}
0022 {
0023     m_sequenceQueue.reset(new SequenceQueue());
0024     m_refocusState.reset(new RefocusState());
0025     m_TargetADUTolerance = Options::calibrationADUValueTolerance();
0026     connect(m_refocusState.get(), &RefocusState::newLog, this, &CaptureModuleState::newLog);
0027 
0028     getGuideDeviationTimer().setInterval(GD_TIMER_TIMEOUT);
0029     connect(&m_guideDeviationTimer, &QTimer::timeout, this, &CaptureModuleState::checkGuideDeviationTimeout);
0030 
0031     setCalibrationPreAction(Options::calibrationPreActionIndex());
0032     setFlatFieldDuration(static_cast<FlatFieldDuration>(Options::calibrationFlatDurationIndex()));
0033     wallCoord().setAz(Options::calibrationWallAz());
0034     wallCoord().setAlt(Options::calibrationWallAlt());
0035     setTargetADU(Options::calibrationADUValue());
0036 }
0037 
0038 QList<SequenceJob *> &CaptureModuleState::allJobs()
0039 {
0040     return m_sequenceQueue->allJobs();
0041 }
0042 
0043 const QUrl &CaptureModuleState::sequenceURL() const
0044 {
0045     return m_sequenceQueue->sequenceURL();
0046 }
0047 
0048 void CaptureModuleState::setSequenceURL(const QUrl &newSequenceURL)
0049 {
0050     m_sequenceQueue->setSequenceURL(newSequenceURL);
0051 }
0052 
0053 void CaptureModuleState::setActiveJob(SequenceJob *value)
0054 {
0055     // do nothing if active job is not changed
0056     if (m_activeJob == value)
0057         return;
0058 
0059     // clear existing job connections
0060     if (m_activeJob != nullptr)
0061     {
0062         disconnect(this, nullptr, m_activeJob, nullptr);
0063         disconnect(m_activeJob, nullptr, this, nullptr);
0064         // ensure that the device adaptor does not send any new events
0065         m_activeJob->disconnectDeviceAdaptor();
0066     }
0067 
0068     // set the new value
0069     m_activeJob = value;
0070 
0071     // create job connections
0072     if (m_activeJob != nullptr)
0073     {
0074         // connect job with device adaptor events
0075         m_activeJob->connectDeviceAdaptor();
0076         // forward signals to the sequence job
0077         connect(this, &CaptureModuleState::newGuiderDrift, m_activeJob, &SequenceJob::updateGuiderDrift);
0078         // react upon sequence job signals
0079         connect(m_activeJob, &SequenceJob::prepareState, this, &CaptureModuleState::updatePrepareState);
0080         connect(m_activeJob, &SequenceJob::prepareComplete, this, [this](bool success)
0081         {
0082             if (success)
0083             {
0084                 setCaptureState(CAPTURE_PROGRESS);
0085                 emit executeActiveJob();
0086             }
0087             else
0088             {
0089                 qWarning(KSTARS_EKOS_CAPTURE) << "Capture preparation failed, aborting.";
0090                 setCaptureState(CAPTURE_ABORTED);
0091                 emit abortCapture();
0092             }
0093         }, Qt::UniqueConnection);
0094         connect(m_activeJob, &SequenceJob::abortCapture, this, &CaptureModuleState::abortCapture);
0095         connect(m_activeJob, &SequenceJob::captureStarted, this, &CaptureModuleState::captureStarted);
0096         connect(m_activeJob, &SequenceJob::newLog, this, &CaptureModuleState::newLog);
0097         // forward the devices and attributes
0098         m_activeJob->updateDeviceStates();
0099         m_activeJob->setAutoFocusReady(getRefocusState()->isAutoFocusReady());
0100     }
0101 
0102 }
0103 
0104 int CaptureModuleState::activeJobID()
0105 {
0106     if (m_activeJob == nullptr)
0107         return -1;
0108 
0109     for (int i = 0; i < allJobs().count(); i++)
0110     {
0111         if (m_activeJob == allJobs().at(i))
0112             return i;
0113     }
0114 
0115     return -1;
0116 
0117 }
0118 
0119 void CaptureModuleState::initCapturePreparation()
0120 {
0121     setStartingCapture(false);
0122 
0123     // Reset progress option if there is no captured frame map set at the time of start - fixes the end-user setting the option just before starting
0124     setIgnoreJobProgress(!hasCapturedFramesMap() && Options::alwaysResetSequenceWhenStarting());
0125 
0126     // Refocus timer should not be reset on deviation error
0127     if (isGuidingDeviationDetected() == false && getCaptureState() != CAPTURE_SUSPENDED)
0128     {
0129         // start timer to measure time until next forced refocus
0130         getRefocusState()->startRefocusTimer();
0131     }
0132 
0133     // Only reset these counters if we are NOT restarting from deviation errors
0134     // So when starting a new job or fresh then we reset them.
0135     if (isGuidingDeviationDetected() == false)
0136     {
0137         resetDitherCounter();
0138         getRefocusState()->resetInSequenceFocusCounter();
0139         getRefocusState()->setAdaptiveFocusDone(false);
0140     }
0141 
0142     setGuidingDeviationDetected(false);
0143     resetSpikesDetected();
0144 
0145     setCaptureState(CAPTURE_PROGRESS);
0146     setBusy(true);
0147 
0148     if (Options::enforceGuideDeviation() && isGuidingOn() == false)
0149         emit newLog(i18n("Warning: Guide deviation is selected but autoguide process was not started."));
0150 
0151 
0152 }
0153 
0154 void CaptureModuleState::setCaptureState(CaptureState value)
0155 {
0156     bool pause_planned = false;
0157     // handle new capture state
0158     switch (value)
0159     {
0160         case CAPTURE_IDLE:
0161         case CAPTURE_ABORTED:
0162         case CAPTURE_SUSPENDED:
0163         case CAPTURE_PAUSED:
0164             // meridian flip may take place if requested
0165             if (mf_state->getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED)
0166                 mf_state->updateMeridianFlipStage(MeridianFlipState::MF_READY);
0167             break;
0168         case CAPTURE_IMAGE_RECEIVED:
0169             // remember pause planning before receiving an image
0170             pause_planned = (m_CaptureState == CAPTURE_PAUSE_PLANNED);
0171             break;
0172         default:
0173             // do nothing
0174             break;
0175     }
0176 
0177     // Only emit status if it changed
0178     if (m_CaptureState != value)
0179     {
0180         qCDebug(KSTARS_EKOS_CAPTURE()) << "Capture State changes from" << getCaptureStatusString(
0181                                            m_CaptureState) << "to" << getCaptureStatusString(value);
0182         m_CaptureState = value;
0183         getMeridianFlipState()->setCaptureState(m_CaptureState);
0184         emit newStatus(m_CaptureState);
0185         // reset to planned state if necessary
0186         if (pause_planned)
0187         {
0188             m_CaptureState = CAPTURE_PAUSE_PLANNED;
0189             emit newStatus(m_CaptureState);
0190         }
0191     }
0192 }
0193 
0194 void CaptureModuleState::setGuideState(GuideState state)
0195 {
0196     if (state != m_GuideState)
0197         qCDebug(KSTARS_EKOS_CAPTURE) << "Guiding state changed from" <<
0198                                      Ekos::getGuideStatusString(m_GuideState)
0199                                      << "to" << Ekos::getGuideStatusString(state);
0200     switch (state)
0201     {
0202         case GUIDE_IDLE:
0203         case GUIDE_GUIDING:
0204         case GUIDE_CALIBRATION_SUCCESS:
0205             break;
0206 
0207         case GUIDE_ABORTED:
0208         case GUIDE_CALIBRATION_ERROR:
0209             processGuidingFailed();
0210             break;
0211 
0212         case GUIDE_DITHERING_SUCCESS:
0213             qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering succeeded, capture state" << getCaptureStatusString(
0214                                             getCaptureState());
0215             // do nothing if something happened during dithering
0216             appendLogText(i18n("Dithering succeeded."));
0217             if (getCaptureState() != CAPTURE_DITHERING)
0218                 break;
0219 
0220             if (Options::guidingSettle() > 0)
0221             {
0222                 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
0223                 appendLogText(i18n("Dither complete. Resuming in %1 seconds...", Options::guidingSettle()));
0224                 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
0225                 {
0226                     setDitheringState(IPS_OK);
0227                 });
0228             }
0229             else
0230             {
0231                 appendLogText(i18n("Dither complete."));
0232                 setDitheringState(IPS_OK);
0233             }
0234             break;
0235 
0236         case GUIDE_DITHERING_ERROR:
0237             qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering failed, capture state" << getCaptureStatusString(
0238                                             getCaptureState());
0239             if (getCaptureState() != CAPTURE_DITHERING)
0240                 break;
0241 
0242             if (Options::guidingSettle() > 0)
0243             {
0244                 // N.B. Do NOT convert to i18np since guidingRate is DOUBLE value (e.g. 1.36) so we always use plural with that.
0245                 appendLogText(i18n("Warning: Dithering failed. Resuming in %1 seconds...", Options::guidingSettle()));
0246                 // set dithering state to OK after settling time and signal to proceed
0247                 QTimer::singleShot(Options::guidingSettle() * 1000, this, [this]()
0248                 {
0249                     setDitheringState(IPS_OK);
0250                 });
0251             }
0252             else
0253             {
0254                 appendLogText(i18n("Warning: Dithering failed."));
0255                 // signal OK so that capturing may continue although dithering failed
0256                 setDitheringState(IPS_OK);
0257             }
0258 
0259             break;
0260 
0261         default:
0262             break;
0263     }
0264 
0265     m_GuideState = state;
0266     // forward it to the currently active sequence job
0267     if (m_activeJob != nullptr)
0268         m_activeJob->setCoreProperty(SequenceJob::SJ_GuiderActive, isActivelyGuiding());
0269 }
0270 
0271 
0272 
0273 void CaptureModuleState::setCurrentFilterPosition(int position, const QString &name, const QString &focusFilterName)
0274 {
0275     m_CurrentFilterPosition = position;
0276     if (position > 0)
0277     {
0278         m_CurrentFilterName = name;
0279         m_CurrentFocusFilterName = focusFilterName;
0280     }
0281     else
0282     {
0283         m_CurrentFilterName = "--";
0284         m_CurrentFocusFilterName = "--";
0285     }
0286 }
0287 
0288 QSharedPointer<MeridianFlipState> CaptureModuleState::getMeridianFlipState()
0289 {
0290     // lazy instantiation
0291     if (mf_state.isNull())
0292         mf_state.reset(new MeridianFlipState());
0293 
0294     return mf_state;
0295 }
0296 
0297 void CaptureModuleState::setMeridianFlipState(QSharedPointer<MeridianFlipState> state)
0298 {
0299     // clear old state machine
0300     if (! mf_state.isNull())
0301     {
0302         mf_state->disconnect(this);
0303         mf_state->deleteLater();
0304     }
0305 
0306     mf_state = state;
0307     connect(mf_state.data(), &Ekos::MeridianFlipState::newMountMFStatus, this, &Ekos::CaptureModuleState::updateMFMountState,
0308             Qt::UniqueConnection);
0309 }
0310 
0311 void CaptureModuleState::setObserverName(const QString &value)
0312 {
0313     m_ObserverName = value;
0314     Options::setDefaultObserver(value);
0315 }
0316 
0317 void CaptureModuleState::setBusy(bool busy)
0318 {
0319     m_Busy = busy;
0320     emit captureBusy(busy);
0321 }
0322 
0323 void CaptureModuleState::decreaseDitherCounter()
0324 {
0325     if (m_ditherCounter > 0)
0326         --m_ditherCounter;
0327 }
0328 
0329 void CaptureModuleState::resetDitherCounter()
0330 {
0331     uint value = 0;
0332     if (m_activeJob)
0333         value = m_activeJob->getCoreProperty(SequenceJob::SJ_DitherPerJobFrequency).toInt(0);
0334 
0335     if (value > 0)
0336         m_ditherCounter = value;
0337     else
0338         m_ditherCounter = Options::ditherFrames();
0339 }
0340 
0341 bool CaptureModuleState::checkDithering()
0342 {
0343     // No need if preview only
0344     if (m_activeJob && m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW)
0345         return false;
0346 
0347     if ( (Options::ditherEnabled() || Options::ditherNoGuiding())
0348             // 2017-09-20 Jasem: No need to dither after post meridian flip guiding
0349             && getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_GUIDING
0350             // We must be either in guide mode or if non-guide dither (via pulsing) is enabled
0351             && (getGuideState() == GUIDE_GUIDING || Options::ditherNoGuiding())
0352             // Must be only done for light frames
0353             && (m_activeJob != nullptr && m_activeJob->getFrameType() == FRAME_LIGHT)
0354             // Check dither counter
0355             && m_ditherCounter == 0)
0356     {
0357         // reset the dither counter
0358         resetDitherCounter();
0359 
0360         qCInfo(KSTARS_EKOS_CAPTURE) << "Dithering...";
0361         appendLogText(i18n("Dithering..."));
0362 
0363         setCaptureState(CAPTURE_DITHERING);
0364         setDitheringState(IPS_BUSY);
0365 
0366         return true;
0367     }
0368     // no dithering required
0369     return false;
0370 }
0371 
0372 void CaptureModuleState::updateMFMountState(MeridianFlipState::MeridianFlipMountState status)
0373 {
0374     qCDebug(KSTARS_EKOS_CAPTURE) << "updateMFMountState: " << MeridianFlipState::meridianFlipStatusString(status);
0375 
0376     switch (status)
0377     {
0378         case MeridianFlipState::MOUNT_FLIP_NONE:
0379             // MF_NONE as external signal ignored so that re-alignment and guiding are processed first
0380             if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
0381                 updateMeridianFlipStage(MeridianFlipState::MF_NONE);
0382             break;
0383 
0384         case MeridianFlipState::MOUNT_FLIP_PLANNED:
0385             if (getMeridianFlipState()->getMeridianFlipStage() > MeridianFlipState::MF_REQUESTED)
0386             {
0387                 // This should never happen, since a meridian flip seems to be ongoing
0388                 qCritical(KSTARS_EKOS_CAPTURE) << "Accepting meridian flip request while being in stage " <<
0389                                                getMeridianFlipState()->getMeridianFlipStage();
0390             }
0391 
0392             // If we are autoguiding, we should resume autoguiding after flip
0393             getMeridianFlipState()->setResumeGuidingAfterFlip(isGuidingOn());
0394 
0395             // mark flip as requested
0396             updateMeridianFlipStage(MeridianFlipState::MF_REQUESTED);
0397             // if capture is not running, immediately accept it
0398             if (m_CaptureState == CAPTURE_IDLE || m_CaptureState == CAPTURE_ABORTED
0399                     || m_CaptureState == CAPTURE_COMPLETE || m_CaptureState == CAPTURE_PAUSED)
0400                 getMeridianFlipState()->updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED);
0401 
0402             break;
0403 
0404         case MeridianFlipState::MOUNT_FLIP_RUNNING:
0405             updateMeridianFlipStage(MeridianFlipState::MF_INITIATED);
0406             setCaptureState(CAPTURE_MERIDIAN_FLIP);
0407             break;
0408 
0409         case MeridianFlipState::MOUNT_FLIP_COMPLETED:
0410             updateMeridianFlipStage(MeridianFlipState::MF_COMPLETED);
0411             break;
0412 
0413         default:
0414             break;
0415 
0416     }
0417 }
0418 
0419 void CaptureModuleState::updateMeridianFlipStage(const MeridianFlipState::MFStage &stage)
0420 {
0421     // forward the stage to the module state
0422     getMeridianFlipState()->updateMeridianFlipStage(stage);
0423 
0424     // handle state changes for other modules
0425     switch (stage)
0426     {
0427         case MeridianFlipState::MF_READY:
0428             break;
0429 
0430         case MeridianFlipState::MF_INITIATED:
0431             emit meridianFlipStarted();
0432             break;
0433 
0434         case MeridianFlipState::MF_COMPLETED:
0435 
0436             // Reset HFR Check counter after meridian flip
0437             if (getRefocusState()->isInSequenceFocus())
0438             {
0439                 qCDebug(KSTARS_EKOS_CAPTURE) << "Resetting HFR Check counter after meridian flip.";
0440                 //firstAutoFocus = true;
0441                 getRefocusState()->setInSequenceFocusCounter(0);
0442             }
0443 
0444             // after a meridian flip we do not need to dither
0445             if ( Options::ditherEnabled() || Options::ditherNoGuiding())
0446                 resetDitherCounter();
0447 
0448             // if requested set flag so it perform refocus before next frame
0449             if (Options::refocusAfterMeridianFlip() == true)
0450                 getRefocusState()->setRefocusAfterMeridianFlip(true);
0451             // If dome is syncing, wait until it stops
0452             if (hasDome && (m_domeState == ISD::Dome::DOME_MOVING_CW || m_domeState == ISD::Dome::DOME_MOVING_CCW))
0453                 return;
0454 
0455             KSNotification::event(QLatin1String("MeridianFlipCompleted"), i18n("Meridian flip is successfully completed"),
0456                                   KSNotification::Capture);
0457 
0458             getMeridianFlipState()->processFlipCompleted();
0459 
0460             // if the capturing has been paused before the flip, reset the state to paused, otherwise to idle
0461             setCaptureState(m_ContinueAction == CONTINUE_ACTION_NONE ? CAPTURE_IDLE : CAPTURE_PAUSED);
0462             break;
0463 
0464         default:
0465             break;
0466     }
0467     // forward the new stage
0468     emit newMeridianFlipStage(stage);
0469 }
0470 
0471 bool CaptureModuleState::checkMeridianFlipActive()
0472 {
0473     return (getMeridianFlipState()->checkMeridianFlipRunning() ||
0474             checkPostMeridianFlipActions() ||
0475             checkMeridianFlipReady());
0476 }
0477 
0478 bool CaptureModuleState::checkMeridianFlipReady()
0479 {
0480     if (hasTelescope == false)
0481         return false;
0482 
0483     // If active job is taking flat field image at a wall source
0484     // then do not flip.
0485     if (m_activeJob && m_activeJob->getFrameType() == FRAME_FLAT && m_activeJob->getCalibrationPreAction() & ACTION_WALL)
0486         return false;
0487 
0488     if (getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_REQUESTED)
0489         // if no flip has been requested or is already ongoing
0490         return false;
0491 
0492     // meridian flip requested or already in action
0493 
0494     // Reset frame if we need to do focusing later on
0495     if (m_refocusState->isInSequenceFocus() ||
0496             (Options::enforceRefocusEveryN() && m_refocusState->getRefocusEveryNTimerElapsedSec() > 0))
0497         emit resetFocus();
0498 
0499     // signal that meridian flip may take place
0500     if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED)
0501         getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_READY);
0502 
0503 
0504     return true;
0505 }
0506 
0507 bool CaptureModuleState::checkPostMeridianFlipActions()
0508 {
0509     // step 1: check if post flip alignment is running
0510     if (m_CaptureState == CAPTURE_ALIGNING || checkAlignmentAfterFlip())
0511         return true;
0512 
0513     // step 2: check if post flip guiding is running
0514     // MF_NONE is set as soon as guiding is running and the guide deviation is below the limit
0515     if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_COMPLETED && m_GuideState != GUIDE_GUIDING
0516             && checkGuidingAfterFlip())
0517         return true;
0518 
0519     // step 3: in case that a meridian flip has been completed and a guide deviation limit is set, we wait
0520     //         until the guide deviation is reported to be below the limit (@see setGuideDeviation(double, double)).
0521     //         Otherwise the meridian flip is complete
0522     if (m_CaptureState == CAPTURE_CALIBRATING
0523             && getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
0524     {
0525         if (Options::enforceGuideDeviation() || Options::enforceStartGuiderDrift())
0526             return true;
0527         else
0528             updateMeridianFlipStage(MeridianFlipState::MF_NONE);
0529     }
0530 
0531     // all actions completed or no flip running
0532     return false;
0533 }
0534 
0535 bool CaptureModuleState::checkGuidingAfterFlip()
0536 {
0537     // if no meridian flip has completed, we do not touch guiding
0538     if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
0539         return false;
0540     // If we're not autoguiding then we're done
0541     if (getMeridianFlipState()->resumeGuidingAfterFlip() == false)
0542     {
0543         getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_NONE);
0544         return false;
0545     }
0546 
0547     // if we are waiting for a calibration, start it
0548     if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_COMPLETED
0549             && getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_GUIDING)
0550     {
0551         appendLogText(i18n("Performing post flip re-calibration and guiding..."));
0552 
0553         setCaptureState(CAPTURE_CALIBRATING);
0554 
0555         getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_GUIDING);
0556         emit guideAfterMeridianFlip();
0557         return true;
0558     }
0559     else if (m_CaptureState == CAPTURE_CALIBRATING)
0560     {
0561         if (getGuideState() == GUIDE_CALIBRATION_ERROR || getGuideState() == GUIDE_ABORTED)
0562         {
0563             // restart guiding after failure
0564             appendLogText(i18n("Post meridian flip calibration error. Restarting..."));
0565             emit guideAfterMeridianFlip();
0566             return true;
0567         }
0568         else if (getGuideState() != GUIDE_GUIDING)
0569             // waiting for guiding to start
0570             return true;
0571         else
0572             // guiding is running
0573             return false;
0574     }
0575     else
0576         // in all other cases, do not touch
0577         return false;
0578 }
0579 
0580 void CaptureModuleState::processGuidingFailed()
0581 {
0582     if (m_FocusState > FOCUS_PROGRESS)
0583     {
0584         appendLogText(i18n("Autoguiding stopped. Waiting for autofocus to finish..."));
0585     }
0586     // If Autoguiding was started before and now stopped, let's abort (unless we're doing a meridian flip)
0587     else if (isGuidingOn()
0588              && getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_NONE &&
0589              // JM 2022.08.03: Only abort if the current job is LIGHT. For calibration frames, we can ignore guide failures.
0590              ((m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT) ||
0591               m_CaptureState == CAPTURE_SUSPENDED || m_CaptureState == CAPTURE_PAUSED))
0592     {
0593         appendLogText(i18n("Autoguiding stopped. Aborting..."));
0594         emit abortCapture();
0595     }
0596     else if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
0597     {
0598         if (increaseAlignmentRetries() >= 3)
0599         {
0600             appendLogText(i18n("Post meridian flip calibration error. Aborting..."));
0601             emit abortCapture();
0602         }
0603     }
0604 }
0605 
0606 void CaptureModuleState::updateAdaptiveFocusState(bool success)
0607 {
0608     m_refocusState->setAdaptiveFocusDone(true);
0609 
0610     // Always process the adaptive focus state change, incl if a MF has also just started
0611     if (success)
0612         qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus completed successfully";
0613     else
0614         qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus failed";
0615 
0616     m_refocusState->setAutoFocusReady(true);
0617     // forward to the active job
0618     if (m_activeJob != nullptr)
0619         m_activeJob->setAutoFocusReady(true);
0620 
0621     setFocusState(FOCUS_COMPLETE);
0622     emit newLog(i18n(success ? "Adaptive focus complete." : "Adaptive focus failed. Continuing..."));
0623 }
0624 
0625 void CaptureModuleState::updateFocusState(FocusState state)
0626 {
0627     if (state != m_FocusState)
0628         qCDebug(KSTARS_EKOS_CAPTURE) << "Focus State changed from" <<
0629                                      Ekos::getFocusStatusString(m_FocusState) <<
0630                                      "to" << Ekos::getFocusStatusString(state);
0631     setFocusState(state);
0632 
0633     // Do not process if meridian flip in progress
0634     if (getMeridianFlipState()->checkMeridianFlipRunning())
0635         return;
0636 
0637     switch (state)
0638     {
0639         // Do not process when aborted
0640         case FOCUS_ABORTED:
0641             if (!(getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_INITIATED))
0642             {
0643                 // Meridian flip will abort focusing. In this case, after the meridian flip has completed capture
0644                 // will restart the re-focus attempt. Therefore we only abort capture if meridian flip is not running.
0645                 emit newFocusStatus(state);
0646                 appendLogText(i18n("Autofocus failed. Aborting exposure..."));
0647                 emit abortCapture();
0648             }
0649             break;
0650         case FOCUS_COMPLETE:
0651             // enable option to have a refocus event occur if HFR goes over threshold
0652             m_refocusState->setAutoFocusReady(true);
0653             // forward to the active job
0654             if (m_activeJob != nullptr)
0655                 m_activeJob->setAutoFocusReady(true);
0656             // reset the timer if a full autofocus was run (rather than an HFR check)
0657             if (m_refocusState->getFocusHFRInAutofocus())
0658                 m_refocusState->startRefocusTimer(true);
0659 
0660             // update HFR Threshold for those algorithms that use Autofocus as reference
0661             if (Options::hFRCheckAlgorithm() == HFR_CHECK_MEDIAN_MEASURE ||
0662                     (m_refocusState->getFocusHFRInAutofocus() && Options::hFRCheckAlgorithm() == HFR_CHECK_LAST_AUTOFOCUS))
0663             {
0664                 m_refocusState->addHFRValue(getFocusFilterName());
0665                 updateHFRThreshold();
0666             }
0667             emit newFocusStatus(state);
0668             break;
0669         default:
0670             break;
0671     }
0672 
0673     if (m_activeJob != nullptr)
0674         m_activeJob->setFocusStatus(state);
0675 }
0676 
0677 bool CaptureModuleState::startFocusIfRequired()
0678 {
0679     // Do not start focus or adaptive focus if:
0680     // 1. There is no active job, or
0681     // 2. Target frame is not LIGHT
0682     // 3. Capture is preview only
0683     if (m_activeJob == nullptr || m_activeJob->getFrameType() != FRAME_LIGHT
0684             || m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW)
0685         return false;
0686 
0687     RefocusState::RefocusReason reason = m_refocusState->checkFocusRequired();
0688 
0689     // no focusing necessary
0690     if (reason == RefocusState::REFOCUS_NONE)
0691         return false;
0692 
0693     // clear the flag for refocusing after the meridian flip
0694     m_refocusState->setRefocusAfterMeridianFlip(false);
0695 
0696     // Post meridian flip we need to reset filter _before_ running in-sequence focusing
0697     // as it could have changed for whatever reason (e.g. alignment used a different filter).
0698     // Then when focus process begins with the _target_ filter in place, it should take all the necessary actions to make it
0699     // work for the next set of captures. This is direct reset to the filter device, not via Filter Manager.
0700     if (getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_NONE)
0701     {
0702         int targetFilterPosition = m_activeJob->getTargetFilter();
0703         if (targetFilterPosition > 0 && targetFilterPosition != getCurrentFilterPosition())
0704             emit newFilterPosition(targetFilterPosition);
0705     }
0706 
0707     emit abortFastExposure();
0708     updateFocusState(FOCUS_PROGRESS);
0709 
0710     switch (reason)
0711     {
0712         case RefocusState::REFOCUS_HFR:
0713             m_refocusState->resetInSequenceFocusCounter();
0714             (Options::hFRDeviation() == 0.0) ? emit runAutoFocus(false) : emit checkFocus(Options::hFRDeviation());
0715             qCDebug(KSTARS_EKOS_CAPTURE) << "In-sequence focusing started...";
0716             break;
0717         case RefocusState::REFOCUS_ADAPTIVE:
0718             m_refocusState->setAdaptiveFocusDone(true);
0719             emit adaptiveFocus();
0720             qCDebug(KSTARS_EKOS_CAPTURE) << "Adaptive focus started...";
0721             break;
0722         case RefocusState::REFOCUS_TEMPERATURE:
0723         case RefocusState::REFOCUS_TIME_ELAPSED:
0724         case RefocusState::REFOCUS_POST_MF:
0725             // If we are over 30 mins since last autofocus, we'll reset frame.
0726             if (m_refocusState->getRefocusEveryNTimerElapsedSec() >= 1800)
0727                 emit resetFocus();
0728 
0729             // force refocus
0730             emit runAutoFocus(false);
0731             // restart in sequence counting
0732             m_refocusState->resetInSequenceFocusCounter();
0733             qCDebug(KSTARS_EKOS_CAPTURE) << "Refocusing started...";
0734             break;
0735         default:
0736             // this should not happen, since this case is handled above
0737             return false;
0738     }
0739 
0740     setCaptureState(CAPTURE_FOCUSING);
0741     return true;
0742 }
0743 
0744 void CaptureModuleState::updateHFRThreshold()
0745 {
0746     // For Ficed algo no need to update the HFR threshold
0747     if (Options::hFRCheckAlgorithm() == HFR_CHECK_FIXED)
0748         return;
0749 
0750     QString finalFilter = getFocusFilterName();
0751     QList<double> filterHFRList = m_refocusState->getHFRMap()[finalFilter];
0752 
0753     // Update the limit only if HFR values have been measured for the current filter
0754     if (filterHFRList.empty())
0755         return;
0756 
0757     double value = 0;
0758     if (Options::hFRCheckAlgorithm() == HFR_CHECK_LAST_AUTOFOCUS)
0759         value = filterHFRList.last();
0760     else // algo = Median Measure
0761     {
0762         int count = filterHFRList.size();
0763         if (count > 1)
0764             value = (count % 2) ? filterHFRList[count / 2] : (filterHFRList[count / 2 - 1] + filterHFRList[count / 2]) / 2.0;
0765         else if (count == 1)
0766             value = filterHFRList[0];
0767     }
0768     value += value * (Options::hFRThresholdPercentage() / 100.0);
0769     Options::setHFRDeviation(value);
0770     emit newLimitFocusHFR(value); // Updates the limits UI with the new HFR threshold
0771 }
0772 
0773 QString CaptureModuleState::getFocusFilterName()
0774 {
0775     QString finalFilter;
0776     if (m_CurrentFilterPosition > 0)
0777         // If we are using filters, then we retrieve which filter is currently active.
0778         // We check if filter lock is used, and store that instead of the current filter.
0779         // e.g. If current filter HA, but lock filter is L, then the HFR value is stored for L filter.
0780         // If no lock filter exists, then we store as is (HA)
0781         finalFilter = (m_CurrentFocusFilterName == "--" ? m_CurrentFilterName : m_CurrentFocusFilterName);
0782     else
0783         // No filters
0784         finalFilter = "--";
0785     return finalFilter;
0786 }
0787 
0788 bool CaptureModuleState::checkAlignmentAfterFlip()
0789 {
0790     // if no meridian flip has completed, we do not touch guiding
0791     if (getMeridianFlipState()->getMeridianFlipStage() < MeridianFlipState::MF_COMPLETED)
0792         return false;
0793     // If we do not need to align then we're done
0794     if (getMeridianFlipState()->resumeAlignmentAfterFlip() == false)
0795         return false;
0796 
0797     // if we are waiting for a calibration, start it
0798     if (m_CaptureState < CAPTURE_ALIGNING)
0799     {
0800         appendLogText(i18n("Performing post flip re-alignment..."));
0801 
0802         resetAlignmentRetries();
0803         setCaptureState(CAPTURE_ALIGNING);
0804 
0805         getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_ALIGNING);
0806         return true;
0807     }
0808     else
0809         // in all other cases, do not touch
0810         return false;
0811 }
0812 
0813 void CaptureModuleState::checkGuideDeviationTimeout()
0814 {
0815     if (m_activeJob && m_activeJob->getStatus() == JOB_ABORTED
0816             && isGuidingDeviationDetected())
0817     {
0818         appendLogText(i18n("Guide module timed out."));
0819         setGuidingDeviationDetected(false);
0820 
0821         // If capture was suspended, it should be aborted (failed) now.
0822         if (m_CaptureState == CAPTURE_SUSPENDED)
0823         {
0824             setCaptureState(CAPTURE_ABORTED);
0825         }
0826     }
0827 }
0828 
0829 void CaptureModuleState::setGuideDeviation(double deviation_rms)
0830 {
0831     // communicate the new guiding deviation
0832     emit newGuiderDrift(deviation_rms);
0833 
0834     const QString deviationText = QString("%1").arg(deviation_rms, 0, 'f', 3);
0835 
0836     // if guiding deviations occur and no job is active, check if a meridian flip is ready to be executed
0837     if (m_activeJob == nullptr && checkMeridianFlipReady())
0838         return;
0839 
0840     // if the job is in the startup phase and no flip is neither requested nor running, check if the deviation is below the initial guiding limit
0841     if (m_CaptureState == CAPTURE_PROGRESS &&
0842             getMeridianFlipState()->getMeridianFlipStage() != MeridianFlipState::MF_REQUESTED &&
0843             getMeridianFlipState()->checkMeridianFlipRunning() == false)
0844     {
0845         // initial guiding deviation irrelevant or below limit
0846         if (Options::enforceStartGuiderDrift() == false || deviation_rms < Options::startGuideDeviation())
0847         {
0848             setCaptureState(CAPTURE_CALIBRATING);
0849             if (Options::enforceStartGuiderDrift())
0850                 appendLogText(i18n("Initial guiding deviation %1 below limit value of %2 arcsecs",
0851                                    deviationText, Options::startGuideDeviation()));
0852             setGuidingDeviationDetected(false);
0853             setStartingCapture(false);
0854         }
0855         else
0856         {
0857             // warn only once
0858             if (isGuidingDeviationDetected() == false)
0859                 appendLogText(i18n("Initial guiding deviation %1 exceeded limit value of %2 arcsecs",
0860                                    deviationText, Options::startGuideDeviation()));
0861 
0862             setGuidingDeviationDetected(true);
0863 
0864             // Check if we need to start meridian flip. If yes, we need to start capturing
0865             // to ensure that capturing is recovered after the flip
0866             if (checkMeridianFlipReady())
0867                 emit startCapture();
0868         }
0869 
0870         // in any case, do not proceed
0871         return;
0872     }
0873 
0874     // If guiding is started after a meridian flip we will start getting guide deviations again
0875     // if the guide deviations are within our limits, we resume the sequence
0876     if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_GUIDING)
0877     {
0878         // If the user didn't select any guiding deviation, the meridian flip is completed
0879         if (Options::enforceGuideDeviation() == false || deviation_rms < Options::guideDeviation())
0880         {
0881             appendLogText(i18n("Post meridian flip calibration completed successfully."));
0882             // N.B. Set meridian flip stage AFTER resumeSequence() always
0883             getMeridianFlipState()->updateMeridianFlipStage(MeridianFlipState::MF_NONE);
0884             return;
0885         }
0886     }
0887 
0888     // Check for initial deviation in the middle of a sequence (above just checks at the start of a sequence).
0889     if (m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT
0890             && isStartingCapture() && Options::enforceStartGuiderDrift())
0891     {
0892         setStartingCapture(false);
0893         if (deviation_rms > Options::startGuideDeviation())
0894         {
0895             appendLogText(i18n("Guiding deviation at capture startup %1 exceeded limit %2 arcsecs.",
0896                                deviationText, Options::startGuideDeviation()));
0897             emit suspendCapture();
0898             setGuidingDeviationDetected(true);
0899 
0900             // Check if we need to start meridian flip. If yes, we need to start capturing
0901             // to ensure that capturing is recovered after the flip
0902             if (checkMeridianFlipReady())
0903                 emit startCapture();
0904             else
0905                 getGuideDeviationTimer().start();
0906             return;
0907         }
0908         else
0909             appendLogText(i18n("Guiding deviation at capture startup %1 below limit value of %2 arcsecs",
0910                                deviationText, Options::startGuideDeviation()));
0911     }
0912 
0913     if (m_CaptureState != CAPTURE_SUSPENDED)
0914     {
0915 
0916         // We don't enforce limit on previews or non-LIGHT frames
0917         if ((Options::enforceGuideDeviation() == false)
0918                 ||
0919                 (m_activeJob  && (m_activeJob->jobType() == SequenceJob::JOBTYPE_PREVIEW ||
0920                                   m_activeJob->getExposeLeft() == 0.0 ||
0921                                   m_activeJob->getFrameType() != FRAME_LIGHT)))
0922             return;
0923 
0924         // If we have an active busy job, let's abort it if guiding deviation is exceeded.
0925         // And we accounted for the spike
0926         if (m_activeJob && m_activeJob->getStatus() == JOB_BUSY && m_activeJob->getFrameType() == FRAME_LIGHT)
0927         {
0928             if (deviation_rms <= Options::guideDeviation())
0929                 resetSpikesDetected();
0930             else
0931             {
0932                 // Require several consecutive spikes to fail.
0933                 if (increaseSpikesDetected() < Options::guideDeviationReps())
0934                     return;
0935 
0936                 appendLogText(i18n("Guiding deviation %1 exceeded limit value of %2 arcsecs for %4 consecutive samples, "
0937                                    "suspending exposure and waiting for guider up to %3 seconds.",
0938                                    deviationText, Options::guideDeviation(),
0939                                    QString("%L1").arg(getGuideDeviationTimer().interval() / 1000.0, 0, 'f', 3),
0940                                    Options::guideDeviationReps()));
0941 
0942                 emit suspendCapture();
0943 
0944                 resetSpikesDetected();
0945                 setGuidingDeviationDetected(true);
0946 
0947                 // Check if we need to start meridian flip. If yes, we need to start capturing
0948                 // to ensure that capturing is recovered after the flip
0949                 if (checkMeridianFlipReady())
0950                     emit startCapture();
0951                 else
0952                     getGuideDeviationTimer().start();
0953             }
0954             return;
0955         }
0956     }
0957 
0958     // Find the first aborted job
0959     SequenceJob *abortedJob = nullptr;
0960     for(auto &job : allJobs())
0961     {
0962         if (job->getStatus() == JOB_ABORTED)
0963         {
0964             abortedJob = job;
0965             break;
0966         }
0967     }
0968 
0969     if (abortedJob != nullptr && isGuidingDeviationDetected())
0970     {
0971         if (deviation_rms <= Options::startGuideDeviation())
0972         {
0973             getGuideDeviationTimer().stop();
0974 
0975             // Start with delay if start hasn't been triggered before
0976             if (! m_captureDelayTimer.isActive())
0977             {
0978                 // if capturing has been suspended, restart it
0979                 if (m_CaptureState == CAPTURE_SUSPENDED)
0980                 {
0981                     const int seqDelay = abortedJob->getCoreProperty(SequenceJob::SJ_Delay).toInt();
0982                     if (seqDelay == 0)
0983                         appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
0984                                            "resuming exposure.",
0985                                            deviationText, Options::startGuideDeviation()));
0986                     else
0987                         appendLogText(i18n("Guiding deviation %1 is now lower than limit value of %2 arcsecs, "
0988                                            "resuming exposure in %3 seconds.",
0989                                            deviationText, Options::startGuideDeviation(), seqDelay / 1000.0));
0990 
0991                     m_captureDelayTimer.start(seqDelay);
0992                 }
0993             }
0994             return;
0995         }
0996         else
0997         {
0998             // stop the delayed capture start if necessary
0999             if (m_captureDelayTimer.isActive())
1000                 m_captureDelayTimer.stop();
1001 
1002             appendLogText(i18n("Guiding deviation %1 is still higher than limit value of %2 arcsecs.",
1003                                deviationText, Options::startGuideDeviation()));
1004         }
1005     }
1006 }
1007 
1008 void CaptureModuleState::addDownloadTime(double time)
1009 {
1010     totalDownloadTime += time;
1011     downloadsCounter++;
1012 }
1013 
1014 int CaptureModuleState::pendingJobCount()
1015 {
1016     int completedJobs = 0;
1017 
1018     foreach (SequenceJob * job, allJobs())
1019     {
1020         if (job->getStatus() == JOB_DONE)
1021             completedJobs++;
1022     }
1023 
1024     return (allJobs().count() - completedJobs);
1025 
1026 }
1027 
1028 QString CaptureModuleState::jobState(int id)
1029 {
1030     if (id < allJobs().count())
1031     {
1032         SequenceJob * job = allJobs().at(id);
1033         return job->getStatusString();
1034     }
1035 
1036     return QString();
1037 
1038 }
1039 
1040 QString CaptureModuleState::jobFilterName(int id)
1041 {
1042     if (id < allJobs().count())
1043     {
1044         SequenceJob * job = allJobs().at(id);
1045         return job->getCoreProperty(SequenceJob::SJ_Filter).toString();
1046     }
1047 
1048     return QString();
1049 
1050 }
1051 
1052 CCDFrameType CaptureModuleState::jobFrameType(int id)
1053 {
1054     if (id < allJobs().count())
1055     {
1056         SequenceJob * job = allJobs().at(id);
1057         return job->getFrameType();
1058     }
1059 
1060     return FRAME_NONE;
1061 }
1062 
1063 int CaptureModuleState::jobImageProgress(int id)
1064 {
1065     if (id < allJobs().count())
1066     {
1067         SequenceJob * job = allJobs().at(id);
1068         return job->getCompleted();
1069     }
1070 
1071     return -1;
1072 }
1073 
1074 int CaptureModuleState::jobImageCount(int id)
1075 {
1076     if (id < allJobs().count())
1077     {
1078         SequenceJob * job = allJobs().at(id);
1079         return job->getCoreProperty(SequenceJob::SJ_Count).toInt();
1080     }
1081 
1082     return -1;
1083 }
1084 
1085 double CaptureModuleState::jobExposureProgress(int id)
1086 {
1087     if (id < allJobs().count())
1088     {
1089         SequenceJob * job = allJobs().at(id);
1090         return job->getExposeLeft();
1091     }
1092 
1093     return -1;
1094 }
1095 
1096 double CaptureModuleState::jobExposureDuration(int id)
1097 {
1098     if (id < allJobs().count())
1099     {
1100         SequenceJob * job = allJobs().at(id);
1101         return job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble();
1102     }
1103 
1104     return -1;
1105 }
1106 
1107 double CaptureModuleState::progressPercentage()
1108 {
1109     int totalImageCount     = 0;
1110     int totalImageCompleted = 0;
1111 
1112     foreach (SequenceJob * job, allJobs())
1113     {
1114         totalImageCount += job->getCoreProperty(SequenceJob::SJ_Count).toInt();
1115         totalImageCompleted += job->getCompleted();
1116     }
1117 
1118     if (totalImageCount != 0)
1119         return ((static_cast<double>(totalImageCompleted) / totalImageCount) * 100.0);
1120     else
1121         return -1;
1122 }
1123 
1124 int CaptureModuleState::activeJobRemainingTime()
1125 {
1126     if (m_activeJob == nullptr)
1127         return -1;
1128 
1129     return m_activeJob->getJobRemainingTime(averageDownloadTime());
1130 }
1131 
1132 int CaptureModuleState::overallRemainingTime()
1133 {
1134     int remaining = 0;
1135     double estimatedDownloadTime = averageDownloadTime();
1136 
1137     foreach (SequenceJob * job, allJobs())
1138         remaining += job->getJobRemainingTime(estimatedDownloadTime);
1139 
1140     return remaining;
1141 }
1142 
1143 QString CaptureModuleState::sequenceQueueStatus()
1144 {
1145     if (allJobs().count() == 0)
1146         return "Invalid";
1147 
1148     if (isBusy())
1149         return "Running";
1150 
1151     int idle = 0, error = 0, complete = 0, aborted = 0, running = 0;
1152 
1153     foreach (SequenceJob * job, allJobs())
1154     {
1155         switch (job->getStatus())
1156         {
1157             case JOB_ABORTED:
1158                 aborted++;
1159                 break;
1160             case JOB_BUSY:
1161                 running++;
1162                 break;
1163             case JOB_DONE:
1164                 complete++;
1165                 break;
1166             case JOB_ERROR:
1167                 error++;
1168                 break;
1169             case JOB_IDLE:
1170                 idle++;
1171                 break;
1172         }
1173     }
1174 
1175     if (error > 0)
1176         return "Error";
1177 
1178     if (aborted > 0)
1179     {
1180         if (m_CaptureState == CAPTURE_SUSPENDED)
1181             return "Suspended";
1182         else
1183             return "Aborted";
1184     }
1185 
1186     if (running > 0)
1187         return "Running";
1188 
1189     if (idle == allJobs().count())
1190         return "Idle";
1191 
1192     if (complete == allJobs().count())
1193         return "Complete";
1194 
1195     return "Invalid";
1196 }
1197 
1198 QJsonObject CaptureModuleState::calibrationSettings()
1199 {
1200     QJsonObject settings =
1201     {
1202         {"preAction", static_cast<int>(calibrationPreAction())},
1203         {"duration", flatFieldDuration()},
1204         {"az", wallCoord().az().Degrees()},
1205         {"al", wallCoord().alt().Degrees()},
1206         {"adu", targetADU()},
1207         {"tolerance", targetADUTolerance()},
1208     };
1209 
1210     return settings;
1211 }
1212 
1213 void CaptureModuleState::setCalibrationSettings(const QJsonObject &settings)
1214 {
1215     const int preAction = settings["preAction"].toInt(calibrationPreAction());
1216     const int duration = settings["duration"].toInt(flatFieldDuration());
1217     const double az = settings["az"].toDouble(wallCoord().az().Degrees());
1218     const double al = settings["al"].toDouble(wallCoord().alt().Degrees());
1219     const int adu = settings["adu"].toInt(static_cast<int>(std::round(targetADU())));
1220     const int tolerance = settings["tolerance"].toInt(static_cast<int>(std::round(targetADUTolerance())));
1221 
1222     setCalibrationPreAction(static_cast<CalibrationPreActions>(preAction));
1223     setFlatFieldDuration(static_cast<FlatFieldDuration>(duration));
1224     wallCoord().setAz(az);
1225     wallCoord().setAlt(al);
1226     setTargetADU(adu);
1227     setTargetADUTolerance(tolerance);
1228 }
1229 
1230 bool CaptureModuleState::setDarkFlatExposure(SequenceJob *job)
1231 {
1232     const auto darkFlatFilter = job->getCoreProperty(SequenceJob::SJ_Filter).toString();
1233     const auto darkFlatBinning = job->getCoreProperty(SequenceJob::SJ_Binning).toPoint();
1234     const auto darkFlatADU = job->getCoreProperty(SequenceJob::SJ_TargetADU).toInt();
1235 
1236     for (auto &oneJob : allJobs())
1237     {
1238         if (oneJob->getFrameType() != FRAME_FLAT)
1239             continue;
1240 
1241         const auto filter = oneJob->getCoreProperty(SequenceJob::SJ_Filter).toString();
1242 
1243         // Match filter, if one exists.
1244         if (!darkFlatFilter.isEmpty() && darkFlatFilter != filter)
1245             continue;
1246 
1247         // Match binning
1248         const auto binning = oneJob->getCoreProperty(SequenceJob::SJ_Binning).toPoint();
1249         if (darkFlatBinning != binning)
1250             continue;
1251 
1252         // Match ADU, if used.
1253         const auto adu = oneJob->getCoreProperty(SequenceJob::SJ_TargetADU).toInt();
1254         if (job->getFlatFieldDuration() == DURATION_ADU)
1255         {
1256             if (darkFlatADU != adu)
1257                 continue;
1258         }
1259 
1260         // Now get the exposure
1261         job->setCoreProperty(SequenceJob::SJ_Exposure, oneJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
1262 
1263         return true;
1264     }
1265     return false;
1266 }
1267 
1268 void CaptureModuleState::checkSeqBoundary(QUrl sequenceURL)
1269 {
1270     // No updates during meridian flip
1271     if (getMeridianFlipState()->getMeridianFlipStage() >= MeridianFlipState::MF_ALIGNING)
1272         return;
1273 
1274     auto placeholderPath = PlaceholderPath(sequenceURL.toLocalFile());
1275     setNextSequenceID(placeholderPath.checkSeqBoundary(*getActiveJob()));
1276 }
1277 
1278 bool CaptureModuleState::isModelinDSLRInfo(const QString &model)
1279 {
1280     auto pos = std::find_if(m_DSLRInfos.begin(), m_DSLRInfos.end(), [model](QMap<QString, QVariant> &oneDSLRInfo)
1281     {
1282         return (oneDSLRInfo["Model"] == model);
1283     });
1284 
1285     return (pos != m_DSLRInfos.end());
1286 }
1287 
1288 void CaptureModuleState::setCapturedFramesCount(const QString &signature, uint16_t count)
1289 {
1290     m_capturedFramesMap[signature] = count;
1291     qCDebug(KSTARS_EKOS_CAPTURE) <<
1292                                  QString("Client module indicates that storage for '%1' has already %2 captures processed.").arg(signature).arg(count);
1293     // Scheduler's captured frame map overrides the progress option of the Capture module
1294     setIgnoreJobProgress(false);
1295 }
1296 
1297 void CaptureModuleState::changeSequenceValue(int index, QString key, QString value)
1298 {
1299     QJsonArray seqArray = getSequence();
1300     QJsonObject oneSequence = seqArray[index].toObject();
1301     oneSequence[key] = value;
1302     seqArray.replace(index, oneSequence);
1303     setSequence(seqArray);
1304     emit sequenceChanged(seqArray);
1305 }
1306 
1307 void CaptureModuleState::addCapturedFrame(const QString &signature)
1308 {
1309     CapturedFramesMap::iterator frame_item = m_capturedFramesMap.find(signature);
1310     if (m_capturedFramesMap.end() != frame_item)
1311         frame_item.value()++;
1312     else m_capturedFramesMap[signature] = 1;
1313 
1314 }
1315 
1316 void CaptureModuleState::removeCapturedFrameCount(const QString &signature, uint16_t count)
1317 {
1318     CapturedFramesMap::iterator frame_item = m_capturedFramesMap.find(signature);
1319     if (m_capturedFramesMap.end() != frame_item)
1320     {
1321         // remove the frame count
1322         frame_item.value() = frame_item.value() - count;
1323         // clear signature entry if none left
1324         if (frame_item.value() <= 0)
1325             m_capturedFramesMap.remove(signature);
1326     }
1327 }
1328 
1329 
1330 
1331 void CaptureModuleState::appendLogText(const QString &message)
1332 {
1333     qCInfo(KSTARS_EKOS_CAPTURE()) << message;
1334     emit newLog(message);
1335 }
1336 
1337 bool CaptureModuleState::isGuidingOn()
1338 {
1339     // In case we are doing non guiding dither, then we are not performing autoguiding.
1340     if (Options::ditherNoGuiding())
1341         return false;
1342 
1343     return (m_GuideState == GUIDE_GUIDING ||
1344             m_GuideState == GUIDE_CALIBRATING ||
1345             m_GuideState == GUIDE_CALIBRATION_SUCCESS ||
1346             m_GuideState == GUIDE_DARK ||
1347             m_GuideState == GUIDE_SUBFRAME ||
1348             m_GuideState == GUIDE_STAR_SELECT ||
1349             m_GuideState == GUIDE_REACQUIRE ||
1350             m_GuideState == GUIDE_DITHERING ||
1351             m_GuideState == GUIDE_DITHERING_SUCCESS ||
1352             m_GuideState == GUIDE_DITHERING_ERROR ||
1353             m_GuideState == GUIDE_DITHERING_SETTLE ||
1354             m_GuideState == GUIDE_SUSPENDED
1355            );
1356 }
1357 
1358 bool CaptureModuleState::isActivelyGuiding()
1359 {
1360     return isGuidingOn() && (m_GuideState == GUIDE_GUIDING);
1361 }
1362 
1363 void CaptureModuleState::setAlignState(AlignState value)
1364 {
1365     if (value != m_AlignState)
1366         qCDebug(KSTARS_EKOS_CAPTURE) << "Align State changed from" << Ekos::getAlignStatusString(
1367                                          m_AlignState) << "to" << Ekos::getAlignStatusString(value);
1368     m_AlignState = value;
1369 
1370     getMeridianFlipState()->setResumeAlignmentAfterFlip(true);
1371 
1372     switch (value)
1373     {
1374         case ALIGN_COMPLETE:
1375             if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_ALIGNING)
1376             {
1377                 appendLogText(i18n("Post flip re-alignment completed successfully."));
1378                 resetAlignmentRetries();
1379                 // Trigger guiding if necessary.
1380                 if (checkGuidingAfterFlip() == false)
1381                 {
1382                     // If no guiding is required, the meridian flip is complete
1383                     updateMeridianFlipStage(MeridianFlipState::MF_NONE);
1384                     setCaptureState(CAPTURE_WAITING);
1385                 }
1386             }
1387             break;
1388 
1389         case ALIGN_ABORTED:
1390         case ALIGN_FAILED:
1391             // TODO run it 3 times before giving up
1392             if (getMeridianFlipState()->getMeridianFlipStage() == MeridianFlipState::MF_ALIGNING)
1393             {
1394                 if (increaseAlignmentRetries() >= 3)
1395                 {
1396                     appendLogText(i18n("Post-flip alignment failed."));
1397                     emit abortCapture();
1398                 }
1399                 else
1400                 {
1401                     appendLogText(i18n("Post-flip alignment failed. Retrying..."));
1402                     // set back the stage
1403                     updateMeridianFlipStage(MeridianFlipState::MF_COMPLETED);
1404                 }
1405             }
1406             break;
1407 
1408         default:
1409             break;
1410     }
1411 }
1412 
1413 } // namespace