1 /*
2     SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     DBus calls from GSoC 2015 Ekos Scheduler project:
5     SPDX-FileCopyrightText: 2015 Daniel Leu <daniel_mihai.leu@cti.pub.ro>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "scheduler.h"
11 
12 #include "ksalmanac.h"
13 #include "ksnotification.h"
14 #include "kstars.h"
15 #include "kstarsdata.h"
16 #include "ksutils.h"
17 #include "skymap.h"
18 #include "mosaic.h"
19 #include "Options.h"
20 #include "scheduleradaptor.h"
21 #include "schedulerjob.h"
22 #include "skymapcomposite.h"
23 #include "auxiliary/QProgressIndicator.h"
24 #include "dialogs/finddialog.h"
25 #include "ekos/manager.h"
26 #include "ekos/capture/sequencejob.h"
27 #include "ekos/capture/placeholderpath.h"
28 #include "skyobjects/starobject.h"
29 
30 #include <KNotifications/KNotification>
31 #include <KConfigDialog>
32 
33 #include <fitsio.h>
34 #include <ekos_scheduler_debug.h>
35 #include <indicom.h>
36 
37 #define BAD_SCORE                -1000
38 #define MAX_FAILURE_ATTEMPTS      5
39 #define RESTART_GUIDING_DELAY_MS  5000
40 
41 #define DEFAULT_CULMINATION_TIME    -60
42 #define DEFAULT_MIN_ALTITUDE        15
43 #define DEFAULT_MIN_MOON_SEPARATION 0
44 
45 // This is a temporary debugging printout introduced while gaining experience developing
46 // the unit tests in test_ekos_scheduler_ops.cpp.
47 // All these printouts should be eventually removed.
48 #define TEST_PRINT if (false) fprintf
49 
50 namespace Ekos
51 {
52 
53 // Functions to make human-readable debug messages for the various enums.
54 
timerStr(Scheduler::SchedulerTimerState state)55 QString timerStr(Scheduler::SchedulerTimerState state)
56 {
57     switch (state)
58     {
59         case Scheduler::RUN_WAKEUP:
60             return QString("RUN_WAKEUP");
61         case Scheduler::RUN_SCHEDULER:
62             return QString("RUN_SCHEDULER");
63         case Scheduler::RUN_JOBCHECK:
64             return QString("RUN_JOBCHECK");
65         case Scheduler::RUN_SHUTDOWN:
66             return QString("RUN_SHUTDOWN");
67         case Scheduler::RUN_NOTHING:
68             return QString("RUN_NOTHING");
69     }
70     return QString("????");
71 }
72 
ekosStateString(Scheduler::EkosState state)73 QString ekosStateString(Scheduler::EkosState state)
74 {
75     switch(state)
76     {
77         case Scheduler::EKOS_IDLE:
78             return "EKOS_IDLE";
79         case Scheduler::EKOS_STARTING:
80             return "EKOS_STARTING";
81         case Scheduler::EKOS_STOPPING:
82             return "EKOS_STOPPING";
83         case Scheduler::EKOS_READY:
84             return "EKOS_READY";
85     }
86     return QString("????");
87 }
88 
indiStateString(Scheduler::INDIState state)89 QString indiStateString(Scheduler::INDIState state)
90 {
91     switch(state)
92     {
93         case Scheduler::INDI_IDLE:
94             return "INDI_IDLE";
95         case Scheduler::INDI_PROPERTY_CHECK:
96             return "INDI_PROPERTY_CHECK";
97         case Scheduler::INDI_CONNECTING:
98             return "INDI_CONNECTING";
99         case Scheduler::INDI_DISCONNECTING:
100             return "INDI_DISCONNECTING";
101         case Scheduler::INDI_READY:
102             return "INDI_READY";
103     }
104     return QString("????");
105 }
106 
startupStateString(Scheduler::StartupState state)107 QString startupStateString(Scheduler::StartupState state)
108 {
109     switch(state)
110     {
111         case Scheduler::STARTUP_IDLE:
112             return "STARTUP_IDLE";
113         case Scheduler::STARTUP_SCRIPT:
114             return "STARTUP_SCRIPT";
115         case Scheduler::STARTUP_UNPARK_DOME:
116             return "STARTUP_UNPARK_DOME";
117         case Scheduler::STARTUP_UNPARKING_DOME:
118             return "STARTUP_UNPARKING_DOME";
119         case Scheduler::STARTUP_UNPARK_MOUNT:
120             return "STARTUP_UNPARK_MOUNT";
121         case Scheduler::STARTUP_UNPARKING_MOUNT:
122             return "STARTUP_UNPARKING_MOUNT";
123         case Scheduler::STARTUP_UNPARK_CAP:
124             return "STARTUP_UNPARK_CAP";
125         case Scheduler::STARTUP_UNPARKING_CAP:
126             return "STARTUP_UNPARKING_CAP";
127         case Scheduler::STARTUP_ERROR:
128             return "STARTUP_ERROR";
129         case Scheduler::STARTUP_COMPLETE:
130             return "STARTUP_COMPLETE";
131     }
132     return QString("????");
133 }
134 
shutdownStateString(Scheduler::ShutdownState state)135 QString shutdownStateString(Scheduler::ShutdownState state)
136 {
137     switch(state)
138     {
139         case Scheduler::SHUTDOWN_IDLE:
140             return "SHUTDOWN_IDLE";
141         case Scheduler::SHUTDOWN_PARK_CAP:
142             return "SHUTDOWN_PARK_CAP";
143         case Scheduler::SHUTDOWN_PARKING_CAP:
144             return "SHUTDOWN_PARKING_CAP";
145         case Scheduler::SHUTDOWN_PARK_MOUNT:
146             return "SHUTDOWN_PARK_MOUNT";
147         case Scheduler::SHUTDOWN_PARKING_MOUNT:
148             return "SHUTDOWN_PARKING_MOUNT";
149         case Scheduler::SHUTDOWN_PARK_DOME:
150             return "SHUTDOWN_PARK_DOME";
151         case Scheduler::SHUTDOWN_PARKING_DOME:
152             return "SHUTDOWN_PARKING_DOME";
153         case Scheduler::SHUTDOWN_SCRIPT:
154             return "SHUTDOWN_SCRIPT";
155         case Scheduler::SHUTDOWN_SCRIPT_RUNNING:
156             return "SHUTDOWN_SCRIPT_RUNNING";
157         case Scheduler::SHUTDOWN_ERROR:
158             return "SHUTDOWN_ERROR";
159         case Scheduler::SHUTDOWN_COMPLETE:
160             return "SHUTDOWN_COMPLETE";
161     }
162     return QString("????");
163 }
164 
parkWaitStateString(Scheduler::ParkWaitStatus state)165 QString parkWaitStateString(Scheduler::ParkWaitStatus state)
166 {
167     switch(state)
168     {
169         case Scheduler::PARKWAIT_IDLE:
170             return "PARKWAIT_IDLE";
171         case Scheduler::PARKWAIT_PARK:
172             return "PARKWAIT_PARK";
173         case Scheduler::PARKWAIT_PARKING:
174             return "PARKWAIT_PARKING";
175         case Scheduler::PARKWAIT_PARKED:
176             return "PARKWAIT_PARKED";
177         case Scheduler::PARKWAIT_UNPARK:
178             return "PARKWAIT_UNPARK";
179         case Scheduler::PARKWAIT_UNPARKING:
180             return "PARKWAIT_UNPARKING";
181         case Scheduler::PARKWAIT_UNPARKED:
182             return "PARKWAIT_UNPARKED";
183         case Scheduler::PARKWAIT_ERROR:
184             return "PARKWAIT_ERROR";
185     }
186     return QString("????");
187 }
188 
commStatusString(Ekos::CommunicationStatus state)189 QString commStatusString(Ekos::CommunicationStatus state)
190 {
191     switch(state)
192     {
193         case Ekos::Idle:
194             return "Idle";
195         case Ekos::Pending:
196             return "Pending";
197         case Ekos::Success:
198             return "Success";
199         case Ekos::Error:
200             return "Error";
201     }
202     return QString("????");
203 }
204 
schedulerStateString(Ekos::SchedulerState state)205 QString schedulerStateString(Ekos::SchedulerState state)
206 {
207     switch(state)
208     {
209         case Ekos::SCHEDULER_IDLE:
210             return "SCHEDULER_IDLE";
211         case Ekos::SCHEDULER_STARTUP:
212             return "SCHEDULER_STARTUP";
213         case Ekos::SCHEDULER_RUNNING:
214             return "SCHEDULER_RUNNING";
215         case Ekos::SCHEDULER_PAUSED:
216             return "SCHEDULER_PAUSED";
217         case Ekos::SCHEDULER_SHUTDOWN:
218             return "SCHEDULER_SHUTDOWN";
219         case Ekos::SCHEDULER_ABORTED:
220             return "SCHEDULER_ABORTED";
221         case Ekos::SCHEDULER_LOADING:
222             return "SCHEDULER_LOADING";
223     }
224     return QString("????");
225 }
226 
printStates(const QString & label)227 void Scheduler::printStates(const QString &label)
228 {
229     TEST_PRINT(stderr, "%s",
230                QString("%1 %2 %3%4 %5 %6 %7 %8 %9\n")
231                .arg(label)
232                .arg(timerStr(timerState))
233                .arg(schedulerStateString(state))
234                .arg((timerState == Scheduler::RUN_JOBCHECK && currentJob != nullptr) ?
235                     QString("(%1 %2)").arg(SchedulerJob::jobStatusString(currentJob->getState()))
236                     .arg(SchedulerJob::jobStageString(currentJob->getStage())) : "")
237                .arg(ekosStateString(ekosState))
238                .arg(indiStateString(indiState))
239                .arg(startupStateString(startupState))
240                .arg(shutdownStateString(shutdownState))
241                .arg(parkWaitStateString(parkWaitState)).toLatin1().data());
242 }
243 
244 QDateTime Scheduler::Dawn, Scheduler::Dusk, Scheduler::preDawnDateTime;
245 
246 // Allows for unit testing of static Scheduler methods,
247 // as can't call KStarsData::Instance() during unit testing.
248 KStarsDateTime *Scheduler::storedLocalTime = nullptr;
getLocalTime()249 KStarsDateTime Scheduler::getLocalTime()
250 {
251     if (hasLocalTime())
252         return *storedLocalTime;
253     return KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc());
254 }
255 
256 // This is the initial conditions that need to be set before starting.
init()257 void Scheduler::init()
258 {
259     // This is needed to get wakeupScheduler() to call start() and startup,
260     // instead of assuming it is already initialized (if preemptiveShutdown was not set).
261     // The time itself is not used.
262     enablePreemptiveShutdown(getLocalTime());
263 
264     iterationSetup = false;
265     setupNextIteration(RUN_WAKEUP, 10);
266 }
267 
268 // Setup the main loop and run.
run()269 void Scheduler::run()
270 {
271     init();
272     iterate();
273 }
274 
275 // This is the main scheduler loop.
276 // Run an iteration, get the sleep time, sleep for that interval, and repeat.
iterate()277 void Scheduler::iterate()
278 {
279     const int msSleep = runSchedulerIteration();
280     if (msSleep < 0)
281         return;
282 
283     connect(&iterationTimer, &QTimer::timeout, this, &Scheduler::iterate, Qt::UniqueConnection);
284     iterationTimer.setSingleShot(true);
285     iterationTimer.start(msSleep);
286 }
287 
currentlySleeping()288 bool Scheduler::currentlySleeping()
289 {
290     return iterationTimer.isActive() && timerState == RUN_WAKEUP;
291 }
292 
runSchedulerIteration()293 int Scheduler::runSchedulerIteration()
294 {
295     qint64 now = QDateTime::currentMSecsSinceEpoch();
296     if (startMSecs == 0)
297         startMSecs = now;
298 
299     printStates(QString("\nrunScheduler Iteration %1 @ %2")
300                 .arg(++schedulerIteration)
301                 .arg((now - startMSecs) / 1000.0, 1, 'f', 3));
302 
303     SchedulerTimerState keepTimerState = timerState;
304 
305     // TODO: At some point we should require that timerState and timerInterval
306     // be explicitly set in all iterations. Not there yet, would require too much
307     // refactoring of the scheduler. When we get there, we'd exectute the following here:
308     // timerState = RUN_NOTHING;    // don't like this comment, it should always set a state and interval!
309     // timerInterval = -1;
310     iterationSetup = false;
311     switch (keepTimerState)
312     {
313         case RUN_WAKEUP:
314             wakeUpScheduler();
315             break;
316         case RUN_SCHEDULER:
317             checkStatus();
318             break;
319         case RUN_JOBCHECK:
320             checkJobStage();
321             break;
322         case RUN_SHUTDOWN:
323             checkShutdownState();
324             break;
325         case RUN_NOTHING:
326             timerInterval = -1;
327             break;
328     }
329     if (!iterationSetup)
330     {
331         // See the above TODO.
332         // Since iterations aren't yet always set up, we repeat the current
333         // iteration type if one wasn't set up in the current iteration.
334         // qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler iteration never set up.";
335         TEST_PRINT(stderr, "Scheduler iteration never set up--repeating %s %d...\n",
336                    timerStr(timerState).toLatin1().data(), timerInterval);
337     }
338     printStates(QString("End iteration, sleep %1: ").arg(timerInterval));
339     return timerInterval;
340 }
setupNextIteration(SchedulerTimerState nextState)341 void Scheduler::setupNextIteration(SchedulerTimerState nextState)
342 {
343     setupNextIteration(nextState, m_UpdatePeriodMs);
344 }
345 
setupNextIteration(SchedulerTimerState nextState,int milliseconds)346 void Scheduler::setupNextIteration(SchedulerTimerState nextState, int milliseconds)
347 {
348     if (iterationSetup)
349     {
350         qCDebug(KSTARS_EKOS_SCHEDULER)
351                 << QString("Multiple setupNextIteration calls: current %1 %2, previous %3 %4")
352                 .arg(nextState).arg(milliseconds).arg(timerState).arg(timerInterval);
353         TEST_PRINT(stderr, "Multiple setupNextIteration calls: current %s %d, previous %s %d.\n",
354                    timerStr(nextState).toLatin1().data(), milliseconds,
355                    timerStr(timerState).toLatin1().data(), timerInterval);
356     }
357     timerState = nextState;
358     timerInterval = milliseconds;
359     iterationSetup = true;
360 }
361 
Scheduler()362 Scheduler::Scheduler()
363 {
364     // Use the default path and interface when running the scheduler.
365     setupScheduler(ekosPathString, ekosInterfaceString);
366 }
367 
Scheduler(const QString & ekosPathStr,const QString & ekosInterfaceStr)368 Scheduler::Scheduler(const QString &ekosPathStr, const QString &ekosInterfaceStr)
369 {
370     // During testing, when mocking ekos, use a special purpose path and interface.
371     setupScheduler(ekosPathStr, ekosInterfaceStr);
372 }
373 
setupScheduler(const QString & ekosPathStr,const QString & ekosInterfaceStr)374 void Scheduler::setupScheduler(const QString &ekosPathStr, const QString &ekosInterfaceStr)
375 {
376     setupUi(this);
377 
378     qRegisterMetaType<Ekos::SchedulerState>("Ekos::SchedulerState");
379     qDBusRegisterMetaType<Ekos::SchedulerState>();
380 
381     dirPath = QUrl::fromLocalFile(QDir::homePath());
382 
383     // Get current KStars time and set seconds to zero
384     QDateTime currentDateTime = getLocalTime();
385     QTime currentTime         = currentDateTime.time();
386     currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0);
387     currentDateTime.setTime(currentTime);
388 
389     // Set initial time for startup and completion times
390     startupTimeEdit->setDateTime(currentDateTime);
391     completionTimeEdit->setDateTime(currentDateTime);
392 
393     // Set up DBus interfaces
394     new SchedulerAdaptor(this);
395     QDBusConnection::sessionBus().unregisterObject("/KStars/Ekos/Scheduler");
396     if (!QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this))
397         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Scheduler failed to register with dbus");
398     ekosInterface = new QDBusInterface("org.kde.kstars", ekosPathStr, ekosInterfaceStr,
399                                        QDBusConnection::sessionBus(), this);
400 
401     // Example of connecting DBus signals
402     //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus)));
403     //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus)));
404     //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString)));
405     QDBusConnection::sessionBus().connect("org.kde.kstars", ekosPathStr, ekosInterfaceStr, "newModule", this,
406                                           SLOT(registerNewModule(QString)));
407     QDBusConnection::sessionBus().connect("org.kde.kstars", ekosPathStr, ekosInterfaceStr, "indiStatusChanged",
408                                           this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus)));
409     QDBusConnection::sessionBus().connect("org.kde.kstars", ekosPathStr, ekosInterfaceStr, "ekosStatusChanged",
410                                           this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus)));
411 
412     sleepLabel->setPixmap(
413         QIcon::fromTheme("chronometer").pixmap(QSize(32, 32)));
414     sleepLabel->hide();
415 
416     pi = new QProgressIndicator(this);
417     bottomLayout->addWidget(pi, 0, nullptr);
418 
419     geo = KStarsData::Instance()->geo();
420 
421     raBox->setDegType(false); //RA box should be HMS-style
422 
423     /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */
424 
425     queueTable->setToolTip(
426         i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields."));
427 
428     /* Set first button mode to add observation job from left-hand fields */
429     setJobAddApply(true);
430 
431     removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
432     removeFromQueueB->setToolTip(
433         i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal."));
434     removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
435 
436     queueUpB->setIcon(QIcon::fromTheme("go-up"));
437     queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n"
438                               "Order only affect observation jobs that are scheduled to start at the same time.\n"
439                               "Not available if option \"Sort jobs by Altitude and Priority\" is set."));
440     queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
441     queueDownB->setIcon(QIcon::fromTheme("go-down"));
442     queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n"
443                                 "Order only affect observation jobs that are scheduled to start at the same time.\n"
444                                 "Not available if option \"Sort jobs by Altitude and Priority\" is set."));
445     queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
446 
447     evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot"));
448     evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs."));
449     evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
450     sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical"));
451     sortJobsB->setToolTip(
452         i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n"
453              "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n"
454              "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n"
455              "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs."));
456     sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
457     mosaicB->setIcon(QIcon::fromTheme("zoom-draw"));
458     mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
459 
460     queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
461     queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
462     queueSaveB->setIcon(QIcon::fromTheme("document-save"));
463     queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
464     queueLoadB->setIcon(QIcon::fromTheme("document-open"));
465     queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
466     queueAppendB->setIcon(QIcon::fromTheme("document-import"));
467     queueAppendB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
468 
469     loadSequenceB->setIcon(QIcon::fromTheme("document-open"));
470     loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
471     selectStartupScriptB->setIcon(QIcon::fromTheme("document-open"));
472     selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
473     selectShutdownScriptB->setIcon(
474         QIcon::fromTheme("document-open"));
475     selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
476     selectFITSB->setIcon(QIcon::fromTheme("document-open"));
477     selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
478 
479     startupB->setIcon(
480         QIcon::fromTheme("media-playback-start"));
481     startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
482     shutdownB->setIcon(
483         QIcon::fromTheme("media-playback-start"));
484     shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
485 
486     connect(startupB, &QPushButton::clicked, this, &Scheduler::runStartupProcedure);
487     connect(shutdownB, &QPushButton::clicked, this, &Scheduler::runShutdownProcedure);
488 
489     connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject);
490     connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS);
491     connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence);
492     connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript);
493     connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript);
494 
495     connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool);
496     connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob);
497     connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob);
498     connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp);
499     connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown);
500     connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation);
501     connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude);
502     connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
503             &Scheduler::queueTableSelectionChanged);
504     connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable);
505     connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob);
506 
507     startB->setIcon(QIcon::fromTheme("media-playback-start"));
508     startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
509     pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
510     pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
511     pauseB->setCheckable(false);
512 
513     connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler);
514     connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause);
515 
516     connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs);
517     connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save);
518     connect(queueLoadB, &QPushButton::clicked, this, [&]()
519     {
520         load(true);
521     });
522     connect(queueAppendB, &QPushButton::clicked, this, [&]()
523     {
524         load(false);
525     });
526 
527     connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning);
528 
529     // Connect simulation clock scale
530     connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &Scheduler::simClockScaleChanged);
531     connect(KStarsData::Instance()->clock(), &SimClock::timeChanged, this, &Scheduler::simClockTimeChanged);
532 
533     // Connect geographical location - when it is available
534     //connect(KStarsData::Instance()..., &LocationDialog::locationChanged..., this, &Scheduler::simClockTimeChanged);
535 
536     // restore default values for error handling strategy
537     setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy()));
538     errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors());
539     errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay());
540 
541     // save new default values for error handling strategy
542 
543     connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [](bool checked)
544     {
545         Options::setRescheduleErrors(checked);
546     });
547     connect(errorHandlingButtonGroup, static_cast<void (QButtonGroup::*)(QAbstractButton *)>
548             (&QButtonGroup::buttonClicked), [this](QAbstractButton * button)
549     {
550         Q_UNUSED(button)
551         Options::setErrorHandlingStrategy(getErrorHandlingStrategy());
552     });
553     connect(errorHandlingDelaySB, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [](int value)
554     {
555         Options::setErrorHandlingStrategyDelay(value);
556     });
557 
558     connect(copySkyCenterB, &QPushButton::clicked, this, [this]()
559     {
560         SkyPoint center = SkyMap::Instance()->getCenterPoint();
561         //center.deprecess(KStarsData::Instance()->updateNum());
562         center.catalogueCoord(KStarsData::Instance()->updateNum()->julianDay());
563         raBox->setDMS(center.ra0().toHMSString());
564         decBox->setDMS(center.dec0().toDMSString());
565     });
566 
567     connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig);
568 
569     calculateDawnDusk();
570     updateNightTime();
571 
572     loadProfiles();
573 
574     watchJobChanges(true);
575 }
576 
getCurrentJobName()577 QString Scheduler::getCurrentJobName()
578 {
579     return (currentJob != nullptr ? currentJob->getName() : "");
580 }
581 
watchJobChanges(bool enable)582 void Scheduler::watchJobChanges(bool enable)
583 {
584     /* Don't double watch, this will cause multiple signals to be connected */
585     if (enable == jobChangesAreWatched)
586         return;
587 
588     /* These are the widgets we want to connect, per signal function, to listen for modifications */
589     QLineEdit * const lineEdits[] =
590     {
591         nameEdit,
592         raBox,
593         decBox,
594         fitsEdit,
595         sequenceEdit,
596         startupScript,
597         shutdownScript
598     };
599 
600     QDateTimeEdit * const dateEdits[] =
601     {
602         startupTimeEdit,
603         completionTimeEdit
604     };
605 
606     QComboBox * const comboBoxes[] =
607     {
608         schedulerProfileCombo
609     };
610 
611     QButtonGroup * const buttonGroups[] =
612     {
613         stepsButtonGroup,
614         errorHandlingButtonGroup,
615         startupButtonGroup,
616         constraintButtonGroup,
617         completionButtonGroup,
618         startupProcedureButtonGroup,
619         shutdownProcedureGroup
620     };
621 
622     QAbstractButton * const buttons[] =
623     {
624         errorHandlingRescheduleErrorsCB
625     };
626 
627     QSpinBox * const spinBoxes[] =
628     {
629         culminationOffset,
630         repeatsSpin,
631         prioritySpin,
632         errorHandlingDelaySB
633     };
634 
635     QDoubleSpinBox * const dspinBoxes[] =
636     {
637         minMoonSeparation,
638         minAltitude
639     };
640 
641     if (enable)
642     {
643         /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will
644          * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions
645          * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the
646          * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'.
647          * The main problem with this implementation compared to the macro method is that it is now possible to
648          * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot
649          * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to
650          * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart.
651          */
652         for (auto * const control : lineEdits)
653             connect(control, &QLineEdit::editingFinished, this, [this]()
654         {
655             setDirty();
656         });
657         for (auto * const control : dateEdits)
658             connect(control, &QDateTimeEdit::editingFinished, this, [this]()
659         {
660             setDirty();
661         });
662         for (auto * const control : comboBoxes)
663             connect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this]()
664         {
665             setDirty();
666         });
667         for (auto * const control : buttonGroups)
668             connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, [this](int, bool)
669         {
670             setDirty();
671         });
672         for (auto * const control : buttons)
673             connect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, [this](bool)
674         {
675             setDirty();
676         });
677         for (auto * const control : spinBoxes)
678             connect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
679         {
680             setDirty();
681         });
682         for (auto * const control : dspinBoxes)
683             connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this](double)
684         {
685             setDirty();
686         });
687     }
688     else
689     {
690         /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets,
691          * because we did not take care to keep the connection object when connecting. No problem in our case, we do not
692          * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to
693          * disconnect selectively.
694          */
695         for (auto * const control : lineEdits)
696             disconnect(control, &QLineEdit::editingFinished, this, nullptr);
697         for (auto * const control : dateEdits)
698             disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr);
699         for (auto * const control : comboBoxes)
700             disconnect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, nullptr);
701         for (auto * const control : buttons)
702             disconnect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, nullptr);
703         for (auto * const control : buttonGroups)
704             disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, nullptr);
705         for (auto * const control : spinBoxes)
706             disconnect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, nullptr);
707         for (auto * const control : dspinBoxes)
708             disconnect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, nullptr);
709     }
710 
711     jobChangesAreWatched = enable;
712 }
713 
appendLogText(const QString & text)714 void Scheduler::appendLogText(const QString &text)
715 {
716     /* FIXME: user settings for log length */
717     int const max_log_count = 2000;
718     if (m_LogText.size() > max_log_count)
719         m_LogText.removeLast();
720 
721     m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
722                             getLocalTime().toString("yyyy-MM-ddThh:mm:ss"), text));
723 
724     qCInfo(KSTARS_EKOS_SCHEDULER) << text;
725 
726     emit newLog(text);
727 }
728 
clearLog()729 void Scheduler::clearLog()
730 {
731     m_LogText.clear();
732     emit newLog(QString());
733 }
734 
applyConfig()735 void Scheduler::applyConfig()
736 {
737     calculateDawnDusk();
738     updateNightTime();
739 
740     if (SCHEDULER_RUNNING != state)
741     {
742         evaluateJobs(true);
743     }
744 }
745 
selectObject()746 void Scheduler::selectObject()
747 {
748     if (FindDialog::Instance()->execWithParent(Ekos::Manager::Instance()) == QDialog::Accepted)
749     {
750         SkyObject *object = FindDialog::Instance()->targetObject();
751         addObject(object);
752     }
753 }
754 
addObject(SkyObject * object)755 void Scheduler::addObject(SkyObject *object)
756 {
757     if (object != nullptr)
758     {
759         QString finalObjectName(object->name());
760 
761         if (object->name() == "star")
762         {
763             StarObject *s = dynamic_cast<StarObject *>(object);
764 
765             if (s->getHDIndex() != 0)
766                 finalObjectName = QString("HD %1").arg(s->getHDIndex());
767         }
768 
769         nameEdit->setText(finalObjectName);
770         raBox->showInHours(object->ra0());
771         decBox->showInDegrees(object->dec0());
772 
773         addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false);
774         mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false);
775 
776         setDirty();
777     }
778 }
779 
selectFITS()780 void Scheduler::selectFITS()
781 {
782     fitsURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select FITS Image"), dirPath,
783                                           "FITS (*.fits *.fit)");
784     if (fitsURL.isEmpty())
785         return;
786 
787     dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename));
788 
789     fitsEdit->setText(fitsURL.toLocalFile());
790 
791     if (nameEdit->text().isEmpty())
792         nameEdit->setText(fitsURL.fileName());
793 
794     addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false);
795     mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false);
796 
797     processFITSSelection();
798 
799     setDirty();
800 }
801 
processFITSSelection()802 void Scheduler::processFITSSelection()
803 {
804     const QString filename = fitsEdit->text();
805     int status = 0;
806     double ra = 0, dec = 0;
807     dms raDMS, deDMS;
808     char comment[128], error_status[512];
809     fitsfile *fptr = nullptr;
810 
811     if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status))
812     {
813         fits_report_error(stderr, status);
814         fits_get_errstatus(status, error_status);
815         qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status);
816         return;
817     }
818 
819     status = 0;
820     if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
821     {
822         fits_report_error(stderr, status);
823         fits_get_errstatus(status, error_status);
824         qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status);
825         return;
826     }
827 
828     status = 0;
829     char objectra_str[32] = {0};
830     if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status))
831     {
832         if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status))
833         {
834             fits_report_error(stderr, status);
835             fits_get_errstatus(status, error_status);
836             appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status)));
837             return;
838         }
839 
840         raDMS.setD(ra);
841     }
842     else
843     {
844         raDMS = dms::fromString(objectra_str, false);
845     }
846 
847     status = 0;
848     char objectde_str[32] = {0};
849     if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status))
850     {
851         if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status))
852         {
853             fits_report_error(stderr, status);
854             fits_get_errstatus(status, error_status);
855             appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status)));
856             return;
857         }
858 
859         deDMS.setD(dec);
860     }
861     else
862     {
863         deDMS = dms::fromString(objectde_str, true);
864     }
865 
866     raBox->setDMS(raDMS.toHMSString());
867     decBox->setDMS(deDMS.toDMSString());
868 
869     char object_str[256] = {0};
870     if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status))
871     {
872         QFileInfo info(filename);
873         nameEdit->setText(info.completeBaseName());
874     }
875     else
876     {
877         nameEdit->setText(object_str);
878     }
879 }
880 
setSequence(const QString & sequenceFileURL)881 void Scheduler::setSequence(const QString &sequenceFileURL)
882 {
883     sequenceURL = QUrl::fromLocalFile(sequenceFileURL);
884 
885     if (sequenceFileURL.isEmpty())
886         return;
887     dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename));
888 
889     sequenceEdit->setText(sequenceURL.toLocalFile());
890 
891     // For object selection, all fields must be filled
892     if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false)
893             // For FITS selection, only the name and fits URL should be filled.
894             || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false))
895     {
896         addToQueueB->setEnabled(true);
897         mosaicB->setEnabled(true);
898     }
899 
900     setDirty();
901 }
902 
selectSequence()903 void Scheduler::selectSequence()
904 {
905     QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"),
906                    dirPath.toLocalFile(),
907                    i18n("Ekos Sequence Queue (*.esq)"));
908 
909     setSequence(file);
910 }
911 
selectStartupScript()912 void Scheduler::selectStartupScript()
913 {
914     startupScriptURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select Startup Script"),
915                        dirPath,
916                        i18n("Script (*)"));
917     if (startupScriptURL.isEmpty())
918         return;
919 
920     dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename));
921 
922     mDirty = true;
923     startupScript->setText(startupScriptURL.toLocalFile());
924 }
925 
selectShutdownScript()926 void Scheduler::selectShutdownScript()
927 {
928     shutdownScriptURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select Shutdown Script"),
929                         dirPath,
930                         i18n("Script (*)"));
931     if (shutdownScriptURL.isEmpty())
932         return;
933 
934     dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename));
935 
936     mDirty = true;
937     shutdownScript->setText(shutdownScriptURL.toLocalFile());
938 }
939 
addJob()940 void Scheduler::addJob()
941 {
942     if (0 <= jobUnderEdit)
943     {
944         /* If a job is being edited, reset edition mode as all fields are already transferred to the job */
945         resetJobEdit();
946     }
947     else
948     {
949         /* If a job is being added, save fields into a new job */
950         saveJob();
951         /* There is now an evaluation for each change, so don't duplicate the evaluation now */
952         // jobEvaluationOnly = true;
953         // evaluateJobs();
954     }
955 }
956 
setupJob(SchedulerJob & job,const QString & name,int priority,const dms & ra,const dms & dec,double djd,double rotation,const QUrl & sequenceUrl,const QUrl & fitsUrl,SchedulerJob::StartupCondition startup,const QDateTime & startupTime,int16_t startupOffset,SchedulerJob::CompletionCondition completion,const QDateTime & completionTime,int completionRepeats,double minimumAltitude,double minimumMoonSeparation,bool enforceWeather,bool enforceTwilight,bool enforceArtificialHorizon,bool track,bool focus,bool align,bool guide)957 void Scheduler::setupJob(
958     SchedulerJob &job, const QString &name, int priority, const dms &ra,
959     const dms &dec, double djd, double rotation, const QUrl &sequenceUrl, const QUrl &fitsUrl,
960     SchedulerJob::StartupCondition startup, const QDateTime &startupTime,
961     int16_t startupOffset,
962     SchedulerJob::CompletionCondition completion,
963     const QDateTime &completionTime, int completionRepeats,
964     double minimumAltitude, double minimumMoonSeparation,
965     bool enforceWeather, bool enforceTwilight, bool enforceArtificialHorizon,
966     bool track, bool focus, bool align, bool guide)
967 {
968     /* Configure or reconfigure the observation job */
969 
970     job.setName(name);
971     job.setPriority(priority);
972     // djd should be ut.djd
973     job.setTargetCoords(ra, dec, djd);
974     job.setRotation(rotation);
975 
976     /* Consider sequence file is new, and clear captured frames map */
977     job.setCapturedFramesMap(SchedulerJob::CapturedFramesMap());
978     job.setSequenceFile(sequenceUrl);
979     job.setFITSFile(fitsUrl);
980     // #1 Startup conditions
981 
982     job.setStartupCondition(startup);
983     if (startup == SchedulerJob::START_CULMINATION)
984     {
985         job.setCulminationOffset(startupOffset);
986     }
987     else if (startup == SchedulerJob::START_AT)
988     {
989         job.setStartupTime(startupTime);
990     }
991     /* Store the original startup condition */
992     job.setFileStartupCondition(job.getStartupCondition());
993     job.setFileStartupTime(job.getStartupTime());
994 
995     // #2 Constraints
996 
997     job.setMinAltitude(minimumAltitude);
998     job.setMinMoonSeparation(minimumMoonSeparation);
999 
1000     // Check enforce weather constraints
1001     job.setEnforceWeather(enforceWeather);
1002     // twilight constraints
1003     job.setEnforceTwilight(enforceTwilight);
1004     job.setEnforceArtificialHorizon(enforceArtificialHorizon);
1005 
1006     job.setCompletionCondition(completion);
1007     if (completion == SchedulerJob::FINISH_AT)
1008         job.setCompletionTime(completionTime);
1009     else if (completion == SchedulerJob::FINISH_REPEAT)
1010     {
1011         job.setRepeatsRequired(completionRepeats);
1012         job.setRepeatsRemaining(completionRepeats);
1013     }
1014     // Job steps
1015     job.setStepPipeline(SchedulerJob::USE_NONE);
1016     if (track)
1017         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_TRACK));
1018     if (focus)
1019         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_FOCUS));
1020     if (align)
1021         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_ALIGN));
1022     if (guide)
1023         job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_GUIDE));
1024 
1025     /* Store the original startup condition */
1026     job.setFileStartupCondition(job.getStartupCondition());
1027     job.setFileStartupTime(job.getStartupTime());
1028 
1029     /* Reset job state to evaluate the changes */
1030     job.reset();
1031 }
1032 
saveJob()1033 void Scheduler::saveJob()
1034 {
1035     if (state == SCHEDULER_RUNNING)
1036     {
1037         appendLogText(i18n("Warning: You cannot add or modify a job while the scheduler is running."));
1038         return;
1039     }
1040 
1041     if (nameEdit->text().isEmpty())
1042     {
1043         appendLogText(i18n("Warning: Target name is required."));
1044         return;
1045     }
1046 
1047     if (sequenceEdit->text().isEmpty())
1048     {
1049         appendLogText(i18n("Warning: Sequence file is required."));
1050         return;
1051     }
1052 
1053     // Coordinates are required unless it is a FITS file
1054     if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty())
1055     {
1056         appendLogText(i18n("Warning: Target coordinates are required."));
1057         return;
1058     }
1059 
1060     bool raOk = false, decOk = false;
1061     dms /*const*/ ra(raBox->createDms(false, &raOk)); //false means expressed in hours
1062     dms /*const*/ dec(decBox->createDms(true, &decOk));
1063 
1064     if (raOk == false)
1065     {
1066         appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text()));
1067         return;
1068     }
1069 
1070     if (decOk == false)
1071     {
1072         appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text()));
1073         return;
1074     }
1075 
1076     watchJobChanges(false);
1077 
1078     /* Create or Update a scheduler job */
1079     int currentRow = queueTable->currentRow();
1080     SchedulerJob * job = nullptr;
1081 
1082     /* If no row is selected for insertion, append at end of list. */
1083     if (currentRow < 0)
1084         currentRow = queueTable->rowCount();
1085 
1086     /* Add job to queue only if it is new, else reuse current row.
1087      * Make sure job is added at the right index, now that queueTable may have a line selected without being edited.
1088      */
1089     if (0 <= jobUnderEdit)
1090     {
1091         /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from queueTable->currentRow(). */
1092         if (jobUnderEdit != currentRow)
1093             qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table.";
1094 
1095         /* Use the job in the row currently edited */
1096         job = jobs.at(currentRow);
1097     }
1098     else
1099     {
1100         /* Instantiate a new job, insert it in the job list and add a row in the table for it just after the row currently selected. */
1101         job = new SchedulerJob();
1102         jobs.insert(currentRow, job);
1103         queueTable->insertRow(currentRow);
1104     }
1105 
1106     /* Configure or reconfigure the observation job */
1107 
1108     job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat());
1109     fitsURL = QUrl::fromLocalFile(fitsEdit->text());
1110 
1111     // Get several job values depending on the state of the UI.
1112 
1113     SchedulerJob::StartupCondition startCondition = SchedulerJob::START_AT;
1114     if (asapConditionR->isChecked())
1115         startCondition = SchedulerJob::START_ASAP;
1116     else if (culminationConditionR->isChecked())
1117         startCondition = SchedulerJob::START_CULMINATION;
1118 
1119     SchedulerJob::CompletionCondition stopCondition = SchedulerJob::FINISH_AT;
1120     if (sequenceCompletionR->isChecked())
1121         stopCondition = SchedulerJob::FINISH_SEQUENCE;
1122     else if (repeatCompletionR->isChecked())
1123         stopCondition = SchedulerJob::FINISH_REPEAT;
1124     else if (loopCompletionR->isChecked())
1125         stopCondition = SchedulerJob::FINISH_LOOP;
1126 
1127     double altConstraint = SchedulerJob::UNDEFINED_ALTITUDE;
1128     if (altConstraintCheck->isChecked())
1129         altConstraint = minAltitude->value();
1130 
1131     double moonConstraint = -1;
1132     if (moonSeparationCheck->isChecked())
1133         moonConstraint = minMoonSeparation->value();
1134 
1135     // The reason for this kitchen-sink function is to separate the UI from the
1136     // job setup, to allow for testing.
1137     setupJob(
1138         *job, nameEdit->text(), prioritySpin->value(), ra, dec,
1139         KStarsData::Instance()->ut().djd(),
1140         rotationSpin->value(), sequenceURL, fitsURL,
1141 
1142         startCondition, startupTimeEdit->dateTime(), culminationOffset->value(),
1143         stopCondition, completionTimeEdit->dateTime(), repeatsSpin->value(),
1144 
1145         altConstraint,
1146         moonConstraint,
1147         weatherCheck->isChecked(),
1148         twilightCheck->isChecked(),
1149         artificialHorizonCheck->isChecked(),
1150 
1151         trackStepCheck->isChecked(),
1152         focusStepCheck->isChecked(),
1153         alignStepCheck->isChecked(),
1154         guideStepCheck->isChecked()
1155     );
1156 
1157 
1158     /* Verifications */
1159     /* FIXME: perhaps use a method more visible to the end-user */
1160     if (SchedulerJob::START_AT == job->getFileStartupCondition())
1161     {
1162         /* Warn if appending a job which startup time doesn't allow proper score */
1163         if (calculateJobScore(job, Dawn, Dusk, job->getStartupTime()) < 0)
1164             appendLogText(
1165                 i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.",
1166                      job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat())));
1167 
1168     }
1169 
1170     // Warn user if a duplicated job is in the list - same target, same sequence
1171     // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list!
1172     foreach (SchedulerJob *a_job, jobs)
1173     {
1174         if (a_job == job)
1175         {
1176             break;
1177         }
1178         else if (a_job->getName() == job->getName())
1179         {
1180             int const a_job_row = a_job->getNameCell() ? a_job->getNameCell()->row() + 1 : 0;
1181 
1182             /* FIXME: Warning about duplicate jobs only checks the target name, doing it properly would require checking storage for each sequence job of each scheduler job. */
1183             appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, "
1184                                "the scheduler may consider the same storage for captures.",
1185                                job->getName(), currentRow, a_job_row));
1186 
1187             /* Warn the user in case the two jobs are really identical */
1188             if (a_job->getSequenceFile() == job->getSequenceFile())
1189             {
1190                 if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress())
1191                     appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count "
1192                                        "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')",
1193                                        job->getName(), currentRow, a_job_row, job->getRepeatsRequired()));
1194 
1195                 if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority())
1196                     appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, "
1197                                        "as currently they will start in order of insertion in the table",
1198                                        job->getName(), currentRow));
1199             }
1200         }
1201     }
1202 
1203     if (-1 == jobUnderEdit)
1204     {
1205         QTableWidgetItem *nameCell = new QTableWidgetItem();
1206         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_NAME), nameCell);
1207         nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1208         nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1209 
1210         QTableWidgetItem *statusCell = new QTableWidgetItem();
1211         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STATUS), statusCell);
1212         statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1213         statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1214 
1215         QTableWidgetItem *captureCount = new QTableWidgetItem();
1216         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_CAPTURES), captureCount);
1217         captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1218         captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1219 
1220         QTableWidgetItem *scoreValue = new QTableWidgetItem();
1221         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_SCORE), scoreValue);
1222         scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1223         scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1224 
1225         QTableWidgetItem *startupCell = new QTableWidgetItem();
1226         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_STARTTIME), startupCell);
1227         startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1228         startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1229 
1230         QTableWidgetItem *altitudeCell = new QTableWidgetItem();
1231         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_ALTITUDE), altitudeCell);
1232         altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1233         altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1234 
1235         QTableWidgetItem *completionCell = new QTableWidgetItem();
1236         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_ENDTIME), completionCell);
1237         completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1238         completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1239 
1240         QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem();
1241         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_DURATION), estimatedTimeCell);
1242         estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1243         estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1244 
1245         QTableWidgetItem *leadTimeCell = new QTableWidgetItem();
1246         queueTable->setItem(currentRow, static_cast<int>(SCHEDCOL_LEADTIME), leadTimeCell);
1247         leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1248         leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
1249     }
1250 
1251     setJobStatusCells(currentRow);
1252 
1253     /* We just added or saved a job, so we have a job in the list - enable relevant buttons */
1254     queueSaveAsB->setEnabled(true);
1255     queueSaveB->setEnabled(true);
1256     startB->setEnabled(true);
1257     evaluateOnlyB->setEnabled(true);
1258     setJobManipulation(!Options::sortSchedulerJobs(), true);
1259 
1260     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1);
1261 
1262     watchJobChanges(true);
1263 
1264     if (SCHEDULER_LOADING != state)
1265     {
1266         evaluateJobs(true);
1267     }
1268 }
1269 
syncGUIToJob(SchedulerJob * job)1270 void Scheduler::syncGUIToJob(SchedulerJob *job)
1271 {
1272     nameEdit->setText(job->getName());
1273 
1274     prioritySpin->setValue(job->getPriority());
1275 
1276     raBox->showInHours(job->getTargetCoords().ra0());
1277     decBox->showInDegrees(job->getTargetCoords().dec0());
1278 
1279     // fitsURL/sequenceURL are not part of UI, but the UI serves as model, so keep them here for now
1280     fitsURL = job->getFITSFile().isEmpty() ? QUrl() : job->getFITSFile();
1281     sequenceURL = job->getSequenceFile();
1282     fitsEdit->setText(fitsURL.toLocalFile());
1283     sequenceEdit->setText(sequenceURL.toLocalFile());
1284 
1285     rotationSpin->setValue(job->getRotation());
1286 
1287     trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK);
1288     focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS);
1289     alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN);
1290     guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE);
1291 
1292     switch (job->getFileStartupCondition())
1293     {
1294         case SchedulerJob::START_ASAP:
1295             asapConditionR->setChecked(true);
1296             culminationOffset->setValue(DEFAULT_CULMINATION_TIME);
1297             break;
1298 
1299         case SchedulerJob::START_CULMINATION:
1300             culminationConditionR->setChecked(true);
1301             culminationOffset->setValue(job->getCulminationOffset());
1302             break;
1303 
1304         case SchedulerJob::START_AT:
1305             startupTimeConditionR->setChecked(true);
1306             startupTimeEdit->setDateTime(job->getStartupTime());
1307             culminationOffset->setValue(DEFAULT_CULMINATION_TIME);
1308             break;
1309     }
1310 
1311     if (job->hasMinAltitude())
1312     {
1313         altConstraintCheck->setChecked(true);
1314         minAltitude->setValue(job->getMinAltitude());
1315     }
1316     else
1317     {
1318         altConstraintCheck->setChecked(false);
1319         minAltitude->setValue(DEFAULT_MIN_ALTITUDE);
1320     }
1321 
1322     if (job->getMinMoonSeparation() >= 0)
1323     {
1324         moonSeparationCheck->setChecked(true);
1325         minMoonSeparation->setValue(job->getMinMoonSeparation());
1326     }
1327     else
1328     {
1329         moonSeparationCheck->setChecked(false);
1330         minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION);
1331     }
1332 
1333     weatherCheck->setChecked(job->getEnforceWeather());
1334 
1335     twilightCheck->blockSignals(true);
1336     twilightCheck->setChecked(job->getEnforceTwilight());
1337     twilightCheck->blockSignals(false);
1338 
1339     artificialHorizonCheck->blockSignals(true);
1340     artificialHorizonCheck->setChecked(job->getEnforceArtificialHorizon());
1341     artificialHorizonCheck->blockSignals(false);
1342 
1343     switch (job->getCompletionCondition())
1344     {
1345         case SchedulerJob::FINISH_SEQUENCE:
1346             sequenceCompletionR->setChecked(true);
1347             break;
1348 
1349         case SchedulerJob::FINISH_REPEAT:
1350             repeatCompletionR->setChecked(true);
1351             repeatsSpin->setValue(job->getRepeatsRequired());
1352             break;
1353 
1354         case SchedulerJob::FINISH_LOOP:
1355             loopCompletionR->setChecked(true);
1356             break;
1357 
1358         case SchedulerJob::FINISH_AT:
1359             timeCompletionR->setChecked(true);
1360             completionTimeEdit->setDateTime(job->getCompletionTime());
1361             break;
1362     }
1363 
1364     updateNightTime(job);
1365 
1366     setJobManipulation(!Options::sortSchedulerJobs(), true);
1367 }
1368 
updateNightTime(SchedulerJob const * job)1369 void Scheduler::updateNightTime(SchedulerJob const *job)
1370 {
1371     if (job == nullptr)
1372     {
1373         int const currentRow = queueTable->currentRow();
1374         if (0 < currentRow)
1375             job = jobs.at(currentRow);
1376     }
1377 
1378     QDateTime const dawn = job ? job->getDawnAstronomicalTwilight() : Dawn;
1379     QDateTime const dusk = job ? job->getDuskAstronomicalTwilight() : Dusk;
1380 
1381     QChar const warning(dawn == dusk ? 0x26A0 : '-');
1382     nightTime->setText(i18n("%1 %2 %3", dusk.toString("hh:mm"), warning, dawn.toString("hh:mm")));
1383 }
1384 
loadJob(QModelIndex i)1385 void Scheduler::loadJob(QModelIndex i)
1386 {
1387     if (jobUnderEdit == i.row())
1388         return;
1389 
1390     if (state == SCHEDULER_RUNNING)
1391     {
1392         appendLogText(i18n("Warning: you cannot add or modify a job while the scheduler is running."));
1393         return;
1394     }
1395 
1396     SchedulerJob * const job = jobs.at(i.row());
1397 
1398     if (job == nullptr)
1399         return;
1400 
1401     watchJobChanges(false);
1402 
1403     //job->setState(SchedulerJob::JOB_IDLE);
1404     //job->setStage(SchedulerJob::STAGE_IDLE);
1405     syncGUIToJob(job);
1406 
1407     /* Turn the add button into an apply button */
1408     setJobAddApply(false);
1409 
1410     /* Disable scheduler start/evaluate buttons */
1411     startB->setEnabled(false);
1412     evaluateOnlyB->setEnabled(false);
1413 
1414     /* Don't let the end-user remove a job being edited */
1415     setJobManipulation(false, false);
1416 
1417     jobUnderEdit = i.row();
1418     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(
1419                                        jobUnderEdit + 1);
1420 
1421     watchJobChanges(true);
1422 }
1423 
queueTableSelectionChanged(QModelIndex current,QModelIndex previous)1424 void Scheduler::queueTableSelectionChanged(QModelIndex current, QModelIndex previous)
1425 {
1426     Q_UNUSED(previous)
1427 
1428     // prevent selection when not idle
1429     if (state != SCHEDULER_IDLE)
1430         return;
1431 
1432     if (current.row() < 0 || (current.row() + 1) > jobs.size())
1433         return;
1434 
1435     SchedulerJob * const job = jobs.at(current.row());
1436 
1437     if (job != nullptr)
1438     {
1439         resetJobEdit();
1440         syncGUIToJob(job);
1441     }
1442     else nightTime->setText("-");
1443 }
1444 
clickQueueTable(QModelIndex index)1445 void Scheduler::clickQueueTable(QModelIndex index)
1446 {
1447     setJobManipulation(!Options::sortSchedulerJobs() && index.isValid(), index.isValid());
1448 }
1449 
setJobAddApply(bool add_mode)1450 void Scheduler::setJobAddApply(bool add_mode)
1451 {
1452     if (add_mode)
1453     {
1454         addToQueueB->setIcon(QIcon::fromTheme("list-add"));
1455         addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list."));
1456         //addToQueueB->setStyleSheet(QString());
1457         addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
1458     }
1459     else
1460     {
1461         addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
1462         addToQueueB->setToolTip(i18n("Apply job changes."));
1463         //addToQueueB->setStyleSheet("background-color:orange;}");
1464         addToQueueB->setEnabled(true);
1465     }
1466 }
1467 
setJobManipulation(bool can_reorder,bool can_delete)1468 void Scheduler::setJobManipulation(bool can_reorder, bool can_delete)
1469 {
1470     bool can_edit = (state == SCHEDULER_IDLE);
1471 
1472     if (can_reorder)
1473     {
1474         int const currentRow = queueTable->currentRow();
1475         queueUpB->setEnabled(can_edit && 0 < currentRow);
1476         queueDownB->setEnabled(can_edit && currentRow < queueTable->rowCount() - 1);
1477     }
1478     else
1479     {
1480         queueUpB->setEnabled(false);
1481         queueDownB->setEnabled(false);
1482     }
1483     sortJobsB->setEnabled(can_edit && can_reorder);
1484     removeFromQueueB->setEnabled(can_edit && can_delete);
1485 }
1486 
reorderJobs(QList<SchedulerJob * > reordered_sublist)1487 bool Scheduler::reorderJobs(QList<SchedulerJob*> reordered_sublist)
1488 {
1489     /* Add jobs not reordered at the end of the list, in initial order */
1490     foreach (SchedulerJob* job, jobs)
1491         if (!reordered_sublist.contains(job))
1492             reordered_sublist.append(job);
1493 
1494     if (jobs != reordered_sublist)
1495     {
1496         /* Remember job currently selected */
1497         int const selectedRow = queueTable->currentRow();
1498         SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr;
1499 
1500         /* Reassign list */
1501         jobs = reordered_sublist;
1502 
1503         /* Reassign status cells for all jobs, and reset them */
1504         for (int row = 0; row < jobs.size(); row++)
1505             setJobStatusCells(row);
1506 
1507         /* Reselect previously selected job */
1508         if (nullptr != selectedJob)
1509             queueTable->selectRow(jobs.indexOf(selectedJob));
1510 
1511         return true;
1512     }
1513     else return false;
1514 }
1515 
moveJobUp()1516 void Scheduler::moveJobUp()
1517 {
1518     /* No move if jobs are sorted automatically */
1519     if (Options::sortSchedulerJobs())
1520         return;
1521 
1522     int const rowCount = queueTable->rowCount();
1523     int const currentRow = queueTable->currentRow();
1524     int const destinationRow = currentRow - 1;
1525 
1526     /* No move if no job selected, if table has one line or less or if destination is out of table */
1527     if (currentRow < 0 || rowCount <= 1 || destinationRow < 0)
1528         return;
1529 
1530     /* Swap jobs in the list */
1531 #if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1532     jobs.swapItemsAt(currentRow, destinationRow);
1533 #else
1534     jobs.swap(currentRow, destinationRow);
1535 #endif
1536 
1537     /* Reassign status cells */
1538     setJobStatusCells(currentRow);
1539     setJobStatusCells(destinationRow);
1540 
1541     /* Move selection to destination row */
1542     queueTable->selectRow(destinationRow);
1543     setJobManipulation(!Options::sortSchedulerJobs(), true);
1544 
1545     /* Jobs are now sorted, so reset all later jobs */
1546     for (int row = destinationRow; row < jobs.size(); row++)
1547         jobs.at(row)->reset();
1548 
1549     /* Make list modified and evaluate jobs */
1550     mDirty = true;
1551     evaluateJobs(true);
1552 }
1553 
moveJobDown()1554 void Scheduler::moveJobDown()
1555 {
1556     /* No move if jobs are sorted automatically */
1557     if (Options::sortSchedulerJobs())
1558         return;
1559 
1560     int const rowCount = queueTable->rowCount();
1561     int const currentRow = queueTable->currentRow();
1562     int const destinationRow = currentRow + 1;
1563 
1564     /* No move if no job selected, if table has one line or less or if destination is out of table */
1565     if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount)
1566         return;
1567 
1568     /* Swap jobs in the list */
1569 #if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1570     jobs.swapItemsAt(currentRow, destinationRow);
1571 #else
1572     jobs.swap(currentRow, destinationRow);
1573 #endif
1574 
1575     /* Reassign status cells */
1576     setJobStatusCells(currentRow);
1577     setJobStatusCells(destinationRow);
1578 
1579     /* Move selection to destination row */
1580     queueTable->selectRow(destinationRow);
1581     setJobManipulation(!Options::sortSchedulerJobs(), true);
1582 
1583     /* Jobs are now sorted, so reset all later jobs */
1584     for (int row = currentRow; row < jobs.size(); row++)
1585         jobs.at(row)->reset();
1586 
1587     /* Make list modified and evaluate jobs */
1588     mDirty = true;
1589     evaluateJobs(true);
1590 }
1591 
setJobStatusCells(int row)1592 void Scheduler::setJobStatusCells(int row)
1593 {
1594     if (row < 0 || jobs.size() <= row)
1595         return;
1596 
1597     SchedulerJob * const job = jobs.at(row);
1598 
1599     job->setNameCell(queueTable->item(row, static_cast<int>(SCHEDCOL_NAME)));
1600     job->setStatusCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS)));
1601     job->setCaptureCountCell(queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES)));
1602     job->setScoreCell(queueTable->item(row, static_cast<int>(SCHEDCOL_SCORE)));
1603     job->setAltitudeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_ALTITUDE)));
1604     job->setStartupCell(queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME)));
1605     job->setCompletionCell(queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME)));
1606     job->setEstimatedTimeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_DURATION)));
1607     job->setLeadTimeCell(queueTable->item(row, static_cast<int>(SCHEDCOL_LEADTIME)));
1608     job->updateJobCells();
1609 }
1610 
resetJobEdit()1611 void Scheduler::resetJobEdit()
1612 {
1613     if (jobUnderEdit < 0)
1614         return;
1615 
1616     SchedulerJob * const job = jobs.at(jobUnderEdit);
1617     Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid");
1618 
1619     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(
1620                                        jobUnderEdit + 1);
1621 
1622     jobUnderEdit = -1;
1623 
1624     watchJobChanges(false);
1625 
1626     /* Revert apply button to add */
1627     setJobAddApply(true);
1628 
1629     /* Refresh state of job manipulation buttons */
1630     setJobManipulation(!Options::sortSchedulerJobs(), true);
1631 
1632     /* Restore scheduler operation buttons */
1633     evaluateOnlyB->setEnabled(true);
1634     startB->setEnabled(true);
1635 
1636     Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode");
1637 }
1638 
removeJob()1639 void Scheduler::removeJob()
1640 {
1641     int currentRow = queueTable->currentRow();
1642 
1643     /* Don't remove a row that is not selected */
1644     if (currentRow < 0)
1645         return;
1646 
1647     /* Grab the job currently selected */
1648     SchedulerJob * const job = jobs.at(currentRow);
1649     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1);
1650 
1651     /* Remove the job from the table */
1652     queueTable->removeRow(currentRow);
1653 
1654     /* If there are no job rows left, update UI buttons */
1655     if (queueTable->rowCount() == 0)
1656     {
1657         setJobManipulation(false, false);
1658         evaluateOnlyB->setEnabled(false);
1659         queueSaveAsB->setEnabled(false);
1660         queueSaveB->setEnabled(false);
1661         startB->setEnabled(false);
1662         pauseB->setEnabled(false);
1663     }
1664 
1665     /* Else update the selection */
1666     else
1667     {
1668         if (currentRow > queueTable->rowCount())
1669             currentRow = queueTable->rowCount() - 1;
1670 
1671         loadJob(queueTable->currentIndex());
1672         queueTable->selectRow(currentRow);
1673     }
1674 
1675     /* If needed, reset edit mode to clean up UI */
1676     if (jobUnderEdit >= 0)
1677         resetJobEdit();
1678 
1679     /* And remove the job object */
1680     jobs.removeOne(job);
1681     delete (job);
1682 
1683     mDirty = true;
1684     evaluateJobs(true);
1685 }
1686 
toggleScheduler()1687 void Scheduler::toggleScheduler()
1688 {
1689     if (state == SCHEDULER_RUNNING)
1690     {
1691         disablePreemptiveShutdown();
1692         stop();
1693     }
1694     else
1695         run();
1696 }
1697 
stop()1698 void Scheduler::stop()
1699 {
1700     if (state != SCHEDULER_RUNNING)
1701         return;
1702 
1703     qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping...";
1704 
1705     // Stop running job and abort all others
1706     // in case of soft shutdown we skip this
1707     if (!preemptiveShutdown())
1708     {
1709         bool wasAborted = false;
1710         foreach (SchedulerJob *job, jobs)
1711         {
1712             if (job == currentJob)
1713                 stopCurrentJobAction();
1714 
1715             if (job->getState() <= SchedulerJob::JOB_BUSY)
1716             {
1717                 appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName()));
1718                 job->setState(SchedulerJob::JOB_ABORTED);
1719                 wasAborted = true;
1720             }
1721         }
1722 
1723         if (wasAborted)
1724             KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted."));
1725     }
1726 
1727     TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_NOTHING).toLatin1().data());
1728     setupNextIteration(RUN_NOTHING);
1729     cancelGuidingTimer();
1730 
1731     state     = SCHEDULER_IDLE;
1732     emit newStatus(state);
1733     ekosState = EKOS_IDLE;
1734     indiState = INDI_IDLE;
1735 
1736     parkWaitState = PARKWAIT_IDLE;
1737 
1738     // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete.
1739     // Or if we're doing a soft shutdown
1740     if (startupState != STARTUP_COMPLETE || preemptiveShutdown())
1741     {
1742         if (startupState == STARTUP_SCRIPT)
1743         {
1744             scriptProcess.disconnect();
1745             scriptProcess.terminate();
1746         }
1747 
1748         startupState = STARTUP_IDLE;
1749     }
1750     // Reset startup state to unparking phase (dome -> mount -> cap)
1751     // We do not want to run the startup script again but unparking should be checked
1752     // whenever the scheduler is running again.
1753     else if (startupState == STARTUP_COMPLETE)
1754     {
1755         if (unparkDomeCheck->isChecked())
1756             startupState = STARTUP_UNPARK_DOME;
1757         else if (unparkMountCheck->isChecked())
1758             startupState = STARTUP_UNPARK_MOUNT;
1759         else if (uncapCheck->isChecked())
1760             startupState = STARTUP_UNPARK_CAP;
1761     }
1762 
1763     shutdownState = SHUTDOWN_IDLE;
1764 
1765     setCurrentJob(nullptr);
1766     captureBatch            = 0;
1767     indiConnectFailureCount = 0;
1768     ekosConnectFailureCount = 0;
1769     focusFailureCount       = 0;
1770     guideFailureCount       = 0;
1771     alignFailureCount       = 0;
1772     captureFailureCount     = 0;
1773     loadAndSlewProgress     = false;
1774     autofocusCompleted      = false;
1775 
1776     startupB->setEnabled(true);
1777     shutdownB->setEnabled(true);
1778 
1779     // If soft shutdown, we return for now
1780     if (preemptiveShutdown())
1781     {
1782         sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready"));
1783         sleepLabel->show();
1784 
1785         QDateTime const now = getLocalTime();
1786         int const nextObservationTime = now.secsTo(getPreemptiveShutdownWakeupTime());
1787         setupNextIteration(RUN_WAKEUP,
1788                            std::lround(((nextObservationTime + 1) * 1000)
1789                                        / KStarsData::Instance()->clock()->scale()));
1790         return;
1791 
1792     }
1793 
1794     // Clear target name in capture interface upon stopping
1795     if (captureInterface.isNull() == false)
1796     {
1797         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:setProperty", "targetName=\"\"");
1798         captureInterface->setProperty("targetName", QString());
1799     }
1800 
1801     if (scriptProcess.state() == QProcess::Running)
1802         scriptProcess.terminate();
1803 
1804     sleepLabel->hide();
1805     pi->stopAnimation();
1806 
1807     startB->setIcon(QIcon::fromTheme("media-playback-start"));
1808     startB->setToolTip(i18n("Start Scheduler"));
1809     pauseB->setEnabled(false);
1810     //startB->setText("Start Scheduler");
1811 
1812     queueLoadB->setEnabled(true);
1813     queueAppendB->setEnabled(true);
1814     addToQueueB->setEnabled(true);
1815     setJobManipulation(false, false);
1816     mosaicB->setEnabled(true);
1817     evaluateOnlyB->setEnabled(true);
1818 }
1819 
start()1820 void Scheduler::start()
1821 {
1822     switch (state)
1823     {
1824         case SCHEDULER_IDLE:
1825             /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */
1826             startupScriptURL = QUrl::fromUserInput(startupScript->text());
1827             if (!startupScript->text().isEmpty() && !startupScriptURL.isValid())
1828             {
1829                 appendLogText(i18n("Warning: startup script URL %1 is not valid.", startupScript->text()));
1830                 return;
1831             }
1832 
1833             /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */
1834             shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text());
1835             if (!shutdownScript->text().isEmpty() && !shutdownScriptURL.isValid())
1836             {
1837                 appendLogText(i18n("Warning: shutdown script URL %1 is not valid.", shutdownScript->text()));
1838                 return;
1839             }
1840 
1841             qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting...";
1842 
1843             /* Update UI to reflect startup */
1844             pi->startAnimation();
1845             sleepLabel->hide();
1846             startB->setIcon(QIcon::fromTheme("media-playback-stop"));
1847             startB->setToolTip(i18n("Stop Scheduler"));
1848             pauseB->setEnabled(true);
1849             pauseB->setChecked(false);
1850 
1851             /* Disable edit-related buttons */
1852             queueLoadB->setEnabled(false);
1853             queueAppendB->setEnabled(false);
1854             addToQueueB->setEnabled(false);
1855             setJobManipulation(false, false);
1856             mosaicB->setEnabled(false);
1857             evaluateOnlyB->setEnabled(false);
1858             startupB->setEnabled(false);
1859             shutdownB->setEnabled(false);
1860 
1861             state = SCHEDULER_RUNNING;
1862             emit newStatus(state);
1863             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
1864             setupNextIteration(RUN_SCHEDULER);
1865 
1866             appendLogText(i18n("Scheduler started."));
1867             qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started.";
1868             break;
1869 
1870         case SCHEDULER_PAUSED:
1871             /* Update UI to reflect resume */
1872             startB->setIcon(QIcon::fromTheme("media-playback-stop"));
1873             startB->setToolTip(i18n("Stop Scheduler"));
1874             pauseB->setEnabled(true);
1875             pauseB->setCheckable(false);
1876             pauseB->setChecked(false);
1877 
1878             /* Edit-related buttons are still disabled */
1879 
1880             /* The end-user cannot update the schedule, don't re-evaluate jobs. Timer schedulerTimer is already running. */
1881             state = SCHEDULER_RUNNING;
1882             emit newStatus(state);
1883             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
1884             setupNextIteration(RUN_SCHEDULER);
1885 
1886             appendLogText(i18n("Scheduler resuming."));
1887             qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming.";
1888             break;
1889 
1890         default:
1891             break;
1892     }
1893 }
1894 
pause()1895 void Scheduler::pause()
1896 {
1897     state = SCHEDULER_PAUSED;
1898     emit newStatus(state);
1899     appendLogText(i18n("Scheduler pause planned..."));
1900     pauseB->setEnabled(false);
1901 
1902     startB->setIcon(QIcon::fromTheme("media-playback-start"));
1903     startB->setToolTip(i18n("Resume Scheduler"));
1904 }
1905 
setPaused()1906 void Scheduler::setPaused()
1907 {
1908     pauseB->setCheckable(true);
1909     pauseB->setChecked(true);
1910     TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_NOTHING).toLatin1().data());
1911     setupNextIteration(RUN_NOTHING);
1912     appendLogText(i18n("Scheduler paused."));
1913 }
1914 
setCurrentJob(SchedulerJob * job)1915 void Scheduler::setCurrentJob(SchedulerJob *job)
1916 {
1917     /* Reset job widgets */
1918     if (currentJob)
1919     {
1920         currentJob->setStageLabel(nullptr);
1921     }
1922 
1923     /* Set current job */
1924     currentJob = job;
1925 
1926     /* Reassign job widgets, or reset to defaults */
1927     if (currentJob)
1928     {
1929         currentJob->setStageLabel(jobStatus);
1930         queueTable->selectRow(jobs.indexOf(currentJob));
1931     }
1932     else
1933     {
1934         jobStatus->setText(i18n("No job running"));
1935         //queueTable->clearSelection();
1936     }
1937 }
1938 
evaluateJobs(bool evaluateOnly)1939 void Scheduler::evaluateJobs(bool evaluateOnly)
1940 {
1941     /* Don't evaluate if list is empty */
1942     if (jobs.isEmpty())
1943         return;
1944     /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */
1945     if (Options::rememberJobProgress())
1946         updateCompletedJobsCount();
1947 
1948     calculateDawnDusk();
1949     bool possiblyDelay = false;
1950     QList<SchedulerJob *> sortedJobs = evaluateJobs(jobs, state, m_CapturedFramesCount, Dawn, Dusk,
1951                                        errorHandlingRescheduleErrorsCB->isChecked(),
1952                                        errorHandlingDontRestartButton->isChecked() == false, &possiblyDelay, this);
1953     if (sortedJobs.empty())
1954     {
1955         setCurrentJob(nullptr);
1956         return;
1957     }
1958     if (possiblyDelay && errorHandlingRestartAfterAllButton->isChecked())
1959     {
1960         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_WAKEUP).toLatin1().data());
1961         setupNextIteration(RUN_WAKEUP, std::lround((errorHandlingDelaySB->value() * 1000) /
1962                            KStarsData::Instance()->clock()->scale()));
1963         // but before we restart them, we wait for the given delay.
1964         appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value()));
1965 
1966         sleepLabel->setToolTip(i18n("Scheduler waits for a retry."));
1967         sleepLabel->show();
1968         // we continue to determine which job should be running, when the delay is over
1969     }
1970 
1971     processJobs(sortedJobs, evaluateOnly);
1972 }
1973 
evaluateJobs(QList<SchedulerJob * > & jobs,SchedulerState state,const QMap<QString,uint16_t> & capturedFramesCount,QDateTime const & Dawn,QDateTime const & Dusk,bool rescheduleErrors,bool restartJobs,bool * possiblyDelay,Scheduler * scheduler)1974 QList<SchedulerJob *> Scheduler::evaluateJobs( QList<SchedulerJob *> &jobs, SchedulerState state,
1975         const QMap<QString, uint16_t> &capturedFramesCount, QDateTime const &Dawn, QDateTime const &Dusk,
1976         bool rescheduleErrors, bool restartJobs, bool *possiblyDelay, Scheduler *scheduler)
1977 {
1978 
1979     /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */
1980     QDateTime const now = getLocalTime();
1981 
1982     /* First, filter out non-schedulable jobs */
1983     /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */
1984     QList<SchedulerJob *> sortedJobs = jobs;
1985 
1986     /* Then enumerate SchedulerJobs to consolidate imaging time */
1987     foreach (SchedulerJob *job, sortedJobs)
1988     {
1989         /* Let aborted jobs be rescheduled later instead of forgetting them */
1990         switch (job->getState())
1991         {
1992             case SchedulerJob::JOB_SCHEDULED:
1993                 /* If job is scheduled, keep it for evaluation against others */
1994                 break;
1995 
1996             case SchedulerJob::JOB_INVALID:
1997             case SchedulerJob::JOB_COMPLETE:
1998                 /* If job is invalid or complete, bypass evaluation */
1999                 continue;
2000 
2001             case SchedulerJob::JOB_BUSY:
2002                 /* If job is busy, edge case, bypass evaluation */
2003                 continue;
2004 
2005             case SchedulerJob::JOB_ERROR:
2006             case SchedulerJob::JOB_ABORTED:
2007                 /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */
2008                 if (state == SCHEDULER_RUNNING)
2009                     continue;
2010             /* Fall through */
2011             case SchedulerJob::JOB_IDLE:
2012             case SchedulerJob::JOB_EVALUATION:
2013             default:
2014                 /* If job is idle, re-evaluate completely */
2015                 job->setEstimatedTime(-1);
2016                 break;
2017         }
2018 
2019         switch (job->getCompletionCondition())
2020         {
2021             case SchedulerJob::FINISH_AT:
2022                 /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */
2023                 if (job->getCompletionTime().isValid() && job->getCompletionTime() < now)
2024                 {
2025                     job->setState(SchedulerJob::JOB_IDLE);
2026                     continue;
2027                 }
2028                 break;
2029 
2030             case SchedulerJob::FINISH_REPEAT:
2031                 // In case of a repeating jobs, let's make sure we have more runs left to go
2032                 // If we don't, re-estimate imaging time for the scheduler job before concluding
2033                 if (job->getRepeatsRemaining() == 0)
2034                 {
2035                     if (scheduler != nullptr) scheduler->appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName()));
2036                     if (Options::rememberJobProgress())
2037                     {
2038                         job->setEstimatedTime(-1);
2039                     }
2040                     else
2041                     {
2042                         job->setState(SchedulerJob::JOB_COMPLETE);
2043                         job->setEstimatedTime(0);
2044                         continue;
2045                     }
2046                 }
2047                 break;
2048 
2049             default:
2050                 break;
2051         }
2052 
2053         // -1 = Job is not estimated yet
2054         // -2 = Job is estimated but time is unknown
2055         // > 0  Job is estimated and time is known
2056         if (job->getEstimatedTime() == -1)
2057         {
2058             if (estimateJobTime(job, capturedFramesCount, scheduler) == false)
2059             {
2060                 job->setState(SchedulerJob::JOB_INVALID);
2061                 continue;
2062             }
2063         }
2064 
2065         if (job->getEstimatedTime() == 0)
2066         {
2067             job->setRepeatsRemaining(0);
2068             job->setState(SchedulerJob::JOB_COMPLETE);
2069             continue;
2070         }
2071 
2072         // In any other case, evaluate
2073         job->setState(SchedulerJob::JOB_EVALUATION);
2074     }
2075 
2076     /*
2077      * At this step, we prepare scheduling of jobs.
2078      * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time.
2079      */
2080 
2081     /* This predicate matches jobs not being evaluated and not aborted */
2082     auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job)
2083     {
2084         SchedulerJob::JOBStatus const s = job->getState();
2085         return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s;
2086     };
2087 
2088     /* This predicate matches jobs neither being evaluated nor aborted nor in error state */
2089     auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job)
2090     {
2091         SchedulerJob::JOBStatus const s = job->getState();
2092         return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != s;
2093     };
2094 
2095     /* This predicate matches jobs that aborted, or completed for whatever reason */
2096     auto finished_or_aborted = [](SchedulerJob const * const job)
2097     {
2098         SchedulerJob::JOBStatus const s = job->getState();
2099         return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s;
2100     };
2101 
2102     bool nea  = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted);
2103     bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error);
2104 
2105     /* If there are no jobs left to run in the filtered list, stop evaluation */
2106     if (sortedJobs.isEmpty() || (!rescheduleErrors && nea)
2107             || (rescheduleErrors && neae))
2108     {
2109         if (scheduler != nullptr) scheduler->appendLogText(i18n("No jobs left in the scheduler queue."));
2110         QList<SchedulerJob *> noJobs;
2111         return noJobs;
2112     }
2113 
2114     /* If there are only aborted jobs that can run, reschedule those */
2115     if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && restartJobs)
2116     {
2117         if (scheduler != nullptr) scheduler->appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.",
2118                     rescheduleErrors ? "aborted or error" : "aborted"));
2119 
2120         // set aborted and error jobs to evaluation state
2121         for (int index = 0; index < sortedJobs.size(); index++)
2122         {
2123             SchedulerJob * const job = sortedJobs.at(index);
2124             if (SchedulerJob::JOB_ABORTED == job->getState() ||
2125                     (rescheduleErrors && SchedulerJob::JOB_ERROR == job->getState()))
2126                 job->setState(SchedulerJob::JOB_EVALUATION);
2127         }
2128         *possiblyDelay = true;
2129     }
2130 
2131     /* If option says so, reorder by altitude and priority before sequencing */
2132     /* FIXME: refactor so all sorts are using the same predicates */
2133     /* FIXME: use std::stable_sort as qStableSort is deprecated */
2134     /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */
2135     qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs();
2136     if (Options::sortSchedulerJobs())
2137     {
2138         using namespace std::placeholders;
2139         std::stable_sort(sortedJobs.begin(), sortedJobs.end(),
2140                          std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, getLocalTime()));
2141         std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder);
2142     }
2143 
2144     /* The first reordered job has no lead time - this could also be the delay from now to startup */
2145     sortedJobs.first()->setLeadTime(0);
2146 
2147     /* The objective of the following block is to make sure jobs are sequential in the list filtered previously.
2148      *
2149      * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable.
2150      * If the completion time of the previous job overlaps the current job, we offset the startup of the current job.
2151      * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time.
2152      * The lead time from the Options registry is used as a buffer between jobs.
2153      *
2154      * Note about the situation where the current job overlaps the next job, and the next job is not movable:
2155      * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory.
2156      * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an
2157      *   infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort.
2158      * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit.
2159      *   Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled
2160      *   at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the
2161      *   schedule will be altered by external events during the execution.
2162      *
2163      * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs:
2164      * - Twilight constraint moves jobs to the next dark sky interval.
2165      * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time.
2166      * - Culmination constraint moves jobs to the next transit time, with arbitrary offset.
2167      * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past.
2168      *
2169      * Here are the constraints that have an effect on jobs following the job being examined:
2170      * - Repeats requirement increases the duration of the current job, pushing subsequent jobs.
2171      * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented).
2172      * - Fixed completion makes subsequent jobs start after that boundary time.
2173      *
2174      * However, we need a way to inform the end-user about failed schedules clearly in the UI.
2175      * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs
2176      * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will
2177      * be processed when possible, probably not at the expected moment.
2178      */
2179 
2180     // Make sure no two jobs have the same scheduled time or overlap with other jobs
2181     for (int index = 0; index < sortedJobs.size(); index++)
2182     {
2183         SchedulerJob * const currentJob = sortedJobs.at(index);
2184 
2185         // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order
2186         if (SchedulerJob::JOB_EVALUATION != currentJob->getState())
2187             continue;
2188 
2189         // At this point, a job with no valid start date is a problem, so consider invalid startup time is now
2190         if (!currentJob->getStartupTime().isValid())
2191             currentJob->setStartupTime(now);
2192 
2193         // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated
2194         SchedulerJob const * previousJob = nullptr;
2195         for (int i = index - 1; 0 <= i; i--)
2196         {
2197             SchedulerJob const * const a_job = sortedJobs.at(i);
2198 
2199             if (SchedulerJob::JOB_SCHEDULED == a_job->getState())
2200             {
2201                 previousJob = a_job;
2202                 break;
2203             }
2204         }
2205 
2206         Q_ASSERT_X(nullptr == previousJob
2207                    || previousJob != currentJob, __FUNCTION__,
2208                    "Previous job considered for schedule is either undefined or not equal to current.");
2209 
2210         // Locate the next job - nothing special required except end of list check
2211         SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr;
2212 
2213         Q_ASSERT_X(nullptr == nextJob
2214                    || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current.");
2215 
2216         // We're attempting to schedule the job 10 times before making it invalid
2217         for (int attempt = 1; attempt < 11; attempt++)
2218         {
2219             qCDebug(KSTARS_EKOS_SCHEDULER) <<
2220                                            QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.")
2221                                            .arg(attempt)
2222                                            .arg(static_cast<int>(currentJob->getEstimatedTime()))
2223                                            .arg(currentJob->getName())
2224                                            .arg(index + 1)
2225                                            .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))
2226                                            .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()));
2227 
2228 
2229             // ----- #1 Should we reject the current job because of its fixed startup time?
2230             //
2231             // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime.
2232             // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm.
2233             // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors.
2234             // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved.
2235 
2236             if (SchedulerJob::START_AT == currentJob->getFileStartupCondition())
2237             {
2238                 // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now
2239                 if (currentJob->getStartupTime().addSecs(static_cast <int> (ceil(Options::leadTime() * 60))) < now)
2240                 {
2241                     currentJob->setState(SchedulerJob::JOB_INVALID);
2242 
2243 
2244                     if (scheduler != nullptr) scheduler->appendLogText(
2245                             i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.",
2246                                  currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())));
2247 
2248                     break;
2249                 }
2250                 // Check whether the current job has a positive dark sky score at the time of startup
2251                 else if (true == currentJob->getEnforceTwilight() && !currentJob->runsDuringAstronomicalNightTime())
2252                 {
2253                     currentJob->setState(SchedulerJob::JOB_INVALID);
2254 
2255                     if (scheduler != nullptr) scheduler->appendLogText(
2256                             i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.",
2257                                  currentJob->getName()));
2258 
2259                     break;
2260                 }
2261                 // Check whether the current job has a positive altitude score at the time of startup
2262                 else if (currentJob->hasAltitudeConstraint() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0)
2263                 {
2264                     currentJob->setState(SchedulerJob::JOB_INVALID);
2265 
2266                     if (scheduler != nullptr) scheduler->appendLogText(
2267                             i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.",
2268                                  currentJob->getName()));
2269 
2270                     break;
2271                 }
2272                 // Check whether the current job has a positive Moon separation score at the time of startup
2273                 else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0)
2274                 {
2275                     currentJob->setState(SchedulerJob::JOB_INVALID);
2276 
2277                     if (scheduler != nullptr) scheduler->appendLogText(
2278                             i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.",
2279                                  currentJob->getName()));
2280 
2281                     break;
2282                 }
2283 
2284                 // Check whether a previous job overlaps the current job
2285                 if (nullptr != previousJob && previousJob->getCompletionTime().isValid())
2286                 {
2287                     // Calculate time we should be at after finishing the previous job
2288                     QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast <int> (ceil(
2289                                 Options::leadTime() * 60.0)));
2290 
2291                     // Make this job invalid if startup time is not achievable because a START_AT job is non-movable
2292                     if (currentJob->getStartupTime() < previousCompletionTime)
2293                     {
2294                         currentJob->setState(SchedulerJob::JOB_INVALID);
2295 
2296                         if (scheduler != nullptr) scheduler->appendLogText(
2297                                 i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.",
2298                                      currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())));
2299 
2300                         break;
2301                     }
2302 
2303                     currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime()));
2304                 }
2305 
2306                 // This job is non-movable, we're done
2307                 currentJob->setScore(calculateJobScore(currentJob, Dawn, Dusk, now));
2308                 currentJob->setState(SchedulerJob::JOB_SCHEDULED);
2309                 qCDebug(KSTARS_EKOS_SCHEDULER) <<
2310                                                QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.")
2311                                                .arg(currentJob->getName())
2312                                                .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()));
2313 
2314                 break;
2315             }
2316 
2317             // ----- #2 Should we delay the current job because it overlaps the previous job?
2318             //
2319             // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job.
2320             // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap.
2321             // If there is a previous job, the current job is simply delayed to avoid an eventual overlap.
2322             // IF there is a previous job but it never finishes, the current job is rejected.
2323             // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable.
2324 
2325             if (nullptr != previousJob)
2326             {
2327                 if (previousJob->getCompletionTime().isValid())
2328                 {
2329                     // Calculate time we should be at after finishing the previous job
2330                     QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast <int> (ceil(
2331                                 Options::leadTime() * 60.0)));
2332 
2333                     // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically
2334                     if (currentJob->getStartupTime() < previousCompletionTime)
2335                     {
2336                         currentJob->setStartupTime(previousCompletionTime);
2337 
2338                         qCDebug(KSTARS_EKOS_SCHEDULER) <<
2339                                                        QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.")
2340                                                        .arg(currentJob->getName())
2341                                                        .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))
2342                                                        .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime()))
2343                                                        .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat()));
2344 
2345                         // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug
2346                         if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition())
2347                             if (false == estimateJobTime(currentJob, capturedFramesCount, scheduler))
2348                                 currentJob->setState(SchedulerJob::JOB_INVALID);
2349 
2350                         continue;
2351                     }
2352                 }
2353                 else
2354                 {
2355                     currentJob->setState(SchedulerJob::JOB_INVALID);
2356 
2357                     if (scheduler != nullptr) scheduler->appendLogText(
2358                             i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.",
2359                                  currentJob->getName()));
2360 
2361                     break;
2362                 }
2363 
2364                 currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime()));
2365 
2366                 // Lead time can be zero, so completion may equal startup
2367                 Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__,
2368                            "Previous and current jobs do not overlap.");
2369             }
2370 
2371 
2372             // ----- #3 Should we delay the current job because it overlaps daylight?
2373             //
2374             // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night.
2375             // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here.
2376             // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn.
2377             // However, completion time during daylight only causes a warning, as this case will be processed as the job runs.
2378 
2379             if (currentJob->getEnforceTwilight())
2380             {
2381                 // During that check, we don't verify the current job can actually complete before dawn.
2382                 // If the job is interrupted while running, it will be aborted and rescheduled at a later time.
2383 
2384                 // If the job does not run during the astronomical night time, delay it to the next dusk
2385                 // This function takes care of Ekos offsets, dawn/dusk and pre-dawn
2386                 if (!currentJob->runsDuringAstronomicalNightTime())
2387                 {
2388                     // Delay job to next dusk - we will check other requirements later on
2389                     currentJob->setStartupTime(currentJob->getDuskAstronomicalTwilight());
2390 
2391                     qCDebug(KSTARS_EKOS_SCHEDULER) <<
2392                                                    QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.")
2393                                                    .arg(currentJob->getName())
2394                                                    .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()));
2395 
2396                     continue;
2397                 }
2398 
2399                 // Check if the completion date overlaps the next dawn, and issue a warning if so
2400                 if (currentJob->getDawnAstronomicalTwilight() < currentJob->getCompletionTime())
2401                 {
2402                     if (scheduler != nullptr) scheduler->appendLogText(
2403                             i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.",
2404                                  currentJob->getName()));
2405                 }
2406             }
2407 
2408 
2409             // ----- #4 Should we delay the current job because of its target culmination?
2410             //
2411             // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time.
2412             // This restriction may be used to start a job at the least air mass, or after a meridian flip.
2413             // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time.
2414             // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid.
2415 
2416             if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition())
2417             {
2418                 // Consolidate the culmination time, with offset, of the current job
2419                 QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime());
2420 
2421                 if (nextCulminationTime.isValid()) // Guaranteed
2422                 {
2423                     if (currentJob->getStartupTime() < nextCulminationTime)
2424                     {
2425                         currentJob->setStartupTime(nextCulminationTime);
2426 
2427                         qCDebug(KSTARS_EKOS_SCHEDULER) <<
2428                                                        QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.")
2429                                                        .arg(currentJob->getName())
2430                                                        .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()));
2431 
2432                         continue;
2433                     }
2434                 }
2435                 else
2436                 {
2437                     currentJob->setState(SchedulerJob::JOB_INVALID);
2438 
2439                     if (scheduler != nullptr) scheduler->appendLogText(
2440                             i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.",
2441                                  currentJob->getName(),
2442                                  QString("%L1").arg(currentJob->getCulminationOffset())));
2443 
2444                     break;
2445                 }
2446 
2447                 // Don't test altitude here, because we will push the job during the next check step
2448                 // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score.");
2449             }
2450 
2451 
2452             // ----- #5 Should we delay the current job because its altitude is incorrect?
2453             //
2454             // Altitude time ensures the job is assigned a startup time when its target is high enough.
2455             // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running.
2456             // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time.
2457             // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case.
2458 
2459             if (currentJob->hasAltitudeConstraint())
2460             {
2461                 // Consolidate a new altitude time from the startup time of the current job
2462                 QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime());
2463 
2464                 if (nextAltitudeTime.isValid())
2465                 {
2466                     if (currentJob->getStartupTime() < nextAltitudeTime)
2467                     {
2468                         currentJob->setStartupTime(nextAltitudeTime);
2469 
2470                         qCDebug(KSTARS_EKOS_SCHEDULER) <<
2471                                                        QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.")
2472                                                        .arg(currentJob->getName())
2473                                                        .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()));
2474 
2475                         continue;
2476                     }
2477                 }
2478                 else
2479                 {
2480                     currentJob->setState(SchedulerJob::JOB_INVALID);
2481 
2482                     if (scheduler != nullptr) scheduler->appendLogText(
2483                             i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.",
2484                                  currentJob->getName(),
2485                                  QString("%L1").arg(static_cast<double>(currentJob->getMinAltitude()), 0, 'f', 2),
2486                                  0.0 < currentJob->getMinMoonSeparation() ?
2487                                  QString("%L1").arg(static_cast<double>(currentJob->getMinMoonSeparation()), 0, 'f', 2) :
2488                                  QString("-")));
2489 
2490                     break;
2491                 }
2492 
2493                 Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__,
2494                            "Consolidated altitude time results in a positive altitude score.");
2495             }
2496 
2497 
2498             // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable?
2499             //
2500             // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account.
2501             // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation.
2502 
2503             if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition())
2504             {
2505                 // In the current implementation, it is not possible to abort a running job when the next job is supposed to start.
2506                 // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers.
2507 
2508                 // Calculate time we have between the end of the current job and the next job
2509                 double const timeToNext = static_cast<double> (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime()));
2510 
2511                 // If that time is overlapping the next job, abort the current job
2512                 if (timeToNext < Options::leadTime() * 60)
2513                 {
2514                     currentJob->setState(SchedulerJob::JOB_ABORTED);
2515 
2516                     if (scheduler != nullptr) scheduler->appendLogText(
2517                             i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.",
2518                                  currentJob->getName()));
2519 
2520                     break;
2521                 }
2522 
2523                 Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__,
2524                            "No overlap ");
2525             }
2526 
2527 
2528             // ----- #7 Should we reject the current job because it exceeded its fixed completion time?
2529             //
2530             // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time.
2531             // Its main objective is to catch wrong dates in the FINISH_AT configuration.
2532 
2533             if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition())
2534             {
2535                 if (currentJob->getCompletionTime() < currentJob->getStartupTime())
2536                 {
2537                     if (scheduler != nullptr) scheduler->appendLogText(
2538                             i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)",
2539                                  currentJob->getName(),
2540                                  currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()),
2541                                  currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())));
2542 
2543                     currentJob->setState(SchedulerJob::JOB_INVALID);
2544 
2545                     break;
2546                 }
2547             }
2548 
2549 
2550             // ----- #8 Should we reject the current job because of weather?
2551             //
2552             // That verification is left for runtime
2553             //
2554             // if (false == isWeatherOK(currentJob))
2555             //{
2556             //    currentJob->setState(SchedulerJob::JOB_ABORTED);
2557             //
2558             //    appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName()));
2559             //}
2560 
2561 
2562             // ----- #9 Update score for current time and mark evaluating jobs as scheduled
2563 
2564             currentJob->setScore(calculateJobScore(currentJob, Dawn, Dusk, now));
2565             currentJob->setState(SchedulerJob::JOB_SCHEDULED);
2566 
2567             qCDebug(KSTARS_EKOS_SCHEDULER) <<
2568                                            QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled")
2569                                            .arg(currentJob->getName())
2570                                            .arg(index + 1)
2571                                            .arg(attempt)
2572                                            .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))
2573                                            .arg(currentJob->getEstimatedTime());
2574 
2575             break;
2576         }
2577 
2578         // Check if job was successfully scheduled, else reject it
2579         if (SchedulerJob::JOB_EVALUATION == currentJob->getState())
2580         {
2581             currentJob->setState(SchedulerJob::JOB_INVALID);
2582 
2583             //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.",
2584             //            currentJob->getName(),
2585             //            index + 1));
2586 
2587         }
2588     }
2589     return sortedJobs;
2590 }
2591 
processJobs(QList<SchedulerJob * > sortedJobs,bool jobEvaluationOnly)2592 void Scheduler::processJobs(QList<SchedulerJob *> sortedJobs, bool jobEvaluationOnly)
2593 {
2594     /* Apply sorting to queue table, and mark it for saving if it changes */
2595     mDirty = reorderJobs(sortedJobs) | mDirty;
2596 
2597     if (jobEvaluationOnly || state != SCHEDULER_RUNNING)
2598     {
2599         qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required.";
2600         return;
2601     }
2602 
2603     /*
2604      * At this step, we finished evaluating jobs.
2605      * We select the first job that has to be run, per schedule.
2606      */
2607     auto finished_or_aborted = [](SchedulerJob const * const job)
2608     {
2609         SchedulerJob::JOBStatus const s = job->getState();
2610         return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s;
2611     };
2612 
2613     /* This predicate matches jobs that are neither scheduled to run nor aborted */
2614     auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job)
2615     {
2616         SchedulerJob::JOBStatus const s = job->getState();
2617         return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s;
2618     };
2619 
2620     /* If there are no jobs left to run in the filtered list, stop evaluation */
2621     if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted))
2622     {
2623         appendLogText(i18n("No jobs left in the scheduler queue after evaluating."));
2624         setCurrentJob(nullptr);
2625         return;
2626     }
2627     /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */
2628     else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) &&
2629              errorHandlingDontRestartButton->isChecked() == false)
2630     {
2631         appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those."));
2632         std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job)
2633         {
2634             if (SchedulerJob::JOB_ABORTED == job->getState())
2635                 job->setState(SchedulerJob::JOB_EVALUATION);
2636         });
2637 
2638         return;
2639     }
2640 
2641     /* The job to run is the first scheduled, locate it in the list */
2642     QList<SchedulerJob*>::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(),
2643             sortedJobs.end(), [](SchedulerJob * const job)
2644     {
2645         return SchedulerJob::JOB_SCHEDULED == job->getState();
2646     });
2647 
2648     /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */
2649     if (sortedJobs.end() == job_to_execute_iterator)
2650     {
2651         appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup."));
2652         setCurrentJob(nullptr);
2653         return;
2654     }
2655 
2656     /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */
2657     QDateTime const now = getLocalTime();
2658 
2659     /* Check if job can be processed right now */
2660     SchedulerJob * const job_to_execute = *job_to_execute_iterator;
2661     if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP)
2662         if( 0 <= calculateJobScore(job_to_execute, Dawn, Dusk, now))
2663             job_to_execute->setStartupTime(now);
2664 
2665     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.")
2666                                    .arg(job_to_execute->getName())
2667                                    .arg(job_to_execute->getPriority())
2668                                    .arg(job_to_execute->getScore());
2669 
2670     // Set the current job, and let the status timer execute it when ready
2671     setCurrentJob(job_to_execute);
2672 }
2673 
wakeUpScheduler()2674 void Scheduler::wakeUpScheduler()
2675 {
2676     sleepLabel->hide();
2677 
2678     if (preemptiveShutdown())
2679     {
2680         disablePreemptiveShutdown();
2681         appendLogText(i18n("Scheduler is awake."));
2682         start();
2683     }
2684     else
2685     {
2686         if (state == SCHEDULER_RUNNING)
2687             appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready..."));
2688         else
2689             appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed."));
2690 
2691         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
2692         setupNextIteration(RUN_SCHEDULER);
2693     }
2694 }
2695 
getWeatherScore() const2696 int16_t Scheduler::getWeatherScore() const
2697 {
2698     if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false)
2699         return 0;
2700 
2701     if (weatherStatus == ISD::Weather::WEATHER_WARNING)
2702         return BAD_SCORE / 2;
2703     else if (weatherStatus == ISD::Weather::WEATHER_ALERT)
2704         return BAD_SCORE;
2705 
2706     return 0;
2707 }
2708 
getDarkSkyScore(QDateTime const & dawn,QDateTime const & dusk,QDateTime const & when)2709 int16_t Scheduler::getDarkSkyScore(QDateTime const &dawn, QDateTime const &dusk, QDateTime const &when)
2710 {
2711     double const secsPerDay = 24.0 * 3600.0;
2712 
2713     // Dark sky score is calculated based on distance to today's next dawn and dusk.
2714     // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes.
2715     // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50.
2716     // - If observation is before next dawn, which arrives first, score is fraction of the day from beginning of observation to dawn time, as percentage.
2717     // - If observation is before next dusk, which arrives first, score is BAD_SCORE.
2718     //
2719     // If observation time is invalid, the score is calculated for the current day time.
2720     // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score.
2721 
2722     // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation.
2723 
2724     // If both dawn and dusk are in the past, (incorrectly) readjust the dawn and dusk to the next day
2725     // This was OK for next-day calculations, but Scheduler should now drop dark sky scores and rely on SchedulerJob dawn and dusk
2726     QDateTime const now = when.isValid() ? when : getLocalTime();
2727     int const earlyDawnSecs = now.secsTo(dawn.addDays(dawn < now ? dawn.daysTo(now) + 1 : 0).addSecs(
2728             -60.0 * Options::preDawnTime()));
2729     int const dawnSecs = now.secsTo(dawn.addDays(dawn < now ? dawn.daysTo(now) + 1 : 0));
2730     int const duskSecs = now.secsTo(dusk.addDays(dawn < now ? dusk.daysTo(now) + 1 : 0));
2731     int const obsSecs = now.secsTo(when);
2732 
2733     Q_ASSERT_X(dawnSecs >= 0, __FUNCTION__, "Scheduler computes the next dawn after the considered event.");
2734     Q_ASSERT_X(duskSecs >= 0, __FUNCTION__, "Scheduler computes the next dusk after the considered event.");
2735 
2736     // If dawn is future and the next event is dusk, it is day time
2737     if (obsSecs < duskSecs && duskSecs <= dawnSecs)
2738         return BAD_SCORE;
2739 
2740     // If dawn is past and the next event is dusk, it is still day time
2741     if (dawnSecs <= obsSecs && obsSecs < duskSecs)
2742         return BAD_SCORE;
2743 
2744     // If early dawn is past and the next event is dawn, it could be OK but nope
2745     if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs && dawnSecs <= duskSecs)
2746         return BAD_SCORE / 50;
2747 
2748     // If the next event is early dawn, then it is night time
2749     if (obsSecs < earlyDawnSecs && earlyDawnSecs <= dawnSecs && earlyDawnSecs <= duskSecs)
2750         return static_cast <int16_t> ((100 * (earlyDawnSecs - obsSecs)) / secsPerDay);
2751 
2752     return BAD_SCORE;
2753 }
2754 
calculateJobScore(SchedulerJob const * job,QDateTime const & dawn,QDateTime const & dusk,QDateTime const & when)2755 int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &dawn, QDateTime const &dusk,
2756                                      QDateTime const &when)
2757 {
2758     if (nullptr == job)
2759         return BAD_SCORE;
2760 
2761     /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */
2762     if (!job->getLightFramesRequired())
2763         return 1000;
2764 
2765     int16_t total = 0;
2766 
2767     /* As soon as one score is negative, it's a no-go and other scores are unneeded */
2768 
2769     if (job->getEnforceTwilight())
2770     {
2771         int16_t const darkSkyScore = getDarkSkyScore(dawn, dusk, when);
2772 
2773         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3")
2774                                        .arg(job->getName())
2775                                        .arg(QString::asprintf("%+d", darkSkyScore))
2776                                        .arg(when.toString(job->getDateTimeDisplayFormat()));
2777 
2778         total += darkSkyScore;
2779     }
2780 
2781     /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user.
2782      * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage.
2783      */
2784     if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/)
2785     {
2786         int16_t const altitudeScore = job->getAltitudeScore(when);
2787 
2788         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3")
2789                                        .arg(job->getName())
2790                                        .arg(QString::asprintf("%+d", altitudeScore))
2791                                        .arg(when.toString(job->getDateTimeDisplayFormat()));
2792 
2793         total += altitudeScore;
2794     }
2795 
2796     if (0 <= total)
2797     {
2798         int16_t const moonSeparationScore = job->getMoonSeparationScore(when);
2799 
2800         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3")
2801                                        .arg(job->getName())
2802                                        .arg(QString::asprintf("%+d", moonSeparationScore))
2803                                        .arg(when.toString(job->getDateTimeDisplayFormat()));
2804 
2805         total += moonSeparationScore;
2806     }
2807 
2808     qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.")
2809                                   .arg(job->getName())
2810                                   .arg(QString::asprintf("%+d", total))
2811                                   .arg(when.toString(job->getDateTimeDisplayFormat()));
2812 
2813     return total;
2814 }
2815 
calculateDawnDusk()2816 void Scheduler::calculateDawnDusk()
2817 {
2818     SchedulerJob::calculateDawnDusk(QDateTime(), Dawn, Dusk);
2819 
2820     preDawnDateTime = Dawn.addSecs(-60.0 * abs(Options::preDawnTime()));
2821 }
2822 
executeJob(SchedulerJob * job)2823 void Scheduler::executeJob(SchedulerJob *job)
2824 {
2825     // Some states have executeJob called after current job is cancelled - checkStatus does this
2826     if (job == nullptr)
2827         return;
2828 
2829     // Don't execute the current job if it is already busy
2830     if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState())
2831         return;
2832 
2833     setCurrentJob(job);
2834     int index = jobs.indexOf(job);
2835     if (index >= 0)
2836         queueTable->selectRow(index);
2837 
2838     // If we already started, we check when the next object is scheduled at.
2839     // If it is more than 30 minutes in the future, we park the mount if that is supported
2840     // and we unpark when it is due to start.
2841     //int const nextObservationTime = now.secsTo(currentJob->getStartupTime());
2842 
2843     // If the time to wait is greater than the lead time (5 minutes by default)
2844     // then we sleep, otherwise we wait. It's the same thing, just different labels.
2845     if (shouldSchedulerSleep(currentJob))
2846         return;
2847     // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt
2848     else if (0 < getLocalTime().secsTo(currentJob->getStartupTime()))
2849         return;
2850 
2851     // From this point job can be executed now
2852 
2853     if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress())
2854     {
2855         QString sanitized = job->getName();
2856         sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
2857                     // Remove any two or more __
2858                     .replace( QRegularExpression("_{2,}"), "_")
2859                     // Remove any _ at the end
2860                     .replace( QRegularExpression("_$"), "");
2861         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s%s\n", __LINE__, "captureInterface:setProperty", "targetName=",
2862                    sanitized.toLatin1().data());
2863         captureInterface->setProperty("targetName", sanitized);
2864     }
2865 
2866     calculateDawnDusk();
2867     updateNightTime();
2868 
2869     // Reset autofocus so that focus step is applied properly when checked
2870     // When the focus step is not checked, the capture module will eventually run focus periodically
2871     autofocusCompleted = false;
2872 
2873     qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName();
2874 
2875     currentJob->setState(SchedulerJob::JOB_BUSY);
2876 
2877     KNotification::event(QLatin1String("EkosSchedulerJobStart"),
2878                          i18n("Ekos job started (%1)", currentJob->getName()));
2879 
2880     // No need to continue evaluating jobs as we already have one.
2881     TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_JOBCHECK).toLatin1().data());
2882     setupNextIteration(RUN_JOBCHECK);
2883 }
2884 
checkEkosState()2885 bool Scheduler::checkEkosState()
2886 {
2887     if (state == SCHEDULER_PAUSED)
2888         return false;
2889 
2890     switch (ekosState)
2891     {
2892         case EKOS_IDLE:
2893         {
2894             if (m_EkosCommunicationStatus == Ekos::Success)
2895             {
2896                 ekosState = EKOS_READY;
2897                 return true;
2898             }
2899             else
2900             {
2901                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "start");
2902                 ekosInterface->call(QDBus::AutoDetect, "start");
2903                 ekosState = EKOS_STARTING;
2904                 startCurrentOperationTimer();
2905 
2906                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << m_EkosCommunicationStatus << "Starting Ekos...";
2907 
2908                 return false;
2909             }
2910         }
2911 
2912         case EKOS_STARTING:
2913         {
2914             if (m_EkosCommunicationStatus == Ekos::Success)
2915             {
2916                 appendLogText(i18n("Ekos started."));
2917                 ekosConnectFailureCount = 0;
2918                 ekosState = EKOS_READY;
2919                 return true;
2920             }
2921             else if (m_EkosCommunicationStatus == Ekos::Error)
2922             {
2923                 if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS)
2924                 {
2925                     appendLogText(i18n("Starting Ekos failed. Retrying..."));
2926                     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "start");
2927                     ekosInterface->call(QDBus::AutoDetect, "start");
2928                     return false;
2929                 }
2930 
2931                 appendLogText(i18n("Starting Ekos failed."));
2932                 stop();
2933                 return false;
2934             }
2935             else if (m_EkosCommunicationStatus == Ekos::Idle)
2936                 return false;
2937             // If a minute passed, give up
2938             else if (getCurrentOperationMsec() > (60 * 1000))
2939             {
2940                 if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS)
2941                 {
2942                     appendLogText(i18n("Starting Ekos timed out. Retrying..."));
2943                     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "stop");
2944                     ekosInterface->call(QDBus::AutoDetect, "stop");
2945                     QTimer::singleShot(1000, this, [&]()
2946                     {
2947                         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "start");
2948                         ekosInterface->call(QDBus::AutoDetect, "start");
2949                         startCurrentOperationTimer();
2950                     });
2951                     return false;
2952                 }
2953 
2954                 appendLogText(i18n("Starting Ekos timed out."));
2955                 stop();
2956                 return false;
2957             }
2958         }
2959         break;
2960 
2961         case EKOS_STOPPING:
2962         {
2963             if (m_EkosCommunicationStatus == Ekos::Idle)
2964             {
2965                 appendLogText(i18n("Ekos stopped."));
2966                 ekosState = EKOS_IDLE;
2967                 return true;
2968             }
2969         }
2970         break;
2971 
2972         case EKOS_READY:
2973             return true;
2974     }
2975     return false;
2976 }
2977 
isINDIConnected()2978 bool Scheduler::isINDIConnected()
2979 {
2980     return (m_INDICommunicationStatus == Ekos::Success);
2981 }
2982 
checkINDIState()2983 bool Scheduler::checkINDIState()
2984 {
2985     if (state == SCHEDULER_PAUSED)
2986         return false;
2987 
2988     //qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State" << indiState;
2989 
2990     switch (indiState)
2991     {
2992         case INDI_IDLE:
2993         {
2994             if (m_INDICommunicationStatus == Ekos::Success)
2995             {
2996                 indiState = INDI_PROPERTY_CHECK;
2997                 indiConnectFailureCount = 0;
2998                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties...";
2999             }
3000             else
3001             {
3002                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices...";
3003                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "connectDevices");
3004                 ekosInterface->call(QDBus::AutoDetect, "connectDevices");
3005                 indiState = INDI_CONNECTING;
3006 
3007                 startCurrentOperationTimer();
3008             }
3009         }
3010         break;
3011 
3012         case INDI_CONNECTING:
3013         {
3014             if (m_INDICommunicationStatus == Ekos::Success)
3015             {
3016                 appendLogText(i18n("INDI devices connected."));
3017                 indiState = INDI_PROPERTY_CHECK;
3018             }
3019             else if (m_INDICommunicationStatus == Ekos::Error)
3020             {
3021                 if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS)
3022                 {
3023                     appendLogText(i18n("One or more INDI devices failed to connect. Retrying..."));
3024                     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "connectDevices");
3025                     ekosInterface->call(QDBus::AutoDetect, "connectDevices");
3026                 }
3027                 else
3028                 {
3029                     appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details."));
3030                     stop();
3031                 }
3032             }
3033             // If 30 seconds passed, we retry
3034             else if (getCurrentOperationMsec() > (30 * 1000))
3035             {
3036                 if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS)
3037                 {
3038                     appendLogText(i18n("One or more INDI devices timed out. Retrying..."));
3039                     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "ekosInterface", "connectDevices");
3040                     ekosInterface->call(QDBus::AutoDetect, "connectDevices");
3041                     startCurrentOperationTimer();
3042                 }
3043                 else
3044                 {
3045                     appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details."));
3046                     stop();
3047                 }
3048             }
3049         }
3050         break;
3051 
3052         case INDI_DISCONNECTING:
3053         {
3054             if (m_INDICommunicationStatus == Ekos::Idle)
3055             {
3056                 appendLogText(i18n("INDI devices disconnected."));
3057                 indiState = INDI_IDLE;
3058                 return true;
3059             }
3060         }
3061         break;
3062 
3063         case INDI_PROPERTY_CHECK:
3064         {
3065             qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties.";
3066             // If dome unparking is required then we wait for dome interface
3067             if (unparkDomeCheck->isChecked() && m_DomeReady == false)
3068             {
3069                 if (getCurrentOperationMsec() > (30 * 1000))
3070                 {
3071                     startCurrentOperationTimer();
3072                     appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover..."));
3073                     disconnectINDI();
3074                     stopEkos();
3075                 }
3076 
3077                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome unpark required but dome is not yet ready.";
3078                 return false;
3079             }
3080 
3081             // If mount unparking is required then we wait for mount interface
3082             if (unparkMountCheck->isChecked() && m_MountReady == false)
3083             {
3084                 if (getCurrentOperationMsec() > (30 * 1000))
3085                 {
3086                     startCurrentOperationTimer();
3087                     appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover..."));
3088                     disconnectINDI();
3089                     stopEkos();
3090                 }
3091 
3092                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready.";
3093                 return false;
3094             }
3095 
3096             // If cap unparking is required then we wait for cap interface
3097             if (uncapCheck->isChecked() && m_CapReady == false)
3098             {
3099                 if (getCurrentOperationMsec() > (30 * 1000))
3100                 {
3101                     startCurrentOperationTimer();
3102                     appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover..."));
3103                     disconnectINDI();
3104                     stopEkos();
3105                 }
3106 
3107                 qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready.";
3108                 return false;
3109             }
3110 
3111             // capture interface is required at all times to proceed.
3112             if (captureInterface.isNull())
3113                 return false;
3114 
3115             if (m_CaptureReady == false)
3116             {
3117                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:property", "coolerControl");
3118                 QVariant hasCoolerControl = captureInterface->property("coolerControl");
3119                 TEST_PRINT(stderr, "  @@@dbus received %s\n",
3120                            !hasCoolerControl.isValid() ? "invalid" : (hasCoolerControl.toBool() ? "T" : "F"));
3121                 if (hasCoolerControl.isValid())
3122                 {
3123                     warmCCDCheck->setEnabled(hasCoolerControl.toBool());
3124                     m_CaptureReady = true;
3125                 }
3126                 else
3127                     qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet...";
3128             }
3129 
3130             indiState = INDI_READY;
3131             indiConnectFailureCount = 0;
3132             return true;
3133         }
3134 
3135         case INDI_READY:
3136             return true;
3137     }
3138 
3139     return false;
3140 }
3141 
checkStartupState()3142 bool Scheduler::checkStartupState()
3143 {
3144     if (state == SCHEDULER_PAUSED)
3145         return false;
3146 
3147     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(startupState);
3148 
3149     switch (startupState)
3150     {
3151         case STARTUP_IDLE:
3152         {
3153             KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process"));
3154 
3155             qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process...";
3156 
3157             // If Ekos is already started, we skip the script and move on to dome unpark step
3158             // unless we do not have light frames, then we skip all
3159             //QDBusReply<int> isEkosStarted;
3160             //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
3161             //if (isEkosStarted.value() == Ekos::Success)
3162             if (m_EkosCommunicationStatus == Ekos::Success)
3163             {
3164                 if (startupScriptURL.isEmpty() == false)
3165                     appendLogText(i18n("Ekos is already started, skipping startup script..."));
3166 
3167                 if (currentJob->getLightFramesRequired())
3168                     startupState = STARTUP_UNPARK_DOME;
3169                 else
3170                     startupState = STARTUP_COMPLETE;
3171                 return true;
3172             }
3173 
3174             if (schedulerProfileCombo->currentText() != i18n("Default"))
3175             {
3176                 QList<QVariant> profile;
3177                 profile.append(schedulerProfileCombo->currentText());
3178                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "ekosInterface:callWithArgs", "setProfile");
3179                 ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile);
3180             }
3181 
3182             if (startupScriptURL.isEmpty() == false)
3183             {
3184                 startupState = STARTUP_SCRIPT;
3185                 executeScript(startupScriptURL.toString(QUrl::PreferLocalFile));
3186                 return false;
3187             }
3188 
3189             startupState = STARTUP_UNPARK_DOME;
3190             return false;
3191         }
3192 
3193         case STARTUP_SCRIPT:
3194             return false;
3195 
3196         case STARTUP_UNPARK_DOME:
3197             // If there is no job in case of manual startup procedure,
3198             // or if the job requires light frames, let's proceed with
3199             // unparking the dome, otherwise startup process is complete.
3200             if (currentJob == nullptr || currentJob->getLightFramesRequired())
3201             {
3202                 if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked())
3203                     unParkDome();
3204                 else
3205                     startupState = STARTUP_UNPARK_MOUNT;
3206             }
3207             else
3208             {
3209                 startupState = STARTUP_COMPLETE;
3210                 return true;
3211             }
3212 
3213             break;
3214 
3215         case STARTUP_UNPARKING_DOME:
3216             checkDomeParkingStatus();
3217             break;
3218 
3219         case STARTUP_UNPARK_MOUNT:
3220             if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked())
3221                 unParkMount();
3222             else
3223                 startupState = STARTUP_UNPARK_CAP;
3224             break;
3225 
3226         case STARTUP_UNPARKING_MOUNT:
3227             checkMountParkingStatus();
3228             break;
3229 
3230         case STARTUP_UNPARK_CAP:
3231             if (uncapCheck->isEnabled() && uncapCheck->isChecked())
3232                 unParkCap();
3233             else
3234                 startupState = STARTUP_COMPLETE;
3235             break;
3236 
3237         case STARTUP_UNPARKING_CAP:
3238             checkCapParkingStatus();
3239             break;
3240 
3241         case STARTUP_COMPLETE:
3242             return true;
3243 
3244         case STARTUP_ERROR:
3245             stop();
3246             return true;
3247     }
3248 
3249     return false;
3250 }
3251 
checkShutdownState()3252 bool Scheduler::checkShutdownState()
3253 {
3254     if (state == SCHEDULER_PAUSED)
3255         return false;
3256 
3257     qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state...";
3258 
3259     switch (shutdownState)
3260     {
3261         case SHUTDOWN_IDLE:
3262             KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process"));
3263 
3264             qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process...";
3265 
3266             //            weatherTimer.stop();
3267             //            weatherTimer.disconnect();
3268             weatherLabel->hide();
3269 
3270             setCurrentJob(nullptr);
3271 
3272             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SHUTDOWN).toLatin1().data());
3273             setupNextIteration(RUN_SHUTDOWN);
3274 
3275             if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked())
3276             {
3277                 appendLogText(i18n("Warming up CCD..."));
3278 
3279                 // Turn it off
3280                 //QVariant arg(false);
3281                 //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg);
3282                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s%s\n", __LINE__, "captureInterface:setProperty", "coolerControl=", "false");
3283                 captureInterface->setProperty("coolerControl", false);
3284             }
3285 
3286             // The following steps require a connection to the INDI server
3287             if (isINDIConnected())
3288             {
3289                 if (capCheck->isEnabled() && capCheck->isChecked())
3290                 {
3291                     shutdownState = SHUTDOWN_PARK_CAP;
3292                     return false;
3293                 }
3294 
3295                 if (parkMountCheck->isEnabled() && parkMountCheck->isChecked())
3296                 {
3297                     shutdownState = SHUTDOWN_PARK_MOUNT;
3298                     return false;
3299                 }
3300 
3301                 if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked())
3302                 {
3303                     shutdownState = SHUTDOWN_PARK_DOME;
3304                     return false;
3305                 }
3306             }
3307             else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection."));
3308 
3309             if (shutdownScriptURL.isEmpty() == false)
3310             {
3311                 shutdownState = SHUTDOWN_SCRIPT;
3312                 return false;
3313             }
3314 
3315             shutdownState = SHUTDOWN_COMPLETE;
3316             return true;
3317 
3318         case SHUTDOWN_PARK_CAP:
3319             if (!isINDIConnected())
3320             {
3321                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
3322                 shutdownState = SHUTDOWN_SCRIPT;
3323             }
3324             else if (capCheck->isEnabled() && capCheck->isChecked())
3325                 parkCap();
3326             else
3327                 shutdownState = SHUTDOWN_PARK_MOUNT;
3328             break;
3329 
3330         case SHUTDOWN_PARKING_CAP:
3331             checkCapParkingStatus();
3332             break;
3333 
3334         case SHUTDOWN_PARK_MOUNT:
3335             if (!isINDIConnected())
3336             {
3337                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
3338                 shutdownState = SHUTDOWN_SCRIPT;
3339             }
3340             else if (parkMountCheck->isEnabled() && parkMountCheck->isChecked())
3341                 parkMount();
3342             else
3343                 shutdownState = SHUTDOWN_PARK_DOME;
3344             break;
3345 
3346         case SHUTDOWN_PARKING_MOUNT:
3347             checkMountParkingStatus();
3348             break;
3349 
3350         case SHUTDOWN_PARK_DOME:
3351             if (!isINDIConnected())
3352             {
3353                 qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection.";
3354                 shutdownState = SHUTDOWN_SCRIPT;
3355             }
3356             else if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked())
3357                 parkDome();
3358             else
3359                 shutdownState = SHUTDOWN_SCRIPT;
3360             break;
3361 
3362         case SHUTDOWN_PARKING_DOME:
3363             checkDomeParkingStatus();
3364             break;
3365 
3366         case SHUTDOWN_SCRIPT:
3367             if (shutdownScriptURL.isEmpty() == false)
3368             {
3369                 // Need to stop Ekos now before executing script if it happens to stop INDI
3370                 if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI())
3371                 {
3372                     stopEkos();
3373                     return false;
3374                 }
3375 
3376                 shutdownState = SHUTDOWN_SCRIPT_RUNNING;
3377                 executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile));
3378             }
3379             else
3380                 shutdownState = SHUTDOWN_COMPLETE;
3381             break;
3382 
3383         case SHUTDOWN_SCRIPT_RUNNING:
3384             return false;
3385 
3386         case SHUTDOWN_COMPLETE:
3387             return completeShutdown();
3388 
3389         case SHUTDOWN_ERROR:
3390             stop();
3391             return true;
3392     }
3393 
3394     return false;
3395 }
3396 
checkParkWaitState()3397 bool Scheduler::checkParkWaitState()
3398 {
3399     if (state == SCHEDULER_PAUSED)
3400         return false;
3401 
3402     if (parkWaitState == PARKWAIT_IDLE)
3403         return true;
3404 
3405     // qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State...";
3406 
3407     switch (parkWaitState)
3408     {
3409         case PARKWAIT_PARK:
3410             parkMount();
3411             break;
3412 
3413         case PARKWAIT_PARKING:
3414             checkMountParkingStatus();
3415             break;
3416 
3417         case PARKWAIT_UNPARK:
3418             unParkMount();
3419             break;
3420 
3421         case PARKWAIT_UNPARKING:
3422             checkMountParkingStatus();
3423             break;
3424 
3425         case PARKWAIT_IDLE:
3426         case PARKWAIT_PARKED:
3427         case PARKWAIT_UNPARKED:
3428             return true;
3429 
3430         case PARKWAIT_ERROR:
3431             appendLogText(i18n("park/unpark wait procedure failed, aborting..."));
3432             stop();
3433             return true;
3434 
3435     }
3436 
3437     return false;
3438 }
3439 
executeScript(const QString & filename)3440 void Scheduler::executeScript(const QString &filename)
3441 {
3442     appendLogText(i18n("Executing script %1...", filename));
3443 
3444     connect(&scriptProcess, &QProcess::readyReadStandardOutput, this, &Scheduler::readProcessOutput);
3445 
3446     connect(&scriptProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
3447             this, [this](int exitCode, QProcess::ExitStatus)
3448     {
3449         checkProcessExit(exitCode);
3450     });
3451 
3452     scriptProcess.start(filename);
3453 }
3454 
readProcessOutput()3455 void Scheduler::readProcessOutput()
3456 {
3457     appendLogText(scriptProcess.readAllStandardOutput().simplified());
3458 }
3459 
checkProcessExit(int exitCode)3460 void Scheduler::checkProcessExit(int exitCode)
3461 {
3462     scriptProcess.disconnect();
3463 
3464     if (exitCode == 0)
3465     {
3466         if (startupState == STARTUP_SCRIPT)
3467             startupState = STARTUP_UNPARK_DOME;
3468         else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING)
3469             shutdownState = SHUTDOWN_COMPLETE;
3470 
3471         return;
3472     }
3473 
3474     if (startupState == STARTUP_SCRIPT)
3475     {
3476         appendLogText(i18n("Startup script failed, aborting..."));
3477         startupState = STARTUP_ERROR;
3478     }
3479     else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING)
3480     {
3481         appendLogText(i18n("Shutdown script failed, aborting..."));
3482         shutdownState = SHUTDOWN_ERROR;
3483     }
3484 }
3485 
completeShutdown()3486 bool Scheduler::completeShutdown()
3487 {
3488     // If INDI is not done disconnecting, try again later
3489     if (indiState == INDI_DISCONNECTING && checkINDIState() == false)
3490         return false;
3491 
3492     // Disconnect INDI if required first
3493     if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown())
3494     {
3495         disconnectINDI();
3496         return false;
3497     }
3498 
3499     // If Ekos is not done stopping, try again later
3500     if (ekosState == EKOS_STOPPING && checkEkosState() == false)
3501         return false;
3502 
3503     // Stop Ekos if required.
3504     if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown())
3505     {
3506         stopEkos();
3507         return false;
3508     }
3509 
3510     if (shutdownState == SHUTDOWN_COMPLETE)
3511         appendLogText(i18n("Shutdown complete."));
3512     else
3513         appendLogText(i18n("Shutdown procedure failed, aborting..."));
3514 
3515     // Stop Scheduler
3516     stop();
3517 
3518     return true;
3519 }
3520 
checkStatus()3521 bool Scheduler::checkStatus()
3522 {
3523     for (auto job : jobs)
3524         job->updateJobCells();
3525 
3526     if (state == SCHEDULER_PAUSED)
3527     {
3528         if (currentJob == nullptr)
3529         {
3530             setPaused();
3531             return false;
3532         }
3533         switch (currentJob->getState())
3534         {
3535             case  SchedulerJob::JOB_BUSY:
3536                 // do nothing
3537                 break;
3538             case  SchedulerJob::JOB_COMPLETE:
3539                 // start finding next job before pausing
3540                 break;
3541             default:
3542                 // in all other cases pause
3543                 setPaused();
3544                 break;
3545         }
3546     }
3547 
3548     // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs
3549     if (currentJob == nullptr)
3550     {
3551         // #2.1 If shutdown is already complete or in error, we need to stop
3552         if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR)
3553         {
3554             return completeShutdown();
3555         }
3556 
3557         // #2.2  Check if shutdown is in progress
3558         if (shutdownState > SHUTDOWN_IDLE)
3559         {
3560             // If Ekos is not done stopping, try again later
3561             if (ekosState == EKOS_STOPPING && checkEkosState() == false)
3562                 return false;
3563 
3564             checkShutdownState();
3565             return false;
3566         }
3567 
3568         // #2.3 Check if park wait procedure is in progress
3569         if (checkParkWaitState() == false)
3570             return false;
3571 
3572         // #2.4 If not in shutdown state, evaluate the jobs
3573         evaluateJobs(false);
3574 
3575         // #2.5 If there is no current job after evaluation, shutdown
3576         if (nullptr == currentJob)
3577         {
3578             checkShutdownState();
3579             return false;
3580         }
3581     }
3582     // JM 2018-12-07: Check if we need to sleep
3583     else if (shouldSchedulerSleep(currentJob) == false)
3584     {
3585         // #3 Check if startup procedure has failed.
3586         if (startupState == STARTUP_ERROR)
3587         {
3588             // Stop Scheduler
3589             stop();
3590             return true;
3591         }
3592 
3593         // #4 Check if startup procedure Phase #1 is complete (Startup script)
3594         if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT)
3595             return false;
3596 
3597         // #5 Check if Ekos is started
3598         if (checkEkosState() == false)
3599             return false;
3600 
3601         // #6 Check if INDI devices are connected.
3602         if (checkINDIState() == false)
3603             return false;
3604 
3605         // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job
3606         if (checkParkWaitState() == false)
3607             return false;
3608 
3609         // #7 Check if startup procedure Phase #2 is complete (Unparking phase)
3610         if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false)
3611             return false;
3612 
3613         // #8 Check it it already completed (should only happen starting a paused job)
3614         //    Find the next job in this case, otherwise execute the current one
3615         if (currentJob->getState() == SchedulerJob::JOB_COMPLETE)
3616             findNextJob();
3617         else
3618             executeJob(currentJob);
3619     }
3620 
3621     return true;
3622 }
3623 
checkJobStage()3624 void Scheduler::checkJobStage()
3625 {
3626     Q_ASSERT_X(currentJob, __FUNCTION__, "Actual current job is required to check job stage");
3627     if (!currentJob)
3628         return;
3629 
3630     if (checkJobStageCounter == 0)
3631     {
3632         qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" <<
3633                                        currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) <<
3634                                        "state" << currentJob->getState();
3635         if (checkJobStageCounter++ == 30)
3636             checkJobStageCounter = 0;
3637     }
3638 
3639     QDateTime const now = getLocalTime();
3640 
3641     /* Refresh the score of the current job */
3642     /* currentJob->setScore(calculateJobScore(currentJob, now)); */
3643 
3644     /* If current job is scheduled and has not started yet, wait */
3645     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
3646         if (now < currentJob->getStartupTime())
3647             return;
3648 
3649     // #1 Check if we need to stop at some point
3650     if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT &&
3651             currentJob->getState() == SchedulerJob::JOB_BUSY)
3652     {
3653         // If the job reached it COMPLETION time, we stop it.
3654         if (now.secsTo(currentJob->getCompletionTime()) <= 0)
3655         {
3656             appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(),
3657                                currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())));
3658             currentJob->setState(SchedulerJob::JOB_COMPLETE);
3659             stopCurrentJobAction();
3660             findNextJob();
3661             return;
3662         }
3663     }
3664 
3665     // #2 Check if altitude restriction still holds true
3666     if (currentJob->hasAltitudeConstraint())
3667     {
3668         SkyPoint p = currentJob->getTargetCoords();
3669 
3670         p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat());
3671 
3672         /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */
3673         const double altitudeConstraint = currentJob->getMinAltitudeConstraint(p.az().Degrees());
3674         if (p.alt().Degrees() < altitudeConstraint)
3675         {
3676             // Only terminate job due to altitude limitation if mount is NOT parked.
3677             if (isMountParked() == false)
3678             {
3679                 appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), "
3680                                    "marking idle.", currentJob->getName(),
3681                                    QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()),
3682                                    QString("%L1").arg(altitudeConstraint, 0, 'f', minAltitude->decimals())));
3683 
3684                 currentJob->setState(SchedulerJob::JOB_IDLE);
3685                 stopCurrentJobAction();
3686                 findNextJob();
3687                 return;
3688             }
3689         }
3690     }
3691 
3692     // #3 Check if moon separation is still valid
3693     if (currentJob->getMinMoonSeparation() > 0)
3694     {
3695         SkyPoint p = currentJob->getTargetCoords();
3696         p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat());
3697 
3698         double moonSeparation = currentJob->getCurrentMoonSeparation();
3699 
3700         if (moonSeparation < currentJob->getMinMoonSeparation())
3701         {
3702             // Only terminate job due to moon separation limitation if mount is NOT parked.
3703             if (isMountParked() == false)
3704             {
3705                 appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 "
3706                                    "degrees), marking idle.",
3707                                    moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation()));
3708 
3709                 currentJob->setState(SchedulerJob::JOB_IDLE);
3710                 stopCurrentJobAction();
3711                 findNextJob();
3712                 return;
3713             }
3714         }
3715     }
3716 
3717     // #4 Check if we're not at dawn - dawn is still next event before dusk, and early dawn is past
3718     if (currentJob->getEnforceTwilight() && ((Dawn < Dusk && preDawnDateTime < now) || (Dusk < Dawn)))
3719     {
3720         // If either mount or dome are not parked, we shutdown if we approach dawn
3721         if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false))
3722         {
3723             // Minute is a DOUBLE value, do not use i18np
3724             appendLogText(i18n(
3725                               "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking idle.",
3726                               preDawnDateTime.toString(), abs(Options::preDawnTime()), currentJob->getName()));
3727             currentJob->setState(SchedulerJob::JOB_IDLE);
3728             stopCurrentJobAction();
3729             findNextJob();
3730             return;
3731         }
3732     }
3733 
3734     // #5 Check system status to improve robustness
3735     // This handles external events such as disconnections or end-user manipulating INDI panel
3736     if (!checkStatus())
3737         return;
3738 
3739     // #5b Check the guiding timer, and possibly restart guiding.
3740     processGuidingTimer();
3741 
3742     // #6 Check each stage is processing properly
3743     // FIXME: Vanishing property should trigger a call to its event callback
3744     switch (currentJob->getStage())
3745     {
3746         case SchedulerJob::STAGE_IDLE:
3747             getNextAction();
3748             break;
3749 
3750         case SchedulerJob::STAGE_ALIGNING:
3751             // Let's make sure align module does not become unresponsive
3752             if (getCurrentOperationMsec() > static_cast<int>(ALIGN_INACTIVITY_TIMEOUT))
3753             {
3754                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "alignInterface:property", "status");
3755                 QVariant const status = alignInterface->property("status");
3756                 TEST_PRINT(stderr, "  @@@dbus received %d\n", !status.isValid() ? -1 : status.toInt());
3757                 Ekos::AlignState alignStatus = static_cast<Ekos::AlignState>(status.toInt());
3758 
3759                 if (alignStatus == Ekos::ALIGN_IDLE)
3760                 {
3761                     if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS)
3762                     {
3763                         qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request...";
3764                         startAstrometry();
3765                     }
3766                     else
3767                     {
3768                         appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName()));
3769                         currentJob->setState(SchedulerJob::JOB_ABORTED);
3770                         findNextJob();
3771                     }
3772                 }
3773                 else
3774                     startCurrentOperationTimer();
3775             }
3776             break;
3777 
3778         case SchedulerJob::STAGE_CAPTURING:
3779             // Let's make sure capture module does not become unresponsive
3780             if (getCurrentOperationMsec() > static_cast<int>(CAPTURE_INACTIVITY_TIMEOUT))
3781             {
3782                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:property", "status");
3783                 QVariant const status = captureInterface->property("status");
3784                 TEST_PRINT(stderr, "  @@@dbus received %d\n", !status.isValid() ? -1 : status.toInt());
3785                 Ekos::CaptureState captureStatus = static_cast<Ekos::CaptureState>(status.toInt());
3786 
3787                 if (captureStatus == Ekos::CAPTURE_IDLE)
3788                 {
3789                     if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS)
3790                     {
3791                         qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request...";
3792                         startCapture();
3793                     }
3794                     else
3795                     {
3796                         appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName()));
3797                         currentJob->setState(SchedulerJob::JOB_ABORTED);
3798                         findNextJob();
3799                     }
3800                 }
3801                 else startCurrentOperationTimer();
3802             }
3803             break;
3804 
3805         case SchedulerJob::STAGE_FOCUSING:
3806             // Let's make sure focus module does not become unresponsive
3807             if (getCurrentOperationMsec() > static_cast<int>(FOCUS_INACTIVITY_TIMEOUT))
3808             {
3809                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "focusInterface:property", "status");
3810                 QVariant const status = focusInterface->property("status");
3811                 TEST_PRINT(stderr, "  @@@dbus received %d\n", !status.isValid() ? -1 : status.toInt());
3812                 Ekos::FocusState focusStatus = static_cast<Ekos::FocusState>(status.toInt());
3813 
3814                 if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING)
3815                 {
3816                     if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS)
3817                     {
3818                         qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request...";
3819                         startFocusing();
3820                     }
3821                     else
3822                     {
3823                         appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName()));
3824                         currentJob->setState(SchedulerJob::JOB_ABORTED);
3825                         findNextJob();
3826                     }
3827                 }
3828                 else startCurrentOperationTimer();
3829             }
3830             break;
3831 
3832         case SchedulerJob::STAGE_GUIDING:
3833             // Let's make sure guide module does not become unresponsive
3834             if (getCurrentOperationMsec() > GUIDE_INACTIVITY_TIMEOUT)
3835             {
3836                 GuideState guideStatus = getGuidingStatus();
3837 
3838                 if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED)
3839                 {
3840                     if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS)
3841                     {
3842                         qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request...";
3843                         startGuiding();
3844                     }
3845                     else
3846                     {
3847                         appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName()));
3848                         currentJob->setState(SchedulerJob::JOB_ABORTED);
3849                         findNextJob();
3850                     }
3851                 }
3852                 else startCurrentOperationTimer();
3853             }
3854             break;
3855 
3856         case SchedulerJob::STAGE_SLEWING:
3857         case SchedulerJob::STAGE_RESLEWING:
3858             // While slewing or re-slewing, check slew status can still be obtained
3859         {
3860             TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "status");
3861             QVariant const slewStatus = mountInterface->property("status");
3862             TEST_PRINT(stderr, "  @@@dbus received %d\n", !slewStatus.isValid() ? -1 : slewStatus.toInt());
3863 
3864             if (slewStatus.isValid())
3865             {
3866                 // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event
3867                 // FIXME: in that case, filter TRACKING events only?
3868                 ISD::Telescope::Status const status = static_cast<ISD::Telescope::Status>(slewStatus.toInt());
3869                 setMountStatus(status);
3870             }
3871             else
3872             {
3873                 appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", currentJob->getName()));
3874                 if (!manageConnectionLoss())
3875                     currentJob->setState(SchedulerJob::JOB_ERROR);
3876                 return;
3877             }
3878         }
3879         break;
3880 
3881         case SchedulerJob::STAGE_SLEW_COMPLETE:
3882         case SchedulerJob::STAGE_RESLEWING_COMPLETE:
3883             // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving
3884             if (m_DomeReady)
3885             {
3886                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "isMoving");
3887                 QVariant const isDomeMoving = domeInterface->property("isMoving");
3888                 TEST_PRINT(stderr, "  @@@dbus received %s\n",
3889                            !isDomeMoving.isValid() ? "invalid" : (isDomeMoving.value<bool>() ? "T" : "F"));
3890 
3891                 if (!isDomeMoving.isValid())
3892                 {
3893                     appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", currentJob->getName()));
3894                     if (!manageConnectionLoss())
3895                         currentJob->setState(SchedulerJob::JOB_ERROR);
3896                     return;
3897                 }
3898 
3899                 if (!isDomeMoving.value<bool>())
3900                     getNextAction();
3901             }
3902             else getNextAction();
3903             break;
3904 
3905         default:
3906             break;
3907     }
3908 }
3909 
getNextAction()3910 void Scheduler::getNextAction()
3911 {
3912     qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action...";
3913 
3914     switch (currentJob->getStage())
3915     {
3916         case SchedulerJob::STAGE_IDLE:
3917             if (currentJob->getLightFramesRequired())
3918             {
3919                 if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK)
3920                     startSlew();
3921                 else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false)
3922                 {
3923                     qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3485";
3924                     startFocusing();
3925                 }
3926                 else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
3927                     startAstrometry();
3928                 else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
3929                     if (getGuidingStatus() == GUIDE_GUIDING)
3930                     {
3931                         appendLogText(i18n("Guiding already running, directly start capturing."));
3932                         startCapture();
3933                     }
3934                     else
3935                         startGuiding();
3936                 else
3937                     startCapture();
3938             }
3939             else
3940             {
3941                 if (currentJob->getStepPipeline())
3942                     appendLogText(
3943                         i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.",
3944                              currentJob->getName()));
3945                 startCapture();
3946             }
3947 
3948             break;
3949 
3950         case SchedulerJob::STAGE_SLEW_COMPLETE:
3951             if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false)
3952             {
3953                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3514";
3954                 startFocusing();
3955             }
3956             else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
3957                 startAstrometry();
3958             else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
3959                 startGuiding();
3960             else
3961                 startCapture();
3962             break;
3963 
3964         case SchedulerJob::STAGE_FOCUS_COMPLETE:
3965             if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
3966                 startAstrometry();
3967             else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
3968                 startGuiding();
3969             else
3970                 startCapture();
3971             break;
3972 
3973         case SchedulerJob::STAGE_ALIGN_COMPLETE:
3974             currentJob->setStage(SchedulerJob::STAGE_RESLEWING);
3975             break;
3976 
3977         case SchedulerJob::STAGE_RESLEWING_COMPLETE:
3978             // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus
3979             // frame is ready for the capture module in-sequence-focus procedure.
3980             if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus())
3981                 // Post alignment re-focusing
3982             {
3983                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3544";
3984                 startFocusing();
3985             }
3986             else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
3987                 startGuiding();
3988             else
3989                 startCapture();
3990             break;
3991 
3992         case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE:
3993             if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
3994                 startGuiding();
3995             else
3996                 startCapture();
3997             break;
3998 
3999         case SchedulerJob::STAGE_GUIDING_COMPLETE:
4000             startCapture();
4001             break;
4002 
4003         default:
4004             break;
4005     }
4006 }
4007 
stopCurrentJobAction()4008 void Scheduler::stopCurrentJobAction()
4009 {
4010     if (nullptr != currentJob)
4011     {
4012         qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." <<
4013                                        currentJob->getStage();
4014 
4015         switch (currentJob->getStage())
4016         {
4017             case SchedulerJob::STAGE_IDLE:
4018                 break;
4019 
4020             case SchedulerJob::STAGE_SLEWING:
4021                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "abort");
4022                 mountInterface->call(QDBus::AutoDetect, "abort");
4023                 break;
4024 
4025             case SchedulerJob::STAGE_FOCUSING:
4026                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "focusInterface:call", "abort");
4027                 focusInterface->call(QDBus::AutoDetect, "abort");
4028                 break;
4029 
4030             case SchedulerJob::STAGE_ALIGNING:
4031                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "alignInterface:call", "abort");
4032                 alignInterface->call(QDBus::AutoDetect, "abort");
4033                 break;
4034 
4035             case SchedulerJob::STAGE_CAPTURING:
4036                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:call", "abort");
4037                 captureInterface->call(QDBus::AutoDetect, "abort");
4038                 break;
4039 
4040             default:
4041                 break;
4042         }
4043 
4044         /* Reset interrupted job stage */
4045         currentJob->setStage(SchedulerJob::STAGE_IDLE);
4046     }
4047 
4048     /* Guiding being a parallel process, check to stop it */
4049     stopGuiding();
4050 }
4051 
manageConnectionLoss()4052 bool Scheduler::manageConnectionLoss()
4053 {
4054     if (SCHEDULER_RUNNING != state)
4055         return false;
4056 
4057     // Don't manage loss if Ekos is actually down in the state machine
4058     switch (ekosState)
4059     {
4060         case EKOS_IDLE:
4061         case EKOS_STOPPING:
4062             return false;
4063 
4064         default:
4065             break;
4066     }
4067 
4068     // Don't manage loss if INDI is actually down in the state machine
4069     switch (indiState)
4070     {
4071         case INDI_IDLE:
4072         case INDI_DISCONNECTING:
4073             return false;
4074 
4075         default:
4076             break;
4077     }
4078 
4079     // If Ekos is assumed to be up, check its state
4080     //QDBusReply<int> const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus");
4081     if (m_EkosCommunicationStatus == Ekos::Success)
4082     {
4083         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss.");
4084 
4085         // If INDI is assumed to be up, check its state
4086         if (isINDIConnected())
4087         {
4088             // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error
4089             qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed.");
4090             return false;
4091         }
4092     }
4093 
4094     // Stop actions of the current job
4095     stopCurrentJobAction();
4096 
4097     // Acknowledge INDI and Ekos disconnections
4098     disconnectINDI();
4099     stopEkos();
4100 
4101     // Let the Scheduler attempt to connect INDI again
4102     return true;
4103 }
4104 
load(bool clearQueue,const QString & filename)4105 void Scheduler::load(bool clearQueue, const QString &filename)
4106 {
4107     QUrl fileURL;
4108 
4109     if (filename.isEmpty())
4110         fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Scheduler List"),
4111                                               dirPath,
4112                                               "Ekos Scheduler List (*.esl)");
4113     else fileURL.setUrl(filename);
4114 
4115     if (fileURL.isEmpty())
4116         return;
4117 
4118     if (fileURL.isValid() == false)
4119     {
4120         QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
4121         KSNotification::sorry(message, i18n("Invalid URL"));
4122         return;
4123     }
4124 
4125     dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
4126 
4127     if (clearQueue)
4128         removeAllJobs();
4129 
4130     /* Run a job idle evaluation after a successful load */
4131     if (appendEkosScheduleList(fileURL.toLocalFile()))
4132         startJobEvaluation();
4133 }
4134 
removeAllJobs()4135 void Scheduler::removeAllJobs()
4136 {
4137     if (jobUnderEdit >= 0)
4138         resetJobEdit();
4139 
4140     while (queueTable->rowCount() > 0)
4141         queueTable->removeRow(0);
4142 
4143     qDeleteAll(jobs);
4144     jobs.clear();
4145 }
4146 
loadScheduler(const QString & fileURL)4147 bool Scheduler::loadScheduler(const QString &fileURL)
4148 {
4149     removeAllJobs();
4150     return appendEkosScheduleList(fileURL);
4151 }
4152 
appendEkosScheduleList(const QString & fileURL)4153 bool Scheduler::appendEkosScheduleList(const QString &fileURL)
4154 {
4155     SchedulerState const old_state = state;
4156     state = SCHEDULER_LOADING;
4157 
4158     QFile sFile;
4159     sFile.setFileName(fileURL);
4160 
4161     if (!sFile.open(QIODevice::ReadOnly))
4162     {
4163         QString message = i18n("Unable to open file %1", fileURL);
4164         KSNotification::sorry(message, i18n("Could Not Open File"));
4165         state = old_state;
4166         return false;
4167     }
4168 
4169     LilXML *xmlParser = newLilXML();
4170     char errmsg[MAXRBUF];
4171     XMLEle *root = nullptr;
4172     XMLEle *ep   = nullptr;
4173     XMLEle *subEP = nullptr;
4174     char c;
4175 
4176     // We expect all data read from the XML to be in the C locale - QLocale::c()
4177     QLocale cLocale = QLocale::c();
4178 
4179     while (sFile.getChar(&c))
4180     {
4181         root = readXMLEle(xmlParser, c, errmsg);
4182 
4183         if (root)
4184         {
4185             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4186             {
4187                 const char *tag = tagXMLEle(ep);
4188                 if (!strcmp(tag, "Job"))
4189                     processJobInfo(ep);
4190                 else if (!strcmp(tag, "Profile"))
4191                 {
4192                     schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep));
4193                 }
4194                 else if (!strcmp(tag, "ErrorHandlingStrategy"))
4195                 {
4196                     setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(cLocale.toInt(findXMLAttValu(ep, "value"))));
4197 
4198                     subEP = findXMLEle(ep, "delay");
4199                     if (subEP)
4200                     {
4201                         errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP)));
4202                     }
4203                     subEP = findXMLEle(ep, "RescheduleErrors");
4204                     errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr);
4205                 }
4206                 else if (!strcmp(tag, "StartupProcedure"))
4207                 {
4208                     XMLEle *procedure;
4209                     startupScript->clear();
4210                     unparkDomeCheck->setChecked(false);
4211                     unparkMountCheck->setChecked(false);
4212                     uncapCheck->setChecked(false);
4213 
4214                     for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
4215                     {
4216                         const char *proc = pcdataXMLEle(procedure);
4217 
4218                         if (!strcmp(proc, "StartupScript"))
4219                         {
4220                             startupScript->setText(findXMLAttValu(procedure, "value"));
4221                             startupScriptURL = QUrl::fromUserInput(startupScript->text());
4222                         }
4223                         else if (!strcmp(proc, "UnparkDome"))
4224                             unparkDomeCheck->setChecked(true);
4225                         else if (!strcmp(proc, "UnparkMount"))
4226                             unparkMountCheck->setChecked(true);
4227                         else if (!strcmp(proc, "UnparkCap"))
4228                             uncapCheck->setChecked(true);
4229                     }
4230                 }
4231                 else if (!strcmp(tag, "ShutdownProcedure"))
4232                 {
4233                     XMLEle *procedure;
4234                     shutdownScript->clear();
4235                     warmCCDCheck->setChecked(false);
4236                     parkDomeCheck->setChecked(false);
4237                     parkMountCheck->setChecked(false);
4238                     capCheck->setChecked(false);
4239 
4240                     for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0))
4241                     {
4242                         const char *proc = pcdataXMLEle(procedure);
4243 
4244                         if (!strcmp(proc, "ShutdownScript"))
4245                         {
4246                             shutdownScript->setText(findXMLAttValu(procedure, "value"));
4247                             shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text());
4248                         }
4249                         else if (!strcmp(proc, "ParkDome"))
4250                             parkDomeCheck->setChecked(true);
4251                         else if (!strcmp(proc, "ParkMount"))
4252                             parkMountCheck->setChecked(true);
4253                         else if (!strcmp(proc, "ParkCap"))
4254                             capCheck->setChecked(true);
4255                         else if (!strcmp(proc, "WarmCCD"))
4256                             warmCCDCheck->setChecked(true);
4257                     }
4258                 }
4259             }
4260             delXMLEle(root);
4261         }
4262         else if (errmsg[0])
4263         {
4264             appendLogText(QString(errmsg));
4265             delLilXML(xmlParser);
4266             state = old_state;
4267             return false;
4268         }
4269     }
4270 
4271     schedulerURL = QUrl::fromLocalFile(fileURL);
4272     mosaicB->setEnabled(true);
4273     mDirty = false;
4274     delLilXML(xmlParser);
4275     // update save button tool tip
4276     queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
4277 
4278 
4279     state = old_state;
4280     return true;
4281 }
4282 
processJobInfo(XMLEle * root)4283 bool Scheduler::processJobInfo(XMLEle *root)
4284 {
4285     XMLEle *ep;
4286     XMLEle *subEP;
4287 
4288     altConstraintCheck->setChecked(false);
4289     moonSeparationCheck->setChecked(false);
4290     weatherCheck->setChecked(false);
4291 
4292     twilightCheck->blockSignals(true);
4293     twilightCheck->setChecked(false);
4294     twilightCheck->blockSignals(false);
4295 
4296     artificialHorizonCheck->blockSignals(true);
4297     artificialHorizonCheck->setChecked(false);
4298     artificialHorizonCheck->blockSignals(false);
4299 
4300     minAltitude->setValue(minAltitude->minimum());
4301     minMoonSeparation->setValue(minMoonSeparation->minimum());
4302     rotationSpin->setValue(0);
4303 
4304     // We expect all data read from the XML to be in the C locale - QLocale::c()
4305     QLocale cLocale = QLocale::c();
4306 
4307     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
4308     {
4309         if (!strcmp(tagXMLEle(ep), "Name"))
4310             nameEdit->setText(pcdataXMLEle(ep));
4311         else if (!strcmp(tagXMLEle(ep), "Priority"))
4312             prioritySpin->setValue(atoi(pcdataXMLEle(ep)));
4313         else if (!strcmp(tagXMLEle(ep), "Coordinates"))
4314         {
4315             subEP = findXMLEle(ep, "J2000RA");
4316             if (subEP)
4317             {
4318                 dms ra;
4319                 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
4320                 raBox->showInHours(ra);
4321             }
4322             subEP = findXMLEle(ep, "J2000DE");
4323             if (subEP)
4324             {
4325                 dms de;
4326                 de.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
4327                 decBox->showInDegrees(de);
4328             }
4329         }
4330         else if (!strcmp(tagXMLEle(ep), "Sequence"))
4331         {
4332             sequenceEdit->setText(pcdataXMLEle(ep));
4333             sequenceURL = QUrl::fromUserInput(sequenceEdit->text());
4334         }
4335         else if (!strcmp(tagXMLEle(ep), "FITS"))
4336         {
4337             fitsEdit->setText(pcdataXMLEle(ep));
4338             fitsURL.setPath(fitsEdit->text());
4339         }
4340         else if (!strcmp(tagXMLEle(ep), "Rotation"))
4341         {
4342             rotationSpin->setValue(cLocale.toDouble(pcdataXMLEle(ep)));
4343         }
4344         else if (!strcmp(tagXMLEle(ep), "StartupCondition"))
4345         {
4346             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4347             {
4348                 if (!strcmp("ASAP", pcdataXMLEle(subEP)))
4349                     asapConditionR->setChecked(true);
4350                 else if (!strcmp("Culmination", pcdataXMLEle(subEP)))
4351                 {
4352                     culminationConditionR->setChecked(true);
4353                     culminationOffset->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value")));
4354                 }
4355                 else if (!strcmp("At", pcdataXMLEle(subEP)))
4356                 {
4357                     startupTimeConditionR->setChecked(true);
4358                     startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate));
4359                 }
4360             }
4361         }
4362         else if (!strcmp(tagXMLEle(ep), "Constraints"))
4363         {
4364             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4365             {
4366                 if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP)))
4367                 {
4368                     altConstraintCheck->setChecked(true);
4369                     minAltitude->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value")));
4370                 }
4371                 else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP)))
4372                 {
4373                     moonSeparationCheck->setChecked(true);
4374                     minMoonSeparation->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value")));
4375                 }
4376                 else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP)))
4377                     weatherCheck->setChecked(true);
4378                 else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP)))
4379                     twilightCheck->setChecked(true);
4380                 else if (!strcmp("EnforceArtificialHorizon", pcdataXMLEle(subEP)))
4381                     artificialHorizonCheck->setChecked(true);
4382             }
4383         }
4384         else if (!strcmp(tagXMLEle(ep), "CompletionCondition"))
4385         {
4386             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
4387             {
4388                 if (!strcmp("Sequence", pcdataXMLEle(subEP)))
4389                     sequenceCompletionR->setChecked(true);
4390                 else if (!strcmp("Repeat", pcdataXMLEle(subEP)))
4391                 {
4392                     repeatCompletionR->setChecked(true);
4393                     repeatsSpin->setValue(cLocale.toInt(findXMLAttValu(subEP, "value")));
4394                 }
4395                 else if (!strcmp("Loop", pcdataXMLEle(subEP)))
4396                     loopCompletionR->setChecked(true);
4397                 else if (!strcmp("At", pcdataXMLEle(subEP)))
4398                 {
4399                     timeCompletionR->setChecked(true);
4400                     completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate));
4401                 }
4402             }
4403         }
4404         else if (!strcmp(tagXMLEle(ep), "Steps"))
4405         {
4406             XMLEle *module;
4407             trackStepCheck->setChecked(false);
4408             focusStepCheck->setChecked(false);
4409             alignStepCheck->setChecked(false);
4410             guideStepCheck->setChecked(false);
4411 
4412             for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0))
4413             {
4414                 const char *proc = pcdataXMLEle(module);
4415 
4416                 if (!strcmp(proc, "Track"))
4417                     trackStepCheck->setChecked(true);
4418                 else if (!strcmp(proc, "Focus"))
4419                     focusStepCheck->setChecked(true);
4420                 else if (!strcmp(proc, "Align"))
4421                     alignStepCheck->setChecked(true);
4422                 else if (!strcmp(proc, "Guide"))
4423                     guideStepCheck->setChecked(true);
4424             }
4425         }
4426     }
4427 
4428     addToQueueB->setEnabled(true);
4429     saveJob();
4430 
4431     return true;
4432 }
4433 
saveAs()4434 void Scheduler::saveAs()
4435 {
4436     schedulerURL.clear();
4437     save();
4438 }
4439 
save()4440 void Scheduler::save()
4441 {
4442     QUrl backupCurrent = schedulerURL;
4443 
4444     if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp"))
4445         schedulerURL.clear();
4446 
4447     // If no changes made, return.
4448     if (mDirty == false && !schedulerURL.isEmpty())
4449         return;
4450 
4451     if (schedulerURL.isEmpty())
4452     {
4453         schedulerURL =
4454             QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Scheduler List"), dirPath,
4455                                         "Ekos Scheduler List (*.esl)");
4456         // if user presses cancel
4457         if (schedulerURL.isEmpty())
4458         {
4459             schedulerURL = backupCurrent;
4460             return;
4461         }
4462 
4463         dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename));
4464 
4465         if (schedulerURL.toLocalFile().contains('.') == 0)
4466             schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl");
4467     }
4468 
4469     if (schedulerURL.isValid())
4470     {
4471         if ((saveScheduler(schedulerURL)) == false)
4472         {
4473             KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save"));
4474             return;
4475         }
4476 
4477         mDirty = false;
4478         // update save button tool tip
4479         queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
4480     }
4481     else
4482     {
4483         QString message = i18n("Invalid URL: %1", schedulerURL.url());
4484         KSNotification::sorry(message, i18n("Invalid URL"));
4485     }
4486 }
4487 
saveScheduler(const QUrl & fileURL)4488 bool Scheduler::saveScheduler(const QUrl &fileURL)
4489 {
4490     QFile file;
4491     file.setFileName(fileURL.toLocalFile());
4492 
4493     if (!file.open(QIODevice::WriteOnly))
4494     {
4495         QString message = i18n("Unable to write to file %1", fileURL.toLocalFile());
4496         KSNotification::sorry(message, i18n("Could Not Open File"));
4497         return false;
4498     }
4499 
4500     QTextStream outstream(&file);
4501 
4502     // We serialize sequence data to XML using the C locale
4503     QLocale cLocale = QLocale::c();
4504 
4505     outstream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << endl;
4506     outstream << "<SchedulerList version='1.4'>" << endl;
4507     // ensure to escape special XML characters
4508     outstream << "<Profile>" << QString(entityXML(strdup(schedulerProfileCombo->currentText().toStdString().c_str()))) <<
4509               "</Profile>" << endl;
4510 
4511     foreach (SchedulerJob *job, jobs)
4512     {
4513         outstream << "<Job>" << endl;
4514 
4515         // ensure to escape special XML characters
4516         outstream << "<Name>" << QString(entityXML(strdup(job->getName().toStdString().c_str()))) << "</Name>" << endl;
4517         outstream << "<Priority>" << job->getPriority() << "</Priority>" << endl;
4518         outstream << "<Coordinates>" << endl;
4519         outstream << "<J2000RA>" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "</J2000RA>" << endl;
4520         outstream << "<J2000DE>" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "</J2000DE>" << endl;
4521         outstream << "</Coordinates>" << endl;
4522 
4523         if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false)
4524             outstream << "<FITS>" << job->getFITSFile().toLocalFile() << "</FITS>" << endl;
4525         else
4526             outstream << "<Rotation>" << job->getRotation() << "</Rotation>" << endl;
4527 
4528         outstream << "<Sequence>" << job->getSequenceFile().toLocalFile() << "</Sequence>" << endl;
4529 
4530         outstream << "<StartupCondition>" << endl;
4531         if (job->getFileStartupCondition() == SchedulerJob::START_ASAP)
4532             outstream << "<Condition>ASAP</Condition>" << endl;
4533         else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION)
4534             outstream << "<Condition value='" << cLocale.toString(job->getCulminationOffset()) << "'>Culmination</Condition>" << endl;
4535         else if (job->getFileStartupCondition() == SchedulerJob::START_AT)
4536             outstream << "<Condition value='" << job->getFileStartupTime().toString(Qt::ISODate) << "'>At</Condition>"
4537                       << endl;
4538         outstream << "</StartupCondition>" << endl;
4539 
4540         outstream << "<Constraints>" << endl;
4541         if (job->hasMinAltitude())
4542             outstream << "<Constraint value='" << cLocale.toString(job->getMinAltitude()) << "'>MinimumAltitude</Constraint>" << endl;
4543         if (job->getMinMoonSeparation() > 0)
4544             outstream << "<Constraint value='" << cLocale.toString(job->getMinMoonSeparation()) << "'>MoonSeparation</Constraint>"
4545                       << endl;
4546         if (job->getEnforceWeather())
4547             outstream << "<Constraint>EnforceWeather</Constraint>" << endl;
4548         if (job->getEnforceTwilight())
4549             outstream << "<Constraint>EnforceTwilight</Constraint>" << endl;
4550         if (job->getEnforceArtificialHorizon())
4551             outstream << "<Constraint>EnforceArtificialHorizon</Constraint>" << endl;
4552         outstream << "</Constraints>" << endl;
4553 
4554         outstream << "<CompletionCondition>" << endl;
4555         if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE)
4556             outstream << "<Condition>Sequence</Condition>" << endl;
4557         else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT)
4558             outstream << "<Condition value='" << cLocale.toString(job->getRepeatsRequired()) << "'>Repeat</Condition>" << endl;
4559         else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP)
4560             outstream << "<Condition>Loop</Condition>" << endl;
4561         else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT)
4562             outstream << "<Condition value='" << job->getCompletionTime().toString(Qt::ISODate) << "'>At</Condition>"
4563                       << endl;
4564         outstream << "</CompletionCondition>" << endl;
4565 
4566         outstream << "<Steps>" << endl;
4567         if (job->getStepPipeline() & SchedulerJob::USE_TRACK)
4568             outstream << "<Step>Track</Step>" << endl;
4569         if (job->getStepPipeline() & SchedulerJob::USE_FOCUS)
4570             outstream << "<Step>Focus</Step>" << endl;
4571         if (job->getStepPipeline() & SchedulerJob::USE_ALIGN)
4572             outstream << "<Step>Align</Step>" << endl;
4573         if (job->getStepPipeline() & SchedulerJob::USE_GUIDE)
4574             outstream << "<Step>Guide</Step>" << endl;
4575         outstream << "</Steps>" << endl;
4576 
4577         outstream << "</Job>" << endl;
4578     }
4579 
4580     outstream << "<ErrorHandlingStrategy value='" << getErrorHandlingStrategy() << "'>" << endl;
4581     if (errorHandlingRescheduleErrorsCB->isChecked())
4582         outstream << "<RescheduleErrors />" << endl;
4583     outstream << "<delay>" << errorHandlingDelaySB->value() << "</delay>" << endl;
4584     outstream << "</ErrorHandlingStrategy>" << endl;
4585 
4586     outstream << "<StartupProcedure>" << endl;
4587     if (startupScript->text().isEmpty() == false)
4588         outstream << "<Procedure value='" << startupScript->text() << "'>StartupScript</Procedure>" << endl;
4589     if (unparkDomeCheck->isChecked())
4590         outstream << "<Procedure>UnparkDome</Procedure>" << endl;
4591     if (unparkMountCheck->isChecked())
4592         outstream << "<Procedure>UnparkMount</Procedure>" << endl;
4593     if (uncapCheck->isChecked())
4594         outstream << "<Procedure>UnparkCap</Procedure>" << endl;
4595     outstream << "</StartupProcedure>" << endl;
4596 
4597     outstream << "<ShutdownProcedure>" << endl;
4598     if (warmCCDCheck->isChecked())
4599         outstream << "<Procedure>WarmCCD</Procedure>" << endl;
4600     if (capCheck->isChecked())
4601         outstream << "<Procedure>ParkCap</Procedure>" << endl;
4602     if (parkMountCheck->isChecked())
4603         outstream << "<Procedure>ParkMount</Procedure>" << endl;
4604     if (parkDomeCheck->isChecked())
4605         outstream << "<Procedure>ParkDome</Procedure>" << endl;
4606     if (shutdownScript->text().isEmpty() == false)
4607         outstream << "<Procedure value='" << shutdownScript->text() << "'>ShutdownScript</Procedure>" << endl;
4608     outstream << "</ShutdownProcedure>" << endl;
4609 
4610     outstream << "</SchedulerList>" << endl;
4611 
4612     appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile()));
4613     file.close();
4614     return true;
4615 }
4616 
startSlew()4617 void Scheduler::startSlew()
4618 {
4619     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting slewing must be valid");
4620 
4621     // If the mount was parked by a pause or the end-user, unpark
4622     if (isMountParked())
4623     {
4624         parkWaitState = PARKWAIT_UNPARK;
4625         return;
4626     }
4627 
4628     if (Options::resetMountModelBeforeJob())
4629     {
4630         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "resetModel");
4631         mountInterface->call(QDBus::AutoDetect, "resetModel");
4632     }
4633 
4634     SkyPoint target = currentJob->getTargetCoords();
4635     QList<QVariant> telescopeSlew;
4636     telescopeSlew.append(target.ra().Hours());
4637     telescopeSlew.append(target.dec().Degrees());
4638 
4639     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s%s,%s\n", __LINE__, "mountInterface:callWithArgs", "slew: ",
4640                target.ra().toHMSString().toLatin1().data(), target.dec().toDMSString().toLatin1().data());
4641     QDBusReply<bool> const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew);
4642     TEST_PRINT(stderr, "  @@@dbus received %s\n", slewModeReply.error().type() == QDBusError::NoError ? "no error" : "error");
4643 
4644     if (slewModeReply.error().type() != QDBusError::NoError)
4645     {
4646         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(
4647                                               currentJob->getName(), QDBusError::errorString(slewModeReply.error().type()));
4648         if (!manageConnectionLoss())
4649             currentJob->setState(SchedulerJob::JOB_ERROR);
4650     }
4651     else
4652     {
4653         currentJob->setStage(SchedulerJob::STAGE_SLEWING);
4654         appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName()));
4655     }
4656 }
4657 
startFocusing()4658 void Scheduler::startFocusing()
4659 {
4660     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting focusing must be valid");
4661 
4662     // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed
4663     // when first focus request is made in capture module
4664     if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE ||
4665             currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING)
4666     {
4667         // Clear the HFR limit value set in the capture module
4668         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "captureInterface", "clearAutoFocusHFR");
4669         captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR");
4670         // Reset Focus frame so that next frame take a full-resolution capture first.
4671         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "focusInterface", "resetFrame");
4672         focusInterface->call(QDBus::AutoDetect, "resetFrame");
4673         currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE);
4674         getNextAction();
4675         return;
4676     }
4677 
4678     // Check if autofocus is supported
4679     QDBusReply<bool> focusModeReply;
4680     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "focusInterface", "canAutoFocus");
4681     focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus");
4682 
4683     if (focusModeReply.error().type() != QDBusError::NoError)
4684     {
4685         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(
4686                                               currentJob->getName(), QDBusError::errorString(focusModeReply.error().type()));
4687         if (!manageConnectionLoss())
4688             currentJob->setState(SchedulerJob::JOB_ERROR);
4689         return;
4690     }
4691 
4692     if (focusModeReply.value() == false)
4693     {
4694         appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName()));
4695         currentJob->setStepPipeline(
4696             static_cast<SchedulerJob::StepPipeline>(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS));
4697         currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE);
4698         getNextAction();
4699         return;
4700     }
4701 
4702     // Clear the HFR limit value set in the capture module
4703     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "captureInterface", "clearAutoFocusHFR");
4704     captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR");
4705 
4706     QDBusMessage reply;
4707 
4708     // We always need to reset frame first
4709     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "focusInterface", "resetFrame");
4710     if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage)
4711     {
4712         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(
4713                                               currentJob->getName(), reply.errorMessage());
4714         if (!manageConnectionLoss())
4715             currentJob->setState(SchedulerJob::JOB_ERROR);
4716         return;
4717     }
4718 
4719     // Set autostar if full field option is false
4720     if (Options::focusUseFullField() == false)
4721     {
4722         QList<QVariant> autoStar;
4723         autoStar.append(true);
4724         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "focusInterface", "setAutoStarEnabled");
4725         if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() ==
4726                 QDBusMessage::ErrorMessage)
4727         {
4728             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(
4729                                                   currentJob->getName(), reply.errorMessage());
4730             if (!manageConnectionLoss())
4731                 currentJob->setState(SchedulerJob::JOB_ERROR);
4732             return;
4733         }
4734     }
4735 
4736     // Start auto-focus
4737     TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "focusInterface", "start");
4738     if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage)
4739     {
4740         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(
4741                                               currentJob->getName(), reply.errorMessage());
4742         if (!manageConnectionLoss())
4743             currentJob->setState(SchedulerJob::JOB_ERROR);
4744         return;
4745     }
4746 
4747     /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE ||
4748         currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING)
4749     {
4750         currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING);
4751         appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName()));
4752     }
4753     else
4754     {
4755         currentJob->setStage(SchedulerJob::STAGE_FOCUSING);
4756         appendLogText(i18n("Focusing %1 ...", currentJob->getName()));
4757     }*/
4758 
4759     currentJob->setStage(SchedulerJob::STAGE_FOCUSING);
4760     appendLogText(i18n("Job '%1' is focusing.", currentJob->getName()));
4761     startCurrentOperationTimer();
4762 }
4763 
findNextJob()4764 void Scheduler::findNextJob()
4765 {
4766     if (state == SCHEDULER_PAUSED)
4767     {
4768         // everything finished, we can pause
4769         setPaused();
4770         return;
4771     }
4772 
4773     Q_ASSERT_X(currentJob->getState() == SchedulerJob::JOB_ERROR ||
4774                currentJob->getState() == SchedulerJob::JOB_ABORTED ||
4775                currentJob->getState() == SchedulerJob::JOB_COMPLETE ||
4776                currentJob->getState() == SchedulerJob::JOB_IDLE,
4777                __FUNCTION__, "Finding next job requires current to be in error, aborted, idle or complete");
4778 
4779     // Reset failed count
4780     alignFailureCount = guideFailureCount = focusFailureCount = captureFailureCount = 0;
4781 
4782     /* FIXME: Other debug logs in that function probably */
4783     qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job...";
4784 
4785     if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED)
4786     {
4787         captureBatch = 0;
4788         // Stop Guiding if it was used
4789         stopGuiding();
4790 
4791         if (currentJob->getState() == SchedulerJob::JOB_ERROR)
4792             appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName()));
4793         else
4794             appendLogText(i18n("Job '%1' is aborted.", currentJob->getName()));
4795 
4796         // Always reset job stage
4797         currentJob->setStage(SchedulerJob::STAGE_IDLE);
4798 
4799         // restart aborted jobs immediately, if error handling strategy is set to "restart immediately"
4800         if (errorHandlingRestartImmediatelyButton->isChecked() &&
4801                 (currentJob->getState() == SchedulerJob::JOB_ABORTED ||
4802                  (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked())))
4803         {
4804             // reset the state so that it will be restarted
4805             currentJob->setState(SchedulerJob::JOB_SCHEDULED);
4806 
4807             appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName()));
4808 
4809             // wait the given delay until the jobs will be evaluated again
4810             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_WAKEUP).toLatin1().data());
4811             setupNextIteration(RUN_WAKEUP, std::lround((errorHandlingDelaySB->value() * 1000) /
4812                                KStarsData::Instance()->clock()->scale()));
4813             sleepLabel->setToolTip(i18n("Scheduler waits for a retry."));
4814             sleepLabel->show();
4815             return;
4816         }
4817 
4818         // otherwise start re-evaluation
4819         setCurrentJob(nullptr);
4820         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
4821         setupNextIteration(RUN_SCHEDULER);
4822     }
4823     else if (currentJob->getState() == SchedulerJob::JOB_IDLE)
4824     {
4825         // job constraints no longer valid, start re-evaluation
4826         setCurrentJob(nullptr);
4827         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
4828         setupNextIteration(RUN_SCHEDULER);
4829     }
4830     // Job is complete, so check completion criteria to optimize processing
4831     // In any case, we're done whether the job completed successfully or not.
4832     else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE)
4833     {
4834         /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */
4835         if (Options::rememberJobProgress())
4836         {
4837             foreach(SchedulerJob *a_job, jobs)
4838                 if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
4839                     a_job->setState(SchedulerJob::JOB_IDLE);
4840         }
4841 
4842         captureBatch = 0;
4843         // Stop Guiding if it was used
4844         stopGuiding();
4845 
4846         appendLogText(i18n("Job '%1' is complete.", currentJob->getName()));
4847 
4848         // Always reset job stage
4849         currentJob->setStage(SchedulerJob::STAGE_IDLE);
4850 
4851         setCurrentJob(nullptr);
4852         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
4853         setupNextIteration(RUN_SCHEDULER);
4854     }
4855     else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT)
4856     {
4857         /* If the job is about to repeat, decrease its repeat count and reset its start time */
4858         if (0 < currentJob->getRepeatsRemaining())
4859         {
4860             currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1);
4861             currentJob->setStartupTime(QDateTime());
4862         }
4863 
4864         /* Mark the job idle as well as all its duplicates for re-evaluation */
4865         foreach(SchedulerJob *a_job, jobs)
4866             if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
4867                 a_job->setState(SchedulerJob::JOB_IDLE);
4868 
4869         /* Re-evaluate all jobs, without selecting a new job */
4870         evaluateJobs(true);
4871 
4872         /* If current job is actually complete because of previous duplicates, prepare for next job */
4873         if (currentJob == nullptr || currentJob->getRepeatsRemaining() == 0)
4874         {
4875             stopCurrentJobAction();
4876 
4877             if (currentJob != nullptr)
4878             {
4879                 appendLogText(i18np("Job '%1' is complete after #%2 batch.",
4880                                     "Job '%1' is complete after #%2 batches.",
4881                                     currentJob->getName(), currentJob->getRepeatsRequired()));
4882                 setCurrentJob(nullptr);
4883             }
4884             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
4885             setupNextIteration(RUN_SCHEDULER);
4886         }
4887         /* If job requires more work, continue current observation */
4888         else
4889         {
4890             /* FIXME: raise priority to allow other jobs to schedule in-between */
4891             executeJob(currentJob);
4892 
4893             /* JM 2020-08-23: If user opts to force realign instead of for each job then we force this FIRST */
4894             if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
4895             {
4896                 stopGuiding();
4897                 currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
4898                 startAstrometry();
4899             }
4900             /* If we are guiding, continue capturing */
4901             else if ( (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) )
4902             {
4903                 currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
4904                 startCapture();
4905             }
4906             /* If we are not guiding, but using alignment, realign */
4907             else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
4908             {
4909                 currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
4910                 startAstrometry();
4911             }
4912             /* Else if we are neither guiding nor using alignment, slew back to target */
4913             else if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK)
4914             {
4915                 currentJob->setStage(SchedulerJob::STAGE_SLEWING);
4916                 startSlew();
4917             }
4918             /* Else just start capturing */
4919             else
4920             {
4921                 currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
4922                 startCapture();
4923             }
4924 
4925             appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
4926                                 "Job '%1' is repeating, #%2 batches remaining.",
4927                                 currentJob->getName(), currentJob->getRepeatsRemaining()));
4928             /* currentJob remains the same */
4929             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_JOBCHECK).toLatin1().data());
4930             setupNextIteration(RUN_JOBCHECK);
4931         }
4932     }
4933     else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP)
4934     {
4935         executeJob(currentJob);
4936 
4937         if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
4938         {
4939             stopGuiding();
4940             currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
4941             startAstrometry();
4942         }
4943         else
4944         {
4945             currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
4946             startCapture();
4947         }
4948 
4949         captureBatch++;
4950 
4951         appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName()));
4952         /* currentJob remains the same */
4953         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_JOBCHECK).toLatin1().data());
4954         setupNextIteration(RUN_JOBCHECK);
4955     }
4956     else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT)
4957     {
4958         if (getLocalTime().secsTo(currentJob->getCompletionTime()) <= 0)
4959         {
4960             /* Mark the job idle as well as all its duplicates for re-evaluation */
4961             foreach(SchedulerJob *a_job, jobs)
4962                 if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
4963                     a_job->setState(SchedulerJob::JOB_IDLE);
4964             stopCurrentJobAction();
4965 
4966             captureBatch = 0;
4967 
4968             appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.",
4969                                 "Job '%1' stopping, reached completion time with #%2 batches done.",
4970                                 currentJob->getName(), captureBatch + 1));
4971 
4972             // Always reset job stage
4973             currentJob->setStage(SchedulerJob::STAGE_IDLE);
4974 
4975             setCurrentJob(nullptr);
4976             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
4977             setupNextIteration(RUN_SCHEDULER);
4978         }
4979         else
4980         {
4981             executeJob(currentJob);
4982 
4983             if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN && Options::forceAlignmentBeforeJob())
4984             {
4985                 stopGuiding();
4986                 currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
4987                 startAstrometry();
4988             }
4989             else
4990             {
4991                 currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
4992                 startCapture();
4993             }
4994 
4995             captureBatch++;
4996 
4997             appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.",
4998                                 "Job '%1' completed #%2 batches before completion time, restarted.",
4999                                 currentJob->getName(), captureBatch));
5000             /* currentJob remains the same */
5001             TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_JOBCHECK).toLatin1().data());
5002             setupNextIteration(RUN_JOBCHECK);
5003         }
5004     }
5005     else
5006     {
5007         /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */
5008         qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << currentJob->getName() << "' timer elapsed, but no action to be taken.";
5009 
5010         // Always reset job stage
5011         currentJob->setStage(SchedulerJob::STAGE_IDLE);
5012 
5013         setCurrentJob(nullptr);
5014         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
5015         setupNextIteration(RUN_SCHEDULER);
5016     }
5017 }
5018 
startAstrometry()5019 void Scheduler::startAstrometry()
5020 {
5021     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting aligning must be valid");
5022 
5023     QDBusMessage reply;
5024     setSolverAction(Align::GOTO_SLEW);
5025 
5026     // Always turn update coords on
5027     //QVariant arg(true);
5028     //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg);
5029 
5030     // If FITS file is specified, then we use load and slew
5031     if (currentJob->getFITSFile().isEmpty() == false)
5032     {
5033         QList<QVariant> solveArgs;
5034         solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile));
5035 
5036         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "alignInterface", "loadAndSlew");
5037         if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() ==
5038                 QDBusMessage::ErrorMessage)
5039         {
5040             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg(
5041                                                   currentJob->getName(), reply.errorMessage());
5042             if (!manageConnectionLoss())
5043                 currentJob->setState(SchedulerJob::JOB_ERROR);
5044             return;
5045         }
5046 
5047         loadAndSlewProgress = true;
5048         appendLogText(i18n("Job '%1' is plate solving %2.", currentJob->getName(), currentJob->getFITSFile().fileName()));
5049     }
5050     else
5051     {
5052         // JM 2020.08.20: Send J2000 TargetCoords to Align module so that we always resort back to the
5053         // target original targets even if we drifted away due to any reason like guiding calibration failures.
5054         const SkyPoint targetCoords = currentJob->getTargetCoords();
5055         QList<QVariant> targetArgs, rotationArgs;
5056         targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
5057         rotationArgs << currentJob->getRotation();
5058 
5059         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "alignInterface", "setTargetCoords");
5060         if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords",
5061                      targetArgs)).type() == QDBusMessage::ErrorMessage)
5062         {
5063             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setTargetCoords request received DBUS error: %2").arg(
5064                                                   currentJob->getName(), reply.errorMessage());
5065             if (!manageConnectionLoss())
5066                 currentJob->setState(SchedulerJob::JOB_ERROR);
5067             return;
5068         }
5069 
5070         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "alignInterface", "setTargetRotation");
5071         if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "setTargetRotation",
5072                      rotationArgs)).type() == QDBusMessage::ErrorMessage)
5073         {
5074             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setTargetRotation request received DBUS error: %2").arg(
5075                                                   currentJob->getName(), reply.errorMessage());
5076             if (!manageConnectionLoss())
5077                 currentJob->setState(SchedulerJob::JOB_ERROR);
5078             return;
5079         }
5080 
5081         TEST_PRINT(stderr, "sch%d @@@dbus(%s): sending %s\n", __LINE__, "alignInterface", "captureAndSolve");
5082         if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage)
5083         {
5084             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(
5085                                                   currentJob->getName(), reply.errorMessage());
5086             if (!manageConnectionLoss())
5087                 currentJob->setState(SchedulerJob::JOB_ERROR);
5088             return;
5089         }
5090 
5091         appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName()));
5092     }
5093 
5094     /* FIXME: not supposed to modify the job */
5095     currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
5096     startCurrentOperationTimer();
5097 }
5098 
startGuiding(bool resetCalibration)5099 void Scheduler::startGuiding(bool resetCalibration)
5100 {
5101     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting guiding must be valid");
5102 
5103     // avoid starting the guider twice
5104     if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING)
5105     {
5106         appendLogText(i18n("Guiding already running for %1 ...", currentJob->getName()));
5107         currentJob->setStage(SchedulerJob::STAGE_GUIDING);
5108         startCurrentOperationTimer();
5109         return;
5110     }
5111 
5112     // Connect Guider
5113     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:call", "connectGuider");
5114     guideInterface->call(QDBus::AutoDetect, "connectGuider");
5115 
5116     // Set Auto Star to true
5117     QVariant arg(true);
5118     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:call", "setCalibrationAutoStar");
5119     guideInterface->call(QDBus::AutoDetect, "setCalibrationAutoStar", arg);
5120 
5121     // Only reset calibration on trouble
5122     // and if we are allowed to reset calibration (true by default)
5123     if (resetCalibration && Options::resetGuideCalibration())
5124     {
5125         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:call", "clearCalibration");
5126         guideInterface->call(QDBus::AutoDetect, "clearCalibration");
5127     }
5128 
5129     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:call", "guide");
5130     guideInterface->call(QDBus::AutoDetect, "guide");
5131 
5132     currentJob->setStage(SchedulerJob::STAGE_GUIDING);
5133 
5134     appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName()));
5135 
5136     startCurrentOperationTimer();
5137 }
5138 
startCapture(bool restart)5139 void Scheduler::startCapture(bool restart)
5140 {
5141     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting capturing must be valid");
5142 
5143     // ensure that guiding is running before we start capturing
5144     if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING)
5145     {
5146         // guiding should run, but it doesn't. So start guiding first
5147         currentJob->setStage(SchedulerJob::STAGE_GUIDING);
5148         startGuiding();
5149         return;
5150     }
5151 
5152     QString sanitized = currentJob->getName();
5153     sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
5154                 // Remove any two or more __
5155                 .replace( QRegularExpression("_{2,}"), "_")
5156                 // Remove any _ at the end
5157                 .replace( QRegularExpression("_$"), "");
5158     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s%s\n", __LINE__, "captureInterface:setProperty", "targetName=",
5159                sanitized.toLatin1().data());
5160     captureInterface->setProperty("targetName", sanitized);
5161 
5162     QString url = currentJob->getSequenceFile().toLocalFile();
5163 
5164     if (restart == false)
5165     {
5166         QList<QVariant> dbusargs;
5167         dbusargs.append(url);
5168         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:callWithArgs", "loadSequenceQueue");
5169         QDBusReply<bool> const captureReply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue",
5170                                               dbusargs);
5171         if (captureReply.error().type() != QDBusError::NoError)
5172         {
5173             qCCritical(KSTARS_EKOS_SCHEDULER) <<
5174                                               QString("Warning: job '%1' loadSequenceQueue request received DBUS error: %1").arg(currentJob->getName()).arg(
5175                                                   captureReply.error().message());
5176             if (!manageConnectionLoss())
5177                 currentJob->setState(SchedulerJob::JOB_ERROR);
5178             return;
5179         }
5180         // Check if loading sequence fails for whatever reason
5181         else if (captureReply.value() == false)
5182         {
5183             qCCritical(KSTARS_EKOS_SCHEDULER) <<
5184                                               QString("Warning: job '%1' loadSequenceQueue request failed").arg(currentJob->getName());
5185             if (!manageConnectionLoss())
5186                 currentJob->setState(SchedulerJob::JOB_ERROR);
5187             return;
5188         }
5189     }
5190 
5191 
5192     switch (currentJob->getCompletionCondition())
5193     {
5194         case SchedulerJob::FINISH_LOOP:
5195         case SchedulerJob::FINISH_AT:
5196             // In these cases, we leave the captured frames map empty
5197             // to ensure, that the capture sequence is executed in any case.
5198             break;
5199 
5200         default:
5201             // Scheduler always sets captured frame map when starting a sequence - count may be different, robustness, dynamic priority
5202 
5203             // hand over the map of captured frames so that the capture
5204             // process knows about existing frames
5205             SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap();
5206 
5207             for (auto &e : fMap.keys())
5208             {
5209                 QList<QVariant> dbusargs;
5210                 QDBusMessage reply;
5211 
5212                 dbusargs.append(e);
5213                 dbusargs.append(fMap.value(e));
5214                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:callWithArgs", "setCapturedFramesMap");
5215                 if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() ==
5216                         QDBusMessage::ErrorMessage)
5217                 {
5218                     qCCritical(KSTARS_EKOS_SCHEDULER) <<
5219                                                       QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg(
5220                                                           reply.errorMessage());
5221                     if (!manageConnectionLoss())
5222                         currentJob->setState(SchedulerJob::JOB_ERROR);
5223                     return;
5224                 }
5225             }
5226             break;
5227     }
5228 
5229     // Start capture process
5230     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:call", "start");
5231     captureInterface->call(QDBus::AutoDetect, "start");
5232 
5233     currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
5234 
5235     KNotification::event(QLatin1String("EkosScheduledImagingStart"),
5236                          i18n("Ekos job (%1) - Capture started", currentJob->getName()));
5237 
5238     if (captureBatch > 0)
5239         appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1));
5240     else
5241         appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName()));
5242 
5243     startCurrentOperationTimer();
5244 }
5245 
stopGuiding()5246 void Scheduler::stopGuiding()
5247 {
5248     if (!guideInterface)
5249         return;
5250 
5251     // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation
5252     if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE))
5253     {
5254         qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName());
5255         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:call", "abort");
5256         guideInterface->call(QDBus::AutoDetect, "abort");
5257         guideFailureCount = 0;
5258     }
5259 
5260     // In any case, stop the automatic guider restart
5261     if (isGuidingTimerActive())
5262         cancelGuidingTimer();
5263 }
5264 
setSolverAction(Align::GotoMode mode)5265 void Scheduler::setSolverAction(Align::GotoMode mode)
5266 {
5267     QVariant gotoMode(static_cast<int>(mode));
5268     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "alignInterface:call", "setSolverAction");
5269     alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode);
5270 }
5271 
disconnectINDI()5272 void Scheduler::disconnectINDI()
5273 {
5274     qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI...";
5275     indiState = INDI_DISCONNECTING;
5276     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "ekosInterface:call", "disconnectDevices");
5277     ekosInterface->call(QDBus::AutoDetect, "disconnectDevices");
5278 }
5279 
stopEkos()5280 void Scheduler::stopEkos()
5281 {
5282     qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos...";
5283     ekosState               = EKOS_STOPPING;
5284     ekosConnectFailureCount = 0;
5285     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "ekosInterface:call", "stop");
5286     ekosInterface->call(QDBus::AutoDetect, "stop");
5287     m_MountReady = m_CapReady = m_CaptureReady = m_DomeReady = false;
5288 }
5289 
setDirty()5290 void Scheduler::setDirty()
5291 {
5292     mDirty = true;
5293 
5294     if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup)
5295         return;
5296 
5297     if (0 <= jobUnderEdit && state != SCHEDULER_RUNNING && 0 <= queueTable->currentRow())
5298     {
5299         // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation
5300         for (int row = jobUnderEdit; row < jobs.size(); row++)
5301             jobs.at(row)->reset();
5302 
5303         saveJob();
5304     }
5305 
5306     // For object selection, all fields must be filled
5307     bool const nameSelectionOK = !raBox->isEmpty()  && !decBox->isEmpty() && !nameEdit->text().isEmpty();
5308 
5309     // For FITS selection, only the name and fits URL should be filled.
5310     bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty();
5311 
5312     // Sequence selection is required
5313     bool const seqSelectionOK = !sequenceEdit->text().isEmpty();
5314 
5315     // Finally, adding is allowed upon object/FITS and sequence selection
5316     bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK;
5317 
5318     addToQueueB->setEnabled(addingOK);
5319     mosaicB->setEnabled(addingOK);
5320 }
5321 
updateCompletedJobsCount(bool forced)5322 void Scheduler::updateCompletedJobsCount(bool forced)
5323 {
5324     /* Use a temporary map in order to limit the number of file searches */
5325     SchedulerJob::CapturedFramesMap newFramesCount;
5326 
5327     /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */
5328 
5329     /* Check if one job is idle or requires evaluation - if so, force refresh */
5330     forced |= std::any_of(jobs.begin(), jobs.end(), [](SchedulerJob * oneJob) -> bool
5331     {
5332         SchedulerJob::JOBStatus const state = oneJob->getState();
5333         return state == SchedulerJob::JOB_IDLE || state == SchedulerJob::JOB_EVALUATION;});
5334 
5335     /* If update is forced, clear the frame map */
5336     if (forced)
5337         m_CapturedFramesCount.clear();
5338 
5339     /* Enumerate SchedulerJobs to count captures that are already stored */
5340     for (SchedulerJob *oneJob : jobs)
5341     {
5342         QList<SequenceJob*> seqjobs;
5343         bool hasAutoFocus = false;
5344 
5345         //oneJob->setLightFramesRequired(false);
5346         /* Look into the sequence requirements, bypass if invalid */
5347         if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus,
5348                               this) == false)
5349         {
5350             appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(),
5351                                oneJob->getSequenceFile().toLocalFile()));
5352             oneJob->setState(SchedulerJob::JOB_INVALID);
5353             continue;
5354         }
5355 
5356         /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */
5357         for (SequenceJob *oneSeqJob : seqjobs)
5358         {
5359             /* Only consider captures stored on client (Ekos) side */
5360             /* FIXME: ask the remote for the file count */
5361             if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
5362                 continue;
5363 
5364             /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */
5365             QString const signature = oneSeqJob->getSignature();
5366 
5367             /* If signature was processed during this run, keep it */
5368             if (newFramesCount.constEnd() != newFramesCount.constFind(signature))
5369                 continue;
5370 
5371             /* If signature was processed during an earlier run, use the earlier count */
5372             QMap<QString, uint16_t>::const_iterator const earlierRunIterator = m_CapturedFramesCount.constFind(signature);
5373             if (m_CapturedFramesCount.constEnd() != earlierRunIterator)
5374             {
5375                 newFramesCount[signature] = earlierRunIterator.value();
5376                 continue;
5377             }
5378 
5379             /* Else recount captures already stored */
5380             newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix());
5381         }
5382 
5383         // determine whether we need to continue capturing, depending on captured frames
5384         bool lightFramesRequired = false;
5385         switch (oneJob->getCompletionCondition())
5386         {
5387             case SchedulerJob::FINISH_SEQUENCE:
5388             case SchedulerJob::FINISH_REPEAT:
5389                 for (SequenceJob *oneSeqJob : seqjobs)
5390                 {
5391                     QString const signature = oneSeqJob->getSignature();
5392                     /* If frame is LIGHT, how hany do we have left? */
5393                     if (oneSeqJob->getFrameType() == FRAME_LIGHT
5394                             && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature])
5395                         lightFramesRequired = true;
5396                 }
5397                 break;
5398             default:
5399                 // in all other cases it does not depend on the number of captured frames
5400                 lightFramesRequired = true;
5401         }
5402 
5403 
5404         oneJob->setLightFramesRequired(lightFramesRequired);
5405     }
5406 
5407     m_CapturedFramesCount = newFramesCount;
5408 
5409     //if (forced)
5410     {
5411         qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:";
5412         QMap<QString, uint16_t>::const_iterator it = m_CapturedFramesCount.constBegin();
5413         for (; it != m_CapturedFramesCount.constEnd(); it++)
5414             qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value();
5415     }
5416 }
5417 
estimateJobTime(SchedulerJob * schedJob,const QMap<QString,uint16_t> & capturedFramesCount,Scheduler * scheduler)5418 bool Scheduler::estimateJobTime(SchedulerJob *schedJob, const QMap<QString, uint16_t> &capturedFramesCount,
5419                                 Scheduler *scheduler)
5420 {
5421     static SchedulerJob *jobWarned = nullptr;
5422 
5423     /* updateCompletedJobsCount(); */
5424 
5425     // Load the sequence job associated with the argument scheduler job.
5426     QList<SequenceJob *> seqJobs;
5427     bool hasAutoFocus = false;
5428     if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus,
5429                           scheduler) == false)
5430     {
5431         qCWarning(KSTARS_EKOS_SCHEDULER) <<
5432                                          QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(
5433                                              schedJob->getSequenceFile().toLocalFile());
5434         return false;
5435     }
5436 
5437     // FIXME: setting in-sequence focus should be done in XML processing.
5438     schedJob->setInSequenceFocus(hasAutoFocus);
5439 
5440     // Stop spam of log on re-evaluation. If we display the warning once, then that's it.
5441     if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS))
5442     {
5443         if (scheduler != nullptr) scheduler->appendLogText(
5444                 i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.",
5445                      schedJob->getName()));
5446         jobWarned = schedJob;
5447     }
5448 
5449     /* This is the map of captured frames for this scheduler job, keyed per storage signature.
5450      * It will be forwarded to the Capture module in order to capture only what frames are required.
5451      * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally.
5452      */
5453     SchedulerJob::CapturedFramesMap capture_map;
5454     bool const rememberJobProgress = Options::rememberJobProgress();
5455 
5456     int totalCompletedCount = 0;
5457     double totalImagingTime  = 0;
5458 
5459     // Determine number of captures in the scheduler job
5460     int capturesPerRepeat = 0;
5461     QMap<QString, uint16_t> expected;
5462     foreach (SequenceJob *seqJob, seqJobs)
5463     {
5464         capturesPerRepeat += seqJob->getCount();
5465         QString const signature = seqJob->getSignature();
5466         expected[signature] = seqJob->getCount() + (expected.contains(signature) ? expected[signature] : 0);
5467     }
5468 
5469     // fill the captured frames map
5470     for (QString key : expected.keys())
5471     {
5472         if (rememberJobProgress)
5473         {
5474             int diff = expected[key] * schedJob->getRepeatsRequired() - capturedFramesCount[key];
5475             // captured more than required?
5476             if (diff <= 0)
5477                 capture_map[key] = expected[key];
5478             // need more frames than one cycle could capture?
5479             else if (diff >= expected[key])
5480                 capture_map[key] = 0;
5481             // else we know that 0 < diff < expected[key]
5482             else
5483                 capture_map[key] = expected[key] - diff;
5484         }
5485         else
5486             capture_map[key] = 0;
5487 
5488         // collect all captured frames counts
5489         if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP)
5490             totalCompletedCount += capturedFramesCount[key];
5491         else
5492             totalCompletedCount += std::min(capturedFramesCount[key],
5493                                             static_cast<uint16_t>(expected[key] * schedJob->getRepeatsRequired()));
5494     }
5495 
5496     // Loop through sequence jobs to calculate the number of required frames and estimate duration.
5497     foreach (SequenceJob *seqJob, seqJobs)
5498     {
5499         // FIXME: find a way to actually display the filter name.
5500         QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(),
5501                                seqJob->getFilterName());
5502 
5503         if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL)
5504         {
5505             qCInfo(KSTARS_EKOS_SCHEDULER) <<
5506                                           QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName);
5507             schedJob->setEstimatedTime(-2);
5508             qDeleteAll(seqJobs);
5509             return true;
5510         }
5511 
5512         // Note that looping jobs will have zero repeats required.
5513         QString const signature      = seqJob->getSignature();
5514         QString const signature_path = QFileInfo(signature).path();
5515         int captures_required        = seqJob->getCount() * schedJob->getRepeatsRequired();
5516         int captures_completed       = capturedFramesCount[signature];
5517 
5518         if (rememberJobProgress && schedJob->getCompletionCondition() != SchedulerJob::FINISH_LOOP)
5519         {
5520             /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count.
5521              *
5522              * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage.
5523              *
5524              * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location.
5525              * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job.
5526              *
5527              * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus
5528              * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are
5529              * missing, first because that's not how the sequence job is designed (completed count, not required count), and second
5530              * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence
5531              * jobs three times.
5532              *
5533              * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them.
5534              *
5535              * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages.
5536              * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0.
5537              * When that scheduler job executes, only B and L captures will be processed.
5538              *
5539              * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage.
5540              * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0.
5541              * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed.
5542              *
5543              * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets.
5544              * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage.
5545              * This is why it is important to manage the repeat count of the scheduler job, as stated earlier.
5546              */
5547 
5548             // we start with the total value
5549             captures_required = expected[seqJob->getSignature()] * schedJob->getRepeatsRequired();
5550 
5551             qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(
5552                                               captures_completed).arg(QFileInfo(signature).path());
5553 
5554             // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one
5555             foreach (SequenceJob *prevSeqJob, seqJobs)
5556             {
5557                 // Enumerate seqJobs up to the current one
5558                 if (seqJob == prevSeqJob)
5559                     break;
5560 
5561                 // If the previous sequence signature matches the current, skip counting to take duplicates into account
5562                 if (!signature.compare(prevSeqJob->getSignature()))
5563                     captures_required = 0;
5564 
5565                 // And break if no captures remain, this job does not need to be executed
5566                 if (captures_required == 0)
5567                     break;
5568             }
5569 
5570             qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(
5571                                                seqName).arg(captures_completed).arg(captures_required).arg(signature_path);
5572 
5573         }
5574         // Else rely on the captures done during this session
5575         else if (0 < capturesPerRepeat)
5576         {
5577             captures_completed = schedJob->getCompletedCount() / capturesPerRepeat * seqJob->getCount();
5578         }
5579         else
5580         {
5581             captures_completed = 0;
5582         }
5583 
5584         // Check if we still need any light frames. Because light frames changes the flow of the observatory startup
5585         // Without light frames, there is no need to do focusing, alignment, guiding...etc
5586         // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required
5587         // OR if the completion condition is set to LOOP so it is never complete due to looping.
5588         // Note that looping jobs will have zero repeats required.
5589         // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job.
5590         bool const areJobCapturesComplete = (0 == captures_required || captures_completed >= captures_required);
5591         if (seqJob->getFrameType() == FRAME_LIGHT)
5592         {
5593             if(areJobCapturesComplete)
5594             {
5595                 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(
5596                                                   captures_required);
5597             }
5598         }
5599         else
5600         {
5601             qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName);
5602         }
5603 
5604         /* If captures are not complete, we have imaging time left */
5605         if (!areJobCapturesComplete)
5606         {
5607             unsigned int const captures_to_go = captures_required - captures_completed;
5608             totalImagingTime += fabs((seqJob->getExposure() + seqJob->getDelay()) * captures_to_go);
5609 
5610             /* If we have light frames to process, add focus/dithering delay */
5611             if (seqJob->getFrameType() == FRAME_LIGHT)
5612             {
5613                 // If inSequenceFocus is true
5614                 if (hasAutoFocus)
5615                 {
5616                     // 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.
5617                     // FIXME: estimating one focus per capture is probably not realistic.
5618                     qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName);
5619                     totalImagingTime += captures_to_go * 30;
5620                 }
5621                 // If we're dithering after each exposure, that's another 10-20 seconds
5622                 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled())
5623                 {
5624                     qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName);
5625                     totalImagingTime += (captures_to_go * 15) / Options::ditherFrames();
5626                 }
5627             }
5628         }
5629     }
5630 
5631     schedJob->setCapturedFramesMap(capture_map);
5632     schedJob->setSequenceCount(capturesPerRepeat * schedJob->getRepeatsRequired());
5633 
5634     // only in case we remember the job progress, we change the completion count
5635     if (rememberJobProgress)
5636         schedJob->setCompletedCount(totalCompletedCount);
5637 
5638     qDeleteAll(seqJobs);
5639 
5640     // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations!
5641 
5642     // We can't estimate times that do not finish when sequence is done
5643     if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP)
5644     {
5645         // We can't know estimated time if it is looping indefinitely
5646         schedJob->setEstimatedTime(-2);
5647 
5648         qCDebug(KSTARS_EKOS_SCHEDULER) <<
5649                                        QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.")
5650                                        .arg(schedJob->getName());
5651     }
5652     // If we know startup and finish times, we can estimate time right away
5653     else if (schedJob->getStartupCondition() == SchedulerJob::START_AT &&
5654              schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT)
5655     {
5656         // FIXME: SchedulerJob is probably doing this already
5657         qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime());
5658         schedJob->setEstimatedTime(diff);
5659 
5660         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.")
5661                                        .arg(schedJob->getName())
5662                                        .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
5663     }
5664     // If we know finish time only, we can roughly estimate the time considering the job starts now
5665     else if (schedJob->getStartupCondition() != SchedulerJob::START_AT &&
5666              schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT)
5667     {
5668         qint64 const diff = getLocalTime().secsTo(schedJob->getCompletionTime());
5669         schedJob->setEstimatedTime(diff);
5670 
5671         qCDebug(KSTARS_EKOS_SCHEDULER) <<
5672                                        QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.")
5673                                        .arg(schedJob->getName())
5674                                        .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
5675     }
5676     // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null
5677     else if (totalImagingTime <= 0)
5678     {
5679         schedJob->setEstimatedTime(0);
5680 
5681         qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.")
5682                                        .arg(schedJob->getName()).arg(schedJob->getCompletedCount()).arg(schedJob->getSequenceCount());
5683     }
5684     // Else consolidate with step durations
5685     else
5686     {
5687         if (schedJob->getLightFramesRequired())
5688         {
5689             totalImagingTime += timeHeuristics(schedJob);
5690         }
5691         dms const estimatedTime(totalImagingTime * 15.0 / 3600.0);
5692         schedJob->setEstimatedTime(std::ceil(totalImagingTime));
5693 
5694         qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(),
5695                                       estimatedTime.toHMSString());
5696     }
5697 
5698     return true;
5699 }
5700 
timeHeuristics(const SchedulerJob * schedJob)5701 int Scheduler::timeHeuristics(const SchedulerJob *schedJob)
5702 {
5703     double imagingTime = 0;
5704     /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */
5705     // Are we doing tracking? It takes about 30 seconds
5706     if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK)
5707         imagingTime += 30;
5708     // Are we doing initial focusing? That can take about 2 minutes
5709     if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)
5710         imagingTime += 120;
5711     // Are we doing astrometry? That can take about 60 seconds
5712     if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
5713     {
5714         imagingTime += 60;
5715     }
5716     // Are we doing guiding?
5717     if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
5718     {
5719         // Looping, finding guide star, settling takes 15 sec
5720         imagingTime += 15;
5721 
5722         // Add guiding settle time from dither setting (used by phd2::guide())
5723         imagingTime += Options::ditherSettle();
5724         // Add guiding settle time from ekos sccheduler setting
5725         imagingTime += Options::guidingSettle();
5726 
5727         // If calibration always cleared
5728         // then calibration process can take about 2 mins
5729         if(Options::resetGuideCalibration())
5730             imagingTime += 120;
5731     }
5732     return imagingTime;
5733 }
5734 
parkMount()5735 void Scheduler::parkMount()
5736 {
5737     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "parkStatus");
5738     QVariant parkingStatus = mountInterface->property("parkStatus");
5739     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
5740 
5741     if (parkingStatus.isValid() == false)
5742     {
5743         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
5744                                               mountInterface->lastError().type());
5745         if (!manageConnectionLoss())
5746             parkWaitState = PARKWAIT_ERROR;
5747     }
5748 
5749     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
5750 
5751     switch (status)
5752     {
5753         case ISD::PARK_PARKED:
5754             if (shutdownState == SHUTDOWN_PARK_MOUNT)
5755                 shutdownState = SHUTDOWN_PARK_DOME;
5756 
5757             parkWaitState = PARKWAIT_PARKED;
5758             appendLogText(i18n("Mount already parked."));
5759             break;
5760 
5761         case ISD::PARK_UNPARKING:
5762         //case Mount::UNPARKING_BUSY:
5763         /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */
5764 
5765         //        case Mount::PARKING_IDLE:
5766         //        case Mount::UNPARKING_OK:
5767         case ISD::PARK_ERROR:
5768         case ISD::PARK_UNKNOWN:
5769         case ISD::PARK_UNPARKED:
5770         {
5771             TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "park");
5772             QDBusReply<bool> const mountReply = mountInterface->call(QDBus::AutoDetect, "park");
5773 
5774             if (mountReply.error().type() != QDBusError::NoError)
5775             {
5776                 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(
5777                                                       QDBusError::errorString(mountReply.error().type()));
5778                 if (!manageConnectionLoss())
5779                     parkWaitState = PARKWAIT_ERROR;
5780             }
5781             else startCurrentOperationTimer();
5782         }
5783 
5784         // Fall through
5785         case ISD::PARK_PARKING:
5786             //case Mount::PARKING_BUSY:
5787             if (shutdownState == SHUTDOWN_PARK_MOUNT)
5788                 shutdownState = SHUTDOWN_PARKING_MOUNT;
5789 
5790             parkWaitState = PARKWAIT_PARKING;
5791             appendLogText(i18n("Parking mount in progress..."));
5792             break;
5793 
5794             // All cases covered above so no need for default
5795             //default:
5796             //    qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value());
5797     }
5798 }
5799 
unParkMount()5800 void Scheduler::unParkMount()
5801 {
5802     if (mountInterface.isNull())
5803         return;
5804 
5805     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "parkStatus");
5806     QVariant parkingStatus = mountInterface->property("parkStatus");
5807     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
5808 
5809     if (parkingStatus.isValid() == false)
5810     {
5811         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
5812                                               mountInterface->lastError().type());
5813         if (!manageConnectionLoss())
5814             parkWaitState = PARKWAIT_ERROR;
5815     }
5816 
5817     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
5818 
5819     switch (status)
5820     {
5821         //case Mount::UNPARKING_OK:
5822         case ISD::PARK_UNPARKED:
5823             if (startupState == STARTUP_UNPARK_MOUNT)
5824                 startupState = STARTUP_UNPARK_CAP;
5825 
5826             parkWaitState = PARKWAIT_UNPARKED;
5827             appendLogText(i18n("Mount already unparked."));
5828             break;
5829 
5830         //case Mount::PARKING_BUSY:
5831         case ISD::PARK_PARKING:
5832         /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */
5833 
5834         //        case Mount::PARKING_IDLE:
5835         //        case Mount::PARKING_OK:
5836         //        case Mount::PARKING_ERROR:
5837         case ISD::PARK_ERROR:
5838         case ISD::PARK_UNKNOWN:
5839         case ISD::PARK_PARKED:
5840         {
5841             TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "unpark");
5842             QDBusReply<bool> const mountReply = mountInterface->call(QDBus::AutoDetect, "unpark");
5843 
5844             if (mountReply.error().type() != QDBusError::NoError)
5845             {
5846                 qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(
5847                                                       QDBusError::errorString(mountReply.error().type()));
5848                 if (!manageConnectionLoss())
5849                     parkWaitState = PARKWAIT_ERROR;
5850             }
5851             else startCurrentOperationTimer();
5852         }
5853 
5854         // Fall through
5855         //case Mount::UNPARKING_BUSY:
5856         case ISD::PARK_UNPARKING:
5857             if (startupState == STARTUP_UNPARK_MOUNT)
5858                 startupState = STARTUP_UNPARKING_MOUNT;
5859 
5860             parkWaitState = PARKWAIT_UNPARKING;
5861             qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
5862             break;
5863 
5864             // All cases covered above
5865             //default:
5866             //    qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value());
5867     }
5868 }
5869 
checkMountParkingStatus()5870 void Scheduler::checkMountParkingStatus()
5871 {
5872     if (mountInterface.isNull())
5873         return;
5874 
5875     static int parkingFailureCount = 0;
5876 
5877     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "parkStatus");
5878     QVariant parkingStatus = mountInterface->property("parkStatus");
5879     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
5880 
5881     if (parkingStatus.isValid() == false)
5882     {
5883         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(
5884                                               mountInterface->lastError().type());
5885         if (!manageConnectionLoss())
5886             parkWaitState = PARKWAIT_ERROR;
5887     }
5888 
5889     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
5890 
5891     switch (status)
5892     {
5893         //case Mount::PARKING_OK:
5894         case ISD::PARK_PARKED:
5895             // If we are starting up, we will unpark the mount in checkParkWaitState soon
5896             // If we are shutting down and mount is parked, proceed to next step
5897             if (shutdownState == SHUTDOWN_PARKING_MOUNT)
5898                 shutdownState = SHUTDOWN_PARK_DOME;
5899 
5900             // Update parking engine state
5901             if (parkWaitState == PARKWAIT_PARKING)
5902                 parkWaitState = PARKWAIT_PARKED;
5903 
5904             appendLogText(i18n("Mount parked."));
5905             parkingFailureCount = 0;
5906             break;
5907 
5908         //case Mount::UNPARKING_OK:
5909         case ISD::PARK_UNPARKED:
5910             // If we are starting up and mount is unparked, proceed to next step
5911             // If we are shutting down, we will park the mount in checkParkWaitState soon
5912             if (startupState == STARTUP_UNPARKING_MOUNT)
5913                 startupState = STARTUP_UNPARK_CAP;
5914 
5915             // Update parking engine state
5916             if (parkWaitState == PARKWAIT_UNPARKING)
5917                 parkWaitState = PARKWAIT_UNPARKED;
5918 
5919             appendLogText(i18n("Mount unparked."));
5920             parkingFailureCount = 0;
5921             break;
5922 
5923         // FIXME: Create an option for the parking/unparking timeout.
5924 
5925         //case Mount::UNPARKING_BUSY:
5926         case ISD::PARK_UNPARKING:
5927             if (getCurrentOperationMsec() > (60 * 1000))
5928             {
5929                 if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS)
5930                 {
5931                     appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...",
5932                                        parkingFailureCount, MAX_FAILURE_ATTEMPTS));
5933                     unParkMount();
5934                 }
5935                 else
5936                 {
5937                     appendLogText(i18n("Warning: mount unpark operation timed out on last attempt."));
5938                     parkWaitState = PARKWAIT_ERROR;
5939                 }
5940             }
5941             else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress...";
5942 
5943             break;
5944 
5945         //case Mount::PARKING_BUSY:
5946         case ISD::PARK_PARKING:
5947             if (getCurrentOperationMsec() > (60 * 1000))
5948             {
5949                 if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS)
5950                 {
5951                     appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount,
5952                                        MAX_FAILURE_ATTEMPTS));
5953                     parkMount();
5954                 }
5955                 else
5956                 {
5957                     appendLogText(i18n("Warning: mount park operation timed out on last attempt."));
5958                     parkWaitState = PARKWAIT_ERROR;
5959                 }
5960             }
5961             else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress...";
5962 
5963             break;
5964 
5965         //case Mount::PARKING_ERROR:
5966         case ISD::PARK_ERROR:
5967             if (startupState == STARTUP_UNPARKING_MOUNT)
5968             {
5969                 appendLogText(i18n("Mount unparking error."));
5970                 startupState = STARTUP_ERROR;
5971                 parkingFailureCount = 0;
5972             }
5973             else if (shutdownState == SHUTDOWN_PARKING_MOUNT)
5974             {
5975                 if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS)
5976                 {
5977                     appendLogText(i18n("Warning: mount park operation failed on attempt %1/%2. Restarting operation...", parkingFailureCount,
5978                                        MAX_FAILURE_ATTEMPTS));
5979                     parkMount();
5980                 }
5981                 else
5982                 {
5983                     appendLogText(i18n("Mount parking error."));
5984                     shutdownState = SHUTDOWN_ERROR;
5985                     parkingFailureCount = 0;
5986                 }
5987 
5988             }
5989             else if (parkWaitState == PARKWAIT_PARKING)
5990             {
5991                 appendLogText(i18n("Mount parking error."));
5992                 parkWaitState = PARKWAIT_ERROR;
5993                 parkingFailureCount = 0;
5994             }
5995             else if (parkWaitState == PARKWAIT_UNPARKING)
5996             {
5997                 appendLogText(i18n("Mount unparking error."));
5998                 parkWaitState = PARKWAIT_ERROR;
5999                 parkingFailureCount = 0;
6000             }
6001             break;
6002 
6003         //case Mount::PARKING_IDLE:
6004         // FIXME Does this work as intended? check!
6005         case ISD::PARK_UNKNOWN:
6006             // Last parking action did not result in an action, so proceed to next step
6007             if (shutdownState == SHUTDOWN_PARKING_MOUNT)
6008                 shutdownState = SHUTDOWN_PARK_DOME;
6009 
6010             // Last unparking action did not result in an action, so proceed to next step
6011             if (startupState == STARTUP_UNPARKING_MOUNT)
6012                 startupState = STARTUP_UNPARK_CAP;
6013 
6014             // Update parking engine state
6015             if (parkWaitState == PARKWAIT_PARKING)
6016                 parkWaitState = PARKWAIT_PARKED;
6017             else if (parkWaitState == PARKWAIT_UNPARKING)
6018                 parkWaitState = PARKWAIT_UNPARKED;
6019 
6020             parkingFailureCount = 0;
6021             break;
6022 
6023             // All cases covered above
6024             //default:
6025             //    qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value());
6026     }
6027 }
6028 
isMountParked()6029 bool Scheduler::isMountParked()
6030 {
6031     if (mountInterface.isNull())
6032         return false;
6033     // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear
6034     //QDBusReply<bool> const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark");
6035     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "canPark");
6036     QVariant canPark = mountInterface->property("canPark");
6037     TEST_PRINT(stderr, "  @@@dbus received %s\n", !canPark.isValid() ? "invalid" : (canPark.toBool() ? "T" : "F"));
6038 
6039     if (canPark.isValid() == false)
6040     {
6041         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(
6042                                               mountInterface->lastError().type());
6043         manageConnectionLoss();
6044         return false;
6045     }
6046     else if (canPark.toBool() == true)
6047     {
6048         // If it is able to park, obtain its current status
6049         //QDBusReply<int> const mountReply  = mountInterface->call(QDBus::AutoDetect, "getParkingStatus");
6050         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "parkStatus");
6051         QVariant parkingStatus = mountInterface->property("parkStatus");
6052         TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6053 
6054         if (parkingStatus.isValid() == false)
6055         {
6056             qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(
6057                                                   mountInterface->lastError().type());
6058             manageConnectionLoss();
6059             return false;
6060         }
6061 
6062         // Deduce state of mount - see getParkingStatus in mount.cpp
6063         switch (static_cast<ISD::ParkStatus>(parkingStatus.toInt()))
6064         {
6065             //            case Mount::PARKING_OK:     // INDI switch ok, and parked
6066             //            case Mount::PARKING_IDLE:   // INDI switch idle, and parked
6067             case ISD::PARK_PARKED:
6068                 return true;
6069 
6070             //            case Mount::UNPARKING_OK:   // INDI switch idle or ok, and unparked
6071             //            case Mount::PARKING_ERROR:  // INDI switch error
6072             //            case Mount::PARKING_BUSY:   // INDI switch busy
6073             //            case Mount::UNPARKING_BUSY: // INDI switch busy
6074             default:
6075                 return false;
6076         }
6077     }
6078     // If the mount is not able to park, consider it not parked
6079     return false;
6080 }
6081 
parkDome()6082 void Scheduler::parkDome()
6083 {
6084     if (domeInterface.isNull())
6085         return;
6086 
6087     //QDBusReply<int> const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus");
6088     //Dome::ParkingStatus status = static_cast<Dome::ParkingStatus>(domeReply.value());
6089     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "parkStatus");
6090     QVariant parkingStatus = domeInterface->property("parkStatus");
6091     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6092 
6093     if (parkingStatus.isValid() == false)
6094     {
6095         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
6096                                               mountInterface->lastError().type());
6097         if (!manageConnectionLoss())
6098             parkingStatus = ISD::PARK_ERROR;
6099     }
6100 
6101     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6102     if (status != ISD::PARK_PARKED)
6103     {
6104         shutdownState = SHUTDOWN_PARKING_DOME;
6105         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:call", "park");
6106         domeInterface->call(QDBus::AutoDetect, "park");
6107         appendLogText(i18n("Parking dome..."));
6108 
6109         startCurrentOperationTimer();
6110     }
6111     else
6112     {
6113         appendLogText(i18n("Dome already parked."));
6114         shutdownState = SHUTDOWN_SCRIPT;
6115     }
6116 }
6117 
unParkDome()6118 void Scheduler::unParkDome()
6119 {
6120     if (domeInterface.isNull())
6121         return;
6122 
6123     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "parkStatus");
6124     QVariant parkingStatus = domeInterface->property("parkStatus");
6125     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6126 
6127     if (parkingStatus.isValid() == false)
6128     {
6129         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
6130                                               mountInterface->lastError().type());
6131         if (!manageConnectionLoss())
6132             parkingStatus = ISD::PARK_ERROR;
6133     }
6134 
6135     if (static_cast<ISD::ParkStatus>(parkingStatus.toInt()) != ISD::PARK_UNPARKED)
6136     {
6137         startupState = STARTUP_UNPARKING_DOME;
6138         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:call", "unpark");
6139         domeInterface->call(QDBus::AutoDetect, "unpark");
6140         appendLogText(i18n("Unparking dome..."));
6141 
6142         startCurrentOperationTimer();
6143     }
6144     else
6145     {
6146         appendLogText(i18n("Dome already unparked."));
6147         startupState = STARTUP_UNPARK_MOUNT;
6148     }
6149 }
6150 
checkDomeParkingStatus()6151 void Scheduler::checkDomeParkingStatus()
6152 {
6153     if (domeInterface.isNull())
6154         return;
6155 
6156     /* FIXME: move this elsewhere */
6157     static int parkingFailureCount = 0;
6158 
6159     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "parkStatus");
6160     QVariant parkingStatus = domeInterface->property("parkStatus");
6161     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6162 
6163     if (parkingStatus.isValid() == false)
6164     {
6165         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
6166                                               mountInterface->lastError().type());
6167         if (!manageConnectionLoss())
6168             parkWaitState = PARKWAIT_ERROR;
6169     }
6170 
6171     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6172 
6173     switch (status)
6174     {
6175         case ISD::PARK_PARKED:
6176             if (shutdownState == SHUTDOWN_PARKING_DOME)
6177             {
6178                 appendLogText(i18n("Dome parked."));
6179 
6180                 shutdownState = SHUTDOWN_SCRIPT;
6181             }
6182             parkingFailureCount = 0;
6183             break;
6184 
6185         case ISD::PARK_UNPARKED:
6186             if (startupState == STARTUP_UNPARKING_DOME)
6187             {
6188                 startupState = STARTUP_UNPARK_MOUNT;
6189                 appendLogText(i18n("Dome unparked."));
6190             }
6191             parkingFailureCount = 0;
6192             break;
6193 
6194         case ISD::PARK_PARKING:
6195         case ISD::PARK_UNPARKING:
6196             // TODO make the timeouts configurable by the user
6197             if (getCurrentOperationMsec() > (120 * 1000))
6198             {
6199                 if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS)
6200                 {
6201                     appendLogText(i18n("Operation timeout. Restarting operation..."));
6202                     if (status == ISD::PARK_PARKING)
6203                         parkDome();
6204                     else
6205                         unParkDome();
6206                     break;
6207                 }
6208             }
6209             break;
6210 
6211         case ISD::PARK_ERROR:
6212             if (shutdownState == SHUTDOWN_PARKING_DOME)
6213             {
6214                 if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS)
6215                 {
6216                     appendLogText(i18n("Dome parking failed. Restarting operation..."));
6217                     parkDome();
6218                 }
6219                 else
6220                 {
6221                     appendLogText(i18n("Dome parking error."));
6222                     shutdownState = SHUTDOWN_ERROR;
6223                     parkingFailureCount = 0;
6224                 }
6225             }
6226             else if (startupState == STARTUP_UNPARKING_DOME)
6227             {
6228                 if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS)
6229                 {
6230                     appendLogText(i18n("Dome unparking failed. Restarting operation..."));
6231                     unParkDome();
6232                 }
6233                 else
6234                 {
6235                     appendLogText(i18n("Dome unparking error."));
6236                     startupState = STARTUP_ERROR;
6237                     parkingFailureCount = 0;
6238                 }
6239             }
6240             break;
6241 
6242         default:
6243             break;
6244     }
6245 }
6246 
isDomeParked()6247 bool Scheduler::isDomeParked()
6248 {
6249     if (domeInterface.isNull())
6250         return false;
6251 
6252     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "parkStatus");
6253     QVariant parkingStatus = domeInterface->property("parkStatus");
6254     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6255 
6256     if (parkingStatus.isValid() == false)
6257     {
6258         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(
6259                                               mountInterface->lastError().type());
6260         if (!manageConnectionLoss())
6261             parkingStatus = ISD::PARK_ERROR;
6262     }
6263 
6264     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6265 
6266     return status == ISD::PARK_PARKED;
6267 }
6268 
parkCap()6269 void Scheduler::parkCap()
6270 {
6271     if (capInterface.isNull())
6272         return;
6273 
6274     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:property", "parkStatus");
6275     QVariant parkingStatus = capInterface->property("parkStatus");
6276     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6277 
6278     if (parkingStatus.isValid() == false)
6279     {
6280         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
6281                                               mountInterface->lastError().type());
6282         if (!manageConnectionLoss())
6283             parkingStatus = ISD::PARK_ERROR;
6284     }
6285 
6286     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6287 
6288     if (status != ISD::PARK_PARKED)
6289     {
6290         shutdownState = SHUTDOWN_PARKING_CAP;
6291         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "distCapInterface:call", "park");
6292         capInterface->call(QDBus::AutoDetect, "park");
6293         appendLogText(i18n("Parking Cap..."));
6294 
6295         startCurrentOperationTimer();
6296     }
6297     else
6298     {
6299         appendLogText(i18n("Cap already parked."));
6300         shutdownState = SHUTDOWN_PARK_MOUNT;
6301     }
6302 }
6303 
unParkCap()6304 void Scheduler::unParkCap()
6305 {
6306     if (capInterface.isNull())
6307         return;
6308 
6309     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:property", "parkStatus");
6310     QVariant parkingStatus = capInterface->property("parkStatus");
6311     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6312 
6313     if (parkingStatus.isValid() == false)
6314     {
6315         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
6316                                               mountInterface->lastError().type());
6317         if (!manageConnectionLoss())
6318             parkingStatus = ISD::PARK_ERROR;
6319     }
6320 
6321     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6322 
6323     if (status != ISD::PARK_UNPARKED)
6324     {
6325         startupState = STARTUP_UNPARKING_CAP;
6326         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:call", "unpark");
6327         capInterface->call(QDBus::AutoDetect, "unpark");
6328         appendLogText(i18n("Unparking cap..."));
6329 
6330         startCurrentOperationTimer();
6331     }
6332     else
6333     {
6334         appendLogText(i18n("Cap already unparked."));
6335         startupState = STARTUP_COMPLETE;
6336     }
6337 }
6338 
checkCapParkingStatus()6339 void Scheduler::checkCapParkingStatus()
6340 {
6341     if (capInterface.isNull())
6342         return;
6343 
6344     /* FIXME: move this elsewhere */
6345     static int parkingFailureCount = 0;
6346 
6347     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:property", "parkStatus");
6348     QVariant parkingStatus = capInterface->property("parkStatus");
6349     TEST_PRINT(stderr, "  @@@dbus received %d\n", !parkingStatus.isValid() ? -1 : parkingStatus.toInt());
6350 
6351     if (parkingStatus.isValid() == false)
6352     {
6353         qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(
6354                                               mountInterface->lastError().type());
6355         if (!manageConnectionLoss())
6356             parkingStatus = ISD::PARK_ERROR;
6357     }
6358 
6359     ISD::ParkStatus status = static_cast<ISD::ParkStatus>(parkingStatus.toInt());
6360 
6361     switch (status)
6362     {
6363         case ISD::PARK_PARKED:
6364             if (shutdownState == SHUTDOWN_PARKING_CAP)
6365             {
6366                 appendLogText(i18n("Cap parked."));
6367                 shutdownState = SHUTDOWN_PARK_MOUNT;
6368             }
6369             parkingFailureCount = 0;
6370             break;
6371 
6372         case ISD::PARK_UNPARKED:
6373             if (startupState == STARTUP_UNPARKING_CAP)
6374             {
6375                 startupState = STARTUP_COMPLETE;
6376                 appendLogText(i18n("Cap unparked."));
6377             }
6378             parkingFailureCount = 0;
6379             break;
6380 
6381         case ISD::PARK_PARKING:
6382         case ISD::PARK_UNPARKING:
6383             // TODO make the timeouts configurable by the user
6384             if (getCurrentOperationMsec() > (60 * 1000))
6385             {
6386                 if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS)
6387                 {
6388                     appendLogText(i18n("Operation timeout. Restarting operation..."));
6389                     if (status == ISD::PARK_PARKING)
6390                         parkCap();
6391                     else
6392                         unParkCap();
6393                     break;
6394                 }
6395             }
6396             break;
6397 
6398         case ISD::PARK_ERROR:
6399             if (shutdownState == SHUTDOWN_PARKING_CAP)
6400             {
6401                 appendLogText(i18n("Cap parking error."));
6402                 shutdownState = SHUTDOWN_ERROR;
6403             }
6404             else if (startupState == STARTUP_UNPARKING_CAP)
6405             {
6406                 appendLogText(i18n("Cap unparking error."));
6407                 startupState = STARTUP_ERROR;
6408             }
6409             parkingFailureCount = 0;
6410             break;
6411 
6412         default:
6413             break;
6414     }
6415 }
6416 
startJobEvaluation()6417 void Scheduler::startJobEvaluation()
6418 {
6419     // Reset current job
6420     setCurrentJob(nullptr);
6421 
6422     // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept
6423     for (SchedulerJob * job : jobs)
6424     {
6425         job->reset();
6426         job->setCompletedCount(0);
6427     }
6428 
6429     // Unconditionally update the capture storage
6430     updateCompletedJobsCount(true);
6431 
6432     // And evaluate all pending jobs per the conditions set in each
6433     evaluateJobs(true);
6434 }
6435 
sortJobsPerAltitude()6436 void Scheduler::sortJobsPerAltitude()
6437 {
6438     // We require a first job to sort, so bail out if list is empty
6439     if (jobs.isEmpty())
6440         return;
6441 
6442     // Don't reset current job
6443     // setCurrentJob(nullptr);
6444 
6445     // Don't reset scheduler jobs startup times before sorting - we need the first job startup time
6446 
6447     // Sort by startup time, using the first job time as reference for altitude calculations
6448     using namespace std::placeholders;
6449     QList<SchedulerJob*> sortedJobs = jobs;
6450     std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(),
6451                      std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime()));
6452 
6453     // If order changed, reset and re-evaluate
6454     if (reorderJobs(sortedJobs))
6455     {
6456         for (SchedulerJob * job : jobs)
6457             job->reset();
6458 
6459         evaluateJobs(true);
6460     }
6461 }
6462 
resumeCheckStatus()6463 void Scheduler::resumeCheckStatus()
6464 {
6465     disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus);
6466     TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
6467     setupNextIteration(RUN_SCHEDULER);
6468 }
6469 
getErrorHandlingStrategy()6470 Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy()
6471 {
6472     // The UI holds the state
6473     if (errorHandlingRestartAfterAllButton->isChecked())
6474         return ERROR_RESTART_AFTER_TERMINATION;
6475     else if (errorHandlingRestartImmediatelyButton->isChecked())
6476         return ERROR_RESTART_IMMEDIATELY;
6477     else
6478         return ERROR_DONT_RESTART;
6479 
6480 }
6481 
setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy)6482 void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy)
6483 {
6484     errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART);
6485 
6486     switch (strategy)
6487     {
6488         case ERROR_RESTART_AFTER_TERMINATION:
6489             errorHandlingRestartAfterAllButton->setChecked(true);
6490             break;
6491         case ERROR_RESTART_IMMEDIATELY:
6492             errorHandlingRestartImmediatelyButton->setChecked(true);
6493             break;
6494         default:
6495             errorHandlingDontRestartButton->setChecked(true);
6496             break;
6497     }
6498 }
6499 
6500 
6501 
startMosaicTool()6502 void Scheduler::startMosaicTool()
6503 {
6504     bool raOk = false, decOk = false;
6505     dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours
6506     dms dec(decBox->createDms(true, &decOk));
6507 
6508     if (raOk == false)
6509     {
6510         appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text()));
6511         return;
6512     }
6513 
6514     if (decOk == false)
6515     {
6516         appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text()));
6517         return;
6518     }
6519 
6520     SkyPoint center;
6521     center.setRA0(ra);
6522     center.setDec0(dec);
6523 
6524     Mosaic mosaicTool(nameEdit->text(), center, Ekos::Manager::Instance());
6525 
6526     if (mosaicTool.exec() == QDialog::Accepted)
6527     {
6528         // #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
6529         // #1.1 Set prefix to Target-Part#
6530         // #1.2 Set directory to output/Target-Part#
6531 
6532         // #2 Save all sequence files in Jobs dir
6533         // #3 Set as current Sequence file
6534         // #4 Change Target name to Target-Part#
6535         // #5 Update J2000 coords
6536         // #6 Repeat and save Ekos Scheduler List in the output directory
6537         qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir "
6538                                        << mosaicTool.getJobsDir();
6539 
6540         QString outputDir  = mosaicTool.getJobsDir();
6541         QString targetName = nameEdit->text().simplified();
6542 
6543         // Sanitize name
6544         targetName = targetName.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" )
6545                      // Remove any two or more __
6546                      .replace( QRegularExpression("_{2,}"), "_")
6547                      // Remove any _ at the end
6548                      .replace( QRegularExpression("_$"), "");
6549 
6550         int batchCount     = 1;
6551 
6552         XMLEle *root = getSequenceJobRoot();
6553         if (root == nullptr)
6554             return;
6555 
6556         // Delete any prior jobs before saving
6557         if (!jobs.empty())
6558         {
6559             if (KMessageBox::questionYesNo(nullptr,
6560                                            i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No)
6561             {
6562                 qDeleteAll(jobs);
6563                 jobs.clear();
6564                 while (queueTable->rowCount() > 0)
6565                     queueTable->removeRow(0);
6566             }
6567         }
6568 
6569         // We do not want FITS image for mosaic job since each job has its own calculated center
6570         QString fitsFileBackup = fitsEdit->text();
6571         fitsEdit->clear();
6572 
6573         foreach (auto oneJob, mosaicTool.getJobs())
6574         {
6575             QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++);
6576 
6577             prefix.replace(' ', '-');
6578             nameEdit->setText(prefix);
6579 
6580             if (createJobSequence(root, prefix, outputDir) == false)
6581                 return;
6582 
6583             QString filename = QString("%1/%2.esq").arg(outputDir, prefix);
6584             sequenceEdit->setText(filename);
6585             sequenceURL = QUrl::fromLocalFile(filename);
6586 
6587             raBox->showInHours(oneJob.center.ra0());
6588             decBox->showInDegrees(oneJob.center.dec0());
6589             rotationSpin->setValue(oneJob.rotation);
6590 
6591             alignStepCheck->setChecked(oneJob.doAlign);
6592             focusStepCheck->setChecked(oneJob.doFocus);
6593 
6594             saveJob();
6595         }
6596 
6597         delXMLEle(root);
6598 
6599         QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir, targetName)));
6600 
6601         if (saveScheduler(mosaicURL))
6602         {
6603             appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile()));
6604         }
6605         else
6606         {
6607             appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile()));
6608         }
6609 
6610         fitsEdit->setText(fitsFileBackup);
6611     }
6612 }
6613 
getSequenceJobRoot()6614 XMLEle * Scheduler::getSequenceJobRoot()
6615 {
6616     QFile sFile;
6617     sFile.setFileName(sequenceURL.toLocalFile());
6618 
6619     if (!sFile.open(QIODevice::ReadOnly))
6620     {
6621         KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()),
6622                               i18n("Could Not Open File"));
6623         return nullptr;
6624     }
6625 
6626     LilXML *xmlParser = newLilXML();
6627     char errmsg[MAXRBUF];
6628     XMLEle *root = nullptr;
6629     char c;
6630 
6631     while (sFile.getChar(&c))
6632     {
6633         root = readXMLEle(xmlParser, c, errmsg);
6634 
6635         if (root)
6636             break;
6637     }
6638 
6639     delLilXML(xmlParser);
6640     sFile.close();
6641     return root;
6642 }
6643 
createJobSequence(XMLEle * root,const QString & prefix,const QString & outputDir)6644 bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir)
6645 {
6646     QFile sFile;
6647     sFile.setFileName(sequenceURL.toLocalFile());
6648 
6649     if (!sFile.open(QIODevice::ReadOnly))
6650     {
6651         KSNotification::sorry(i18n("Unable to open sequence file %1", sFile.fileName()),
6652                               i18n("Could Not Open File"));
6653         return false;
6654     }
6655 
6656     XMLEle *ep    = nullptr;
6657     XMLEle *subEP = nullptr;
6658 
6659     for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
6660     {
6661         if (!strcmp(tagXMLEle(ep), "Job"))
6662         {
6663             for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
6664             {
6665                 if (!strcmp(tagXMLEle(subEP), "Prefix"))
6666                 {
6667                     XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix");
6668                     if (rawPrefix)
6669                     {
6670                         editXMLEle(rawPrefix, prefix.toLatin1().constData());
6671                     }
6672                 }
6673                 else if (!strcmp(tagXMLEle(subEP), "FITSDirectory"))
6674                 {
6675                     editXMLEle(subEP, QString("%1/%2").arg(outputDir, prefix).toLatin1().constData());
6676                 }
6677             }
6678         }
6679     }
6680 
6681     QDir().mkpath(outputDir);
6682 
6683     QString filename = QString("%1/%2.esq").arg(outputDir, prefix);
6684     FILE *outputFile = fopen(filename.toLatin1().constData(), "w");
6685 
6686     if (outputFile == nullptr)
6687     {
6688         QString message = i18n("Unable to write to file %1", filename);
6689         KSNotification::sorry(message, i18n("Could Not Open File"));
6690         return false;
6691     }
6692 
6693     fprintf(outputFile, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
6694     prXMLEle(outputFile, root, 0);
6695 
6696     fclose(outputFile);
6697 
6698     return true;
6699 }
6700 
resetAllJobs()6701 void Scheduler::resetAllJobs()
6702 {
6703     if (state == SCHEDULER_RUNNING)
6704         return;
6705 
6706     // Reset capture count of all jobs before re-evaluating
6707     foreach (SchedulerJob *job, jobs)
6708         job->setCompletedCount(0);
6709 
6710     // Evaluate all jobs, this refreshes storage and resets job states
6711     startJobEvaluation();
6712 }
6713 
checkTwilightWarning(bool enabled)6714 void Scheduler::checkTwilightWarning(bool enabled)
6715 {
6716     if (enabled)
6717         return;
6718 
6719     if (KMessageBox::warningContinueCancel(
6720                 nullptr,
6721                 i18n("Turning off astronomial twilight check may cause the observatory "
6722                      "to run during daylight. This can cause irreversible damage to your equipment!"),
6723                 i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
6724                 "astronomical_twilight_warning") == KMessageBox::Cancel)
6725     {
6726         twilightCheck->setChecked(true);
6727     }
6728 }
6729 
checkStartupProcedure()6730 void Scheduler::checkStartupProcedure()
6731 {
6732     if (checkStartupState() == false)
6733         QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
6734     else
6735     {
6736         if (startupState == STARTUP_COMPLETE)
6737             appendLogText(i18n("Manual startup procedure completed successfully."));
6738         else if (startupState == STARTUP_ERROR)
6739             appendLogText(i18n("Manual startup procedure terminated due to errors."));
6740 
6741         startupB->setIcon(
6742             QIcon::fromTheme("media-playback-start"));
6743     }
6744 }
6745 
runStartupProcedure()6746 void Scheduler::runStartupProcedure()
6747 {
6748     if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE)
6749     {
6750         /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */
6751         if (indiState == INDI_IDLE)
6752         {
6753             KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online."));
6754             return;
6755         }
6756 
6757         if (KMessageBox::questionYesNo(
6758                     nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes)
6759         {
6760             appendLogText(i18n("Warning: executing startup procedure manually..."));
6761             startupB->setIcon(
6762                 QIcon::fromTheme("media-playback-stop"));
6763             startupState = STARTUP_IDLE;
6764             checkStartupState();
6765             QTimer::singleShot(1000, this, SLOT(checkStartupProcedure()));
6766         }
6767     }
6768     else
6769     {
6770         switch (startupState)
6771         {
6772             case STARTUP_IDLE:
6773                 break;
6774 
6775             case STARTUP_SCRIPT:
6776                 scriptProcess.terminate();
6777                 break;
6778 
6779             case STARTUP_UNPARK_DOME:
6780                 break;
6781 
6782             case STARTUP_UNPARKING_DOME:
6783                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:call", "abort");
6784                 domeInterface->call(QDBus::AutoDetect, "abort");
6785                 break;
6786 
6787             case STARTUP_UNPARK_MOUNT:
6788                 break;
6789 
6790             case STARTUP_UNPARKING_MOUNT:
6791                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "abort");
6792                 mountInterface->call(QDBus::AutoDetect, "abort");
6793                 break;
6794 
6795             case STARTUP_UNPARK_CAP:
6796                 break;
6797 
6798             case STARTUP_UNPARKING_CAP:
6799                 break;
6800 
6801             case STARTUP_COMPLETE:
6802                 break;
6803 
6804             case STARTUP_ERROR:
6805                 break;
6806         }
6807 
6808         startupState = STARTUP_IDLE;
6809 
6810         appendLogText(i18n("Startup procedure terminated."));
6811     }
6812 }
6813 
checkShutdownProcedure()6814 void Scheduler::checkShutdownProcedure()
6815 {
6816     // If shutdown procedure is not finished yet, let's check again in 1 second.
6817     if (checkShutdownState() == false)
6818         QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
6819     else
6820     {
6821         if (shutdownState == SHUTDOWN_COMPLETE)
6822         {
6823             appendLogText(i18n("Manual shutdown procedure completed successfully."));
6824             // Stop Ekos
6825             if (Options::stopEkosAfterShutdown())
6826                 stopEkos();
6827         }
6828         else if (shutdownState == SHUTDOWN_ERROR)
6829             appendLogText(i18n("Manual shutdown procedure terminated due to errors."));
6830 
6831         shutdownState = SHUTDOWN_IDLE;
6832         shutdownB->setIcon(
6833             QIcon::fromTheme("media-playback-start"));
6834     }
6835 }
6836 
runShutdownProcedure()6837 void Scheduler::runShutdownProcedure()
6838 {
6839     if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE)
6840     {
6841         if (KMessageBox::questionYesNo(
6842                     nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes)
6843         {
6844             appendLogText(i18n("Warning: executing shutdown procedure manually..."));
6845             shutdownB->setIcon(
6846                 QIcon::fromTheme("media-playback-stop"));
6847             shutdownState = SHUTDOWN_IDLE;
6848             checkShutdownState();
6849             QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure()));
6850         }
6851     }
6852     else
6853     {
6854         switch (shutdownState)
6855         {
6856             case SHUTDOWN_IDLE:
6857                 break;
6858 
6859             case SHUTDOWN_SCRIPT:
6860                 break;
6861 
6862             case SHUTDOWN_SCRIPT_RUNNING:
6863                 scriptProcess.terminate();
6864                 break;
6865 
6866             case SHUTDOWN_PARK_DOME:
6867                 break;
6868 
6869             case SHUTDOWN_PARKING_DOME:
6870                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:call", "abort");
6871                 domeInterface->call(QDBus::AutoDetect, "abort");
6872                 break;
6873 
6874             case SHUTDOWN_PARK_MOUNT:
6875                 break;
6876 
6877             case SHUTDOWN_PARKING_MOUNT:
6878                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "abort");
6879                 mountInterface->call(QDBus::AutoDetect, "abort");
6880                 break;
6881 
6882             case SHUTDOWN_PARK_CAP:
6883                 break;
6884 
6885             case SHUTDOWN_PARKING_CAP:
6886                 break;
6887 
6888             case SHUTDOWN_COMPLETE:
6889                 break;
6890 
6891             case SHUTDOWN_ERROR:
6892                 break;
6893         }
6894 
6895         shutdownState = SHUTDOWN_IDLE;
6896 
6897         appendLogText(i18n("Shutdown procedure terminated."));
6898     }
6899 }
6900 
loadProfiles()6901 void Scheduler::loadProfiles()
6902 {
6903     QString currentProfile = schedulerProfileCombo->currentText();
6904 
6905     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "ekosInterface:call", "getProfiles");
6906     QDBusReply<QStringList> profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles");
6907 
6908     if (profiles.error().type() == QDBusError::NoError)
6909     {
6910         schedulerProfileCombo->blockSignals(true);
6911         schedulerProfileCombo->clear();
6912         schedulerProfileCombo->addItem(i18n("Default"));
6913         schedulerProfileCombo->addItems(profiles);
6914         schedulerProfileCombo->setCurrentText(currentProfile);
6915         schedulerProfileCombo->blockSignals(false);
6916     }
6917 }
6918 
loadSequenceQueue(const QString & fileURL,SchedulerJob * schedJob,QList<SequenceJob * > & jobs,bool & hasAutoFocus,Scheduler * scheduler)6919 bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList<SequenceJob *> &jobs,
6920                                   bool &hasAutoFocus, Scheduler *scheduler)
6921 {
6922     QFile sFile;
6923     sFile.setFileName(fileURL);
6924 
6925     if (!sFile.open(QIODevice::ReadOnly))
6926     {
6927         scheduler->appendLogText(i18n("Unable to open sequence queue file '%1'", fileURL));
6928         return false;
6929     }
6930 
6931     LilXML *xmlParser = newLilXML();
6932     char errmsg[MAXRBUF];
6933     XMLEle *root = nullptr;
6934     XMLEle *ep   = nullptr;
6935     char c;
6936 
6937     while (sFile.getChar(&c))
6938     {
6939         root = readXMLEle(xmlParser, c, errmsg);
6940 
6941         if (root)
6942         {
6943             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
6944             {
6945                 if (!strcmp(tagXMLEle(ep), "Autofocus"))
6946                     hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true"));
6947                 else if (!strcmp(tagXMLEle(ep), "Job"))
6948                     jobs.append(processJobInfo(ep, schedJob));
6949             }
6950             delXMLEle(root);
6951         }
6952         else if (errmsg[0])
6953         {
6954             if (scheduler != nullptr) scheduler->appendLogText(QString(errmsg));
6955             delLilXML(xmlParser);
6956             qDeleteAll(jobs);
6957             return false;
6958         }
6959     }
6960 
6961     return true;
6962 }
6963 
processJobInfo(XMLEle * root,SchedulerJob * schedJob)6964 SequenceJob * Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob)
6965 {
6966     SequenceJob *job = new SequenceJob(root);
6967     if (FRAME_LIGHT == job->getFrameType() && nullptr != schedJob)
6968         schedJob->setLightFramesRequired(true);
6969 
6970     auto placeholderPath = Ekos::PlaceholderPath();
6971     placeholderPath.processJobInfo(job, schedJob->getName());
6972 
6973     return job;
6974 }
6975 
getCompletedFiles(const QString & path,const QString & seqPrefix)6976 int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix)
6977 {
6978     int seqFileCount = 0;
6979     QFileInfo const path_info(path);
6980     QString const sig_dir(path_info.dir().path());
6981     QString const sig_file(path_info.completeBaseName());
6982 
6983     qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file,
6984                                    seqPrefix);
6985     QDirIterator it(sig_dir, QDir::Files);
6986 
6987     /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */
6988     while (it.hasNext())
6989     {
6990         QString const fileName = QFileInfo(it.next()).completeBaseName();
6991 
6992         if (fileName.startsWith(seqPrefix))
6993         {
6994             qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName);
6995             seqFileCount++;
6996         }
6997     }
6998 
6999     TEST_PRINT(stderr, "sch%d @@@getCompletedFiles(%s %s): %d\n", __LINE__, path.toLatin1().data(), seqPrefix.toLatin1().data(),
7000                seqFileCount);
7001     return seqFileCount;
7002 }
7003 
setINDICommunicationStatus(Ekos::CommunicationStatus status)7004 void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status)
7005 {
7006     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %d\n", __LINE__, "ekosInterface:indiStatusChanged", status);
7007     qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status;
7008 
7009     m_INDICommunicationStatus = status;
7010 }
7011 
setEkosCommunicationStatus(Ekos::CommunicationStatus status)7012 void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status)
7013 {
7014     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %d\n", __LINE__, "ekosInterface:ekosStatusChanged", status);
7015     qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status;
7016 
7017     m_EkosCommunicationStatus = status;
7018 }
7019 
simClockScaleChanged(float newScale)7020 void Scheduler::simClockScaleChanged(float newScale)
7021 {
7022     if (currentlySleeping())
7023     {
7024         QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround(static_cast<double>
7025                                       (iterationTimer.remainingTime())
7026                                       * KStarsData::Instance()->clock()->scale()
7027                                       / newScale));
7028         appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...",
7029                            remainingTimeMs.toString("hh:mm:ss")));
7030         iterationTimer.stop();
7031         iterationTimer.start(remainingTimeMs.msecsSinceStartOfDay());
7032     }
7033 }
7034 
simClockTimeChanged()7035 void Scheduler::simClockTimeChanged()
7036 {
7037     calculateDawnDusk();
7038     updateNightTime();
7039 
7040     // If the Scheduler is not running, reset all jobs and re-evaluate from a new current start point
7041     if (SCHEDULER_RUNNING != state)
7042     {
7043         startJobEvaluation();
7044     }
7045 }
7046 
registerNewModule(const QString & name)7047 void Scheduler::registerNewModule(const QString &name)
7048 {
7049     qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")";
7050 
7051     if (name == "Focus")
7052     {
7053         delete focusInterface;
7054         focusInterface   = new QDBusInterface("org.kde.kstars", focusPathString, focusInterfaceString,
7055                                               QDBusConnection::sessionBus(), this);
7056         connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)),
7057                 Qt::UniqueConnection);
7058     }
7059     else if (name == "Capture")
7060     {
7061         delete captureInterface;
7062         captureInterface = new QDBusInterface("org.kde.kstars", capturePathString, captureInterfaceString,
7063                                               QDBusConnection::sessionBus(), this);
7064 
7065         connect(captureInterface, SIGNAL(ready()), this, SLOT(syncProperties()));
7066         connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)),
7067                 Qt::UniqueConnection);
7068         checkInterfaceReady(captureInterface);
7069     }
7070     else if (name == "Mount")
7071     {
7072         delete mountInterface;
7073         mountInterface   = new QDBusInterface("org.kde.kstars", mountPathString, mountInterfaceString,
7074                                               QDBusConnection::sessionBus(), this);
7075 
7076         connect(mountInterface, SIGNAL(ready()), this, SLOT(syncProperties()));
7077         connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)),
7078                 Qt::UniqueConnection);
7079 
7080         checkInterfaceReady(mountInterface);
7081     }
7082     else if (name == "Align")
7083     {
7084         delete alignInterface;
7085         alignInterface   = new QDBusInterface("org.kde.kstars", alignPathString, alignInterfaceString,
7086                                               QDBusConnection::sessionBus(), this);
7087         connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)),
7088                 Qt::UniqueConnection);
7089     }
7090     else if (name == "Guide")
7091     {
7092         delete guideInterface;
7093         guideInterface   = new QDBusInterface("org.kde.kstars", guidePathString, guideInterfaceString,
7094                                               QDBusConnection::sessionBus(), this);
7095         connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)),
7096                 Qt::UniqueConnection);
7097     }
7098     else if (name == "Dome")
7099     {
7100         delete domeInterface;
7101         domeInterface    = new QDBusInterface("org.kde.kstars", domePathString, domeInterfaceString,
7102                                               QDBusConnection::sessionBus(), this);
7103 
7104         connect(domeInterface, SIGNAL(ready()), this, SLOT(syncProperties()));
7105         checkInterfaceReady(domeInterface);
7106     }
7107     else if (name == "Weather")
7108     {
7109         delete weatherInterface;
7110         weatherInterface = new QDBusInterface("org.kde.kstars", weatherPathString, weatherInterfaceString,
7111                                               QDBusConnection::sessionBus(), this);
7112 
7113         connect(weatherInterface, SIGNAL(ready()), this, SLOT(syncProperties()));
7114         connect(weatherInterface, SIGNAL(newStatus(ISD::Weather::Status)), this, SLOT(setWeatherStatus(ISD::Weather::Status)));
7115         checkInterfaceReady(weatherInterface);
7116     }
7117     else if (name == "DustCap")
7118     {
7119         delete capInterface;
7120         capInterface = new QDBusInterface("org.kde.kstars", dustCapPathString, dustCapInterfaceString,
7121                                           QDBusConnection::sessionBus(), this);
7122 
7123         connect(capInterface, SIGNAL(ready()), this, SLOT(syncProperties()), Qt::UniqueConnection);
7124         checkInterfaceReady(capInterface);
7125     }
7126 }
7127 
syncProperties()7128 void Scheduler::syncProperties()
7129 {
7130     QDBusInterface *iface = qobject_cast<QDBusInterface*>(sender());
7131 
7132     if (iface == mountInterface)
7133     {
7134         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "canPark");
7135         QVariant canMountPark = mountInterface->property("canPark");
7136         TEST_PRINT(stderr, "  @@@dbus received %s\n", !canMountPark.isValid() ? "invalid" : (canMountPark.toBool() ? "T" : "F"));
7137 
7138         unparkMountCheck->setEnabled(canMountPark.toBool());
7139         parkMountCheck->setEnabled(canMountPark.toBool());
7140         m_MountReady = true;
7141     }
7142     else if (iface == capInterface)
7143     {
7144         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:property", "canPark");
7145         QVariant canCapPark = capInterface->property("canPark");
7146         TEST_PRINT(stderr, "  @@@dbus received %s\n", !canCapPark.isValid() ? "invalid" : (canCapPark.toBool() ? "T" : "F"));
7147 
7148         if (canCapPark.isValid())
7149         {
7150             capCheck->setEnabled(canCapPark.toBool());
7151             uncapCheck->setEnabled(canCapPark.toBool());
7152             m_CapReady = true;
7153         }
7154         else
7155         {
7156             capCheck->setEnabled(false);
7157             uncapCheck->setEnabled(false);
7158         }
7159     }
7160     else if (iface == weatherInterface)
7161     {
7162         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "weatherInterface:property", "updatePeriod");
7163         QVariant updatePeriod = weatherInterface->property("updatePeriod");
7164         TEST_PRINT(stderr, "  @@@dbus received %d\n", !updatePeriod.isValid() ? -1 : updatePeriod.toInt());
7165 
7166         if (updatePeriod.isValid())
7167         {
7168             weatherCheck->setEnabled(true);
7169 
7170             TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "weatherInterface:property", "status");
7171             QVariant status = weatherInterface->property("status");
7172             TEST_PRINT(stderr, "  @@@dbus received %d\n", !status.isValid() ? -1 : status.toInt());
7173             setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
7174 
7175             //            if (updatePeriod.toInt() > 0)
7176             //            {
7177             //                weatherTimer.setInterval(updatePeriod.toInt() * 1000);
7178             //                connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather, Qt::UniqueConnection);
7179             //                weatherTimer.start();
7180 
7181             //                // Check weather initially
7182             //                checkWeather();
7183             //            }
7184         }
7185         else
7186             weatherCheck->setEnabled(true);
7187     }
7188     else if (iface == domeInterface)
7189     {
7190         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "canPark");
7191         QVariant canDomePark = domeInterface->property("canPark");
7192         TEST_PRINT(stderr, "  @@@dbus received %s\n", !canDomePark.isValid() ? "invalid" : (canDomePark.toBool() ? "T" : "F"));
7193         unparkDomeCheck->setEnabled(canDomePark.toBool());
7194         parkDomeCheck->setEnabled(canDomePark.toBool());
7195         m_DomeReady = true;
7196     }
7197     else if (iface == captureInterface)
7198     {
7199         TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:property", "coolerControl");
7200         QVariant hasCoolerControl = captureInterface->property("coolerControl");
7201         TEST_PRINT(stderr, "  @@@dbus received %s\n",
7202                    !hasCoolerControl.isValid() ? "invalid" : (hasCoolerControl.toBool() ? "T" : "F"));
7203         warmCCDCheck->setEnabled(hasCoolerControl.toBool());
7204         m_CaptureReady = true;
7205     }
7206 }
7207 
checkInterfaceReady(QDBusInterface * iface)7208 void Scheduler::checkInterfaceReady(QDBusInterface *iface)
7209 {
7210     if (iface == mountInterface)
7211     {
7212         QVariant canMountPark = mountInterface->property("canPark");
7213         if (canMountPark.isValid())
7214         {
7215             unparkMountCheck->setEnabled(canMountPark.toBool());
7216             parkMountCheck->setEnabled(canMountPark.toBool());
7217             m_MountReady = true;
7218         }
7219     }
7220     else if (iface == capInterface)
7221     {
7222         QVariant canCapPark = capInterface->property("canPark");
7223         if (canCapPark.isValid())
7224         {
7225             capCheck->setEnabled(canCapPark.toBool());
7226             uncapCheck->setEnabled(canCapPark.toBool());
7227             m_CapReady = true;
7228         }
7229         else
7230         {
7231             capCheck->setEnabled(false);
7232             uncapCheck->setEnabled(false);
7233         }
7234     }
7235     else if (iface == weatherInterface)
7236     {
7237         QVariant updatePeriod = weatherInterface->property("updatePeriod");
7238         if (updatePeriod.isValid())
7239         {
7240             weatherCheck->setEnabled(true);
7241             QVariant status = weatherInterface->property("status");
7242             setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
7243         }
7244         else
7245             weatherCheck->setEnabled(true);
7246     }
7247     else if (iface == domeInterface)
7248     {
7249         QVariant canDomePark = domeInterface->property("canPark");
7250         if (canDomePark.isValid())
7251         {
7252             unparkDomeCheck->setEnabled(canDomePark.toBool());
7253             parkDomeCheck->setEnabled(canDomePark.toBool());
7254             m_DomeReady = true;
7255         }
7256     }
7257     else if (iface == captureInterface)
7258     {
7259         QVariant hasCoolerControl = captureInterface->property("coolerControl");
7260         if (hasCoolerControl.isValid())
7261         {
7262             warmCCDCheck->setEnabled(hasCoolerControl.toBool());
7263             m_CaptureReady = true;
7264         }
7265     }
7266 }
7267 
setAlignStatus(Ekos::AlignState status)7268 void Scheduler::setAlignStatus(Ekos::AlignState status)
7269 {
7270     TEST_PRINT(stderr, "sch%d @@@setAlignStatus(%d)%s\n", __LINE__, static_cast<int>(status), (state == SCHEDULER_PAUSED
7271                || currentJob == nullptr) ? "IGNORED" : "OK");
7272 
7273     if (state == SCHEDULER_PAUSED || currentJob == nullptr)
7274         return;
7275 
7276     qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status);
7277 
7278     /* If current job is scheduled and has not started yet, wait */
7279     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
7280     {
7281         QDateTime const now = getLocalTime();
7282         if (now < currentJob->getStartupTime())
7283             return;
7284     }
7285 
7286     if (currentJob->getStage() == SchedulerJob::STAGE_ALIGNING)
7287     {
7288         // Is solver complete?
7289         if (status == Ekos::ALIGN_COMPLETE)
7290         {
7291             appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName()));
7292             alignFailureCount = 0;
7293 
7294             currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE);
7295 
7296             // If we solved a FITS file, let's use its center coords as our target.
7297             if (currentJob->getFITSFile().isEmpty() == false)
7298             {
7299                 QDBusReply<QList<double>> solutionReply = alignInterface->call("getTargetCoords");
7300                 if (solutionReply.isValid())
7301                 {
7302                     QList<double> const values = solutionReply.value();
7303                     currentJob->setTargetCoords(dms(values[0] * 15.0), dms(values[1]), KStarsData::Instance()->ut().djd());
7304                 }
7305             }
7306             getNextAction();
7307         }
7308         else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED)
7309         {
7310             appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName()));
7311 
7312             if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS)
7313             {
7314                 if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount)
7315                 {
7316                     appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(),
7317                                        alignFailureCount));
7318                     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:call", "resetModel");
7319                     mountInterface->call(QDBus::AutoDetect, "resetModel");
7320                 }
7321                 appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName()));
7322                 startAstrometry();
7323             }
7324             else
7325             {
7326                 appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName()));
7327                 currentJob->setState(SchedulerJob::JOB_ABORTED);
7328 
7329                 findNextJob();
7330             }
7331         }
7332     }
7333 }
7334 
setGuideStatus(Ekos::GuideState status)7335 void Scheduler::setGuideStatus(Ekos::GuideState status)
7336 {
7337     TEST_PRINT(stderr, "sch%d @@@setGuideStatus(%d)%s\n", __LINE__, static_cast<int>(status), (state == SCHEDULER_PAUSED
7338                || currentJob == nullptr) ? "IGNORED" : "OK");
7339     if (state == SCHEDULER_PAUSED || currentJob == nullptr)
7340         return;
7341 
7342     qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status);
7343 
7344     /* If current job is scheduled and has not started yet, wait */
7345     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
7346     {
7347         QDateTime const now = getLocalTime();
7348         if (now < currentJob->getStartupTime())
7349             return;
7350     }
7351 
7352     if (currentJob->getStage() == SchedulerJob::STAGE_GUIDING)
7353     {
7354         qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage...";
7355 
7356         // If calibration stage complete?
7357         if (status == Ekos::GUIDE_GUIDING)
7358         {
7359             appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName()));
7360             guideFailureCount = 0;
7361             // if guiding recovered while we are waiting, abort the restart
7362             cancelGuidingTimer();
7363 
7364             currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE);
7365             getNextAction();
7366         }
7367         else if (status == Ekos::GUIDE_CALIBRATION_ERROR ||
7368                  status == Ekos::GUIDE_ABORTED)
7369         {
7370             if (status == Ekos::GUIDE_ABORTED)
7371                 appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName()));
7372             else
7373                 appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName()));
7374 
7375             // if the timer for restarting the guiding is already running, we do nothing and
7376             // wait for the action triggered by the timer. This way we avoid that a small guiding problem
7377             // abort the scheduler job
7378 
7379             if (isGuidingTimerActive())
7380                 return;
7381 
7382             if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS)
7383             {
7384                 if (status == Ekos::GUIDE_CALIBRATION_ERROR &&
7385                         Options::realignAfterCalibrationFailure())
7386                 {
7387                     appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName()));
7388                     startAstrometry();
7389                 }
7390                 else
7391                 {
7392                     appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(),
7393                                        (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000));
7394                     startGuidingTimer(RESTART_GUIDING_DELAY_MS * guideFailureCount);
7395                 }
7396             }
7397             else
7398             {
7399                 appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName()));
7400                 currentJob->setState(SchedulerJob::JOB_ABORTED);
7401 
7402                 findNextJob();
7403             }
7404         }
7405     }
7406 }
7407 
getGuidingStatus()7408 GuideState Scheduler::getGuidingStatus()
7409 {
7410     TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "guideInterface:property", "status");
7411     QVariant guideStatus = guideInterface->property("status");
7412     TEST_PRINT(stderr, "  @@@dbus received %d\n", !guideStatus.isValid() ? -1 : guideStatus.toInt());
7413     Ekos::GuideState gStatus = static_cast<Ekos::GuideState>(guideStatus.toInt());
7414 
7415     return gStatus;
7416 }
7417 
setCaptureStatus(Ekos::CaptureState status)7418 void Scheduler::setCaptureStatus(Ekos::CaptureState status)
7419 {
7420     TEST_PRINT(stderr, "sch%d @@@setCaptureStatus(%d) %s\n", __LINE__, static_cast<int>(status),
7421                (currentJob == nullptr) ? "IGNORED" : "OK");
7422     if (currentJob == nullptr)
7423         return;
7424 
7425     qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status);
7426 
7427     /* If current job is scheduled and has not started yet, wait */
7428     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
7429     {
7430         QDateTime const now = getLocalTime();
7431         if (now < currentJob->getStartupTime())
7432             return;
7433     }
7434 
7435     if (currentJob->getStage() == SchedulerJob::STAGE_CAPTURING)
7436     {
7437         if (status == Ekos::CAPTURE_PROGRESS && (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN))
7438         {
7439             // JM 2021.09.20
7440             // Re-set target coords in align module
7441             // When capture starts, alignment module automatically rests target coords to mount coords.
7442             // However, we want to keep align module target synced with the scheduler target and not
7443             // the mount coord
7444             const SkyPoint targetCoords = currentJob->getTargetCoords();
7445             QList<QVariant> targetArgs;
7446             targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees();
7447             alignInterface->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords", targetArgs);
7448         }
7449         else if (status == Ekos::CAPTURE_ABORTED)
7450         {
7451             appendLogText(i18n("Warning: job '%1' failed to capture target.", currentJob->getName()));
7452 
7453             if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS)
7454             {
7455                 // If capture failed due to guiding error, let's try to restart that
7456                 if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
7457                 {
7458                     // Check if it is guiding related.
7459                     Ekos::GuideState gStatus = getGuidingStatus();
7460                     if (gStatus == Ekos::GUIDE_ABORTED ||
7461                             gStatus == Ekos::GUIDE_CALIBRATION_ERROR ||
7462                             gStatus == GUIDE_DITHERING_ERROR)
7463                     {
7464                         appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(),
7465                                            captureFailureCount, MAX_FAILURE_ATTEMPTS));
7466                         startGuiding(true);
7467                         return;
7468                     }
7469                 }
7470 
7471                 /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */
7472                 appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName()));
7473                 startCapture(true);
7474             }
7475             else
7476             {
7477                 /* FIXME: it's not clear whether this situation can be recovered at all */
7478                 appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName()));
7479                 currentJob->setState(SchedulerJob::JOB_ABORTED);
7480 
7481                 findNextJob();
7482             }
7483         }
7484         else if (status == Ekos::CAPTURE_COMPLETE)
7485         {
7486             KNotification::event(QLatin1String("EkosScheduledImagingFinished"),
7487                                  i18n("Ekos job (%1) - Capture finished", currentJob->getName()));
7488 
7489             currentJob->setState(SchedulerJob::JOB_COMPLETE);
7490             findNextJob();
7491         }
7492         else if (status == Ekos::CAPTURE_IMAGE_RECEIVED)
7493         {
7494             // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times.
7495             // FIXME: rework this once capture storage is reworked
7496             if (Options::rememberJobProgress())
7497             {
7498                 updateCompletedJobsCount(true);
7499 
7500                 for (const auto &job : jobs)
7501                     estimateJobTime(job, m_CapturedFramesCount, this);
7502             }
7503             // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks
7504             else currentJob->setCompletedCount(currentJob->getCompletedCount() + 1);
7505 
7506             captureFailureCount = 0;
7507         }
7508     }
7509 }
7510 
setFocusStatus(Ekos::FocusState status)7511 void Scheduler::setFocusStatus(Ekos::FocusState status)
7512 {
7513     TEST_PRINT(stderr, "sch%d @@@setFocusStatus(%d)%s\n", __LINE__, static_cast<int>(status), (state == SCHEDULER_PAUSED
7514                || currentJob == nullptr) ? "IGNORED" : "OK");
7515     if (state == SCHEDULER_PAUSED || currentJob == nullptr)
7516         return;
7517 
7518     qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status);
7519 
7520     /* If current job is scheduled and has not started yet, wait */
7521     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
7522     {
7523         QDateTime const now = getLocalTime();
7524         if (now < currentJob->getStartupTime())
7525             return;
7526     }
7527 
7528     if (currentJob->getStage() == SchedulerJob::STAGE_FOCUSING)
7529     {
7530         // Is focus complete?
7531         if (status == Ekos::FOCUS_COMPLETE)
7532         {
7533             appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName()));
7534 
7535             autofocusCompleted = true;
7536 
7537             currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE);
7538 
7539             getNextAction();
7540         }
7541         else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED)
7542         {
7543             appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName()));
7544 
7545             if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS)
7546             {
7547                 appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName()));
7548                 // Reset frame to original size.
7549                 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "focusInterface:call", "resetFrame");
7550                 focusInterface->call(QDBus::AutoDetect, "resetFrame");
7551                 // Restart focusing
7552                 qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 6883";
7553                 startFocusing();
7554             }
7555             else
7556             {
7557                 appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName()));
7558                 currentJob->setState(SchedulerJob::JOB_ABORTED);
7559 
7560                 findNextJob();
7561             }
7562         }
7563     }
7564 }
7565 
setMountStatus(ISD::Telescope::Status status)7566 void Scheduler::setMountStatus(ISD::Telescope::Status status)
7567 {
7568     TEST_PRINT(stderr, "sch%d @@@setMountStatus(%d)%s\n", __LINE__, static_cast<int>(status), (state == SCHEDULER_PAUSED
7569                || currentJob == nullptr) ? "IGNORED" : "OK");
7570     if (state == SCHEDULER_PAUSED || currentJob == nullptr)
7571         return;
7572 
7573     qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status;
7574 
7575     /* If current job is scheduled and has not started yet, wait */
7576     if (SchedulerJob::JOB_SCHEDULED == currentJob->getState())
7577         if (static_cast<QDateTime const>(getLocalTime()) < currentJob->getStartupTime())
7578             return;
7579 
7580     switch (currentJob->getStage())
7581     {
7582         case SchedulerJob::STAGE_SLEWING:
7583         {
7584             qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage...";
7585 
7586             if (status == ISD::Telescope::MOUNT_TRACKING)
7587             {
7588                 appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName()));
7589                 currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE);
7590                 /* getNextAction is deferred to checkJobStage for dome support */
7591             }
7592             else if (status == ISD::Telescope::MOUNT_ERROR)
7593             {
7594                 appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", currentJob->getName()));
7595                 currentJob->setState(SchedulerJob::JOB_ERROR);
7596                 findNextJob();
7597             }
7598             else if (status == ISD::Telescope::MOUNT_IDLE)
7599             {
7600                 appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", currentJob->getName()));
7601                 currentJob->setStage(SchedulerJob::STAGE_IDLE);
7602                 getNextAction();
7603             }
7604         }
7605         break;
7606 
7607         case SchedulerJob::STAGE_RESLEWING:
7608         {
7609             qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage...";
7610 
7611             if (status == ISD::Telescope::MOUNT_TRACKING)
7612             {
7613                 appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName()));
7614                 currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE);
7615                 /* getNextAction is deferred to checkJobStage for dome support */
7616             }
7617             else if (status == ISD::Telescope::MOUNT_ERROR)
7618             {
7619                 appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName()));
7620                 currentJob->setState(SchedulerJob::JOB_ERROR);
7621                 findNextJob();
7622             }
7623             else if (status == ISD::Telescope::MOUNT_IDLE)
7624             {
7625                 appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", currentJob->getName()));
7626                 currentJob->setStage(SchedulerJob::STAGE_IDLE);
7627                 getNextAction();
7628             }
7629         }
7630         break;
7631 
7632         default:
7633             break;
7634     }
7635 }
7636 
setWeatherStatus(ISD::Weather::Status status)7637 void Scheduler::setWeatherStatus(ISD::Weather::Status status)
7638 {
7639     TEST_PRINT(stderr, "sch%d @@@setWeatherStatus(%d)\n", __LINE__, static_cast<int>(status));
7640     ISD::Weather::Status newStatus = status;
7641     QString statusString;
7642 
7643     switch (newStatus)
7644     {
7645         case ISD::Weather::WEATHER_OK:
7646             statusString = i18n("Weather conditions are OK.");
7647             break;
7648 
7649         case ISD::Weather::WEATHER_WARNING:
7650             statusString = i18n("Warning: weather conditions are in the WARNING zone.");
7651             break;
7652 
7653         case ISD::Weather::WEATHER_ALERT:
7654             statusString = i18n("Caution: weather conditions are in the DANGER zone!");
7655             break;
7656 
7657         default:
7658             break;
7659     }
7660 
7661     if (newStatus != weatherStatus)
7662     {
7663         weatherStatus = newStatus;
7664 
7665         qCDebug(KSTARS_EKOS_SCHEDULER) << statusString;
7666 
7667         if (weatherStatus == ISD::Weather::WEATHER_OK)
7668             weatherLabel->setPixmap(
7669                 QIcon::fromTheme("security-high")
7670                 .pixmap(QSize(32, 32)));
7671         else if (weatherStatus == ISD::Weather::WEATHER_WARNING)
7672         {
7673             weatherLabel->setPixmap(
7674                 QIcon::fromTheme("security-medium")
7675                 .pixmap(QSize(32, 32)));
7676             KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone"));
7677         }
7678         else if (weatherStatus == ISD::Weather::WEATHER_ALERT)
7679         {
7680             weatherLabel->setPixmap(
7681                 QIcon::fromTheme("security-low")
7682                 .pixmap(QSize(32, 32)));
7683             KNotification::event(QLatin1String("WeatherAlert"),
7684                                  i18n("Weather conditions are critical. Observatory shutdown is imminent"));
7685         }
7686         else
7687             weatherLabel->setPixmap(QIcon::fromTheme("chronometer")
7688                                     .pixmap(QSize(32, 32)));
7689 
7690         weatherLabel->show();
7691         weatherLabel->setToolTip(statusString);
7692 
7693         appendLogText(statusString);
7694 
7695         emit weatherChanged(weatherStatus);
7696     }
7697 
7698     // Shutdown scheduler if it was started and not already in shutdown
7699     // and if weather checkbox is checked.
7700     if (weatherCheck->isChecked() && weatherStatus == ISD::Weather::WEATHER_ALERT && state != Ekos::SCHEDULER_IDLE
7701             && state != Ekos::SCHEDULER_SHUTDOWN)
7702     {
7703         appendLogText(i18n("Starting shutdown procedure due to severe weather."));
7704         if (currentJob)
7705         {
7706             currentJob->setState(SchedulerJob::JOB_ABORTED);
7707             stopCurrentJobAction();
7708         }
7709         checkShutdownState();
7710     }
7711 }
7712 
shouldSchedulerSleep(SchedulerJob * currentJob)7713 bool Scheduler::shouldSchedulerSleep(SchedulerJob *currentJob)
7714 {
7715     Q_ASSERT_X(nullptr != currentJob, __FUNCTION__,
7716                "There must be a valid current job for Scheduler to test sleep requirement");
7717 
7718     if (currentJob->getLightFramesRequired() == false)
7719         return false;
7720 
7721     QDateTime const now = getLocalTime();
7722     int const nextObservationTime = now.secsTo(currentJob->getStartupTime());
7723 
7724     // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed
7725     // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready
7726     if (startupState == STARTUP_COMPLETE &&
7727             Options::preemptiveShutdown() &&
7728             nextObservationTime > (Options::preemptiveShutdownTime() * 3600))
7729     {
7730         appendLogText(i18n(
7731                           "Job '%1' scheduled for execution at %2. "
7732                           "Observatory scheduled for shutdown until next job is ready.",
7733                           currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())));
7734         enablePreemptiveShutdown(currentJob->getStartupTime());
7735         weatherCheck->setEnabled(false);
7736         weatherLabel->hide();
7737         checkShutdownState();
7738         return true;
7739     }
7740     // Otherwise, sleep until job is ready
7741     /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */
7742     // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled
7743     // 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.
7744     // This is also only performed if next job is due more than the default lead time (5 minutes).
7745     // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes.
7746     else if (nextObservationTime > Options::leadTime() * 60 &&
7747              startupState == STARTUP_COMPLETE &&
7748              parkWaitState == PARKWAIT_IDLE &&
7749              (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) &&
7750              parkMountCheck->isEnabled() &&
7751              parkMountCheck->isChecked())
7752     {
7753         appendLogText(i18n(
7754                           "Job '%1' scheduled for execution at %2. "
7755                           "Parking the mount until the job is ready.",
7756                           currentJob->getName(), currentJob->getStartupTime().toString()));
7757 
7758         parkWaitState = PARKWAIT_PARK;
7759 
7760         return false;
7761     }
7762     else if (nextObservationTime > Options::leadTime() * 60)
7763     {
7764         appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(),
7765                            now.addSecs(nextObservationTime + 1).toString()));
7766         sleepLabel->setToolTip(i18n("Scheduler is in sleep mode"));
7767         sleepLabel->show();
7768 
7769         // Warn the user if the next job is really far away - 60/5 = 12 times the lead time
7770         if (nextObservationTime > Options::leadTime() * 60 * 12 && !Options::preemptiveShutdown())
7771         {
7772             dms delay(static_cast<double>(nextObservationTime * 15.0 / 3600.0));
7773             appendLogText(i18n(
7774                               "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.",
7775                               currentJob->getName(), delay.toHMSString()));
7776         }
7777 
7778         /* FIXME: stop tracking now */
7779 
7780         // Wake up when job is due.
7781         // FIXME: Implement waking up periodically before job is due for weather check.
7782         // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60;
7783         TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_WAKEUP).toLatin1().data());
7784         setupNextIteration(RUN_WAKEUP, std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale()));
7785 
7786         return true;
7787     }
7788 
7789     return false;
7790 }
7791 
7792 // TODO. It would be better to make this a class and give each operation its own timer.
7793 // TODO. These should be disabled once no longer relevant.
7794 // These are implement with a KStarsDateTime instead of a QTimer type class
7795 // so that the simulated clock can be used.
startCurrentOperationTimer()7796 void Scheduler::startCurrentOperationTimer()
7797 {
7798     currentOperationTimeStarted = true;
7799     currentOperationTime = KStarsData::Instance()->ut();
7800 }
7801 
7802 // Returns milliseconds since startCurrentOperationTImer() was called.
getCurrentOperationMsec()7803 qint64 Scheduler::getCurrentOperationMsec()
7804 {
7805     if (!currentOperationTimeStarted) return 0;
7806     return currentOperationTime.msecsTo(KStarsData::Instance()->ut());
7807 }
7808 
7809 // Operations on the guiding timer, which can restart guiding after failure.
startGuidingTimer(int milliseconds)7810 void Scheduler::startGuidingTimer(int milliseconds)
7811 {
7812     restartGuidingInterval = milliseconds;
7813     restartGuidingTime = KStarsData::Instance()->ut();
7814 }
7815 
cancelGuidingTimer()7816 void Scheduler::cancelGuidingTimer()
7817 {
7818     restartGuidingInterval = -1;
7819     restartGuidingTime = KStarsDateTime();
7820 }
7821 
isGuidingTimerActive()7822 bool Scheduler::isGuidingTimerActive()
7823 {
7824     return (restartGuidingInterval > 0 &&
7825             restartGuidingTime.msecsTo(KStarsData::Instance()->ut()) >= 0);
7826 }
7827 
processGuidingTimer()7828 void Scheduler::processGuidingTimer()
7829 {
7830     if ((restartGuidingInterval > 0) &&
7831             (restartGuidingTime.msecsTo(KStarsData::Instance()->ut()) > restartGuidingInterval))
7832     {
7833         cancelGuidingTimer();
7834         startGuiding(true);
7835     }
7836 }
7837 
enablePreemptiveShutdown(const QDateTime & wakeupTime)7838 void Scheduler::enablePreemptiveShutdown(const QDateTime &wakeupTime)
7839 {
7840     preemptiveShutdownWakeupTime = wakeupTime;
7841 }
7842 
disablePreemptiveShutdown()7843 void Scheduler::disablePreemptiveShutdown()
7844 {
7845     preemptiveShutdownWakeupTime = QDateTime();
7846 }
7847 
getPreemptiveShutdownWakeupTime()7848 QDateTime Scheduler::getPreemptiveShutdownWakeupTime()
7849 {
7850     return preemptiveShutdownWakeupTime;
7851 }
7852 
preemptiveShutdown()7853 bool Scheduler::preemptiveShutdown()
7854 {
7855     return preemptiveShutdownWakeupTime.isValid();
7856 }
7857 
7858 }
7859