File indexing completed on 2024-05-05 07:42:14
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