diff --git a/Tests/scheduler/1x1s_Lum.esq b/Tests/scheduler/1x1s_Lum.esq new file mode 100644 --- /dev/null +++ b/Tests/scheduler/1x1s_Lum.esq @@ -0,0 +1,48 @@ + + +CCD Simulator +CCD Simulator +2 +0 +60 +0 + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Luminance +Light + + +0 +0 +0 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + diff --git a/Tests/scheduler/1x1s_RGBLumRGB.esq b/Tests/scheduler/1x1s_RGBLumRGB.esq new file mode 100644 --- /dev/null +++ b/Tests/scheduler/1x1s_RGBLumRGB.esq @@ -0,0 +1,282 @@ + + +CCD Simulator +CCD Simulator +2 +0 +60 +0 + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Red +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Green +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Blue +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Luminance +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Red +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Green +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + +1 + +1 +1 + + +0 +0 +1280 +1024 + +0 +Blue +Light + + +1 +1 +1 + +1 +1 +/tmp/kstars_tests +0 +0 + + + + +Manual + + +Manual + +False +False + + + diff --git a/Tests/scheduler/culmination_no_twilight.esl b/Tests/scheduler/culmination_no_twilight.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/culmination_no_twilight.esl @@ -0,0 +1,263 @@ + + +Default + +Alnath +10 + +5.43806 +28.6078 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alnilam +10 + +5.60306 +-1.20167 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alnitak +10 + +5.67917 +-1.9425 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphard +10 + +9.45917 +-8.65806 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphecca +10 + +15.5781 +26.7142 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alpheratz +10 + +0.139167 +29.0906 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphirk +10 + +21.4775 +70.5606 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alshain +10 + +19.9217 +6.40667 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Altair +10 + +19.8456 +8.86667 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Altais +10 + +19.2092 +67.6614 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Aludra +10 + +7.40139 +-29.3031 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +Alya +10 + +18.9367 +4.20306 + +/tmp/kstars_tests/1x1s_Lum.esq + +Culmination + + +MinimumAltitude + + +Sequence + + +Track + + + +UnparkMount + + +WarmCCD +ParkMount + + diff --git a/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl b/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl @@ -0,0 +1,95 @@ + + +Default + +Kocab +10 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_RGBLumRGB.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Kocab +10 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_RGBLumRGB.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Kocab +11 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_RGBLumRGB.esq + +ASAP + + +MinimumAltitude + + +Repeat + + +Track + + + +Kocab +12 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_RGBLumRGB.esq + +ASAP + + +MinimumAltitude + + +Repeat + + +Track + + + +UnparkMount + + +WarmCCD +ParkMount + + diff --git a/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl b/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl @@ -0,0 +1,95 @@ + + +Default + +Kocab +10 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Kocab +10 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Kocab +11 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Repeat + + +Track + + + +Kocab +12 + +14.845 +74.1553 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Repeat + + +Track + + + +UnparkMount + + +WarmCCD +ParkMount + + diff --git a/Tests/scheduler/readme.txt b/Tests/scheduler/readme.txt new file mode 100644 --- /dev/null +++ b/Tests/scheduler/readme.txt @@ -0,0 +1,5 @@ +Scheduler test vectors. + +Create folder /tmp/kstars_tests and copy the .esq and .esl files there. +Load them from that folder to test the scheduler. +To reset the tests, simply remove the capture subfolders that the scheduler creates when running. diff --git a/Tests/scheduler/simple_test.esl b/Tests/scheduler/simple_test.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/simple_test.esl @@ -0,0 +1,275 @@ + + + Default + + Alnath + 10 + + 5.43806 + 28.6078 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alnilam + 10 + + 5.60333 + -1.20167 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alnitak + 10 + + 5.67917 + -1.9425 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alphard + 10 + + 9.45972 + -8.65833 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alphecca + 10 + + 15.5781 + 26.7147 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alpheratz + 10 + + 0.139722 + 29.0906 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alphirk + 10 + + 21.4775 + 70.5606 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alshain + 10 + + 19.9217 + 6.40667 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Altair + 10 + + 19.8461 + 8.86722 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Altais + 10 + + 19.2092 + 67.6614 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Aludra + 10 + + 7.40139 + -29.3031 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + Alya + 10 + + 18.9369 + 4.20333 + + /tmp/kstars_tests/1x1s_Lum.esq + + ASAP + + + MinimumAltitude + EnforceTwilight + + + Sequence + + + Track + + + + UnparkMount + + + WarmCCD + ParkMount + + diff --git a/Tests/scheduler/simple_test_no_twilight.esl b/Tests/scheduler/simple_test_no_twilight.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/simple_test_no_twilight.esl @@ -0,0 +1,263 @@ + + +Default + +Alnath +10 + +5.43806 +28.6078 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alnilam +10 + +5.60306 +-1.20167 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alnitak +10 + +5.67917 +-1.9425 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphard +10 + +9.45944 +-8.65806 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphecca +10 + +15.5781 +26.7144 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alpheratz +10 + +0.139444 +29.0906 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alphirk +10 + +21.4775 +70.5606 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alshain +10 + +19.9217 +6.40667 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Altair +10 + +19.8458 +8.86694 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Altais +10 + +19.2092 +67.6614 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Aludra +10 + +7.40139 +-29.3031 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +Alya +10 + +18.9367 +4.20306 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude + + +Sequence + + +Track + + + +UnparkMount + + +WarmCCD +ParkMount + + diff --git a/Tests/scheduler/start_at_finish_at_test.esl b/Tests/scheduler/start_at_finish_at_test.esl new file mode 100644 --- /dev/null +++ b/Tests/scheduler/start_at_finish_at_test.esl @@ -0,0 +1,275 @@ + + +Default + +Alnath +10 + +5.43806 +28.6078 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alnilam +10 + +5.60333 +-1.20167 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alnitak +10 + +5.67917 +-1.9425 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alphard +10 + +9.45944 +-8.65806 + +/tmp/kstars_tests/1x1s_Lum.esq + +At + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alphecca +10 + +15.5781 +26.7144 + +/tmp/kstars_tests/1x1s_Lum.esq + +At + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alpheratz +10 + +0.139722 +29.0906 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alphirk +10 + +21.4775 +70.5606 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +At + + +Track + + + +Alshain +10 + +19.9217 +6.40667 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Altair +10 + +19.8458 +8.86694 + +/tmp/kstars_tests/1x1s_Lum.esq + +At + + +MinimumAltitude +EnforceTwilight + + +At + + +Track + + + +Altais +10 + +19.2092 +67.6614 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Aludra +10 + +7.40139 +-29.3031 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +Alya +10 + +18.9369 +4.20333 + +/tmp/kstars_tests/1x1s_Lum.esq + +ASAP + + +MinimumAltitude +EnforceTwilight + + +Sequence + + +Track + + + +UnparkMount + + +WarmCCD +ParkMount + + 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 @@ -183,31 +183,36 @@ { if (enable) { + connect(nameEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); connect(fitsEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); + connect(sequenceEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); connect(startupScript, SIGNAL(editingFinished()), this, SLOT(setDirty())); connect(shutdownScript, SIGNAL(editingFinished()), this, SLOT(setDirty())); - connect(schedulerProfileCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); + connect(stepsButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); connect(startupButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); connect(constraintButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); connect(completionButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); connect(startupProcedureButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); connect(shutdownProcedureGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); - connect(culminationOffset, SIGNAL(editingFinished()), this, SLOT(setDirty())); + connect(culminationOffset, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(startupTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); - connect(minAltitude, SIGNAL(editingFinished()), this, SLOT(setDirty())); - connect(repeatsSpin, SIGNAL(editingFinished()), this, SLOT(setDirty())); - connect(minMoonSeparation, SIGNAL(editingFinished()), this, SLOT(setDirty())); + connect(minAltitude, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); + connect(repeatsSpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); + connect(minMoonSeparation, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); connect(completionTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); + connect(prioritySpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); } else { //disconnect(this, SLOT(setDirty())); + disconnect(nameEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); disconnect(fitsEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); + disconnect(sequenceEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); disconnect(startupScript, SIGNAL(editingFinished()), this, SLOT(setDirty())); disconnect(shutdownScript, SIGNAL(editingFinished()), this, SLOT(setDirty())); disconnect(schedulerProfileCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty())); @@ -220,12 +225,13 @@ disconnect(startupProcedureButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); disconnect(shutdownProcedureGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); - disconnect(culminationOffset, SIGNAL(editingFinished()), this, SLOT(setDirty())); + disconnect(culminationOffset, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); disconnect(startupTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); - disconnect(minAltitude, SIGNAL(editingFinished()), this, SLOT(setDirty())); - disconnect(repeatsSpin, SIGNAL(editingFinished()), this, SLOT(setDirty())); - disconnect(minMoonSeparation, SIGNAL(editingFinished()), this, SLOT(setDirty())); + disconnect(minAltitude, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); + disconnect(repeatsSpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); + disconnect(minMoonSeparation, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); disconnect(completionTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty())); + disconnect(prioritySpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty())); } } @@ -361,6 +367,7 @@ //jobUnderEdit = false; saveJob(); + jobEvaluationOnly = true; evaluateJobs(); } @@ -380,7 +387,6 @@ if(SchedulerJob::FINISH_LOOP == job->getCompletionCondition()) appendLogText(i18n("Warning! Job '%1' has completion condition set to infinite repeat, other jobs may not execute.",job->getName())); - if (nameEdit->text().isEmpty()) { appendLogText(i18n("Target name is required.")); @@ -459,6 +465,8 @@ /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); + job->setFileStartupTime(job->getStartupTime()); + // #2 Constraints @@ -478,6 +486,21 @@ // twilight constraints job->setEnforceTwilight(twilightCheck->isChecked()); + /* Verifications */ + /* FIXME: perhaps use a method more visible to the end-user */ + if (SchedulerJob::START_AT == job->getFileStartupCondition()) + { + /* Warn if appending a job which startup time doesn't allow proper score */ + if (calculateJobScore(job, job->getStartupTime()) < 0) + appendLogText(i18n("Warning! Job '%1' has startup time %1 resulting in a negative score, and will be marked invalid when processed.", + job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); + + /* Warn if appending a job with a startup time that is in the past */ + if (job->getStartupTime() < KStarsData::Instance()->lt()) + appendLogText(i18n("Warning! Job '%1' has fixed startup time %1 set in the past, and will be marked invalid when evaluated.", + job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); + } + // #3 Completion conditions if (sequenceCompletionR->isChecked()) { @@ -523,6 +546,25 @@ else currentRow = queueTable->currentRow(); + // Warn user if a duplicated job is in the list - same target, same sequence + foreach (SchedulerJob *a_job, jobs) + { + if(a_job == job) + { + break; + } + else if(a_job->getName() == job->getName() && a_job->getSequenceFile() == job->getSequenceFile()) + { + appendLogText(i18n("Warning! Job '%1' at row %2 has a duplicate at row %3 (same target, same sequence file), " + "the scheduler will consider the same storage for captures!", + job->getName(), currentRow, + a_job->getNameCell()? a_job->getNameCell()->row()+1 : 0)); + appendLogText(i18n("Make sure job '%1' at row %2 has a specific startup time or a different priority, " + "and a greater repeat count (or disable option 'Remember job progress')", + job->getName(), currentRow)); + } + } + /* Reset job state to evaluate the changes - so this is equivalent to double-clicking the job */ /* FIXME: should we do that if no change was done to the job? */ /* FIXME: move this to SchedulerJob as a "reset" method */ @@ -713,11 +755,11 @@ case SchedulerJob::FINISH_REPEAT: repeatCompletionR->setChecked(true); + repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_LOOP: loopCompletionR->setChecked(true); - repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_AT: @@ -760,6 +802,7 @@ startB->setEnabled(true); //removeFromQueueB->setToolTip(i18n("Remove observation job from list.")); + jobEvaluationOnly = true; evaluateJobs(); } @@ -844,6 +887,7 @@ if (job->getState() <= SchedulerJob::JOB_BUSY) { + appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName())); job->setState(SchedulerJob::JOB_ABORTED); wasAborted = true; } @@ -974,8 +1018,8 @@ startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); - if (Dawn < 0) - calculateDawnDusk(); + /* Recalculate dawn and dusk astronomical times - unconditionally in case date changed */ + calculateDawnDusk(); state = SCHEDULER_RUNNIG; @@ -1027,37 +1071,80 @@ if (currentJob) { currentJob->setStageLabel(jobStatus); + queueTable->selectRow(currentJob->getStartupCell()->row()); } else { jobStatus->setText(i18n("No job running")); + queueTable->clearSelection(); } } void Scheduler::evaluateJobs() { + /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ + QDateTime const now = KStarsData::Instance()->lt(); + /* Start by refreshing the number of captures already present */ updateCompletedJobsCount(); + /* Update dawn and dusk astronomical times - unconditionally in case date changed */ + calculateDawnDusk(); + + /* 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 reorder jobs by priority */ + /* FIXME: refactor so all sorts are using the same predicates */ + /* FIXME: use std::sort as qSort is deprecated */ + if (Options::sortSchedulerJobs()) + qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); + /* Then enumerate SchedulerJobs, scheduling only what is required */ - foreach (SchedulerJob *job, jobs) + foreach (SchedulerJob *job, sortedJobs) { /* Let aborted jobs be rescheduled later instead of forgetting them */ /* FIXME: minimum altitude and altitude cutoff may cause loops here */ - if (job->getState() == SchedulerJob::JOB_ABORTED) - job->setState(SchedulerJob::JOB_EVALUATION); + switch (job->getState()) + { + /* If job is idle, set it for evaluation */ + case SchedulerJob::JOB_IDLE: + job->setState(SchedulerJob::JOB_EVALUATION); + job->setEstimatedTime(-1); + break; - if (job->getState() > SchedulerJob::JOB_SCHEDULED) - continue; + /* If job is aborted, reset it for evaluation */ + case SchedulerJob::JOB_ABORTED: + job->setState(SchedulerJob::JOB_EVALUATION); + break; - // If job is idle, let's set it up for evaluation. - if (job->getState() == SchedulerJob::JOB_IDLE) - { - job->setState(SchedulerJob::JOB_EVALUATION); - job->setEstimatedTime(-1); + /* If job is scheduled, quick-check startup and bypass evaluation if in future */ + case SchedulerJob::JOB_SCHEDULED: + if (job->getStartupTime() < now) + break; + continue; + + /* If job is in error, invalid or complete, bypass evaluation */ + case SchedulerJob::JOB_ERROR: + case SchedulerJob::JOB_INVALID: + case SchedulerJob::JOB_COMPLETE: + continue; + + /* If job is busy, edge case, bypass evaluation */ + case SchedulerJob::JOB_BUSY: + continue; + + /* Else evaluate */ + case SchedulerJob::JOB_EVALUATION: + default: + break; } // In case of a repeating jobs, let's make sure we have more runs left to go + /* FIXME: if finding a repeated job with no repeats remaining, state this is a safeguard - Is it really? Set complete? */ if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { if (job->getRepeatsRemaining() == 0) @@ -1068,19 +1155,6 @@ } } - // Warn user if a duplicated job is in the list - same target, same sequence - foreach (SchedulerJob *a_job, jobs) - if(a_job == job) - break; - else if(a_job->getName() == job->getName() && a_job->getSequenceFile() == job->getSequenceFile()) - appendLogText(i18n("Warning! Job '%1' is duplicated (same target, same sequence file), the scheduler will consider the same storage for captures!")); - - int16_t score = 0; - - QDateTime now = KStarsData::Instance()->lt(); - - /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ - // -1 = Job is not estimated yet // -2 = Job is estimated but time is unknown // > 0 Job is estimated and time is known @@ -1104,15 +1178,11 @@ { // #1.1 ASAP? case SchedulerJob::START_ASAP: - // If not light frames are required, run it now - if (job->getLightFramesRequired()) - score = calculateJobScore(job, now); - else - score = 1000; - - job->setScore(score); + { + /* Job is to be started as soon as possible, so check its current score */ + int16_t const score = calculateJobScore(job, now); - // If we can't start now, let's schedule it + /* If it's not possible to run the job now, find proper altitude time */ if (score < 0) { // If Altitude or Dark score are negative, we try to schedule a better time for altitude and dark sky period. @@ -1122,166 +1192,181 @@ //appendLogText(i18n("%1 observation job is scheduled at %2", job->getName(), job->getStartupTime().toString())); job->setState(SchedulerJob::JOB_SCHEDULED); // Since it's scheduled, we need to skip it now and re-check it later since its startup condition changed to START_AT - job->setScore(BAD_SCORE); - continue; + /*job->setScore(BAD_SCORE); + continue;*/ } else { job->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Ekos failed to schedule %1.", job->getName())); } + + /* Keep the job score for current time, score will refresh as scheduler progresses */ + /* score = calculateJobScore(job, job->getStartupTime()); */ + job->setScore(score); } + /* If it's possible to run the job now, check weather */ else if (isWeatherOK(job) == false) { - appendLogText(i18n("Job '%1' cannot run because of bad weather.", job->getName())); + appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName())); + job->setState(SchedulerJob::JOB_ABORTED); job->setScore(BAD_SCORE); } + /* If weather is ok, schedule the job to run now */ else { appendLogText(i18n("Job '%1' is due to run as soon as possible.", job->getName())); /* Give a proper start time, so that job can be rescheduled if others also start asap */ job->setStartupTime(now); + job->setState(SchedulerJob::JOB_SCHEDULED); + job->setScore(score); } - break; + } + break; // #1.2 Culmination? case SchedulerJob::START_CULMINATION: + { if (calculateCulmination(job)) { - appendLogText(i18n("Job '%1' is scheduled at %2", job->getName(), + appendLogText(i18n("Job '%1' is scheduled at %2 for culmination.", job->getName(), job->getStartupTime().toString())); job->setState(SchedulerJob::JOB_SCHEDULED); // Since it's scheduled, we need to skip it now and re-check it later since its startup condition changed to START_AT - job->setScore(BAD_SCORE); - continue; + /*job->setScore(BAD_SCORE); + continue;*/ } else + { + appendLogText(i18n("Job '%1' culmination cannot be scheduled, marking invalid.", job->getName())); job->setState(SchedulerJob::JOB_INVALID); - break; + } + } + break; // #1.3 Start at? case SchedulerJob::START_AT: { if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) { - if (job->getStartupTime().secsTo(job->getCompletionTime()) <= 0) + if (job->getCompletionTime() <= job->getStartupTime()) { appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3), marking invalid", job->getName(), job->getCompletionTime().toString(), job->getStartupTime().toString())); job->setState(SchedulerJob::JOB_INVALID); continue; } } - QDateTime startupTime = job->getStartupTime(); - int timeUntil = KStarsData::Instance()->lt().secsTo(startupTime); - // If starting time already passed by 5 minutes (default), we mark the job as invalid - /* FIXME: altitude calculation will change the job start condition to START_AT, so this might be a deadend while the end-user didn't request so */ + int const timeUntil = now.secsTo(job->getStartupTime()); + + // 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); - if (job->getState() == SchedulerJob::JOB_EVALUATION) + dms const passedUp(-timeUntil / 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) { - appendLogText(i18n("Job '%1' startup time is fixed, and is already passed by %2, marking invalid.", - job->getName(), passedUp.toHMSString())); + appendLogText(i18n("Job '%1' startup time was fixed at %2, and is already passed by %3, marking invalid.", + job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString())); job->setState(SchedulerJob::JOB_INVALID); } else { - appendLogText(i18n("Job '%1' startup time already passed by %2, marking aborted.", job->getName(), - passedUp.toHMSString())); + appendLogText(i18n("Job '%1' startup time was %2, and is already passed by %3, marking aborted.", + job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString())); job->setState(SchedulerJob::JOB_ABORTED); } continue; } // Start scoring once we reach startup time else if (timeUntil <= 0) { - /*score += getAltitudeScore(job, now); - score += getMoonSeparationScore(job, now); - score += getDarkSkyScore(now);*/ - score = calculateJobScore(job, now); + /* Consolidate altitude, moon separation and sky darkness scores */ + int16_t const score = calculateJobScore(job, now); if (score < 0) { /* If job score is already negative, silently abort the job to avoid spamming the user */ - if (job->getScore() < 0) - { - job->setState(SchedulerJob::JOB_ABORTED); - } - else + if (0 < job->getScore()) { if (job->getState() == SchedulerJob::JOB_EVALUATION) - { - appendLogText( - i18n("Job '%1' evaluation failed with a score of %2, marking aborted.", - job->getName(), score)); - job->setState(SchedulerJob::JOB_ABORTED); - } + appendLogText(i18n("Job '%1' evaluation failed with a score of %2, marking aborted.", + job->getName(), score)); + else if (timeUntil == 0) + appendLogText(i18n("Job '%1' updated score is %2 at startup time, marking aborted.", + job->getName(), score)); else - { - if (timeUntil == 0) - appendLogText(i18n( - "Job '%1' updated score is %2 at startup time, marking aborted.", job->getName(), score)); - else - appendLogText(i18n( - "Job '%1' updated score is %2 %3 seconds after startup time, marking aborted.", - job->getName(), score, abs(timeUntil))); - job->setState(SchedulerJob::JOB_ABORTED); - } + appendLogText(i18n("Job '%1' updated score is %2 %3 seconds after startup time, marking aborted.", + job->getName(), score, abs(timeUntil))); } + job->setState(SchedulerJob::JOB_ABORTED); + job->setScore(score); + continue; } - // If job is already scheduled, we check the weather, and if it is not OK, we set bad score until weather improves. + /* Positive score means job is already scheduled, so we check the weather, and if it is not OK, we set bad score until weather improves. */ else if (isWeatherOK(job) == false) - score += BAD_SCORE; + { + appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName())); + job->setState(SchedulerJob::JOB_ABORTED); + job->setScore(BAD_SCORE); + } + /* Else record current score */ + else job->setScore(score); } +#if 0 // If it is in the future and originally was designated as ASAP job // Job must be less than 12 hours away to be considered for re-evaluation else if (timeUntil > (Options::leadTime() * 60) && (timeUntil < 12 * 3600) && job->getFileStartupCondition() == SchedulerJob::START_ASAP) { QDateTime nextJobTime = now.addSecs(Options::leadTime() * 60); if (job->getEnforceTwilight() == false || (now > duskDateTime && now < preDawnDateTime)) { - appendLogText(i18n("Job '%1' is imminent, scheduled to run at %2.", + appendLogText(i18n("Job '%1' can be scheduled under 12 hours, but will be re-evaluated at %2.", job->getName(), nextJobTime.toString(job->getDateTimeDisplayFormat()))); job->setStartupTime(nextJobTime); } - score += BAD_SCORE; + job->setScore(BAD_SCORE); } // If time is far in the future, we make the score negative else { if (job->getState() == SchedulerJob::JOB_EVALUATION && calculateJobScore(job, job->getStartupTime()) < 0) { - appendLogText(i18n("Job '%1' evaluation failed with a score of %2, marking aborted.", - job->getName(), score)); + appendLogText(i18n("Job '%1' can only be scheduled in more than 12 hours, marking aborted.", + job->getName())); job->setState(SchedulerJob::JOB_ABORTED); continue; } - score += BAD_SCORE; + /*score += BAD_SCORE;*/ } +#endif - job->setScore(score); } break; } - // appendLogText(i18n("Job total score is %1", score)); //if (score > 0 && job->getState() == SchedulerJob::JOB_EVALUATION) if (job->getState() == SchedulerJob::JOB_EVALUATION) job->setState(SchedulerJob::JOB_SCHEDULED); } + /* + * At this step, we scheduled all jobs that had to be scheduled because they could not start as soon as possible. + * Now we check the amount of jobs we have to run. + */ + int invalidJobs = 0, completedJobs = 0, abortedJobs = 0, upcomingJobs = 0; - // Find invalid jobs + /* Partition jobs into invalid/aborted/completed/upcoming jobs */ foreach (SchedulerJob *job, jobs) { switch (job->getState()) @@ -1344,25 +1429,32 @@ return; } - SchedulerJob *bestCandidate = nullptr; + /* + * At this step, we still have jobs to run. + * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. + */ updatePreDawn(); - QList sortedJobs = jobs; - - sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job) - { return job->getState() > SchedulerJob::JOB_SCHEDULED || job->getScore() < 0;}), sortedJobs.end()); - + /* If there jobs left to run in the filtered list, check reschedule */ if (sortedJobs.isEmpty()) + { + if (startupState == STARTUP_COMPLETE) + { + appendLogText(i18n("No jobs left in the scheduler queue, starting shutdown procedure...")); + // Let's start shutdown procedure + checkShutdownState(); + } + else + stop(); + return; + } - /* FIXME: refactor so all sorts are using the same predicates */ - /* FIXME: use std::sort as qSort is deprecated */ + /* Now that jobs are scheduled, possibly at the same time, reorder by altitude and priority again */ if (Options::sortSchedulerJobs()) { - // Order by altitude, greater altitude first qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingAltitudeOrder); - // Then by priority, lower priority value first qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); } @@ -1432,199 +1524,117 @@ return; } - // Find best score - /*foreach(SchedulerJob *job, jobs) - { - if (job->getState() != SchedulerJob::JOB_SCHEDULED) - continue; - - int jobScore = job->getScore(); - int jobPriority = job->getPriority(); - - if (jobPriority <= maxPriority) - { - maxPriority = jobPriority; + /* + * At this step, we finished evaluating jobs. + * We select the first job that has to be run, per schedule. + */ - if (jobScore > 0 && jobScore > maxScore) - { - maxScore = jobScore; - bestCandidate = job; - } - } - }*/ + // Sort again by schedule, sooner first, as some jobs may have shifted during the last step + qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingStartupTimeOrder); -#if 0 - /* FIXME: refactor so all sorts are using the same predicates */ - /* FIXME: use std::sort as qSort is deprecated */ - if (Options::sortSchedulerJobs()) - { - // Order by score score first - qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingScoreOrder); - // Then by priority - qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); + setCurrentJob(sortedJobs.first()); - foreach (SchedulerJob *job, sortedJobs) - { - if (job->getState() != SchedulerJob::JOB_SCHEDULED || job->getScore() <= 0) - continue; + int nextObservationTime = now.secsTo(currentJob->getStartupTime()); - bestCandidate = job; - break; - } - } - else - { - // Get the first job that can run. - for (SchedulerJob *job : sortedJobs) - { - if (job->getScore() > 0) - { - bestCandidate = job; - break; - } - } - } -#endif + /* Check if job can be processed right now */ + if (currentJob->getFileStartupCondition() == SchedulerJob::START_ASAP) + if( 0 < calculateJobScore(currentJob, now)) + nextObservationTime = 0; - /* FIXME: refactor so all sorts are using the same predicates */ - /* FIXME: use std::sort as qSort is deprecated */ - if (Options::sortSchedulerJobs()) - { - // Order by score first - qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingScoreOrder); - // Then by priority - qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); - } + appendLogText(i18n("Job '%1' is selected for next observation with priority #%2 and score %3.", + currentJob->getName(), currentJob->getPriority(), currentJob->getScore())); - // Get the first job that can run. - for (SchedulerJob *job : sortedJobs) + // If mount was previously parked awaiting job activation, we unpark it. + if (parkWaitState == PARKWAIT_PARKED) { - if (job->getScore() > 0) - { - bestCandidate = job; - break; - } + parkWaitState = PARKWAIT_UNPARK; + return; } - if (bestCandidate != nullptr) - { - // If mount was previously parked awaiting job activation, we unpark it. - if (parkWaitState == PARKWAIT_PARKED) - { - parkWaitState = PARKWAIT_UNPARK; - return; - } - - appendLogText(i18n("Job '%1' is selected for next observation with priority #%2 and score %3.", - bestCandidate->getName(), bestCandidate->getPriority(), bestCandidate->getScore())); - - queueTable->selectRow(bestCandidate->getStartupCell()->row()); - setCurrentJob(bestCandidate); - } // 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. - else // if (startupState == STARTUP_COMPLETE) - { - int nextObservationTime = 1e6; - SchedulerJob *nextObservationJob = nullptr; - foreach (SchedulerJob *job, sortedJobs) + // 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, SIGNAL(timeout()), this, SLOT(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()) { - if (job->getState() != SchedulerJob::JOB_SCHEDULED || job->getStartupCondition() != SchedulerJob::START_AT) - continue; - - int timeLeft = KStarsData::Instance()->lt().secsTo(job->getStartupTime()); - - if (timeLeft > 0 && timeLeft < nextObservationTime) - { - nextObservationTime = timeLeft; - nextObservationJob = job; - } + 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 (nextObservationJob) - { - // 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.", - nextObservationJob->getName(), nextObservationJob->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, SIGNAL(timeout()), this, SLOT(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 && - (nextObservationJob->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.", - nextObservationJob->getName(), nextObservationJob->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 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...", nextObservationJob->getName(), - KStars::Instance()->data()->lt().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(); - } + 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...", nextObservationJob->getName(), - KStars::Instance()->data()->lt().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)); - //connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler())); - sleepTimer.start(); - } - } + // 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, SIGNAL(timeout()), this, SLOT(wakeUpScheduler())); + sleepTimer.start(); } } } @@ -1676,66 +1686,76 @@ bool Scheduler::calculateAltitudeTime(SchedulerJob *job, double minAltitude, double minMoonAngle) { // We wouldn't stat observation 30 mins (default) before dawn. - double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); - double altitude = 0; - QDateTime lt(KStarsData::Instance()->lt().date(), QTime()); - KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(lt)); + double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); + /* Compute UTC for beginning of today */ + QDateTime const lt(KStarsData::Instance()->lt().date(), QTime()); + KStarsDateTime const ut = geo->LTtoUT(KStarsDateTime(lt)); + + /* Retrieve target coordinates to be converted to horizontal to determine altitude */ SkyPoint target = job->getTargetCoords(); - QTime now = KStarsData::Instance()->lt().time(); - double fraction = now.hour() + now.minute() / 60.0 + now.second() / 3600; + /* Retrieve the current fraction of the day */ + QTime const now = KStarsData::Instance()->lt().time(); + double const fraction = now.hour() + now.minute() / 60.0 + now.second() / 3600; /* This attempts to locate the first minute of the next 24 hours when the job target matches the altitude and moon constraints */ for (double hour = fraction; hour < (fraction + 24); hour += 1.0 / 60.0) { - double rawFrac = 0; - KStarsDateTime myUT = ut.addSecs(hour * 3600.0); - - rawFrac = (hour > 24 ? (hour - 24) : hour) / 24.0; + double const rawFrac = (hour > 24 ? (hour - 24) : hour) / 24.0; /* Test twilight enforcement, and if enforced, bail out if start time is during day */ - if (!job->getEnforceTwilight() || rawFrac < Dawn || rawFrac > Dusk) + /* FIXME: rework day fraction loop to shift to dusk directly */ + if (job->getEnforceTwilight() && Dawn <= rawFrac && rawFrac <= Dusk) + continue; + + /* Compute altitude of target for the current fraction of the day */ + KStarsDateTime const myUT = ut.addSecs(hour * 3600.0); + CachingDms const LST = geo->GSTtoLST(myUT.gst()); + target.EquatorialToHorizontal(&LST, geo->lat()); + double const altitude = target.alt().Degrees(); + + if (altitude > minAltitude) { - CachingDms LST = geo->GSTtoLST(myUT.gst()); - target.EquatorialToHorizontal(&LST, geo->lat()); - altitude = target.alt().Degrees(); + QDateTime const startTime = geo->UTtoLT(myUT); - if (altitude > minAltitude) + /* Test twilight enforcement, and if enforced, bail out if start time is too close to dawn */ + if (job->getEnforceTwilight() && earlyDawn < rawFrac && rawFrac < Dawn) { - QDateTime startTime = geo->UTtoLT(myUT); - - /* Test twilight enforcement, and if enforced, bail out if start time is too close to dawn */ - if (job->getEnforceTwilight() && rawFrac > earlyDawn && rawFrac < Dawn) - { - appendLogText(i18n("Job '%1' reaches an altitude of %2 degrees at %3 but will not be scheduled due to " - "close proximity to astronomical twilight rise.", - job->getName(), QString::number(minAltitude, 'g', 3), startTime.toString(job->getDateTimeDisplayFormat()))); - return false; - } + appendLogText(i18n("Job '%1' reaches an altitude of %2 degrees at %3 but will not be scheduled due to " + "close proximity to astronomical twilight rise.", + job->getName(), QString::number(minAltitude, 'g', 3), startTime.toString(job->getDateTimeDisplayFormat()))); + return false; + } - if (minMoonAngle > 0 && getMoonSeparationScore(job, startTime) < 0) - continue; + /* Continue searching if Moon separation is not good enough */ + if (minMoonAngle > 0 && getMoonSeparationScore(job, startTime) < 0) + continue; - /* FIXME: the name of the function doesn't suggest the job can be modified */ - job->setStartupTime(startTime); - /* Kept the informative log because of the reschedule of aborted jobs */ - appendLogText(i18n("Job '%1' is scheduled to start at %2 where its altitude is %3 degrees.", job->getName(), - startTime.toString(job->getDateTimeDisplayFormat()), QString::number(altitude, 'g', 3))); - return true; - } + /* FIXME: the name of the function doesn't suggest the job can be modified */ + job->setStartupTime(startTime); + /* Kept the informative log because of the reschedule of aborted jobs */ + appendLogText(i18n("Job '%1' is scheduled to start at %2 where its altitude is %3 degrees.", job->getName(), + startTime.toString(job->getDateTimeDisplayFormat()), QString::number(altitude, 'g', 3))); + return true; } } /* FIXME: move this to the caller too to comment the decision to reject the job */ if (minMoonAngle == -1) - appendLogText(i18n("Job '%1' cannot rise above minimum altitude of %2 degrees in the next 24 hours, marking invalid.", job->getName(), - QString::number(minAltitude, 'g', 3))); - else - appendLogText(i18n("Job '%1' cannot rise above minimum altitude of %2 degrees with minimum moon " - "separation of %3 degrees in the next 24 hours, marking invalid.", - job->getName(), QString::number(minAltitude, 'g', 3), - QString::number(minMoonAngle, 'g', 3))); + { + if (job->getEnforceTwilight()) + { + appendLogText(i18n("Job '%1' has no night time with an altitude above %2 degrees during the next 24 hours, marking invalid.", + job->getName(), QString::number(minAltitude, 'g', 3))); + } + else appendLogText(i18n("Job '%1' can't rise to an altitude above %2 degrees in the next 24 hours, marking invalid.", + job->getName(), QString::number(minAltitude, 'g', 3))); + } + else appendLogText(i18n("Job '%1' can't be scheduled with an altitude above %2 degrees with minimum moon " + "separation of %3 degrees in the next 24 hours, marking invalid.", + job->getName(), QString::number(minAltitude, 'g', 3), + QString::number(minMoonAngle, 'g', 3))); return false; } @@ -1920,16 +1940,24 @@ int16_t Scheduler::calculateJobScore(SchedulerJob *job, QDateTime when) { + /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ + if (!job->getLightFramesRequired()) + return 1000; + int16_t total = 0; - /* FIXME: as soon as one score is negative, it's a no-go and other scores are unneeded */ + /* As soon as one score is negative, it's a no-go and other scores are unneeded */ if (job->getEnforceTwilight()) total += getDarkSkyScore(when); - if (job->getStepPipeline() != SchedulerJob::USE_NONE) + + if (0 <= total && job->getStepPipeline() != SchedulerJob::USE_NONE) total += getAltitudeScore(job, when); - total += getMoonSeparationScore(job, when); + if (0 <= total) + total += getMoonSeparationScore(job, when); + + appendLogText(i18n("Job '%1' has a total score of %2", job->getName(), total)); return total; } @@ -1939,7 +1967,9 @@ double currentAlt = findAltitude(job->getTargetCoords(), when); if (currentAlt < 0) + { score = BAD_SCORE; + } // If minimum altitude is specified else if (job->getMinAltitude() > 0) { @@ -1973,14 +2003,18 @@ } // If it's below minimum hard altitude (15 degrees now), set score to 10% of altitude value else if (currentAlt < minAltitude->minimum()) + { score = currentAlt / 10.0; + } // If no minimum altitude, then adjust altitude score to account for current target altitude else + { score = (1.5 * pow(1.06, currentAlt)) - (minAltitude->minimum() / 10.0); + } /* Kept the informative log now that scores are displayed */ - appendLogText(i18n("Job '%1' target altitude is %3 degrees at %2, resulting in a score of %4.", job->getName(), when.toString(job->getDateTimeDisplayFormat()), - QString::number(currentAlt, 'g', 3), score)); + appendLogText(i18n("Job '%1' target altitude is %3 degrees at %2 (score %4).", job->getName(), when.toString(job->getDateTimeDisplayFormat()), + QString::number(currentAlt, 'g', 3), QString::asprintf("%+d", score))); return score; } @@ -2063,7 +2097,7 @@ score /= 5.0; /* Kept the informative log now that score is displayed */ - appendLogText(i18n("Job '%1' target is %3 degrees from Moon, resulting in a score of %2.", job->getName(), score, separation)); + appendLogText(i18n("Job '%1' target is %3 degrees from Moon (score %2).", job->getName(), QString::asprintf("%+d", score), separation)); return score; } @@ -3317,9 +3351,9 @@ dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); - loadScheduler(fileURL.toLocalFile()); - - evaluateJobs(); + /* Run a job idle evaluation after a successful load */ + if (loadScheduler(fileURL.toLocalFile())) + startJobEvaluation(); } bool Scheduler::loadScheduler(const QString &fileURL) @@ -3654,7 +3688,7 @@ else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) - outstream << "At" + outstream << "At" << endl; outstream << "" << endl; @@ -3876,7 +3910,10 @@ // We're done whether the job completed successfully or not. else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) { - currentJob->setState(SchedulerJob::JOB_COMPLETE); + /* Mark the job idle as well as all its duplicates for re-evaluation */ + foreach(SchedulerJob *a_job, jobs) + if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) + a_job->setState(SchedulerJob::JOB_IDLE); captureBatch = 0; // Stop Guiding if it was used stopGuiding(); @@ -3892,7 +3929,10 @@ // If we're done if (currentJob->getRepeatsRemaining() == 0) { - currentJob->setState(SchedulerJob::JOB_COMPLETE); + /* Mark the job idle as well as all its duplicates for re-evaluation */ + foreach(SchedulerJob *a_job, jobs) + if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) + a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); stopGuiding(); @@ -3908,17 +3948,24 @@ currentJob->setState(SchedulerJob::JOB_BUSY); - /* If we are guiding, continue capturing, else realign */ + /* If we are guiding, continue capturing */ if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } - else + /* If we are using alignment, realign */ + else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { currentJob->setStage(SchedulerJob::STAGE_ALIGNING); startAstrometry(); } + /* Else just slew back to target - no-op probably, but having only 'track' checked is an edge case */ + else + { + currentJob->setStage(SchedulerJob::STAGE_SLEWING); + startSlew(); + } appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.", "Job '%1' is repeating, #%2 batches remaining.", @@ -3942,7 +3989,10 @@ { if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { - currentJob->setState(SchedulerJob::JOB_COMPLETE); + /* Mark the job idle as well as all its duplicates for re-evaluation */ + foreach(SchedulerJob *a_job, jobs) + if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) + a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); stopGuiding(); captureBatch = 0; @@ -4136,8 +4186,6 @@ void Scheduler::updateCompletedJobsCount() { - /* QMap finishedFramesCount; see later FIXME in that function */ - /* Use a temporary map in order to limit the number of file searches */ QMap newFramesCount; @@ -4186,13 +4234,7 @@ } /* Count captures already stored */ - int const completed = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); - - /* FIXME: finishedFramesCount isn't documented, and is getting in the way of counting the amount of captures in the storage */ - newFramesCount[signature] += completed; /* - finishedFramesCount[signature]; */ - - /* if (oneJob->getState() == SchedulerJob::JOB_COMPLETE) - finishedFramesCount[signature] += oneSeqJob->getCount(); */ + newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); } } @@ -4262,11 +4304,15 @@ // If the previous job signature matches the current, reduce completion count to compare duplicates if (!signature.compare(prevJob->getLocalDir() + prevJob->getDirectoryPostfix())) { - appendLogText(i18n("%1 has a previous duplicate with %2 completed captures.", seqName, prevJob->getCount())); + appendLogText(i18n("%1 has a previous duplicate sequence requiring %2 captures.", seqName, prevJob->getCount())); completed -= prevJob->getCount(); } } + // Now completed count can be needlessly negative for this job, so clamp to zero + if (completed < 0) + completed = 0; + // Update the completion count for this signature if we still have captures to take QMap fMap = schedJob->getCapturedFramesMap(); fMap[signature] = completed < job->getCount() ? completed : job->getCount(); @@ -4285,7 +4331,7 @@ { if(areJobCapturesComplete) { - appendLogText(i18n("%1 completed its sequence of %2 light frames.", seqName, job->getCount())); + appendLogText(i18n("%1 completed its sequence of %2 light frames.", seqName, job->getCount()*schedJob->getRepeatsRequired())); } else { @@ -4316,7 +4362,7 @@ if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) totalImagingTime += fabs((job->getExposure() + job->getDelay()) * 1); else - totalImagingTime += fabs((job->getExposure() + job->getDelay()) * (job->getCount() - completed)); + totalImagingTime += fabs((job->getExposure() + job->getDelay()) * (job->getCount()*schedJob->getRepeatsRequired() - completed)); /* If we have light frames to process, add focus/dithering delay */ if (job->getFrameType() == FRAME_LIGHT) @@ -4437,7 +4483,7 @@ if (mountReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type()))); + appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type()))); status = Mount::PARKING_ERROR; } @@ -4477,7 +4523,7 @@ if (mountReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type()))); + appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type()))); status = Mount::PARKING_ERROR; } @@ -4554,7 +4600,7 @@ if (mountReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type()))); + appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type()))); status = Mount::PARKING_ERROR; } @@ -4568,7 +4614,7 @@ if (domeReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type()))); + appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type()))); status = Dome::PARKING_ERROR; } @@ -4594,7 +4640,7 @@ if (domeReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type()))); + appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type()))); status = Dome::PARKING_ERROR; } @@ -4623,7 +4669,7 @@ if (domeReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type()))); + appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type()))); status = Dome::PARKING_ERROR; } @@ -4691,7 +4737,7 @@ if (domeReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type()))); + appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type()))); status = Dome::PARKING_ERROR; } @@ -4705,7 +4751,7 @@ if (capReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type()))); + appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type()))); status = DustCap::PARKING_ERROR; } @@ -4731,7 +4777,7 @@ if (capReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type()))); + appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type()))); status = DustCap::PARKING_ERROR; } @@ -4760,7 +4806,7 @@ if (capReply.error().type() != QDBusError::NoError) { - appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type()))); + appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type()))); status = DustCap::PARKING_ERROR; } @@ -4822,15 +4868,12 @@ void Scheduler::startJobEvaluation() { - jobEvaluationOnly = true; - if (Dawn < 0) - calculateDawnDusk(); - // Reset ALL scheduler jobs to IDLE and re-evalute them all again for(SchedulerJob *job : jobs) job->reset(); // Now evaluate all pending jobs per the conditions set in each + jobEvaluationOnly = true; evaluateJobs(); } diff --git a/kstars/ekos/scheduler/schedulerjob.h b/kstars/ekos/scheduler/schedulerjob.h --- a/kstars/ekos/scheduler/schedulerjob.h +++ b/kstars/ekos/scheduler/schedulerjob.h @@ -26,14 +26,14 @@ /** @brief States of a SchedulerJob. */ typedef enum { - JOB_IDLE, - JOB_EVALUATION, - JOB_SCHEDULED, - JOB_BUSY, - JOB_ERROR, - JOB_ABORTED, - JOB_INVALID, - JOB_COMPLETE + JOB_IDLE, /**< Job was just created, and is not evaluated yet */ + JOB_EVALUATION, /**< Job is being evaluated */ + JOB_SCHEDULED, /**< Job was evaluated, and has a schedule */ + JOB_BUSY, /**< Job is being processed */ + JOB_ERROR, /**< Job encountered a fatal issue while processing, and must be reset manually */ + JOB_ABORTED, /**< Job encountered a transitory issue while processing, and will be rescheduled */ + JOB_INVALID, /**< Job has an incorrect configuration, and cannot proceed */ + JOB_COMPLETE /**< Job finished all required captures */ } JOBStatus; /** @brief Running stages of a SchedulerJob. */ @@ -147,8 +147,17 @@ void setDateTimeDisplayFormat(const QString &value); /** @} */ + /** @brief Original startup condition, as entered by the user. */ + /** @{ */ StartupCondition getFileStartupCondition() const { return fileStartupCondition; } void setFileStartupCondition(const StartupCondition &value); + /** @} */ + + /** @brief Original time at which the job must start, as entered by the user. */ + /** @{ */ + QDateTime getFileStartupTime() const { return fileStartupTime; } + void setFileStartupTime(const QDateTime &value); + /** @} */ /** @brief Whether this job requires re-focus while running its capture sequence. */ /** @{ */ @@ -180,7 +189,12 @@ void setNameCell(QTableWidgetItem *cell); /** @} */ - /** @brief Current state of the scheduler job. */ + /** @brief Current state of the scheduler job. + * Setting state to JOB_ABORTED automatically resets the startup characteristics. + * Setting state to JOB_INVALID automatically resets the startup characteristics and the duration estimation. + * @see SchedulerJob::setStartupCondition, SchedulerJob::setFileStartupCondition, SchedulerJob::setStartupTime + * and SchedulerJob::setFileStartupTime. + */ /** @{ */ JOBStatus getState() const { return state; } void setState(const JOBStatus &value); @@ -304,6 +318,13 @@ */ void reset(); + /** @brief Determining whether a SchedulerJob is a duplicate of another. + * @param a_job is the other SchedulerJob to test duplication against. + * @return True if objects are different, but name and sequence file are identical, else false. + * @fixme This is a weak comparison, but that's what the scheduler looks at to decide completion. + */ + bool isDuplicateOf(SchedulerJob const *a_job) const { return this != a_job && name == a_job->name && sequenceFile == a_job->sequenceFile; } + /** @brief Compare ::SchedulerJob instances based on score. This is a qSort predicate, deprecated in QT5. * @arg a, b are ::SchedulerJob instances to compare. * @return true if the score of b is lower than the score of a. @@ -325,6 +346,13 @@ */ static bool decreasingAltitudeOrder(SchedulerJob const *a, SchedulerJob const *b); + /** @brief Compare ::SchedulerJob instances based on startup time. This is a qSort predicate, deprecated in QT5. + * @arg a, b are ::SchedulerJob instances to compare. + * @return true if the startup time of a is sooner than the priority of b. + * @return false if the startup time of a is later than or equal to the priority of b. + */ + static bool increasingStartupTimeOrder(SchedulerJob const *a, SchedulerJob const *b); + private: QString name; SkyPoint targetCoords; @@ -338,6 +366,7 @@ int sequenceCount { 0 }; int completedCount { 0 }; + QDateTime fileStartupTime; QDateTime startupTime; QDateTime completionTime; diff --git a/kstars/ekos/scheduler/schedulerjob.cpp b/kstars/ekos/scheduler/schedulerjob.cpp --- a/kstars/ekos/scheduler/schedulerjob.cpp +++ b/kstars/ekos/scheduler/schedulerjob.cpp @@ -38,9 +38,12 @@ void SchedulerJob::setStartupTime(const QDateTime &value) { startupTime = value; + if (value.isValid()) startupCondition = START_AT; - updateJobCell(); + + /* Refresh estimated time - which update job cells */ + setEstimatedTime(estimatedTime); } void SchedulerJob::setSequenceFile(const QUrl &value) @@ -70,13 +73,26 @@ void SchedulerJob::setCompletionTime(const QDateTime &value) { - completionTime = value; - updateJobCell(); + /* If argument completion time is valid, automatically switch condition to FINISH_AT */ + if (value.isValid()) + { + setCompletionCondition(FINISH_AT); + completionTime = value; + } + /* If completion time is not valid, but startup time is, deduce completion from startup and duration */ + else if (startupTime.isValid()) + { + completionTime = startupTime.addSecs(estimatedTime); + } + + /* Refresh estimated time - which update job cells */ + setEstimatedTime(estimatedTime); } void SchedulerJob::setCompletionCondition(const CompletionCondition &value) { completionCondition = value; + updateJobCell(); } void SchedulerJob::setStepPipeline(const StepPipeline &value) @@ -92,6 +108,21 @@ if (JOB_ERROR == state) KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName())); + /* If job becomes invalid, automatically reset its startup characteristics, and force its duration to be reestimated */ + if (JOB_INVALID == value) + { + setStartupCondition(fileStartupCondition); + setStartupTime(fileStartupTime); + setEstimatedTime(-1); + } + + /* If job is aborted, automatically reset its startup characteristics */ + if (JOB_ABORTED == value) + { + setStartupCondition(fileStartupCondition); + /* setStartupTime(fileStartupTime); */ + } + updateJobCell(); } @@ -128,30 +159,54 @@ { statusCell = value; updateJobCell(); + if (statusCell) + statusCell->setToolTip(i18n("Current status of job '%1', managed by the Scheduler.\n" + "If invalid, the Scheduler was not able to find a proper observation time for the target.\n" + "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n" + "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats.", + name)); } void SchedulerJob::setStartupCell(QTableWidgetItem *value) { startupCell = value; updateJobCell(); + if (startupCell) + startupCell->setToolTip(i18n("Startup time for job '%1', as estimated by the Scheduler.\n" + "Fixed time from user or culmination time is marked with a chronometer symbol. ", + name)); } void SchedulerJob::setCompletionCell(QTableWidgetItem *value) { completionCell = value; updateJobCell(); + if (completionCell) + completionCell->setToolTip(i18n("Completion time for job '%1', as estimated by the Scheduler.\n" + "Can be specified by the user to limit duration of looping jobs.\n" + "Fixed time from user is marked with a chronometer symbol. ", + name)); } void SchedulerJob::setCaptureCountCell(QTableWidgetItem *value) { captureCountCell = value; updateJobCell(); + if (captureCountCell) + captureCountCell->setToolTip(i18n("Count of captures stored for job '%1', based on its sequence job.\n" + "This is a summary, additional specific frame types may be required to complete the job.", + name)); } void SchedulerJob::setScoreCell(QTableWidgetItem *value) { scoreCell = value; updateJobCell(); + if (scoreCell) + scoreCell->setToolTip(i18n("Current score for job '%1', from its altitude, moon separation and sky darkness.\n" + "Negative if adequate altitude is not achieved yet or if there is no proper observation time today.\n" + "The Scheduler will refresh scores when picking a new candidate job.", + name)); } void SchedulerJob::setDateTimeDisplayFormat(const QString &value) @@ -183,9 +238,34 @@ fileStartupCondition = value; } +void SchedulerJob::setFileStartupTime(const QDateTime &value) +{ + fileStartupTime = value; +} + void SchedulerJob::setEstimatedTime(const int64_t &value) { - estimatedTime = value; + /* If startup and completion times are fixed, estimated time cannot change */ + if (START_AT == startupCondition && FINISH_AT == completionCondition) + { + estimatedTime = startupTime.secsTo(completionTime); + } + /* If startup time is fixed but not completion time, estimated time adjusts completion time */ + else if (START_AT == startupCondition) + { + estimatedTime = value; + completionTime = startupTime.addSecs(value); + } + /* If completion time is fixed but not startup time, estimated time adjusts startup time */ + /* FIXME: adjusting startup time will probably not work, because jobs are scheduled from first available altitude */ + else if (FINISH_AT == completionCondition) + { + estimatedTime = value; + startupTime = completionTime.addSecs(-value); + } + /* Else estimated time is simply stored as is */ + else estimatedTime = value; + updateJobCell(); } @@ -208,6 +288,10 @@ { estimatedTimeCell = value; updateJobCell(); + if (estimatedTimeCell) + estimatedTimeCell->setToolTip(i18n("Duration job '%1' will take to complete when started, as estimated by the Scheduler.\n" + "Depends on the actions to be run, and the sequence job to be processed.", + name)); } void SchedulerJob::setLightFramesRequired(bool value) @@ -240,7 +324,6 @@ targetCoords.updateCoordsNow(KStarsData::Instance()->updateNum()); } -/* FIXME: unrelated to model, move this in the view */ void SchedulerJob::updateJobCell() { if (nameCell) @@ -276,7 +359,7 @@ if (stageCell || stageLabel) { - /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case shouldi ; also, not thread-safe */ + /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */ /* FIXME: this should work with a static initializer in C++11, but QT versions are touchy on this, and perhaps i18n can't be used? */ static QMap stageStrings; static QString stageStringUnknown; @@ -312,13 +395,73 @@ if (startupCell) { - startupCell->setText(startupTime.toString(dateTimeDisplayFormat)); + /* Display a startup time if job is running, scheduled to run or about to be re-scheduled */ + if (JOB_SCHEDULED == state || JOB_BUSY == state || JOB_ABORTED == state) switch (fileStartupCondition) + { + /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */ + case START_AT: + case START_CULMINATION: + startupCell->setText(startupTime.toString(dateTimeDisplayFormat)); + startupCell->setIcon(QIcon::fromTheme("chronometer")); + break; + + /* If the original condition is START_ASAP, startup time is informational */ + case START_ASAP: + startupCell->setText(startupTime.toString(dateTimeDisplayFormat)); + startupCell->setIcon(QIcon()); + break; + + /* Else do not display any startup time */ + default: + startupCell->setText(QString()); + startupCell->setIcon(QIcon()); + break; + } + /* Display a missed startup time if job is invalid */ + else if (JOB_INVALID == state && START_AT == fileStartupCondition) + { + startupCell->setText(startupTime.toString(dateTimeDisplayFormat)); + startupCell->setIcon(QIcon::fromTheme("chronometer")); + } + /* Else do not display any startup time */ + else + { + startupCell->setText(QString()); + startupCell->setIcon(QIcon()); + } + startupCell->tableWidget()->resizeColumnToContents(startupCell->column()); } if (completionCell) { - completionCell->setText(completionTime.toString(dateTimeDisplayFormat)); + /* Display a completion time if job is running, scheduled to run or about to be re-scheduled */ + if (JOB_SCHEDULED == state || JOB_BUSY == state || JOB_ABORTED == state) switch (completionCondition) + { + case FINISH_LOOP: + completionCell->setText(QString("-")); + completionCell->setIcon(QIcon()); + break; + + case FINISH_AT: + completionCell->setText(completionTime.toString(dateTimeDisplayFormat)); + completionCell->setIcon(QIcon::fromTheme("chronometer")); + break; + + case FINISH_SEQUENCE: + case FINISH_REPEAT: + default: + completionCell->setText(completionTime.toString(dateTimeDisplayFormat)); + completionCell->setIcon(QIcon()); + break; + } + /* Else do not display any completion time */ + else + { + completionCell->setText(QString()); + completionCell->setIcon(QIcon()); + } + completionCell->tableWidget()->resizeColumnToContents(completionCell->column()); } @@ -361,7 +504,7 @@ stage = STAGE_IDLE; estimatedTime = -1; startupCondition = fileStartupCondition; - startupTime = fileStartupCondition == START_AT ? startupTime : QDateTime(); + startupTime = fileStartupCondition == START_AT ? fileStartupTime : QDateTime(); /* No change to culmination offset */ repeatsRemaining = repeatsRequired; } @@ -381,3 +524,8 @@ return Ekos::Scheduler::findAltitude(job1->getTargetCoords(), job1->getStartupTime()) > Ekos::Scheduler::findAltitude(job2->getTargetCoords(), job2->getStartupTime()); } + +bool SchedulerJob::increasingStartupTimeOrder(SchedulerJob const *job1, SchedulerJob const *job2) +{ + return job1->getStartupTime() < job2->getStartupTime(); +}