diff --git a/kstars/ekos/mount/mount.cpp b/kstars/ekos/mount/mount.cpp --- a/kstars/ekos/mount/mount.cpp +++ b/kstars/ekos/mount/mount.cpp @@ -900,9 +900,13 @@ Mount::ParkingStatus Mount::getParkingStatus() { - if (currentTelescope == nullptr || currentTelescope->canPark() == false) + if (currentTelescope == nullptr) return PARKING_ERROR; + // In the case mount can't park, return mount is unparked + if (currentTelescope->canPark() == false) + return UNPARKING_OK; + ISwitchVectorProperty *parkSP = currentTelescope->getBaseDevice()->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) @@ -932,7 +936,12 @@ return UNPARKING_BUSY; case IPS_ALERT: - return PARKING_ERROR; + // If mount replied with an error to the last un/park request, + // assume state did not change in order to return a clear state + if (parkSP->sp[0].s == ISS_ON) + return PARKING_OK; + else + return UNPARKING_OK; } return PARKING_ERROR; diff --git a/kstars/ekos/scheduler/scheduler.h b/kstars/ekos/scheduler/scheduler.h --- a/kstars/ekos/scheduler/scheduler.h +++ b/kstars/ekos/scheduler/scheduler.h @@ -314,9 +314,10 @@ void resetJobEdit(); /** - * @brief checkJobStatus Check the overall state of the scheduler, Ekos, and INDI. When all is OK, it call evaluateJobs(); + * @brief checkJobStatus Check the overall state of the scheduler, Ekos, and INDI. When all is OK, it calls evaluateJobs() when no job is current or executeJob() if a job is selected. + * @return False if this function needs to be called again later, true if situation is stable and operations may continue. */ - void checkStatus(); + bool checkStatus(); /** * @brief checkJobStage Check the progress of the job states and make DBUS call to start the next stage until the job is complete. @@ -466,6 +467,12 @@ */ bool checkEkosState(); + /** + * @brief isINDIConnected Determines the status of the INDI connection. + * @return True if INDI connection is up and usable, else false. + */ + bool isINDIConnected(); + /** * @brief checkINDIState Check INDI startup stages and take whatever action necessary to get INDI devices connected. * @return True if INDI devices are connected, false if it is under progress. diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1429,7 +1429,7 @@ // If starting time already passed by 5 minutes (default), we mark the job as invalid or aborted if (timeUntil < (-1 * Options::leadTime() * 60)) { - dms const passedUp(-timeUntil / 3600.0); + dms const passedUp(-timeUntil * 15.0 / 3600.0); /* Mark the job invalid only if its startup time was a user request, else just abort it for later reschedule */ if (job->getFileStartupCondition() == SchedulerJob::START_AT) @@ -1706,111 +1706,18 @@ // Sort again by schedule, sooner first, as some jobs may have shifted during the last step qStableSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingStartupTimeOrder); - setCurrentJob(sortedJobs.first()); + SchedulerJob * const job_to_execute = sortedJobs.first(); /* Check if job can be processed right now */ - if (currentJob->getFileStartupCondition() == SchedulerJob::START_ASAP) - if( 0 < calculateJobScore(currentJob, now)) - currentJob->setStartupTime(now); - - int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); + if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) + if( 0 < calculateJobScore(job_to_execute, now)) + job_to_execute->setStartupTime(now); appendLogText(i18n("Job '%1' is selected for next observation with priority #%2 and score %3.", - currentJob->getName(), currentJob->getPriority(), currentJob->getScore())); - - // If mount was previously parked awaiting job activation, we unpark it. - if (parkWaitState == PARKWAIT_PARKED) - { - parkWaitState = PARKWAIT_UNPARK; - return; - } - - // If we already started, we check when the next object is scheduled at. - // If it is more than 30 minutes in the future, we park the mount if that is supported - // and we unpark when it is due to start. + job_to_execute->getName(), job_to_execute->getPriority(), job_to_execute->getScore())); - // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed - // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready - if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() && - nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) - { - appendLogText(i18n("Job '%1' scheduled for execution at %2. Observatory scheduled for " - "shutdown until next job is ready.", - currentJob->getName(), currentJob->getStartupTime().toString())); - preemptiveShutdown = true; - weatherCheck->setEnabled(false); - weatherLabel->hide(); - checkShutdownState(); - - // Wake up when job is due - //sleepTimer.setInterval((nextObservationTime * 1000 - (1000 * Options::leadTime() * 60))); - sleepTimer.setInterval(( (nextObservationTime+1) * 1000)); - //connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); - sleepTimer.start(); - } - // Otherise, sleep until job is ready - /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ - //else if (nextObservationTime > (Options::leadTime() * 60)) - else if (nextObservationTime > 1) - { - // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled - // 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. - // This is also only performed if next job is due more than the default lead time (5 minutes). - // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. - if ((nextObservationTime > (Options::leadTime() * 60)) && - startupState == STARTUP_COMPLETE && - parkWaitState == PARKWAIT_IDLE && - (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && - parkMountCheck->isEnabled() && - parkMountCheck->isChecked()) - { - appendLogText(i18n("Job '%1' scheduled for execution at %2. Parking the mount until " - "the job is ready.", - currentJob->getName(), currentJob->getStartupTime().toString())); - parkWaitState = PARKWAIT_PARK; - } - // If mount was pre-emptivally parked OR if parking is not supported or if start up procedure is IDLE then go into - // sleep mode until next job is ready. -#if 0 - else if ((nextObservationTime > (Options::leadTime() * 60)) && - (parkWaitState == PARKWAIT_PARKED || - parkMountCheck->isEnabled() == false || - parkMountCheck->isChecked() == false || - startupState == STARTUP_IDLE)) - { - appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), - now.addSecs(nextObservationTime+1).toString())); - sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); - schedulerTimer.stop(); - sleepLabel->show(); - - // Wake up when job is ready. - // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second - // So just wake it up when it is exactly due - sleepTimer.setInterval(( (nextObservationTime+1) * 1000)); - sleepTimer.start(); - } -#endif - // The only difference between sleep and wait modes is the time. If the time more than lead time (5 minutes by default) - // then we sleep, otherwise we wait. It's the same thing, just different labels. - else - { - appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), - now.addSecs(nextObservationTime+1).toString())); - sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); - schedulerTimer.stop(); - sleepLabel->show(); - - /* FIXME: stop tracking now */ - - // Wake up when job is ready. - // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second - // So just wake it up when it is exactly due - sleepTimer.setInterval(( (nextObservationTime+1) * 1000)); - //connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); - sleepTimer.start(); - } - } + // Set the current job, and let the status timer execute it when ready + setCurrentJob(job_to_execute); } void Scheduler::wakeUpScheduler() @@ -2297,11 +2204,101 @@ void Scheduler::executeJob(SchedulerJob *job) { + // Don't execute the current job if it is already busy + if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState()) + return; + setCurrentJob(job); - /* If job schedule isn't now, postpone - else this will cancel a parking attempt */ - if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) + QDateTime const now = KStarsData::Instance()->lt(); + + // If we already started, we check when the next object is scheduled at. + // If it is more than 30 minutes in the future, we park the mount if that is supported + // and we unpark when it is due to start. + int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); + + // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed + // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready + if (startupState == STARTUP_COMPLETE && + Options::preemptiveShutdown() && + nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) + { + appendLogText(i18n( + "Job '%1' scheduled for execution at %2. " + "Observatory scheduled for shutdown until next job is ready.", + currentJob->getName(), currentJob->getStartupTime().toString())); + preemptiveShutdown = true; + weatherCheck->setEnabled(false); + weatherLabel->hide(); + checkShutdownState(); + + schedulerTimer.stop(); + + // Wake up when job is due. + // FIXME: Implement waking up periodically before job is due for weather check. + // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; + sleepTimer.setInterval( (nextObservationTime+1) * 1000); + sleepTimer.start(); + return; + } + // Otherise, sleep until job is ready + /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ + // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled + // 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. + // This is also only performed if next job is due more than the default lead time (5 minutes). + // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. + else if (nextObservationTime > Options::leadTime() * 60 && + startupState == STARTUP_COMPLETE && + parkWaitState == PARKWAIT_IDLE && + (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && + parkMountCheck->isEnabled() && + parkMountCheck->isChecked()) + { + appendLogText(i18n( + "Job '%1' scheduled for execution at %2. " + "Parking the mount until the job is ready.", + currentJob->getName(), currentJob->getStartupTime().toString())); + + parkWaitState = PARKWAIT_PARK; + + return; + } + // If the time to wait is greater than the lead time (5 minutes by default) + // then we sleep, otherwise we wait. It's the same thing, just different labels. + else if (nextObservationTime > Options::leadTime() * 60) + { + appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), + now.addSecs(nextObservationTime+1).toString())); + sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); + sleepLabel->show(); + + // Warn the user if the next job is really far away - 60/5 = 12 times the lead time + if (nextObservationTime > Options::leadTime() * 60 * 12) + { + dms delay((double) nextObservationTime * 15.0 / 3600.0); + appendLogText(i18n( + "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.", + currentJob->getName(), delay.toHMSString())); + } + + /* FIXME: stop tracking now */ + + schedulerTimer.stop(); + + // Wake up when job is due. + // FIXME: Implement waking up periodically before job is due for weather check. + // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; + sleepTimer.setInterval(( (nextObservationTime+1) * 1000)); + sleepTimer.start(); + + return; + } + // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt + else if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) + return; + + // From this point job can be executed now if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { @@ -2319,7 +2316,7 @@ currentJob->setState(SchedulerJob::JOB_BUSY); KNotification::event(QLatin1String("EkosSchedulerJobStart"), - i18n("Ekos job started (%1)", currentJob->getName())); + i18n("Ekos job started (%1)", currentJob->getName())); // No need to continue evaluating jobs as we already have one. @@ -2417,6 +2414,18 @@ return false; } +bool Scheduler::isINDIConnected() +{ + QDBusReply isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); + + if (isINDIConnected.error().type() != QDBusError::NoError) + return false; + else if (isINDIConnected.value() == EkosManager::EKOS_STATUS_SUCCESS) + return true; + else // ERROR or IDLE + return false; +} + bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) @@ -2429,8 +2438,7 @@ case INDI_IDLE: { // Even in idle state, we make sure that INDI is not already connected. - QDBusReply isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); - if (isINDIConnected.value() == EkosManager::EKOS_STATUS_SUCCESS) + if (isINDIConnected()) { indiState = INDI_PROPERTY_CHECK; @@ -2714,7 +2722,7 @@ } // The following steps require a connection to the INDI server - if (indiState == INDI_READY) + if (isINDIConnected()) { if (capCheck->isEnabled() && capCheck->isChecked()) { @@ -2747,7 +2755,7 @@ break; case SHUTDOWN_PARK_CAP: - if (indiState != INDI_READY) + if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; @@ -2763,7 +2771,7 @@ break; case SHUTDOWN_PARK_MOUNT: - if (indiState != INDI_READY) + if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; @@ -2779,7 +2787,7 @@ break; case SHUTDOWN_PARK_DOME: - if (indiState != INDI_READY) + if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; @@ -2912,10 +2920,10 @@ } } -void Scheduler::checkStatus() +bool Scheduler::checkStatus() { if (state == SCHEDULER_PAUSED) - return; + return true; // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) @@ -2925,24 +2933,24 @@ { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) - return; + return false; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); - return; + return false; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) - return; + return false; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); - return; + return false; } if (shutdownState == SHUTDOWN_COMPLETE) @@ -2953,23 +2961,23 @@ // Stop Scheduler stop(); - return; + return true; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) - return; + return false; checkShutdownState(); - return; + return false; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) - return; + return false; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); @@ -2981,32 +2989,34 @@ { // Stop Scheduler stop(); - return; + return true; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) - return; + return false; // #5 Check if Ekos is started if (checkEkosState() == false) - return; + return false; // #6 Check if INDI devices are connected. if (checkINDIState() == false) - return; + return false; // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job if (checkParkWaitState() == false) - return; + return false; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) - return; + return false; // #8 Execute the job executeJob(currentJob); } + + return true; } void Scheduler::checkJobStage() @@ -3114,12 +3124,9 @@ } } - // Check Ekos state - if (!checkEkosState()) - return; - - // Check INDI state - if (!checkINDIState()) + // #5 Check system status to improve robustness + // This handles external events such as disconnections or end-user manipulating INDI panel + if (!checkStatus()) return; switch (currentJob->getStage()) @@ -3543,7 +3550,7 @@ void Scheduler::stopCurrentJobAction() { - if (currentJob) + if (nullptr != currentJob) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); @@ -3618,8 +3625,7 @@ qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss."); // If INDI is assumed to be up, check its state - QDBusReply const isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); - if (isINDIConnected.value() == EkosManager::EKOS_STATUS_SUCCESS) + if (isINDIConnected()) { // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed."); @@ -4071,6 +4077,13 @@ { Q_ASSERT(currentJob != nullptr); + // If the mount was parked by a pause or the end-user, unpark + if (isMountParked()) + { + parkWaitState = PARKWAIT_UNPARK; + return; + } + if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); @@ -4470,13 +4483,19 @@ void Scheduler::stopGuiding() { - if ((currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) && - (currentJob->getStage() == SchedulerJob::STAGE_GUIDING_COMPLETE || - currentJob->getStage() == SchedulerJob::STAGE_CAPTURING)) + if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)) { - qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping guiding..."; - guideInterface->call(QDBus::AutoDetect, "abort"); - guideFailureCount = 0; + switch (currentJob->getStage()) + { + case SchedulerJob::STAGE_GUIDING_COMPLETE: + case SchedulerJob::STAGE_CAPTURING: + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName()); + guideInterface->call(QDBus::AutoDetect, "abort"); + guideFailureCount = 0; + break; + + default: break; + } } } @@ -4796,15 +4815,15 @@ schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); - appendLogText(i18n("Job '%1' will run for %2.", schedJob->getName(), dms(diff / 3600.0f).toHMSString())); + appendLogText(i18n("Job '%1' will run for %2.", schedJob->getName(), dms(diff * 15.0 / 3600.0f).toHMSString())); schedJob->setEstimatedTime(diff); } // If we know finish time only, we can roughly estimate the time considering the job starts now else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); - appendLogText(i18n("Job '%1' will run for %2 if started now.", schedJob->getName(), dms(diff / 3600.0f).toHMSString())); + appendLogText(i18n("Job '%1' will run for %2 if started now.", schedJob->getName(), dms(diff * 15.0 / 3600.0f).toHMSString())); schedJob->setEstimatedTime(diff); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null @@ -4833,8 +4852,7 @@ totalImagingTime += 120*schedJob->getRepeatsRequired(); } - dms estimatedTime; - estimatedTime.setH(totalImagingTime / 3600.0); + dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); schedJob->setEstimatedTime(totalImagingTime); @@ -4947,28 +4965,30 @@ else switch ((Mount::ParkingStatus)mountReply.value()) { case Mount::PARKING_OK: - appendLogText(i18n("Mount parked.")); - - if (startupState == STARTUP_UNPARKING_MOUNT) - startupState = STARTUP_UNPARK_CAP; - else if (shutdownState == SHUTDOWN_PARKING_MOUNT) + // If we are starting up, we will unpark the mount in checkParkWaitState soon + // If we are shutting down and mount is parked, proceed to next step + if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; - else if (parkWaitState == PARKWAIT_PARKING) + + // Update parking engine state + if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; + appendLogText(i18n("Mount parked.")); parkingFailureCount = 0; break; case Mount::UNPARKING_OK: - appendLogText(i18n("Mount unparked.")); - + // If we are starting up and mount is unparked, proceed to next step + // If we are shutting down, we will park the mount in checkParkWaitState soon if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; - else if (shutdownState == SHUTDOWN_PARKING_MOUNT) - shutdownState = SHUTDOWN_PARK_DOME; - else if (parkWaitState == PARKWAIT_UNPARKING) + + // Update parking engine state + if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; + appendLogText(i18n("Mount unparked.")); parkingFailureCount = 0; break; @@ -5035,6 +5055,24 @@ parkingFailureCount = 0; break; + case Mount::PARKING_IDLE: + // Last parking action did not result in an action, so proceed to next step + if (shutdownState == SHUTDOWN_PARKING_MOUNT) + shutdownState = SHUTDOWN_PARK_DOME; + + // Last unparking action did not result in an action, so proceed to next step + if (startupState == STARTUP_UNPARKING_MOUNT) + startupState = STARTUP_UNPARK_CAP; + + // Update parking engine state + if (parkWaitState == PARKWAIT_PARKING) + parkWaitState = PARKWAIT_PARKED; + else if (parkWaitState == PARKWAIT_UNPARKING) + parkWaitState = PARKWAIT_UNPARKED; + + parkingFailureCount = 0; + break; + default: qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value()); }