Kstars

scheduler.cpp
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 "ekos/scheduler/framingassistantui.h"
13#include "ksnotification.h"
14#include "ksmessagebox.h"
15#include "kstars.h"
16#include "kstarsdata.h"
17#include "skymap.h"
18#include "Options.h"
19#include "scheduleradaptor.h"
20#include "schedulerjob.h"
21#include "schedulerprocess.h"
22#include "schedulermodulestate.h"
23#include "schedulerutils.h"
24#include "skymapcomposite.h"
25#include "skycomponents/mosaiccomponent.h"
26#include "skyobjects/mosaictiles.h"
27#include "auxiliary/QProgressIndicator.h"
28#include "dialogs/finddialog.h"
29#include "ekos/manager.h"
30#include "ekos/capture/sequencejob.h"
31#include "ekos/capture/placeholderpath.h"
32#include "skyobjects/starobject.h"
33#include "greedyscheduler.h"
34#include "ekos/auxiliary/solverutils.h"
35#include "ekos/auxiliary/stellarsolverprofile.h"
36
37#include <KConfigDialog>
38#include <KActionCollection>
39
40#include <fitsio.h>
41#include <ekos_scheduler_debug.h>
42#include <indicom.h>
43#include "ekos/capture/sequenceeditor.h"
44
45// Qt version calming
46#include <qtendl.h>
47
48#define BAD_SCORE -1000
49#define RESTART_GUIDING_DELAY_MS 5000
50
51#define DEFAULT_MIN_ALTITUDE 15
52#define DEFAULT_MIN_MOON_SEPARATION 0
53
54// This is a temporary debugging printout introduced while gaining experience developing
55// the unit tests in test_ekos_scheduler_ops.cpp.
56// All these printouts should be eventually removed.
57#define TEST_PRINT if (false) fprintf
58
59namespace
60{
61
62// This needs to match the definition order for the QueueTable in scheduler.ui
63enum QueueTableColumns
64{
65 NAME_COLUMN = 0,
71};
72
73QString CAPTURE_COUNT_TOOLTIP = i18n("Count of captures stored for the job, based on its sequence job.\n"
74 "This is a summary, additional specific frame types may be required to complete the job.");
75}
76
77namespace Ekos
78{
79
81{
82 // Use the default path and interface when running the scheduler.
83 setupScheduler(ekosPathString, ekosInterfaceString);
84}
85
86Scheduler::Scheduler(const QString path, const QString interface,
88{
89 // During testing, when mocking ekos, use a special purpose path and interface.
90 schedulerPathString = path;
91 kstarsInterfaceString = interface;
92 setupScheduler(ekosPathStr, ekosInterfaceStr);
93}
94
95void Scheduler::setupScheduler(const QString &ekosPathStr, const QString &ekosInterfaceStr)
96{
97 setupUi(this);
98
99 qRegisterMetaType<Ekos::SchedulerState>("Ekos::SchedulerState");
101
102 m_moduleState.reset(new SchedulerModuleState());
103 m_process.reset(new SchedulerProcess(moduleState(), ekosPathStr, ekosInterfaceStr));
104
106
107 // Get current KStars time and set seconds to zero
108 QDateTime currentDateTime = SchedulerModuleState::getLocalTime();
109 QTime currentTime = currentDateTime.time();
110 currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0);
111 currentDateTime.setTime(currentTime);
112
113 // Set initial time for startup and completion times
114 startupTimeEdit->setDateTime(currentDateTime);
115 schedulerUntilValue->setDateTime(currentDateTime);
116
117
118 sleepLabel->setPixmap(
119 QIcon::fromTheme("chronometer").pixmap(QSize(32, 32)));
120 changeSleepLabel("", false);
121
122 pi = new QProgressIndicator(this);
123 bottomLayout->addWidget(pi, 0);
124
125 geo = KStarsData::Instance()->geo();
126
127 //RA box should be HMS-style
128 raBox->setUnits(dmsBox::HOURS);
129
130 /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */
131
132 queueTable->setToolTip(
133 i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields."));
134 QTableWidgetItem *statusHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STATUS);
135 QTableWidgetItem *altitudeHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ALTITUDE);
136 QTableWidgetItem *startupHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STARTTIME);
137 QTableWidgetItem *completionHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ENDTIME);
138 QTableWidgetItem *captureCountHeader = queueTable->horizontalHeaderItem(SCHEDCOL_CAPTURES);
139
140 if (statusHeader != nullptr)
141 statusHeader->setToolTip(i18n("Current status of the job, managed by the Scheduler.\n"
142 "If invalid, the Scheduler was not able to find a proper observation time for the target.\n"
143 "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n"
144 "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats."));
145 if (altitudeHeader != nullptr)
146 altitudeHeader->setToolTip(i18n("Current altitude of the target of the job.\n"
147 "A rising target is indicated with an arrow going up.\n"
148 "A setting target is indicated with an arrow going down."));
149 if (startupHeader != nullptr)
150 startupHeader->setToolTip(i18n("Startup time of the job, as estimated by the Scheduler.\n"
151 "The altitude at startup, if available, is displayed too.\n"
152 "Fixed time from user or culmination time is marked with a chronometer symbol."));
153 if (completionHeader != nullptr)
154 completionHeader->setToolTip(i18n("Completion time for the job, as estimated by the Scheduler.\n"
155 "You may specify a fixed time to limit duration of looping jobs. "
156 "A warning symbol indicates the altitude at completion may cause the job to abort before completion.\n"));
157 if (captureCountHeader != nullptr)
159
160 /* Set first button mode to add observation job from left-hand fields */
161 setJobAddApply(true);
162
163 removeFromQueueB->setIcon(QIcon::fromTheme("list-remove"));
164 removeFromQueueB->setToolTip(
165 i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal."));
167
168 queueUpB->setIcon(QIcon::fromTheme("go-up"));
169 queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n"));
171 queueDownB->setIcon(QIcon::fromTheme("go-down"));
172 queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n"));
174
175 evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot"));
176 evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs."));
178 sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical"));
179 sortJobsB->setToolTip(
180 i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n"
181 "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n"
182 "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs."));
184 mosaicB->setIcon(QIcon::fromTheme("zoom-draw"));
186
187 positionAngleSpin->setSpecialValueText("--");
188
189 queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as"));
191 queueSaveB->setIcon(QIcon::fromTheme("document-save"));
193 queueLoadB->setIcon(QIcon::fromTheme("document-open"));
195 queueAppendB->setIcon(QIcon::fromTheme("document-import"));
197
198 loadSequenceB->setIcon(QIcon::fromTheme("document-open"));
200 selectStartupScriptB->setIcon(QIcon::fromTheme("document-open"));
202 selectShutdownScriptB->setIcon(
203 QIcon::fromTheme("document-open"));
205 selectFITSB->setIcon(QIcon::fromTheme("document-open"));
207
208 startupB->setIcon(
209 QIcon::fromTheme("media-playback-start"));
211 shutdownB->setIcon(
212 QIcon::fromTheme("media-playback-start"));
214
215 // 2023-06-27 sterne-jaeger: For simplicity reasons, the repeat option
216 // for all sequences is only active if we do consider the past
217 schedulerRepeatEverything->setEnabled(Options::rememberJobProgress() == false);
218 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false);
219 executionSequenceLimit->setValue(Options::schedulerExecutionSequencesLimit());
220
223
229
230 connect(KStars::Instance()->actionCollection()->action("show_mosaic_panel"), &QAction::triggered, this, [this](bool checked)
231 {
232 mosaicB->setDown(checked);
233 });
235 {
236 KStars::Instance()->actionCollection()->action("show_mosaic_panel")->trigger();
237 });
239 {
240 // add job from UI
241 addJob();
242 });
251
252
253 // These connections are looking for changes in the rows queueTable is displaying.
254 connect(queueTable->verticalScrollBar(), &QScrollBar::valueChanged, [this]()
255 {
256 updateJobTable();
257 });
258 connect(queueTable->verticalScrollBar(), &QAbstractSlider::rangeChanged, [this]()
259 {
260 updateJobTable();
261 });
262
263 startB->setIcon(QIcon::fromTheme("media-playback-start"));
264 startB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
265 pauseB->setIcon(QIcon::fromTheme("media-playback-pause"));
266 pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
267 pauseB->setCheckable(false);
268
269 connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler);
270 connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause);
271
272 connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs);
273 connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save);
275 {
276 load(true);
277 });
279 {
280 load(false);
281 });
282
284
285 // Connect to the state machine
286 connect(moduleState().data(), &SchedulerModuleState::ekosStateChanged, this, &Scheduler::ekosStateChanged);
287 connect(moduleState().data(), &SchedulerModuleState::indiStateChanged, this, &Scheduler::indiStateChanged);
288 connect(moduleState().data(), &SchedulerModuleState::schedulerStateChanged, this, &Scheduler::handleSchedulerStateChanged);
289 connect(moduleState().data(), &SchedulerModuleState::startupStateChanged, this, &Scheduler::startupStateChanged);
290 connect(moduleState().data(), &SchedulerModuleState::shutdownStateChanged, this, &Scheduler::shutdownStateChanged);
291 connect(moduleState().data(), &SchedulerModuleState::parkWaitStateChanged, this, &Scheduler::parkWaitStateChanged);
292 connect(moduleState().data(), &SchedulerModuleState::profilesChanged, this, &Scheduler::updateProfiles);
293 connect(moduleState().data(), &SchedulerModuleState::currentPositionChanged, queueTable, &QTableWidget::selectRow);
294 connect(moduleState().data(), &SchedulerModuleState::jobStageChanged, this, &Scheduler::updateJobStageUI);
295 connect(moduleState().data(), &SchedulerModuleState::updateNightTime, this, &Scheduler::updateNightTime);
296 connect(moduleState().data(), &SchedulerModuleState::currentProfileChanged, this, [&]()
297 {
298 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile());
299 });
300 // Connect to process engine
301 connect(process().data(), &SchedulerProcess::schedulerStopped, this, &Scheduler::schedulerStopped);
302 connect(process().data(), &SchedulerProcess::schedulerPaused, this, &Scheduler::handleSetPaused);
303 connect(process().data(), &SchedulerProcess::shutdownStarted, this, &Scheduler::handleShutdownStarted);
304 connect(process().data(), &SchedulerProcess::schedulerSleeping, this, &Scheduler::handleSchedulerSleeping);
305 connect(process().data(), &SchedulerProcess::jobsUpdated, this, &Scheduler::handleJobsUpdated);
306 connect(process().data(), &SchedulerProcess::targetDistance, this, &Scheduler::targetDistance);
307 connect(process().data(), &SchedulerProcess::updateJobTable, this, &Scheduler::updateJobTable);
308 connect(process().data(), &SchedulerProcess::clearJobTable, this, &Scheduler::clearJobTable);
309 connect(process().data(), &SchedulerProcess::addJob, this, &Scheduler::addJob);
310 connect(process().data(), &SchedulerProcess::changeCurrentSequence, this, &Scheduler::setSequence);
311 connect(process().data(), &SchedulerProcess::jobStarted, this, &Scheduler::jobStarted);
312 connect(process().data(), &SchedulerProcess::jobEnded, this, &Scheduler::jobEnded);
313 connect(process().data(), &SchedulerProcess::syncGreedyParams, this, &Scheduler::syncGreedyParams);
314 connect(process().data(), &SchedulerProcess::syncGUIToGeneralSettings, this, &Scheduler::syncGUIToGeneralSettings);
315 connect(process().data(), &SchedulerProcess::changeSleepLabel, this, &Scheduler::changeSleepLabel);
316 connect(process().data(), &SchedulerProcess::updateSchedulerURL, this, &Scheduler::updateSchedulerURL);
317 connect(process().data(), &SchedulerProcess::interfaceReady, this, &Scheduler::interfaceReady);
318 connect(process().data(), &SchedulerProcess::newWeatherStatus, this, &Scheduler::setWeatherStatus);
319 // Connect geographical location - when it is available
320 //connect(KStarsData::Instance()..., &LocationDialog::locationChanged..., this, &Scheduler::simClockTimeChanged);
321
322 // Restore values for general settings.
324
325
328 {
329 Q_UNUSED(button)
331 Options::setErrorHandlingStrategy(strategy);
332 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART);
333 });
334 connect(errorHandlingStrategyDelay, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [](int value)
335 {
336 Options::setErrorHandlingStrategyDelay(value);
337 });
338
339 // Retiring the Classic algorithm.
340 if (Options::schedulerAlgorithm() != ALGORITHM_GREEDY)
341 {
342 process()->appendLogText(
343 i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
344 Options::setSchedulerAlgorithm(ALGORITHM_GREEDY);
345 }
346
347 // restore default values for scheduler algorithm
348 setAlgorithm(Options::schedulerAlgorithm());
349
351 {
352 SkyPoint center = SkyMap::Instance()->getCenterPoint();
353 //center.deprecess(KStarsData::Instance()->updateNum());
354 center.catalogueCoord(KStarsData::Instance()->updateNum()->julianDay());
355 raBox->show(center.ra0());
356 decBox->show(center.dec0());
357 });
358
360
362 {
363 if (!m_SequenceEditor)
364 m_SequenceEditor.reset(new SequenceEditor(this));
365
366 m_SequenceEditor->show();
367 m_SequenceEditor->raise();
368 });
369
370 m_JobUpdateDebounce.setSingleShot(true);
371 m_JobUpdateDebounce.setInterval(1000);
372 connect(&m_JobUpdateDebounce, &QTimer::timeout, this, [this]()
373 {
374 emit jobsUpdated(moduleState()->getJSONJobs());
375 });
376
377 moduleState()->calculateDawnDusk();
378 process()->loadProfiles();
379
380 watchJobChanges(true);
381
382 loadGlobalSettings();
383 connectSettings();
384}
385
386QString Scheduler::getCurrentJobName()
387{
388 return (activeJob() != nullptr ? activeJob()->getName() : "");
389}
390
392{
393 /* Don't double watch, this will cause multiple signals to be connected */
394 if (enable == jobChangesAreWatched)
395 return;
396
397 /* These are the widgets we want to connect, per signal function, to listen for modifications */
398 QLineEdit * const lineEdits[] =
399 {
400 nameEdit,
401 groupEdit,
402 raBox,
403 decBox,
404 fitsEdit,
408 };
409
410 QDateTimeEdit * const dateEdits[] =
411 {
414 };
415
416 QComboBox * const comboBoxes[] =
417 {
419 };
420
421 QButtonGroup * const buttonGroups[] =
422 {
430 };
431
432 QAbstractButton * const buttons[] =
433 {
435 };
436
437 QSpinBox * const spinBoxes[] =
438 {
441 };
442
443 QDoubleSpinBox * const dspinBoxes[] =
444 {
448 };
449
450 if (enable)
451 {
452 /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will
453 * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions
454 * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the
455 * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'.
456 * The main problem with this implementation compared to the macro method is that it is now possible to
457 * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot
458 * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to
459 * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart.
460 */
461 for (auto * const control : lineEdits)
462 connect(control, &QLineEdit::editingFinished, this, [this]()
463 {
464 setDirty();
465 });
466 for (auto * const control : dateEdits)
467 connect(control, &QDateTimeEdit::editingFinished, this, [this]()
468 {
469 setDirty();
470 });
471 for (auto * const control : comboBoxes)
472 connect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this]()
473 {
474 setDirty();
475 });
476 for (auto * const control : buttonGroups)
477#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
478 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, [this](int, bool)
479#else
480 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, [this](int, bool)
481#endif
482 {
483 setDirty();
484 });
485 for (auto * const control : buttons)
486 connect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, [this](bool)
487 {
488 setDirty();
489 });
490 for (auto * const control : spinBoxes)
491 connect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]()
492 {
493 setDirty();
494 });
495 for (auto * const control : dspinBoxes)
496 connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this](double)
497 {
498 setDirty();
499 });
500 }
501 else
502 {
503 /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets,
504 * because we did not take care to keep the connection object when connecting. No problem in our case, we do not
505 * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to
506 * disconnect selectively.
507 */
508 for (auto * const control : lineEdits)
509 disconnect(control, &QLineEdit::editingFinished, this, nullptr);
510 for (auto * const control : dateEdits)
511 disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr);
512 for (auto * const control : comboBoxes)
513 disconnect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, nullptr);
514 for (auto * const control : buttons)
515 disconnect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, nullptr);
516 for (auto * const control : buttonGroups)
517#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
518 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, nullptr);
519#else
520 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, nullptr);
521#endif
522 for (auto * const control : spinBoxes)
523 disconnect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, nullptr);
524 for (auto * const control : dspinBoxes)
525 disconnect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, nullptr);
526 }
527
528 jobChangesAreWatched = enable;
529}
530
532{
533 schedulerRepeatEverything->setEnabled(Options::rememberJobProgress() == false);
534 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false);
535}
536
538{
539 if (FindDialog::Instance()->execWithParent(Ekos::Manager::Instance()) == QDialog::Accepted)
540 {
541 SkyObject *object = FindDialog::Instance()->targetObject();
542 addObject(object);
543 }
544}
545
546void Scheduler::addObject(SkyObject *object)
547{
548 if (object != nullptr)
549 {
550 QString finalObjectName(object->name());
551
552 if (object->name() == "star")
553 {
554 StarObject *s = dynamic_cast<StarObject *>(object);
555
556 if (s->getHDIndex() != 0)
557 finalObjectName = QString("HD %1").arg(s->getHDIndex());
558 }
559
560 nameEdit->setText(finalObjectName);
561 raBox->show(object->ra0());
562 decBox->show(object->dec0());
563
564 setDirty();
565 }
566}
567
569{
570 auto url = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select FITS/XISF Image"), dirPath,
571 "FITS (*.fits *.fit);;XISF (*.xisf)");
572 if (url.isEmpty())
573 return;
574
575 processFITSSelection(url);
576}
577
578void Scheduler::processFITSSelection(const QUrl &url)
579{
580 if (url.isEmpty())
581 return;
582
583 fitsURL = url;
584 dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename));
585 fitsEdit->setText(fitsURL.toLocalFile());
586 setDirty();
587
588 const QString filename = fitsEdit->text();
589 int status = 0;
590 double ra = 0, dec = 0;
591 dms raDMS, deDMS;
592 char comment[128], error_status[512];
593 fitsfile *fptr = nullptr;
594
595 if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status))
596 {
597 fits_report_error(stderr, status);
600 return;
601 }
602
603 status = 0;
604 if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status))
605 {
606 fits_report_error(stderr, status);
609 return;
610 }
611
612 status = 0;
613 char objectra_str[32] = {0};
614 if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status))
615 {
616 if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status))
617 {
618 fits_report_error(stderr, status);
620 process()->appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status)));
621 return;
622 }
623
624 raDMS.setD(ra);
625 }
626 else
627 {
629 }
630
631 status = 0;
632 char objectde_str[32] = {0};
633 if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status))
634 {
635 if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status))
636 {
637 fits_report_error(stderr, status);
639 process()->appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status)));
640 return;
641 }
642
643 deDMS.setD(dec);
644 }
645 else
646 {
648 }
649
650 raBox->show(raDMS);
651 decBox->show(deDMS);
652
653 char object_str[256] = {0};
654 if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status))
655 {
656 QFileInfo info(filename);
657 nameEdit->setText(info.completeBaseName());
658 }
659 else
660 {
661 nameEdit->setText(object_str);
662 }
663}
664
666{
668
669 if (sequenceFileURL.isEmpty())
670 return;
671 dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename));
672
673 sequenceEdit->setText(sequenceURL.toLocalFile());
674
675 setDirty();
676}
677
679{
680 QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"),
681 dirPath.toLocalFile(),
682 i18n("Ekos Sequence Queue (*.esq)"));
683
684 setSequence(file);
685}
686
688{
689 moduleState()->setStartupScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window",
690 "Select Startup Script"),
691 dirPath,
692 i18n("Script (*)")));
693 if (moduleState()->startupScriptURL().isEmpty())
694 return;
695
696 dirPath = QUrl(moduleState()->startupScriptURL().url(QUrl::RemoveFilename));
697
698 moduleState()->setDirty(true);
699 schedulerStartupScript->setText(moduleState()->startupScriptURL().toLocalFile());
700}
701
703{
704 moduleState()->setShutdownScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window",
705 "Select Shutdown Script"),
706 dirPath,
707 i18n("Script (*)")));
708 if (moduleState()->shutdownScriptURL().isEmpty())
709 return;
710
711 dirPath = QUrl(moduleState()->shutdownScriptURL().url(QUrl::RemoveFilename));
712
713 moduleState()->setDirty(true);
714 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toLocalFile());
715}
716
717void Scheduler::addJob(SchedulerJob *job)
718{
719 if (0 <= jobUnderEdit)
720 {
721 // select the job currently being edited
722 job = moduleState()->jobs().at(jobUnderEdit);
723 // if existing, save it
724 if (job != nullptr)
725 saveJob(job);
726 // in any case, reset editing
727 resetJobEdit();
728 }
729 else
730 {
731 // remember the number of rows to select the first one appended
732 int currentRow = moduleState()->currentPosition();
733
734 //If no row is selected, the job will be appended at the end of the list, otherwise below the current selection
735 if (currentRow < 0)
736 currentRow = queueTable->rowCount();
737 else
738 currentRow++;
739
740 /* If a job is being added, save fields into a new job */
741 saveJob(job);
742
743 // select the first appended row (if any was added)
744 if (moduleState()->jobs().count() > currentRow)
745 moduleState()->setCurrentPosition(currentRow);
746 }
747
748 emit jobsUpdated(moduleState()->getJSONJobs());
749}
750
751bool Scheduler::fillJobFromUI(SchedulerJob *job)
752{
753 if (nameEdit->text().isEmpty())
754 {
755 process()->appendLogText(i18n("Warning: Target name is required."));
756 return false;
757 }
758
759 if (sequenceEdit->text().isEmpty())
760 {
761 process()->appendLogText(i18n("Warning: Sequence file is required."));
762 return false;
763 }
764
765 // Coordinates are required unless it is a FITS file
766 if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty())
767 {
768 process()->appendLogText(i18n("Warning: Target coordinates are required."));
769 return false;
770 }
771
772 bool raOk = false, decOk = false;
773 dms /*const*/ ra(raBox->createDms(&raOk));
774 dms /*const*/ dec(decBox->createDms(&decOk));
775
776 if (raOk == false)
777 {
778 process()->appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text()));
779 return false;
780 }
781
782 if (decOk == false)
783 {
784 process()->appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text()));
785 return false;
786 }
787
788 /* Configure or reconfigure the observation job */
789 fitsURL = QUrl::fromLocalFile(fitsEdit->text());
790
791 // Get several job values depending on the state of the UI.
792
794 if (asapConditionR->isChecked())
795 startCondition = START_ASAP;
796
798 if (schedulerCompleteSequences->isChecked())
799 stopCondition = FINISH_SEQUENCE;
800 else if (schedulerRepeatSequences->isChecked())
801 stopCondition = FINISH_REPEAT;
802 else if (schedulerUntilTerminated->isChecked())
803 stopCondition = FINISH_LOOP;
804
805 double altConstraint = SchedulerJob::UNDEFINED_ALTITUDE;
806 if (schedulerAltitude->isChecked())
808
809 double moonConstraint = -1;
810 if (schedulerMoonSeparation->isChecked())
812
813 // The reason for this kitchen-sink function is to separate the UI from the
814 // job setup, to allow for testing.
815 SchedulerUtils::setupJob(*job, nameEdit->text(), groupEdit->text(), ra, dec,
816 KStarsData::Instance()->ut().djd(),
817 positionAngleSpin->value(), sequenceURL, fitsURL,
818
819 startCondition, startupTimeEdit->dateTime(),
821
824 schedulerWeather->isChecked(),
825 schedulerTwilight->isChecked(),
826 schedulerHorizon->isChecked(),
827
828 schedulerTrackStep->isChecked(),
829 schedulerFocusStep->isChecked(),
830 schedulerAlignStep->isChecked(),
831 schedulerGuideStep->isChecked());
832
833 // success
834 updateJobTable(job);
835 return true;
836}
837
838void Scheduler::saveJob(SchedulerJob *job)
839{
840 watchJobChanges(false);
841
842 /* Create or Update a scheduler job, append below current selection */
843 int currentRow = moduleState()->currentPosition() + 1;
844
845 /* Add job to queue only if it is new, else reuse current row.
846 * Make sure job is added at the right index, now that queueTable may have a line selected without being edited.
847 */
848 if (0 <= jobUnderEdit)
849 {
850 /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from moduleState()->currentPosition(). */
851 if (jobUnderEdit != currentRow - 1)
852 {
853 qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table.";
854 }
855
856 /* Use the job in the row currently edited */
857 job = moduleState()->jobs().at(jobUnderEdit);
858 // try to fill the job from the UI and exit if it fails
859 if (fillJobFromUI(job) == false)
860 {
861 watchJobChanges(true);
862 return;
863 }
864 }
865 else
866 {
867 if (job == nullptr)
868 {
869 /* 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. */
870 job = new SchedulerJob();
871 // try to fill the job from the UI and exit if it fails
872 if (fillJobFromUI(job) == false)
873 {
874 delete(job);
875 watchJobChanges(true);
876 return;
877 }
878 }
879 /* Insert the job in the job list and add a row in the table for it just after the row currently selected. */
880 moduleState()->mutlableJobs().insert(currentRow, job);
881 insertJobTableRow(currentRow);
882 }
883
884 /* Verifications */
885 // Warn user if a duplicated job is in the list - same target, same sequence
886 // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list!
887 int numWarnings = 0;
888 foreach (SchedulerJob *a_job, moduleState()->jobs())
889 {
890 if (a_job == job)
891 {
892 break;
893 }
894 else if (a_job->getName() == job->getName())
895 {
896 int const a_job_row = moduleState()->jobs().indexOf(a_job);
897
898 /* 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. */
899 process()->appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, "
900 "the scheduler may consider the same storage for captures.",
901 job->getName(), currentRow, a_job_row));
902
903 /* Warn the user in case the two jobs are really identical */
904 if (a_job->getSequenceFile() == job->getSequenceFile())
905 {
906 if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress())
907 process()->appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count "
908 "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')",
909 job->getName(), currentRow, a_job_row, job->getRepeatsRequired()));
910 }
911
912 // Don't need to warn over and over.
913 if (++numWarnings >= 1)
914 {
915 process()->appendLogText(i18n("Skipped checking for duplicates."));
916 break;
917 }
918 }
919 }
920
921 updateJobTable(job);
922
923 /* We just added or saved a job, so we have a job in the list - enable relevant buttons */
924 queueSaveAsB->setEnabled(true);
925 queueSaveB->setEnabled(true);
926 startB->setEnabled(true);
927 evaluateOnlyB->setEnabled(true);
928 setJobManipulation(true, true);
929 checkJobInputComplete();
930
931 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1);
932
933 watchJobChanges(true);
934
935 if (SCHEDULER_LOADING != moduleState()->schedulerState())
936 {
937 process()->evaluateJobs(true);
938 }
939}
940
941void Scheduler::syncGUIToJob(SchedulerJob *job)
942{
943 nameEdit->setText(job->getName());
944 groupEdit->setText(job->getGroup());
945
946 raBox->show(job->getTargetCoords().ra0());
947 decBox->show(job->getTargetCoords().dec0());
948
949 // fitsURL/sequenceURL are not part of UI, but the UI serves as model, so keep them here for now
950 fitsURL = job->getFITSFile().isEmpty() ? QUrl() : job->getFITSFile();
951 sequenceURL = job->getSequenceFile();
952 fitsEdit->setText(fitsURL.toLocalFile());
953 sequenceEdit->setText(sequenceURL.toLocalFile());
954
955 positionAngleSpin->setValue(job->getPositionAngle());
956
957 schedulerTrackStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK);
958 schedulerFocusStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS);
959 schedulerAlignStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN);
960 schedulerGuideStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE);
961
962 switch (job->getFileStartupCondition())
963 {
964 case START_ASAP:
965 asapConditionR->setChecked(true);
966 break;
967
968 case START_AT:
969 startupTimeConditionR->setChecked(true);
970 startupTimeEdit->setDateTime(job->getStartupTime());
971 break;
972 }
973
974 if (job->getMinAltitude())
975 {
976 schedulerAltitude->setChecked(true);
977 schedulerAltitudeValue->setValue(job->getMinAltitude());
978 }
979 else
980 {
981 schedulerAltitude->setChecked(false);
982 schedulerAltitudeValue->setValue(DEFAULT_MIN_ALTITUDE);
983 }
984
985 if (job->getMinMoonSeparation() >= 0)
986 {
987 schedulerMoonSeparation->setChecked(true);
988 schedulerMoonSeparationValue->setValue(job->getMinMoonSeparation());
989 }
990 else
991 {
992 schedulerMoonSeparation->setChecked(false);
993 schedulerMoonSeparationValue->setValue(DEFAULT_MIN_MOON_SEPARATION);
994 }
995
996 schedulerWeather->setChecked(job->getEnforceWeather());
997
998 schedulerTwilight->blockSignals(true);
999 schedulerTwilight->setChecked(job->getEnforceTwilight());
1000 schedulerTwilight->blockSignals(false);
1001
1002 schedulerHorizon->blockSignals(true);
1003 schedulerHorizon->setChecked(job->getEnforceArtificialHorizon());
1004 schedulerHorizon->blockSignals(false);
1005
1006 switch (job->getCompletionCondition())
1007 {
1008 case FINISH_SEQUENCE:
1009 schedulerCompleteSequences->setChecked(true);
1010 break;
1011
1012 case FINISH_REPEAT:
1013 schedulerRepeatSequences->setChecked(true);
1014 schedulerExecutionSequencesLimit->setValue(job->getRepeatsRequired());
1015 break;
1016
1017 case FINISH_LOOP:
1018 schedulerUntilTerminated->setChecked(true);
1019 break;
1020
1021 case FINISH_AT:
1022 schedulerUntil->setChecked(true);
1023 schedulerUntilValue->setDateTime(job->getCompletionTime());
1024 break;
1025 }
1026
1027 updateNightTime(job);
1028
1029 setJobManipulation(true, true);
1030}
1031
1033{
1034 schedulerParkDome->setChecked(Options::schedulerParkDome());
1035 schedulerParkMount->setChecked(Options::schedulerParkMount());
1036 schedulerCloseDustCover->setChecked(Options::schedulerCloseDustCover());
1037 schedulerWarmCCD->setChecked(Options::schedulerWarmCCD());
1038 schedulerUnparkDome->setChecked(Options::schedulerUnparkDome());
1039 schedulerUnparkMount->setChecked(Options::schedulerUnparkMount());
1040 schedulerOpenDustCover->setChecked(Options::schedulerOpenDustCover());
1041 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy()));
1042 errorHandlingStrategyDelay->setValue(Options::errorHandlingStrategyDelay());
1043 errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors());
1044 schedulerStartupScript->setText(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile));
1045 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile));
1046
1047 if (process()->captureInterface() != nullptr)
1048 {
1049 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl");
1050 if (hasCoolerControl.isValid())
1051 {
1052 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool());
1053 moduleState()->setCaptureReady(true);
1054 }
1055 }
1056}
1057
1058void Scheduler::updateNightTime(SchedulerJob const *job)
1059{
1060 // select job from current position
1061 if (job == nullptr && moduleState()->jobs().size() > 0)
1062 {
1063 int const currentRow = moduleState()->currentPosition();
1064 if (0 <= currentRow && currentRow < moduleState()->jobs().size())
1065 job = moduleState()->jobs().at(currentRow);
1066
1067 if (job == nullptr)
1068 {
1069 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Cannot update night time, no matching job found at line" << currentRow;
1070 return;
1071 }
1072 }
1073
1074 QDateTime const dawn = job ? job->getDawnAstronomicalTwilight() : moduleState()->Dawn();
1075 QDateTime const dusk = job ? job->getDuskAstronomicalTwilight() : moduleState()->Dusk();
1076
1077 QChar const warning(dawn == dusk ? 0x26A0 : '-');
1078 nightTime->setText(i18n("%1 %2 %3", dusk.toString("hh:mm"), warning, dawn.toString("hh:mm")));
1079}
1080
1082{
1083 if (jobUnderEdit == i.row())
1084 return;
1085
1086 SchedulerJob * const job = moduleState()->jobs().at(i.row());
1087
1088 if (job == nullptr)
1089 return;
1090
1091 watchJobChanges(false);
1092
1093 //job->setState(SCHEDJOB_IDLE);
1094 //job->setStage(SCHEDSTAGE_IDLE);
1095 syncGUIToJob(job);
1096
1097 /* Turn the add button into an apply button */
1098 setJobAddApply(false);
1099
1100 /* Disable scheduler start/evaluate buttons */
1101 startB->setEnabled(false);
1102 evaluateOnlyB->setEnabled(false);
1103
1104 /* Don't let the end-user remove a job being edited */
1105 setJobManipulation(false, false);
1106
1107 jobUnderEdit = i.row();
1108 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(
1109 jobUnderEdit + 1);
1110
1111 watchJobChanges(true);
1112}
1113
1115{
1116 schedulerURL = QUrl::fromLocalFile(fileURL);
1117 // update save button tool tip
1118 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
1119}
1120
1122{
1124
1125
1126 if (jobChangesAreWatched == false || selected.empty())
1127 // || (current.row() + 1) > moduleState()->jobs().size())
1128 return;
1129
1130 const QModelIndex current = selected.indexes().first();
1131 // this should not happen, but avoids crashes
1132 if ((current.row() + 1) > moduleState()->jobs().size())
1133 {
1134 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Unexpected row number" << current.row() << "- ignoring.";
1135 return;
1136 }
1137 moduleState()->setCurrentPosition(current.row());
1138 SchedulerJob * const job = moduleState()->jobs().at(current.row());
1139
1140 if (job != nullptr)
1141 {
1142 if (jobUnderEdit < 0)
1143 syncGUIToJob(job);
1144 else if (jobUnderEdit != current.row())
1145 {
1146 // avoid changing the UI values for the currently edited job
1147 process()->appendLogText(i18n("Stop editing of job #%1, resetting to original value.", jobUnderEdit + 1));
1148 resetJobEdit();
1149 syncGUIToJob(job);
1150 }
1151 }
1152 else nightTime->setText("-");
1153}
1154
1156{
1157 setJobManipulation(index.isValid(), index.isValid());
1158}
1159
1161{
1162 if (add_mode)
1163 {
1164 addToQueueB->setIcon(QIcon::fromTheme("list-add"));
1165 addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list."));
1167 }
1168 else
1169 {
1170 addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply"));
1171 addToQueueB->setToolTip(i18n("Apply job changes."));
1172 }
1173 // check if the button should be enabled
1174 checkJobInputComplete();
1175}
1176
1178{
1179 if (can_reorder)
1180 {
1181 int const currentRow = moduleState()->currentPosition();
1182 queueUpB->setEnabled(0 < currentRow);
1183 queueDownB->setEnabled(currentRow < queueTable->rowCount() - 1);
1184 }
1185 else
1186 {
1187 queueUpB->setEnabled(false);
1188 queueDownB->setEnabled(false);
1189 }
1190 sortJobsB->setEnabled(can_reorder);
1191 removeFromQueueB->setEnabled(can_delete);
1192}
1193
1195{
1196 /* Add jobs not reordered at the end of the list, in initial order */
1197 foreach (SchedulerJob* job, moduleState()->jobs())
1198 if (!reordered_sublist.contains(job))
1199 reordered_sublist.append(job);
1200
1201 if (moduleState()->jobs() != reordered_sublist)
1202 {
1203 /* Remember job currently selected */
1204 int const selectedRow = moduleState()->currentPosition();
1205 SchedulerJob * const selectedJob = 0 <= selectedRow ? moduleState()->jobs().at(selectedRow) : nullptr;
1206
1207 /* Reassign list */
1208 moduleState()->setJobs(reordered_sublist);
1209
1210 /* Refresh the table */
1211 for (SchedulerJob *job : moduleState()->jobs())
1212 updateJobTable(job);
1213
1214 /* Reselect previously selected job */
1215 if (nullptr != selectedJob)
1216 moduleState()->setCurrentPosition(moduleState()->jobs().indexOf(selectedJob));
1217
1218 return true;
1219 }
1220 else return false;
1221}
1222
1224{
1225 int const rowCount = queueTable->rowCount();
1226 int const currentRow = queueTable->currentRow();
1227 int const destinationRow = currentRow - 1;
1228
1229 /* No move if no job selected, if table has one line or less or if destination is out of table */
1230 if (currentRow < 0 || rowCount <= 1 || destinationRow < 0)
1231 return;
1232
1233 /* Swap jobs in the list */
1234#if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1235 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow);
1236#else
1237 moduleState()->jobs().swap(currentRow, destinationRow);
1238#endif
1239
1240 //Update the two table rows
1241 updateJobTable(moduleState()->jobs().at(currentRow));
1242 updateJobTable(moduleState()->jobs().at(destinationRow));
1243
1244 /* Move selection to destination row */
1245 moduleState()->setCurrentPosition(destinationRow);
1246 setJobManipulation(true, true);
1247
1248 /* Make list modified and evaluate jobs */
1249 moduleState()->setDirty(true);
1250 process()->evaluateJobs(true);
1251}
1252
1254{
1255 int const rowCount = queueTable->rowCount();
1256 int const currentRow = queueTable->currentRow();
1257 int const destinationRow = currentRow + 1;
1258
1259 /* No move if no job selected, if table has one line or less or if destination is out of table */
1260 if (currentRow < 0 || rowCount <= 1 || destinationRow >= rowCount)
1261 return;
1262
1263 /* Swap jobs in the list */
1264#if QT_VERSION >= QT_VERSION_CHECK(5,13,0)
1265 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow);
1266#else
1267 moduleState()->mutlableJobs().swap(currentRow, destinationRow);
1268#endif
1269
1270 //Update the two table rows
1271 updateJobTable(moduleState()->jobs().at(currentRow));
1272 updateJobTable(moduleState()->jobs().at(destinationRow));
1273
1274 /* Move selection to destination row */
1275 moduleState()->setCurrentPosition(destinationRow);
1276 setJobManipulation(true, true);
1277
1278 /* Make list modified and evaluate jobs */
1279 moduleState()->setDirty(true);
1280 process()->evaluateJobs(true);
1281}
1282
1283void Scheduler::updateJobTable(SchedulerJob *job)
1284{
1285 // handle full table update
1286 if (job == nullptr)
1287 {
1288 for (auto onejob : moduleState()->jobs())
1290
1291 return;
1292 }
1293
1294 const int row = moduleState()->jobs().indexOf(job);
1295 // Ignore unknown jobs
1296 if (row < 0)
1297 return;
1298 // ensure that the row in the table exists
1299 if (row >= queueTable->rowCount())
1300 insertJobTableRow(row - 1, false);
1301
1302 QTableWidgetItem *nameCell = queueTable->item(row, static_cast<int>(SCHEDCOL_NAME));
1303 QTableWidgetItem *statusCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS));
1304 QTableWidgetItem *altitudeCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ALTITUDE));
1305 QTableWidgetItem *startupCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME));
1306 QTableWidgetItem *completionCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME));
1307 QTableWidgetItem *captureCountCell = queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES));
1308
1309 // Only in testing.
1310 if (!nameCell) return;
1311
1312 if (nullptr != nameCell)
1313 {
1314 nameCell->setText(job->getName());
1316 if (nullptr != nameCell->tableWidget())
1317 nameCell->tableWidget()->resizeColumnToContents(nameCell->column());
1318 }
1319
1320 if (nullptr != statusCell)
1321 {
1324 if (stateStrings.isEmpty())
1325 {
1326 stateStrings[SCHEDJOB_IDLE] = i18n("Idle");
1327 stateStrings[SCHEDJOB_EVALUATION] = i18n("Evaluating");
1328 stateStrings[SCHEDJOB_SCHEDULED] = i18n("Scheduled");
1329 stateStrings[SCHEDJOB_BUSY] = i18n("Running");
1330 stateStrings[SCHEDJOB_INVALID] = i18n("Invalid");
1331 stateStrings[SCHEDJOB_COMPLETE] = i18n("Complete");
1332 stateStrings[SCHEDJOB_ABORTED] = i18n("Aborted");
1333 stateStrings[SCHEDJOB_ERROR] = i18n("Error");
1334 stateStringUnknown = i18n("Unknown");
1335 }
1336 statusCell->setText(stateStrings.value(job->getState(), stateStringUnknown));
1338
1339 if (nullptr != statusCell->tableWidget())
1340 statusCell->tableWidget()->resizeColumnToContents(statusCell->column());
1341 }
1342
1343 if (nullptr != startupCell)
1344 {
1345 auto time = (job->getState() == SCHEDJOB_BUSY) ? job->getStateTime() : job->getStartupTime();
1346 /* Display startup time if it is valid */
1347 if (time.isValid())
1348 {
1349 startupCell->setText(QString("%1%2%L3° %4")
1350 .arg(job->getAltitudeAtStartup() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "")
1351 .arg(QChar(job->isSettingAtStartup() ? 0x2193 : 0x2191))
1352 .arg(job->getAltitudeAtStartup(), 0, 'f', 1)
1353 .arg(time.toString(startupTimeEdit->displayFormat())));
1354
1355 switch (job->getFileStartupCondition())
1356 {
1357 /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */
1358 case START_AT:
1359 startupCell->setIcon(QIcon::fromTheme("chronometer"));
1360 break;
1361
1362 /* If the original condition is START_ASAP, startup time is informational */
1363 case START_ASAP:
1364 startupCell->setIcon(QIcon());
1365 break;
1366
1367 default:
1368 break;
1369 }
1370 }
1371 /* Else do not display any startup time */
1372 else
1373 {
1374 startupCell->setText("-");
1375 startupCell->setIcon(QIcon());
1376 }
1377
1379
1380 if (nullptr != startupCell->tableWidget())
1381 startupCell->tableWidget()->resizeColumnToContents(startupCell->column());
1382 }
1383
1384 if (nullptr != altitudeCell)
1385 {
1386 // FIXME: Cache altitude calculations
1387 bool is_setting = false;
1388 double const alt = SchedulerUtils::findAltitude(job->getTargetCoords(), QDateTime(), &is_setting);
1389
1390 altitudeCell->setText(QString("%1%L2°")
1391 .arg(QChar(is_setting ? 0x2193 : 0x2191))
1392 .arg(alt, 0, 'f', 1));
1394
1395 if (nullptr != altitudeCell->tableWidget())
1396 altitudeCell->tableWidget()->resizeColumnToContents(altitudeCell->column());
1397 }
1398
1399 if (nullptr != completionCell)
1400 {
1401 if (job->getGreedyCompletionTime().isValid())
1402 {
1403 completionCell->setText(QString("%1")
1404 .arg(job->getGreedyCompletionTime().toString("hh:mm")));
1405 }
1406 else
1407 /* Display completion time if it is valid and job is not looping */
1408 if (FINISH_LOOP != job->getCompletionCondition() && job->getCompletionTime().isValid())
1409 {
1410 completionCell->setText(QString("%1%2%L3° %4")
1411 .arg(job->getAltitudeAtCompletion() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "")
1412 .arg(QChar(job->isSettingAtCompletion() ? 0x2193 : 0x2191))
1413 .arg(job->getAltitudeAtCompletion(), 0, 'f', 1)
1414 .arg(job->getCompletionTime().toString(startupTimeEdit->displayFormat())));
1415
1416 switch (job->getCompletionCondition())
1417 {
1418 case FINISH_AT:
1419 completionCell->setIcon(QIcon::fromTheme("chronometer"));
1420 break;
1421
1422 case FINISH_SEQUENCE:
1423 case FINISH_REPEAT:
1424 default:
1425 completionCell->setIcon(QIcon());
1426 break;
1427 }
1428 }
1429 /* Else do not display any completion time */
1430 else
1431 {
1432 completionCell->setText("-");
1433 completionCell->setIcon(QIcon());
1434 }
1435
1437 if (nullptr != completionCell->tableWidget())
1438 completionCell->tableWidget()->resizeColumnToContents(completionCell->column());
1439 }
1440
1441 if (nullptr != captureCountCell)
1442 {
1443 switch (job->getCompletionCondition())
1444 {
1445 case FINISH_AT:
1446 // FIXME: Attempt to calculate the number of frames until end - requires detailed imaging time
1447
1448 case FINISH_LOOP:
1449 // If looping, display the count of completed frames
1450 captureCountCell->setText(QString("%L1/-").arg(job->getCompletedCount()));
1451 break;
1452
1453 case FINISH_SEQUENCE:
1454 case FINISH_REPEAT:
1455 default:
1456 // If repeating, display the count of completed frames to the count of requested frames
1457 captureCountCell->setText(QString("%L1/%L2").arg(job->getCompletedCount()).arg(job->getSequenceCount()));
1458 break;
1459 }
1460
1461 QString tooltip = job->getProgressSummary();
1462 if (tooltip.size() == 0) tooltip = CAPTURE_COUNT_TOOLTIP;
1463 captureCountCell->setToolTip(tooltip);
1464
1466 if (nullptr != captureCountCell->tableWidget())
1467 captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column());
1468 }
1469
1470 m_JobUpdateDebounce.start();
1471}
1472
1473void Scheduler::insertJobTableRow(int row, bool above)
1474{
1475 const int pos = above ? row : row + 1;
1476
1477 // ensure that there are no gaps
1478 if (row > queueTable->rowCount())
1479 insertJobTableRow(row - 1, above);
1480
1481 queueTable->insertRow(pos);
1482
1484 queueTable->setItem(row, static_cast<int>(SCHEDCOL_NAME), nameCell);
1485 nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1487
1489 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STATUS), statusCell);
1490 statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1492
1493 QTableWidgetItem *captureCount = new QTableWidgetItem();
1494 queueTable->setItem(row, static_cast<int>(SCHEDCOL_CAPTURES), captureCount);
1497
1499 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STARTTIME), startupCell);
1500 startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1502
1504 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ALTITUDE), altitudeCell);
1505 altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
1507
1509 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ENDTIME), completionCell);
1512}
1513
1514void Scheduler::updateCellStyle(SchedulerJob *job, QTableWidgetItem *cell)
1515{
1516 QFont font(cell->font());
1517 font.setBold(job->getState() == SCHEDJOB_BUSY);
1518 font.setItalic(job->getState() == SCHEDJOB_BUSY);
1519 cell->setFont(font);
1520}
1521
1522void Scheduler::resetJobEdit()
1523{
1524 if (jobUnderEdit < 0)
1525 return;
1526
1527 SchedulerJob * const job = moduleState()->jobs().at(jobUnderEdit);
1528 Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid");
1529
1530 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(
1531 jobUnderEdit + 1);
1532
1533 jobUnderEdit = -1;
1534
1535 watchJobChanges(false);
1536
1537 /* Revert apply button to add */
1538 setJobAddApply(true);
1539
1540 /* Refresh state of job manipulation buttons */
1541 setJobManipulation(true, true);
1542
1543 /* Restore scheduler operation buttons */
1544 evaluateOnlyB->setEnabled(true);
1545 startB->setEnabled(true);
1546
1547 watchJobChanges(true);
1548 Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode");
1549}
1550
1552{
1553 int currentRow = moduleState()->currentPosition();
1554
1555 watchJobChanges(false);
1556 if (moduleState()->removeJob(currentRow) == false)
1557 return;
1558
1559 /* removing the job succeeded, update UI */
1560 /* Remove the job from the table */
1561 queueTable->removeRow(currentRow);
1562
1563 /* If there are no job rows left, update UI buttons */
1564 if (queueTable->rowCount() == 0)
1565 {
1566 setJobManipulation(false, false);
1567 evaluateOnlyB->setEnabled(false);
1568 queueSaveAsB->setEnabled(false);
1569 queueSaveB->setEnabled(false);
1570 startB->setEnabled(false);
1571 pauseB->setEnabled(false);
1572 }
1573
1574 // Otherwise, clear the selection, leave the UI values holding the values of the removed job.
1575 // The position in the job list, where the job has been removed from, is still held in the module state.
1576 // This leaves the option directly adding the old values reverting the deletion.
1577 else
1578 queueTable->clearSelection();
1579
1580 /* If needed, reset edit mode to clean up UI */
1581 if (jobUnderEdit >= 0)
1582 resetJobEdit();
1583
1584 watchJobChanges(true);
1585 process()->evaluateJobs(true);
1587 // disable moving and deleting, since selection is cleared
1588 setJobManipulation(false, false);
1589}
1590
1592{
1593 moduleState()->setCurrentPosition(index);
1594 removeJob();
1595}
1596void Scheduler::toggleScheduler()
1597{
1598 if (moduleState()->schedulerState() == SCHEDULER_RUNNING)
1599 {
1600 moduleState()->disablePreemptiveShutdown();
1601 process()->stop();
1602 }
1603 else
1604 process()->start();
1605}
1606
1607void Scheduler::pause()
1608{
1609 moduleState()->setSchedulerState(SCHEDULER_PAUSED);
1610 process()->appendLogText(i18n("Scheduler pause planned..."));
1611 pauseB->setEnabled(false);
1612
1613 startB->setIcon(QIcon::fromTheme("media-playback-start"));
1614 startB->setToolTip(i18n("Resume Scheduler"));
1615}
1616
1617void Scheduler::syncGreedyParams()
1618{
1619 process()->getGreedyScheduler()->setParams(
1625}
1626
1627void Scheduler::handleShutdownStarted()
1628{
1629 KSNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process"),
1630 KSNotification::Scheduler);
1631 weatherLabel->hide();
1632}
1633
1634void Ekos::Scheduler::changeSleepLabel(QString text, bool show)
1635{
1636 sleepLabel->setToolTip(text);
1637 if (show)
1638 sleepLabel->show();
1639 else
1640 sleepLabel->hide();
1641}
1642
1644{
1645 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_NOTHING).toLatin1().data());
1646
1647 // Update job table rows for aborted ones (the others remain unchanged in their state)
1648 bool wasAborted = false;
1649 for (auto &oneJob : moduleState()->jobs())
1650 {
1651 if (oneJob->getState() == SCHEDJOB_ABORTED)
1652 {
1654 wasAborted = true;
1655 }
1656 }
1657
1658 if (wasAborted)
1659 KSNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted."), KSNotification::Scheduler,
1660 KSNotification::Alert);
1661
1662 startupB->setEnabled(true);
1663 shutdownB->setEnabled(true);
1664
1665 // If soft shutdown, we return for now
1666 if (moduleState()->preemptiveShutdown())
1667 {
1668 changeSleepLabel(i18n("Scheduler is in shutdown until next job is ready"));
1669 pi->stopAnimation();
1670 return;
1671 }
1672
1673 changeSleepLabel("", false);
1674
1675 startB->setIcon(QIcon::fromTheme("media-playback-start"));
1676 startB->setToolTip(i18n("Start Scheduler"));
1677 pauseB->setEnabled(false);
1678 //startB->setText("Start Scheduler");
1679
1680 queueLoadB->setEnabled(true);
1681 queueAppendB->setEnabled(true);
1682 addToQueueB->setEnabled(true);
1683 setJobManipulation(false, false);
1684 //mosaicB->setEnabled(true);
1685 evaluateOnlyB->setEnabled(true);
1686}
1687
1688
1689bool Scheduler::loadFile(const QUrl &path)
1690{
1691 return load(true, path.toLocalFile());
1692}
1693
1694bool Scheduler::load(bool clearQueue, const QString &filename)
1695{
1696 QUrl fileURL;
1697
1698 if (filename.isEmpty())
1699 fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Scheduler List"),
1700 dirPath,
1701 "Ekos Scheduler List (*.esl)");
1702 else
1703 fileURL = QUrl::fromLocalFile(filename);
1704
1705 if (fileURL.isEmpty())
1706 return false;
1707
1708 if (fileURL.isValid() == false)
1709 {
1710 QString message = i18n("Invalid URL: %1", fileURL.toLocalFile());
1711 KSNotification::sorry(message, i18n("Invalid URL"));
1712 return false;
1713 }
1714
1715 dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
1716
1717 if (clearQueue)
1718 process()->removeAllJobs();
1719 // remember toe number of rows to select the first one appended
1720 const int row = moduleState()->jobs().count();
1721
1722 // do not update while appending
1723 watchJobChanges(false);
1724 // try appending the jobs from the file to the job list
1725 const bool success = process()->appendEkosScheduleList(fileURL.toLocalFile());
1726 // turn on whatching
1727 watchJobChanges(true);
1728
1729 if (success)
1730 {
1731 // select the first appended row (if any was added)
1732 if (moduleState()->jobs().count() > row)
1733 moduleState()->setCurrentPosition(row);
1734
1735 /* Run a job idle evaluation after a successful load */
1736 process()->startJobEvaluation();
1737
1738 return true;
1739 }
1740
1741 return false;
1742}
1743
1745{
1746 if (jobUnderEdit >= 0)
1747 resetJobEdit();
1748
1749 while (queueTable->rowCount() > 0)
1750 queueTable->removeRow(0);
1751}
1752
1754{
1755 process()->clearLog();
1756}
1757
1758void Scheduler::saveAs()
1759{
1760 schedulerURL.clear();
1761 save();
1762}
1763
1764bool Scheduler::saveFile(const QUrl &path)
1765{
1766 QUrl backupCurrent = schedulerURL;
1767 schedulerURL = path;
1768
1769 if (save())
1770 return true;
1771 else
1772 {
1773 schedulerURL = backupCurrent;
1774 return false;
1775 }
1776}
1777
1778bool Scheduler::save()
1779{
1780 QUrl backupCurrent = schedulerURL;
1781
1782 if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp"))
1783 schedulerURL.clear();
1784
1785 // If no changes made, return.
1786 if (moduleState()->dirty() == false && !schedulerURL.isEmpty())
1787 return true;
1788
1789 if (schedulerURL.isEmpty())
1790 {
1791 schedulerURL =
1792 QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Scheduler List"), dirPath,
1793 "Ekos Scheduler List (*.esl)");
1794 // if user presses cancel
1795 if (schedulerURL.isEmpty())
1796 {
1797 schedulerURL = backupCurrent;
1798 return false;
1799 }
1800
1801 dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename));
1802
1803 if (schedulerURL.toLocalFile().contains('.') == 0)
1804 schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl");
1805 }
1806
1807 if (schedulerURL.isValid())
1808 {
1809 if ((process()->saveScheduler(schedulerURL)) == false)
1810 {
1811 KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save"));
1812 return false;
1813 }
1814
1815 // update save button tool tip
1816 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName());
1817 }
1818 else
1819 {
1820 QString message = i18n("Invalid URL: %1", schedulerURL.url());
1821 KSNotification::sorry(message, i18n("Invalid URL"));
1822 return false;
1823 }
1824
1825 return true;
1826}
1827
1828void Scheduler::checkJobInputComplete()
1829{
1830 // For object selection, all fields must be filled
1831 bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty();
1832
1833 // For FITS selection, only the name and fits URL should be filled.
1834 bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty();
1835
1836 // Sequence selection is required
1837 bool const seqSelectionOK = !sequenceEdit->text().isEmpty();
1838
1839 // Finally, adding is allowed upon object/FITS and sequence selection
1841
1842 addToQueueB->setEnabled(addingOK);
1843}
1844
1846{
1847 // check if all fields are filled to allow adding a job
1848 checkJobInputComplete();
1849
1850 // ignore changes that are a result of syncGUIToJob() or syncGUIToGeneralSettings()
1851 if (jobUnderEdit < 0)
1852 return;
1853
1854 moduleState()->setDirty(true);
1855
1857 return;
1858
1859 // update state
1861 moduleState()->setStartupScriptURL(QUrl::fromUserInput(schedulerStartupScript->text()));
1862 else if (sender() == schedulerShutdownScript)
1863 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(schedulerShutdownScript->text()));
1864}
1865
1867{
1868 // We require a first job to sort, so bail out if list is empty
1869 if (moduleState()->jobs().isEmpty())
1870 return;
1871
1872 // Don't reset current job
1873 // setCurrentJob(nullptr);
1874
1875 // Don't reset scheduler jobs startup times before sorting - we need the first job startup time
1876
1877 // Sort by startup time, using the first job time as reference for altitude calculations
1878 using namespace std::placeholders;
1879 QList<SchedulerJob*> sortedJobs = moduleState()->jobs();
1880 std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(),
1881 std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, moduleState()->jobs().first()->getStartupTime()));
1882
1883 // If order changed, reset and re-evaluate
1885 {
1886 for (SchedulerJob * job : moduleState()->jobs())
1887 job->reset();
1888
1889 process()->evaluateJobs(true);
1890 }
1891}
1892
1894{
1895 disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus);
1896 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data());
1897 moduleState()->setupNextIteration(RUN_SCHEDULER);
1898}
1899
1901{
1902 // The UI holds the state
1903 if (errorHandlingRestartQueueButton->isChecked())
1904 return ERROR_RESTART_AFTER_TERMINATION;
1905 else if (errorHandlingRestartImmediatelyButton->isChecked())
1906 return ERROR_RESTART_IMMEDIATELY;
1907 else
1908 return ERROR_DONT_RESTART;
1909}
1910
1912{
1913 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART);
1914
1915 switch (strategy)
1916 {
1917 case ERROR_RESTART_AFTER_TERMINATION:
1918 errorHandlingRestartQueueButton->setChecked(true);
1919 break;
1920 case ERROR_RESTART_IMMEDIATELY:
1921 errorHandlingRestartImmediatelyButton->setChecked(true);
1922 break;
1923 default:
1924 errorHandlingDontRestartButton->setChecked(true);
1925 break;
1926 }
1927}
1928
1929// Can't use a SchedulerAlgorithm type for the arg here
1930// as the compiler is unhappy connecting the signals currentIndexChanged(int)
1931// or activated(int) to an enum.
1932void Scheduler::setAlgorithm(int algIndex)
1933{
1934 if (algIndex != ALGORITHM_GREEDY)
1935 {
1936 process()->appendLogText(
1937 i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm."));
1938 algIndex = ALGORITHM_GREEDY;
1939 }
1940 Options::setSchedulerAlgorithm(algIndex);
1941
1942 groupLabel->setDisabled(false);
1943 groupEdit->setDisabled(false);
1944 queueTable->model()->setHeaderData(START_TIME_COLUMN, Qt::Horizontal, tr("Next Start"));
1945 queueTable->model()->setHeaderData(END_TIME_COLUMN, Qt::Horizontal, tr("Next End"));
1946 queueTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
1947}
1948
1950{
1951 if (enabled)
1952 return;
1953
1955 nullptr,
1956 i18n("Turning off astronomial twilight check may cause the observatory "
1957 "to run during daylight. This can cause irreversible damage to your equipment!"),
1958 i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
1959 "astronomical_twilight_warning") == KMessageBox::Cancel)
1960 {
1961 schedulerTwilight->setChecked(true);
1962 }
1963}
1964
1965void Scheduler::updateProfiles()
1966{
1967 schedulerProfileCombo->blockSignals(true);
1968 schedulerProfileCombo->clear();
1969 schedulerProfileCombo->addItems(moduleState()->profiles());
1970 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile());
1971 schedulerProfileCombo->blockSignals(false);
1972}
1973
1974void Scheduler::updateJobStageUI(SchedulerJobStage stage)
1975{
1976 /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */
1977 /* FIXME: this should work with a static initializer in C++11, but QT versions are touchy on this, and perhaps i18n can't be used? */
1980 if (stageStrings.isEmpty())
1981 {
1982 stageStrings[SCHEDSTAGE_IDLE] = i18n("Idle");
1983 stageStrings[SCHEDSTAGE_SLEWING] = i18n("Slewing");
1984 stageStrings[SCHEDSTAGE_SLEW_COMPLETE] = i18n("Slew complete");
1985 stageStrings[SCHEDSTAGE_FOCUSING] =
1986 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING] = i18n("Focusing");
1987 stageStrings[SCHEDSTAGE_FOCUS_COMPLETE] =
1988 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete");
1989 stageStrings[SCHEDSTAGE_ALIGNING] = i18n("Aligning");
1990 stageStrings[SCHEDSTAGE_ALIGN_COMPLETE] = i18n("Align complete");
1991 stageStrings[SCHEDSTAGE_RESLEWING] = i18n("Repositioning");
1992 stageStrings[SCHEDSTAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete");
1993 /*stageStrings[SCHEDSTAGE_CALIBRATING] = i18n("Calibrating");*/
1994 stageStrings[SCHEDSTAGE_GUIDING] = i18n("Guiding");
1995 stageStrings[SCHEDSTAGE_GUIDING_COMPLETE] = i18n("Guiding complete");
1996 stageStrings[SCHEDSTAGE_CAPTURING] = i18n("Capturing");
1997 stageStringUnknown = i18n("Unknown");
1998 }
1999
2000 if (activeJob() == nullptr)
2001 jobStatus->setText(stageStrings[SCHEDSTAGE_IDLE]);
2002 else
2003 jobStatus->setText(QString("%1: %2").arg(activeJob()->getName(),
2004 stageStrings.value(stage, stageStringUnknown)));
2005
2006}
2007
2009{
2010 if (iface == process()->mountInterface())
2011 {
2012 QVariant canMountPark = process()->mountInterface()->property("canPark");
2013 if (canMountPark.isValid())
2014 {
2015 schedulerUnparkMount->setEnabled(canMountPark.toBool());
2016 schedulerParkMount->setEnabled(canMountPark.toBool());
2017 }
2018 }
2019 else if (iface == process()->capInterface())
2020 {
2021 QVariant canCapPark = process()->capInterface()->property("canPark");
2022 if (canCapPark.isValid())
2023 {
2024 schedulerCloseDustCover->setEnabled(canCapPark.toBool());
2025 schedulerOpenDustCover->setEnabled(canCapPark.toBool());
2026 }
2027 else
2028 {
2029 schedulerCloseDustCover->setEnabled(false);
2030 schedulerOpenDustCover->setEnabled(false);
2031 }
2032 }
2033 else if (iface == process()->weatherInterface())
2034 {
2035 QVariant status = process()->weatherInterface()->property("status");
2036 if (status.isValid())
2037 {
2038 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt()));
2039 schedulerWeather->setEnabled(true);
2040 }
2041 else
2042 schedulerWeather->setEnabled(false);
2043 }
2044 else if (iface == process()->domeInterface())
2045 {
2046 QVariant canDomePark = process()->domeInterface()->property("canPark");
2047 if (canDomePark.isValid())
2048 {
2049 schedulerUnparkDome->setEnabled(canDomePark.toBool());
2050 schedulerParkDome->setEnabled(canDomePark.toBool());
2051 }
2052 }
2053 else if (iface == process()->captureInterface())
2054 {
2055 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl");
2056 if (hasCoolerControl.isValid())
2057 {
2058 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool());
2059 }
2060 }
2061}
2062
2063void Scheduler::setWeatherStatus(ISD::Weather::Status status)
2064{
2065 TEST_PRINT(stderr, "sch%d @@@setWeatherStatus(%d)\n", __LINE__, static_cast<int>(status));
2066 ISD::Weather::Status newStatus = status;
2067 QString statusString;
2068
2069 switch (newStatus)
2070 {
2071 case ISD::Weather::WEATHER_OK:
2072 statusString = i18n("Weather conditions are OK.");
2073 break;
2074
2075 case ISD::Weather::WEATHER_WARNING:
2076 statusString = i18n("Warning: weather conditions are in the WARNING zone.");
2077 break;
2078
2079 case ISD::Weather::WEATHER_ALERT:
2080 statusString = i18n("Caution: weather conditions are in the DANGER zone!");
2081 break;
2082
2083 default:
2084 break;
2085 }
2086
2087 if (newStatus != moduleState()->weatherStatus())
2088 {
2089 qCDebug(KSTARS_EKOS_SCHEDULER) << statusString;
2090
2091 if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_OK)
2092 weatherLabel->setPixmap(
2093 QIcon::fromTheme("security-high")
2094 .pixmap(QSize(32, 32)));
2095 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_WARNING)
2096 {
2097 weatherLabel->setPixmap(
2098 QIcon::fromTheme("security-medium")
2099 .pixmap(QSize(32, 32)));
2100 KSNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone"),
2101 KSNotification::Scheduler, KSNotification::Warn);
2102 }
2103 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT)
2104 {
2105 weatherLabel->setPixmap(
2106 QIcon::fromTheme("security-low")
2107 .pixmap(QSize(32, 32)));
2108 KSNotification::event(QLatin1String("WeatherAlert"),
2109 i18n("Weather conditions are critical. Observatory shutdown is imminent"), KSNotification::Scheduler,
2110 KSNotification::Alert);
2111 }
2112 else
2113 weatherLabel->setPixmap(QIcon::fromTheme("chronometer")
2114 .pixmap(QSize(32, 32)));
2115
2116 weatherLabel->show();
2117 weatherLabel->setToolTip(statusString);
2118
2119 process()->appendLogText(statusString);
2120
2121 emit weatherChanged(moduleState()->weatherStatus());
2122 }
2123}
2124
2125void Scheduler::handleSchedulerSleeping(bool shutdown, bool sleep)
2126{
2127 if (shutdown)
2128 {
2129 schedulerWeather->setEnabled(false);
2130 weatherLabel->hide();
2131 }
2132 if (sleep)
2133 changeSleepLabel(i18n("Scheduler is in sleep mode"));
2134}
2135
2137{
2138 switch (newState)
2139 {
2140 case SCHEDULER_RUNNING:
2141 /* Update UI to reflect startup */
2142 pi->startAnimation();
2143 sleepLabel->hide();
2144 startB->setIcon(QIcon::fromTheme("media-playback-stop"));
2145 startB->setToolTip(i18n("Stop Scheduler"));
2146 pauseB->setEnabled(true);
2147 pauseB->setChecked(false);
2148
2149 /* Disable edit-related buttons */
2150 queueLoadB->setEnabled(false);
2151 setJobManipulation(true, false);
2152 //mosaicB->setEnabled(false);
2153 evaluateOnlyB->setEnabled(false);
2154 startupB->setEnabled(false);
2155 shutdownB->setEnabled(false);
2156 break;
2157
2158 default:
2159 break;
2160 }
2161 // forward the state chqnge
2162 emit newStatus(newState);
2163}
2164
2166{
2167 pauseB->setCheckable(true);
2168 pauseB->setChecked(true);
2169}
2170
2171void Scheduler::handleJobsUpdated(QJsonArray jobsList)
2172{
2173 syncGreedyParams();
2175
2176 emit jobsUpdated(jobsList);
2177}
2178
2180{
2181 QScopedPointer<FramingAssistantUI> assistant(new FramingAssistantUI());
2182 return assistant->importMosaic(payload);
2183}
2184
2185void Scheduler::startupStateChanged(StartupState state)
2186{
2187 jobStatus->setText(startupStateString(state));
2188
2189 switch (moduleState()->startupState())
2190 {
2191 case STARTUP_IDLE:
2192 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2193 break;
2194 case STARTUP_COMPLETE:
2195 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2196 process()->appendLogText(i18n("Manual startup procedure completed successfully."));
2197 break;
2198 case STARTUP_ERROR:
2199 startupB->setIcon(QIcon::fromTheme("media-playback-start"));
2200 process()->appendLogText(i18n("Manual startup procedure terminated due to errors."));
2201 break;
2202 default:
2203 // in all other cases startup is running
2204 startupB->setIcon(QIcon::fromTheme("media-playback-stop"));
2205 break;
2206 }
2207}
2208void Scheduler::shutdownStateChanged(ShutdownState state)
2209{
2210 if (state == SHUTDOWN_COMPLETE || state == SHUTDOWN_IDLE
2211 || state == SHUTDOWN_ERROR)
2212 {
2213 shutdownB->setIcon(QIcon::fromTheme("media-playback-start"));
2214 pi->stopAnimation();
2215 }
2216 else
2217 shutdownB->setIcon(QIcon::fromTheme("media-playback-stop"));
2218
2219 if (state == SHUTDOWN_IDLE)
2220 jobStatus->setText(i18n("Idle"));
2221 else
2222 jobStatus->setText(shutdownStateString(state));
2223}
2224void Scheduler::ekosStateChanged(EkosState state)
2225{
2226 if (state == EKOS_IDLE)
2227 {
2228 jobStatus->setText(i18n("Idle"));
2229 pi->stopAnimation();
2230 }
2231 else
2232 jobStatus->setText(ekosStateString(state));
2233}
2234void Scheduler::indiStateChanged(INDIState state)
2235{
2236 if (state == INDI_IDLE)
2237 {
2238 jobStatus->setText(i18n("Idle"));
2239 pi->stopAnimation();
2240 }
2241 else
2242 jobStatus->setText(indiStateString(state));
2243}
2244void Scheduler::parkWaitStateChanged(ParkWaitState state)
2245{
2246 jobStatus->setText(parkWaitStateString(state));
2247}
2248
2249SchedulerJob *Scheduler::activeJob()
2250{
2251 return moduleState()->activeJob();
2252}
2253
2254void Scheduler::loadGlobalSettings()
2255{
2256 QString key;
2257 QVariant value;
2258
2259 QVariantMap settings;
2260 // All Combo Boxes
2261 for (auto &oneWidget : findChildren<QComboBox*>())
2262 {
2263 key = oneWidget->objectName();
2264 value = Options::self()->property(key.toLatin1());
2265 if (value.isValid())
2266 {
2267 oneWidget->setCurrentText(value.toString());
2268 settings[key] = value;
2269 }
2270 else
2271 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2272 }
2273
2274 // All Double Spin Boxes
2275 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2276 {
2277 key = oneWidget->objectName();
2278 value = Options::self()->property(key.toLatin1());
2279 if (value.isValid())
2280 {
2281 oneWidget->setValue(value.toDouble());
2282 settings[key] = value;
2283 }
2284 else
2285 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2286 }
2287
2288 // All Spin Boxes
2289 for (auto &oneWidget : findChildren<QSpinBox*>())
2290 {
2291 key = oneWidget->objectName();
2292 value = Options::self()->property(key.toLatin1());
2293 if (value.isValid())
2294 {
2295 oneWidget->setValue(value.toInt());
2296 settings[key] = value;
2297 }
2298 else
2299 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2300 }
2301
2302 // All Checkboxes
2303 for (auto &oneWidget : findChildren<QCheckBox*>())
2304 {
2305 key = oneWidget->objectName();
2306 value = Options::self()->property(key.toLatin1());
2307 if (value.isValid())
2308 {
2309 oneWidget->setChecked(value.toBool());
2310 settings[key] = value;
2311 }
2312 else
2313 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2314 }
2315
2316 // All Line Edits
2317 for (auto &oneWidget : findChildren<QLineEdit*>())
2318 {
2319 key = oneWidget->objectName();
2320 value = Options::self()->property(key.toLatin1());
2321 if (value.isValid())
2322 {
2323 oneWidget->setText(value.toString());
2324 settings[key] = value;
2325
2326 if (key == "sequenceEdit")
2327 setSequence(value.toString());
2328 else if (key == "schedulerStartupScript")
2329 moduleState()->setStartupScriptURL(QUrl::fromUserInput(value.toString()));
2330 else if (key == "schedulerShutdownScript")
2331 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(value.toString()));
2332 }
2333 else
2334 qCDebug(KSTARS_EKOS_SCHEDULER) << "Option" << key << "not found!";
2335 }
2336
2337 // All Radio buttons
2338 for (auto &oneWidget : findChildren<QRadioButton*>())
2339 {
2340 key = oneWidget->objectName();
2341 value = Options::self()->property(key.toLatin1());
2342 if (value.isValid())
2343 {
2344 oneWidget->setChecked(value.toBool());
2345 settings[key] = value;
2346 }
2347 }
2348
2349 // All QDateTime edits
2350 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2351 {
2352 key = oneWidget->objectName();
2353 value = Options::self()->property(key.toLatin1());
2354 if (value.isValid())
2355 {
2356 oneWidget->setDateTime(QDateTime::fromString(value.toString(), Qt::ISODate));
2357 settings[key] = value;
2358 }
2359 }
2360
2361 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy()));
2362
2363 m_GlobalSettings = m_Settings = settings;
2364}
2365
2366void Scheduler::syncSettings()
2367{
2368 QDoubleSpinBox *dsb = nullptr;
2369 QSpinBox *sb = nullptr;
2370 QCheckBox *cb = nullptr;
2371 QRadioButton *rb = nullptr;
2372 QComboBox *cbox = nullptr;
2373 QLineEdit *lineedit = nullptr;
2374 QDateTimeEdit *datetimeedit = nullptr;
2375
2376 QString key;
2377 QVariant value;
2378
2380 {
2381 key = dsb->objectName();
2382 value = dsb->value();
2383
2384 }
2385 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
2386 {
2387 key = sb->objectName();
2388 value = sb->value();
2389 }
2390 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
2391 {
2392 key = cb->objectName();
2393 value = cb->isChecked();
2394 }
2395 else if ( (rb = qobject_cast<QRadioButton*>(sender())))
2396 {
2397 key = rb->objectName();
2398 if (rb->isChecked() == false)
2399 {
2400 m_Settings.remove(key);
2401 return;
2402 }
2403 value = true;
2404 }
2405 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
2406 {
2407 key = cbox->objectName();
2408 value = cbox->currentText();
2409 }
2410 else if ( (lineedit = qobject_cast<QLineEdit*>(sender())))
2411 {
2412 key = lineedit->objectName();
2413 value = lineedit->text();
2414 }
2416 {
2417 key = datetimeedit->objectName();
2418 value = datetimeedit->dateTime().toString(Qt::ISODate);
2419 }
2420
2421 // Save immediately
2422 Options::self()->setProperty(key.toLatin1(), value);
2423
2424 m_Settings[key] = value;
2425 m_GlobalSettings[key] = value;
2426
2427 emit settingsUpdated(getAllSettings());
2428}
2429
2430///////////////////////////////////////////////////////////////////////////////////////////
2431///
2432///////////////////////////////////////////////////////////////////////////////////////////
2433QVariantMap Scheduler::getAllSettings() const
2434{
2435 QVariantMap settings;
2436
2437 // All Combo Boxes
2438 for (auto &oneWidget : findChildren<QComboBox*>())
2439 settings.insert(oneWidget->objectName(), oneWidget->currentText());
2440
2441 // All Double Spin Boxes
2442 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2443 settings.insert(oneWidget->objectName(), oneWidget->value());
2444
2445 // All Spin Boxes
2446 for (auto &oneWidget : findChildren<QSpinBox*>())
2447 settings.insert(oneWidget->objectName(), oneWidget->value());
2448
2449 // All Checkboxes
2450 for (auto &oneWidget : findChildren<QCheckBox*>())
2451 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
2452
2453 // All Line Edits
2454 for (auto &oneWidget : findChildren<QLineEdit*>())
2455 {
2456 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those
2457 if (!oneWidget->objectName().startsWith("qt_"))
2458 settings.insert(oneWidget->objectName(), oneWidget->text());
2459 }
2460
2461 // All Radio Buttons
2462 for (auto &oneWidget : findChildren<QRadioButton*>())
2463 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
2464
2465 // All QDateTime
2466 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2467 {
2468 settings.insert(oneWidget->objectName(), oneWidget->dateTime().toString(Qt::ISODate));
2469 }
2470
2471 return settings;
2472}
2473
2474///////////////////////////////////////////////////////////////////////////////////////////
2475///
2476///////////////////////////////////////////////////////////////////////////////////////////
2477void Scheduler::setAllSettings(const QVariantMap &settings)
2478{
2479 // Disconnect settings that we don't end up calling syncSettings while
2480 // performing the changes.
2481 disconnectSettings();
2482
2483 for (auto &name : settings.keys())
2484 {
2485 // Combo
2486 auto comboBox = findChild<QComboBox*>(name);
2487 if (comboBox)
2488 {
2489 syncControl(settings, name, comboBox);
2490 continue;
2491 }
2492
2493 // Double spinbox
2494 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
2495 if (doubleSpinBox)
2496 {
2497 syncControl(settings, name, doubleSpinBox);
2498 continue;
2499 }
2500
2501 // spinbox
2502 auto spinBox = findChild<QSpinBox*>(name);
2503 if (spinBox)
2504 {
2505 syncControl(settings, name, spinBox);
2506 continue;
2507 }
2508
2509 // checkbox
2510 auto checkbox = findChild<QCheckBox*>(name);
2511 if (checkbox)
2512 {
2513 syncControl(settings, name, checkbox);
2514 continue;
2515 }
2516
2517 // Line Edits
2518 auto lineedit = findChild<QLineEdit*>(name);
2519 if (lineedit)
2520 {
2521 syncControl(settings, name, lineedit);
2522
2523 if (name == "sequenceEdit")
2524 setSequence(lineedit->text());
2525 else if (name == "fitsEdit")
2526 processFITSSelection(QUrl::fromLocalFile(lineedit->text()));
2527 else if (name == "schedulerStartupScript")
2528 moduleState()->setStartupScriptURL(QUrl::fromUserInput(lineedit->text()));
2529 else if (name == "schedulerShutdownScript")
2530 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(lineedit->text()));
2531
2532 continue;
2533 }
2534
2535 // Radio button
2537 if (radioButton)
2538 {
2539 syncControl(settings, name, radioButton);
2540 continue;
2541 }
2542
2544 if (datetimeedit)
2545 {
2546 syncControl(settings, name, datetimeedit);
2547 continue;
2548 }
2549 }
2550
2551 m_Settings = settings;
2552
2553 // Restablish connections
2554 connectSettings();
2555}
2556
2557///////////////////////////////////////////////////////////////////////////////////////////
2558///
2559///////////////////////////////////////////////////////////////////////////////////////////
2560bool Scheduler::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
2561{
2562 QSpinBox *pSB = nullptr;
2563 QDoubleSpinBox *pDSB = nullptr;
2564 QCheckBox *pCB = nullptr;
2565 QComboBox *pComboBox = nullptr;
2566 QLineEdit *pLineEdit = nullptr;
2567 QRadioButton *pRadioButton = nullptr;
2568 QDateTimeEdit *pDateTimeEdit = nullptr;
2569 bool ok = true;
2570
2571 if ((pSB = qobject_cast<QSpinBox *>(widget)))
2572 {
2573 const int value = settings[key].toInt(&ok);
2574 if (ok)
2575 {
2576 pSB->setValue(value);
2577 return true;
2578 }
2579 }
2580 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
2581 {
2582 const double value = settings[key].toDouble(&ok);
2583 if (ok)
2584 {
2585 pDSB->setValue(value);
2586 return true;
2587 }
2588 }
2589 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
2590 {
2591 const bool value = settings[key].toBool();
2592 if (value != pCB->isChecked())
2593 pCB->click();
2594 return true;
2595 }
2596 // ONLY FOR STRINGS, not INDEX
2597 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
2598 {
2599 const QString value = settings[key].toString();
2600 pComboBox->setCurrentText(value);
2601 return true;
2602 }
2603 else if ((pLineEdit = qobject_cast<QLineEdit *>(widget)))
2604 {
2605 const auto value = settings[key].toString();
2606 pLineEdit->setText(value);
2607 return true;
2608 }
2609 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
2610 {
2611 const bool value = settings[key].toBool();
2612 if (value)
2613 pRadioButton->click();
2614 return true;
2615 }
2616 else if ((pDateTimeEdit = qobject_cast<QDateTimeEdit *>(widget)))
2617 {
2618 const auto value = QDateTime::fromString(settings[key].toString(), Qt::ISODate);
2619 pDateTimeEdit->setDateTime(value);
2620 return true;
2621 }
2622
2623 return false;
2624};
2625
2626void Scheduler::connectSettings()
2627{
2628 // All Combo Boxes
2629 for (auto &oneWidget : findChildren<QComboBox*>())
2630 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings);
2631
2632 // All Double Spin Boxes
2633 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2634 connect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2635
2636 // All Spin Boxes
2637 for (auto &oneWidget : findChildren<QSpinBox*>())
2638 connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2639
2640 // All Checkboxes
2641 for (auto &oneWidget : findChildren<QCheckBox*>())
2642 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings);
2643
2644 // All Radio Butgtons
2645 for (auto &oneWidget : findChildren<QRadioButton*>())
2646 connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings);
2647
2648 // All QLineEdits
2649 for (auto &oneWidget : findChildren<QLineEdit*>())
2650 {
2651 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those
2652 if (!oneWidget->objectName().startsWith("qt_"))
2653 connect(oneWidget, &QLineEdit::textChanged, this, &Ekos::Scheduler::syncSettings);
2654 }
2655
2656 // All QDateTimeEdit
2657 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2658 connect(oneWidget, &QDateTimeEdit::dateTimeChanged, this, &Ekos::Scheduler::syncSettings);
2659}
2660
2661void Scheduler::disconnectSettings()
2662{
2663 // All Combo Boxes
2664 for (auto &oneWidget : findChildren<QComboBox*>())
2665 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings);
2666
2667 // All Double Spin Boxes
2668 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
2669 disconnect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2670
2671 // All Spin Boxes
2672 for (auto &oneWidget : findChildren<QSpinBox*>())
2673 disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings);
2674
2675 // All Checkboxes
2676 for (auto &oneWidget : findChildren<QCheckBox*>())
2677 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings);
2678
2679 // All Radio Butgtons
2680 for (auto &oneWidget : findChildren<QRadioButton*>())
2681 disconnect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings);
2682
2683 // All QLineEdits
2684 for (auto &oneWidget : findChildren<QLineEdit*>())
2685 disconnect(oneWidget, &QLineEdit::editingFinished, this, &Ekos::Scheduler::syncSettings);
2686
2687 // All QDateTimeEdit
2688 for (auto &oneWidget : findChildren<QDateTimeEdit*>())
2689 disconnect(oneWidget, &QDateTimeEdit::editingFinished, this, &Ekos::Scheduler::syncSettings);
2690}
2691
2692}
The SchedulerProcess class holds the entire business logic for controlling the execution of the EKOS ...
Q_SCRIPTABLE Q_NOREPLY void runStartupProcedure()
runStartupProcedure Execute the startup of the scheduler itself to be prepared for running scheduler ...
Q_SCRIPTABLE Q_NOREPLY void startJobEvaluation()
startJobEvaluation Start job evaluation only without starting the scheduler process itself.
Q_SCRIPTABLE Q_NOREPLY void runShutdownProcedure()
runShutdownProcedure Shutdown the scheduler itself and EKOS (if configured to do so).
ErrorHandlingStrategy getErrorHandlingStrategy()
retrieve the error handling strategy from the UI
void moveJobUp()
moveJobUp Move the selected job up in the job list.
void watchJobChanges(bool enable)
Q_INVOKABLE void clearLog()
clearLog Clears log entry
void checkTwilightWarning(bool enabled)
checkWeather Check weather status and act accordingly depending on the current status of the schedule...
void saveJob(SchedulerJob *job=nullptr)
addToQueue Construct a SchedulerJob and add it to the queue or save job settings from current form va...
void updateSchedulerURL(const QString &fileURL)
updateSchedulerURL Update scheduler URL after succesful loading a new file.
void addJob(SchedulerJob *job=nullptr)
addJob Add a new job from form values
void selectSequence()
Selects sequence queue.
void insertJobTableRow(int row, bool above=true)
insertJobTableRow Insert a new row (empty) into the job table
bool load(bool clearQueue, const QString &filename=QString())
load Open a file dialog to select an ESL file, and load its contents.
void resumeCheckStatus()
resumeCheckStatus If the scheduler primary loop was suspended due to weather or sleep event,...
void handleSchedulerSleeping(bool shutdown, bool sleep)
handleSchedulerSleeping Update UI if scheduler is set to sleep
void setJobManipulation(bool can_reorder, bool can_delete)
setJobManipulation Enable or disable job manipulation buttons.
void moveJobDown()
moveJobDown Move the selected job down in the list.
bool importMosaic(const QJsonObject &payload)
importMosaic Import mosaic into planner and generate jobs for the scheduler.
void handleSetPaused()
handleSetPaused Update the UI when {
bool reorderJobs(QList< SchedulerJob * > reordered_sublist)
reorderJobs Change the order of jobs in the UI based on a subset of its jobs.
void syncGUIToGeneralSettings()
syncGUIToGeneralSettings set all UI fields that are not job specific
void updateNightTime(SchedulerJob const *job=nullptr)
updateNightTime update the Twilight restriction with the argument job properties.
bool loadFile(const QUrl &path)
loadFile Load scheduler jobs from disk
void handleSchedulerStateChanged(SchedulerState newState)
handleSchedulerStateChanged Update UI when the scheduler state changes
bool fillJobFromUI(SchedulerJob *job)
createJob Create a new job from form values.
void loadJob(QModelIndex i)
editJob Edit an observation job
void setSequence(const QString &sequenceFileURL)
Set the file URL pointing to the capture sequence file.
void selectStartupScript()
Selects sequence queue.
void syncGUIToJob(SchedulerJob *job)
set all GUI fields to the values of the given scheduler job
void schedulerStopped()
schedulerStopped React when the process engine has stopped the scheduler
void selectObject()
select object from KStars's find dialog.
void updateCellStyle(SchedulerJob *job, QTableWidgetItem *cell)
Update the style of a cell, depending on the job's state.
void clearJobTable()
clearJobTable delete all rows in the job table
void setJobAddApply(bool add_mode)
setJobAddApply Set first button state to add new job or apply changes.
void handleConfigChanged()
handleConfigChanged Update UI after changes to the global configuration
bool saveFile(const QUrl &path)
saveFile Save scheduler jobs to disk
Q_SCRIPTABLE void sortJobsPerAltitude()
DBUS interface function.
void setErrorHandlingStrategy(ErrorHandlingStrategy strategy)
select the error handling strategy (no restart, restart after all terminated, restart immediately)
void clickQueueTable(QModelIndex index)
jobSelectionChanged Update UI state when the job list is clicked once.
void updateJobTable(SchedulerJob *job=nullptr)
updateJobTable Update the job's row in the job table.
void removeJob()
Remove a job from current table row.
void removeOneJob(int index)
Remove a job by selecting a table row.
void selectFITS()
Selects FITS file for solving.
Scheduler()
Constructor, the starndard scheduler constructor.
Definition scheduler.cpp:80
void interfaceReady(QDBusInterface *iface)
checkInterfaceReady Sometimes syncProperties() is not sufficient since the ready signal could have fi...
void queueTableSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
Update scheduler parameters to the currently selected scheduler job.
void selectShutdownScript()
Selects sequence queue.
static KConfigDialog * exists(const QString &name)
void settingsChanged(const QString &dialogName)
static KStars * Instance()
Definition kstars.h:123
The QProgressIndicator class lets an application display a progress indicator to show that a long tas...
void stopAnimation()
Stops the spin animation.
void startAnimation()
Starts the spin animation.
The SchedulerState class holds all attributes defining the scheduler's state.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
virtual QString name(void) const
Definition skyobject.h:145
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & ra0() const
Definition skypoint.h:251
const CachingDms & dec0() const
Definition skypoint.h:257
This is a subclass of SkyObject.
Definition starobject.h:33
int getHDIndex() const
Definition starobject.h:248
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static dms fromString(const QString &s, bool deg)
Static function to create a DMS object from a QString.
Definition dms.cpp:429
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
bool insert(Part *part, qint64 *insertId=nullptr)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
StartupCondition
Conditions under which a SchedulerJob may start.
@ SCHEDJOB_ABORTED
Job encountered a transitory issue while processing, and will be rescheduled.
@ SCHEDJOB_INVALID
Job has an incorrect configuration, and cannot proceed.
@ SCHEDJOB_ERROR
Job encountered a fatal issue while processing, and must be reset manually.
@ SCHEDJOB_COMPLETE
Job finished all required captures.
@ SCHEDJOB_EVALUATION
Job is being evaluated.
@ SCHEDJOB_SCHEDULED
Job was evaluated, and has a schedule.
@ SCHEDJOB_BUSY
Job is being processed.
@ SCHEDJOB_IDLE
Job was just created, and is not evaluated yet.
ErrorHandlingStrategy
options what should happen if an error or abort occurs
CompletionCondition
Conditions under which a SchedulerJob may complete.
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
KGuiItem cont()
KGuiItem cancel()
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void clicked(const QModelIndex &index)
void doubleClicked(const QModelIndex &index)
void rangeChanged(int min, int max)
void valueChanged(int value)
void editingFinished()
void triggered(bool checked)
void buttonClicked(QAbstractButton *button)
void buttonToggled(QAbstractButton *button, bool checked)
void idToggled(int id, bool checked)
void currentIndexChanged(int index)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
void setTime(QTime time)
QTime time() const const
QString toString(QStringView format, QCalendar cal) const const
QString homePath()
void valueChanged(double d)
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QUrl getOpenFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
Int toInt() const const
QIcon fromTheme(const QString &name)
QModelIndexList indexes() const const
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void editingFinished()
void textChanged(const QString &text)
bool empty() const const
bool isValid() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QList< T > findChildren(Qt::FindChildOptions options) const const
QObject * sender() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
void valueChanged(int i)
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
AlignHCenter
ItemIsSelectable
Horizontal
WA_LayoutUsesWidgetRect
QTextStream & center(QTextStream &stream)
void selectRow(int row)
QFont font() const const
void setFlags(Qt::ItemFlags flags)
void setFont(const QFont &font)
void setTextAlignment(Qt::Alignment alignment)
int hour() const const
int minute() const const
bool setHMS(int h, int m, int s, int ms)
void setInterval(int msec)
void setSingleShot(bool singleShot)
void start()
void timeout()
RemoveFilename
void clear()
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
bool isEmpty() const const
bool isValid() const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool isValid() const const
bool toBool() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
void setupUi(QWidget *widget)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 11:49:22 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.