diff --git a/kstars/ekos/capture/capture.cpp b/kstars/ekos/capture/capture.cpp --- a/kstars/ekos/capture/capture.cpp +++ b/kstars/ekos/capture/capture.cpp @@ -641,12 +641,15 @@ } } + // Only emit a new status if there is an active job or if capturing is suspended. + // The latter is necessary since suspending clears the active job, but the Capture + // module keeps the control. + if (activeJob != nullptr || m_State == CAPTURE_SUSPENDED) + emit newStatus(targetState); + calibrationStage = CAL_NONE; m_State = targetState; - if (activeJob != nullptr) - emit newStatus(targetState); - // Turn off any calibration light, IF they were turned on by Capture module if (currentDustCap && dustCapLightEnabled) { 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 @@ -90,6 +90,14 @@ PARKWAIT_ERROR } ParkWaitStatus; + /** @brief options what should happen if an error or abort occurs */ + typedef enum + { + ERROR_DONT_RESTART, + ERROR_RESTART_AFTER_TERMINATION, + ERROR_RESTART_IMMEDIATELY + } ErrorHandlingStrategy; + /** @brief Columns, in the same order as UI. */ typedef enum { @@ -224,6 +232,16 @@ return schedulerProfileCombo->currentText(); } + /** + * @brief retrieve the error handling strategy from the UI + */ + ErrorHandlingStrategy getErrorHandlingStrategy(); + + /** + * @brief select the error handling strategy (no restart, restart after all terminated, restart immediately) + */ + void setErrorHandlingStrategy (ErrorHandlingStrategy strategy); + /** @}*/ /** @{ */ 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 @@ -196,6 +196,28 @@ connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); + // restore default values for error handling strategy + setErrorHandlingStrategy(static_cast(Options::errorHandlingStrategy())); + errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); + errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay()); + + // save new default values for error handling strategy + + connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [this](bool checked) + { + Options::setRescheduleErrors(checked); + }); + connect(errorHandlingButtonGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton *button) + { + Q_UNUSED(button); + Options::setErrorHandlingStrategy(getErrorHandlingStrategy()); + }); + connect(errorHandlingDelaySB, static_cast(&QSpinBox::valueChanged), [this](int value) + { + Options::setErrorHandlingStrategyDelay(value); + }); + + loadProfiles(); watchJobChanges(true); @@ -238,18 +260,25 @@ QButtonGroup * const buttonGroups[] = { stepsButtonGroup, + errorHandlingButtonGroup, startupButtonGroup, constraintButtonGroup, completionButtonGroup, startupProcedureButtonGroup, shutdownProcedureGroup }; + QAbstractButton * const buttons[] = + { + errorHandlingRescheduleErrorsCB + }; + QSpinBox * const spinBoxes[] = { culminationOffset, repeatsSpin, - prioritySpin + prioritySpin, + errorHandlingDelaySB }; QDoubleSpinBox * const dspinBoxes[] = @@ -289,6 +318,11 @@ { setDirty(); }); + for (auto * const control : buttons) + connect(control, static_cast(&QAbstractButton::clicked), this, [this](bool) + { + setDirty(); + }); for (auto * const control : spinBoxes) connect(control, static_cast(&QSpinBox::valueChanged), this, [this]() { @@ -313,6 +347,8 @@ disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); for (auto * const control : comboBoxes) disconnect(control, static_cast(&QComboBox::currentIndexChanged), this, nullptr); + for (auto * const control : buttons) + disconnect(control, static_cast(&QAbstractButton::clicked), this, nullptr); for (auto * const control : buttonGroups) disconnect(control, static_cast(&QButtonGroup::buttonToggled), this, nullptr); for (auto * const control : spinBoxes) @@ -1393,10 +1429,6 @@ /* First, filter out non-schedulable jobs */ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ QList sortedJobs = jobs; - sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) - { - return SchedulerJob::JOB_ABORTED < job->getState(); - }), sortedJobs.end()); /* Then enumerate SchedulerJobs to consolidate imaging time */ foreach (SchedulerJob *job, sortedJobs) @@ -1408,18 +1440,18 @@ /* If job is scheduled, keep it for evaluation against others */ break; - case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_INVALID: case SchedulerJob::JOB_COMPLETE: - /* If job is in error, invalid or complete, bypass evaluation */ + /* If job is invalid or complete, bypass evaluation */ continue; case SchedulerJob::JOB_BUSY: /* If job is busy, edge case, bypass evaluation */ continue; + case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: - /* If job is aborted and we're running, keep its evaluation until there is nothing else to do */ + /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */ if (state == SCHEDULER_RUNNING) continue; /* Fall through */ @@ -1502,31 +1534,63 @@ return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; }; + /* This predicate matches jobs neither being evaluated nor aborted nor in error state */ + auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job) + { + SchedulerJob::JOBStatus const s = job->getState(); + return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != 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; }; + bool nea = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted); + bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error); + /* If there are no jobs left to run in the filtered list, stop evaluation */ - if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted)) + if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) { 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)) + if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && + errorHandlingDontRestartButton->isChecked() == false) { - appendLogText(i18n("Only aborted jobs left in the scheduler queue, rescheduling those.")); - std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) + appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.", + errorHandlingRescheduleErrorsCB->isChecked() ? "aborted or error" : "aborted")); + + // set aborted and error jobs to evaluation state + for (int index = 0; index < sortedJobs.size(); index++) { - if (SchedulerJob::JOB_ABORTED == job->getState()) + SchedulerJob * const job = sortedJobs.at(index); + if (SchedulerJob::JOB_ABORTED == job->getState() || + (errorHandlingRescheduleErrorsCB->isChecked() && SchedulerJob::JOB_ERROR == job->getState())) job->setState(SchedulerJob::JOB_EVALUATION); - }); + } + + if (errorHandlingRestartAfterAllButton->isChecked()) + { + // interrupt regular status checks during the sleep time + schedulerTimer.stop(); + + // but before we restart them, we wait for the given delay. + appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value())); + + // wait the given delay until the jobs will be evaluated again + sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); + sleepTimer.start(); + sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); + sleepLabel->show(); + // we continue to determine which job should be running, when the delay is over + } } /* If option says so, reorder by altitude and priority before sequencing */ @@ -1536,10 +1600,6 @@ 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())); @@ -2003,13 +2063,6 @@ } } - /* Remove unscheduled jobs that may have appeared during the last step - safeguard */ - sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) - { - 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 */ mDirty = reorderJobs(sortedJobs) | mDirty; @@ -2041,7 +2094,8 @@ 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)) + else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && + errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) @@ -3116,7 +3170,7 @@ { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); - currentJob->setState(SchedulerJob::JOB_ABORTED); + currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); @@ -3142,7 +3196,7 @@ QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()), QString("%L1").arg(currentJob->getMinAltitude(), 0, 'f', minAltitude->decimals()))); - currentJob->setState(SchedulerJob::JOB_ABORTED); + currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); @@ -3168,7 +3222,7 @@ "degrees), marking aborted.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); - currentJob->setState(SchedulerJob::JOB_ABORTED); + currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); @@ -3187,7 +3241,7 @@ appendLogText(i18n( "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking aborted.", preDawnDateTime.toString(), Options::preDawnTime(), currentJob->getName())); - currentJob->setState(SchedulerJob::JOB_ABORTED); + currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); @@ -3224,7 +3278,7 @@ } else { - appendLogText(i18n("Warning: job '%1' alignment procedure failed, aborting job.", currentJob->getName())); + appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } @@ -3250,7 +3304,7 @@ } else { - appendLogText(i18n("Warning: job '%1' capture procedure failed, aborting job.", currentJob->getName())); + appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } @@ -3275,7 +3329,7 @@ } else { - appendLogText(i18n("Warning: job '%1' focusing procedure failed, aborting job.", currentJob->getName())); + appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } @@ -3299,7 +3353,7 @@ } else { - appendLogText(i18n("Warning: job '%1' guiding procedure failed, aborting job.", currentJob->getName())); + appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } @@ -3840,8 +3894,12 @@ char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; + XMLEle *subEP = nullptr; char c; + // We expect all data read from the XML to be in the C locale - QLocale::c() + QLocale cLocale = QLocale::c(); + while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); @@ -3857,6 +3915,18 @@ { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } + else if (!strcmp(tag, "ErrorHandlingStrategy")) + { + setErrorHandlingStrategy(static_cast(cLocale.toInt(findXMLAttValu(ep, "value")))); + + subEP = findXMLEle(ep, "delay"); + if (subEP) + { + errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP))); + } + subEP = findXMLEle(ep, "RescheduleErrors"); + errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr); + } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; @@ -4207,6 +4277,12 @@ outstream << "" << endl; } + outstream << "" << endl; + if (errorHandlingRescheduleErrorsCB->isChecked()) + outstream << "" << endl; + outstream << "" << errorHandlingDelaySB->value() << "" << endl; + outstream << "" << endl; + outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; @@ -4386,30 +4462,39 @@ /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; - if (currentJob->getState() == SchedulerJob::JOB_ERROR) + if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); - appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); + if (currentJob->getState() == SchedulerJob::JOB_ERROR) + appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); + else + appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); - setCurrentJob(nullptr); - schedulerTimer.start(); - } - else if (currentJob->getState() == SchedulerJob::JOB_ABORTED) - { - // Stop Guiding if it was used - stopGuiding(); + // restart aborted jobs immediately, if error handling strategy is set to "restart immediately" + if (errorHandlingRestartImmediatelyButton->isChecked() && + (currentJob->getState() == SchedulerJob::JOB_ABORTED || + (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked()))) + { + // reset the state so that it will be restarted + currentJob->setState(SchedulerJob::JOB_SCHEDULED); - appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); + appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName())); - // Always reset job stage - currentJob->setStage(SchedulerJob::STAGE_IDLE); + // wait the given delay until the jobs will be evaluated again + sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); + sleepTimer.start(); + sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); + sleepLabel->show(); + return; + } + // otherwise start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } @@ -5818,6 +5903,7 @@ } } + void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); @@ -5866,6 +5952,38 @@ schedulerTimer.start(); } +Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() +{ + // The UI holds the state + if (errorHandlingRestartAfterAllButton->isChecked()) + return ERROR_RESTART_AFTER_TERMINATION; + else if (errorHandlingRestartImmediatelyButton->isChecked()) + return ERROR_RESTART_IMMEDIATELY; + else + return ERROR_DONT_RESTART; + +} + +void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy) +{ + errorHandlingWaitLabel->setEnabled(strategy != ERROR_DONT_RESTART); + errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART); + + switch (strategy) { + case ERROR_RESTART_AFTER_TERMINATION: + errorHandlingRestartAfterAllButton->setChecked(true); + break; + case ERROR_RESTART_IMMEDIATELY: + errorHandlingRestartImmediatelyButton->setChecked(true); + break; + default: + errorHandlingDontRestartButton->setChecked(true); + break; + } +} + + + void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; @@ -6653,7 +6771,7 @@ } else { - appendLogText(i18n("Warning: job '%1' alignment procedure failed, aborting job.", currentJob->getName())); + appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); @@ -6727,8 +6845,8 @@ } else { - appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking terminated due to errors.", currentJob->getName())); - currentJob->setState(SchedulerJob::JOB_ERROR); + appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); + currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } @@ -6864,8 +6982,8 @@ } else { - appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking terminated due to errors.", currentJob->getName())); - currentJob->setState(SchedulerJob::JOB_ERROR); + appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); + currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } diff --git a/kstars/ekos/scheduler/scheduler.ui b/kstars/ekos/scheduler/scheduler.ui --- a/kstars/ekos/scheduler/scheduler.ui +++ b/kstars/ekos/scheduler/scheduler.ui @@ -717,7 +717,7 @@ Start Time - + End Time @@ -765,8 +765,8 @@ Start Scheduler - - QPushButton:checked + + QPushButton:checked { background-color: maroon; border: 1px outset; @@ -805,7 +805,7 @@ Pause Scheduler - QPushButton:checked + QPushButton:checked { background-color: maroon; border: 1px outset; @@ -816,7 +816,7 @@ - true + true @@ -975,7 +975,7 @@ start the job on the specified date and time - On + O&n startupButtonGroup @@ -1036,34 +1036,15 @@ 1 - - - - The object's altitude must remain equal or higher than the given value. - + + - Alt > - - - true - - - constraintButtonGroup - - - - - - - -15.000000000000000 - - - 89.900000000000006 + ° - - + + ° @@ -1092,11 +1073,30 @@ - - + + + + -15.000000000000000 + + + 89.900000000000006 + + + + + + + The object's altitude must remain equal or higher than the given value. + - ° + Alt > + + + true + + constraintButtonGroup + @@ -1214,7 +1214,7 @@ - Repeat for + &Repeat for completionButtonGroup @@ -1321,14 +1321,114 @@ + + + + <html><head/><body><p>Define what should happen when a job steps into an error or aborts:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">don't restart</span>: Don't restart the job in case of an error or an abort.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">re-schedule after all terminated</span>: If a job gets aborted, the scheduler will only re-schedule it if when all jobs are finished or aborted. If this is the case, the scheduler re-schedules all aborted jobs and sleeps for the given delay.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">re-schedule immediately</span>: As soon as a job gets aborted, the scheduler will re-schedule it and waits the given delay.</li></ul><p>If the option for re-scheduling errors is selected, errors are handled like aborts. Otherwise, jobs that step into an error are never re-scheduled.</p></body></html> + + + Handling of aborted jobs + + + + 18 + + + 0 + + + + + Do not re-schedule aborted jobs. + + + don'&t re-schedule + + + errorHandlingButtonGroup + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Re-schedule aborted jobs as soon as all executable jobs are either completed or aborted. + + + re-s&chedule after all finished + + + errorHandlingButtonGroup + + + + + + + Treat errors like aborts. + + + re-schedule errors + + + + + + + Delay how long should be waited until an aborted job will be restarted. + + + wait (secs) + + + + + + + Re-schedule an aborted job immediately. + + + re-s&chedule immediately + + + errorHandlingButtonGroup + + + + + + + Delay in seconds. + + + 10000 + + + 10 + + + + + + - - - 3 - - + + <html><head/><body><p>One-time startup procedure to be executed before starting Ekos. The script is executed <span style=" font-weight:600; text-decoration: underline;">before</span> the startup procedures (e.g. unpark scope), if selected, are executed.</p></body></html> @@ -1476,7 +1576,7 @@ - + @@ -1654,6 +1754,13 @@ + + + + 3 + + + @@ -1734,24 +1841,25 @@ - + false - + false + false - - + + false diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -2326,6 +2326,18 @@ 0 + + + 1 + + + + 0 + + + + false +