diff --git a/Tests/scheduler/1x1s_Lum.esq b/Tests/scheduler/1x1s_Lum.esq new file mode 100644 index 000000000..3ce87e7ff --- /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 index 000000000..723dc61a7 --- /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 index 000000000..d95837685 --- /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 index 000000000..353de756a --- /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 index 000000000..1b5917d16 --- /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 index 000000000..bfaf51ae8 --- /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 index 000000000..e1c09f495 --- /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 index 000000000..cb077acd0 --- /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 index 000000000..dfc1bc4b4 --- /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 index c5ff53668..7ffed7820 100644 --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1,5453 +1,5496 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "scheduler.h" #include "ksalmanac.h" #include "ksnotification.h" #include "kstars.h" #include "kstarsdata.h" #include "ksutils.h" #include "mosaic.h" #include "Options.h" #include "scheduleradaptor.h" #include "schedulerjob.h" #include "skymapcomposite.h" #include "auxiliary/QProgressIndicator.h" #include "dialogs/finddialog.h" #include "ekos/ekosmanager.h" #include "ekos/capture/sequencejob.h" #include "skyobjects/starobject.h" #include #include #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 #define SETTING_ALTITUDE_CUTOFF 3 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 #define DEFAULT_MIN_MOON_SEPARATION 0 namespace Ekos { Scheduler::Scheduler() { setupUi(this); new SchedulerAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); dirPath = QUrl::fromLocalFile(QDir::homePath()); // Get current KStars time and set seconds to zero QDateTime currentDateTime = KStarsData::Instance()->lt(); QTime currentTime = currentDateTime.time(); currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); currentDateTime.setTime(currentTime); // Set initial time for startup and completion times startupTimeEdit->setDateTime(currentDateTime); completionTimeEdit->setDateTime(currentDateTime); // Set up DBus interfaces QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); focusInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Focus", "org.kde.kstars.Ekos.Focus", QDBusConnection::sessionBus(), this); captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); alignInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Align", "org.kde.kstars.Ekos.Align", QDBusConnection::sessionBus(), this); guideInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Guide", "org.kde.kstars.Ekos.Guide", QDBusConnection::sessionBus(), this); domeInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Dome", "org.kde.kstars.Ekos.Dome", QDBusConnection::sessionBus(), this); weatherInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Weather", "org.kde.kstars.Ekos.Weather", QDBusConnection::sessionBus(), this); capInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/DustCap", "org.kde.kstars.Ekos.DustCap", QDBusConnection::sessionBus(), this); moon = dynamic_cast(KStarsData::Instance()->skyComposite()->findByName("Moon")); sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler())); schedulerTimer.setInterval(UPDATE_PERIOD_MS); jobTimer.setInterval(UPDATE_PERIOD_MS); connect(&schedulerTimer, SIGNAL(timeout()), this, SLOT(checkStatus())); connect(&jobTimer, SIGNAL(timeout()), this, SLOT(checkJobStage())); pi = new QProgressIndicator(this); bottomLayout->addWidget(pi, 0, 0); geo = KStarsData::Instance()->geo(); raBox->setDegType(false); //RA box should be HMS-style addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Add observation job to list.")); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setToolTip(i18n("Remove observation job from list.")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); evaluateOnlyB->setIcon(QIcon::fromTheme("tools-wizard")); evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); loadSequenceB->setIcon(QIcon::fromTheme("document-open")); loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectShutdownScriptB->setIcon( QIcon::fromTheme("document-open")); selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSB->setIcon(QIcon::fromTheme("document-open")); selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); startupB->setIcon( QIcon::fromTheme("media-playback-start")); startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startupB, SIGNAL(clicked()), this, SLOT(runStartupProcedure())); connect(shutdownB, SIGNAL(clicked()), this, SLOT(runShutdownProcedure())); selectObjectB->setIcon(QIcon::fromTheme("edit-find")); connect(selectObjectB, SIGNAL(clicked()), this, SLOT(selectObject())); connect(selectFITSB, SIGNAL(clicked()), this, SLOT(selectFITS())); connect(loadSequenceB, SIGNAL(clicked()), this, SLOT(selectSequence())); connect(selectStartupScriptB, SIGNAL(clicked()), this, SLOT(selectStartupScript())); connect(selectShutdownScriptB, SIGNAL(clicked()), this, SLOT(selectShutdownScript())); connect(mosaicB, SIGNAL(clicked()), this, SLOT(startMosaicTool())); connect(addToQueueB, SIGNAL(clicked()), this, SLOT(addJob())); connect(removeFromQueueB, SIGNAL(clicked()), this, SLOT(removeJob())); connect(evaluateOnlyB, SIGNAL(clicked()), this, SLOT(startJobEvaluation())); connect(queueTable, SIGNAL(clicked(QModelIndex)), this, SLOT(loadJob(QModelIndex))); connect(queueTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(resetJobState(QModelIndex))); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startB, SIGNAL(clicked()), this, SLOT(toggleScheduler())); connect(pauseB, SIGNAL(clicked()), this, SLOT(pause())); connect(queueSaveAsB, SIGNAL(clicked()), this, SLOT(saveAs())); connect(queueSaveB, SIGNAL(clicked()), this, SLOT(save())); connect(queueLoadB, SIGNAL(clicked()), this, SLOT(load())); connect(twilightCheck, SIGNAL(toggled(bool)), this, SLOT(checkTwilightWarning(bool))); loadProfiles(); } QString Scheduler::getCurrentJobName() { return (currentJob != nullptr ? currentJob->getName() : ""); } void Scheduler::watchJobChanges(bool enable) { 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())); disconnect(stepsButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); disconnect(startupButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); disconnect(constraintButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); disconnect(completionButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty())); 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())); } } void Scheduler::appendLogText(const QString &text) { /* FIXME: user settings for log length */ int const max_log_count = 2000; if (logText.size() > max_log_count) logText.removeLast(); logText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_SCHEDULER) << text; emit newLog(); } void Scheduler::clearLog() { logText.clear(); emit newLog(); } void Scheduler::selectObject() { QPointer fd = new FindDialog(this); if (fd->exec() == QDialog::Accepted) { SkyObject *object = fd->targetObject(); addObject(object); } delete fd; } void Scheduler::addObject(SkyObject *object) { if (object != nullptr) { QString finalObjectName(object->name()); if (object->name() == "star") { StarObject *s = (StarObject *)object; if (s->getHDIndex() != 0) finalObjectName = QString("HD %1").arg(QString::number(s->getHDIndex())); } nameEdit->setText(finalObjectName); raBox->setText(object->ra0().toHMSString()); decBox->setText(object->dec0().toDMSString()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); } } void Scheduler::selectFITS() { fitsURL = QFileDialog::getOpenFileUrl(this, i18n("Select FITS Image"), dirPath, "FITS (*.fits *.fit)"); if (fitsURL.isEmpty()) return; dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); fitsEdit->setText(fitsURL.toLocalFile()); if (nameEdit->text().isEmpty()) nameEdit->setText(fitsURL.fileName()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); setDirty(); } void Scheduler::selectSequence() { sequenceURL = QFileDialog::getOpenFileUrl(this, i18n("Select Sequence Queue"), dirPath, i18n("Ekos Sequence Queue (*.esq)")); if (sequenceURL.isEmpty()) return; dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); sequenceEdit->setText(sequenceURL.toLocalFile()); // For object selection, all fields must be filled if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false) // For FITS selection, only the name and fits URL should be filled. || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false)) { addToQueueB->setEnabled(true); mosaicB->setEnabled(true); } setDirty(); } void Scheduler::selectStartupScript() { startupScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Startup Script"), dirPath, i18n("Script (*)")); if (startupScriptURL.isEmpty()) return; dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename)); mDirty = true; startupScript->setText(startupScriptURL.toLocalFile()); } void Scheduler::selectShutdownScript() { shutdownScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Shutdown Script"), dirPath, i18n("Script (*)")); if (shutdownScriptURL.isEmpty()) return; dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename)); mDirty = true; shutdownScript->setText(shutdownScriptURL.toLocalFile()); } void Scheduler::addJob() { if (jobUnderEdit >= 0) { resetJobEdit(); return; } //jobUnderEdit = false; saveJob(); + jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::saveJob() { if (state == SCHEDULER_RUNNIG) { appendLogText(i18n("You cannot add or modify a job while the scheduler is running.")); return; } watchJobChanges(false); /* Warn if appending a job after infinite repeat */ /* FIXME: alter looping job priorities so that they are rescheduled later */ foreach(SchedulerJob * job, jobs) 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.")); return; } if (sequenceEdit->text().isEmpty()) { appendLogText(i18n("Sequence file is required.")); return; } // Coordinates are required unless it is a FITS file if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) { appendLogText(i18n("Target coordinates are required.")); return; } // Create or Update a scheduler job SchedulerJob *job = nullptr; if (jobUnderEdit >= 0) job = jobs.at(queueTable->currentRow()); else job = new SchedulerJob(); job->setName(nameEdit->text()); job->setPriority(prioritySpin->value()); bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { if(jobUnderEdit < 0) delete job; appendLogText(i18n("RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { if(jobUnderEdit < 0) delete job; appendLogText(i18n("DEC value %1 is invalid.", decBox->text())); return; } job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); job->setFITSFile(fitsURL); // #1 Startup conditions if (asapConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_ASAP); } else if (culminationConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_CULMINATION); job->setCulminationOffset(culminationOffset->value()); } else { job->setStartupCondition(SchedulerJob::START_AT); job->setStartupTime(startupTimeEdit->dateTime()); } /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); + job->setFileStartupTime(job->getStartupTime()); + // #2 Constraints // Do we have minimum altitude constraint? if (altConstraintCheck->isChecked()) job->setMinAltitude(minAltitude->value()); else job->setMinAltitude(-1); // Do we have minimum moon separation constraint? if (moonSeparationCheck->isChecked()) job->setMinMoonSeparation(minMoonSeparation->value()); else job->setMinMoonSeparation(-1); // Check enforce weather constraints job->setEnforceWeather(weatherCheck->isChecked()); // 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()) { job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); } else if (repeatCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_REPEAT); job->setRepeatsRequired(repeatsSpin->value()); job->setRepeatsRemaining(repeatsSpin->value()); } else if (loopCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_LOOP); } else { job->setCompletionCondition(SchedulerJob::FINISH_AT); job->setCompletionTime(completionTimeEdit->dateTime()); } // Job steps job->setStepPipeline(SchedulerJob::USE_NONE); if (trackStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_TRACK)); if (focusStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_FOCUS)); if (alignStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_ALIGN)); if (guideStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_GUIDE)); // Add job to queue if it is new if (jobUnderEdit == -1) jobs.append(job); int currentRow = 0; if (jobUnderEdit == -1) { currentRow = queueTable->rowCount(); queueTable->insertRow(currentRow); } 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 */ job->setState(SchedulerJob::JOB_IDLE); job->setStage(SchedulerJob::STAGE_IDLE); job->setEstimatedTime(-1); QTableWidgetItem *nameCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_NAME) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_NAME, nameCell); nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setNameCell(nameCell); QTableWidgetItem *statusCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_STATUS) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_STATUS, statusCell); statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setStatusCell(statusCell); QTableWidgetItem *captureCount = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_CAPTURES) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_CAPTURES, captureCount); captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setCaptureCountCell(captureCount); QTableWidgetItem *scoreValue = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_SCORE) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_SCORE, scoreValue); scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setScoreCell(scoreValue); QTableWidgetItem *startupCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_STARTTIME) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_STARTTIME, startupCell); startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setStartupCell(startupCell); QTableWidgetItem *completionCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_ENDTIME) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_ENDTIME, completionCell); completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setCompletionCell(completionCell); QTableWidgetItem *estimatedTimeCell = (jobUnderEdit >= 0) ? queueTable->item(currentRow, (int)SCHEDCOL_DURATION) : new QTableWidgetItem(); if (jobUnderEdit == -1) queueTable->setItem(currentRow, (int)SCHEDCOL_DURATION, estimatedTimeCell); estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); job->setEstimatedTimeCell(estimatedTimeCell); if (queueTable->rowCount() > 0) { queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); startB->setEnabled(true); mDirty = true; } removeFromQueueB->setEnabled(true); if (jobUnderEdit == -1) { startB->setEnabled(true); evaluateOnlyB->setEnabled(true); } watchJobChanges(true); } void Scheduler::resetJobState(QModelIndex i) { if (state == SCHEDULER_RUNNIG) { appendLogText(i18n("You cannot reset a job while the scheduler is running.")); return; } SchedulerJob *job = jobs.at(i.row()); if (job == nullptr) return; job->setState(SchedulerJob::JOB_IDLE); job->setStage(SchedulerJob::STAGE_IDLE); job->setEstimatedTime(-1); appendLogText(i18n("Job '%1' status is reset.", job->getName())); } void Scheduler::loadJob(QModelIndex i) { if (jobUnderEdit == i.row()) return; if (state == SCHEDULER_RUNNIG) { appendLogText(i18n("Warning! You cannot add or modify a job while the scheduler is running.")); return; } SchedulerJob *job = jobs.at(i.row()); if (job == nullptr) return; watchJobChanges(false); //job->setState(SchedulerJob::JOB_IDLE); //job->setStage(SchedulerJob::STAGE_IDLE); nameEdit->setText(job->getName()); prioritySpin->setValue(job->getPriority()); raBox->setText(job->getTargetCoords().ra0().toHMSString()); decBox->setText(job->getTargetCoords().dec0().toDMSString()); if (job->getFITSFile().isEmpty() == false) { fitsEdit->setText(job->getFITSFile().toLocalFile()); fitsURL = job->getFITSFile(); } else { fitsEdit->clear(); fitsURL = QUrl(); } sequenceEdit->setText(job->getSequenceFile().toLocalFile()); sequenceURL = job->getSequenceFile(); trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); switch (job->getFileStartupCondition()) { case SchedulerJob::START_ASAP: asapConditionR->setChecked(true); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; case SchedulerJob::START_CULMINATION: culminationConditionR->setChecked(true); culminationOffset->setValue(job->getCulminationOffset()); break; case SchedulerJob::START_AT: startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(job->getStartupTime()); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; } if (job->getMinAltitude() >= 0) { altConstraintCheck->setChecked(true); minAltitude->setValue(job->getMinAltitude()); } else { altConstraintCheck->setChecked(false); minAltitude->setValue(DEFAULT_MIN_ALTITUDE); } if (job->getMinMoonSeparation() >= 0) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(job->getMinMoonSeparation()); } else { moonSeparationCheck->setChecked(false); minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION); } weatherCheck->setChecked(job->getEnforceWeather()); twilightCheck->blockSignals(true); twilightCheck->setChecked(job->getEnforceTwilight()); twilightCheck->blockSignals(false); switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: sequenceCompletionR->setChecked(true); break; 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: timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(job->getCompletionTime()); break; } appendLogText(i18n("Editing job #%1...", i.row() + 1)); addToQueueB->setIcon(QIcon::fromTheme("edit-undo")); addToQueueB->setStyleSheet("background-color:orange;}"); addToQueueB->setEnabled(true); startB->setEnabled(false); evaluateOnlyB->setEnabled(false); addToQueueB->setToolTip(i18n("Exit edit mode")); jobUnderEdit = i.row(); watchJobChanges(true); } void Scheduler::resetJobEdit() { if (jobUnderEdit == -1) return; /* appendLogText(i18n("Edit mode cancelled.")); */ jobUnderEdit = -1; watchJobChanges(false); addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setStyleSheet(QString()); addToQueueB->setToolTip(i18n("Add observation job to list.")); queueTable->clearSelection(); evaluateOnlyB->setEnabled(true); startB->setEnabled(true); //removeFromQueueB->setToolTip(i18n("Remove observation job from list.")); + jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::removeJob() { /*if (jobUnderEdit) { resetJobEdit(); return; }*/ int currentRow = queueTable->currentRow(); if (currentRow < 0) { currentRow = queueTable->rowCount() - 1; if (currentRow < 0) return; } queueTable->removeRow(currentRow); queueTable->resizeColumnsToContents(); SchedulerJob *job = jobs.at(currentRow); jobs.removeOne(job); delete (job); if (queueTable->rowCount() == 0) { removeFromQueueB->setEnabled(false); evaluateOnlyB->setEnabled(false); } queueTable->selectRow(queueTable->currentRow()); if (queueTable->rowCount() == 0) { queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); startB->setEnabled(false); pauseB->setEnabled(false); if (jobUnderEdit >= 0) resetJobEdit(); } else loadJob(queueTable->currentIndex()); mDirty = true; } void Scheduler::toggleScheduler() { if (state == SCHEDULER_RUNNIG) { preemptiveShutdown = false; stop(); } else start(); } void Scheduler::stop() { if (state != SCHEDULER_RUNNIG) return; qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopped."; // Stop running job and abort all others // in case of soft shutdown we skip this if (preemptiveShutdown == false) { bool wasAborted = false; foreach (SchedulerJob *job, jobs) { if (job == currentJob) { stopCurrentJobAction(); stopGuiding(); } 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; } } if (wasAborted) KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted.")); } schedulerTimer.stop(); jobTimer.stop(); state = SCHEDULER_IDLE; ekosState = EKOS_IDLE; indiState = INDI_IDLE; parkWaitState = PARKWAIT_IDLE; // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete. // Or if we're doing a soft shutdown if (startupState != STARTUP_COMPLETE || preemptiveShutdown) { if (startupState == STARTUP_SCRIPT) { scriptProcess.disconnect(); scriptProcess.terminate(); } startupState = STARTUP_IDLE; } // Reset startup state to unparking phase (dome -> mount -> cap) // We do not want to run the startup script again but unparking should be checked // whenever the scheduler is running again. else if (startupState == STARTUP_COMPLETE) { if (unparkDomeCheck->isChecked()) startupState = STARTUP_UNPARKING_DOME; else if (unparkMountCheck->isChecked()) startupState = STARTUP_UNPARKING_MOUNT; else if (uncapCheck->isChecked()) startupState = STARTUP_UNPARKING_CAP; } shutdownState = SHUTDOWN_IDLE; setCurrentJob(nullptr); captureBatch = 0; indiConnectFailureCount = 0; focusFailureCount = 0; guideFailureCount = 0; alignFailureCount = 0; captureFailureCount = 0; jobEvaluationOnly = false; loadAndSlewProgress = false; autofocusCompleted = false; startupB->setEnabled(true); shutdownB->setEnabled(true); // If soft shutdown, we return for now if (preemptiveShutdown) { sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready")); sleepLabel->show(); return; } // Clear target name in capture interface upon stopping captureInterface->call(QDBus::AutoDetect, "setTargetName", QString()); if (scriptProcess.state() == QProcess::Running) scriptProcess.terminate(); sleepTimer.stop(); //sleepTimer.disconnect(); sleepLabel->hide(); pi->stopAnimation(); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Scheduler")); pauseB->setEnabled(false); //startB->setText("Start Scheduler"); queueLoadB->setEnabled(true); addToQueueB->setEnabled(true); removeFromQueueB->setEnabled(true); mosaicB->setEnabled(true); evaluateOnlyB->setEnabled(true); } void Scheduler::start() { if (state == SCHEDULER_RUNNIG) return; else if (state == SCHEDULER_PAUSED) { state = SCHEDULER_RUNNIG; appendLogText(i18n("Scheduler resumed.")); startB->setIcon( QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); return; } startupScriptURL = QUrl::fromUserInput(startupScript->text()); if (startupScript->text().isEmpty() == false && startupScriptURL.isValid() == false) { appendLogText(i18n("Startup script URL %1 is not valid.", startupScript->text())); return; } shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); if (shutdownScript->text().isEmpty() == false && shutdownScriptURL.isValid() == false) { appendLogText(i18n("Shutdown script URL %1 is not valid.", shutdownScript->text())); return; } qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting..."; pi->startAnimation(); sleepLabel->hide(); //startB->setText("Stop Scheduler"); startB->setIcon(QIcon::fromTheme("media-playback-stop")); 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; setCurrentJob(nullptr); jobEvaluationOnly = false; /* Reset all aborted jobs when starting the Scheduler. * When the Scheduler is stopped manually, all scheduled and running jobs do abort. * This snippet essentially has the same effect as double-clicking all aborted jobs before restarting. */ foreach (SchedulerJob *job, jobs) if (job->getState() == SchedulerJob::JOB_ABORTED) job->reset(); queueLoadB->setEnabled(false); addToQueueB->setEnabled(false); removeFromQueueB->setEnabled(false); mosaicB->setEnabled(false); evaluateOnlyB->setEnabled(false); startupB->setEnabled(false); shutdownB->setEnabled(false); schedulerTimer.start(); } void Scheduler::pause() { state = SCHEDULER_PAUSED; appendLogText(i18n("Scheduler paused.")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Scheduler")); } void Scheduler::setCurrentJob(SchedulerJob *job) { /* Reset job widgets */ if (currentJob) { currentJob->setStageLabel(nullptr); } /* Set current job */ currentJob = job; /* Reassign job widgets, or reset to defaults */ 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) { appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); job->setState(SchedulerJob::JOB_INVALID); continue; } } - // 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 if (job->getEstimatedTime() == -1) { if (estimateJobTime(job) == false) { job->setState(SchedulerJob::JOB_INVALID); continue; } } if (job->getEstimatedTime() == 0) { job->setState(SchedulerJob::JOB_COMPLETE); continue; } // #1 Check startup conditions switch (job->getStartupCondition()) { // #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. if (calculateAltitudeTime(job, job->getMinAltitude() > 0 ? job->getMinAltitude() : 0, job->getMinMoonSeparation())) { //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()) { case SchedulerJob::JOB_INVALID: invalidJobs++; break; case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: abortedJobs++; break; case SchedulerJob::JOB_COMPLETE: completedJobs++; break; case SchedulerJob::JOB_SCHEDULED: case SchedulerJob::JOB_BUSY: upcomingJobs++; break; default: break; } } if (upcomingJobs == 0) { if (jobEvaluationOnly == false) { if (invalidJobs == jobs.count()) { appendLogText(i18n("No valid jobs found, aborting schedule...")); stop(); return; } if (invalidJobs > 0) appendLogText(i18np("%1 job is invalid.", "%1 jobs are invalid.", invalidJobs)); if (abortedJobs > 0) appendLogText(i18np("%1 job aborted.", "%1 jobs aborted", abortedJobs)); if (completedJobs > 0) appendLogText(i18np("%1 job completed.", "%1 jobs completed.", completedJobs)); if (startupState == STARTUP_COMPLETE) { appendLogText(i18n("Scheduler complete. Starting shutdown procedure...")); // Let's start shutdown procedure checkShutdownState(); } else stop(); return; } else if (jobs.isEmpty()) 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); } // Our first job now takes priority over ALL others. // So if any other jobs conflicts with ours, we re-schedule that job to another time. SchedulerJob *firstJob = sortedJobs.first(); QDateTime firstStartTime = firstJob->getStartupTime(); QDateTime lastStartTime = firstJob->getStartupTime(); double lastJobEstimatedTime = firstJob->getEstimatedTime(); int daysCount = 0; // Make sure no two jobs have the same scheduled time or overlap with other jobs foreach (SchedulerJob *job, sortedJobs) { // If this job is not scheduled, continue // If this job startup conditon is not to start at a specific time, continue if (job == firstJob || job->getState() != SchedulerJob::JOB_SCHEDULED || job->getStartupCondition() != SchedulerJob::START_AT) continue; double timeBetweenJobs = (double)std::abs(firstStartTime.secsTo(job->getStartupTime())); // If there are within 5 minutes of each other, try to advance scheduling time of the lower altitude one if (timeBetweenJobs < (Options::leadTime()) * 60) { double delayJob = timeBetweenJobs + lastJobEstimatedTime; if (delayJob < (Options::leadTime() * 60)) delayJob = Options::leadTime() * 60; QDateTime otherjob_time = lastStartTime.addSecs(delayJob); QDateTime nextPreDawnTime = preDawnDateTime.addDays(daysCount); // If other jobs starts after pre-dawn limit, then we schedule it to the next day. // But we only take this action IF the job we are checking against starts _before_ dawn and our // job therefore carry us after down, then there is an actual need to schedule it next day. // FIXME: After changing time we are not evaluating job again when we should. if (job->getEnforceTwilight() && lastStartTime < nextPreDawnTime && otherjob_time >= nextPreDawnTime) { QDateTime date; daysCount++; lastStartTime = job->getStartupTime().addDays(daysCount); job->setStartupTime(lastStartTime); date = lastStartTime.addSecs(delayJob); } else { lastStartTime = lastStartTime.addSecs(delayJob); job->setStartupTime(lastStartTime); } job->setState(SchedulerJob::JOB_SCHEDULED); /* Kept the informative log now that aborted jobs are rescheduled */ appendLogText(i18n("Jobs '%1' and '%2' have close start up times, job '%2' is rescheduled to %3.", firstJob->getName(), job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); } lastJobEstimatedTime = job->getEstimatedTime(); } if (jobEvaluationOnly) { appendLogText(i18n("Job evaluation complete.")); jobEvaluationOnly = false; 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(); } } } void Scheduler::wakeUpScheduler() { sleepLabel->hide(); sleepTimer.stop(); if (preemptiveShutdown) { preemptiveShutdown = false; appendLogText(i18n("Scheduler is awake.")); start(); } else { if (state == SCHEDULER_RUNNIG) appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); else appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); schedulerTimer.start(); } } double Scheduler::findAltitude(const SkyPoint &target, const QDateTime &when) { // Make a copy /*SkyPoint p = target; QDateTime lt(when.date(), QTime()); KStarsDateTime ut = KStarsData::Instance()->geo()->LTtoUT(KStarsDateTime(lt)); KStarsDateTime myUT = ut.addSecs(when.time().msecsSinceStartOfDay() / 1000); CachingDms LST = KStarsData::Instance()->geo()->GSTtoLST(myUT.gst()); p.EquatorialToHorizontal(&LST, KStarsData::Instance()->geo()->lat()); return p.alt().Degrees();*/ SkyPoint p = target; KStarsDateTime lt(when); CachingDms LST = KStarsData::Instance()->geo()->GSTtoLST(lt.gst()); p.EquatorialToHorizontal(&LST, KStarsData::Instance()->geo()->lat()); return p.alt().Degrees(); } 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; } bool Scheduler::calculateCulmination(SchedulerJob *job) { SkyPoint target = job->getTargetCoords(); SkyObject o; o.setRA0(target.ra0()); o.setDec0(target.dec0()); o.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); QDateTime midnight(KStarsData::Instance()->lt().date(), QTime()); KStarsDateTime dt = geo->LTtoUT(KStarsDateTime(midnight)); QTime transitTime = o.transitTime(dt, geo); appendLogText(i18n("%1 Transit time is %2", job->getName(), transitTime.toString(job->getDateTimeDisplayFormat()))); int dayOffset = 0; if (KStarsData::Instance()->lt().time() > transitTime) dayOffset = 1; QDateTime observationDateTime(QDate::currentDate().addDays(dayOffset), transitTime.addSecs(job->getCulminationOffset() * 60)); appendLogText(i18np("%1 Observation time is %2 adjusted for %3 minute.", "%1 Observation time is %2 adjusted for %3 minutes.", job->getName(), observationDateTime.toString(job->getDateTimeDisplayFormat()), job->getCulminationOffset())); if (job->getEnforceTwilight() && getDarkSkyScore(observationDateTime) < 0) { appendLogText(i18n("%1 culminates during the day and cannot be scheduled for observation.", job->getName())); return false; } if (observationDateTime < (static_cast(KStarsData::Instance()->lt()))) { appendLogText(i18n("Observation time for %1 already passed.", job->getName())); return false; } job->setStartupTime(observationDateTime); return true; } void Scheduler::checkWeather() { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return; IPState newStatus; QString statusString; QDBusReply weatherReply = weatherInterface->call(QDBus::AutoDetect, "getWeatherStatus"); if (weatherReply.error().type() == QDBusError::NoError) { newStatus = (IPState)weatherReply.value(); switch (newStatus) { case IPS_OK: statusString = i18n("Weather conditions are OK."); break; case IPS_BUSY: statusString = i18n("Warning! Weather conditions are in the WARNING zone."); break; case IPS_ALERT: statusString = i18n("Caution! Weather conditions are in the DANGER zone!"); break; default: if (noWeatherCounter++ >= MAX_FAILURE_ATTEMPTS) { noWeatherCounter = 0; appendLogText(i18n("Warning: Ekos did not receive any weather updates for the last %1 minutes.", weatherTimer.interval() / (60000.0))); } break; } if (newStatus != weatherStatus) { weatherStatus = newStatus; qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; if (weatherStatus == IPS_OK) weatherLabel->setPixmap( QIcon::fromTheme("security-high") .pixmap(QSize(32, 32))); else if (weatherStatus == IPS_BUSY) { weatherLabel->setPixmap( QIcon::fromTheme("security-medium") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone")); } else if (weatherStatus == IPS_ALERT) { weatherLabel->setPixmap( QIcon::fromTheme("security-low") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherAlert"), i18n("Weather conditions are critical. Observatory shutdown is imminent")); } else weatherLabel->setPixmap(QIcon::fromTheme("chronometer") .pixmap(QSize(32, 32))); weatherLabel->show(); weatherLabel->setToolTip(statusString); appendLogText(statusString); emit weatherChanged(weatherStatus); } if (weatherStatus == IPS_ALERT) { appendLogText(i18n("Starting shutdown procedure due to severe weather.")); if (currentJob) { stopCurrentJobAction(); stopGuiding(); jobTimer.stop(); currentJob->setState(SchedulerJob::JOB_ABORTED); currentJob->setStage(SchedulerJob::STAGE_IDLE); } checkShutdownState(); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), Qt::UniqueConnection); } } } int16_t Scheduler::getWeatherScore() { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return 0; if (weatherStatus == IPS_BUSY) return BAD_SCORE / 2; else if (weatherStatus == IPS_ALERT) return BAD_SCORE; return 0; } int16_t Scheduler::getDarkSkyScore(const QDateTime &observationDateTime) { // if (job->getStartingCondition() == SchedulerJob::START_CULMINATION) // return -1000; int16_t score = 0; double dayFraction = 0; // Anything half an hour before dawn shouldn't be a good candidate double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); dayFraction = observationDateTime.time().msecsSinceStartOfDay() / (24.0 * 60.0 * 60.0 * 1000.0); // The farther the target from dawn, the better. if (dayFraction > earlyDawn && dayFraction < Dawn) score = BAD_SCORE / 50; else if (dayFraction < Dawn) score = (Dawn - dayFraction) * 100; else if (dayFraction > Dusk) { score = (dayFraction - Dusk) * 100; } else score = BAD_SCORE; qCDebug(KSTARS_EKOS_SCHEDULER) << "Dark sky score is" << score << "for time" << observationDateTime.toString(); return score; } 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; } int16_t Scheduler::getAltitudeScore(SchedulerJob *job, QDateTime when) { int16_t score = 0; double currentAlt = findAltitude(job->getTargetCoords(), when); if (currentAlt < 0) + { score = BAD_SCORE; + } // If minimum altitude is specified else if (job->getMinAltitude() > 0) { // if current altitude is lower that's not good if (currentAlt < job->getMinAltitude()) score = BAD_SCORE; else { // Get HA of actual object, and not of the mount as was done below double HA = KStars::Instance()->data()->lst()->Hours() - job->getTargetCoords().ra().Hours(); #if 0 if (indiState == INDI_READY) { QDBusReply haReply = mountInterface->call(QDBus::AutoDetect, "getHourAngle"); if (haReply.error().type() == QDBusError::NoError) HA = haReply.value(); } #endif // If already passed the merdian and setting we check if it is within setting alttidue cut off value (3 degrees default) // If it is within that value then it is useless to start the job which will end very soon so we better look for a better job. /* FIXME: don't use BAD_SCORE/2, a negative result implies the job has to be aborted - we'd be annoyed if that score became positive again */ /* FIXME: bug here, raising target will get a negative score if under cutoff, issue mitigated by aborted jobs getting rescheduled */ if (HA > 0 && (currentAlt - SETTING_ALTITUDE_CUTOFF) < job->getMinAltitude()) score = BAD_SCORE / 2.0; else // Otherwise, adjust score and add current altitude to score weight score = (1.5 * pow(1.06, currentAlt)) - (minAltitude->minimum() / 10.0); } } // 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; } double Scheduler::getCurrentMoonSeparation(SchedulerJob *job) { // Get target altitude given the time SkyPoint p = job->getTargetCoords(); QDateTime midnight(KStarsData::Instance()->lt().date(), QTime()); KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(midnight)); KStarsDateTime myUT = ut.addSecs(KStarsData::Instance()->lt().time().msecsSinceStartOfDay() / 1000); CachingDms LST = geo->GSTtoLST(myUT.gst()); p.EquatorialToHorizontal(&LST, geo->lat()); // Update moon ut = geo->LTtoUT(KStarsData::Instance()->lt()); KSNumbers ksnum(ut.djd()); LST = geo->GSTtoLST(ut.gst()); moon->updateCoords(&ksnum, true, geo->lat(), &LST, true); // Moon/Sky separation p return moon->angularDistanceTo(&p).Degrees(); } int16_t Scheduler::getMoonSeparationScore(SchedulerJob *job, QDateTime when) { int16_t score = 0; // Get target altitude given the time SkyPoint p = job->getTargetCoords(); QDateTime midnight(when.date(), QTime()); KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(midnight)); KStarsDateTime myUT = ut.addSecs(when.time().msecsSinceStartOfDay() / 1000); CachingDms LST = geo->GSTtoLST(myUT.gst()); p.EquatorialToHorizontal(&LST, geo->lat()); double currentAlt = p.alt().Degrees(); // Update moon ut = geo->LTtoUT(KStarsDateTime(when)); KSNumbers ksnum(ut.djd()); LST = geo->GSTtoLST(ut.gst()); moon->updateCoords(&ksnum, true, geo->lat(), &LST, true); double moonAltitude = moon->alt().Degrees(); // Lunar illumination % double illum = moon->illum() * 100.0; // Moon/Sky separation p double separation = moon->angularDistanceTo(&p).Degrees(); // Zenith distance of the moon double zMoon = (90 - moonAltitude); // Zenith distance of target double zTarget = (90 - currentAlt); // If target = Moon, or no illuminiation, or moon below horizon, return static score. if (zMoon == zTarget || illum == 0 || zMoon >= 90) score = 100; else { // JM: Some magic voodoo formula I came up with! double moonEffect = (pow(separation, 1.7) * pow(zMoon, 0.5)) / (pow(zTarget, 1.1) * pow(illum, 0.5)); // Limit to 0 to 100 range. moonEffect = KSUtils::clamp(moonEffect, 0.0, 100.0); if (job->getMinMoonSeparation() > 0) { if (separation < job->getMinMoonSeparation()) score = BAD_SCORE * 5; else score = moonEffect; } else score = moonEffect; } // Limit to 0 to 20 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; } void Scheduler::calculateDawnDusk() { KSAlmanac ksal; Dawn = ksal.getDawnAstronomicalTwilight(); Dusk = ksal.getDuskAstronomicalTwilight(); QTime now = KStarsData::Instance()->lt().time(); QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); QTime dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); duskDateTime.setDate(KStars::Instance()->data()->lt().date()); duskDateTime.setTime(dusk); appendLogText(i18n("Astronomical twilight rise is at %1, set is at %2, and current time is %3", dawn.toString(), dusk.toString(), now.toString())); } void Scheduler::executeJob(SchedulerJob *job) { if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { QString targetName = job->getName().replace(' ', ""); QList targetArgs; targetArgs.append(targetName); captureInterface->callWithArgumentList(QDBus::AutoDetect, "setTargetName", targetArgs); } setCurrentJob(job); qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName(); KNotification::event(QLatin1String("EkosSchedulerJobStart"), i18n("Ekos job started (%1)", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_BUSY); updatePreDawn(); // No need to continue evaluating jobs as we already have one. schedulerTimer.stop(); jobTimer.start(); } bool Scheduler::checkEkosState() { if (state == SCHEDULER_PAUSED) return false; switch (ekosState) { case EKOS_IDLE: { // Even if state is IDLE, check if Ekos is already started. If not, start it. QDBusReply isEkosStarted; isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (isEkosStarted.value() == EkosManager::EKOS_STATUS_SUCCESS) { ekosState = EKOS_READY; return true; } else { ekosInterface->call(QDBus::AutoDetect, "start"); ekosState = EKOS_STARTING; currentOperationTime.start(); return false; } } break; case EKOS_STARTING: { QDBusReply isEkosStarted; isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (isEkosStarted.value() == EkosManager::EKOS_STATUS_SUCCESS) { appendLogText(i18n("Ekos started.")); ekosState = EKOS_READY; return true; } else if (isEkosStarted.value() == EkosManager::EKOS_STATUS_ERROR) { appendLogText(i18n("Ekos failed to start.")); stop(); return false; } // If a minute passed, give up else if (currentOperationTime.elapsed() > (60 * 1000)) { appendLogText(i18n("Ekos timed out.")); stop(); return false; } } break; case EKOS_STOPPING: { QDBusReply isEkosStarted; isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (isEkosStarted.value() == EkosManager::EKOS_STATUS_IDLE) { appendLogText(i18n("Ekos stopped.")); ekosState = EKOS_IDLE; return true; } } break; case EKOS_READY: return true; break; } return false; } bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State..."; switch (indiState) { case INDI_IDLE: { // Even in idle state, we make sure that INDI is not already connected. QDBusReply isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); if (isINDIConnected.value() == EkosManager::EKOS_STATUS_SUCCESS) { indiState = INDI_PROPERTY_CHECK; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties..."; return false; } else { ekosInterface->call(QDBus::AutoDetect, "connectDevices"); indiState = INDI_CONNECTING; currentOperationTime.start(); qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI Devices"; return false; } } break; case INDI_CONNECTING: { QDBusReply isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); if (isINDIConnected.value() == EkosManager::EKOS_STATUS_SUCCESS) { appendLogText(i18n("INDI devices connected.")); indiState = INDI_PROPERTY_CHECK; return false; } else if (isINDIConnected.value() == EkosManager::EKOS_STATUS_ERROR) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); return false; } appendLogText(i18n("INDI devices failed to connect. Check INDI control panel for details.")); stop(); return false; } // If 30 seconds passed, we retry else if (currentOperationTime.elapsed() > (30 * 1000)) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); return false; } appendLogText(i18n("INDI devices connection timed out. Check INDI control panel for details.")); stop(); return false; } else return false; } break; case INDI_DISCONNECTING: { QDBusReply isINDIConnected = ekosInterface->call(QDBus::AutoDetect, "getINDIConnectionStatus"); if (isINDIConnected.value() == EkosManager::EKOS_STATUS_IDLE) { appendLogText(i18n("INDI devices disconnected.")); indiState = INDI_IDLE; return true; } } break; case INDI_PROPERTY_CHECK: { // Check if mount and dome support parking or not. QDBusReply boolReply = mountInterface->call(QDBus::AutoDetect, "canPark"); unparkMountCheck->setEnabled(boolReply.value()); parkMountCheck->setEnabled(boolReply.value()); //qDebug() << "Mount can park " << boolReply.value(); boolReply = domeInterface->call(QDBus::AutoDetect, "canPark"); unparkDomeCheck->setEnabled(boolReply.value()); parkDomeCheck->setEnabled(boolReply.value()); boolReply = captureInterface->call(QDBus::AutoDetect, "hasCoolerControl"); warmCCDCheck->setEnabled(boolReply.value()); QDBusReply updateReply = weatherInterface->call(QDBus::AutoDetect, "getUpdatePeriod"); if (updateReply.error().type() == QDBusError::NoError) { weatherCheck->setEnabled(true); if (updateReply.value() > 0) { weatherTimer.setInterval(updateReply.value() * 1000); connect(&weatherTimer, SIGNAL(timeout()), this, SLOT(checkWeather())); weatherTimer.start(); // Check weather initially checkWeather(); } } else weatherCheck->setEnabled(false); QDBusReply capReply = capInterface->call(QDBus::AutoDetect, "canPark"); if (capReply.error().type() == QDBusError::NoError) { capCheck->setEnabled(capReply.value()); uncapCheck->setEnabled(capReply.value()); } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } indiState = INDI_READY; return true; } break; case INDI_READY: return true; } return false; } bool Scheduler::checkStartupState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Startup State..."; switch (startupState) { case STARTUP_IDLE: { KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process..."; // If Ekos is already started, we skip the script and move on to dome unpark step // unless we do not have light frames, then we skip all QDBusReply isEkosStarted; isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (isEkosStarted.value() == EkosManager::EKOS_STATUS_SUCCESS) { if (startupScriptURL.isEmpty() == false) appendLogText(i18n("Ekos is already started, skipping startup script...")); if (currentJob->getLightFramesRequired()) startupState = STARTUP_UNPARK_DOME; else startupState = STARTUP_COMPLETE; return true; } if (schedulerProfileCombo->currentText() != i18n("Default")) { QList profile; profile.append(schedulerProfileCombo->currentText()); ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile); } if (startupScriptURL.isEmpty() == false) { startupState = STARTUP_SCRIPT; executeScript(startupScriptURL.toString(QUrl::PreferLocalFile)); return false; } startupState = STARTUP_UNPARK_DOME; return false; } break; case STARTUP_SCRIPT: return false; break; case STARTUP_UNPARK_DOME: // If there is no job in case of manual startup procedure, // or if the job requires light frames, let's proceed with // unparking the dome, otherwise startup process is complete. if (currentJob == nullptr || currentJob->getLightFramesRequired()) { if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked()) unParkDome(); else startupState = STARTUP_UNPARK_MOUNT; } else { startupState = STARTUP_COMPLETE; return true; } break; case STARTUP_UNPARKING_DOME: checkDomeParkingStatus(); break; case STARTUP_UNPARK_MOUNT: if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked()) unParkMount(); else startupState = STARTUP_UNPARK_CAP; break; case STARTUP_UNPARKING_MOUNT: checkMountParkingStatus(); break; case STARTUP_UNPARK_CAP: if (uncapCheck->isEnabled() && uncapCheck->isChecked()) unParkCap(); else startupState = STARTUP_COMPLETE; break; case STARTUP_UNPARKING_CAP: checkCapParkingStatus(); break; case STARTUP_COMPLETE: return true; case STARTUP_ERROR: stop(); return true; break; } return false; } bool Scheduler::checkShutdownState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutown state..."; switch (shutdownState) { case SHUTDOWN_IDLE: KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process")); qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process..."; weatherTimer.stop(); weatherTimer.disconnect(); weatherLabel->hide(); jobTimer.stop(); setCurrentJob(nullptr); if (state == SCHEDULER_RUNNIG) schedulerTimer.start(); if (preemptiveShutdown == false) { sleepTimer.stop(); //sleepTimer.disconnect(); } if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked()) { appendLogText(i18n("Warming up CCD...")); // Turn it off QVariant arg(false); captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg); } if (capCheck->isEnabled() && capCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_CAP; return false; } if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_MOUNT; return false; } if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_DOME; return false; } if (shutdownScriptURL.isEmpty() == false) { shutdownState = SHUTDOWN_SCRIPT; return false; } shutdownState = SHUTDOWN_COMPLETE; return true; break; case SHUTDOWN_PARK_CAP: if (capCheck->isEnabled() && capCheck->isChecked()) parkCap(); else shutdownState = SHUTDOWN_PARK_MOUNT; break; case SHUTDOWN_PARKING_CAP: checkCapParkingStatus(); break; case SHUTDOWN_PARK_MOUNT: if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) parkMount(); else shutdownState = SHUTDOWN_PARK_DOME; break; case SHUTDOWN_PARKING_MOUNT: checkMountParkingStatus(); break; case SHUTDOWN_PARK_DOME: if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) parkDome(); else shutdownState = SHUTDOWN_SCRIPT; break; case SHUTDOWN_PARKING_DOME: checkDomeParkingStatus(); break; case SHUTDOWN_SCRIPT: if (shutdownScriptURL.isEmpty() == false) { // Need to stop Ekos now before executing script if it happens to stop INDI if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI()) { stopEkos(); return false; } shutdownState = SHUTDOWN_SCRIPT_RUNNING; executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile)); } else shutdownState = SHUTDOWN_COMPLETE; break; case SHUTDOWN_SCRIPT_RUNNING: return false; case SHUTDOWN_COMPLETE: return true; case SHUTDOWN_ERROR: stop(); return true; break; } return false; } bool Scheduler::checkParkWaitState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State..."; switch (parkWaitState) { case PARKWAIT_IDLE: return true; case PARKWAIT_PARK: parkMount(); break; case PARKWAIT_PARKING: checkMountParkingStatus(); break; case PARKWAIT_PARKED: return true; case PARKWAIT_UNPARK: unParkMount(); break; case PARKWAIT_UNPARKING: checkMountParkingStatus(); break; case PARKWAIT_UNPARKED: return true; case PARKWAIT_ERROR: appendLogText(i18n("park/unpark wait procedure failed, aborting...")); stop(); return true; break; } return false; } void Scheduler::executeScript(const QString &filename) { appendLogText(i18n("Executing script %1 ...", filename)); connect(&scriptProcess, SIGNAL(readyReadStandardOutput()), this, SLOT(readProcessOutput())); connect(&scriptProcess, SIGNAL(finished(int)), this, SLOT(checkProcessExit(int))); scriptProcess.start(filename); } void Scheduler::readProcessOutput() { appendLogText(scriptProcess.readAllStandardOutput().simplified()); } void Scheduler::checkProcessExit(int exitCode) { scriptProcess.disconnect(); if (exitCode == 0) { if (startupState == STARTUP_SCRIPT) startupState = STARTUP_UNPARK_DOME; else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) shutdownState = SHUTDOWN_COMPLETE; return; } if (startupState == STARTUP_SCRIPT) { appendLogText(i18n("Startup script failed, aborting...")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) { appendLogText(i18n("Shutdown script failed, aborting...")); shutdownState = SHUTDOWN_ERROR; } } void Scheduler::checkStatus() { if (state == SCHEDULER_PAUSED) return; // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) { // #2.1 If shutdown is already complete or in error, we need to stop if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR) { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) return; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); return; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); return; } if (shutdownState == SHUTDOWN_COMPLETE) appendLogText(i18n("Shutdown complete.")); else appendLogText(i18n("Shutdown procedure failed, aborting...")); // Stop Scheduler stop(); return; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return; checkShutdownState(); return; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) return; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); } else { // #3 Check if startup procedure has failed. if (startupState == STARTUP_ERROR) { // Stop Scheduler stop(); return; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) return; // #5 Check if Ekos is started if (checkEkosState() == false) return; // #6 Check if INDI devices are connected. if (checkINDIState() == false) return; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) return; // #8 Execute the job executeJob(currentJob); } } void Scheduler::checkJobStage() { if (state == SCHEDULER_PAUSED) return; Q_ASSERT(currentJob != nullptr); // #1 Check if we need to stop at some point if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT && currentJob->getState() == SchedulerJob::JOB_BUSY) { // If the job reached it COMPLETION time, we stop it. if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); findNextJob(); return; } } // #2 Check if altitude restriction still holds true if (currentJob->getMinAltitude() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ if (p.alt().Degrees() < currentJob->getMinAltitude()) { // Only terminate job due to altitude limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " "marking aborted.", currentJob->getName(), p.alt().Degrees(), currentJob->getMinAltitude())); currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } } // #3 Check if moon separation is still valid if (currentJob->getMinMoonSeparation() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); double moonSeparation = getCurrentMoonSeparation(currentJob); if (moonSeparation < currentJob->getMinMoonSeparation()) { // Only terminate job due to moon separation limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 " "degrees), marking aborted.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } } // #4 Check if we're not at dawn if (currentJob->getEnforceTwilight() && KStarsData::Instance()->lt() > KStarsDateTime(preDawnDateTime)) { // If either mount or dome are not parked, we shutdown if we approach dawn if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false)) { // Minute is a DOUBLE value, do not use i18np 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); stopCurrentJobAction(); stopGuiding(); checkShutdownState(); //disconnect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkJobStage()), Qt::UniqueConnection); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), Qt::UniqueConnection); return; } } switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: getNextAction(); break; case SchedulerJob::STAGE_SLEWING: { QDBusReply slewStatus = mountInterface->call(QDBus::AutoDetect, "getSlewStatus"); bool isDomeMoving = false; if (parkDomeCheck->isEnabled()) { QDBusReply domeReply = domeInterface->call(QDBus::AutoDetect, "isMoving"); if (domeReply.error().type() == QDBusError::NoError && domeReply.value() == true) isDomeMoving = true; } if (slewStatus.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI while slewing, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); return; } qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing Stage... Slew Status is " << pstateStr(static_cast(slewStatus.value())); if (slewStatus.value() == IPS_OK && isDomeMoving == false) { appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE); getNextAction(); } else if (slewStatus.value() == IPS_ALERT) { appendLogText(i18n("Warning! Job '%1' slew failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (slewStatus.value() == IPS_IDLE) { appendLogText(i18n("Warning! Job '%1' found not slewing, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_FOCUSING: { QDBusReply focusReply = focusInterface->call(QDBus::AutoDetect, "getStatus"); if (focusReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI server while focusing, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); return; } qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus stage..."; Ekos::FocusState focusStatus = static_cast(focusReply.value()); // Is focus complete? if (focusStatus == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (focusStatus == Ekos::FOCUS_FAILED || focusStatus == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning! Job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning! Job '%1' focusing procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; /*case SchedulerJob::STAGE_POSTALIGN_FOCUSING: focusInterface->call(QDBus::AutoDetect,"resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); break;*/ case SchedulerJob::STAGE_ALIGNING: { QDBusReply alignReply; qCDebug(KSTARS_EKOS_SCHEDULER) << "Alignment stage..."; alignReply = alignInterface->call(QDBus::AutoDetect, "getStatus"); if (alignReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI server while aligning, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); return; } Ekos::AlignState alignStatus = static_cast(alignReply.value()); // Is solver complete? if (alignStatus == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (alignStatus == Ekos::ALIGN_FAILED || alignStatus == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning! Job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS-1 < alignFailureCount) { appendLogText(i18n("Warning! Job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning! Job '%1' alignment procedure failed, aborting job.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } break; case SchedulerJob::STAGE_RESLEWING: { QDBusReply slewStatus = mountInterface->call(QDBus::AutoDetect, "getSlewStatus"); bool isDomeMoving = false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage..."; if (parkDomeCheck->isEnabled()) { QDBusReply domeReply = domeInterface->call(QDBus::AutoDetect, "isMoving"); if (domeReply.error().type() == QDBusError::NoError && domeReply.value() == true) isDomeMoving = true; } if (slewStatus.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI server while reslewing, marking aborted.",currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); } else if (slewStatus.value() == IPS_OK && isDomeMoving == false) { appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE); getNextAction(); } else if (slewStatus.value() == IPS_ALERT) { appendLogText(i18n("Warning! Job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (slewStatus.value() == IPS_IDLE) { appendLogText(i18n("Warning! Job '%1' found not repositioning, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_GUIDING: { QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; if (guideReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI server while guiding, marking aborted.",currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); return; } Ekos::GuideState guideStatus = static_cast(guideReply.value()); // If calibration stage complete? if (guideStatus == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } else if (guideStatus == Ekos::GUIDE_CALIBRATION_ERROR || guideStatus == Ekos::GUIDE_ABORTED) { if (guideStatus == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning! Job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning! Job '%1' calibration failed.", currentJob->getName())); if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is guiding, and is restarting its guiding procedure.", currentJob->getName())); startGuiding(true); } else { appendLogText(i18n("Warning! Job '%1' guiding procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; case SchedulerJob::STAGE_CAPTURING: { QDBusReply captureReply = captureInterface->call(QDBus::AutoDetect, "getSequenceQueueStatus"); if (captureReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning! Job '%1' lost connection to INDI server while capturing, marking aborted.",currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); checkShutdownState(); } else if (captureReply.value().toStdString() == "Aborted" || captureReply.value().toStdString() == "Error") { appendLogText(i18n("Warning! Job '%1' failed to capture target (%2).", currentJob->getName(), captureReply.value())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); if (guideReply.value() == Ekos::GUIDE_ABORTED || guideReply.value() == Ekos::GUIDE_CALIBRATION_ERROR || guideReply.value() == GUIDE_DITHERING_ERROR) // If guiding failed, let's restart it //if(guideReply.value() == false) { appendLogText(i18n("Job '%1' is capturing, and is restarting its guiding procedure.", currentJob->getName())); //currentJob->setStage(SchedulerJob::STAGE_GUIDING); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning! Job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning! Job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (captureReply.value().toStdString() == "Complete") { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); /* Set evaluation state so that job is reevaluated for repeats or other things */ currentJob->setState(SchedulerJob::JOB_EVALUATION); captureInterface->call(QDBus::AutoDetect, "clearSequenceQueue"); findNextJob(); } else { captureFailureCount = 0; /* currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); */ } } break; default: break; } } void Scheduler::getNextAction() { qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action..."; switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: if (currentJob->getLightFramesRequired()) { if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) startSlew(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); } else { if (currentJob->getStepPipeline()) appendLogText( i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", currentJob->getName())); startCapture(); } break; case SchedulerJob::STAGE_SLEW_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_FOCUS_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_ALIGN_COMPLETE: currentJob->setStage(SchedulerJob::STAGE_RESLEWING); break; case SchedulerJob::STAGE_RESLEWING_COMPLETE: // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus // frame is ready for the capture module in-sequence-focus procedure. if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus()) // Post alignment re-focusing startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_GUIDING_COMPLETE: startCapture(); break; default: break; } } void Scheduler::stopCurrentJobAction() { if (currentJob) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: break; case SchedulerJob::STAGE_SLEWING: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_FOCUSING: focusInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_ALIGNING: alignInterface->call(QDBus::AutoDetect, "abort"); break; //case SchedulerJob::STAGE_CALIBRATING: // guideInterface->call(QDBus::AutoDetect,"stopCalibration"); // break; case SchedulerJob::STAGE_GUIDING: stopGuiding(); break; case SchedulerJob::STAGE_CAPTURING: captureInterface->call(QDBus::AutoDetect, "abort"); //stopGuiding(); break; default: break; } /* Reset interrupted job stage */ currentJob->setStage(SchedulerJob::STAGE_IDLE); } } void Scheduler::load() { QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18n("Open Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KMessageBox::sorry(0, message, i18n("Invalid URL")); return; } 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) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KMessageBox::sorry(0, message, i18n("Could Not Open File")); return false; } if (jobUnderEdit >= 0) resetJobEdit(); qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *tag = tagXMLEle(ep); if (!strcmp(tag, "Job")) processJobInfo(ep); else if (!strcmp(tag, "Profile")) { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; startupScript->clear(); unparkDomeCheck->setChecked(false); unparkMountCheck->setChecked(false); uncapCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "StartupScript")) { startupScript->setText(findXMLAttValu(procedure, "value")); startupScriptURL = QUrl::fromUserInput(startupScript->text()); } else if (!strcmp(proc, "UnparkDome")) unparkDomeCheck->setChecked(true); else if (!strcmp(proc, "UnparkMount")) unparkMountCheck->setChecked(true); else if (!strcmp(proc, "UnparkCap")) uncapCheck->setChecked(true); } } else if (!strcmp(tag, "ShutdownProcedure")) { XMLEle *procedure; shutdownScript->clear(); warmCCDCheck->setChecked(false); parkDomeCheck->setChecked(false); parkMountCheck->setChecked(false); capCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "ShutdownScript")) { shutdownScript->setText(findXMLAttValu(procedure, "value")); shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); } else if (!strcmp(proc, "ParkDome")) parkDomeCheck->setChecked(true); else if (!strcmp(proc, "ParkMount")) parkMountCheck->setChecked(true); else if (!strcmp(proc, "ParkCap")) capCheck->setChecked(true); else if (!strcmp(proc, "WarmCCD")) warmCCDCheck->setChecked(true); } } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); return false; } } schedulerURL = QUrl::fromLocalFile(fileURL); mosaicB->setEnabled(true); mDirty = false; delLilXML(xmlParser); return true; } bool Scheduler::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; altConstraintCheck->setChecked(false); moonSeparationCheck->setChecked(false); weatherCheck->setChecked(false); twilightCheck->blockSignals(true); twilightCheck->setChecked(false); twilightCheck->blockSignals(false); minAltitude->setValue(minAltitude->minimum()); minMoonSeparation->setValue(minMoonSeparation->minimum()); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Name")) nameEdit->setText(pcdataXMLEle(ep)); else if (!strcmp(tagXMLEle(ep), "Priority")) prioritySpin->setValue(atoi(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Coordinates")) { subEP = findXMLEle(ep, "J2000RA"); if (subEP) raBox->setDMS(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "J2000DE"); if (subEP) decBox->setDMS(pcdataXMLEle(subEP)); } else if (!strcmp(tagXMLEle(ep), "Sequence")) { sequenceEdit->setText(pcdataXMLEle(ep)); sequenceURL = QUrl::fromUserInput(sequenceEdit->text()); } else if (!strcmp(tagXMLEle(ep), "FITS")) { fitsEdit->setText(pcdataXMLEle(ep)); fitsURL.setPath(fitsEdit->text()); } else if (!strcmp(tagXMLEle(ep), "StartupCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("ASAP", pcdataXMLEle(subEP))) asapConditionR->setChecked(true); else if (!strcmp("Culmination", pcdataXMLEle(subEP))) { culminationConditionR->setChecked(true); culminationOffset->setValue(atof(findXMLAttValu(subEP, "value"))); } else if (!strcmp("At", pcdataXMLEle(subEP))) { startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Constraints")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) { altConstraintCheck->setChecked(true); minAltitude->setValue(atof(findXMLAttValu(subEP, "value"))); } else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(atof(findXMLAttValu(subEP, "value"))); } else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) weatherCheck->setChecked(true); else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) twilightCheck->setChecked(true); } } else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("Sequence", pcdataXMLEle(subEP))) sequenceCompletionR->setChecked(true); else if (!strcmp("Repeat", pcdataXMLEle(subEP))) { repeatCompletionR->setChecked(true); repeatsSpin->setValue(atoi(findXMLAttValu(subEP, "value"))); } else if (!strcmp("Loop", pcdataXMLEle(subEP))) loopCompletionR->setChecked(true); else if (!strcmp("At", pcdataXMLEle(subEP))) { timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Steps")) { XMLEle *module; trackStepCheck->setChecked(false); focusStepCheck->setChecked(false); alignStepCheck->setChecked(false); guideStepCheck->setChecked(false); for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(module); if (!strcmp(proc, "Track")) trackStepCheck->setChecked(true); else if (!strcmp(proc, "Focus")) focusStepCheck->setChecked(true); else if (!strcmp(proc, "Align")) alignStepCheck->setChecked(true); else if (!strcmp(proc, "Guide")) guideStepCheck->setChecked(true); } } } addToQueueB->setEnabled(true); saveJob(); return true; } void Scheduler::saveAs() { schedulerURL.clear(); save(); } void Scheduler::save() { QUrl backupCurrent = schedulerURL; if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) schedulerURL.clear(); // If no changes made, return. if (mDirty == false && !schedulerURL.isEmpty()) return; if (schedulerURL.isEmpty()) { schedulerURL = QFileDialog::getSaveFileUrl(this, i18n("Save Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); // if user presses cancel if (schedulerURL.isEmpty()) { schedulerURL = backupCurrent; return; } dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); if (schedulerURL.toLocalFile().contains('.') == 0) schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); } if (schedulerURL.isValid()) { if ((saveScheduler(schedulerURL)) == false) { KMessageBox::error(KStars::Instance(), i18n("Failed to save scheduler list"), i18n("Save")); return; } mDirty = false; } else { QString message = i18n("Invalid URL: %1", schedulerURL.url()); KMessageBox::sorry(KStars::Instance(), message, i18n("Invalid URL")); } } bool Scheduler::saveScheduler(const QUrl &fileURL) { QFile file; file.setFileName(fileURL.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", fileURL.toLocalFile()); KMessageBox::sorry(0, message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); outstream << "" << endl; outstream << "" << endl; outstream << "" << schedulerProfileCombo->currentText() << "" << endl; foreach (SchedulerJob *job, jobs) { outstream << "" << endl; outstream << "" << job->getName() << "" << endl; outstream << "" << job->getPriority() << "" << endl; outstream << "" << endl; outstream << "" << job->getTargetCoords().ra0().Hours() << "" << endl; outstream << "" << job->getTargetCoords().dec0().Degrees() << "" << endl; outstream << "" << endl; if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false) outstream << "" << job->getFITSFile().toLocalFile() << "" << endl; outstream << "" << job->getSequenceFile().toLocalFile() << "" << endl; outstream << "" << endl; if (job->getFileStartupCondition() == SchedulerJob::START_ASAP) outstream << "ASAP" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) - outstream << "At" + outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getMinAltitude() > 0) outstream << "MinimumAltitude" << endl; if (job->getMinMoonSeparation() > 0) outstream << "MoonSeparation" << endl; if (job->getEnforceWeather()) outstream << "EnforceWeather" << endl; if (job->getEnforceTwilight()) outstream << "EnforceTwilight" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) outstream << "Sequence" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) outstream << "Repeat" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP) outstream << "Loop" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getStepPipeline() & SchedulerJob::USE_TRACK) outstream << "Track" << endl; if (job->getStepPipeline() & SchedulerJob::USE_FOCUS) outstream << "Focus" << endl; if (job->getStepPipeline() & SchedulerJob::USE_ALIGN) outstream << "Align" << endl; if (job->getStepPipeline() & SchedulerJob::USE_GUIDE) outstream << "Guide" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; if (unparkDomeCheck->isChecked()) outstream << "UnparkDome" << endl; if (unparkMountCheck->isChecked()) outstream << "UnparkMount" << endl; if (uncapCheck->isChecked()) outstream << "UnparkCap" << endl; outstream << "" << endl; outstream << "" << endl; if (warmCCDCheck->isChecked()) outstream << "WarmCCD" << endl; if (capCheck->isChecked()) outstream << "ParkCap" << endl; if (parkMountCheck->isChecked()) outstream << "ParkMount" << endl; if (parkDomeCheck->isChecked()) outstream << "ParkDome" << endl; if (shutdownScript->text().isEmpty() == false) outstream << "ShutdownScript" << endl; outstream << "" << endl; outstream << "" << endl; appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile())); file.close(); return true; } void Scheduler::startSlew() { Q_ASSERT(currentJob != nullptr); if (isMountParked()) { appendLogText(i18n("Warning! Job '%1' found mount parked unexpectedly, attempting to unpark.", currentJob->getName())); startupState = STARTUP_UNPARK_MOUNT; unParkMount(); return; } if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); SkyPoint target = currentJob->getTargetCoords(); //target.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); QList telescopeSlew; telescopeSlew.append(target.ra().Hours()); telescopeSlew.append(target.dec().Degrees()); appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName())); QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); if (slewModeReply.error().type() != QDBusError::NoError) { /* FIXME: manage error */ appendLogText(i18n("Warning! Job '%1' slew request received DBUS error: %2", currentJob->getName(), QDBusError::errorString(slewModeReply.error().type()))); return; } currentJob->setStage(SchedulerJob::STAGE_SLEWING); } void Scheduler::startFocusing() { // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed // when first focus request is made in capture module if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); // Reset Focus frame so that next frame take a full-resolution capture first. focusInterface->call(QDBus::AutoDetect,"resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); return; } // Check if autofocus is supported QDBusReply focusModeReply; focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus"); if (focusModeReply.error().type() != QDBusError::NoError) { appendLogText(i18n("Warning! Job '%1' canAutoFocus request received DBUS error: %2", currentJob->getName(), QDBusError::errorString(focusModeReply.error().type()))); return; } if (focusModeReply.value() == false) { appendLogText(i18n("Warning! Job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName())); currentJob->setStepPipeline( static_cast(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS)); currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); return; } // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); QDBusMessage reply; // We always need to reset frame first if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' resetFrame request received DBUS error: %2", currentJob->getName(), reply.errorMessage())); return; } // Set autostar if full field option is false if (Options::focusUseFullField() == false) { QList autoStar; autoStar.append(true); if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' setAutoFocusStar request received DBUS error: %1", currentJob->getName(), reply.errorMessage())); return; } } // Start auto-focus if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' startFocus request received DBUS error: %2", currentJob->getName(), reply.errorMessage())); return; } /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING); appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName())); } else { currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Focusing %1 ...", currentJob->getName())); }*/ currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Job '%1' is focusing.", currentJob->getName())); } void Scheduler::findNextJob() { jobTimer.stop(); /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; if (currentJob->getState() == SchedulerJob::JOB_ERROR) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getState() == SchedulerJob::JOB_ABORTED) { // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); setCurrentJob(nullptr); schedulerTimer.start(); } // Check completion criteria // 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(); appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); // 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(); appendLogText(i18np("Job '%1' is complete after #%2 batch.", "Job '%1' is complete after #%2 batches.", currentJob->getName(), currentJob->getRepeatsRequired())); setCurrentJob(nullptr); schedulerTimer.start(); } else { /* FIXME: raise priority to allow other jobs to schedule in-between */ 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.", currentJob->getName(), currentJob->getRepeatsRemaining())); /* currentJob remains the same */ jobTimer.start(); } } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { currentJob->setState(SchedulerJob::JOB_BUSY); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName())); /* currentJob remains the same */ jobTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { 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; appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.", "Job '%1' stopping, reached completion time with #%2 batches done.", currentJob->getName(), captureBatch + 1)); setCurrentJob(nullptr); schedulerTimer.start(); } else { currentJob->setState(SchedulerJob::JOB_BUSY); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.", "Job '%1' completed #%2 batches before completion time, restarted.", currentJob->getName(), captureBatch)); /* currentJob remains the same */ jobTimer.start(); } } else { /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */ appendLogText(i18n("BUGBUG: Job '%1' timer elapsed, but no action to be taken.", currentJob->getName())); setCurrentJob(nullptr); schedulerTimer.start(); } } void Scheduler::startAstrometry() { QDBusMessage reply; setSolverAction(Align::GOTO_SLEW); // Always turn update coords on QVariant arg(true); alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg); // If FITS file is specified, then we use load and slew if (currentJob->getFITSFile().isEmpty() == false) { QList solveArgs; solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile)); if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' loadAndSlew request received DBUS error: %2", currentJob->getName(), reply.errorMessage())); return; } loadAndSlewProgress = true; appendLogText(i18n("Job '%1' is plate solving capture %2.", currentJob->getName(), currentJob->getFITSFile().fileName())); } else { if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' captureAndSolve request received DBUS error: %2", currentJob->getName(), reply.errorMessage())); return; } appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName())); } /* FIXME: not supposed to modify the job */ currentJob->setStage(SchedulerJob::STAGE_ALIGNING); } void Scheduler::startGuiding(bool resetCalibration) { // Make sure calibration is auto //QVariant arg(true); //guideInterface->call(QDBus::AutoDetect,"setCalibrationAutoStar", arg); if (resetCalibration) guideInterface->call(QDBus::AutoDetect, "clearCalibration"); //QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect,"startAutoCalibrateGuide"); guideInterface->call(QDBus::AutoDetect, "startAutoCalibrateGuide"); /*if (guideReply.value() == false) { appendLogText(i18n("Starting guide calibration failed. If using external guide application, ensure it is up and running.")); currentJob->setState(SchedulerJob::JOB_ERROR); } else {*/ currentJob->setStage(SchedulerJob::STAGE_GUIDING); appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName())); //} } void Scheduler::startCapture() { captureInterface->call(QDBus::AutoDetect, "clearSequenceQueue"); QString targetName = currentJob->getName().replace(' ', ""); QList targetArgs; targetArgs.append(targetName); captureInterface->callWithArgumentList(QDBus::AutoDetect, "setTargetName", targetArgs); QString url = currentJob->getSequenceFile().toLocalFile(); QList dbusargs; dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); QMap fMap = currentJob->getCapturedFramesMap(); for (auto e : fMap.keys()) { QList dbusargs; QDBusMessage reply; dbusargs.append(e); dbusargs.append(fMap.value(e)); if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() == QDBusMessage::ErrorMessage) { appendLogText(i18n("Warning! Job '%1' setCapturedFramesCount request received DBUS error: %1", currentJob->getName(), reply.errorMessage())); return; } } // If sequence is a loop, ignore sequence history if (currentJob->getCompletionCondition() != SchedulerJob::FINISH_SEQUENCE) captureInterface->call(QDBus::AutoDetect, "ignoreSequenceHistory"); // Start capture process captureInterface->call(QDBus::AutoDetect, "start"); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); KNotification::event(QLatin1String("EkosScheduledImagingStart"), i18n("Ekos job (%1) - Capture started", currentJob->getName())); if (captureBatch > 0) appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1)); else appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName())); } void Scheduler::stopGuiding() { if ((currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) && (currentJob->getStage() == SchedulerJob::STAGE_GUIDING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_CAPTURING)) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping guiding..."; guideInterface->call(QDBus::AutoDetect, "abort"); guideFailureCount = 0; } } void Scheduler::setSolverAction(Align::GotoMode mode) { QVariant gotoMode(static_cast(mode)); alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode); } void Scheduler::disconnectINDI() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI..."; indiState = INDI_DISCONNECTING; indiConnectFailureCount = 0; ekosInterface->call(QDBus::AutoDetect, "disconnectDevices"); } void Scheduler::stopEkos() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos..."; ekosState = EKOS_STOPPING; ekosInterface->call(QDBus::AutoDetect, "stop"); } void Scheduler::setDirty() { mDirty = true; if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) return; if (jobUnderEdit >= 0 && state != SCHEDULER_RUNNIG && queueTable->selectedItems().isEmpty() == false) saveJob(); } 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; /* Enumerate SchedulerJobs to count captures that are already stored */ for (SchedulerJob *oneJob : jobs) { QList seqjobs; bool hasAutoFocus = false; /* Look into the sequence requirements, bypass if invalid */ if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus) == false) { appendLogText(i18n("Warning! Job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), oneJob->getSequenceFile().toLocalFile())); oneJob->setState(SchedulerJob::JOB_INVALID); continue; } /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */ foreach (SequenceJob *oneSeqJob, seqjobs) { /* Only consider captures stored on client (Ekos) side */ /* FIXME: ask the remote for the file count */ if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) continue; /* FIXME: refactor signature determination in a separate function in order to support multiple backends */ /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */ QString const signature = oneSeqJob->getLocalDir() + oneSeqJob->getDirectoryPostfix(); /* Bypass this SchedulerJob if we already checked its signature */ switch(oneJob->getState()) { case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: /* We recount idle/evaluated jobs systematically */ break; default: /* We recount other jobs if somehow we don't have any count for their signature, else we reuse the previous count */ QMap::iterator const sigCount = capturedFramesCount.find(signature); if (capturedFramesCount.end() != sigCount) { newFramesCount[signature] = sigCount.value(); continue; } } /* 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()); } } capturedFramesCount = newFramesCount; } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { /* updateCompletedJobsCount(); */ QList jobs; bool hasAutoFocus = false; if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, jobs, hasAutoFocus) == false) return false; schedJob->setInSequenceFocus(hasAutoFocus); bool lightFramesRequired = false; int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; bool rememberJobProgress = Options::rememberJobProgress(); foreach (SequenceJob *job, jobs) { /* FIXME: find a way to actually display the filter name */ QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), job->getCount(), job->getExposure(), job->getFilterName()); if (job->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { appendLogText(i18n("%1 duration cannot be estimated time since the sequence saves the files remotely.", seqName)); schedJob->setEstimatedTime(-2); // Iterate over all jobs, if just one requires FRAME_LIGHT then we set it as is and return foreach (SequenceJob *oneJob, jobs) { if (oneJob->getFrameType() == FRAME_LIGHT) { lightFramesRequired = true; break; } } schedJob->setLightFramesRequired(lightFramesRequired); qDeleteAll(jobs); return true; } int completed = 0; if (rememberJobProgress) { // Retrieve cached count of completed captures for the output folder of this job QString signature = job->getLocalDir() + job->getDirectoryPostfix(); completed = capturedFramesCount[signature]; appendLogText(i18n("%1 matches %2 captures in output folder '%3'.", seqName, completed, signature)); // If we have multiple jobs storing their captures in the same output folder (duplicated jobs), we need to recalculate // the completion count for the current scheduler job using sequence jobs which had the same signature earlier in the list // This DOES NOT handle the case of duplicated Scheduler jobs having the same output folder //int const overallCompleted = completed; foreach (SequenceJob *prevJob, jobs) { // Enumerate jobs up to the current one if (job == prevJob) break; // 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(); schedJob->setCapturedFramesMap(fMap); // From now on, 'completed' is the number of frames completed for the *current* sequence job } // Check if we still need any light frames. Because light frames changes the flow of the observatory startup // Without light frames, there is no need to do focusing, alignment, guiding...etc // We check if the frame type is LIGHT and if either the number of completed frames is less than required // OR if the completion condition is set to LOOP so it is never complete due to looping. bool const areJobCapturesComplete = !(completed < job->getCount()*schedJob->getRepeatsRequired() || schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP); if (job->getFrameType() == FRAME_LIGHT) { 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 { lightFramesRequired = true; // In some cases we do not need to calculate time we just need to know // if light frames are required or not. So we break out /* if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP || (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT)) break; */ } } else { appendLogText(i18n("%1 captures calibration frames.", seqName)); } totalSequenceCount += job->getCount()*schedJob->getRepeatsRequired(); totalCompletedCount += rememberJobProgress ? completed : 0; /* If captures are not complete, we have imaging time left */ if (!areJobCapturesComplete) { /* if looping, consider we always have one capture left - currently this is discarded afterwards as -2 */ 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) { // If inSequenceFocus is true if (hasAutoFocus) { // Wild guess that each in sequence auto focus takes an average of 30 seconds. It can take any where from 2 seconds to 2+ minutes. appendLogText(i18n("%1 requires a focus procedure.", seqName)); totalImagingTime += (job->getCount()*schedJob->getRepeatsRequired() - completed) * 30; } // If we're dithering after each exposure, that's another 10-20 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) { appendLogText(i18n("%1 requires a dither procedure.", seqName)); totalImagingTime += ((job->getCount()*schedJob->getRepeatsRequired() - completed) * 15) / Options::ditherFrames(); } } } } schedJob->setLightFramesRequired(lightFramesRequired); schedJob->setSequenceCount(totalSequenceCount); schedJob->setCompletedCount(totalCompletedCount); qDeleteAll(jobs); // We can't estimate times that do not finish when sequence is done if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { // We can't know estimated time if it is looping indefinitely appendLogText(i18n("Warning! Job '%1' will be looping until Scheduler is stopped manually.", schedJob->getName())); schedJob->setEstimatedTime(-2); } // If we know startup and finish times, we can estimate time right away else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); appendLogText(i18n("Job '%1' will run for %2.", schedJob->getName(), dms(diff / 3600.0f).toHMSString())); schedJob->setEstimatedTime(diff); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null else if (totalImagingTime <= 0) { appendLogText(i18n("Job '%1' will not run, complete with %2/%3 captures.", schedJob->getName(), totalCompletedCount, totalSequenceCount)); schedJob->setEstimatedTime(0); } else { if (lightFramesRequired) { // Are we doing tracking? It takes about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) totalImagingTime += 30*schedJob->getRepeatsRequired(); // Are we doing initial focusing? That can take about 2 minutes if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) totalImagingTime += 120*schedJob->getRepeatsRequired(); // Are we doing astrometry? That can take about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) totalImagingTime += 30*schedJob->getRepeatsRequired(); // Are we doing guiding? Calibration process can take about 2 mins if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) totalImagingTime += 120*schedJob->getRepeatsRequired(); } dms estimatedTime; estimatedTime.setH(totalImagingTime / 3600.0); /* Kept the informative log because the estimation is displayed */ appendLogText(i18n("Job '%1' estimated to take %2 to complete.", schedJob->getName(), estimatedTime.toHMSString())); schedJob->setEstimatedTime(totalImagingTime); } return true; } void Scheduler::parkMount() { QDBusReply MountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); Mount::ParkingStatus status = (Mount::ParkingStatus)MountReply.value(); if (status != Mount::PARKING_OK) { if (status == Mount::PARKING_BUSY) { appendLogText(i18n("Parking mount in progress...")); } else { mountInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking mount...")); currentOperationTime.start(); } if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARKING_MOUNT; else if (parkWaitState == PARKWAIT_PARK) parkWaitState = PARKWAIT_PARKING; } else { appendLogText(i18n("Mount already parked.")); if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; else if (parkWaitState == PARKWAIT_PARK) parkWaitState = PARKWAIT_PARKED; } } void Scheduler::unParkMount() { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); Mount::ParkingStatus status = (Mount::ParkingStatus)mountReply.value(); 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; } if (status != Mount::UNPARKING_OK) { if (status == Mount::UNPARKING_BUSY) appendLogText(i18n("Unparking mount in progress...")); else { mountInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking mount...")); currentOperationTime.start(); } if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARKING_MOUNT; else if (parkWaitState == PARKWAIT_UNPARK) parkWaitState = PARKWAIT_UNPARKING; } else { appendLogText(i18n("Mount already unparked.")); if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARK_CAP; else if (parkWaitState == PARKWAIT_UNPARK) parkWaitState = PARKWAIT_UNPARKED; } } void Scheduler::checkMountParkingStatus() { static int parkingFailureCount = 0; QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); Mount::ParkingStatus status = (Mount::ParkingStatus)mountReply.value(); 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; } switch (status) { case Mount::PARKING_OK: appendLogText(i18n("Mount parked.")); if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; else if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; parkingFailureCount = 0; break; case Mount::UNPARKING_OK: appendLogText(i18n("Mount unparked.")); if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; else if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; parkingFailureCount = 0; break; case Mount::PARKING_BUSY: case Mount::UNPARKING_BUSY: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == Mount::PARKING_BUSY) parkMount(); else unParkMount(); break; } } break; case Mount::PARKING_ERROR: if (startupState == STARTUP_UNPARKING_MOUNT) { appendLogText(i18n("Mount unparking error.")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_PARKING_MOUNT) { appendLogText(i18n("Mount parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (parkWaitState == PARKWAIT_PARKING) { appendLogText(i18n("Mount parking error.")); parkWaitState = PARKWAIT_ERROR; } else if (parkWaitState == PARKWAIT_UNPARK) { appendLogText(i18n("Mount unparking error.")); parkWaitState = PARKWAIT_ERROR; } parkingFailureCount = 0; break; default: break; } } bool Scheduler::isMountParked() { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); Mount::ParkingStatus status = (Mount::ParkingStatus)mountReply.value(); 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; } return status == Mount::PARKING_OK || status == Mount::PARKING_IDLE; } void Scheduler::parkDome() { QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); Dome::ParkingStatus status = (Dome::ParkingStatus)domeReply.value(); 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; } if (status != Dome::PARKING_OK) { shutdownState = SHUTDOWN_PARKING_DOME; domeInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already parked.")); shutdownState = SHUTDOWN_SCRIPT; } } void Scheduler::unParkDome() { QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); Dome::ParkingStatus status = (Dome::ParkingStatus)domeReply.value(); 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; } if (status != Dome::UNPARKING_OK) { startupState = STARTUP_UNPARKING_DOME; domeInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already unparked.")); startupState = STARTUP_UNPARK_MOUNT; } } void Scheduler::checkDomeParkingStatus() { /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); Dome::ParkingStatus status = (Dome::ParkingStatus)domeReply.value(); 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; } switch (status) { case Dome::PARKING_OK: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parked.")); shutdownState = SHUTDOWN_SCRIPT; } parkingFailureCount = 0; break; case Dome::UNPARKING_OK: if (startupState == STARTUP_UNPARKING_DOME) { startupState = STARTUP_UNPARK_MOUNT; appendLogText(i18n("Dome unparked.")); } parkingFailureCount = 0; break; case Dome::PARKING_BUSY: case Dome::UNPARKING_BUSY: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (120 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == Dome::PARKING_BUSY) parkDome(); else unParkDome(); break; } } break; case Dome::PARKING_ERROR: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_DOME) { appendLogText(i18n("Dome unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } bool Scheduler::isDomeParked() { QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); Dome::ParkingStatus status = (Dome::ParkingStatus)domeReply.value(); 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; } return status == Dome::PARKING_OK || status == Dome::PARKING_IDLE; } void Scheduler::parkCap() { QDBusReply const capReply = capInterface->call(QDBus::AutoDetect, "getParkingStatus"); DustCap::ParkingStatus status = (DustCap::ParkingStatus)capReply.value(); 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; } if (status != DustCap::PARKING_OK) { shutdownState = SHUTDOWN_PARKING_CAP; capInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking Cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } } void Scheduler::unParkCap() { QDBusReply const capReply = capInterface->call(QDBus::AutoDetect, "getParkingStatus"); DustCap::ParkingStatus status = (DustCap::ParkingStatus)capReply.value(); 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; } if (status != DustCap::UNPARKING_OK) { startupState = STARTUP_UNPARKING_CAP; capInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already unparked.")); startupState = STARTUP_COMPLETE; } } void Scheduler::checkCapParkingStatus() { /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QDBusReply const capReply = capInterface->call(QDBus::AutoDetect, "getParkingStatus"); DustCap::ParkingStatus status = (DustCap::ParkingStatus)capReply.value(); 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; } switch (status) { case DustCap::PARKING_OK: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } parkingFailureCount = 0; break; case DustCap::UNPARKING_OK: if (startupState == STARTUP_UNPARKING_CAP) { startupState = STARTUP_COMPLETE; appendLogText(i18n("Cap unparked.")); } parkingFailureCount = 0; break; case DustCap::PARKING_BUSY: case DustCap::UNPARKING_BUSY: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == DustCap::PARKING_BUSY) parkCap(); else unParkCap(); break; } } break; case DustCap::PARKING_ERROR: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_CAP) { appendLogText(i18n("Cap unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } 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(); } void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); int dayOffset = 0; QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); if (KStarsData::Instance()->lt().time() >= dawn) dayOffset = 1; preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); } bool Scheduler::isWeatherOK(SchedulerJob *job) { if (weatherStatus == IPS_OK || weatherCheck->isChecked() == false) return true; else if (weatherStatus == IPS_IDLE) { if (indiState == INDI_READY) appendLogText(i18n("Weather information is pending...")); return true; } // Temporary BUSY is ALSO accepted for now // TODO Figure out how to exactly handle this if (weatherStatus == IPS_BUSY) return true; if (weatherStatus == IPS_ALERT) { job->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName())); } /*else if (weatherStatus == IPS_BUSY) { appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); schedulerTimer.stop(); connect(this, SIGNAL(weatherChanged(IPState)), this, SLOT(resumeCheckStatus())); }*/ return false; } void Scheduler::resumeCheckStatus() { disconnect(this, SIGNAL(weatherChanged(IPState)), this, SLOT(resumeCheckStatus())); schedulerTimer.start(); } void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("DEC value %1 is invalid.", decBox->text())); return; } Mosaic mosaicTool; SkyPoint center; center.setRA0(ra); center.setDec0(dec); mosaicTool.setCenter(center); mosaicTool.calculateFOV(); mosaicTool.adjustSize(); if (mosaicTool.exec() == QDialog::Accepted) { // #1 Edit Sequence File ---> Not needed as of 2016-09-12 since Scheduler can send Target Name to Capture module it will append it to root dir // #1.1 Set prefix to Target-Part# // #1.2 Set directory to output/Target-Part# // #2 Save all sequence files in Jobs dir // #3 Set as currnet Sequence file // #4 Change Target name to Target-Part# // #5 Update J2000 coords // #6 Repeat and save Ekos Scheduler List in the output directory qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir " << mosaicTool.getJobsDir(); QString outputDir = mosaicTool.getJobsDir(); QString targetName = nameEdit->text().simplified().remove(' '); int batchCount = 1; XMLEle *root = getSequenceJobRoot(); if (root == nullptr) return; int currentJobsCount = jobs.count(); foreach (OneTile *oneJob, mosaicTool.getJobs()) { QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++); prefix.replace(' ', '-'); nameEdit->setText(prefix); if (createJobSequence(root, prefix, outputDir) == false) return; QString filename = QString("%1/%2.esq").arg(outputDir).arg(prefix); sequenceEdit->setText(filename); sequenceURL = QUrl::fromLocalFile(filename); raBox->setText(oneJob->skyCenter.ra0().toHMSString()); decBox->setText(oneJob->skyCenter.dec0().toDMSString()); saveJob(); } delXMLEle(root); // Delete any prior jobs before saving for (int i = 0; i < currentJobsCount; i++) { delete (jobs.takeFirst()); queueTable->removeRow(0); } QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir).arg(targetName))); if (saveScheduler(mosaicURL)) { appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile())); } else { appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile())); } } } XMLEle *Scheduler::getSequenceJobRoot() { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KMessageBox::sorry(KStars::Instance(), i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return nullptr; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) break; } delLilXML(xmlParser); sFile.close(); return root; } bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir) { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KMessageBox::sorry(KStars::Instance(), i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Job")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp(tagXMLEle(subEP), "Prefix")) { XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix"); if (rawPrefix) { editXMLEle(rawPrefix, prefix.toLatin1().constData()); } } else if (!strcmp(tagXMLEle(subEP), "FITSDirectory")) { editXMLEle(subEP, QString("%1/%2").arg(outputDir).arg(prefix).toLatin1().constData()); } } } } QDir().mkpath(outputDir); QString filename = QString("%1/%2.esq").arg(outputDir).arg(prefix); FILE *outputFile = fopen(filename.toLatin1().constData(), "w"); if (outputFile == nullptr) { QString message = i18n("Unable to write to file %1", filename); KMessageBox::sorry(0, message, i18n("Could Not Open File")); return false; } fprintf(outputFile, ""); prXMLEle(outputFile, root, 0); fclose(outputFile); return true; } void Scheduler::resetAllJobs() { if (state == SCHEDULER_RUNNIG) return; foreach (SchedulerJob *job, jobs) job->reset(); } void Scheduler::checkTwilightWarning(bool enabled) { if (enabled) return; if (KMessageBox::warningContinueCancel( NULL, i18n("Warning! Turning off astronomial twilight check may cause the observatory " "to run during daylight. This can cause irreversible damage to your equipment!"), i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "astronomical_twilight_warning") == KMessageBox::Cancel) { twilightCheck->setChecked(true); } } void Scheduler::checkStartupProcedure() { if (checkStartupState() == false) QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); else { if (startupState == STARTUP_COMPLETE) appendLogText(i18n("Manual startup procedure completed successfully.")); else if (startupState == STARTUP_ERROR) appendLogText(i18n("Manual startup procedure terminated due to errors.")); startupB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runStartupProcedure() { if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE) { /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */ if (indiState == INDI_IDLE) { KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online.")); return; } if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning! Executing startup procedure manually...")); startupB->setIcon( QIcon::fromTheme("media-playback-stop")); startupState = STARTUP_IDLE; checkStartupState(); QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); } } else { switch (startupState) { case STARTUP_IDLE: break; case STARTUP_SCRIPT: scriptProcess.terminate(); break; case STARTUP_UNPARK_DOME: break; case STARTUP_UNPARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_MOUNT: break; case STARTUP_UNPARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_CAP: break; case STARTUP_UNPARKING_CAP: break; case STARTUP_COMPLETE: break; case STARTUP_ERROR: break; } startupState = STARTUP_IDLE; appendLogText(i18n("Startup procedure terminated.")); } } void Scheduler::checkShutdownProcedure() { // If shutdown procedure is not finished yet, let's check again in 1 second. if (checkShutdownState() == false) QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); else { if (shutdownState == SHUTDOWN_COMPLETE) { appendLogText(i18n("Manual shutdown procedure completed successfully.")); // Stop Ekos if (Options::stopEkosAfterShutdown()) stopEkos(); } else if (shutdownState == SHUTDOWN_ERROR) appendLogText(i18n("Manual shutdown procedure terminated due to errors.")); shutdownState = SHUTDOWN_IDLE; shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runShutdownProcedure() { if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE) { if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning! Executing shutdown procedure manually...")); shutdownB->setIcon( QIcon::fromTheme("media-playback-stop")); shutdownState = SHUTDOWN_IDLE; checkShutdownState(); QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); } } else { switch (shutdownState) { case SHUTDOWN_IDLE: break; case SHUTDOWN_SCRIPT: break; case SHUTDOWN_SCRIPT_RUNNING: scriptProcess.terminate(); break; case SHUTDOWN_PARK_DOME: break; case SHUTDOWN_PARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_MOUNT: break; case SHUTDOWN_PARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_CAP: break; case SHUTDOWN_PARKING_CAP: break; case SHUTDOWN_COMPLETE: break; case SHUTDOWN_ERROR: break; } shutdownState = SHUTDOWN_IDLE; appendLogText(i18n("Shutdown procedure terminated.")); } } void Scheduler::loadProfiles() { QString currentProfile = schedulerProfileCombo->currentText(); QDBusReply profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles"); if (profiles.error().type() == QDBusError::NoError) { schedulerProfileCombo->blockSignals(true); schedulerProfileCombo->clear(); schedulerProfileCombo->addItem(i18n("Default")); schedulerProfileCombo->addItems(profiles); schedulerProfileCombo->setCurrentText(currentProfile); schedulerProfileCombo->blockSignals(false); } } bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { appendLogText(i18n("Unable to open file %1", fileURL)); return false; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Autofocus")) hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); else if (!strcmp(tagXMLEle(ep), "Job")) jobs.append(processJobInfo(ep, schedJob)); } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); qDeleteAll(jobs); return false; } } return true; } SequenceJob *Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob) { XMLEle *ep = nullptr; XMLEle *subEP = nullptr; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; SequenceJob *job = new SequenceJob(); QString rawPrefix, frameType, filterType; double exposure = 0; bool filterEnabled = false, expEnabled = false, tsEnabled = false; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) { exposure = atof(pcdataXMLEle(ep)); job->setExposure(exposure); } else if (!strcmp(tagXMLEle(ep), "Filter")) { filterType = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameType = QString(pcdataXMLEle(ep)); job->setFrameType(frameTypes[frameType]); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) rawPrefix = QString(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterEnabled = !strcmp("1", pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expEnabled = (!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) tsEnabled = (!strcmp("1", pcdataXMLEle(subEP))); job->setPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); } else if (!strcmp(tagXMLEle(ep), "Count")) { job->setCount(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { job->setDelay(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { job->setLocalDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { job->setRemoteDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { job->setUploadMode(static_cast(atoi(pcdataXMLEle(ep)))); } } // Make full prefix QString imagePrefix = rawPrefix; if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameType; if (filterEnabled && filterType.isEmpty() == false && (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += filterType; } if (expEnabled) { imagePrefix += '_'; imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs"); } job->setFullPrefix(imagePrefix); QString targetName = schedJob->getName().remove(' '); // Directory postfix QString directoryPostfix; directoryPostfix = QLatin1Literal("/") + targetName + QLatin1Literal("/") + frameType; if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && filterType.isEmpty() == false) directoryPostfix += QLatin1Literal("/") + filterType; job->setDirectoryPostfix(directoryPostfix); return job; } int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix) { int seqFileCount = 0; qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in '%1' for prefix '%2'...").arg(path).arg(seqPrefix); QDirIterator it(path, QDir::Files); /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */ while (it.hasNext()) { QString const fileName = QFileInfo(it.next()).baseName(); if (fileName.startsWith(seqPrefix)) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName); seqFileCount++; } } return seqFileCount; } } diff --git a/kstars/ekos/scheduler/schedulerjob.cpp b/kstars/ekos/scheduler/schedulerjob.cpp index 8de513858..b9b2e1abc 100644 --- a/kstars/ekos/scheduler/schedulerjob.cpp +++ b/kstars/ekos/scheduler/schedulerjob.cpp @@ -1,383 +1,531 @@ /* Ekos Scheduler Job Copyright (C) Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "schedulerjob.h" #include "scheduler.h" #include "dms.h" #include "kstarsdata.h" #include #include SchedulerJob::SchedulerJob() { } void SchedulerJob::setName(const QString &value) { name = value; updateJobCell(); } void SchedulerJob::setStartupCondition(const StartupCondition &value) { startupCondition = value; if (value == START_ASAP) startupTime = QDateTime(); updateJobCell(); } 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) { sequenceFile = value; } void SchedulerJob::setFITSFile(const QUrl &value) { fitsFile = value; } void SchedulerJob::setMinAltitude(const double &value) { minAltitude = value; } void SchedulerJob::setMinMoonSeparation(const double &value) { minMoonSeparation = value; } void SchedulerJob::setEnforceWeather(bool value) { enforceWeather = value; } 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) { stepPipeline = value; } void SchedulerJob::setState(const JOBStatus &value) { state = value; /* FIXME: move this to Scheduler, SchedulerJob is mostly a model */ 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(); } void SchedulerJob::setScore(int value) { score = value; updateJobCell(); } void SchedulerJob::setCulminationOffset(const int16_t &value) { culminationOffset = value; } void SchedulerJob::setSequenceCount(const int count) { sequenceCount = count; updateJobCell(); } void SchedulerJob::setNameCell(QTableWidgetItem *value) { nameCell = value; updateJobCell(); } void SchedulerJob::setCompletedCount(const int count) { completedCount = count; updateJobCell(); } void SchedulerJob::setStatusCell(QTableWidgetItem *value) { 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) { dateTimeDisplayFormat = value; updateJobCell(); } void SchedulerJob::setStage(const JOBStage &value) { stage = value; updateJobCell(); } void SchedulerJob::setStageCell(QTableWidgetItem *cell) { stageCell = cell; updateJobCell(); } void SchedulerJob::setStageLabel(QLabel *label) { stageLabel = label; updateJobCell(); } void SchedulerJob::setFileStartupCondition(const StartupCondition &value) { 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(); } void SchedulerJob::setInSequenceFocus(bool value) { inSequenceFocus = value; } void SchedulerJob::setPriority(const uint8_t &value) { priority = value; } void SchedulerJob::setEnforceTwilight(bool value) { enforceTwilight = value; } void SchedulerJob::setEstimatedTimeCell(QTableWidgetItem *value) { 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) { lightFramesRequired = value; } void SchedulerJob::setRepeatsRequired(const uint16_t &value) { repeatsRequired = value; updateJobCell(); } void SchedulerJob::setRepeatsRemaining(const uint16_t &value) { repeatsRemaining = value; updateJobCell(); } void SchedulerJob::setCapturedFramesMap(const QMap &value) { capturedFramesMap = value; } void SchedulerJob::setTargetCoords(dms& ra, dms& dec) { targetCoords.setRA0(ra); targetCoords.setDec0(dec); targetCoords.updateCoordsNow(KStarsData::Instance()->updateNum()); } -/* FIXME: unrelated to model, move this in the view */ void SchedulerJob::updateJobCell() { if (nameCell) { nameCell->setText(name); nameCell->tableWidget()->resizeColumnToContents(nameCell->column()); } if (nameLabel) { nameLabel->setText(name + QString(":")); } if (statusCell) { static QMap stateStrings; static QString stateStringUnknown; if (stateStrings.isEmpty()) { stateStrings[JOB_IDLE] = i18n("Idle"); stateStrings[JOB_EVALUATION] = i18n("Evaluating"); stateStrings[JOB_SCHEDULED] = i18n("Scheduled"); stateStrings[JOB_BUSY] = i18n("Running"); stateStrings[JOB_INVALID] = i18n("Invalid"); stateStrings[JOB_COMPLETE] = i18n("Complete"); stateStrings[JOB_ABORTED] = i18n("Aborted"); stateStrings[JOB_ERROR] = i18n("Error"); stateStringUnknown = i18n("Unknown"); } statusCell->setText(stateStrings.value(state, stateStringUnknown)); statusCell->tableWidget()->resizeColumnToContents(statusCell->column()); } 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; if (stageStrings.isEmpty()) { stageStrings[STAGE_IDLE] = i18n("Idle"); stageStrings[STAGE_SLEWING] = i18n("Slewing"); stageStrings[STAGE_SLEW_COMPLETE] = i18n("Slew complete"); stageStrings[STAGE_FOCUSING] = stageStrings[STAGE_POSTALIGN_FOCUSING] = i18n("Focusing"); stageStrings[STAGE_FOCUS_COMPLETE] = stageStrings[STAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete"); stageStrings[STAGE_ALIGNING] = i18n("Aligning"); stageStrings[STAGE_ALIGN_COMPLETE] = i18n("Align complete"); stageStrings[STAGE_RESLEWING] = i18n("Repositioning"); stageStrings[STAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete"); /*stageStrings[STAGE_CALIBRATING] = i18n("Calibrating");*/ stageStrings[STAGE_GUIDING] = i18n("Guiding"); stageStrings[STAGE_GUIDING_COMPLETE] = i18n("Guiding complete"); stageStrings[STAGE_CAPTURING] = i18n("Capturing"); stageStringUnknown = i18n("Unknown"); } if (stageCell) { stageCell->setText(stageStrings.value(stage, stageStringUnknown)); stageCell->tableWidget()->resizeColumnToContents(stageCell->column()); } if (stageLabel) { stageLabel->setText(QString("%1: %2").arg(name).arg(stageStrings.value(stage, stageStringUnknown))); } } 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()); } if (estimatedTimeCell) { if (0 < estimatedTime) /* Seconds to ms - this doesn't follow dateTimeDisplayFormat, which renders YMD too */ estimatedTimeCell->setText(QTime::fromMSecsSinceStartOfDay(estimatedTime*1000).toString("HH:mm:ss")); else if(0 == estimatedTime) /* FIXME: this special case could be merged with the previous, kept for future to indicate actual duration */ estimatedTimeCell->setText("00:00:00"); else /* Invalid marker */ estimatedTimeCell->setText("-"); estimatedTimeCell->tableWidget()->resizeColumnToContents(estimatedTimeCell->column()); } if (captureCountCell) { captureCountCell->setText(QString("%1/%2").arg(completedCount).arg(sequenceCount)); captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column()); } if (scoreCell) { if (0 <= score) scoreCell->setText(QString("%1").arg(score)); else /* FIXME: negative scores are just weird for the end-user */ scoreCell->setText(QString("<0")); scoreCell->tableWidget()->resizeColumnToContents(scoreCell->column()); } } void SchedulerJob::reset() { state = JOB_IDLE; 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; } bool SchedulerJob::decreasingScoreOrder(SchedulerJob const *job1, SchedulerJob const *job2) { return job1->getScore() > job2->getScore(); } bool SchedulerJob::increasingPriorityOrder(SchedulerJob const *job1, SchedulerJob const *job2) { return job1->getPriority() < job2->getPriority(); } bool SchedulerJob::decreasingAltitudeOrder(SchedulerJob const *job1, SchedulerJob const *job2) { 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(); +} diff --git a/kstars/ekos/scheduler/schedulerjob.h b/kstars/ekos/scheduler/schedulerjob.h index 7a0642cdb..10328a4af 100644 --- a/kstars/ekos/scheduler/schedulerjob.h +++ b/kstars/ekos/scheduler/schedulerjob.h @@ -1,385 +1,414 @@ /* Ekos Scheduler Job Copyright (C) Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "skypoint.h" #include #include class QTableWidgetItem; class QLabel; class dms; class SchedulerJob { public: SchedulerJob(); /** @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. */ typedef enum { STAGE_IDLE, STAGE_SLEWING, STAGE_SLEW_COMPLETE, STAGE_FOCUSING, STAGE_FOCUS_COMPLETE, STAGE_ALIGNING, STAGE_ALIGN_COMPLETE, STAGE_RESLEWING, STAGE_RESLEWING_COMPLETE, STAGE_POSTALIGN_FOCUSING, STAGE_POSTALIGN_FOCUSING_COMPLETE, STAGE_GUIDING, STAGE_GUIDING_COMPLETE, STAGE_CAPTURING, STAGE_COMPLETE } JOBStage; /** @brief Conditions under which a SchedulerJob may start. */ typedef enum { START_ASAP, START_CULMINATION, START_AT } StartupCondition; /** @brief Conditions under which a SchedulerJob may complete. */ typedef enum { FINISH_SEQUENCE, FINISH_REPEAT, FINISH_LOOP, FINISH_AT } CompletionCondition; /** @brief Actions that may be processed when running a SchedulerJob. * FIXME: StepPipeLine is actually a mask, change this into a bitfield. */ typedef enum { USE_NONE = 0, USE_TRACK = 1 << 0, USE_FOCUS = 1 << 1, USE_ALIGN = 1 << 2, USE_GUIDE = 1 << 3 } StepPipeline; /** @brief Coordinates of the target of this job. */ /** @{ */ SkyPoint const & getTargetCoords() const { return targetCoords; } void setTargetCoords(dms& ra, dms& dec); /** @} */ /** @brief Capture sequence this job uses while running. */ /** @{ */ QUrl getSequenceFile() const { return sequenceFile; } void setSequenceFile(const QUrl &value); /** @} */ /** @brief FITS file whose plate solve produces target coordinates. */ /** @{ */ QUrl getFITSFile() const { return fitsFile; } void setFITSFile(const QUrl &value); /** @} */ /** @brief Minimal target altitude to process this job */ /** @{ */ double getMinAltitude() const { return minAltitude; } void setMinAltitude(const double &value); /** @} */ /** @brief Minimal Moon separation to process this job. */ /** @{ */ double getMinMoonSeparation() const { return minMoonSeparation; } void setMinMoonSeparation(const double &value); /** @} */ /** @brief Whether to restrict this job to good weather. */ /** @{ */ bool getEnforceWeather() const { return enforceWeather; } void setEnforceWeather(bool value); /** @} */ /** @brief Mask of actions to process for this job. */ /** @{ */ StepPipeline getStepPipeline() const { return stepPipeline; } void setStepPipeline(const StepPipeline &value); /** @} */ /** @brief Condition under which this job starts. */ /** @{ */ StartupCondition getStartupCondition() const { return startupCondition; } void setStartupCondition(const StartupCondition &value); /** @} */ /** @brief Condition under which this job completes. */ /** @{ */ CompletionCondition getCompletionCondition() const { return completionCondition; } void setCompletionCondition(const CompletionCondition &value); /** @} */ /** @brief Target culmination proximity under which this job starts. */ /** @{ */ int16_t getCulminationOffset() const { return culminationOffset; } void setCulminationOffset(const int16_t &value); /** @} */ /** @brief Timestamp format to use when displaying information about this job. */ /** @{ */ QString const & getDateTimeDisplayFormat() const { return dateTimeDisplayFormat; } 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. */ /** @{ */ bool getInSequenceFocus() const { return inSequenceFocus; } void setInSequenceFocus(bool value); /** @} */ /** @brief Job priority, low priority value means most prioritary. */ /** @{ */ uint8_t getPriority() const { return priority; } void setPriority(const uint8_t &value); /** @} */ /** @brief Whether to restrict job to night time. */ /** @{ */ bool getEnforceTwilight() const { return enforceTwilight; } void setEnforceTwilight(bool value); /** @} */ /** @brief Current name of the scheduler job. */ /** @{ */ QString getName() const { return name; } void setName(const QString &value); /** @} */ /** @brief Shortcut to widget cell for job name in the job queue table. */ /** @{ */ QTableWidgetItem *getNameCell() const { return nameCell; } 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); /** @} */ /** @brief Shortcut to widget cell for job state in the job queue table. */ /** @{ */ QTableWidgetItem *getStatusCell() const { return statusCell; } void setStatusCell(QTableWidgetItem *cell); /** @} */ /** @brief Current stage of the scheduler job. */ /** @{ */ JOBStage getStage() const { return stage; } void setStage(const JOBStage &value); /** @} */ /** @brief Shortcut to widget cell for job stage in the job queue table. */ /** @{ */ QTableWidgetItem *getStageCell() const { return stageCell; } void setStageCell(QTableWidgetItem *cell); QLabel *getStageLabel() const { return stageLabel; } void setStageLabel(QLabel *label); /** @} */ /** @brief Number of captures required in the associated sequence. */ /** @{ */ int getSequenceCount() const { return sequenceCount; } void setSequenceCount(const int count); /** @} */ /** @brief Number of captures completed in the associated sequence. */ /** @{ */ int getCompletedCount() const { return completedCount; } void setCompletedCount(const int count); /** @} */ /** @brief Shortcut to widget cell for captures in the job queue table. */ /** @{ */ QTableWidgetItem *getCaptureCountCell() const { return captureCountCell; } void setCaptureCountCell(QTableWidgetItem *value); /** @} */ /** @brief Time at which the job must start. */ /** @{ */ QDateTime getStartupTime() const { return startupTime; } void setStartupTime(const QDateTime &value); /** @} */ /** @brief Shortcut to widget cell for startup time in the job queue table. */ /** @{ */ QTableWidgetItem *getStartupCell() const { return startupCell; } void setStartupCell(QTableWidgetItem *value); /** @} */ /** @brief Time after which the job is considered complete. */ /** @{ */ QDateTime getCompletionTime() const { return completionTime; } void setCompletionTime(const QDateTime &value); /** @} */ /** @brief Shortcut to widget cell for completion time in the job queue table. */ /** @{ */ QTableWidgetItem *getCompletionCell() const { return completionCell; } void setCompletionCell(QTableWidgetItem *value); /** @} */ /** @brief Estimation of the time the job will take to process. */ /** @{ */ int64_t getEstimatedTime() const { return estimatedTime; } void setEstimatedTime(const int64_t &value); /** @} */ /** @brief Shortcut to widget cell for estimated time in the job queue table. */ /** @{ */ QTableWidgetItem *getEstimatedTimeCell() const { return estimatedTimeCell; } void setEstimatedTimeCell(QTableWidgetItem *value); /** @} */ /** @brief Current score of the scheduler job. */ /** @{ */ int getScore() const { return score; } void setScore(int value); /** @} */ /** @brief Shortcut to widget cell for job score in the job queue table. */ /** @{ */ QTableWidgetItem *getScoreCell() const { return scoreCell; } void setScoreCell(QTableWidgetItem *value); /** @} */ /** @brief Whether this job requires light frames, or only calibration frames. */ /** @{ */ bool getLightFramesRequired() const { return lightFramesRequired; } void setLightFramesRequired(bool value); /** @} */ /** @brief Number of times this job must be repeated (in terms of capture count). */ /** @{ */ uint16_t getRepeatsRequired() const { return repeatsRequired; } void setRepeatsRequired(const uint16_t &value); /** @} */ /** @brief Number of times this job still has to be repeated (in terms of capture count). */ /** @{ */ uint16_t getRepeatsRemaining() const { return repeatsRemaining; } void setRepeatsRemaining(const uint16_t &value); /** @} */ /** @brief The map of capture counts for this job, keyed by its capture storage signatures. */ /** @{ */ QMap getCapturedFramesMap() const { return capturedFramesMap; } void setCapturedFramesMap(const QMap &value); /** @} */ /** @brief Resetting a job to original values: * - idle state and stage * - original startup, none if asap, else user original setting * - duration not estimated * - full repeat count */ 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. * @return false if the score of b is higher than or equal to the score of a. */ static bool decreasingScoreOrder(SchedulerJob const *a, SchedulerJob const *b); /** @brief Compare ::SchedulerJob instances based on priority. This is a qSort predicate, deprecated in QT5. * @arg a, b are ::SchedulerJob instances to compare. * @return true if the priority of a is lower than the priority of b. * @return false if the priority of a is higher than or equal to the priority of b. */ static bool increasingPriorityOrder(SchedulerJob const *a, SchedulerJob const *b); /** @brief Compare ::SchedulerJob instances based on altitude. This is a qSort predicate, deprecated in QT5. * @arg a, b are ::SchedulerJob instances to compare. * @return true if the altitude of b is lower than the altitude of a. * @return false if the altitude of b is higher than or equal to the altitude of a. */ 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; JOBStatus state { JOB_IDLE }; JOBStage stage { STAGE_IDLE }; StartupCondition fileStartupCondition { START_ASAP }; StartupCondition startupCondition { START_ASAP }; CompletionCondition completionCondition { FINISH_SEQUENCE }; int sequenceCount { 0 }; int completedCount { 0 }; + QDateTime fileStartupTime; QDateTime startupTime; QDateTime completionTime; QUrl sequenceFile; QUrl fitsFile; double minAltitude { -1 }; double minMoonSeparation { -1 }; bool enforceWeather { false }; bool enforceTwilight { false }; StepPipeline stepPipeline { USE_NONE }; /** @internal Widget cell/label shortcuts. */ /** @{ */ QTableWidgetItem *nameCell { nullptr }; QLabel *nameLabel { nullptr }; QTableWidgetItem *statusCell { nullptr }; QTableWidgetItem *stageCell { nullptr }; QLabel *stageLabel { nullptr }; QTableWidgetItem *startupCell { nullptr }; QTableWidgetItem *completionCell { nullptr }; QTableWidgetItem *estimatedTimeCell { nullptr }; QTableWidgetItem *captureCountCell { nullptr }; QTableWidgetItem *scoreCell { nullptr }; /** @} */ /** @internal General cell refresh. */ void updateJobCell(); int score { 0 }; int16_t culminationOffset { 0 }; uint8_t priority { 10 }; int64_t estimatedTime { -1 }; uint16_t repeatsRequired { 1 }; uint16_t repeatsRemaining { 1 }; bool inSequenceFocus { false }; QString dateTimeDisplayFormat; bool lightFramesRequired { false }; QMap capturedFramesMap; };