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

0001 /*
0002     SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "schedulerprocess.h"
0007 #include "schedulermodulestate.h"
0008 #include "greedyscheduler.h"
0009 #include "schedulerutils.h"
0010 #include "schedulerjob.h"
0011 #include "ekos/capture/sequencejob.h"
0012 #include "Options.h"
0013 #include "ksmessagebox.h"
0014 #include "ksnotification.h"
0015 #include "kstarsdata.h"
0016 #include "indi/indistd.h"
0017 #include "skymapcomposite.h"
0018 #include "mosaiccomponent.h"
0019 #include "mosaictiles.h"
0020 #include <ekos_scheduler_debug.h>
0021 
0022 #include <QDBusReply>
0023 
0024 #define RESTART_GUIDING_DELAY_MS  5000
0025 
0026 // This is a temporary debugging printout introduced while gaining experience developing
0027 // the unit tests in test_ekos_scheduler_ops.cpp.
0028 // All these printouts should be eventually removed.
0029 
0030 namespace Ekos
0031 {
0032 
0033 SchedulerProcess::SchedulerProcess(QSharedPointer<SchedulerModuleState> state)
0034 {
0035     m_moduleState = state;
0036     m_GreedyScheduler = new GreedyScheduler();
0037 }
0038 
0039 void SchedulerProcess::execute()
0040 {
0041     switch (moduleState()->schedulerState())
0042     {
0043         case SCHEDULER_IDLE:
0044             /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */
0045             if (!moduleState()->startupScriptURL().isEmpty() && ! moduleState()->startupScriptURL().isValid())
0046             {
0047                 appendLogText(i18n("Warning: startup script URL %1 is not valid.",
0048                                    moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile)));
0049                 return;
0050             }
0051 
0052             /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */
0053             if (!moduleState()->shutdownScriptURL().isEmpty() && !moduleState()->shutdownScriptURL().isValid())
0054             {
0055                 appendLogText(i18n("Warning: shutdown script URL %1 is not valid.",
0056                                    moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile)));
0057                 return;
0058             }
0059 
0060 
0061             qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting...";
0062 
0063             moduleState()->setSchedulerState(SCHEDULER_RUNNING);
0064             moduleState()->setupNextIteration(RUN_SCHEDULER);
0065 
0066             appendLogText(i18n("Scheduler started."));
0067             qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started.";
0068             break;
0069 
0070         case SCHEDULER_PAUSED:
0071             moduleState()->setSchedulerState(SCHEDULER_RUNNING);
0072             moduleState()->setupNextIteration(RUN_SCHEDULER);
0073 
0074             appendLogText(i18n("Scheduler resuming."));
0075             qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming.";
0076             break;
0077 
0078         default:
0079             break;
0080     }
0081 
0082 }
0083 
0084 // FindNextJob (probably misnamed) deals with what to do when jobs end.
0085 // For instance, if they complete their capture sequence, they may
0086 // (a) be done, (b) be part of a repeat N times, or (c) be part of a loop forever.
0087 // Similarly, if jobs are aborted they may (a) restart right away, (b) restart after a delay, (c) be ended.
0088 void SchedulerProcess::findNextJob()
0089 {
0090     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
0091     {
0092         // everything finished, we can pause
0093         setPaused();
0094         return;
0095     }
0096 
0097     Q_ASSERT_X(activeJob()->getState() == SCHEDJOB_ERROR ||
0098                activeJob()->getState() == SCHEDJOB_ABORTED ||
0099                activeJob()->getState() == SCHEDJOB_COMPLETE ||
0100                activeJob()->getState() == SCHEDJOB_IDLE,
0101                __FUNCTION__, "Finding next job requires current to be in error, aborted, idle or complete");
0102 
0103     // Reset failed count
0104     moduleState()->resetAlignFailureCount();
0105     moduleState()->resetGuideFailureCount();
0106     moduleState()->resetFocusFailureCount();
0107     moduleState()->resetCaptureFailureCount();
0108 
0109     if (activeJob()->getState() == SCHEDJOB_ERROR || activeJob()->getState() == SCHEDJOB_ABORTED)
0110     {
0111         emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
0112         moduleState()->resetCaptureBatch();
0113         // Stop Guiding if it was used
0114         stopGuiding();
0115 
0116         if (activeJob()->getState() == SCHEDJOB_ERROR)
0117             appendLogText(i18n("Job '%1' is terminated due to errors.", activeJob()->getName()));
0118         else
0119             appendLogText(i18n("Job '%1' is aborted.", activeJob()->getName()));
0120 
0121         // Always reset job stage
0122         moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
0123 
0124         // restart aborted jobs immediately, if error handling strategy is set to "restart immediately"
0125         if (Options::errorHandlingStrategy() == ERROR_RESTART_IMMEDIATELY &&
0126                 (activeJob()->getState() == SCHEDJOB_ABORTED ||
0127                  (activeJob()->getState() == SCHEDJOB_ERROR && Options::rescheduleErrors())))
0128         {
0129             // reset the state so that it will be restarted
0130             activeJob()->setState(SCHEDJOB_SCHEDULED);
0131 
0132             appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", Options::errorHandlingStrategyDelay(),
0133                                activeJob()->getName()));
0134 
0135             // wait the given delay until the jobs will be evaluated again
0136             moduleState()->setupNextIteration(RUN_WAKEUP, std::lround((Options::errorHandlingStrategyDelay() * 1000) /
0137                                               KStarsData::Instance()->clock()->scale()));
0138             emit changeSleepLabel(i18n("Scheduler waits for a retry."));
0139             return;
0140         }
0141 
0142         // otherwise start re-evaluation
0143         moduleState()->setActiveJob(nullptr);
0144         moduleState()->setupNextIteration(RUN_SCHEDULER);
0145     }
0146     else if (activeJob()->getState() == SCHEDJOB_IDLE)
0147     {
0148         emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
0149 
0150         // job constraints no longer valid, start re-evaluation
0151         moduleState()->setActiveJob(nullptr);
0152         moduleState()->setupNextIteration(RUN_SCHEDULER);
0153     }
0154     // Job is complete, so check completion criteria to optimize processing
0155     // In any case, we're done whether the job completed successfully or not.
0156     else if (activeJob()->getCompletionCondition() == FINISH_SEQUENCE)
0157     {
0158         emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
0159 
0160         /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */
0161         if (Options::rememberJobProgress())
0162         {
0163             foreach(SchedulerJob *a_job, moduleState()->jobs())
0164                 if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
0165                     a_job->setState(SCHEDJOB_IDLE);
0166         }
0167 
0168         moduleState()->resetCaptureBatch();
0169         // Stop Guiding if it was used
0170         stopGuiding();
0171 
0172         appendLogText(i18n("Job '%1' is complete.", activeJob()->getName()));
0173 
0174         // Always reset job stage
0175         moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
0176 
0177         // If saving remotely, then can't tell later that the job has been completed.
0178         // Set it complete now.
0179         if (!canCountCaptures(*activeJob()))
0180             activeJob()->setState(SCHEDJOB_COMPLETE);
0181 
0182         moduleState()->setActiveJob(nullptr);
0183         moduleState()->setupNextIteration(RUN_SCHEDULER);
0184     }
0185     else if (activeJob()->getCompletionCondition() == FINISH_REPEAT &&
0186              (activeJob()->getRepeatsRemaining() <= 1))
0187     {
0188         /* If the job is about to repeat, decrease its repeat count and reset its start time */
0189         if (activeJob()->getRepeatsRemaining() > 0)
0190         {
0191             // If we can remember job progress, this is done in estimateJobTime()
0192             if (!Options::rememberJobProgress())
0193             {
0194                 activeJob()->setRepeatsRemaining(activeJob()->getRepeatsRemaining() - 1);
0195                 activeJob()->setCompletedIterations(activeJob()->getCompletedIterations() + 1);
0196             }
0197             activeJob()->setStartupTime(QDateTime());
0198         }
0199 
0200         /* Mark the job idle as well as all its duplicates for re-evaluation */
0201         foreach(SchedulerJob *a_job, moduleState()->jobs())
0202             if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
0203                 a_job->setState(SCHEDJOB_IDLE);
0204 
0205         /* Re-evaluate all jobs, without selecting a new job */
0206         evaluateJobs(true);
0207 
0208         /* If current job is actually complete because of previous duplicates, prepare for next job */
0209         if (activeJob() == nullptr || activeJob()->getRepeatsRemaining() == 0)
0210         {
0211             stopCurrentJobAction();
0212 
0213             if (activeJob() != nullptr)
0214             {
0215                 emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
0216                 appendLogText(i18np("Job '%1' is complete after #%2 batch.",
0217                                     "Job '%1' is complete after #%2 batches.",
0218                                     activeJob()->getName(), activeJob()->getRepeatsRequired()));
0219                 if (!canCountCaptures(*activeJob()))
0220                     activeJob()->setState(SCHEDJOB_COMPLETE);
0221                 moduleState()->setActiveJob(nullptr);
0222             }
0223             moduleState()->setupNextIteration(RUN_SCHEDULER);
0224         }
0225         /* If job requires more work, continue current observation */
0226         else
0227         {
0228             /* FIXME: raise priority to allow other jobs to schedule in-between */
0229             if (executeJob(activeJob()) == false)
0230                 return;
0231 
0232             /* JM 2020-08-23: If user opts to force realign instead of for each job then we force this FIRST */
0233             if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
0234             {
0235                 stopGuiding();
0236                 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
0237                 startAstrometry();
0238             }
0239             /* If we are guiding, continue capturing */
0240             else if ( (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE) )
0241             {
0242                 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
0243                 startCapture();
0244             }
0245             /* If we are not guiding, but using alignment, realign */
0246             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
0247             {
0248                 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
0249                 startAstrometry();
0250             }
0251             /* Else if we are neither guiding nor using alignment, slew back to target */
0252             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_TRACK)
0253             {
0254                 moduleState()->updateJobStage(SCHEDSTAGE_SLEWING);
0255                 startSlew();
0256             }
0257             /* Else just start capturing */
0258             else
0259             {
0260                 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
0261                 startCapture();
0262             }
0263 
0264             appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
0265                                 "Job '%1' is repeating, #%2 batches remaining.",
0266                                 activeJob()->getName(), activeJob()->getRepeatsRemaining()));
0267             /* getActiveJob() remains the same */
0268             moduleState()->setupNextIteration(RUN_JOBCHECK);
0269         }
0270     }
0271     else if ((activeJob()->getCompletionCondition() == FINISH_LOOP) ||
0272              (activeJob()->getCompletionCondition() == FINISH_REPEAT &&
0273               activeJob()->getRepeatsRemaining() > 0))
0274     {
0275         /* If the job is about to repeat, decrease its repeat count and reset its start time */
0276         if ((activeJob()->getCompletionCondition() == FINISH_REPEAT) &&
0277                 (activeJob()->getRepeatsRemaining() > 1))
0278         {
0279             // If we can remember job progress, this is done in estimateJobTime()
0280             if (!Options::rememberJobProgress())
0281             {
0282                 activeJob()->setRepeatsRemaining(activeJob()->getRepeatsRemaining() - 1);
0283                 activeJob()->setCompletedIterations(activeJob()->getCompletedIterations() + 1);
0284             }
0285             activeJob()->setStartupTime(QDateTime());
0286         }
0287 
0288         if (executeJob(activeJob()) == false)
0289             return;
0290 
0291         if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
0292         {
0293             stopGuiding();
0294             moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
0295             startAstrometry();
0296         }
0297         else
0298         {
0299             moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
0300             startCapture();
0301         }
0302 
0303         moduleState()->increaseCaptureBatch();
0304 
0305         if (activeJob()->getCompletionCondition() == FINISH_REPEAT )
0306             appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
0307                                 "Job '%1' is repeating, #%2 batches remaining.",
0308                                 activeJob()->getName(), activeJob()->getRepeatsRemaining()));
0309         else
0310             appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", activeJob()->getName()));
0311 
0312         /* getActiveJob() remains the same */
0313         moduleState()->setupNextIteration(RUN_JOBCHECK);
0314     }
0315     else if (activeJob()->getCompletionCondition() == FINISH_AT)
0316     {
0317         if (SchedulerModuleState::getLocalTime().secsTo(activeJob()->getCompletionTime()) <= 0)
0318         {
0319             emit jobEnded(activeJob()->getName(), activeJob()->getStopReason());
0320 
0321             /* Mark the job idle as well as all its duplicates for re-evaluation */
0322             foreach(SchedulerJob *a_job, moduleState()->jobs())
0323                 if (a_job == activeJob() || a_job->isDuplicateOf(activeJob()))
0324                     a_job->setState(SCHEDJOB_IDLE);
0325             stopCurrentJobAction();
0326 
0327             moduleState()->resetCaptureBatch();
0328 
0329             appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.",
0330                                 "Job '%1' stopping, reached completion time with #%2 batches done.",
0331                                 activeJob()->getName(), moduleState()->captureBatch() + 1));
0332 
0333             // Always reset job stage
0334             moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
0335 
0336             moduleState()->setActiveJob(nullptr);
0337             moduleState()->setupNextIteration(RUN_SCHEDULER);
0338         }
0339         else
0340         {
0341             if (executeJob(activeJob()) == false)
0342                 return;
0343 
0344             if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
0345             {
0346                 stopGuiding();
0347                 moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
0348                 startAstrometry();
0349             }
0350             else
0351             {
0352                 moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
0353                 startCapture();
0354             }
0355 
0356             moduleState()->increaseCaptureBatch();
0357 
0358             appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.",
0359                                 "Job '%1' completed #%2 batches before completion time, restarted.",
0360                                 activeJob()->getName(), moduleState()->captureBatch()));
0361             /* getActiveJob() remains the same */
0362             moduleState()->setupNextIteration(RUN_JOBCHECK);
0363         }
0364     }
0365     else
0366     {
0367         /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */
0368         qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << activeJob()->getName() <<
0369                                        "' timer elapsed, but no action to be taken.";
0370 
0371         // Always reset job stage
0372         moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
0373 
0374         moduleState()->setActiveJob(nullptr);
0375         moduleState()->setupNextIteration(RUN_SCHEDULER);
0376     }
0377 }
0378 
0379 void SchedulerProcess::stopCurrentJobAction()
0380 {
0381     if (nullptr != activeJob())
0382     {
0383         qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << activeJob()->getName() << "' is stopping current action..." <<
0384                                        activeJob()->getStage();
0385 
0386         switch (activeJob()->getStage())
0387         {
0388             case SCHEDSTAGE_IDLE:
0389                 break;
0390 
0391             case SCHEDSTAGE_SLEWING:
0392                 mountInterface()->call(QDBus::AutoDetect, "abort");
0393                 break;
0394 
0395             case SCHEDSTAGE_FOCUSING:
0396                 focusInterface()->call(QDBus::AutoDetect, "abort");
0397                 break;
0398 
0399             case SCHEDSTAGE_ALIGNING:
0400                 alignInterface()->call(QDBus::AutoDetect, "abort");
0401                 break;
0402 
0403             // N.B. Need to use BlockWithGui as proposed by Wolfgang
0404             // to ensure capture is properly aborted before taking any further actions.
0405             case SCHEDSTAGE_CAPTURING:
0406                 captureInterface()->call(QDBus::BlockWithGui, "abort");
0407                 break;
0408 
0409             default:
0410                 break;
0411         }
0412 
0413         /* Reset interrupted job stage */
0414         moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
0415     }
0416 
0417     /* Guiding being a parallel process, check to stop it */
0418     stopGuiding();
0419 }
0420 
0421 void SchedulerProcess::wakeUpScheduler()
0422 {
0423     if (moduleState()->preemptiveShutdown())
0424     {
0425         moduleState()->disablePreemptiveShutdown();
0426         appendLogText(i18n("Scheduler is awake."));
0427         execute();
0428     }
0429     else
0430     {
0431         if (moduleState()->schedulerState() == SCHEDULER_RUNNING)
0432             appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready..."));
0433         else
0434             appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed."));
0435 
0436         moduleState()->setupNextIteration(RUN_SCHEDULER);
0437     }
0438 }
0439 
0440 void SchedulerProcess::startScheduler()
0441 {
0442     // New scheduler session shouldn't inherit ABORT or ERROR states from the last one.
0443     foreach (auto j, moduleState()->jobs())
0444     {
0445         j->setState(SCHEDJOB_IDLE);
0446         emit updateJobTable(j);
0447     }
0448     moduleState()->init();
0449     iterate();
0450 }
0451 
0452 void SchedulerProcess::stopScheduler()
0453 {
0454     // do nothing if the scheduler is not running
0455     if (moduleState()->schedulerState() != SCHEDULER_RUNNING)
0456         return;
0457 
0458     qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping...";
0459 
0460     // Stop running job and abort all others
0461     // in case of soft shutdown we skip this
0462     if (!moduleState()->preemptiveShutdown())
0463     {
0464         for (auto &oneJob : moduleState()->jobs())
0465         {
0466             if (oneJob == activeJob())
0467                 stopCurrentJobAction();
0468 
0469             if (oneJob->getState() <= SCHEDJOB_BUSY)
0470             {
0471                 appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", oneJob->getName()));
0472                 oneJob->setState(SCHEDJOB_ABORTED);
0473             }
0474         }
0475     }
0476 
0477     moduleState()->setupNextIteration(RUN_NOTHING);
0478     moduleState()->cancelGuidingTimer();
0479 
0480     moduleState()->setSchedulerState(SCHEDULER_IDLE);
0481     moduleState()->setParkWaitState(PARKWAIT_IDLE);
0482     moduleState()->setEkosState(EKOS_IDLE);
0483     moduleState()->setIndiState(INDI_IDLE);
0484 
0485     // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete.
0486     // Or if we're doing a soft shutdown
0487     if (moduleState()->startupState() != STARTUP_COMPLETE || moduleState()->preemptiveShutdown())
0488     {
0489         if (moduleState()->startupState() == STARTUP_SCRIPT)
0490         {
0491             scriptProcess().disconnect();
0492             scriptProcess().terminate();
0493         }
0494 
0495         moduleState()->setStartupState(STARTUP_IDLE);
0496     }
0497     // Reset startup state to unparking phase (dome -> mount -> cap)
0498     // We do not want to run the startup script again but unparking should be checked
0499     // whenever the scheduler is running again.
0500     else if (moduleState()->startupState() == STARTUP_COMPLETE)
0501     {
0502         if (Options::schedulerUnparkDome())
0503             moduleState()->setStartupState(STARTUP_UNPARK_DOME);
0504         else if (Options::schedulerUnparkMount())
0505             moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
0506         else if (Options::schedulerOpenDustCover())
0507             moduleState()->setStartupState(STARTUP_UNPARK_CAP);
0508     }
0509 
0510     moduleState()->setShutdownState(SHUTDOWN_IDLE);
0511 
0512     moduleState()->setActiveJob(nullptr);
0513     moduleState()->resetFailureCounters();
0514     moduleState()->setAutofocusCompleted(false);
0515 
0516     // If soft shutdown, we return for now
0517     if (moduleState()->preemptiveShutdown())
0518     {
0519         QDateTime const now = SchedulerModuleState::getLocalTime();
0520         int const nextObservationTime = now.secsTo(moduleState()->preemptiveShutdownWakeupTime());
0521         moduleState()->setupNextIteration(RUN_WAKEUP,
0522                                           std::lround(((nextObservationTime + 1) * 1000)
0523                                                   / KStarsData::Instance()->clock()->scale()));
0524         // report success
0525         emit schedulerStopped();
0526         return;
0527     }
0528 
0529     // Clear target name in capture interface upon stopping
0530     if (captureInterface().isNull() == false)
0531         captureInterface()->setProperty("targetName", QString());
0532 
0533     if (scriptProcess().state() == QProcess::Running)
0534         scriptProcess().terminate();
0535 
0536     // report success
0537     emit schedulerStopped();
0538 }
0539 
0540 bool SchedulerProcess::shouldSchedulerSleep(SchedulerJob * job)
0541 {
0542     Q_ASSERT_X(nullptr != job, __FUNCTION__,
0543                "There must be a valid current job for Scheduler to test sleep requirement");
0544 
0545     if (job->getLightFramesRequired() == false)
0546         return false;
0547 
0548     QDateTime const now = SchedulerModuleState::getLocalTime();
0549     int const nextObservationTime = now.secsTo(job->getStartupTime());
0550 
0551     // It is possible that the nextObservationTime is far away, but the reason is that
0552     // the user has edited the jobs, and now the active job is not the next thing scheduled.
0553     if (getGreedyScheduler()->getScheduledJob() != job)
0554         return false;
0555 
0556     // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed
0557     // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready
0558     if (moduleState()->startupState() == STARTUP_COMPLETE &&
0559             Options::preemptiveShutdown() &&
0560             nextObservationTime > (Options::preemptiveShutdownTime() * 3600))
0561     {
0562         appendLogText(i18n(
0563                           "Job '%1' scheduled for execution at %2. "
0564                           "Observatory scheduled for shutdown until next job is ready.",
0565                           job->getName(), job->getStartupTime().toString()));
0566         moduleState()->enablePreemptiveShutdown(job->getStartupTime());
0567         checkShutdownState();
0568         emit schedulerSleeping(true, false);
0569         return true;
0570     }
0571     // Otherwise, sleep until job is ready
0572     /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */
0573     // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled
0574     // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again.
0575     // This is also only performed if next job is due more than the default lead time (5 minutes).
0576     // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes.
0577     else if (nextObservationTime > Options::leadTime() * 60 &&
0578              moduleState()->startupState() == STARTUP_COMPLETE &&
0579              moduleState()->parkWaitState() == PARKWAIT_IDLE &&
0580              (job->getStepPipeline() & SchedulerJob::USE_TRACK) &&
0581              // schedulerParkMount->isEnabled() &&
0582              Options::schedulerParkMount())
0583     {
0584         appendLogText(i18n(
0585                           "Job '%1' scheduled for execution at %2. "
0586                           "Parking the mount until the job is ready.",
0587                           job->getName(), job->getStartupTime().toString()));
0588 
0589         moduleState()->setParkWaitState(PARKWAIT_PARK);
0590 
0591         return false;
0592     }
0593     else if (nextObservationTime > Options::leadTime() * 60)
0594     {
0595         appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", job->getName(),
0596                            now.addSecs(nextObservationTime + 1).toString()));
0597 
0598         // Warn the user if the next job is really far away - 60/5 = 12 times the lead time
0599         if (nextObservationTime > Options::leadTime() * 60 * 12 && !Options::preemptiveShutdown())
0600         {
0601             dms delay(static_cast<double>(nextObservationTime * 15.0 / 3600.0));
0602             appendLogText(i18n(
0603                               "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.",
0604                               job->getName(), delay.toHMSString()));
0605         }
0606 
0607         /* FIXME: stop tracking now */
0608 
0609         // Wake up when job is due.
0610         // FIXME: Implement waking up periodically before job is due for weather check.
0611         // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60;
0612         moduleState()->setupNextIteration(RUN_WAKEUP,
0613                                           std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale()));
0614 
0615         emit schedulerSleeping(false, true);
0616         return true;
0617     }
0618 
0619     return false;
0620 }
0621 
0622 void SchedulerProcess::startSlew()
0623 {
0624     Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting slewing must be valid");
0625 
0626     // If the mount was parked by a pause or the end-user, unpark
0627     if (isMountParked())
0628     {
0629         moduleState()->setParkWaitState(PARKWAIT_UNPARK);
0630         return;
0631     }
0632 
0633     if (Options::resetMountModelBeforeJob())
0634     {
0635         mountInterface()->call(QDBus::AutoDetect, "resetModel");
0636     }
0637 
0638     SkyPoint target = activeJob()->getTargetCoords();
0639     QList<QVariant> telescopeSlew;
0640     telescopeSlew.append(target.ra().Hours());
0641     telescopeSlew.append(target.dec().Degrees());
0642 
0643     QDBusReply<bool> const slewModeReply = mountInterface()->callWithArgumentList(QDBus::AutoDetect, "slew",
0644                                            telescopeSlew);
0645 
0646     if (slewModeReply.error().type() != QDBusError::NoError)
0647     {
0648         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(
0649                                               activeJob()->getName(), QDBusError::errorString(slewModeReply.error().type()));
0650         if (!manageConnectionLoss())
0651             activeJob()->setState(SCHEDJOB_ERROR);
0652     }
0653     else
0654     {
0655         moduleState()->updateJobStage(SCHEDSTAGE_SLEWING);
0656         appendLogText(i18n("Job '%1' is slewing to target.", activeJob()->getName()));
0657     }
0658 }
0659 
0660 void SchedulerProcess::startFocusing()
0661 {
0662     Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting focusing must be valid");
0663 
0664     // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed
0665     // when first focus request is made in capture module
0666     if (activeJob()->getStage() == SCHEDSTAGE_RESLEWING_COMPLETE ||
0667             activeJob()->getStage() == SCHEDSTAGE_POSTALIGN_FOCUSING)
0668     {
0669         // Clear the HFR limit value set in the capture module
0670         captureInterface()->call(QDBus::AutoDetect, "clearAutoFocusHFR");
0671         // Reset Focus frame so that next frame take a full-resolution capture first.
0672         focusInterface()->call(QDBus::AutoDetect, "resetFrame");
0673         moduleState()->updateJobStage(SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE);
0674         getNextAction();
0675         return;
0676     }
0677 
0678     // Check if autofocus is supported
0679     QDBusReply<bool> focusModeReply;
0680     focusModeReply = focusInterface()->call(QDBus::AutoDetect, "canAutoFocus");
0681 
0682     if (focusModeReply.error().type() != QDBusError::NoError)
0683     {
0684         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(
0685                                               activeJob()->getName(), QDBusError::errorString(focusModeReply.error().type()));
0686         if (!manageConnectionLoss())
0687         {
0688             activeJob()->setState(SCHEDJOB_ERROR);
0689             findNextJob();
0690         }
0691         return;
0692     }
0693 
0694     if (focusModeReply.value() == false)
0695     {
0696         appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", activeJob()->getName()));
0697         activeJob()->setStepPipeline(
0698             static_cast<SchedulerJob::StepPipeline>(activeJob()->getStepPipeline() & ~SchedulerJob::USE_FOCUS));
0699         moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
0700         getNextAction();
0701         return;
0702     }
0703 
0704     // Clear the HFR limit value set in the capture module
0705     captureInterface()->call(QDBus::AutoDetect, "clearAutoFocusHFR");
0706 
0707     QDBusMessage reply;
0708 
0709     // We always need to reset frame first
0710     if ((reply = focusInterface()->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage)
0711     {
0712         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(
0713                                               activeJob()->getName(), reply.errorMessage());
0714         if (!manageConnectionLoss())
0715         {
0716             activeJob()->setState(SCHEDJOB_ERROR);
0717             findNextJob();
0718         }
0719         return;
0720     }
0721 
0722 
0723     // If we have a LIGHT filter set, let's set it.
0724     if (!activeJob()->getInitialFilter().isEmpty())
0725     {
0726         focusInterface()->setProperty("filter", activeJob()->getInitialFilter());
0727     }
0728 
0729     // Set autostar if full field option is false
0730     if (Options::focusUseFullField() == false)
0731     {
0732         QList<QVariant> autoStar;
0733         autoStar.append(true);
0734         if ((reply = focusInterface()->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() ==
0735                 QDBusMessage::ErrorMessage)
0736         {
0737             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(
0738                                                   activeJob()->getName(), reply.errorMessage());
0739             if (!manageConnectionLoss())
0740             {
0741                 activeJob()->setState(SCHEDJOB_ERROR);
0742                 findNextJob();
0743             }
0744             return;
0745         }
0746     }
0747 
0748     // Start auto-focus
0749     if ((reply = focusInterface()->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage)
0750     {
0751         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(
0752                                               activeJob()->getName(), reply.errorMessage());
0753         if (!manageConnectionLoss())
0754         {
0755             activeJob()->setState(SCHEDJOB_ERROR);
0756             findNextJob();
0757         }
0758         return;
0759     }
0760 
0761     moduleState()->updateJobStage(SCHEDSTAGE_FOCUSING);
0762     appendLogText(i18n("Job '%1' is focusing.", activeJob()->getName()));
0763     moduleState()->startCurrentOperationTimer();
0764 }
0765 
0766 void SchedulerProcess::startAstrometry()
0767 {
0768     Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting aligning must be valid");
0769 
0770     QDBusMessage reply;
0771     setSolverAction(Align::GOTO_SLEW);
0772 
0773     // Always turn update coords on
0774     //QVariant arg(true);
0775     //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg);
0776 
0777     // Reset the solver speedup (using the last successful index file and healpix for the
0778     // pointing check) when re-aligning.
0779     moduleState()->setIndexToUse(-1);
0780     moduleState()->setHealpixToUse(-1);
0781 
0782     // If FITS file is specified, then we use load and slew
0783     if (activeJob()->getFITSFile().isEmpty() == false)
0784     {
0785         auto path = activeJob()->getFITSFile().toString(QUrl::PreferLocalFile);
0786         // check if the file exists
0787         if (QFile::exists(path) == false)
0788         {
0789             appendLogText(i18n("Warning: job '%1' target FITS file does not exist.", activeJob()->getName()));
0790             activeJob()->setState(SCHEDJOB_ERROR);
0791             findNextJob();
0792             return;
0793         }
0794 
0795         QList<QVariant> solveArgs;
0796         solveArgs.append(path);
0797 
0798         if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() ==
0799                 QDBusMessage::ErrorMessage)
0800         {
0801             appendLogText(i18n("Warning: job '%1' loadAndSlew request received DBUS error: %2",
0802                                activeJob()->getName(), reply.errorMessage()));
0803             if (!manageConnectionLoss())
0804             {
0805                 activeJob()->setState(SCHEDJOB_ERROR);
0806                 findNextJob();
0807             }
0808             return;
0809         }
0810         else if (reply.arguments().first().toBool() == false)
0811         {
0812             appendLogText(i18n("Warning: job '%1' loadAndSlew request failed.", activeJob()->getName()));
0813             activeJob()->setState(SCHEDJOB_ABORTED);
0814             findNextJob();
0815             return;
0816         }
0817 
0818         appendLogText(i18n("Job '%1' is plate solving %2.", activeJob()->getName(), activeJob()->getFITSFile().fileName()));
0819     }
0820     else
0821     {
0822         // JM 2020.08.20: Send J2000 TargetCoords to Align module so that we always resort back to the
0823         // target original targets even if we drifted away due to any reason like guiding calibration failures.
0824         const SkyPoint targetCoords = activeJob()->getTargetCoords();
0825         QList<QVariant> targetArgs, rotationArgs;
0826         targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
0827         rotationArgs << activeJob()->getPositionAngle();
0828 
0829         if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords",
0830                      targetArgs)).type() == QDBusMessage::ErrorMessage)
0831         {
0832             appendLogText(i18n("Warning: job '%1' setTargetCoords request received DBUS error: %2",
0833                                activeJob()->getName(), reply.errorMessage()));
0834             if (!manageConnectionLoss())
0835             {
0836                 activeJob()->setState(SCHEDJOB_ERROR);
0837                 findNextJob();
0838             }
0839             return;
0840         }
0841 
0842         // Only send if it has valid value.
0843         if (activeJob()->getPositionAngle() >= -180)
0844         {
0845             if ((reply = alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetPositionAngle",
0846                          rotationArgs)).type() == QDBusMessage::ErrorMessage)
0847             {
0848                 appendLogText(i18n("Warning: job '%1' setTargetPositionAngle request received DBUS error: %2").arg(
0849                                   activeJob()->getName(), reply.errorMessage()));
0850                 if (!manageConnectionLoss())
0851                 {
0852                     activeJob()->setState(SCHEDJOB_ERROR);
0853                     findNextJob();
0854                 }
0855                 return;
0856             }
0857         }
0858 
0859         if ((reply = alignInterface()->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage)
0860         {
0861             appendLogText(i18n("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(
0862                               activeJob()->getName(), reply.errorMessage()));
0863             if (!manageConnectionLoss())
0864             {
0865                 activeJob()->setState(SCHEDJOB_ERROR);
0866                 findNextJob();
0867             }
0868             return;
0869         }
0870         else if (reply.arguments().first().toBool() == false)
0871         {
0872             appendLogText(i18n("Warning: job '%1' captureAndSolve request failed.", activeJob()->getName()));
0873             activeJob()->setState(SCHEDJOB_ABORTED);
0874             findNextJob();
0875             return;
0876         }
0877 
0878         appendLogText(i18n("Job '%1' is capturing and plate solving.", activeJob()->getName()));
0879     }
0880 
0881     /* FIXME: not supposed to modify the job */
0882     moduleState()->updateJobStage(SCHEDSTAGE_ALIGNING);
0883     moduleState()->startCurrentOperationTimer();
0884 }
0885 
0886 void SchedulerProcess::startGuiding(bool resetCalibration)
0887 {
0888     Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting guiding must be valid");
0889 
0890     // avoid starting the guider twice
0891     if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING)
0892     {
0893         moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
0894         appendLogText(i18n("Guiding already running for %1, starting next scheduler action...", activeJob()->getName()));
0895         getNextAction();
0896         moduleState()->startCurrentOperationTimer();
0897         return;
0898     }
0899 
0900     // Connect Guider
0901     guideInterface()->call(QDBus::AutoDetect, "connectGuider");
0902 
0903     // Set Auto Star to true
0904     QVariant arg(true);
0905     guideInterface()->call(QDBus::AutoDetect, "setAutoStarEnabled", arg);
0906 
0907     // Only reset calibration on trouble
0908     // and if we are allowed to reset calibration (true by default)
0909     if (resetCalibration && Options::resetGuideCalibration())
0910     {
0911         guideInterface()->call(QDBus::AutoDetect, "clearCalibration");
0912     }
0913 
0914     guideInterface()->call(QDBus::AutoDetect, "guide");
0915 
0916     moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
0917 
0918     appendLogText(i18n("Starting guiding procedure for %1 ...", activeJob()->getName()));
0919 
0920     moduleState()->startCurrentOperationTimer();
0921 }
0922 
0923 void SchedulerProcess::stopGuiding()
0924 {
0925     if (!guideInterface())
0926         return;
0927 
0928     // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation
0929     if (nullptr != activeJob() && (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE))
0930     {
0931         qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(activeJob()->getName());
0932         guideInterface()->call(QDBus::AutoDetect, "abort");
0933         moduleState()->resetGuideFailureCount();
0934     }
0935 
0936     // In any case, stop the automatic guider restart
0937     if (moduleState()->isGuidingTimerActive())
0938         moduleState()->cancelGuidingTimer();
0939 }
0940 
0941 void SchedulerProcess::processGuidingTimer()
0942 {
0943     if ((moduleState()->restartGuidingInterval() > 0) &&
0944             (moduleState()->restartGuidingTime().msecsTo(KStarsData::Instance()->ut()) > moduleState()->restartGuidingInterval()))
0945     {
0946         moduleState()->cancelGuidingTimer();
0947         startGuiding(true);
0948     }
0949 }
0950 
0951 void SchedulerProcess::startCapture(bool restart)
0952 {
0953     Q_ASSERT_X(nullptr != activeJob(), __FUNCTION__, "Job starting capturing must be valid");
0954 
0955     // ensure that guiding is running before we start capturing
0956     if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING)
0957     {
0958         // guiding should run, but it doesn't. So start guiding first
0959         moduleState()->updateJobStage(SCHEDSTAGE_GUIDING);
0960         startGuiding();
0961         return;
0962     }
0963 
0964     captureInterface()->setProperty("targetName", activeJob()->getName());
0965 
0966     QString url = activeJob()->getSequenceFile().toLocalFile();
0967 
0968     if (restart == false)
0969     {
0970         QList<QVariant> dbusargs;
0971         dbusargs.append(url);
0972         // override targets from sequence queue file
0973         QVariant targetName(activeJob()->getName());
0974         dbusargs.append(targetName);
0975         QDBusReply<bool> const captureReply = captureInterface()->callWithArgumentList(QDBus::AutoDetect,
0976                                               "loadSequenceQueue",
0977                                               dbusargs);
0978         if (captureReply.error().type() != QDBusError::NoError)
0979         {
0980             qCCritical(KSTARS_EKOS_SCHEDULER) <<
0981                                               QString("Warning: job '%1' loadSequenceQueue request received DBUS error: %1").arg(activeJob()->getName()).arg(
0982                                                   captureReply.error().message());
0983             if (!manageConnectionLoss())
0984                 activeJob()->setState(SCHEDJOB_ERROR);
0985             return;
0986         }
0987         // Check if loading sequence fails for whatever reason
0988         else if (captureReply.value() == false)
0989         {
0990             qCCritical(KSTARS_EKOS_SCHEDULER) <<
0991                                               QString("Warning: job '%1' loadSequenceQueue request failed").arg(activeJob()->getName());
0992             if (!manageConnectionLoss())
0993                 activeJob()->setState(SCHEDJOB_ERROR);
0994             return;
0995         }
0996     }
0997 
0998 
0999     CapturedFramesMap fMap = activeJob()->getCapturedFramesMap();
1000 
1001     for (auto &e : fMap.keys())
1002     {
1003         QList<QVariant> dbusargs;
1004         QDBusMessage reply;
1005 
1006         dbusargs.append(e);
1007         dbusargs.append(fMap.value(e));
1008         if ((reply = captureInterface()->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap",
1009                      dbusargs)).type() ==
1010                 QDBusMessage::ErrorMessage)
1011         {
1012             qCCritical(KSTARS_EKOS_SCHEDULER) <<
1013                                               QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(activeJob()->getName()).arg(
1014                                                   reply.errorMessage());
1015             if (!manageConnectionLoss())
1016                 activeJob()->setState(SCHEDJOB_ERROR);
1017             return;
1018         }
1019     }
1020 
1021     // Start capture process
1022     captureInterface()->call(QDBus::AutoDetect, "start");
1023 
1024     moduleState()->updateJobStage(SCHEDSTAGE_CAPTURING);
1025 
1026     KSNotification::event(QLatin1String("EkosScheduledImagingStart"),
1027                           i18n("Ekos job (%1) - Capture started", activeJob()->getName()), KSNotification::Scheduler);
1028 
1029     if (moduleState()->captureBatch() > 0)
1030         appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", activeJob()->getName(),
1031                            moduleState()->captureBatch() + 1));
1032     else
1033         appendLogText(i18n("Job '%1' capture is in progress...", activeJob()->getName()));
1034 
1035     moduleState()->startCurrentOperationTimer();
1036 }
1037 
1038 void SchedulerProcess::setSolverAction(Align::GotoMode mode)
1039 {
1040     QVariant gotoMode(static_cast<int>(mode));
1041     alignInterface()->call(QDBus::AutoDetect, "setSolverAction", gotoMode);
1042 }
1043 
1044 void SchedulerProcess::loadProfiles()
1045 {
1046     qCDebug(KSTARS_EKOS_SCHEDULER) << "Loading profiles";
1047     QDBusReply<QStringList> profiles = ekosInterface()->call(QDBus::AutoDetect, "getProfiles");
1048 
1049     if (profiles.error().type() == QDBusError::NoError)
1050         moduleState()->updateProfiles(profiles);
1051 }
1052 
1053 void SchedulerProcess::executeScript(const QString &filename)
1054 {
1055     appendLogText(i18n("Executing script %1...", filename));
1056 
1057     connect(&scriptProcess(), &QProcess::readyReadStandardOutput, this, &SchedulerProcess::readProcessOutput);
1058 
1059     connect(&scriptProcess(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
1060             this, [this](int exitCode, QProcess::ExitStatus)
1061     {
1062         checkProcessExit(exitCode);
1063     });
1064 
1065     QStringList arguments;
1066     scriptProcess().start(filename, arguments);
1067 }
1068 
1069 bool SchedulerProcess::checkEkosState()
1070 {
1071     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1072         return false;
1073 
1074     switch (moduleState()->ekosState())
1075     {
1076         case EKOS_IDLE:
1077         {
1078             if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1079             {
1080                 moduleState()->setEkosState(EKOS_READY);
1081                 return true;
1082             }
1083             else
1084             {
1085                 ekosInterface()->call(QDBus::AutoDetect, "start");
1086                 moduleState()->setEkosState(EKOS_STARTING);
1087                 moduleState()->startCurrentOperationTimer();
1088 
1089                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << moduleState()->ekosCommunicationStatus() <<
1090                                               "Starting Ekos...";
1091 
1092                 return false;
1093             }
1094         }
1095 
1096         case EKOS_STARTING:
1097         {
1098             if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1099             {
1100                 appendLogText(i18n("Ekos started."));
1101                 moduleState()->resetEkosConnectFailureCount();
1102                 moduleState()->setEkosState(EKOS_READY);
1103                 return true;
1104             }
1105             else if (moduleState()->ekosCommunicationStatus() == Ekos::Error)
1106             {
1107                 if (moduleState()->increaseEkosConnectFailureCount())
1108                 {
1109                     appendLogText(i18n("Starting Ekos failed. Retrying..."));
1110                     ekosInterface()->call(QDBus::AutoDetect, "start");
1111                     return false;
1112                 }
1113 
1114                 appendLogText(i18n("Starting Ekos failed."));
1115                 stopScheduler();
1116                 return false;
1117             }
1118             else if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1119                 return false;
1120             // If a minute passed, give up
1121             else if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1122             {
1123                 if (moduleState()->increaseEkosConnectFailureCount())
1124                 {
1125                     appendLogText(i18n("Starting Ekos timed out. Retrying..."));
1126                     ekosInterface()->call(QDBus::AutoDetect, "stop");
1127                     QTimer::singleShot(1000, this, [&]()
1128                     {
1129                         ekosInterface()->call(QDBus::AutoDetect, "start");
1130                         moduleState()->startCurrentOperationTimer();
1131                     });
1132                     return false;
1133                 }
1134 
1135                 appendLogText(i18n("Starting Ekos timed out."));
1136                 stopScheduler();
1137                 return false;
1138             }
1139         }
1140         break;
1141 
1142         case EKOS_STOPPING:
1143         {
1144             if (moduleState()->ekosCommunicationStatus() == Ekos::Idle)
1145             {
1146                 appendLogText(i18n("Ekos stopped."));
1147                 moduleState()->setEkosState(EKOS_IDLE);
1148                 return true;
1149             }
1150         }
1151         break;
1152 
1153         case EKOS_READY:
1154             return true;
1155     }
1156     return false;
1157 }
1158 
1159 bool SchedulerProcess::checkINDIState()
1160 {
1161     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1162         return false;
1163 
1164     switch (moduleState()->indiState())
1165     {
1166         case INDI_IDLE:
1167         {
1168             if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1169             {
1170                 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1171                 moduleState()->resetIndiConnectFailureCount();
1172                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties...";
1173             }
1174             else
1175             {
1176                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices...";
1177                 ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1178                 moduleState()->setIndiState(INDI_CONNECTING);
1179 
1180                 moduleState()->startCurrentOperationTimer();
1181             }
1182         }
1183         break;
1184 
1185         case INDI_CONNECTING:
1186         {
1187             if (moduleState()->indiCommunicationStatus() == Ekos::Success)
1188             {
1189                 appendLogText(i18n("INDI devices connected."));
1190                 moduleState()->setIndiState(INDI_PROPERTY_CHECK);
1191             }
1192             else if (moduleState()->indiCommunicationStatus() == Ekos::Error)
1193             {
1194                 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1195                 {
1196                     appendLogText(i18n("One or more INDI devices failed to connect. Retrying..."));
1197                     ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1198                 }
1199                 else
1200                 {
1201                     appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details."));
1202                     stopScheduler();
1203                 }
1204             }
1205             // If 30 seconds passed, we retry
1206             else if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1207             {
1208                 if (moduleState()->increaseIndiConnectFailureCount() <= moduleState()->maxFailureAttempts())
1209                 {
1210                     appendLogText(i18n("One or more INDI devices timed out. Retrying..."));
1211                     ekosInterface()->call(QDBus::AutoDetect, "connectDevices");
1212                     moduleState()->startCurrentOperationTimer();
1213                 }
1214                 else
1215                 {
1216                     appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details."));
1217                     stopScheduler();
1218                 }
1219             }
1220         }
1221         break;
1222 
1223         case INDI_DISCONNECTING:
1224         {
1225             if (moduleState()->indiCommunicationStatus() == Ekos::Idle)
1226             {
1227                 appendLogText(i18n("INDI devices disconnected."));
1228                 moduleState()->setIndiState(INDI_IDLE);
1229                 return true;
1230             }
1231         }
1232         break;
1233 
1234         case INDI_PROPERTY_CHECK:
1235         {
1236             qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties.";
1237             // If dome unparking is required then we wait for dome interface
1238             if (Options::schedulerUnparkDome() && moduleState()->domeReady() == false)
1239             {
1240                 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1241                 {
1242                     moduleState()->startCurrentOperationTimer();
1243                     appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover..."));
1244                     disconnectINDI();
1245                     stopEkos();
1246                 }
1247 
1248                 appendLogText(i18n("Dome unpark required but dome is not yet ready."));
1249                 return false;
1250             }
1251 
1252             // If mount unparking is required then we wait for mount interface
1253             if (Options::schedulerUnparkMount() && moduleState()->mountReady() == false)
1254             {
1255                 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1256                 {
1257                     moduleState()->startCurrentOperationTimer();
1258                     appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover..."));
1259                     disconnectINDI();
1260                     stopEkos();
1261                 }
1262 
1263                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready.";
1264                 return false;
1265             }
1266 
1267             // If cap unparking is required then we wait for cap interface
1268             if (Options::schedulerOpenDustCover() && moduleState()->capReady() == false)
1269             {
1270                 if (moduleState()->getCurrentOperationMsec() > (30 * 1000))
1271                 {
1272                     moduleState()->startCurrentOperationTimer();
1273                     appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover..."));
1274                     disconnectINDI();
1275                     stopEkos();
1276                 }
1277 
1278                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready.";
1279                 return false;
1280             }
1281 
1282             // capture interface is required at all times to proceed.
1283             if (captureInterface().isNull())
1284                 return false;
1285 
1286             if (moduleState()->captureReady() == false)
1287             {
1288                 QVariant hasCoolerControl = captureInterface()->property("coolerControl");
1289                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cooler control" << (!hasCoolerControl.isValid() ? "invalid" :
1290                                                (hasCoolerControl.toBool() ? "True" : "Faklse"));
1291                 if (hasCoolerControl.isValid())
1292                     moduleState()->setCaptureReady(true);
1293                 else
1294                     qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet...";
1295             }
1296 
1297             moduleState()->setIndiState(INDI_READY);
1298             moduleState()->resetIndiConnectFailureCount();
1299             return true;
1300         }
1301 
1302         case INDI_READY:
1303             return true;
1304     }
1305 
1306     return false;
1307 }
1308 
1309 bool SchedulerProcess::completeShutdown()
1310 {
1311     // If INDI is not done disconnecting, try again later
1312     if (moduleState()->indiState() == INDI_DISCONNECTING
1313             && checkINDIState() == false)
1314         return false;
1315 
1316     // Disconnect INDI if required first
1317     if (moduleState()->indiState() != INDI_IDLE && Options::stopEkosAfterShutdown())
1318     {
1319         disconnectINDI();
1320         return false;
1321     }
1322 
1323     // If Ekos is not done stopping, try again later
1324     if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
1325         return false;
1326 
1327     // Stop Ekos if required.
1328     if (moduleState()->ekosState() != EKOS_IDLE && Options::stopEkosAfterShutdown())
1329     {
1330         stopEkos();
1331         return false;
1332     }
1333 
1334     if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
1335         appendLogText(i18n("Shutdown complete."));
1336     else
1337         appendLogText(i18n("Shutdown procedure failed, aborting..."));
1338 
1339     // Stop Scheduler
1340     stopScheduler();
1341 
1342     return true;
1343 }
1344 
1345 void SchedulerProcess::disconnectINDI()
1346 {
1347     qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI...";
1348     moduleState()->setIndiState(INDI_DISCONNECTING);
1349     ekosInterface()->call(QDBus::AutoDetect, "disconnectDevices");
1350 }
1351 
1352 void SchedulerProcess::stopEkos()
1353 {
1354     qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos...";
1355     moduleState()->setEkosState(EKOS_STOPPING);
1356     moduleState()->resetEkosConnectFailureCount();
1357     ekosInterface()->call(QDBus::AutoDetect, "stop");
1358     moduleState()->setMountReady(false);
1359     moduleState()->setCaptureReady(false);
1360     moduleState()->setDomeReady(false);
1361     moduleState()->setCapReady(false);
1362 }
1363 
1364 bool SchedulerProcess::manageConnectionLoss()
1365 {
1366     if (SCHEDULER_RUNNING != moduleState()->schedulerState())
1367         return false;
1368 
1369     // Don't manage loss if Ekos is actually down in the state machine
1370     switch (moduleState()->ekosState())
1371     {
1372         case EKOS_IDLE:
1373         case EKOS_STOPPING:
1374             return false;
1375 
1376         default:
1377             break;
1378     }
1379 
1380     // Don't manage loss if INDI is actually down in the state machine
1381     switch (moduleState()->indiState())
1382     {
1383         case INDI_IDLE:
1384         case INDI_DISCONNECTING:
1385             return false;
1386 
1387         default:
1388             break;
1389     }
1390 
1391     // If Ekos is assumed to be up, check its state
1392     //QDBusReply<int> const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1393     if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1394     {
1395         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss.");
1396 
1397         // If INDI is assumed to be up, check its state
1398         if (moduleState()->isINDIConnected())
1399         {
1400             // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error
1401             qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed.");
1402             return false;
1403         }
1404     }
1405 
1406     // Stop actions of the current job
1407     stopCurrentJobAction();
1408 
1409     // Acknowledge INDI and Ekos disconnections
1410     disconnectINDI();
1411     stopEkos();
1412 
1413     // Let the Scheduler attempt to connect INDI again
1414     return true;
1415 
1416 }
1417 
1418 void SchedulerProcess::checkCapParkingStatus()
1419 {
1420     if (capInterface().isNull())
1421         return;
1422 
1423     QVariant parkingStatus = capInterface()->property("parkStatus");
1424     qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1425 
1426     if (parkingStatus.isValid() == false)
1427     {
1428         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
1429                                               capInterface()->lastError().type());
1430         if (!manageConnectionLoss())
1431             parkingStatus = ISD::PARK_ERROR;
1432     }
1433 
1434     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1435 
1436     switch (status)
1437     {
1438         case ISD::PARK_PARKED:
1439             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1440             {
1441                 appendLogText(i18n("Cap parked."));
1442                 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
1443             }
1444             moduleState()->resetParkingCapFailureCount();
1445             break;
1446 
1447         case ISD::PARK_UNPARKED:
1448             if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1449             {
1450                 moduleState()->setStartupState(STARTUP_COMPLETE);
1451                 appendLogText(i18n("Cap unparked."));
1452             }
1453             moduleState()->resetParkingCapFailureCount();
1454             break;
1455 
1456         case ISD::PARK_PARKING:
1457         case ISD::PARK_UNPARKING:
1458             // TODO make the timeouts configurable by the user
1459             if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1460             {
1461                 if (moduleState()->increaseParkingCapFailureCount())
1462                 {
1463                     appendLogText(i18n("Operation timeout. Restarting operation..."));
1464                     if (status == ISD::PARK_PARKING)
1465                         parkCap();
1466                     else
1467                         unParkCap();
1468                     break;
1469                 }
1470             }
1471             break;
1472 
1473         case ISD::PARK_ERROR:
1474             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_CAP)
1475             {
1476                 appendLogText(i18n("Cap parking error."));
1477                 moduleState()->setShutdownState(SHUTDOWN_ERROR);
1478             }
1479             else if (moduleState()->startupState() == STARTUP_UNPARKING_CAP)
1480             {
1481                 appendLogText(i18n("Cap unparking error."));
1482                 moduleState()->setStartupState(STARTUP_ERROR);
1483             }
1484             moduleState()->resetParkingCapFailureCount();
1485             break;
1486 
1487         default:
1488             break;
1489     }
1490 }
1491 
1492 void SchedulerProcess::checkMountParkingStatus()
1493 {
1494     if (mountInterface().isNull())
1495         return;
1496 
1497     QVariant parkingStatus = mountInterface()->property("parkStatus");
1498     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1499 
1500     if (parkingStatus.isValid() == false)
1501     {
1502         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
1503                                               mountInterface()->lastError().type());
1504         if (!manageConnectionLoss())
1505             moduleState()->setParkWaitState(PARKWAIT_ERROR);
1506     }
1507 
1508     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1509 
1510     switch (status)
1511     {
1512         //case Mount::PARKING_OK:
1513         case ISD::PARK_PARKED:
1514             // If we are starting up, we will unpark the mount in checkParkWaitState soon
1515             // If we are shutting down and mount is parked, proceed to next step
1516             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1517                 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1518 
1519             // Update parking engine state
1520             if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1521                 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1522 
1523             appendLogText(i18n("Mount parked."));
1524             moduleState()->resetParkingMountFailureCount();
1525             break;
1526 
1527         //case Mount::UNPARKING_OK:
1528         case ISD::PARK_UNPARKED:
1529             // If we are starting up and mount is unparked, proceed to next step
1530             // If we are shutting down, we will park the mount in checkParkWaitState soon
1531             if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1532                 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1533 
1534             // Update parking engine state
1535             if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1536                 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1537 
1538             appendLogText(i18n("Mount unparked."));
1539             moduleState()->resetParkingMountFailureCount();
1540             break;
1541 
1542         // FIXME: Create an option for the parking/unparking timeout.
1543 
1544         //case Mount::UNPARKING_BUSY:
1545         case ISD::PARK_UNPARKING:
1546             if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1547             {
1548                 if (moduleState()->increaseParkingMountFailureCount())
1549                 {
1550                     appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...",
1551                                        moduleState()->parkingMountFailureCount(), moduleState()->maxFailureAttempts()));
1552                     unParkMount();
1553                 }
1554                 else
1555                 {
1556                     appendLogText(i18n("Warning: mount unpark operation timed out on last attempt."));
1557                     moduleState()->setParkWaitState(PARKWAIT_ERROR);
1558                 }
1559             }
1560             else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
1561 
1562             break;
1563 
1564         //case Mount::PARKING_BUSY:
1565         case ISD::PARK_PARKING:
1566             if (moduleState()->getCurrentOperationMsec() > (60 * 1000))
1567             {
1568                 if (moduleState()->increaseParkingMountFailureCount())
1569                 {
1570                     appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...",
1571                                        moduleState()->parkingMountFailureCount(),
1572                                        moduleState()->maxFailureAttempts()));
1573                     parkMount();
1574                 }
1575                 else
1576                 {
1577                     appendLogText(i18n("Warning: mount park operation timed out on last attempt."));
1578                     moduleState()->setParkWaitState(PARKWAIT_ERROR);
1579                 }
1580             }
1581             else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress...";
1582 
1583             break;
1584 
1585         //case Mount::PARKING_ERROR:
1586         case ISD::PARK_ERROR:
1587             if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1588             {
1589                 appendLogText(i18n("Mount unparking error."));
1590                 moduleState()->setStartupState(STARTUP_ERROR);
1591                 moduleState()->resetParkingMountFailureCount();
1592             }
1593             else if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1594             {
1595                 if (moduleState()->increaseParkingMountFailureCount())
1596                 {
1597                     appendLogText(i18n("Warning: mount park operation failed on attempt %1/%2. Restarting operation...",
1598                                        moduleState()->parkingMountFailureCount(),
1599                                        moduleState()->maxFailureAttempts()));
1600                     parkMount();
1601                 }
1602                 else
1603                 {
1604                     appendLogText(i18n("Mount parking error."));
1605                     moduleState()->setShutdownState(SHUTDOWN_ERROR);
1606                     moduleState()->resetParkingMountFailureCount();
1607                 }
1608 
1609             }
1610             else if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1611             {
1612                 appendLogText(i18n("Mount parking error."));
1613                 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1614                 moduleState()->resetParkingMountFailureCount();
1615             }
1616             else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1617             {
1618                 appendLogText(i18n("Mount unparking error."));
1619                 moduleState()->setParkWaitState(PARKWAIT_ERROR);
1620                 moduleState()->resetParkingMountFailureCount();
1621             }
1622             break;
1623 
1624         //case Mount::PARKING_IDLE:
1625         // FIXME Does this work as intended? check!
1626         case ISD::PARK_UNKNOWN:
1627             // Last parking action did not result in an action, so proceed to next step
1628             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_MOUNT)
1629                 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1630 
1631             // Last unparking action did not result in an action, so proceed to next step
1632             if (moduleState()->startupState() == STARTUP_UNPARKING_MOUNT)
1633                 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1634 
1635             // Update parking engine state
1636             if (moduleState()->parkWaitState() == PARKWAIT_PARKING)
1637                 moduleState()->setParkWaitState(PARKWAIT_PARKED);
1638             else if (moduleState()->parkWaitState() == PARKWAIT_UNPARKING)
1639                 moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
1640 
1641             moduleState()->resetParkingMountFailureCount();
1642             break;
1643     }
1644 }
1645 
1646 void SchedulerProcess::checkDomeParkingStatus()
1647 {
1648     if (domeInterface().isNull())
1649         return;
1650 
1651     QVariant parkingStatus = domeInterface()->property("parkStatus");
1652     qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
1653 
1654     if (parkingStatus.isValid() == false)
1655     {
1656         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
1657                                               mountInterface()->lastError().type());
1658         if (!manageConnectionLoss())
1659             moduleState()->setParkWaitState(PARKWAIT_ERROR);
1660     }
1661 
1662     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
1663 
1664     switch (status)
1665     {
1666         case ISD::PARK_PARKED:
1667             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1668             {
1669                 appendLogText(i18n("Dome parked."));
1670 
1671                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1672             }
1673             moduleState()->resetParkingDomeFailureCount();
1674             break;
1675 
1676         case ISD::PARK_UNPARKED:
1677             if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1678             {
1679                 moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
1680                 appendLogText(i18n("Dome unparked."));
1681             }
1682             moduleState()->resetParkingDomeFailureCount();
1683             break;
1684 
1685         case ISD::PARK_PARKING:
1686         case ISD::PARK_UNPARKING:
1687             // TODO make the timeouts configurable by the user
1688             if (moduleState()->getCurrentOperationMsec() > (120 * 1000))
1689             {
1690                 if (moduleState()->increaseParkingDomeFailureCount())
1691                 {
1692                     appendLogText(i18n("Operation timeout. Restarting operation..."));
1693                     if (status == ISD::PARK_PARKING)
1694                         parkDome();
1695                     else
1696                         unParkDome();
1697                     break;
1698                 }
1699             }
1700             break;
1701 
1702         case ISD::PARK_ERROR:
1703             if (moduleState()->shutdownState() == SHUTDOWN_PARKING_DOME)
1704             {
1705                 if (moduleState()->increaseParkingDomeFailureCount())
1706                 {
1707                     appendLogText(i18n("Dome parking failed. Restarting operation..."));
1708                     parkDome();
1709                 }
1710                 else
1711                 {
1712                     appendLogText(i18n("Dome parking error."));
1713                     moduleState()->setShutdownState(SHUTDOWN_ERROR);
1714                     moduleState()->resetParkingDomeFailureCount();
1715                 }
1716             }
1717             else if (moduleState()->startupState() == STARTUP_UNPARKING_DOME)
1718             {
1719                 if (moduleState()->increaseParkingDomeFailureCount())
1720                 {
1721                     appendLogText(i18n("Dome unparking failed. Restarting operation..."));
1722                     unParkDome();
1723                 }
1724                 else
1725                 {
1726                     appendLogText(i18n("Dome unparking error."));
1727                     moduleState()->setStartupState(STARTUP_ERROR);
1728                     moduleState()->resetParkingDomeFailureCount();
1729                 }
1730             }
1731             break;
1732 
1733         default:
1734             break;
1735     }
1736 }
1737 
1738 bool SchedulerProcess::checkStartupState()
1739 {
1740     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1741         return false;
1742 
1743     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(moduleState()->startupState());
1744 
1745     switch (moduleState()->startupState())
1746     {
1747         case STARTUP_IDLE:
1748         {
1749             KSNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process"),
1750                                   KSNotification::Scheduler);
1751 
1752             qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process...";
1753 
1754             // If Ekos is already started, we skip the script and move on to dome unpark step
1755             // unless we do not have light frames, then we skip all
1756             //QDBusReply<int> isEkosStarted;
1757             //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
1758             //if (isEkosStarted.value() == Ekos::Success)
1759             if (moduleState()->ekosCommunicationStatus() == Ekos::Success)
1760             {
1761                 if (moduleState()->startupScriptURL().isEmpty() == false)
1762                     appendLogText(i18n("Ekos is already started, skipping startup script..."));
1763 
1764                 if (!activeJob() || activeJob()->getLightFramesRequired())
1765                     moduleState()->setStartupState(STARTUP_UNPARK_DOME);
1766                 else
1767                     moduleState()->setStartupState(STARTUP_COMPLETE);
1768                 return true;
1769             }
1770 
1771             if (moduleState()->currentProfile() != i18n("Default"))
1772             {
1773                 QList<QVariant> profile;
1774                 profile.append(moduleState()->currentProfile());
1775                 ekosInterface()->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile);
1776             }
1777 
1778             if (moduleState()->startupScriptURL().isEmpty() == false)
1779             {
1780                 moduleState()->setStartupState(STARTUP_SCRIPT);
1781                 executeScript(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile));
1782                 return false;
1783             }
1784 
1785             moduleState()->setStartupState(STARTUP_UNPARK_DOME);
1786             return false;
1787         }
1788 
1789         case STARTUP_SCRIPT:
1790             return false;
1791 
1792         case STARTUP_UNPARK_DOME:
1793             // If there is no job in case of manual startup procedure,
1794             // or if the job requires light frames, let's proceed with
1795             // unparking the dome, otherwise startup process is complete.
1796             if (activeJob() == nullptr || activeJob()->getLightFramesRequired())
1797             {
1798                 if (Options::schedulerUnparkDome())
1799                     unParkDome();
1800                 else
1801                     moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
1802             }
1803             else
1804             {
1805                 moduleState()->setStartupState(STARTUP_COMPLETE);
1806                 return true;
1807             }
1808 
1809             break;
1810 
1811         case STARTUP_UNPARKING_DOME:
1812             checkDomeParkingStatus();
1813             break;
1814 
1815         case STARTUP_UNPARK_MOUNT:
1816             if (Options::schedulerUnparkMount())
1817                 unParkMount();
1818             else
1819                 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
1820             break;
1821 
1822         case STARTUP_UNPARKING_MOUNT:
1823             checkMountParkingStatus();
1824             break;
1825 
1826         case STARTUP_UNPARK_CAP:
1827             if (Options::schedulerOpenDustCover())
1828                 unParkCap();
1829             else
1830                 moduleState()->setStartupState(STARTUP_COMPLETE);
1831             break;
1832 
1833         case STARTUP_UNPARKING_CAP:
1834             checkCapParkingStatus();
1835             break;
1836 
1837         case STARTUP_COMPLETE:
1838             return true;
1839 
1840         case STARTUP_ERROR:
1841             stopScheduler();
1842             return true;
1843     }
1844 
1845     return false;
1846 }
1847 
1848 bool SchedulerProcess::checkShutdownState()
1849 {
1850     qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state...";
1851 
1852     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1853         return false;
1854 
1855     switch (moduleState()->shutdownState())
1856     {
1857         case SHUTDOWN_IDLE:
1858 
1859             qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process...";
1860 
1861             moduleState()->setActiveJob(nullptr);
1862             moduleState()->setupNextIteration(RUN_SHUTDOWN);
1863             emit shutdownStarted();
1864 
1865             if (Options::schedulerWarmCCD())
1866             {
1867                 appendLogText(i18n("Warming up CCD..."));
1868 
1869                 // Turn it off
1870                 //QVariant arg(false);
1871                 //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg);
1872                 if (captureInterface())
1873                 {
1874                     qCDebug(KSTARS_EKOS_SCHEDULER) << "Setting coolerControl=false";
1875                     captureInterface()->setProperty("coolerControl", false);
1876                 }
1877             }
1878 
1879             // The following steps require a connection to the INDI server
1880             if (moduleState()->isINDIConnected())
1881             {
1882                 if (Options::schedulerCloseDustCover())
1883                 {
1884                     moduleState()->setShutdownState(SHUTDOWN_PARK_CAP);
1885                     return false;
1886                 }
1887 
1888                 if (Options::schedulerParkMount())
1889                 {
1890                     moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
1891                     return false;
1892                 }
1893 
1894                 if (Options::schedulerParkDome())
1895                 {
1896                     moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1897                     return false;
1898                 }
1899             }
1900             else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection."));
1901 
1902             if (moduleState()->shutdownScriptURL().isEmpty() == false)
1903             {
1904                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1905                 return false;
1906             }
1907 
1908             moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
1909             return true;
1910 
1911         case SHUTDOWN_PARK_CAP:
1912             if (!moduleState()->isINDIConnected())
1913             {
1914                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
1915                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1916             }
1917             else if (Options::schedulerCloseDustCover())
1918                 parkCap();
1919             else
1920                 moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
1921             break;
1922 
1923         case SHUTDOWN_PARKING_CAP:
1924             checkCapParkingStatus();
1925             break;
1926 
1927         case SHUTDOWN_PARK_MOUNT:
1928             if (!moduleState()->isINDIConnected())
1929             {
1930                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
1931                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1932             }
1933             else if (Options::schedulerParkMount())
1934                 parkMount();
1935             else
1936                 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
1937             break;
1938 
1939         case SHUTDOWN_PARKING_MOUNT:
1940             checkMountParkingStatus();
1941             break;
1942 
1943         case SHUTDOWN_PARK_DOME:
1944             if (!moduleState()->isINDIConnected())
1945             {
1946                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
1947                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1948             }
1949             else if (Options::schedulerParkDome())
1950                 parkDome();
1951             else
1952                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
1953             break;
1954 
1955         case SHUTDOWN_PARKING_DOME:
1956             checkDomeParkingStatus();
1957             break;
1958 
1959         case SHUTDOWN_SCRIPT:
1960             if (moduleState()->shutdownScriptURL().isEmpty() == false)
1961             {
1962                 // Need to stop Ekos now before executing script if it happens to stop INDI
1963                 if (moduleState()->ekosState() != EKOS_IDLE && Options::shutdownScriptTerminatesINDI())
1964                 {
1965                     stopEkos();
1966                     return false;
1967                 }
1968 
1969                 moduleState()->setShutdownState(SHUTDOWN_SCRIPT_RUNNING);
1970                 executeScript(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile));
1971             }
1972             else
1973                 moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
1974             break;
1975 
1976         case SHUTDOWN_SCRIPT_RUNNING:
1977             return false;
1978 
1979         case SHUTDOWN_COMPLETE:
1980             return completeShutdown();
1981 
1982         case SHUTDOWN_ERROR:
1983             stopScheduler();
1984             return true;
1985     }
1986 
1987     return false;
1988 }
1989 
1990 bool SchedulerProcess::checkParkWaitState()
1991 {
1992     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
1993         return false;
1994 
1995     if (moduleState()->parkWaitState() == PARKWAIT_IDLE)
1996         return true;
1997 
1998     // qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State...";
1999 
2000     switch (moduleState()->parkWaitState())
2001     {
2002         case PARKWAIT_PARK:
2003             parkMount();
2004             break;
2005 
2006         case PARKWAIT_PARKING:
2007             checkMountParkingStatus();
2008             break;
2009 
2010         case PARKWAIT_UNPARK:
2011             unParkMount();
2012             break;
2013 
2014         case PARKWAIT_UNPARKING:
2015             checkMountParkingStatus();
2016             break;
2017 
2018         case PARKWAIT_IDLE:
2019         case PARKWAIT_PARKED:
2020         case PARKWAIT_UNPARKED:
2021             return true;
2022 
2023         case PARKWAIT_ERROR:
2024             appendLogText(i18n("park/unpark wait procedure failed, aborting..."));
2025             stopScheduler();
2026             return true;
2027 
2028     }
2029 
2030     return false;
2031 }
2032 
2033 void SchedulerProcess::runStartupProcedure()
2034 {
2035     if (moduleState()->startupState() == STARTUP_IDLE
2036             || moduleState()->startupState() == STARTUP_ERROR
2037             || moduleState()->startupState() == STARTUP_COMPLETE)
2038     {
2039         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2040         {
2041             KSMessageBox::Instance()->disconnect(this);
2042 
2043             appendLogText(i18n("Warning: executing startup procedure manually..."));
2044             moduleState()->setStartupState(STARTUP_IDLE);
2045             checkStartupState();
2046             QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
2047 
2048         });
2049 
2050         KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the startup procedure manually?"));
2051     }
2052     else
2053     {
2054         switch (moduleState()->startupState())
2055         {
2056             case STARTUP_IDLE:
2057                 break;
2058 
2059             case STARTUP_SCRIPT:
2060                 scriptProcess().terminate();
2061                 break;
2062 
2063             case STARTUP_UNPARK_DOME:
2064                 break;
2065 
2066             case STARTUP_UNPARKING_DOME:
2067                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking dome...";
2068                 domeInterface()->call(QDBus::AutoDetect, "abort");
2069                 break;
2070 
2071             case STARTUP_UNPARK_MOUNT:
2072                 break;
2073 
2074             case STARTUP_UNPARKING_MOUNT:
2075                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting unparking mount...";
2076                 mountInterface()->call(QDBus::AutoDetect, "abort");
2077                 break;
2078 
2079             case STARTUP_UNPARK_CAP:
2080                 break;
2081 
2082             case STARTUP_UNPARKING_CAP:
2083                 break;
2084 
2085             case STARTUP_COMPLETE:
2086                 break;
2087 
2088             case STARTUP_ERROR:
2089                 break;
2090         }
2091 
2092         moduleState()->setStartupState(STARTUP_IDLE);
2093 
2094         appendLogText(i18n("Startup procedure terminated."));
2095     }
2096 
2097 }
2098 
2099 void SchedulerProcess::runShutdownProcedure()
2100 {
2101     if (moduleState()->shutdownState() == SHUTDOWN_IDLE
2102             || moduleState()->shutdownState() == SHUTDOWN_ERROR
2103             || moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
2104     {
2105         connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this]()
2106         {
2107             KSMessageBox::Instance()->disconnect(this);
2108             appendLogText(i18n("Warning: executing shutdown procedure manually..."));
2109             moduleState()->setShutdownState(SHUTDOWN_IDLE);
2110             checkShutdownState();
2111             QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
2112         });
2113 
2114         KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to execute the shutdown procedure manually?"));
2115     }
2116     else
2117     {
2118         switch (moduleState()->shutdownState())
2119         {
2120             case SHUTDOWN_IDLE:
2121                 break;
2122 
2123             case SHUTDOWN_SCRIPT:
2124                 break;
2125 
2126             case SHUTDOWN_SCRIPT_RUNNING:
2127                 scriptProcess().terminate();
2128                 break;
2129 
2130             case SHUTDOWN_PARK_DOME:
2131                 break;
2132 
2133             case SHUTDOWN_PARKING_DOME:
2134                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking dome...";
2135                 domeInterface()->call(QDBus::AutoDetect, "abort");
2136                 break;
2137 
2138             case SHUTDOWN_PARK_MOUNT:
2139                 break;
2140 
2141             case SHUTDOWN_PARKING_MOUNT:
2142                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Aborting parking mount...";
2143                 mountInterface()->call(QDBus::AutoDetect, "abort");
2144                 break;
2145 
2146             case SHUTDOWN_PARK_CAP:
2147             case SHUTDOWN_PARKING_CAP:
2148                 break;
2149 
2150             case SHUTDOWN_COMPLETE:
2151                 break;
2152 
2153             case SHUTDOWN_ERROR:
2154                 break;
2155         }
2156 
2157         moduleState()->setShutdownState(SHUTDOWN_IDLE);
2158 
2159         appendLogText(i18n("Shutdown procedure terminated."));
2160     }
2161 }
2162 
2163 void SchedulerProcess::setPaused()
2164 {
2165     moduleState()->setupNextIteration(RUN_NOTHING);
2166     appendLogText(i18n("Scheduler paused."));
2167     emit schedulerPaused();
2168 }
2169 
2170 void SchedulerProcess::resetJobs()
2171 {
2172     // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept
2173     for (SchedulerJob * job : moduleState()->jobs())
2174     {
2175         job->reset();
2176         job->setCompletedCount(0);
2177     }
2178 
2179     // Unconditionally update the capture storage
2180     updateCompletedJobsCount(true);
2181 }
2182 
2183 void SchedulerProcess::selectActiveJob(const QList<SchedulerJob *> &jobs)
2184 {
2185     auto finished_or_aborted = [](SchedulerJob const * const job)
2186     {
2187         SchedulerJobStatus const s = job->getState();
2188         return SCHEDJOB_ERROR <= s || SCHEDJOB_ABORTED == s;
2189     };
2190 
2191     /* This predicate matches jobs that are neither scheduled to run nor aborted */
2192     auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job)
2193     {
2194         SchedulerJobStatus const s = job->getState();
2195         return SCHEDJOB_SCHEDULED != s && SCHEDJOB_ABORTED != s;
2196     };
2197 
2198     /* If there are no jobs left to run in the filtered list, stop evaluation */
2199     ErrorHandlingStrategy strategy = static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy());
2200     if (jobs.isEmpty() || std::all_of(jobs.begin(), jobs.end(), neither_scheduled_nor_aborted))
2201     {
2202         appendLogText(i18n("No jobs left in the scheduler queue after evaluating."));
2203         moduleState()->setActiveJob(nullptr);
2204         return;
2205     }
2206     /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */
2207     else if (std::all_of(jobs.begin(), jobs.end(), finished_or_aborted) &&
2208              strategy != ERROR_DONT_RESTART)
2209     {
2210         appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those."));
2211         std::for_each(jobs.begin(), jobs.end(), [](SchedulerJob * job)
2212         {
2213             if (SCHEDJOB_ABORTED == job->getState())
2214                 job->setState(SCHEDJOB_EVALUATION);
2215         });
2216 
2217         return;
2218     }
2219 
2220     // GreedyScheduler::scheduleJobs() must be called first.
2221     SchedulerJob *scheduledJob = getGreedyScheduler()->getScheduledJob();
2222     if (!scheduledJob)
2223     {
2224         appendLogText(i18n("No jobs scheduled."));
2225         moduleState()->setActiveJob(nullptr);
2226         return;
2227     }
2228     moduleState()->setActiveJob(scheduledJob);
2229 
2230 }
2231 
2232 void SchedulerProcess::evaluateJobs(bool evaluateOnly)
2233 {
2234     for (auto job : moduleState()->jobs())
2235         job->clearCache();
2236 
2237     /* Don't evaluate if list is empty */
2238     if (moduleState()->jobs().isEmpty())
2239         return;
2240     /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */
2241     if (Options::rememberJobProgress())
2242         updateCompletedJobsCount();
2243 
2244     moduleState()->calculateDawnDusk();
2245 
2246     getGreedyScheduler()->scheduleJobs(moduleState()->jobs(), SchedulerModuleState::getLocalTime(),
2247                                        moduleState()->capturedFramesCount(), this);
2248     // schedule or job states might have been changed, update the table
2249 
2250     if (!evaluateOnly && moduleState()->schedulerState() == SCHEDULER_RUNNING)
2251         // At this step, we finished evaluating jobs.
2252         // We select the first job that has to be run, per schedule.
2253         selectActiveJob(moduleState()->jobs());
2254     else
2255         qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required.";
2256 
2257     emit jobsUpdated(moduleState()->getJSONJobs());
2258 }
2259 
2260 bool SchedulerProcess::checkStatus()
2261 {
2262     if (moduleState()->schedulerState() == SCHEDULER_PAUSED)
2263     {
2264         if (activeJob() == nullptr)
2265         {
2266             setPaused();
2267             return false;
2268         }
2269         switch (activeJob()->getState())
2270         {
2271             case  SCHEDJOB_BUSY:
2272                 // do nothing
2273                 break;
2274             case  SCHEDJOB_COMPLETE:
2275                 // start finding next job before pausing
2276                 break;
2277             default:
2278                 // in all other cases pause
2279                 setPaused();
2280                 break;
2281         }
2282     }
2283 
2284     // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs
2285     if (activeJob() == nullptr)
2286     {
2287         // #2.1 If shutdown is already complete or in error, we need to stop
2288         if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE
2289                 || moduleState()->shutdownState() == SHUTDOWN_ERROR)
2290         {
2291             return completeShutdown();
2292         }
2293 
2294         // #2.2  Check if shutdown is in progress
2295         if (moduleState()->shutdownState() > SHUTDOWN_IDLE)
2296         {
2297             // If Ekos is not done stopping, try again later
2298             if (moduleState()->ekosState() == EKOS_STOPPING && checkEkosState() == false)
2299                 return false;
2300 
2301             checkShutdownState();
2302             return false;
2303         }
2304 
2305         // #2.3 Check if park wait procedure is in progress
2306         if (checkParkWaitState() == false)
2307             return false;
2308 
2309         // #2.4 If not in shutdown state, evaluate the jobs
2310         evaluateJobs(false);
2311 
2312         // #2.5 check if all jobs have completed and repeat is set
2313         if (nullptr == activeJob() && moduleState()->checkRepeatSequence())
2314         {
2315             // Reset all jobs
2316             resetJobs();
2317             // Re-evaluate all jobs to check whether there is at least one that might be executed
2318             evaluateJobs(false);
2319             // if there is an executable job, restart;
2320             if (activeJob())
2321             {
2322                 moduleState()->increaseSequenceExecutionCounter();
2323                 appendLogText(i18n("Starting job sequence iteration #%1", moduleState()->sequenceExecutionCounter()));
2324                 return true;
2325             }
2326         }
2327 
2328         // #2.6 If there is no current job after evaluation, shutdown
2329         if (nullptr == activeJob())
2330         {
2331             checkShutdownState();
2332             return false;
2333         }
2334     }
2335     // JM 2018-12-07: Check if we need to sleep
2336     else if (shouldSchedulerSleep(activeJob()) == false)
2337     {
2338         // #3 Check if startup procedure has failed.
2339         if (moduleState()->startupState() == STARTUP_ERROR)
2340         {
2341             // Stop Scheduler
2342             stopScheduler();
2343             return true;
2344         }
2345 
2346         // #4 Check if startup procedure Phase #1 is complete (Startup script)
2347         if ((moduleState()->startupState() == STARTUP_IDLE
2348                 && checkStartupState() == false)
2349                 || moduleState()->startupState() == STARTUP_SCRIPT)
2350             return false;
2351 
2352         // #5 Check if Ekos is started
2353         if (checkEkosState() == false)
2354             return false;
2355 
2356         // #6 Check if INDI devices are connected.
2357         if (checkINDIState() == false)
2358             return false;
2359 
2360         // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job
2361         if (checkParkWaitState() == false)
2362             return false;
2363 
2364         // #7 Check if startup procedure Phase #2 is complete (Unparking phase)
2365         if (moduleState()->startupState() > STARTUP_SCRIPT
2366                 && moduleState()->startupState() < STARTUP_ERROR
2367                 && checkStartupState() == false)
2368             return false;
2369 
2370         // #8 Check it it already completed (should only happen starting a paused job)
2371         //    Find the next job in this case, otherwise execute the current one
2372         if (activeJob() && activeJob()->getState() == SCHEDJOB_COMPLETE)
2373             findNextJob();
2374 
2375         // N.B. We explicitly do not check for return result here because regardless of execution result
2376         // we do not have any pending tasks further down.
2377         executeJob(activeJob());
2378         emit updateJobTable();
2379     }
2380 
2381     return true;
2382 }
2383 
2384 void SchedulerProcess::getNextAction()
2385 {
2386     qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action...";
2387 
2388     switch (activeJob()->getStage())
2389     {
2390         case SCHEDSTAGE_IDLE:
2391             if (activeJob()->getLightFramesRequired())
2392             {
2393                 if (activeJob()->getStepPipeline() & SchedulerJob::USE_TRACK)
2394                     startSlew();
2395                 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2396                 {
2397                     qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3485";
2398                     startFocusing();
2399                 }
2400                 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2401                     startAstrometry();
2402                 else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2403                     if (getGuidingStatus() == GUIDE_GUIDING)
2404                     {
2405                         appendLogText(i18n("Guiding already running, directly start capturing."));
2406                         startCapture();
2407                     }
2408                     else
2409                         startGuiding();
2410                 else
2411                     startCapture();
2412             }
2413             else
2414             {
2415                 if (activeJob()->getStepPipeline())
2416                     appendLogText(
2417                         i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.",
2418                              activeJob()->getName()));
2419                 startCapture();
2420             }
2421 
2422             break;
2423 
2424         case SCHEDSTAGE_SLEW_COMPLETE:
2425             if (activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS && moduleState()->autofocusCompleted() == false)
2426             {
2427                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3514";
2428                 startFocusing();
2429             }
2430             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2431                 startAstrometry();
2432             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2433                 startGuiding();
2434             else
2435                 startCapture();
2436             break;
2437 
2438         case SCHEDSTAGE_FOCUS_COMPLETE:
2439             if (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)
2440                 startAstrometry();
2441             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2442                 startGuiding();
2443             else
2444                 startCapture();
2445             break;
2446 
2447         case SCHEDSTAGE_ALIGN_COMPLETE:
2448             moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING);
2449             break;
2450 
2451         case SCHEDSTAGE_RESLEWING_COMPLETE:
2452             // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus
2453             // frame is ready for the capture module in-sequence-focus procedure.
2454             if ((activeJob()->getStepPipeline() & SchedulerJob::USE_FOCUS) && activeJob()->getInSequenceFocus())
2455                 // Post alignment re-focusing
2456             {
2457                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3544";
2458                 startFocusing();
2459             }
2460             else if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2461                 startGuiding();
2462             else
2463                 startCapture();
2464             break;
2465 
2466         case SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE:
2467             if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE)
2468                 startGuiding();
2469             else
2470                 startCapture();
2471             break;
2472 
2473         case SCHEDSTAGE_GUIDING_COMPLETE:
2474             startCapture();
2475             break;
2476 
2477         default:
2478             break;
2479     }
2480 }
2481 
2482 void SchedulerProcess::iterate()
2483 {
2484     const int msSleep = runSchedulerIteration();
2485     if (msSleep < 0)
2486         return;
2487 
2488     connect(&moduleState()->iterationTimer(), &QTimer::timeout, this, &SchedulerProcess::iterate, Qt::UniqueConnection);
2489     moduleState()->iterationTimer().setSingleShot(true);
2490     moduleState()->iterationTimer().start(msSleep);
2491 
2492 }
2493 
2494 int SchedulerProcess::runSchedulerIteration()
2495 {
2496     qint64 now = QDateTime::currentMSecsSinceEpoch();
2497     if (moduleState()->startMSecs() == 0)
2498         moduleState()->setStartMSecs(now);
2499 
2500     //    printStates(QString("\nrunScheduler Iteration %1 @ %2")
2501     //                .arg(moduleState()->increaseSchedulerIteration())
2502     //                .arg((now - moduleState()->startMSecs()) / 1000.0, 1, 'f', 3));
2503 
2504     SchedulerTimerState keepTimerState = moduleState()->timerState();
2505 
2506     // TODO: At some point we should require that timerState and timerInterval
2507     // be explicitly set in all iterations. Not there yet, would require too much
2508     // refactoring of the scheduler. When we get there, we'd exectute the following here:
2509     // timerState = RUN_NOTHING;    // don't like this comment, it should always set a state and interval!
2510     // timerInterval = -1;
2511     moduleState()->setIterationSetup(false);
2512     switch (keepTimerState)
2513     {
2514         case RUN_WAKEUP:
2515             changeSleepLabel("", false);
2516             wakeUpScheduler();
2517             break;
2518         case RUN_SCHEDULER:
2519             checkStatus();
2520             break;
2521         case RUN_JOBCHECK:
2522             checkJobStage();
2523             break;
2524         case RUN_SHUTDOWN:
2525             checkShutdownState();
2526             break;
2527         case RUN_NOTHING:
2528             moduleState()->setTimerInterval(-1);
2529             break;
2530     }
2531     if (!moduleState()->iterationSetup())
2532     {
2533         // See the above TODO.
2534         // Since iterations aren't yet always set up, we repeat the current
2535         // iteration type if one wasn't set up in the current iteration.
2536         // qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler iteration never set up.";
2537         moduleState()->setTimerInterval(moduleState()->updatePeriodMs());
2538     }
2539     //    printStates(QString("End iteration, sleep %1: ").arg(moduleState()->timerInterval()));
2540     return moduleState()->timerInterval();
2541 }
2542 
2543 void SchedulerProcess::checkJobStage()
2544 {
2545     Q_ASSERT_X(activeJob(), __FUNCTION__, "Actual current job is required to check job stage");
2546     if (!activeJob())
2547         return;
2548 
2549     if (checkJobStageCounter == 0)
2550     {
2551         qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << activeJob()->getName() << "startup" <<
2552                                        activeJob()->getStartupCondition() << activeJob()->getStartupTime().toString() << "state" << activeJob()->getState();
2553         if (checkJobStageCounter++ == 30)
2554             checkJobStageCounter = 0;
2555     }
2556 
2557     emit syncGreedyParams();
2558     if (!getGreedyScheduler()->checkJob(moduleState()->jobs(), SchedulerModuleState::getLocalTime(), activeJob()))
2559     {
2560         activeJob()->setState(SCHEDJOB_IDLE);
2561         stopCurrentJobAction();
2562         findNextJob();
2563         return;
2564     }
2565     checkJobStageEpilogue();
2566 }
2567 
2568 void SchedulerProcess::checkJobStageEpilogue()
2569 {
2570     if (!activeJob())
2571         return;
2572 
2573     // #5 Check system status to improve robustness
2574     // This handles external events such as disconnections or end-user manipulating INDI panel
2575     if (!checkStatus())
2576         return;
2577 
2578     // #5b Check the guiding timer, and possibly restart guiding.
2579     processGuidingTimer();
2580 
2581     // #6 Check each stage is processing properly
2582     // FIXME: Vanishing property should trigger a call to its event callback
2583     if (!activeJob()) return;
2584     switch (activeJob()->getStage())
2585     {
2586         case SCHEDSTAGE_IDLE:
2587             // Job is just starting.
2588             emit jobStarted(activeJob()->getName());
2589             getNextAction();
2590             break;
2591 
2592         case SCHEDSTAGE_ALIGNING:
2593             // Let's make sure align module does not become unresponsive
2594             if (moduleState()->getCurrentOperationMsec() > static_cast<int>(ALIGN_INACTIVITY_TIMEOUT))
2595             {
2596                 QVariant const status = alignInterface()->property("status");
2597                 Ekos::AlignState alignStatus = static_cast<Ekos::AlignState>(status.toInt());
2598 
2599                 if (alignStatus == Ekos::ALIGN_IDLE)
2600                 {
2601                     if (moduleState()->increaseAlignFailureCount())
2602                     {
2603                         qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request...";
2604                         startAstrometry();
2605                     }
2606                     else
2607                     {
2608                         appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
2609                         activeJob()->setState(SCHEDJOB_ABORTED);
2610                         findNextJob();
2611                     }
2612                 }
2613                 else
2614                     moduleState()->startCurrentOperationTimer();
2615             }
2616             break;
2617 
2618         case SCHEDSTAGE_CAPTURING:
2619             // Let's make sure capture module does not become unresponsive
2620             if (moduleState()->getCurrentOperationMsec() > static_cast<int>(CAPTURE_INACTIVITY_TIMEOUT))
2621             {
2622                 QVariant const status = captureInterface()->property("status");
2623                 Ekos::CaptureState captureStatus = static_cast<Ekos::CaptureState>(status.toInt());
2624 
2625                 if (captureStatus == Ekos::CAPTURE_IDLE)
2626                 {
2627                     if (moduleState()->increaseCaptureFailureCount())
2628                     {
2629                         qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request...";
2630                         startCapture();
2631                     }
2632                     else
2633                     {
2634                         appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", activeJob()->getName()));
2635                         activeJob()->setState(SCHEDJOB_ABORTED);
2636                         findNextJob();
2637                     }
2638                 }
2639                 else moduleState()->startCurrentOperationTimer();
2640             }
2641             break;
2642 
2643         case SCHEDSTAGE_FOCUSING:
2644             // Let's make sure focus module does not become unresponsive
2645             if (moduleState()->getCurrentOperationMsec() > static_cast<int>(FOCUS_INACTIVITY_TIMEOUT))
2646             {
2647                 QVariant const status = focusInterface()->property("status");
2648                 Ekos::FocusState focusStatus = static_cast<Ekos::FocusState>(status.toInt());
2649 
2650                 if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING)
2651                 {
2652                     if (moduleState()->increaseFocusFailureCount())
2653                     {
2654                         qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request...";
2655                         startFocusing();
2656                     }
2657                     else
2658                     {
2659                         appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", activeJob()->getName()));
2660                         activeJob()->setState(SCHEDJOB_ABORTED);
2661                         findNextJob();
2662                     }
2663                 }
2664                 else moduleState()->startCurrentOperationTimer();
2665             }
2666             break;
2667 
2668         case SCHEDSTAGE_GUIDING:
2669             // Let's make sure guide module does not become unresponsive
2670             if (moduleState()->getCurrentOperationMsec() > GUIDE_INACTIVITY_TIMEOUT)
2671             {
2672                 GuideState guideStatus = getGuidingStatus();
2673 
2674                 if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED)
2675                 {
2676                     if (moduleState()->increaseGuideFailureCount())
2677                     {
2678                         qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request...";
2679                         startGuiding();
2680                     }
2681                     else
2682                     {
2683                         appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
2684                         activeJob()->setState(SCHEDJOB_ABORTED);
2685                         findNextJob();
2686                     }
2687                 }
2688                 else moduleState()->startCurrentOperationTimer();
2689             }
2690             break;
2691 
2692         case SCHEDSTAGE_SLEWING:
2693         case SCHEDSTAGE_RESLEWING:
2694             // While slewing or re-slewing, check slew status can still be obtained
2695         {
2696             QVariant const slewStatus = mountInterface()->property("status");
2697 
2698             if (slewStatus.isValid())
2699             {
2700                 // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event
2701                 // FIXME: in that case, filter TRACKING events only?
2702                 ISD::Mount::Status const status = static_cast<ISD::Mount::Status>(slewStatus.toInt());
2703                 setMountStatus(status);
2704             }
2705             else
2706             {
2707                 appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", activeJob()->getName()));
2708                 if (!manageConnectionLoss())
2709                     activeJob()->setState(SCHEDJOB_ERROR);
2710                 return;
2711             }
2712         }
2713         break;
2714 
2715         case SCHEDSTAGE_SLEW_COMPLETE:
2716         case SCHEDSTAGE_RESLEWING_COMPLETE:
2717             // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving
2718             if (moduleState()->domeReady())
2719             {
2720                 QVariant const isDomeMoving = domeInterface()->property("isMoving");
2721 
2722                 if (!isDomeMoving.isValid())
2723                 {
2724                     appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", activeJob()->getName()));
2725                     if (!manageConnectionLoss())
2726                         activeJob()->setState(SCHEDJOB_ERROR);
2727                     return;
2728                 }
2729 
2730                 if (!isDomeMoving.value<bool>())
2731                     getNextAction();
2732             }
2733             else getNextAction();
2734             break;
2735 
2736         default:
2737             break;
2738     }
2739 }
2740 
2741 bool SchedulerProcess::executeJob(SchedulerJob * job)
2742 {
2743     if (job == nullptr)
2744         return false;
2745 
2746     // Don't execute the current job if it is already busy
2747     if (activeJob() == job && SCHEDJOB_BUSY == activeJob()->getState())
2748         return false;
2749 
2750     moduleState()->setActiveJob(job);
2751 
2752     // If we already started, we check when the next object is scheduled at.
2753     // If it is more than 30 minutes in the future, we park the mount if that is supported
2754     // and we unpark when it is due to start.
2755     //int const nextObservationTime = now.secsTo(getActiveJob()->getStartupTime());
2756 
2757     // If the time to wait is greater than the lead time (5 minutes by default)
2758     // then we sleep, otherwise we wait. It's the same thing, just different labels.
2759     if (shouldSchedulerSleep(activeJob()))
2760         return false;
2761     // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt
2762     else if (0 < SchedulerModuleState::getLocalTime().secsTo(activeJob()->getStartupTime()))
2763         return false;
2764 
2765     // From this point job can be executed now
2766 
2767     if (job->getCompletionCondition() == FINISH_SEQUENCE && Options::rememberJobProgress())
2768         captureInterface()->setProperty("targetName", job->getName());
2769 
2770     moduleState()->calculateDawnDusk();
2771 
2772     // Reset autofocus so that focus step is applied properly when checked
2773     // When the focus step is not checked, the capture module will eventually run focus periodically
2774     moduleState()->setAutofocusCompleted(false);
2775 
2776     qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << activeJob()->getName();
2777 
2778     activeJob()->setState(SCHEDJOB_BUSY);
2779     emit jobsUpdated(moduleState()->getJSONJobs());
2780 
2781     KSNotification::event(QLatin1String("EkosSchedulerJobStart"),
2782                           i18n("Ekos job started (%1)", activeJob()->getName()), KSNotification::Scheduler);
2783 
2784     // No need to continue evaluating jobs as we already have one.
2785     moduleState()->setupNextIteration(RUN_JOBCHECK);
2786     return true;
2787 }
2788 
2789 bool SchedulerProcess::saveScheduler(const QUrl &fileURL)
2790 {
2791     QFile file;
2792     file.setFileName(fileURL.toLocalFile());
2793 
2794     if (!file.open(QIODevice::WriteOnly))
2795     {
2796         QString message = i18n("Unable to write to file %1", fileURL.toLocalFile());
2797         KSNotification::sorry(message, i18n("Could Not Open File"));
2798         return false;
2799     }
2800 
2801     QTextStream outstream(&file);
2802 
2803     // We serialize sequence data to XML using the C locale
2804     QLocale cLocale = QLocale::c();
2805 
2806     outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << Qt::endl;
2807     outstream << "<SchedulerList version='1.6'>" << Qt::endl;
2808     // ensure to escape special XML characters
2809     outstream << "<Profile>" << QString(entityXML(strdup(moduleState()->currentProfile().toStdString().c_str()))) <<
2810               "</Profile>" << Qt::endl;
2811 
2812     auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
2813     bool useMosaicInfo = !tiles->sequenceFile().isEmpty();
2814 
2815     if (useMosaicInfo)
2816     {
2817         outstream << "<Mosaic>" << Qt::endl;
2818         outstream << "<Target>" << tiles->targetName() << "</Target>" << Qt::endl;
2819         outstream << "<Group>" << tiles->group() << "</Group>" << Qt::endl;
2820 
2821         QString ccArg, ccValue = tiles->completionCondition(&ccArg);
2822         if (ccValue == "FinishSequence")
2823             outstream << "<FinishSequence/>" << Qt::endl;
2824         else if (ccValue == "FinishLoop")
2825             outstream << "<FinishLoop/>" << Qt::endl;
2826         else if (ccValue == "FinishRepeat")
2827             outstream << "<FinishRepeat>" << ccArg << "</FinishRepeat>" << Qt::endl;
2828 
2829         outstream << "<Sequence>" << tiles->sequenceFile() << "</Sequence>" << Qt::endl;
2830         outstream << "<Directory>" << tiles->outputDirectory() << "</Directory>" << Qt::endl;
2831 
2832         outstream << "<FocusEveryN>" << tiles->focusEveryN() << "</FocusEveryN>" << Qt::endl;
2833         outstream << "<AlignEveryN>" << tiles->alignEveryN() << "</AlignEveryN>" << Qt::endl;
2834         if (tiles->isTrackChecked())
2835             outstream << "<TrackChecked/>" << Qt::endl;
2836         if (tiles->isFocusChecked())
2837             outstream << "<FocusChecked/>" << Qt::endl;
2838         if (tiles->isAlignChecked())
2839             outstream << "<AlignChecked/>" << Qt::endl;
2840         if (tiles->isGuideChecked())
2841             outstream << "<GuideChecked/>" << Qt::endl;
2842         outstream << "<Overlap>" << cLocale.toString(tiles->overlap()) << "</Overlap>" << Qt::endl;
2843         outstream << "<CenterRA>" << cLocale.toString(tiles->ra0().Hours()) << "</CenterRA>" << Qt::endl;
2844         outstream << "<CenterDE>" << cLocale.toString(tiles->dec0().Degrees()) << "</CenterDE>" << Qt::endl;
2845         outstream << "<GridW>" << tiles->gridSize().width() << "</GridW>" << Qt::endl;
2846         outstream << "<GridH>" << tiles->gridSize().height() << "</GridH>" << Qt::endl;
2847         outstream << "<FOVW>" << cLocale.toString(tiles->mosaicFOV().width()) << "</FOVW>" << Qt::endl;
2848         outstream << "<FOVH>" << cLocale.toString(tiles->mosaicFOV().height()) << "</FOVH>" << Qt::endl;
2849         outstream << "<CameraFOVW>" << cLocale.toString(tiles->cameraFOV().width()) << "</CameraFOVW>" << Qt::endl;
2850         outstream << "<CameraFOVH>" << cLocale.toString(tiles->cameraFOV().height()) << "</CameraFOVH>" << Qt::endl;
2851         outstream << "</Mosaic>" << Qt::endl;
2852     }
2853 
2854     int index = 0;
2855     for (auto &job : moduleState()->jobs())
2856     {
2857         outstream << "<Job>" << Qt::endl;
2858 
2859         // ensure to escape special XML characters
2860         outstream << "<Name>" << QString(entityXML(strdup(job->getName().toStdString().c_str()))) << "</Name>" << Qt::endl;
2861         outstream << "<Group>" << QString(entityXML(strdup(job->getGroup().toStdString().c_str()))) << "</Group>" << Qt::endl;
2862         outstream << "<Coordinates>" << Qt::endl;
2863         outstream << "<J2000RA>" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "</J2000RA>" << Qt::endl;
2864         outstream << "<J2000DE>" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "</J2000DE>" << Qt::endl;
2865         outstream << "</Coordinates>" << Qt::endl;
2866 
2867         if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false)
2868             outstream << "<FITS>" << job->getFITSFile().toLocalFile() << "</FITS>" << Qt::endl;
2869         else
2870             outstream << "<PositionAngle>" << job->getPositionAngle() << "</PositionAngle>" << Qt::endl;
2871 
2872         outstream << "<Sequence>" << job->getSequenceFile().toLocalFile() << "</Sequence>" << Qt::endl;
2873 
2874         if (useMosaicInfo && index < tiles->tiles().size())
2875         {
2876             auto oneTile = tiles->tiles().at(index++);
2877             outstream << "<TileCenter>" << Qt::endl;
2878             outstream << "<X>" << cLocale.toString(oneTile->center.x()) << "</X>" << Qt::endl;
2879             outstream << "<Y>" << cLocale.toString(oneTile->center.y()) << "</Y>" << Qt::endl;
2880             outstream << "<Rotation>" << cLocale.toString(oneTile->rotation) << "</Rotation>" << Qt::endl;
2881             outstream << "</TileCenter>" << Qt::endl;
2882         }
2883 
2884         outstream << "<StartupCondition>" << Qt::endl;
2885         if (job->getFileStartupCondition() == START_ASAP)
2886             outstream << "<Condition>ASAP</Condition>" << Qt::endl;
2887         else if (job->getFileStartupCondition() == START_AT)
2888             outstream << "<Condition value='" << job->getFileStartupTime().toString(Qt::ISODate) << "'>At</Condition>"
2889                       << Qt::endl;
2890         outstream << "</StartupCondition>" << Qt::endl;
2891 
2892         outstream << "<Constraints>" << Qt::endl;
2893         if (job->hasMinAltitude())
2894             outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" <<
2895                       Qt::endl;
2896         if (job->getMinMoonSeparation() > 0)
2897             outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>"
2898                       << Qt::endl;
2899         if (job->getEnforceWeather())
2900             outstream << "<Constraint>EnforceWeather</Constraint>" << Qt::endl;
2901         if (job->getEnforceTwilight())
2902             outstream << "<Constraint>EnforceTwilight</Constraint>" << Qt::endl;
2903         if (job->getEnforceArtificialHorizon())
2904             outstream << "<Constraint>EnforceArtificialHorizon</Constraint>" << Qt::endl;
2905         outstream << "</Constraints>" << Qt::endl;
2906 
2907         outstream << "<CompletionCondition>" << Qt::endl;
2908         if (job->getCompletionCondition() == FINISH_SEQUENCE)
2909             outstream << "<Condition>Sequence</Condition>" << Qt::endl;
2910         else if (job->getCompletionCondition() == FINISH_REPEAT)
2911             outstream << "<Condition value='" << cLocale.toString(job->getRepeatsRequired()) << "'>Repeat</Condition>" << Qt::endl;
2912         else if (job->getCompletionCondition() == FINISH_LOOP)
2913             outstream << "<Condition>Loop</Condition>" << Qt::endl;
2914         else if (job->getCompletionCondition() == FINISH_AT)
2915             outstream << "<Condition value='" << job->getCompletionTime().toString(Qt::ISODate) << "'>At</Condition>"
2916                       << Qt::endl;
2917         outstream << "</CompletionCondition>" << Qt::endl;
2918 
2919         outstream << "<Steps>" << Qt::endl;
2920         if (job->getStepPipeline() & SchedulerJob::USE_TRACK)
2921             outstream << "<Step>Track</Step>" << Qt::endl;
2922         if (job->getStepPipeline() & SchedulerJob::USE_FOCUS)
2923             outstream << "<Step>Focus</Step>" << Qt::endl;
2924         if (job->getStepPipeline() & SchedulerJob::USE_ALIGN)
2925             outstream << "<Step>Align</Step>" << Qt::endl;
2926         if (job->getStepPipeline() & SchedulerJob::USE_GUIDE)
2927             outstream << "<Step>Guide</Step>" << Qt::endl;
2928         outstream << "</Steps>" << Qt::endl;
2929 
2930         outstream << "</Job>" << Qt::endl;
2931     }
2932 
2933     outstream << "<SchedulerAlgorithm value='" << ALGORITHM_GREEDY << "'/>" << Qt::endl;
2934     outstream << "<ErrorHandlingStrategy value='" << Options::errorHandlingStrategy() << "'>" << Qt::endl;
2935     if (Options::rescheduleErrors())
2936         outstream << "<RescheduleErrors />" << Qt::endl;
2937     outstream << "<delay>" << Options::errorHandlingStrategyDelay() << "</delay>" << Qt::endl;
2938     outstream << "</ErrorHandlingStrategy>" << Qt::endl;
2939 
2940     outstream << "<StartupProcedure>" << Qt::endl;
2941     if (moduleState()->startupScriptURL().isEmpty() == false)
2942         outstream << "<Procedure value='" << moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile) <<
2943                   "'>StartupScript</Procedure>" << Qt::endl;
2944     if (Options::schedulerUnparkDome())
2945         outstream << "<Procedure>UnparkDome</Procedure>" << Qt::endl;
2946     if (Options::schedulerUnparkMount())
2947         outstream << "<Procedure>UnparkMount</Procedure>" << Qt::endl;
2948     if (Options::schedulerOpenDustCover())
2949         outstream << "<Procedure>UnparkCap</Procedure>" << Qt::endl;
2950     outstream << "</StartupProcedure>" << Qt::endl;
2951 
2952     outstream << "<ShutdownProcedure>" << Qt::endl;
2953     if (Options::schedulerWarmCCD())
2954         outstream << "<Procedure>WarmCCD</Procedure>" << Qt::endl;
2955     if (Options::schedulerCloseDustCover())
2956         outstream << "<Procedure>ParkCap</Procedure>" << Qt::endl;
2957     if (Options::schedulerParkMount())
2958         outstream << "<Procedure>ParkMount</Procedure>" << Qt::endl;
2959     if (Options::schedulerParkDome())
2960         outstream << "<Procedure>ParkDome</Procedure>" << Qt::endl;
2961     if (moduleState()->shutdownScriptURL().isEmpty() == false)
2962         outstream << "<Procedure value='" << moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile) <<
2963                   "'>schedulerStartupScript</Procedure>" <<
2964                   Qt::endl;
2965     outstream << "</ShutdownProcedure>" << Qt::endl;
2966 
2967     outstream << "</SchedulerList>" << Qt::endl;
2968 
2969     appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile()));
2970     file.close();
2971     moduleState()->setDirty(false);
2972     return true;
2973 }
2974 
2975 bool SchedulerProcess::appendEkosScheduleList(const QString &fileURL)
2976 {
2977     SchedulerState const old_state = moduleState()->schedulerState();
2978     moduleState()->setSchedulerState(SCHEDULER_LOADING);
2979 
2980     QFile sFile;
2981     sFile.setFileName(fileURL);
2982 
2983     if (!sFile.open(QIODevice::ReadOnly))
2984     {
2985         QString message = i18n("Unable to open file %1", fileURL);
2986         KSNotification::sorry(message, i18n("Could Not Open File"));
2987         moduleState()->setSchedulerState(old_state);
2988         return false;
2989     }
2990 
2991     LilXML *xmlParser = newLilXML();
2992     char errmsg[MAXRBUF];
2993     XMLEle *root = nullptr;
2994     XMLEle *ep   = nullptr;
2995     XMLEle *subEP = nullptr;
2996     char c;
2997 
2998     // We expect all data read from the XML to be in the C locale - QLocale::c()
2999     QLocale cLocale = QLocale::c();
3000 
3001     while (sFile.getChar(&c))
3002     {
3003         root = readXMLEle(xmlParser, c, errmsg);
3004 
3005         if (root)
3006         {
3007             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
3008             {
3009                 const char *tag = tagXMLEle(ep);
3010                 if (!strcmp(tag, "Job"))
3011                 {
3012                     emit addJob(SchedulerUtils::createJob(ep));
3013                 }
3014                 else if (!strcmp(tag, "Mosaic"))
3015                 {
3016                     // If we have mosaic info, load it up.
3017                     auto tiles = KStarsData::Instance()->skyComposite()->mosaicComponent()->tiles();
3018                     tiles->fromXML(fileURL);
3019                 }
3020                 else if (!strcmp(tag, "Profile"))
3021                 {
3022                     moduleState()->setCurrentProfile(pcdataXMLEle(ep));
3023                 }
3024                 // disabled, there is only one algorithm
3025                 else if (!strcmp(tag, "SchedulerAlgorithm"))
3026                 {
3027                     int algIndex = cLocale.toInt(findXMLAttValu(ep, "value"));
3028                     if (algIndex != ALGORITHM_GREEDY)
3029                         appendLogText(i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
3030                 }
3031                 else if (!strcmp(tag, "ErrorHandlingStrategy"))
3032                 {
3033                     Options::setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(cLocale.toInt(findXMLAttValu(ep,
3034                                                       "value"))));
3035 
3036                     subEP = findXMLEle(ep, "delay");
3037                     if (subEP)
3038                     {
3039                         Options::setErrorHandlingStrategyDelay(cLocale.toInt(pcdataXMLEle(subEP)));
3040                     }
3041                     subEP = findXMLEle(ep, "RescheduleErrors");
3042                     Options::setRescheduleErrors(subEP != nullptr);
3043                 }
3044                 else if (!strcmp(tag, "StartupProcedure"))
3045                 {
3046                     XMLEle *procedure;
3047                     Options::setSchedulerUnparkDome(false);
3048                     Options::setSchedulerUnparkMount(false);
3049                     Options::setSchedulerOpenDustCover(false);
3050 
3051                     for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3052                     {
3053                         const char *proc = pcdataXMLEle(procedure);
3054 
3055                         if (!strcmp(proc, "StartupScript"))
3056                         {
3057                             moduleState()->setStartupScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3058                         }
3059                         else if (!strcmp(proc, "UnparkDome"))
3060                             Options::setSchedulerUnparkDome(true);
3061                         else if (!strcmp(proc, "UnparkMount"))
3062                             Options::setSchedulerUnparkMount(true);
3063                         else if (!strcmp(proc, "UnparkCap"))
3064                             Options::setSchedulerOpenDustCover(true);
3065                     }
3066                 }
3067                 else if (!strcmp(tag, "ShutdownProcedure"))
3068                 {
3069                     XMLEle *procedure;
3070                     Options::setSchedulerWarmCCD(false);
3071                     Options::setSchedulerParkDome(false);
3072                     Options::setSchedulerParkMount(false);
3073                     Options::setSchedulerCloseDustCover(false);
3074 
3075                     for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
3076                     {
3077                         const char *proc = pcdataXMLEle(procedure);
3078 
3079                         if (!strcmp(proc, "ShutdownScript"))
3080                         {
3081                             moduleState()->setShutdownScriptURL(QUrl::fromUserInput(findXMLAttValu(procedure, "value")));
3082                         }
3083                         else if (!strcmp(proc, "WarmCCD"))
3084                             Options::setSchedulerWarmCCD(true);
3085                         else if (!strcmp(proc, "ParkDome"))
3086                             Options::setSchedulerParkDome(true);
3087                         else if (!strcmp(proc, "ParkMount"))
3088                             Options::setSchedulerParkMount(true);
3089                         else if (!strcmp(proc, "ParkCap"))
3090                             Options::setSchedulerCloseDustCover(true);
3091                     }
3092                 }
3093             }
3094             delXMLEle(root);
3095             emit syncGUIToGeneralSettings();
3096         }
3097         else if (errmsg[0])
3098         {
3099             appendLogText(QString(errmsg));
3100             delLilXML(xmlParser);
3101             moduleState()->setSchedulerState(old_state);
3102             return false;
3103         }
3104     }
3105 
3106     moduleState()->setDirty(false);
3107     delLilXML(xmlParser);
3108     emit updateSchedulerURL(fileURL);
3109 
3110     moduleState()->setSchedulerState(old_state);
3111     return true;
3112 }
3113 
3114 void SchedulerProcess::setAlignStatus(AlignState status)
3115 {
3116     if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3117         return;
3118 
3119     qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status);
3120 
3121     /* If current job is scheduled and has not started yet, wait */
3122     if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3123     {
3124         QDateTime const now = SchedulerModuleState::getLocalTime();
3125         if (now < activeJob()->getStartupTime())
3126             return;
3127     }
3128 
3129     if (activeJob()->getStage() == SCHEDSTAGE_ALIGNING)
3130     {
3131         // Is solver complete?
3132         if (status == Ekos::ALIGN_COMPLETE)
3133         {
3134             appendLogText(i18n("Job '%1' alignment is complete.", activeJob()->getName()));
3135             moduleState()->resetAlignFailureCount();
3136 
3137             moduleState()->updateJobStage(SCHEDSTAGE_ALIGN_COMPLETE);
3138 
3139             // If we solved a FITS file, let's use its center coords as our target.
3140             if (activeJob()->getFITSFile().isEmpty() == false)
3141             {
3142                 QDBusReply<QList<double>> solutionReply = alignInterface()->call("getTargetCoords");
3143                 if (solutionReply.isValid())
3144                 {
3145                     QList<double> const values = solutionReply.value();
3146                     activeJob()->setTargetCoords(dms(values[0] * 15.0), dms(values[1]), KStarsData::Instance()->ut().djd());
3147                 }
3148             }
3149             getNextAction();
3150         }
3151         else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED)
3152         {
3153             appendLogText(i18n("Warning: job '%1' alignment failed.", activeJob()->getName()));
3154 
3155             if (moduleState()->increaseAlignFailureCount())
3156             {
3157                 if (Options::resetMountModelOnAlignFail() && moduleState()->maxFailureAttempts() - 1 < moduleState()->alignFailureCount())
3158                 {
3159                     appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", activeJob()->getName(),
3160                                        moduleState()->alignFailureCount()));
3161                     mountInterface()->call(QDBus::AutoDetect, "resetModel");
3162                 }
3163                 appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3164                 startAstrometry();
3165             }
3166             else
3167             {
3168                 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", activeJob()->getName()));
3169                 activeJob()->setState(SCHEDJOB_ABORTED);
3170 
3171                 findNextJob();
3172             }
3173         }
3174     }
3175 }
3176 
3177 void SchedulerProcess::setGuideStatus(GuideState status)
3178 {
3179     if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3180         return;
3181 
3182     qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status);
3183 
3184     /* If current job is scheduled and has not started yet, wait */
3185     if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3186     {
3187         QDateTime const now = SchedulerModuleState::getLocalTime();
3188         if (now < activeJob()->getStartupTime())
3189             return;
3190     }
3191 
3192     if (activeJob()->getStage() == SCHEDSTAGE_GUIDING)
3193     {
3194         qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage...";
3195 
3196         // If calibration stage complete?
3197         if (status == Ekos::GUIDE_GUIDING)
3198         {
3199             appendLogText(i18n("Job '%1' guiding is in progress.", activeJob()->getName()));
3200             moduleState()->resetGuideFailureCount();
3201             // if guiding recovered while we are waiting, abort the restart
3202             moduleState()->cancelGuidingTimer();
3203 
3204             moduleState()->updateJobStage(SCHEDSTAGE_GUIDING_COMPLETE);
3205             getNextAction();
3206         }
3207         else if (status == Ekos::GUIDE_CALIBRATION_ERROR ||
3208                  status == Ekos::GUIDE_ABORTED)
3209         {
3210             if (status == Ekos::GUIDE_ABORTED)
3211                 appendLogText(i18n("Warning: job '%1' guiding failed.", activeJob()->getName()));
3212             else
3213                 appendLogText(i18n("Warning: job '%1' calibration failed.", activeJob()->getName()));
3214 
3215             // if the timer for restarting the guiding is already running, we do nothing and
3216             // wait for the action triggered by the timer. This way we avoid that a small guiding problem
3217             // abort the scheduler job
3218 
3219             if (moduleState()->isGuidingTimerActive())
3220                 return;
3221 
3222             if (moduleState()->increaseGuideFailureCount())
3223             {
3224                 if (status == Ekos::GUIDE_CALIBRATION_ERROR &&
3225                         Options::realignAfterCalibrationFailure())
3226                 {
3227                     appendLogText(i18n("Restarting %1 alignment procedure...", activeJob()->getName()));
3228                     startAstrometry();
3229                 }
3230                 else
3231                 {
3232                     appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", activeJob()->getName(),
3233                                        (RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount()) / 1000));
3234                     moduleState()->startGuidingTimer(RESTART_GUIDING_DELAY_MS * moduleState()->guideFailureCount());
3235                 }
3236             }
3237             else
3238             {
3239                 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", activeJob()->getName()));
3240                 activeJob()->setState(SCHEDJOB_ABORTED);
3241 
3242                 findNextJob();
3243             }
3244         }
3245     }
3246 }
3247 
3248 void SchedulerProcess::setFocusStatus(FocusState status)
3249 {
3250     if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3251         return;
3252 
3253     qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status);
3254 
3255     /* If current job is scheduled and has not started yet, wait */
3256     if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3257     {
3258         QDateTime const now = SchedulerModuleState::getLocalTime();
3259         if (now < activeJob()->getStartupTime())
3260             return;
3261     }
3262 
3263     if (activeJob()->getStage() == SCHEDSTAGE_FOCUSING)
3264     {
3265         // Is focus complete?
3266         if (status == Ekos::FOCUS_COMPLETE)
3267         {
3268             appendLogText(i18n("Job '%1' focusing is complete.", activeJob()->getName()));
3269 
3270             moduleState()->setAutofocusCompleted(true);
3271 
3272             moduleState()->updateJobStage(SCHEDSTAGE_FOCUS_COMPLETE);
3273 
3274             getNextAction();
3275         }
3276         else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED)
3277         {
3278             appendLogText(i18n("Warning: job '%1' focusing failed.", activeJob()->getName()));
3279 
3280             if (moduleState()->increaseFocusFailureCount())
3281             {
3282                 appendLogText(i18n("Job '%1' is restarting its focusing procedure.", activeJob()->getName()));
3283                 // Reset frame to original size.
3284                 focusInterface()->call(QDBus::AutoDetect, "resetFrame");
3285                 // Restart focusing
3286                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 6883";
3287                 startFocusing();
3288             }
3289             else
3290             {
3291                 appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", activeJob()->getName()));
3292                 activeJob()->setState(SCHEDJOB_ABORTED);
3293 
3294                 findNextJob();
3295             }
3296         }
3297     }
3298 }
3299 
3300 void SchedulerProcess::setMountStatus(ISD::Mount::Status status)
3301 {
3302     if (moduleState()->schedulerState() == SCHEDULER_PAUSED || activeJob() == nullptr)
3303         return;
3304 
3305     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status;
3306 
3307     /* If current job is scheduled and has not started yet, wait */
3308     if (SCHEDJOB_SCHEDULED == activeJob()->getState())
3309         if (static_cast<QDateTime const>(SchedulerModuleState::getLocalTime()) < activeJob()->getStartupTime())
3310             return;
3311 
3312     switch (activeJob()->getStage())
3313     {
3314         case SCHEDSTAGE_SLEWING:
3315         {
3316             qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage...";
3317 
3318             if (status == ISD::Mount::MOUNT_TRACKING)
3319             {
3320                 appendLogText(i18n("Job '%1' slew is complete.", activeJob()->getName()));
3321                 moduleState()->updateJobStage(SCHEDSTAGE_SLEW_COMPLETE);
3322                 /* getNextAction is deferred to checkJobStage for dome support */
3323             }
3324             else if (status == ISD::Mount::MOUNT_ERROR)
3325             {
3326                 appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", activeJob()->getName()));
3327                 activeJob()->setState(SCHEDJOB_ERROR);
3328                 findNextJob();
3329             }
3330             else if (status == ISD::Mount::MOUNT_IDLE)
3331             {
3332                 appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", activeJob()->getName()));
3333                 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
3334                 getNextAction();
3335             }
3336         }
3337         break;
3338 
3339         case SCHEDSTAGE_RESLEWING:
3340         {
3341             qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage...";
3342 
3343             if (status == ISD::Mount::MOUNT_TRACKING)
3344             {
3345                 appendLogText(i18n("Job '%1' repositioning is complete.", activeJob()->getName()));
3346                 moduleState()->updateJobStage(SCHEDSTAGE_RESLEWING_COMPLETE);
3347                 /* getNextAction is deferred to checkJobStage for dome support */
3348             }
3349             else if (status == ISD::Mount::MOUNT_ERROR)
3350             {
3351                 appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", activeJob()->getName()));
3352                 activeJob()->setState(SCHEDJOB_ERROR);
3353                 findNextJob();
3354             }
3355             else if (status == ISD::Mount::MOUNT_IDLE)
3356             {
3357                 appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", activeJob()->getName()));
3358                 moduleState()->updateJobStage(SCHEDSTAGE_IDLE);
3359                 getNextAction();
3360             }
3361         }
3362         break;
3363 
3364         default:
3365             break;
3366     }
3367 }
3368 
3369 void SchedulerProcess::checkStartupProcedure()
3370 {
3371     if (checkStartupState() == false)
3372         QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
3373 }
3374 
3375 void SchedulerProcess::checkShutdownProcedure()
3376 {
3377     if (checkShutdownState())
3378     {
3379         // shutdown completed
3380         if (moduleState()->shutdownState() == SHUTDOWN_COMPLETE)
3381         {
3382             appendLogText(i18n("Manual shutdown procedure completed successfully."));
3383             // Stop Ekos
3384             if (Options::stopEkosAfterShutdown())
3385                 stopEkos();
3386         }
3387         else if (moduleState()->shutdownState() == SHUTDOWN_ERROR)
3388             appendLogText(i18n("Manual shutdown procedure terminated due to errors."));
3389 
3390         moduleState()->setShutdownState(SHUTDOWN_IDLE);
3391     }
3392     else
3393         // If shutdown procedure is not finished yet, let's check again in 1 second.
3394         QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
3395 
3396 }
3397 
3398 
3399 void SchedulerProcess::parkCap()
3400 {
3401     if (capInterface().isNull())
3402     {
3403         appendLogText(i18n("Dust cover park requested but no dust covers detected."));
3404         moduleState()->setShutdownState(SHUTDOWN_ERROR);
3405         return;
3406     }
3407 
3408     QVariant parkingStatus = capInterface()->property("parkStatus");
3409     qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3410 
3411     if (parkingStatus.isValid() == false)
3412     {
3413         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
3414                                               mountInterface()->lastError().type());
3415         if (!manageConnectionLoss())
3416             parkingStatus = ISD::PARK_ERROR;
3417     }
3418 
3419     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3420 
3421     if (status != ISD::PARK_PARKED)
3422     {
3423         moduleState()->setShutdownState(SHUTDOWN_PARKING_CAP);
3424         qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking dust cap...";
3425         capInterface()->call(QDBus::AutoDetect, "park");
3426         appendLogText(i18n("Parking Cap..."));
3427 
3428         moduleState()->startCurrentOperationTimer();
3429     }
3430     else
3431     {
3432         appendLogText(i18n("Cap already parked."));
3433         moduleState()->setShutdownState(SHUTDOWN_PARK_MOUNT);
3434     }
3435 }
3436 
3437 void SchedulerProcess::unParkCap()
3438 {
3439     if (capInterface().isNull())
3440     {
3441         appendLogText(i18n("Dust cover unpark requested but no dust covers detected."));
3442         moduleState()->setStartupState(STARTUP_ERROR);
3443         return;
3444     }
3445 
3446     QVariant parkingStatus = capInterface()->property("parkStatus");
3447     qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3448 
3449     if (parkingStatus.isValid() == false)
3450     {
3451         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
3452                                               mountInterface()->lastError().type());
3453         if (!manageConnectionLoss())
3454             parkingStatus = ISD::PARK_ERROR;
3455     }
3456 
3457     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3458 
3459     if (status != ISD::PARK_UNPARKED)
3460     {
3461         moduleState()->setStartupState(STARTUP_UNPARKING_CAP);
3462         capInterface()->call(QDBus::AutoDetect, "unpark");
3463         appendLogText(i18n("Unparking cap..."));
3464 
3465         moduleState()->startCurrentOperationTimer();
3466     }
3467     else
3468     {
3469         appendLogText(i18n("Cap already unparked."));
3470         moduleState()->setStartupState(STARTUP_COMPLETE);
3471     }
3472 }
3473 
3474 void SchedulerProcess::parkMount()
3475 {
3476     if (mountInterface().isNull())
3477     {
3478         appendLogText(i18n("Mount park requested but no mounts detected."));
3479         moduleState()->setShutdownState(SHUTDOWN_ERROR);
3480         return;
3481     }
3482 
3483     QVariant parkingStatus = mountInterface()->property("parkStatus");
3484     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3485 
3486     if (parkingStatus.isValid() == false)
3487     {
3488         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
3489                                               mountInterface()->lastError().type());
3490         if (!manageConnectionLoss())
3491             moduleState()->setParkWaitState(PARKWAIT_ERROR);
3492     }
3493 
3494     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3495 
3496     switch (status)
3497     {
3498         case ISD::PARK_PARKED:
3499             if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
3500                 moduleState()->setShutdownState(SHUTDOWN_PARK_DOME);
3501 
3502             moduleState()->setParkWaitState(PARKWAIT_PARKED);
3503             appendLogText(i18n("Mount already parked."));
3504             break;
3505 
3506         case ISD::PARK_UNPARKING:
3507         //case Mount::UNPARKING_BUSY:
3508         /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */
3509 
3510         //        case Mount::PARKING_IDLE:
3511         //        case Mount::UNPARKING_OK:
3512         case ISD::PARK_ERROR:
3513         case ISD::PARK_UNKNOWN:
3514         case ISD::PARK_UNPARKED:
3515         {
3516             qCDebug(KSTARS_EKOS_SCHEDULER) << "Parking mount...";
3517             QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "park");
3518 
3519             if (mountReply.error().type() != QDBusError::NoError)
3520             {
3521                 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(
3522                                                       QDBusError::errorString(mountReply.error().type()));
3523                 if (!manageConnectionLoss())
3524                     moduleState()->setParkWaitState(PARKWAIT_ERROR);
3525             }
3526             else moduleState()->startCurrentOperationTimer();
3527         }
3528 
3529         // Fall through
3530         case ISD::PARK_PARKING:
3531             //case Mount::PARKING_BUSY:
3532             if (moduleState()->shutdownState() == SHUTDOWN_PARK_MOUNT)
3533                 moduleState()->setShutdownState(SHUTDOWN_PARKING_MOUNT);
3534 
3535             moduleState()->setParkWaitState(PARKWAIT_PARKING);
3536             appendLogText(i18n("Parking mount in progress..."));
3537             break;
3538 
3539             // All cases covered above so no need for default
3540             //default:
3541             //    qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value());
3542     }
3543 
3544 }
3545 
3546 void SchedulerProcess::unParkMount()
3547 {
3548     if (mountInterface().isNull())
3549     {
3550         appendLogText(i18n("Mount unpark requested but no mounts detected."));
3551         moduleState()->setStartupState(STARTUP_ERROR);
3552         return;
3553     }
3554 
3555     QVariant parkingStatus = mountInterface()->property("parkStatus");
3556     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3557 
3558     if (parkingStatus.isValid() == false)
3559     {
3560         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
3561                                               mountInterface()->lastError().type());
3562         if (!manageConnectionLoss())
3563             moduleState()->setParkWaitState(PARKWAIT_ERROR);
3564     }
3565 
3566     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3567 
3568     switch (status)
3569     {
3570         //case Mount::UNPARKING_OK:
3571         case ISD::PARK_UNPARKED:
3572             if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
3573                 moduleState()->setStartupState(STARTUP_UNPARK_CAP);
3574 
3575             moduleState()->setParkWaitState(PARKWAIT_UNPARKED);
3576             appendLogText(i18n("Mount already unparked."));
3577             break;
3578 
3579         //case Mount::PARKING_BUSY:
3580         case ISD::PARK_PARKING:
3581         /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */
3582 
3583         //        case Mount::PARKING_IDLE:
3584         //        case Mount::PARKING_OK:
3585         //        case Mount::PARKING_ERROR:
3586         case ISD::PARK_ERROR:
3587         case ISD::PARK_UNKNOWN:
3588         case ISD::PARK_PARKED:
3589         {
3590             QDBusReply<bool> const mountReply = mountInterface()->call(QDBus::AutoDetect, "unpark");
3591 
3592             if (mountReply.error().type() != QDBusError::NoError)
3593             {
3594                 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(
3595                                                       QDBusError::errorString(mountReply.error().type()));
3596                 if (!manageConnectionLoss())
3597                     moduleState()->setParkWaitState(PARKWAIT_ERROR);
3598             }
3599             else moduleState()->startCurrentOperationTimer();
3600         }
3601 
3602         // Fall through
3603         //case Mount::UNPARKING_BUSY:
3604         case ISD::PARK_UNPARKING:
3605             if (moduleState()->startupState() == STARTUP_UNPARK_MOUNT)
3606                 moduleState()->setStartupState(STARTUP_UNPARKING_MOUNT);
3607 
3608             moduleState()->setParkWaitState(PARKWAIT_UNPARKING);
3609             qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
3610             break;
3611 
3612             // All cases covered above
3613             //default:
3614             //    qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value());
3615     }
3616 }
3617 
3618 bool SchedulerProcess::isMountParked()
3619 {
3620     if (mountInterface().isNull())
3621         return false;
3622     // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear
3623     //QDBusReply<bool> const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark");
3624     QVariant canPark = mountInterface()->property("canPark");
3625     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount can park:" << (!canPark.isValid() ? "invalid" : (canPark.toBool() ? "T" : "F"));
3626 
3627     if (canPark.isValid() == false)
3628     {
3629         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(
3630                                               mountInterface()->lastError().type());
3631         manageConnectionLoss();
3632         return false;
3633     }
3634     else if (canPark.toBool() == true)
3635     {
3636         // If it is able to park, obtain its current status
3637         //QDBusReply<int> const mountReply  = mountInterface->call(QDBus::AutoDetect, "getParkingStatus");
3638         QVariant parkingStatus = mountInterface()->property("parkStatus");
3639         qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3640 
3641         if (parkingStatus.isValid() == false)
3642         {
3643             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(
3644                                                   mountInterface()->lastError().type());
3645             manageConnectionLoss();
3646             return false;
3647         }
3648 
3649         // Deduce state of mount - see getParkingStatus in mount.cpp
3650         switch (static_cast<ISD::ParkStatus>(parkingStatus.toInt()))
3651         {
3652             //            case Mount::PARKING_OK:     // INDI switch ok, and parked
3653             //            case Mount::PARKING_IDLE:   // INDI switch idle, and parked
3654             case ISD::PARK_PARKED:
3655                 return true;
3656 
3657             //            case Mount::UNPARKING_OK:   // INDI switch idle or ok, and unparked
3658             //            case Mount::PARKING_ERROR:  // INDI switch error
3659             //            case Mount::PARKING_BUSY:   // INDI switch busy
3660             //            case Mount::UNPARKING_BUSY: // INDI switch busy
3661             default:
3662                 return false;
3663         }
3664     }
3665     // If the mount is not able to park, consider it not parked
3666     return false;
3667 }
3668 
3669 void SchedulerProcess::parkDome()
3670 {
3671     // If there is no dome, mark error
3672     if (domeInterface().isNull())
3673     {
3674         appendLogText(i18n("Dome park requested but no domes detected."));
3675         moduleState()->setShutdownState(SHUTDOWN_ERROR);
3676         return;
3677     }
3678 
3679     //QDBusReply<int> const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus");
3680     //Dome::ParkingStatus status = static_cast<Dome::ParkingStatus>(domeReply.value());
3681     QVariant parkingStatus = domeInterface()->property("parkStatus");
3682     qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3683 
3684     if (parkingStatus.isValid() == false)
3685     {
3686         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
3687                                               mountInterface()->lastError().type());
3688         if (!manageConnectionLoss())
3689             parkingStatus = ISD::PARK_ERROR;
3690     }
3691 
3692     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3693     if (status != ISD::PARK_PARKED)
3694     {
3695         moduleState()->setShutdownState(SHUTDOWN_PARKING_DOME);
3696         domeInterface()->call(QDBus::AutoDetect, "park");
3697         appendLogText(i18n("Parking dome..."));
3698 
3699         moduleState()->startCurrentOperationTimer();
3700     }
3701     else
3702     {
3703         appendLogText(i18n("Dome already parked."));
3704         moduleState()->setShutdownState(SHUTDOWN_SCRIPT);
3705     }
3706 }
3707 
3708 void SchedulerProcess::unParkDome()
3709 {
3710     // If there is no dome, mark error
3711     if (domeInterface().isNull())
3712     {
3713         appendLogText(i18n("Dome unpark requested but no domes detected."));
3714         moduleState()->setStartupState(STARTUP_ERROR);
3715         return;
3716     }
3717 
3718     QVariant parkingStatus = domeInterface()->property("parkStatus");
3719     qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3720 
3721     if (parkingStatus.isValid() == false)
3722     {
3723         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
3724                                               mountInterface()->lastError().type());
3725         if (!manageConnectionLoss())
3726             parkingStatus = ISD::PARK_ERROR;
3727     }
3728 
3729     if (static_cast<ISD::ParkStatus>(parkingStatus.toInt()) != ISD::PARK_UNPARKED)
3730     {
3731         moduleState()->setStartupState(STARTUP_UNPARKING_DOME);
3732         domeInterface()->call(QDBus::AutoDetect, "unpark");
3733         appendLogText(i18n("Unparking dome..."));
3734 
3735         moduleState()->startCurrentOperationTimer();
3736     }
3737     else
3738     {
3739         appendLogText(i18n("Dome already unparked."));
3740         moduleState()->setStartupState(STARTUP_UNPARK_MOUNT);
3741     }
3742 }
3743 
3744 GuideState SchedulerProcess::getGuidingStatus()
3745 {
3746     QVariant guideStatus = guideInterface()->property("status");
3747     Ekos::GuideState gStatus = static_cast<Ekos::GuideState>(guideStatus.toInt());
3748 
3749     return gStatus;
3750 }
3751 
3752 bool SchedulerProcess::isDomeParked()
3753 {
3754     if (domeInterface().isNull())
3755         return false;
3756 
3757     QVariant parkingStatus = domeInterface()->property("parkStatus");
3758     qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome parking status" << (!parkingStatus.isValid() ? -1 : parkingStatus.toInt());
3759 
3760     if (parkingStatus.isValid() == false)
3761     {
3762         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
3763                                               mountInterface()->lastError().type());
3764         if (!manageConnectionLoss())
3765             parkingStatus = ISD::PARK_ERROR;
3766     }
3767 
3768     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
3769 
3770     return status == ISD::PARK_PARKED;
3771 }
3772 
3773 bool SchedulerProcess::createJobSequence(XMLEle * root, const QString &prefix, const QString &outputDir)
3774 {
3775     XMLEle *ep    = nullptr;
3776     XMLEle *subEP = nullptr;
3777 
3778     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
3779     {
3780         if (!strcmp(tagXMLEle(ep), "Job"))
3781         {
3782             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
3783             {
3784                 if (!strcmp(tagXMLEle(subEP), "Prefix"))
3785                 {
3786                     XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix");
3787                     if (rawPrefix)
3788                     {
3789                         editXMLEle(rawPrefix, prefix.toLatin1().constData());
3790                     }
3791                 }
3792                 else if (!strcmp(tagXMLEle(subEP), "FITSDirectory"))
3793                 {
3794                     editXMLEle(subEP, outputDir.toLatin1().constData());
3795                 }
3796             }
3797         }
3798     }
3799 
3800     QDir().mkpath(outputDir);
3801 
3802     QString filename = QString("%1/%2.esq").arg(outputDir, prefix);
3803     FILE *outputFile = fopen(filename.toLatin1().constData(), "w");
3804 
3805     if (outputFile == nullptr)
3806     {
3807         QString message = i18n("Unable to write to file %1", filename);
3808         KSNotification::sorry(message, i18n("Could Not Open File"));
3809         return false;
3810     }
3811 
3812     fprintf(outputFile, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
3813     prXMLEle(outputFile, root, 0);
3814 
3815     fclose(outputFile);
3816 
3817     return true;
3818 }
3819 
3820 XMLEle *SchedulerProcess::getSequenceJobRoot(const QString &filename) const
3821 {
3822     QFile sFile;
3823     sFile.setFileName(filename);
3824 
3825     if (!sFile.open(QIODevice::ReadOnly))
3826     {
3827         KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()),
3828                               i18n("Could Not Open File"));
3829         return nullptr;
3830     }
3831 
3832     LilXML *xmlParser = newLilXML();
3833     char errmsg[MAXRBUF];
3834     XMLEle *root = nullptr;
3835     char c;
3836 
3837     while (sFile.getChar(&c))
3838     {
3839         root = readXMLEle(xmlParser, c, errmsg);
3840 
3841         if (root)
3842             break;
3843     }
3844 
3845     delLilXML(xmlParser);
3846     sFile.close();
3847     return root;
3848 }
3849 
3850 void SchedulerProcess::checkProcessExit(int exitCode)
3851 {
3852     scriptProcess().disconnect();
3853 
3854     if (exitCode == 0)
3855     {
3856         if (moduleState()->startupState() == STARTUP_SCRIPT)
3857             moduleState()->setStartupState(STARTUP_UNPARK_DOME);
3858         else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
3859             moduleState()->setShutdownState(SHUTDOWN_COMPLETE);
3860 
3861         return;
3862     }
3863 
3864     if (moduleState()->startupState() == STARTUP_SCRIPT)
3865     {
3866         appendLogText(i18n("Startup script failed, aborting..."));
3867         moduleState()->setStartupState(STARTUP_ERROR);
3868     }
3869     else if (moduleState()->shutdownState() == SHUTDOWN_SCRIPT_RUNNING)
3870     {
3871         appendLogText(i18n("Shutdown script failed, aborting..."));
3872         moduleState()->setShutdownState(SHUTDOWN_ERROR);
3873     }
3874 
3875 }
3876 
3877 void SchedulerProcess::readProcessOutput()
3878 {
3879     appendLogText(scriptProcess().readAllStandardOutput().simplified());
3880 }
3881 
3882 bool SchedulerProcess::canCountCaptures(const SchedulerJob &job)
3883 {
3884     QList<SequenceJob*> seqjobs;
3885     bool hasAutoFocus = false;
3886     SchedulerJob tempJob = job;
3887     if (SchedulerUtils::loadSequenceQueue(tempJob.getSequenceFile().toLocalFile(), &tempJob, seqjobs, hasAutoFocus,
3888                                           nullptr) == false)
3889         return false;
3890 
3891     for (const SequenceJob *oneSeqJob : seqjobs)
3892     {
3893         if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
3894             return false;
3895     }
3896     return true;
3897 }
3898 
3899 void SchedulerProcess::updateCompletedJobsCount(bool forced)
3900 {
3901     /* Use a temporary map in order to limit the number of file searches */
3902     CapturedFramesMap newFramesCount;
3903 
3904     /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */
3905 
3906     /* Check if one job is idle or requires evaluation - if so, force refresh */
3907     forced |= std::any_of(moduleState()->jobs().begin(),
3908                           moduleState()->jobs().end(), [](SchedulerJob * oneJob) -> bool
3909     {
3910         SchedulerJobStatus const state = oneJob->getState();
3911         return state == SCHEDJOB_IDLE || state == SCHEDJOB_EVALUATION;});
3912 
3913     /* If update is forced, clear the frame map */
3914     if (forced)
3915         moduleState()->capturedFramesCount().clear();
3916 
3917     /* Enumerate SchedulerJobs to count captures that are already stored */
3918     for (SchedulerJob *oneJob : moduleState()->jobs())
3919     {
3920         QList<SequenceJob*> seqjobs;
3921         bool hasAutoFocus = false;
3922 
3923         //oneJob->setLightFramesRequired(false);
3924         /* Look into the sequence requirements, bypass if invalid */
3925         if (SchedulerUtils::loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus,
3926                                               this) == false)
3927         {
3928             appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(),
3929                                oneJob->getSequenceFile().toLocalFile()));
3930             oneJob->setState(SCHEDJOB_INVALID);
3931             continue;
3932         }
3933 
3934         /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */
3935         for (SequenceJob *oneSeqJob : seqjobs)
3936         {
3937             /* Only consider captures stored on client (Ekos) side */
3938             /* FIXME: ask the remote for the file count */
3939             if (oneSeqJob->getUploadMode() == ISD::Camera::UPLOAD_LOCAL)
3940                 continue;
3941 
3942             /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */
3943             QString const signature = oneSeqJob->getSignature();
3944 
3945             /* If signature was processed during this run, keep it */
3946             if (newFramesCount.constEnd() != newFramesCount.constFind(signature))
3947                 continue;
3948 
3949             /* If signature was processed during an earlier run, use the earlier count */
3950             QMap<QString, uint16_t>::const_iterator const earlierRunIterator = moduleState()->capturedFramesCount().constFind(
3951                         signature);
3952             if (moduleState()->capturedFramesCount().constEnd() != earlierRunIterator)
3953             {
3954                 newFramesCount[signature] = earlierRunIterator.value();
3955                 continue;
3956             }
3957 
3958             /* Else recount captures already stored */
3959             newFramesCount[signature] = PlaceholderPath::getCompletedFiles(signature);
3960         }
3961 
3962         // determine whether we need to continue capturing, depending on captured frames
3963         SchedulerUtils::updateLightFramesRequired(oneJob, seqjobs, newFramesCount);
3964     }
3965 
3966     moduleState()->setCapturedFramesCount(newFramesCount);
3967 
3968     {
3969         qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:";
3970         QMap<QString, uint16_t>::const_iterator it = moduleState()->capturedFramesCount().constBegin();
3971         for (; it != moduleState()->capturedFramesCount().constEnd(); it++)
3972             qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value();
3973     }
3974 }
3975 
3976 SchedulerJob *SchedulerProcess::activeJob()
3977 {
3978     return  moduleState()->activeJob();
3979 }
3980 
3981 void SchedulerProcess::printStates(const QString &label)
3982 {
3983     qCDebug(KSTARS_EKOS_SCHEDULER) <<
3984                                    QString("%1 %2 %3%4 %5 %6 %7 %8 %9\n")
3985                                    .arg(label)
3986                                    .arg(timerStr(moduleState()->timerState()))
3987                                    .arg(getSchedulerStatusString(moduleState()->schedulerState()))
3988                                    .arg((moduleState()->timerState() == RUN_JOBCHECK && activeJob() != nullptr) ?
3989                                         QString("(%1 %2)").arg(SchedulerJob::jobStatusString(activeJob()->getState()))
3990                                         .arg(SchedulerJob::jobStageString(activeJob()->getStage())) : "")
3991                                    .arg(ekosStateString(moduleState()->ekosState()))
3992                                    .arg(indiStateString(moduleState()->indiState()))
3993                                    .arg(startupStateString(moduleState()->startupState()))
3994                                    .arg(shutdownStateString(moduleState()->shutdownState()))
3995                                    .arg(parkWaitStateString(moduleState()->parkWaitState())).toLatin1().data();
3996     foreach (auto j, moduleState()->jobs())
3997         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("job %1 %2\n").arg(j->getName()).arg(SchedulerJob::jobStatusString(
3998                                            j->getState())).toLatin1().data();
3999 }
4000 
4001 } // Ekos namespace