diff --git a/kstars/ekos/opsekos.ui b/kstars/ekos/opsekos.ui --- a/kstars/ekos/opsekos.ui +++ b/kstars/ekos/opsekos.ui @@ -198,6 +198,19 @@ + + + + <html><body><p>Do not permit jobs to be scheduled less than this many degrees before the altitude restriction. Actual execution proceeds until the altitude limit.</p></body></html> + + + Setting Altitude Cutoff + + + + + + 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 @@ -34,7 +34,6 @@ #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 -#define SETTING_ALTITUDE_CUTOFF 3 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 @@ -1359,11 +1358,15 @@ /* If job is busy, edge case, bypass evaluation */ continue; - case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_ABORTED: + /* If job is aborted and we're running, keep its evaluation until there is nothing else to do */ + if (state == SCHEDULER_RUNNIG) + continue; + /* Fall through */ + case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: default: - /* If job is idle or aborted, re-evaluate completely */ + /* If job is idle, re-evaluate completely */ job->setEstimatedTime(-1); break; } @@ -1432,28 +1435,51 @@ updatePreDawn(); - /* Remove jobs that don't need evaluation */ - sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) + /* This predicate matches jobs not being evaluated and not aborted */ + auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job) { - return SchedulerJob::JOB_EVALUATION != job->getState(); - }), sortedJobs.end()); + SchedulerJob::JOBStatus const s = job->getState(); + return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; + }; + + /* This predicate matches jobs that aborted, or completed for whatever reason */ + auto finished_or_aborted = [](SchedulerJob const * const job) + { + SchedulerJob::JOBStatus const s = job->getState(); + return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s; + }; /* If there are no jobs left to run in the filtered list, stop evaluation */ - if (sortedJobs.isEmpty()) + if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted)) { appendLogText(i18n("No jobs left in the scheduler queue.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } + /* If there are only aborted jobs that can run, reschedule those */ + if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted)) + { + appendLogText(i18n("Only aborted jobs left in the scheduler queue, rescheduling those.")); + std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) + { + if (SchedulerJob::JOB_ABORTED == job->getState()) + job->setState(SchedulerJob::JOB_EVALUATION); + }); + } + /* If option says so, reorder by altitude and priority before sequencing */ /* FIXME: refactor so all sorts are using the same predicates */ /* FIXME: use std::stable_sort as qStableSort is deprecated */ /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); if (Options::sortSchedulerJobs()) { + // If we reorder, remove all non-runnable jobs so that they end up at the end of the list and do not disturb the reorder + // We tested that the list could not be empty after that operation above + sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted), sortedJobs.end()); + using namespace std::placeholders; std::stable_sort(sortedJobs.begin(), sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); @@ -1501,6 +1527,10 @@ { SchedulerJob * const currentJob = sortedJobs.at(index); + // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order + if (SchedulerJob::JOB_EVALUATION != currentJob->getState()) + continue; + // At this point, a job with no valid start date is a problem, so consider invalid startup time is now if (!currentJob->getStartupTime().isValid()) currentJob->setStartupTime(now); @@ -1664,7 +1694,8 @@ currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); - Q_ASSERT_X(previousJob->getCompletionTime() < currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); + // Lead time can be zero, so completion may equal startup + Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); } @@ -1845,13 +1876,13 @@ { if (currentJob->getCompletionTime() < currentJob->getStartupTime()) { - currentJob->setState(SchedulerJob::JOB_INVALID); - appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); + currentJob->setState(SchedulerJob::JOB_INVALID); + break; } } @@ -1911,7 +1942,8 @@ /* Remove unscheduled jobs that may have appeared during the last step - safeguard */ sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) { - return SchedulerJob::JOB_SCHEDULED != job->getState(); + SchedulerJob::JOBStatus const s = job->getState(); + return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; }), sortedJobs.end()); /* Apply sorting to queue table, and mark it for saving if it changes */ @@ -1929,18 +1961,52 @@ * We select the first job that has to be run, per schedule. */ + /* This predicate matches jobs that are neither scheduled to run nor aborted */ + auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job) + { + SchedulerJob::JOBStatus const s = job->getState(); + return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; + }; + /* If there are no jobs left to run in the filtered list, stop evaluation */ - if (sortedJobs.isEmpty()) + if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted)) { - appendLogText(i18n("No jobs left in the scheduler queue.")); + appendLogText(i18n("No jobs left in the scheduler queue after evaluating.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } + /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */ + else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted)) + { + appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); + std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) + { + if (SchedulerJob::JOB_ABORTED == job->getState()) + job->setState(SchedulerJob::JOB_EVALUATION); + }); + + jobEvaluationOnly = false; + return; + } - SchedulerJob * const job_to_execute = sortedJobs.first(); + /* The job to run is the first scheduled, locate it in the list */ + QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * const job) + { + return SchedulerJob::JOB_SCHEDULED == job->getState(); + }); + + /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */ + if (sortedJobs.end() == job_to_execute_iterator) + { + appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup.")); + setCurrentJob(nullptr); + jobEvaluationOnly = false; + return; + } /* Check if job can be processed right now */ + SchedulerJob * const job_to_execute = *job_to_execute_iterator; if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) if( 0 <= calculateJobScore(job_to_execute, now)) job_to_execute->setStartupTime(now); @@ -2044,6 +2110,8 @@ // Calculate the UT at the argument time KStarsDateTime const ut = geo->LTtoUT(ltWhen); + double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); + // Within the next 24 hours, search when the job target matches the altitude and moon constraints for (unsigned int minute = 0; minute < 24 * 60; minute++) { @@ -2290,6 +2358,7 @@ o.EquatorialToHorizontal(&LST, geo->lat()); double const altitude = o.alt().Degrees(); + double const SETTING_ALTITUDE_CUTOFF = Options::settingAltitudeCutoff(); int16_t score = BAD_SCORE - 1; // If altitude is negative, bad score diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -2220,6 +2220,10 @@ 30 + + + 3 + 0