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