File indexing completed on 2024-05-05 07:42:11
0001 /* 0002 SPDX-FileCopyrightText: 2015 Jasem Mutlaq <mutlaqja@ikarustech.com> 0003 0004 DBus calls from GSoC 2015 Ekos Scheduler project: 0005 SPDX-FileCopyrightText: 2015 Daniel Leu <daniel_mihai.leu@cti.pub.ro> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #include "scheduler.h" 0011 0012 #include "ekos/scheduler/framingassistantui.h" 0013 #include "ksnotification.h" 0014 #include "ksmessagebox.h" 0015 #include "kstars.h" 0016 #include "kstarsdata.h" 0017 #include "skymap.h" 0018 #include "Options.h" 0019 #include "scheduleradaptor.h" 0020 #include "schedulerjob.h" 0021 #include "schedulerprocess.h" 0022 #include "schedulermodulestate.h" 0023 #include "schedulerutils.h" 0024 #include "skymapcomposite.h" 0025 #include "skycomponents/mosaiccomponent.h" 0026 #include "skyobjects/mosaictiles.h" 0027 #include "auxiliary/QProgressIndicator.h" 0028 #include "dialogs/finddialog.h" 0029 #include "ekos/manager.h" 0030 #include "ekos/capture/sequencejob.h" 0031 #include "ekos/capture/placeholderpath.h" 0032 #include "skyobjects/starobject.h" 0033 #include "greedyscheduler.h" 0034 #include "ekos/auxiliary/solverutils.h" 0035 #include "ekos/auxiliary/stellarsolverprofile.h" 0036 0037 #include <KConfigDialog> 0038 #include <KActionCollection> 0039 0040 #include <fitsio.h> 0041 #include <ekos_scheduler_debug.h> 0042 #include <indicom.h> 0043 #include "ekos/capture/sequenceeditor.h" 0044 0045 // Qt version calming 0046 #include <qtendl.h> 0047 0048 #define BAD_SCORE -1000 0049 #define RESTART_GUIDING_DELAY_MS 5000 0050 0051 #define DEFAULT_MIN_ALTITUDE 15 0052 #define DEFAULT_MIN_MOON_SEPARATION 0 0053 0054 // This is a temporary debugging printout introduced while gaining experience developing 0055 // the unit tests in test_ekos_scheduler_ops.cpp. 0056 // All these printouts should be eventually removed. 0057 #define TEST_PRINT if (false) fprintf 0058 0059 namespace 0060 { 0061 0062 // This needs to match the definition order for the QueueTable in scheduler.ui 0063 enum QueueTableColumns 0064 { 0065 NAME_COLUMN = 0, 0066 STATUS_COLUMN, 0067 CAPTURES_COLUMN, 0068 ALTITUDE_COLUMN, 0069 START_TIME_COLUMN, 0070 END_TIME_COLUMN, 0071 }; 0072 } 0073 0074 namespace Ekos 0075 { 0076 0077 // Setup the main loop and start. 0078 void Scheduler::start() 0079 { 0080 process()->startScheduler(); 0081 } 0082 0083 0084 Scheduler::Scheduler() 0085 { 0086 // Use the default path and interface when running the scheduler. 0087 setupScheduler(ekosPathString, ekosInterfaceString); 0088 } 0089 0090 Scheduler::Scheduler(const QString path, const QString interface, 0091 const QString &ekosPathStr, const QString &ekosInterfaceStr) 0092 { 0093 // During testing, when mocking ekos, use a special purpose path and interface. 0094 schedulerPathString = path; 0095 kstarsInterfaceString = interface; 0096 setupScheduler(ekosPathStr, ekosInterfaceStr); 0097 } 0098 0099 void Scheduler::setupScheduler(const QString &ekosPathStr, const QString &ekosInterfaceStr) 0100 { 0101 setupUi(this); 0102 0103 qRegisterMetaType<Ekos::SchedulerState>("Ekos::SchedulerState"); 0104 qDBusRegisterMetaType<Ekos::SchedulerState>(); 0105 0106 m_moduleState.reset(new SchedulerModuleState()); 0107 m_process.reset(new SchedulerProcess(moduleState())); 0108 0109 dirPath = QUrl::fromLocalFile(QDir::homePath()); 0110 0111 // Get current KStars time and set seconds to zero 0112 QDateTime currentDateTime = SchedulerModuleState::getLocalTime(); 0113 QTime currentTime = currentDateTime.time(); 0114 currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); 0115 currentDateTime.setTime(currentTime); 0116 0117 // Set initial time for startup and completion times 0118 startupTimeEdit->setDateTime(currentDateTime); 0119 schedulerUntilValue->setDateTime(currentDateTime); 0120 0121 // Set up DBus interfaces 0122 new SchedulerAdaptor(this); 0123 QDBusConnection::sessionBus().unregisterObject(schedulerPathString); 0124 if (!QDBusConnection::sessionBus().registerObject(schedulerPathString, this)) 0125 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Scheduler failed to register with dbus"); 0126 process()->setEkosInterface(new QDBusInterface(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, 0127 QDBusConnection::sessionBus(), this)); 0128 0129 process()->setIndiInterface(new QDBusInterface(kstarsInterfaceString, INDIPathString, INDIInterfaceString, 0130 QDBusConnection::sessionBus(), this)); 0131 0132 // Example of connecting DBus signals 0133 //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); 0134 //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); 0135 //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); 0136 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "newModule", this, 0137 SLOT(registerNewModule(QString))); 0138 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "newDevice", this, 0139 SLOT(registerNewDevice(QString, int))); 0140 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "indiStatusChanged", 0141 this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); 0142 QDBusConnection::sessionBus().connect(kstarsInterfaceString, ekosPathStr, ekosInterfaceStr, "ekosStatusChanged", 0143 this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); 0144 0145 sleepLabel->setPixmap( 0146 QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); 0147 changeSleepLabel("", false); 0148 0149 pi = new QProgressIndicator(this); 0150 bottomLayout->addWidget(pi, 0); 0151 0152 geo = KStarsData::Instance()->geo(); 0153 0154 //RA box should be HMS-style 0155 raBox->setUnits(dmsBox::HOURS); 0156 0157 /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */ 0158 0159 queueTable->setToolTip( 0160 i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); 0161 QTableWidgetItem *statusHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STATUS); 0162 QTableWidgetItem *altitudeHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ALTITUDE); 0163 QTableWidgetItem *startupHeader = queueTable->horizontalHeaderItem(SCHEDCOL_STARTTIME); 0164 QTableWidgetItem *completionHeader = queueTable->horizontalHeaderItem(SCHEDCOL_ENDTIME); 0165 QTableWidgetItem *captureCountHeader = queueTable->horizontalHeaderItem(SCHEDCOL_CAPTURES); 0166 0167 if (statusHeader != nullptr) 0168 statusHeader->setToolTip(i18n("Current status of the job, managed by the Scheduler.\n" 0169 "If invalid, the Scheduler was not able to find a proper observation time for the target.\n" 0170 "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n" 0171 "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats.")); 0172 if (altitudeHeader != nullptr) 0173 altitudeHeader->setToolTip(i18n("Current altitude of the target of the job.\n" 0174 "A rising target is indicated with an arrow going up.\n" 0175 "A setting target is indicated with an arrow going down.")); 0176 if (startupHeader != nullptr) 0177 startupHeader->setToolTip(i18n("Startup time of the job, as estimated by the Scheduler.\n" 0178 "The altitude at startup, if available, is displayed too.\n" 0179 "Fixed time from user or culmination time is marked with a chronometer symbol.")); 0180 if (completionHeader != nullptr) 0181 completionHeader->setToolTip(i18n("Completion time for the job, as estimated by the Scheduler.\n" 0182 "You may specify a fixed time to limit duration of looping jobs. " 0183 "A warning symbol indicates the altitude at completion may cause the job to abort before completion.\n")); 0184 if (captureCountHeader != nullptr) 0185 captureCountHeader->setToolTip(i18n("Count of captures stored for the job, based on its sequence job.\n" 0186 "This is a summary, additional specific frame types may be required to complete the job.")); 0187 0188 /* Set first button mode to add observation job from left-hand fields */ 0189 setJobAddApply(true); 0190 0191 removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); 0192 removeFromQueueB->setToolTip( 0193 i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); 0194 removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0195 0196 queueUpB->setIcon(QIcon::fromTheme("go-up")); 0197 queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n")); 0198 queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0199 queueDownB->setIcon(QIcon::fromTheme("go-down")); 0200 queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n")); 0201 queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0202 0203 evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); 0204 evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); 0205 evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0206 sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); 0207 sortJobsB->setToolTip( 0208 i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" 0209 "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" 0210 "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); 0211 sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0212 mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); 0213 mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0214 0215 positionAngleSpin->setSpecialValueText("--"); 0216 0217 queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); 0218 queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0219 queueSaveB->setIcon(QIcon::fromTheme("document-save")); 0220 queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0221 queueLoadB->setIcon(QIcon::fromTheme("document-open")); 0222 queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0223 queueAppendB->setIcon(QIcon::fromTheme("document-import")); 0224 queueAppendB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0225 0226 loadSequenceB->setIcon(QIcon::fromTheme("document-open")); 0227 loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0228 selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); 0229 selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0230 selectShutdownScriptB->setIcon( 0231 QIcon::fromTheme("document-open")); 0232 selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0233 selectFITSB->setIcon(QIcon::fromTheme("document-open")); 0234 selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0235 0236 startupB->setIcon( 0237 QIcon::fromTheme("media-playback-start")); 0238 startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0239 shutdownB->setIcon( 0240 QIcon::fromTheme("media-playback-start")); 0241 shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0242 0243 // 2023-06-27 sterne-jaeger: For simplicity reasons, the repeat option 0244 // for all sequences is only active of we do consider the past 0245 repeatSequenceCB->setEnabled(Options::rememberJobProgress() == false); 0246 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false); 0247 repeatSequenceCB->setChecked(Options::schedulerRepeatSequences()); 0248 executionSequenceLimit->setValue(Options::schedulerExecutionSequencesLimit()); 0249 0250 connect(startupB, &QPushButton::clicked, process().data(), &SchedulerProcess::runStartupProcedure); 0251 connect(shutdownB, &QPushButton::clicked, process().data(), &SchedulerProcess::runShutdownProcedure); 0252 0253 connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject); 0254 connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS); 0255 connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence); 0256 connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript); 0257 connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); 0258 0259 connect(KStars::Instance()->actionCollection()->action("show_mosaic_panel"), &QAction::triggered, this, [this](bool checked) 0260 { 0261 mosaicB->setDown(checked); 0262 }); 0263 connect(mosaicB, &QPushButton::clicked, this, []() 0264 { 0265 KStars::Instance()->actionCollection()->action("show_mosaic_panel")->trigger(); 0266 }); 0267 connect(addToQueueB, &QPushButton::clicked, [this]() 0268 { 0269 // add job from UI 0270 addJob(); 0271 }); 0272 connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); 0273 connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); 0274 connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); 0275 connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); 0276 connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); 0277 connect(queueTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Scheduler::queueTableSelectionChanged); 0278 connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); 0279 connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); 0280 0281 0282 // These connections are looking for changes in the rows queueTable is displaying. 0283 connect(queueTable->verticalScrollBar(), &QScrollBar::valueChanged, [this]() 0284 { 0285 updateJobTable(); 0286 }); 0287 connect(queueTable->verticalScrollBar(), &QAbstractSlider::rangeChanged, [this]() 0288 { 0289 updateJobTable(); 0290 }); 0291 0292 startB->setIcon(QIcon::fromTheme("media-playback-start")); 0293 startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0294 pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); 0295 pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 0296 pauseB->setCheckable(false); 0297 0298 connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler); 0299 connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause); 0300 0301 connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs); 0302 connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save); 0303 connect(queueLoadB, &QPushButton::clicked, this, [&]() 0304 { 0305 load(true); 0306 }); 0307 connect(queueAppendB, &QPushButton::clicked, this, [&]() 0308 { 0309 load(false); 0310 }); 0311 0312 connect(schedulerTwilight, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); 0313 0314 // Connect simulation clock scale 0315 connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &Scheduler::simClockScaleChanged); 0316 connect(KStarsData::Instance()->clock(), &SimClock::timeChanged, this, &Scheduler::simClockTimeChanged); 0317 0318 // Connect to the state machine 0319 connect(moduleState().data(), &SchedulerModuleState::newLog, this, &Scheduler::appendLogText); 0320 connect(moduleState().data(), &SchedulerModuleState::ekosStateChanged, this, &Scheduler::ekosStateChanged); 0321 connect(moduleState().data(), &SchedulerModuleState::indiStateChanged, this, &Scheduler::indiStateChanged); 0322 connect(moduleState().data(), &SchedulerModuleState::schedulerStateChanged, this, &Scheduler::handleSchedulerStateChanged); 0323 connect(moduleState().data(), &SchedulerModuleState::startupStateChanged, this, &Scheduler::startupStateChanged); 0324 connect(moduleState().data(), &SchedulerModuleState::shutdownStateChanged, this, &Scheduler::shutdownStateChanged); 0325 connect(moduleState().data(), &SchedulerModuleState::parkWaitStateChanged, this, &Scheduler::parkWaitStateChanged); 0326 connect(moduleState().data(), &SchedulerModuleState::profilesChanged, this, &Scheduler::updateProfiles); 0327 connect(moduleState().data(), &SchedulerModuleState::currentPositionChanged, queueTable, &QTableWidget::selectRow); 0328 connect(moduleState().data(), &SchedulerModuleState::jobStageChanged, this, &Scheduler::updateJobStageUI); 0329 connect(moduleState().data(), &SchedulerModuleState::updateNightTime, this, &Scheduler::updateNightTime); 0330 connect(moduleState().data(), &SchedulerModuleState::currentProfileChanged, this, [&]() 0331 { 0332 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile()); 0333 }); 0334 // Connect to process engine 0335 connect(process().data(), &SchedulerProcess::newLog, this, &Scheduler::appendLogText); 0336 connect(process().data(), &SchedulerProcess::schedulerStopped, this, &Scheduler::schedulerStopped); 0337 connect(process().data(), &SchedulerProcess::schedulerPaused, this, &Scheduler::handleSetPaused); 0338 connect(process().data(), &SchedulerProcess::shutdownStarted, this, &Scheduler::handleShutdownStarted); 0339 connect(process().data(), &SchedulerProcess::schedulerSleeping, this, &Scheduler::handleSchedulerSleeping); 0340 connect(process().data(), &SchedulerProcess::jobsUpdated, this, &Scheduler::handleJobsUpdated); 0341 connect(process().data(), &SchedulerProcess::updateJobTable, this, &Scheduler::updateJobTable); 0342 connect(process().data(), &SchedulerProcess::addJob, this, &Scheduler::addJob); 0343 connect(process().data(), &SchedulerProcess::jobStarted, this, &Scheduler::jobStarted); 0344 connect(process().data(), &SchedulerProcess::jobEnded, this, &Scheduler::jobEnded); 0345 connect(process().data(), &SchedulerProcess::syncGreedyParams, this, &Scheduler::syncGreedyParams); 0346 connect(process().data(), &SchedulerProcess::syncGUIToGeneralSettings, this, &Scheduler::syncGUIToGeneralSettings); 0347 connect(process().data(), &SchedulerProcess::changeSleepLabel, this, &Scheduler::changeSleepLabel); 0348 connect(process().data(), &SchedulerProcess::updateSchedulerURL, this, &Scheduler::updateSchedulerURL); 0349 // Connect geographical location - when it is available 0350 //connect(KStarsData::Instance()..., &LocationDialog::locationChanged..., this, &Scheduler::simClockTimeChanged); 0351 0352 // Restore values for general settings. 0353 syncGUIToGeneralSettings(); 0354 0355 0356 connect(errorHandlingButtonGroup, static_cast<void (QButtonGroup::*)(QAbstractButton *)> 0357 (&QButtonGroup::buttonClicked), [this](QAbstractButton * button) 0358 { 0359 Q_UNUSED(button) 0360 ErrorHandlingStrategy strategy = getErrorHandlingStrategy(); 0361 Options::setErrorHandlingStrategy(strategy); 0362 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART); 0363 }); 0364 connect(errorHandlingStrategyDelay, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [](int value) 0365 { 0366 Options::setErrorHandlingStrategyDelay(value); 0367 }); 0368 0369 // Retiring the Classic algorithm. 0370 if (Options::schedulerAlgorithm() != ALGORITHM_GREEDY) 0371 { 0372 appendLogText(i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm.")); 0373 Options::setSchedulerAlgorithm(ALGORITHM_GREEDY); 0374 } 0375 0376 // restore default values for scheduler algorithm 0377 setAlgorithm(Options::schedulerAlgorithm()); 0378 0379 connect(copySkyCenterB, &QPushButton::clicked, this, [this]() 0380 { 0381 SkyPoint center = SkyMap::Instance()->getCenterPoint(); 0382 //center.deprecess(KStarsData::Instance()->updateNum()); 0383 center.catalogueCoord(KStarsData::Instance()->updateNum()->julianDay()); 0384 raBox->show(center.ra0()); 0385 decBox->show(center.dec0()); 0386 }); 0387 0388 connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig); 0389 0390 connect(editSequenceB, &QPushButton::clicked, this, [this]() 0391 { 0392 if (!m_SequenceEditor) 0393 m_SequenceEditor.reset(new SequenceEditor(this)); 0394 0395 m_SequenceEditor->show(); 0396 m_SequenceEditor->raise(); 0397 }); 0398 0399 moduleState()->calculateDawnDusk(); 0400 updateNightTime(); 0401 0402 process()->loadProfiles(); 0403 0404 watchJobChanges(true); 0405 0406 loadGlobalSettings(); 0407 connectSettings(); 0408 } 0409 0410 QString Scheduler::getCurrentJobName() 0411 { 0412 return (activeJob() != nullptr ? activeJob()->getName() : ""); 0413 } 0414 0415 void Scheduler::watchJobChanges(bool enable) 0416 { 0417 /* Don't double watch, this will cause multiple signals to be connected */ 0418 if (enable == jobChangesAreWatched) 0419 return; 0420 0421 /* These are the widgets we want to connect, per signal function, to listen for modifications */ 0422 QLineEdit * const lineEdits[] = 0423 { 0424 nameEdit, 0425 groupEdit, 0426 raBox, 0427 decBox, 0428 fitsEdit, 0429 sequenceEdit, 0430 schedulerStartupScript, 0431 schedulerShutdownScript 0432 }; 0433 0434 QDateTimeEdit * const dateEdits[] = 0435 { 0436 startupTimeEdit, 0437 schedulerUntilValue 0438 }; 0439 0440 QComboBox * const comboBoxes[] = 0441 { 0442 schedulerProfileCombo, 0443 }; 0444 0445 QButtonGroup * const buttonGroups[] = 0446 { 0447 stepsButtonGroup, 0448 errorHandlingButtonGroup, 0449 startupButtonGroup, 0450 constraintButtonGroup, 0451 completionButtonGroup, 0452 startupProcedureButtonGroup, 0453 shutdownProcedureGroup 0454 }; 0455 0456 QAbstractButton * const buttons[] = 0457 { 0458 errorHandlingRescheduleErrorsCB 0459 }; 0460 0461 QSpinBox * const spinBoxes[] = 0462 { 0463 schedulerExecutionSequencesLimit, 0464 errorHandlingStrategyDelay 0465 }; 0466 0467 QDoubleSpinBox * const dspinBoxes[] = 0468 { 0469 schedulerMoonSeparationValue, 0470 schedulerAltitudeValue, 0471 positionAngleSpin, 0472 }; 0473 0474 if (enable) 0475 { 0476 /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will 0477 * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions 0478 * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the 0479 * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'. 0480 * The main problem with this implementation compared to the macro method is that it is now possible to 0481 * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot 0482 * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to 0483 * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart. 0484 */ 0485 for (auto * const control : lineEdits) 0486 connect(control, &QLineEdit::editingFinished, this, [this]() 0487 { 0488 setDirty(); 0489 }); 0490 for (auto * const control : dateEdits) 0491 connect(control, &QDateTimeEdit::editingFinished, this, [this]() 0492 { 0493 setDirty(); 0494 }); 0495 for (auto * const control : comboBoxes) 0496 connect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this]() 0497 { 0498 setDirty(); 0499 }); 0500 for (auto * const control : buttonGroups) 0501 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) 0502 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, [this](int, bool) 0503 #else 0504 connect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, [this](int, bool) 0505 #endif 0506 { 0507 setDirty(); 0508 }); 0509 for (auto * const control : buttons) 0510 connect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, [this](bool) 0511 { 0512 setDirty(); 0513 }); 0514 for (auto * const control : spinBoxes) 0515 connect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [this]() 0516 { 0517 setDirty(); 0518 }); 0519 for (auto * const control : dspinBoxes) 0520 connect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, [this](double) 0521 { 0522 setDirty(); 0523 }); 0524 } 0525 else 0526 { 0527 /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets, 0528 * because we did not take care to keep the connection object when connecting. No problem in our case, we do not 0529 * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to 0530 * disconnect selectively. 0531 */ 0532 for (auto * const control : lineEdits) 0533 disconnect(control, &QLineEdit::editingFinished, this, nullptr); 0534 for (auto * const control : dateEdits) 0535 disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); 0536 for (auto * const control : comboBoxes) 0537 disconnect(control, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, nullptr); 0538 for (auto * const control : buttons) 0539 disconnect(control, static_cast<void (QAbstractButton::*)(bool)>(&QAbstractButton::clicked), this, nullptr); 0540 for (auto * const control : buttonGroups) 0541 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) 0542 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, nullptr); 0543 #else 0544 disconnect(control, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled), this, nullptr); 0545 #endif 0546 for (auto * const control : spinBoxes) 0547 disconnect(control, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, nullptr); 0548 for (auto * const control : dspinBoxes) 0549 disconnect(control, static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, nullptr); 0550 } 0551 0552 jobChangesAreWatched = enable; 0553 } 0554 0555 void Scheduler::appendLogText(const QString &text) 0556 { 0557 /* FIXME: user settings for log length */ 0558 int const max_log_count = 2000; 0559 if (m_LogText.size() > max_log_count) 0560 m_LogText.removeLast(); 0561 0562 m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", 0563 SchedulerModuleState::getLocalTime().toString("yyyy-MM-ddThh:mm:ss"), text)); 0564 0565 qCInfo(KSTARS_EKOS_SCHEDULER) << text; 0566 0567 emit newLog(text); 0568 } 0569 0570 void Scheduler::clearLog() 0571 { 0572 m_LogText.clear(); 0573 emit newLog(QString()); 0574 } 0575 0576 void Scheduler::applyConfig() 0577 { 0578 moduleState()->calculateDawnDusk(); 0579 updateNightTime(); 0580 repeatSequenceCB->setEnabled(Options::rememberJobProgress() == false); 0581 executionSequenceLimit->setEnabled(Options::rememberJobProgress() == false); 0582 0583 if (SCHEDULER_RUNNING != moduleState()->schedulerState()) 0584 { 0585 process()->evaluateJobs(true); 0586 } 0587 } 0588 0589 void Scheduler::selectObject() 0590 { 0591 if (FindDialog::Instance()->execWithParent(Ekos::Manager::Instance()) == QDialog::Accepted) 0592 { 0593 SkyObject *object = FindDialog::Instance()->targetObject(); 0594 addObject(object); 0595 } 0596 } 0597 0598 void Scheduler::addObject(SkyObject *object) 0599 { 0600 if (object != nullptr) 0601 { 0602 QString finalObjectName(object->name()); 0603 0604 if (object->name() == "star") 0605 { 0606 StarObject *s = dynamic_cast<StarObject *>(object); 0607 0608 if (s->getHDIndex() != 0) 0609 finalObjectName = QString("HD %1").arg(s->getHDIndex()); 0610 } 0611 0612 nameEdit->setText(finalObjectName); 0613 raBox->show(object->ra0()); 0614 decBox->show(object->dec0()); 0615 0616 setDirty(); 0617 } 0618 } 0619 0620 void Scheduler::selectFITS() 0621 { 0622 auto url = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Select FITS/XISF Image"), dirPath, 0623 "FITS (*.fits *.fit);;XISF (*.xisf)"); 0624 if (url.isEmpty()) 0625 return; 0626 0627 processFITSSelection(url); 0628 } 0629 0630 void Scheduler::processFITSSelection(const QUrl &url) 0631 { 0632 if (url.isEmpty()) 0633 return; 0634 0635 fitsURL = url; 0636 dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); 0637 fitsEdit->setText(fitsURL.toLocalFile()); 0638 setDirty(); 0639 0640 const QString filename = fitsEdit->text(); 0641 int status = 0; 0642 double ra = 0, dec = 0; 0643 dms raDMS, deDMS; 0644 char comment[128], error_status[512]; 0645 fitsfile *fptr = nullptr; 0646 0647 if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) 0648 { 0649 fits_report_error(stderr, status); 0650 fits_get_errstatus(status, error_status); 0651 qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); 0652 return; 0653 } 0654 0655 status = 0; 0656 if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) 0657 { 0658 fits_report_error(stderr, status); 0659 fits_get_errstatus(status, error_status); 0660 qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); 0661 return; 0662 } 0663 0664 status = 0; 0665 char objectra_str[32] = {0}; 0666 if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) 0667 { 0668 if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) 0669 { 0670 fits_report_error(stderr, status); 0671 fits_get_errstatus(status, error_status); 0672 appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); 0673 return; 0674 } 0675 0676 raDMS.setD(ra); 0677 } 0678 else 0679 { 0680 raDMS = dms::fromString(objectra_str, false); 0681 } 0682 0683 status = 0; 0684 char objectde_str[32] = {0}; 0685 if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) 0686 { 0687 if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) 0688 { 0689 fits_report_error(stderr, status); 0690 fits_get_errstatus(status, error_status); 0691 appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); 0692 return; 0693 } 0694 0695 deDMS.setD(dec); 0696 } 0697 else 0698 { 0699 deDMS = dms::fromString(objectde_str, true); 0700 } 0701 0702 raBox->show(raDMS); 0703 decBox->show(deDMS); 0704 0705 char object_str[256] = {0}; 0706 if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status)) 0707 { 0708 QFileInfo info(filename); 0709 nameEdit->setText(info.completeBaseName()); 0710 } 0711 else 0712 { 0713 nameEdit->setText(object_str); 0714 } 0715 } 0716 0717 void Scheduler::setSequence(const QString &sequenceFileURL) 0718 { 0719 sequenceURL = QUrl::fromLocalFile(sequenceFileURL); 0720 0721 if (sequenceFileURL.isEmpty()) 0722 return; 0723 dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); 0724 0725 sequenceEdit->setText(sequenceURL.toLocalFile()); 0726 0727 setDirty(); 0728 } 0729 0730 void Scheduler::selectSequence() 0731 { 0732 QString file = QFileDialog::getOpenFileName(Ekos::Manager::Instance(), i18nc("@title:window", "Select Sequence Queue"), 0733 dirPath.toLocalFile(), 0734 i18n("Ekos Sequence Queue (*.esq)")); 0735 0736 setSequence(file); 0737 } 0738 0739 void Scheduler::selectStartupScript() 0740 { 0741 moduleState()->setStartupScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", 0742 "Select Startup Script"), 0743 dirPath, 0744 i18n("Script (*)"))); 0745 if (moduleState()->startupScriptURL().isEmpty()) 0746 return; 0747 0748 dirPath = QUrl(moduleState()->startupScriptURL().url(QUrl::RemoveFilename)); 0749 0750 moduleState()->setDirty(true); 0751 schedulerStartupScript->setText(moduleState()->startupScriptURL().toLocalFile()); 0752 } 0753 0754 void Scheduler::selectShutdownScript() 0755 { 0756 moduleState()->setShutdownScriptURL(QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", 0757 "Select Shutdown Script"), 0758 dirPath, 0759 i18n("Script (*)"))); 0760 if (moduleState()->shutdownScriptURL().isEmpty()) 0761 return; 0762 0763 dirPath = QUrl(moduleState()->shutdownScriptURL().url(QUrl::RemoveFilename)); 0764 0765 moduleState()->setDirty(true); 0766 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toLocalFile()); 0767 } 0768 0769 void Scheduler::addJob(SchedulerJob *job) 0770 { 0771 if (0 <= jobUnderEdit) 0772 { 0773 // select the job currently being edited 0774 job = moduleState()->jobs().at(jobUnderEdit); 0775 // if existing, save it 0776 if (job != nullptr) 0777 saveJob(job); 0778 // in any case, reset editing 0779 resetJobEdit(); 0780 } 0781 else 0782 { 0783 // remember the number of rows to select the first one appended 0784 int currentRow = moduleState()->currentPosition(); 0785 0786 //If no row is selected, the job will be appended at the end of the list, otherwise below the current selection 0787 if (currentRow < 0) 0788 currentRow = queueTable->rowCount(); 0789 else 0790 currentRow++; 0791 0792 /* If a job is being added, save fields into a new job */ 0793 saveJob(job); 0794 0795 // select the first appended row (if any was added) 0796 if (moduleState()->jobs().count() > currentRow) 0797 moduleState()->setCurrentPosition(currentRow); 0798 } 0799 0800 emit jobsUpdated(moduleState()->getJSONJobs()); 0801 } 0802 0803 bool Scheduler::fillJobFromUI(SchedulerJob *job) 0804 { 0805 if (nameEdit->text().isEmpty()) 0806 { 0807 appendLogText(i18n("Warning: Target name is required.")); 0808 return false; 0809 } 0810 0811 if (sequenceEdit->text().isEmpty()) 0812 { 0813 appendLogText(i18n("Warning: Sequence file is required.")); 0814 return false; 0815 } 0816 0817 // Coordinates are required unless it is a FITS file 0818 if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) 0819 { 0820 appendLogText(i18n("Warning: Target coordinates are required.")); 0821 return false; 0822 } 0823 0824 bool raOk = false, decOk = false; 0825 dms /*const*/ ra(raBox->createDms(&raOk)); 0826 dms /*const*/ dec(decBox->createDms(&decOk)); 0827 0828 if (raOk == false) 0829 { 0830 appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); 0831 return false; 0832 } 0833 0834 if (decOk == false) 0835 { 0836 appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); 0837 return false; 0838 } 0839 0840 /* Configure or reconfigure the observation job */ 0841 fitsURL = QUrl::fromLocalFile(fitsEdit->text()); 0842 0843 // Get several job values depending on the state of the UI. 0844 0845 StartupCondition startCondition = START_AT; 0846 if (asapConditionR->isChecked()) 0847 startCondition = START_ASAP; 0848 0849 CompletionCondition stopCondition = FINISH_AT; 0850 if (schedulerCompleteSequences->isChecked()) 0851 stopCondition = FINISH_SEQUENCE; 0852 else if (schedulerRepeatSequences->isChecked()) 0853 stopCondition = FINISH_REPEAT; 0854 else if (schedulerUntilTerminated->isChecked()) 0855 stopCondition = FINISH_LOOP; 0856 0857 double altConstraint = SchedulerJob::UNDEFINED_ALTITUDE; 0858 if (schedulerAltitude->isChecked()) 0859 altConstraint = schedulerAltitudeValue->value(); 0860 0861 double moonConstraint = -1; 0862 if (schedulerMoonSeparation->isChecked()) 0863 moonConstraint = schedulerMoonSeparationValue->value(); 0864 0865 // The reason for this kitchen-sink function is to separate the UI from the 0866 // job setup, to allow for testing. 0867 SchedulerUtils::setupJob(*job, nameEdit->text(), groupEdit->text(), ra, dec, 0868 KStarsData::Instance()->ut().djd(), 0869 positionAngleSpin->value(), sequenceURL, fitsURL, 0870 0871 startCondition, startupTimeEdit->dateTime(), 0872 stopCondition, schedulerUntilValue->dateTime(), schedulerExecutionSequencesLimit->value(), 0873 0874 altConstraint, 0875 moonConstraint, 0876 schedulerWeather->isChecked(), 0877 schedulerTwilight->isChecked(), 0878 schedulerHorizon->isChecked(), 0879 0880 schedulerTrackStep->isChecked(), 0881 schedulerFocusStep->isChecked(), 0882 schedulerAlignStep->isChecked(), 0883 schedulerGuideStep->isChecked()); 0884 0885 // success 0886 updateJobTable(job); 0887 return true; 0888 } 0889 0890 void Scheduler::saveJob(SchedulerJob *job) 0891 { 0892 watchJobChanges(false); 0893 0894 /* Create or Update a scheduler job, append below current selection */ 0895 int currentRow = moduleState()->currentPosition() + 1; 0896 0897 /* Add job to queue only if it is new, else reuse current row. 0898 * Make sure job is added at the right index, now that queueTable may have a line selected without being edited. 0899 */ 0900 if (0 <= jobUnderEdit) 0901 { 0902 /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from moduleState()->currentPosition(). */ 0903 if (jobUnderEdit != currentRow - 1) 0904 { 0905 qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table."; 0906 } 0907 0908 /* Use the job in the row currently edited */ 0909 job = moduleState()->jobs().at(jobUnderEdit); 0910 // try to fill the job from the UI and exit if it fails 0911 if (fillJobFromUI(job) == false) 0912 { 0913 watchJobChanges(true); 0914 return; 0915 } 0916 } 0917 else 0918 { 0919 if (job == nullptr) 0920 { 0921 /* 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. */ 0922 job = new SchedulerJob(); 0923 // try to fill the job from the UI and exit if it fails 0924 if (fillJobFromUI(job) == false) 0925 { 0926 delete(job); 0927 watchJobChanges(true); 0928 return; 0929 } 0930 } 0931 /* Insert the job in the job list and add a row in the table for it just after the row currently selected. */ 0932 moduleState()->mutlableJobs().insert(currentRow, job); 0933 insertJobTableRow(currentRow); 0934 } 0935 0936 /* Verifications */ 0937 // Warn user if a duplicated job is in the list - same target, same sequence 0938 // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! 0939 int numWarnings = 0; 0940 foreach (SchedulerJob *a_job, moduleState()->jobs()) 0941 { 0942 if (a_job == job) 0943 { 0944 break; 0945 } 0946 else if (a_job->getName() == job->getName()) 0947 { 0948 int const a_job_row = moduleState()->jobs().indexOf(a_job); 0949 0950 /* 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. */ 0951 appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, " 0952 "the scheduler may consider the same storage for captures.", 0953 job->getName(), currentRow, a_job_row)); 0954 0955 /* Warn the user in case the two jobs are really identical */ 0956 if (a_job->getSequenceFile() == job->getSequenceFile()) 0957 { 0958 if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress()) 0959 appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count " 0960 "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')", 0961 job->getName(), currentRow, a_job_row, job->getRepeatsRequired())); 0962 } 0963 0964 // Don't need to warn over and over. 0965 if (++numWarnings >= 1) 0966 { 0967 appendLogText(i18n("Skipped checking for duplicates.")); 0968 break; 0969 } 0970 } 0971 } 0972 0973 updateJobTable(job); 0974 0975 /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ 0976 queueSaveAsB->setEnabled(true); 0977 queueSaveB->setEnabled(true); 0978 startB->setEnabled(true); 0979 evaluateOnlyB->setEnabled(true); 0980 setJobManipulation(true, true); 0981 checkJobInputComplete(); 0982 0983 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1); 0984 0985 watchJobChanges(true); 0986 0987 if (SCHEDULER_LOADING != moduleState()->schedulerState()) 0988 { 0989 process()->evaluateJobs(true); 0990 } 0991 } 0992 0993 void Scheduler::syncGUIToJob(SchedulerJob *job) 0994 { 0995 nameEdit->setText(job->getName()); 0996 groupEdit->setText(job->getGroup()); 0997 0998 raBox->show(job->getTargetCoords().ra0()); 0999 decBox->show(job->getTargetCoords().dec0()); 1000 1001 // fitsURL/sequenceURL are not part of UI, but the UI serves as model, so keep them here for now 1002 fitsURL = job->getFITSFile().isEmpty() ? QUrl() : job->getFITSFile(); 1003 sequenceURL = job->getSequenceFile(); 1004 fitsEdit->setText(fitsURL.toLocalFile()); 1005 sequenceEdit->setText(sequenceURL.toLocalFile()); 1006 1007 positionAngleSpin->setValue(job->getPositionAngle()); 1008 1009 schedulerTrackStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); 1010 schedulerFocusStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); 1011 schedulerAlignStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); 1012 schedulerGuideStep->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); 1013 1014 switch (job->getFileStartupCondition()) 1015 { 1016 case START_ASAP: 1017 asapConditionR->setChecked(true); 1018 break; 1019 1020 case START_AT: 1021 startupTimeConditionR->setChecked(true); 1022 startupTimeEdit->setDateTime(job->getStartupTime()); 1023 break; 1024 } 1025 1026 if (job->getMinAltitude()) 1027 { 1028 schedulerAltitude->setChecked(true); 1029 schedulerAltitudeValue->setValue(job->getMinAltitude()); 1030 } 1031 else 1032 { 1033 schedulerAltitude->setChecked(false); 1034 schedulerAltitudeValue->setValue(DEFAULT_MIN_ALTITUDE); 1035 } 1036 1037 if (job->getMinMoonSeparation() >= 0) 1038 { 1039 schedulerMoonSeparation->setChecked(true); 1040 schedulerMoonSeparationValue->setValue(job->getMinMoonSeparation()); 1041 } 1042 else 1043 { 1044 schedulerMoonSeparation->setChecked(false); 1045 schedulerMoonSeparationValue->setValue(DEFAULT_MIN_MOON_SEPARATION); 1046 } 1047 1048 schedulerWeather->setChecked(job->getEnforceWeather()); 1049 1050 schedulerTwilight->blockSignals(true); 1051 schedulerTwilight->setChecked(job->getEnforceTwilight()); 1052 schedulerTwilight->blockSignals(false); 1053 1054 schedulerHorizon->blockSignals(true); 1055 schedulerHorizon->setChecked(job->getEnforceArtificialHorizon()); 1056 schedulerHorizon->blockSignals(false); 1057 1058 switch (job->getCompletionCondition()) 1059 { 1060 case FINISH_SEQUENCE: 1061 schedulerCompleteSequences->setChecked(true); 1062 break; 1063 1064 case FINISH_REPEAT: 1065 schedulerRepeatSequences->setChecked(true); 1066 schedulerExecutionSequencesLimit->setValue(job->getRepeatsRequired()); 1067 break; 1068 1069 case FINISH_LOOP: 1070 schedulerUntilTerminated->setChecked(true); 1071 break; 1072 1073 case FINISH_AT: 1074 schedulerUntil->setChecked(true); 1075 schedulerUntilValue->setDateTime(job->getCompletionTime()); 1076 break; 1077 } 1078 1079 updateNightTime(job); 1080 1081 setJobManipulation(true, true); 1082 } 1083 1084 void Scheduler::syncGUIToGeneralSettings() 1085 { 1086 schedulerParkDome->setChecked(Options::schedulerParkDome()); 1087 schedulerParkMount->setChecked(Options::schedulerParkMount()); 1088 schedulerCloseDustCover->setChecked(Options::schedulerCloseDustCover()); 1089 schedulerWarmCCD->setChecked(Options::schedulerWarmCCD()); 1090 schedulerUnparkDome->setChecked(Options::schedulerUnparkDome()); 1091 schedulerUnparkMount->setChecked(Options::schedulerUnparkMount()); 1092 schedulerOpenDustCover->setChecked(Options::schedulerOpenDustCover()); 1093 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy())); 1094 errorHandlingStrategyDelay->setValue(Options::errorHandlingStrategyDelay()); 1095 errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); 1096 schedulerStartupScript->setText(moduleState()->startupScriptURL().toString(QUrl::PreferLocalFile)); 1097 schedulerShutdownScript->setText(moduleState()->shutdownScriptURL().toString(QUrl::PreferLocalFile)); 1098 1099 if (process()->captureInterface() != nullptr) 1100 { 1101 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl"); 1102 if (hasCoolerControl.isValid()) 1103 { 1104 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool()); 1105 moduleState()->setCaptureReady(true); 1106 } 1107 } 1108 } 1109 1110 void Scheduler::updateNightTime(SchedulerJob const *job) 1111 { 1112 if (job == nullptr) 1113 { 1114 int const currentRow = moduleState()->currentPosition(); 1115 if (0 < currentRow && currentRow < moduleState()->jobs().size()) 1116 job = moduleState()->jobs().at(currentRow); 1117 1118 if (job == nullptr) 1119 { 1120 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Cannot update night time, no matching job found at line" << currentRow; 1121 return; 1122 } 1123 } 1124 1125 QDateTime const dawn = job ? job->getDawnAstronomicalTwilight() : moduleState()->Dawn(); 1126 QDateTime const dusk = job ? job->getDuskAstronomicalTwilight() : moduleState()->Dusk(); 1127 1128 QChar const warning(dawn == dusk ? 0x26A0 : '-'); 1129 nightTime->setText(i18n("%1 %2 %3", dusk.toString("hh:mm"), warning, dawn.toString("hh:mm"))); 1130 } 1131 1132 void Scheduler::loadJob(QModelIndex i) 1133 { 1134 if (jobUnderEdit == i.row()) 1135 return; 1136 1137 SchedulerJob * const job = moduleState()->jobs().at(i.row()); 1138 1139 if (job == nullptr) 1140 return; 1141 1142 watchJobChanges(false); 1143 1144 //job->setState(SCHEDJOB_IDLE); 1145 //job->setStage(SCHEDSTAGE_IDLE); 1146 syncGUIToJob(job); 1147 1148 /* Turn the add button into an apply button */ 1149 setJobAddApply(false); 1150 1151 /* Disable scheduler start/evaluate buttons */ 1152 startB->setEnabled(false); 1153 evaluateOnlyB->setEnabled(false); 1154 1155 /* Don't let the end-user remove a job being edited */ 1156 setJobManipulation(false, false); 1157 1158 jobUnderEdit = i.row(); 1159 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg( 1160 jobUnderEdit + 1); 1161 1162 watchJobChanges(true); 1163 } 1164 1165 void Scheduler::updateSchedulerURL(const QString &fileURL) 1166 { 1167 schedulerURL = QUrl::fromLocalFile(fileURL); 1168 // update save button tool tip 1169 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName()); 1170 } 1171 1172 void Scheduler::queueTableSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) 1173 { 1174 Q_UNUSED(deselected) 1175 1176 1177 if (jobChangesAreWatched == false || selected.empty()) 1178 // || (current.row() + 1) > moduleState()->jobs().size()) 1179 return; 1180 1181 const QModelIndex current = selected.indexes().first(); 1182 // this should not happen, but avoids crashes 1183 if ((current.row() + 1) > moduleState()->jobs().size()) 1184 { 1185 qCWarning(KSTARS_EKOS_SCHEDULER()) << "Unexpected row number" << current.row() << "- ignoring."; 1186 return; 1187 } 1188 moduleState()->setCurrentPosition(current.row()); 1189 SchedulerJob * const job = moduleState()->jobs().at(current.row()); 1190 1191 if (job != nullptr) 1192 { 1193 if (jobUnderEdit < 0) 1194 syncGUIToJob(job); 1195 else if (jobUnderEdit != current.row()) 1196 { 1197 // avoid changing the UI values for the currently edited job 1198 appendLogText(i18n("Stop editing of job #%1, resetting to original value.", jobUnderEdit + 1)); 1199 resetJobEdit(); 1200 syncGUIToJob(job); 1201 } 1202 } 1203 else nightTime->setText("-"); 1204 } 1205 1206 void Scheduler::clickQueueTable(QModelIndex index) 1207 { 1208 setJobManipulation(index.isValid(), index.isValid()); 1209 } 1210 1211 void Scheduler::setJobAddApply(bool add_mode) 1212 { 1213 if (add_mode) 1214 { 1215 addToQueueB->setIcon(QIcon::fromTheme("list-add")); 1216 addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list.")); 1217 addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); 1218 } 1219 else 1220 { 1221 addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); 1222 addToQueueB->setToolTip(i18n("Apply job changes.")); 1223 } 1224 // check if the button should be enabled 1225 checkJobInputComplete(); 1226 } 1227 1228 void Scheduler::setJobManipulation(bool can_reorder, bool can_delete) 1229 { 1230 if (can_reorder) 1231 { 1232 int const currentRow = moduleState()->currentPosition(); 1233 queueUpB->setEnabled(0 < currentRow); 1234 queueDownB->setEnabled(currentRow < queueTable->rowCount() - 1); 1235 } 1236 else 1237 { 1238 queueUpB->setEnabled(false); 1239 queueDownB->setEnabled(false); 1240 } 1241 sortJobsB->setEnabled(can_reorder); 1242 removeFromQueueB->setEnabled(can_delete); 1243 } 1244 1245 bool Scheduler::reorderJobs(QList<SchedulerJob*> reordered_sublist) 1246 { 1247 /* Add jobs not reordered at the end of the list, in initial order */ 1248 foreach (SchedulerJob* job, moduleState()->jobs()) 1249 if (!reordered_sublist.contains(job)) 1250 reordered_sublist.append(job); 1251 1252 if (moduleState()->jobs() != reordered_sublist) 1253 { 1254 /* Remember job currently selected */ 1255 int const selectedRow = moduleState()->currentPosition(); 1256 SchedulerJob * const selectedJob = 0 <= selectedRow ? moduleState()->jobs().at(selectedRow) : nullptr; 1257 1258 /* Reassign list */ 1259 moduleState()->setJobs(reordered_sublist); 1260 1261 /* Refresh the table */ 1262 for (SchedulerJob *job : moduleState()->jobs()) 1263 updateJobTable(job); 1264 1265 /* Reselect previously selected job */ 1266 if (nullptr != selectedJob) 1267 moduleState()->setCurrentPosition(moduleState()->jobs().indexOf(selectedJob)); 1268 1269 return true; 1270 } 1271 else return false; 1272 } 1273 1274 void Scheduler::moveJobUp() 1275 { 1276 int const rowCount = queueTable->rowCount(); 1277 int const currentRow = queueTable->currentRow(); 1278 int const destinationRow = currentRow - 1; 1279 1280 /* No move if no job selected, if table has one line or less or if destination is out of table */ 1281 if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) 1282 return; 1283 1284 /* Swap jobs in the list */ 1285 #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) 1286 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow); 1287 #else 1288 moduleState()->jobs().swap(currentRow, destinationRow); 1289 #endif 1290 1291 //Update the two table rows 1292 updateJobTable(moduleState()->jobs().at(currentRow)); 1293 updateJobTable(moduleState()->jobs().at(destinationRow)); 1294 1295 /* Move selection to destination row */ 1296 moduleState()->setCurrentPosition(destinationRow); 1297 setJobManipulation(true, true); 1298 1299 /* Make list modified and evaluate jobs */ 1300 moduleState()->setDirty(true); 1301 process()->evaluateJobs(true); 1302 } 1303 1304 void Scheduler::moveJobDown() 1305 { 1306 int const rowCount = queueTable->rowCount(); 1307 int const currentRow = queueTable->currentRow(); 1308 int const destinationRow = currentRow + 1; 1309 1310 /* No move if no job selected, if table has one line or less or if destination is out of table */ 1311 if (currentRow < 0 || rowCount <= 1 || destinationRow >= rowCount) 1312 return; 1313 1314 /* Swap jobs in the list */ 1315 #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) 1316 moduleState()->mutlableJobs().swapItemsAt(currentRow, destinationRow); 1317 #else 1318 moduleState()->mutlableJobs().swap(currentRow, destinationRow); 1319 #endif 1320 1321 //Update the two table rows 1322 updateJobTable(moduleState()->jobs().at(currentRow)); 1323 updateJobTable(moduleState()->jobs().at(destinationRow)); 1324 1325 /* Move selection to destination row */ 1326 moduleState()->setCurrentPosition(destinationRow); 1327 setJobManipulation(true, true); 1328 1329 /* Make list modified and evaluate jobs */ 1330 moduleState()->setDirty(true); 1331 process()->evaluateJobs(true); 1332 } 1333 1334 void Scheduler::updateJobTable(SchedulerJob *job) 1335 { 1336 // handle full table update 1337 if (job == nullptr) 1338 { 1339 for (auto onejob : moduleState()->jobs()) 1340 updateJobTable(onejob); 1341 1342 return; 1343 } 1344 1345 const int row = moduleState()->jobs().indexOf(job); 1346 // Ignore unknown jobs 1347 if (row < 0) 1348 return; 1349 // ensure that the row in the table exists 1350 if (row >= queueTable->rowCount()) 1351 insertJobTableRow(row - 1, false); 1352 1353 QTableWidgetItem *nameCell = queueTable->item(row, static_cast<int>(SCHEDCOL_NAME)); 1354 QTableWidgetItem *statusCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STATUS)); 1355 QTableWidgetItem *altitudeCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ALTITUDE)); 1356 QTableWidgetItem *startupCell = queueTable->item(row, static_cast<int>(SCHEDCOL_STARTTIME)); 1357 QTableWidgetItem *completionCell = queueTable->item(row, static_cast<int>(SCHEDCOL_ENDTIME)); 1358 QTableWidgetItem *captureCountCell = queueTable->item(row, static_cast<int>(SCHEDCOL_CAPTURES)); 1359 1360 // Only in testing. 1361 if (!nameCell) return; 1362 1363 if (nullptr != nameCell) 1364 { 1365 nameCell->setText(job->getName()); 1366 updateCellStyle(job, nameCell); 1367 if (nullptr != nameCell->tableWidget()) 1368 nameCell->tableWidget()->resizeColumnToContents(nameCell->column()); 1369 } 1370 1371 if (nullptr != statusCell) 1372 { 1373 static QMap<SchedulerJobStatus, QString> stateStrings; 1374 static QString stateStringUnknown; 1375 if (stateStrings.isEmpty()) 1376 { 1377 stateStrings[SCHEDJOB_IDLE] = i18n("Idle"); 1378 stateStrings[SCHEDJOB_EVALUATION] = i18n("Evaluating"); 1379 stateStrings[SCHEDJOB_SCHEDULED] = i18n("Scheduled"); 1380 stateStrings[SCHEDJOB_BUSY] = i18n("Running"); 1381 stateStrings[SCHEDJOB_INVALID] = i18n("Invalid"); 1382 stateStrings[SCHEDJOB_COMPLETE] = i18n("Complete"); 1383 stateStrings[SCHEDJOB_ABORTED] = i18n("Aborted"); 1384 stateStrings[SCHEDJOB_ERROR] = i18n("Error"); 1385 stateStringUnknown = i18n("Unknown"); 1386 } 1387 statusCell->setText(stateStrings.value(job->getState(), stateStringUnknown)); 1388 updateCellStyle(job, statusCell); 1389 1390 if (nullptr != statusCell->tableWidget()) 1391 statusCell->tableWidget()->resizeColumnToContents(statusCell->column()); 1392 } 1393 1394 if (nullptr != startupCell) 1395 { 1396 auto time = (job->getState() == SCHEDJOB_BUSY) ? job->getStateTime() : job->getStartupTime(); 1397 /* Display startup time if it is valid */ 1398 if (time.isValid()) 1399 { 1400 startupCell->setText(QString("%1%2%L3° %4") 1401 .arg(job->getAltitudeAtStartup() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "") 1402 .arg(QChar(job->isSettingAtStartup() ? 0x2193 : 0x2191)) 1403 .arg(job->getAltitudeAtStartup(), 0, 'f', 1) 1404 .arg(time.toString(startupTimeEdit->displayFormat()))); 1405 1406 switch (job->getFileStartupCondition()) 1407 { 1408 /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */ 1409 case START_AT: 1410 startupCell->setIcon(QIcon::fromTheme("chronometer")); 1411 break; 1412 1413 /* If the original condition is START_ASAP, startup time is informational */ 1414 case START_ASAP: 1415 startupCell->setIcon(QIcon()); 1416 break; 1417 1418 default: 1419 break; 1420 } 1421 } 1422 /* Else do not display any startup time */ 1423 else 1424 { 1425 startupCell->setText("-"); 1426 startupCell->setIcon(QIcon()); 1427 } 1428 1429 updateCellStyle(job, startupCell); 1430 1431 if (nullptr != startupCell->tableWidget()) 1432 startupCell->tableWidget()->resizeColumnToContents(startupCell->column()); 1433 } 1434 1435 if (nullptr != altitudeCell) 1436 { 1437 // FIXME: Cache altitude calculations 1438 bool is_setting = false; 1439 double const alt = SchedulerUtils::findAltitude(job->getTargetCoords(), QDateTime(), &is_setting); 1440 1441 altitudeCell->setText(QString("%1%L2°") 1442 .arg(QChar(is_setting ? 0x2193 : 0x2191)) 1443 .arg(alt, 0, 'f', 1)); 1444 updateCellStyle(job, altitudeCell); 1445 1446 if (nullptr != altitudeCell->tableWidget()) 1447 altitudeCell->tableWidget()->resizeColumnToContents(altitudeCell->column()); 1448 } 1449 1450 if (nullptr != completionCell) 1451 { 1452 if (job->getGreedyCompletionTime().isValid()) 1453 { 1454 completionCell->setText(QString("%1") 1455 .arg(job->getGreedyCompletionTime().toString("hh:mm"))); 1456 } 1457 else 1458 /* Display completion time if it is valid and job is not looping */ 1459 if (FINISH_LOOP != job->getCompletionCondition() && job->getCompletionTime().isValid()) 1460 { 1461 completionCell->setText(QString("%1%2%L3° %4") 1462 .arg(job->getAltitudeAtCompletion() < job->getMinAltitude() ? QString(QChar(0x26A0)) : "") 1463 .arg(QChar(job->isSettingAtCompletion() ? 0x2193 : 0x2191)) 1464 .arg(job->getAltitudeAtCompletion(), 0, 'f', 1) 1465 .arg(job->getCompletionTime().toString(startupTimeEdit->displayFormat()))); 1466 1467 switch (job->getCompletionCondition()) 1468 { 1469 case FINISH_AT: 1470 completionCell->setIcon(QIcon::fromTheme("chronometer")); 1471 break; 1472 1473 case FINISH_SEQUENCE: 1474 case FINISH_REPEAT: 1475 default: 1476 completionCell->setIcon(QIcon()); 1477 break; 1478 } 1479 } 1480 /* Else do not display any completion time */ 1481 else 1482 { 1483 completionCell->setText("-"); 1484 completionCell->setIcon(QIcon()); 1485 } 1486 1487 updateCellStyle(job, completionCell); 1488 if (nullptr != completionCell->tableWidget()) 1489 completionCell->tableWidget()->resizeColumnToContents(completionCell->column()); 1490 } 1491 1492 if (nullptr != captureCountCell) 1493 { 1494 switch (job->getCompletionCondition()) 1495 { 1496 case FINISH_AT: 1497 // FIXME: Attempt to calculate the number of frames until end - requires detailed imaging time 1498 1499 case FINISH_LOOP: 1500 // If looping, display the count of completed frames 1501 captureCountCell->setText(QString("%L1/-").arg(job->getCompletedCount())); 1502 break; 1503 1504 case FINISH_SEQUENCE: 1505 case FINISH_REPEAT: 1506 default: 1507 // If repeating, display the count of completed frames to the count of requested frames 1508 captureCountCell->setText(QString("%L1/%L2").arg(job->getCompletedCount()).arg(job->getSequenceCount())); 1509 break; 1510 } 1511 1512 updateCellStyle(job, captureCountCell); 1513 if (nullptr != captureCountCell->tableWidget()) 1514 captureCountCell->tableWidget()->resizeColumnToContents(captureCountCell->column()); 1515 } 1516 1517 emit jobsUpdated(moduleState()->getJSONJobs()); 1518 } 1519 1520 void Scheduler::insertJobTableRow(int row, bool above) 1521 { 1522 const int pos = above ? row : row + 1; 1523 1524 // ensure that there are no gaps 1525 if (row > queueTable->rowCount()) 1526 insertJobTableRow(row - 1, above); 1527 1528 queueTable->insertRow(pos); 1529 1530 QTableWidgetItem *nameCell = new QTableWidgetItem(); 1531 queueTable->setItem(row, static_cast<int>(SCHEDCOL_NAME), nameCell); 1532 nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1533 nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1534 1535 QTableWidgetItem *statusCell = new QTableWidgetItem(); 1536 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STATUS), statusCell); 1537 statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1538 statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1539 1540 QTableWidgetItem *captureCount = new QTableWidgetItem(); 1541 queueTable->setItem(row, static_cast<int>(SCHEDCOL_CAPTURES), captureCount); 1542 captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1543 captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1544 1545 QTableWidgetItem *startupCell = new QTableWidgetItem(); 1546 queueTable->setItem(row, static_cast<int>(SCHEDCOL_STARTTIME), startupCell); 1547 startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1548 startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1549 1550 QTableWidgetItem *altitudeCell = new QTableWidgetItem(); 1551 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ALTITUDE), altitudeCell); 1552 altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1553 altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1554 1555 QTableWidgetItem *completionCell = new QTableWidgetItem(); 1556 queueTable->setItem(row, static_cast<int>(SCHEDCOL_ENDTIME), completionCell); 1557 completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); 1558 completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 1559 } 1560 1561 void Scheduler::updateCellStyle(SchedulerJob *job, QTableWidgetItem *cell) 1562 { 1563 QFont font(cell->font()); 1564 font.setBold(job->getState() == SCHEDJOB_BUSY); 1565 font.setItalic(job->getState() == SCHEDJOB_BUSY); 1566 cell->setFont(font); 1567 } 1568 1569 void Scheduler::resetJobEdit() 1570 { 1571 if (jobUnderEdit < 0) 1572 return; 1573 1574 SchedulerJob * const job = moduleState()->jobs().at(jobUnderEdit); 1575 Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid"); 1576 1577 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg( 1578 jobUnderEdit + 1); 1579 1580 jobUnderEdit = -1; 1581 1582 watchJobChanges(false); 1583 1584 /* Revert apply button to add */ 1585 setJobAddApply(true); 1586 1587 /* Refresh state of job manipulation buttons */ 1588 setJobManipulation(true, true); 1589 1590 /* Restore scheduler operation buttons */ 1591 evaluateOnlyB->setEnabled(true); 1592 startB->setEnabled(true); 1593 1594 watchJobChanges(true); 1595 Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode"); 1596 } 1597 1598 void Scheduler::removeJob() 1599 { 1600 int currentRow = moduleState()->currentPosition(); 1601 1602 watchJobChanges(false); 1603 if (moduleState()->removeJob(currentRow) == false) 1604 return; 1605 1606 /* removing the job succeeded, update UI */ 1607 /* Remove the job from the table */ 1608 queueTable->removeRow(currentRow); 1609 1610 /* If there are no job rows left, update UI buttons */ 1611 if (queueTable->rowCount() == 0) 1612 { 1613 setJobManipulation(false, false); 1614 evaluateOnlyB->setEnabled(false); 1615 queueSaveAsB->setEnabled(false); 1616 queueSaveB->setEnabled(false); 1617 startB->setEnabled(false); 1618 pauseB->setEnabled(false); 1619 } 1620 1621 // Otherwise, clear the selection, leave the UI values holding the values of the removed job. 1622 // The position in the job list, where the job has been removed from, is still held in the module state. 1623 // This leaves the option directly adding the old values reverting the deletion. 1624 else 1625 queueTable->clearSelection(); 1626 1627 /* If needed, reset edit mode to clean up UI */ 1628 if (jobUnderEdit >= 0) 1629 resetJobEdit(); 1630 1631 watchJobChanges(true); 1632 process()->evaluateJobs(true); 1633 emit jobsUpdated(moduleState()->getJSONJobs()); 1634 updateJobTable(); 1635 // disable moving and deleting, since selection is cleared 1636 setJobManipulation(false, false); 1637 } 1638 1639 void Scheduler::removeOneJob(int index) 1640 { 1641 moduleState()->setCurrentPosition(index); 1642 removeJob(); 1643 } 1644 void Scheduler::toggleScheduler() 1645 { 1646 if (moduleState()->schedulerState() == SCHEDULER_RUNNING) 1647 { 1648 moduleState()->disablePreemptiveShutdown(); 1649 stop(); 1650 } 1651 else 1652 start(); 1653 } 1654 1655 void Scheduler::stop() 1656 { 1657 process()->stopScheduler(); 1658 } 1659 1660 void Scheduler::pause() 1661 { 1662 moduleState()->setSchedulerState(SCHEDULER_PAUSED); 1663 emit newStatus(moduleState()->schedulerState()); 1664 appendLogText(i18n("Scheduler pause planned...")); 1665 pauseB->setEnabled(false); 1666 1667 startB->setIcon(QIcon::fromTheme("media-playback-start")); 1668 startB->setToolTip(i18n("Resume Scheduler")); 1669 } 1670 1671 void Scheduler::syncGreedyParams() 1672 { 1673 process()->getGreedyScheduler()->setParams( 1674 errorHandlingRestartImmediatelyButton->isChecked(), 1675 errorHandlingRestartQueueButton->isChecked(), 1676 errorHandlingRescheduleErrorsCB->isChecked(), 1677 errorHandlingStrategyDelay->value(), 1678 errorHandlingStrategyDelay->value()); 1679 } 1680 1681 void Scheduler::handleShutdownStarted() 1682 { 1683 KSNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process"), 1684 KSNotification::Scheduler); 1685 weatherLabel->hide(); 1686 } 1687 1688 void Ekos::Scheduler::changeSleepLabel(QString text, bool show) 1689 { 1690 sleepLabel->setToolTip(text); 1691 if (show) 1692 sleepLabel->show(); 1693 else 1694 sleepLabel->hide(); 1695 } 1696 1697 void Scheduler::schedulerStopped() 1698 { 1699 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_NOTHING).toLatin1().data()); 1700 1701 // Update job table rows for aborted ones (the others remain unchanged in their state) 1702 bool wasAborted = false; 1703 for (auto &oneJob : moduleState()->jobs()) 1704 { 1705 if (oneJob->getState() == SCHEDJOB_ABORTED) 1706 { 1707 updateJobTable(oneJob); 1708 wasAborted = true; 1709 } 1710 } 1711 1712 if (wasAborted) 1713 KSNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted."), KSNotification::Scheduler, 1714 KSNotification::Alert); 1715 1716 startupB->setEnabled(true); 1717 shutdownB->setEnabled(true); 1718 1719 // If soft shutdown, we return for now 1720 if (moduleState()->preemptiveShutdown()) 1721 { 1722 changeSleepLabel(i18n("Scheduler is in shutdown until next job is ready")); 1723 pi->stopAnimation(); 1724 return; 1725 } 1726 1727 changeSleepLabel("", false); 1728 1729 startB->setIcon(QIcon::fromTheme("media-playback-start")); 1730 startB->setToolTip(i18n("Start Scheduler")); 1731 pauseB->setEnabled(false); 1732 //startB->setText("Start Scheduler"); 1733 1734 queueLoadB->setEnabled(true); 1735 queueAppendB->setEnabled(true); 1736 addToQueueB->setEnabled(true); 1737 setJobManipulation(false, false); 1738 //mosaicB->setEnabled(true); 1739 evaluateOnlyB->setEnabled(true); 1740 } 1741 1742 1743 void Scheduler::load(bool clearQueue, const QString &filename) 1744 { 1745 QUrl fileURL; 1746 1747 if (filename.isEmpty()) 1748 fileURL = QFileDialog::getOpenFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Open Ekos Scheduler List"), 1749 dirPath, 1750 "Ekos Scheduler List (*.esl)"); 1751 else fileURL.setUrl(filename); 1752 1753 if (fileURL.isEmpty()) 1754 return; 1755 1756 if (fileURL.isValid() == false) 1757 { 1758 QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); 1759 KSNotification::sorry(message, i18n("Invalid URL")); 1760 return; 1761 } 1762 1763 dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); 1764 1765 if (clearQueue) 1766 removeAllJobs(); 1767 // remember toe number of rows to select the first one appended 1768 const int row = moduleState()->jobs().count(); 1769 1770 // do not update while appending 1771 watchJobChanges(false); 1772 // try appending the jobs from the file to the job list 1773 const bool success = process()->appendEkosScheduleList(fileURL.toLocalFile()); 1774 // turn on whatching 1775 watchJobChanges(true); 1776 1777 if (success) 1778 { 1779 // select the first appended row (if any was added) 1780 if (moduleState()->jobs().count() > row) 1781 moduleState()->setCurrentPosition(row); 1782 1783 /* Run a job idle evaluation after a successful load */ 1784 startJobEvaluation(); 1785 } 1786 } 1787 1788 void Scheduler::removeAllJobs() 1789 { 1790 if (jobUnderEdit >= 0) 1791 resetJobEdit(); 1792 1793 while (queueTable->rowCount() > 0) 1794 queueTable->removeRow(0); 1795 1796 qDeleteAll(moduleState()->jobs()); 1797 moduleState()->mutlableJobs().clear(); 1798 } 1799 1800 bool Scheduler::loadScheduler(const QString &fileURL) 1801 { 1802 removeAllJobs(); 1803 return process()->appendEkosScheduleList(fileURL); 1804 } 1805 1806 void Scheduler::saveAs() 1807 { 1808 schedulerURL.clear(); 1809 save(); 1810 } 1811 1812 void Scheduler::save() 1813 { 1814 QUrl backupCurrent = schedulerURL; 1815 1816 if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) 1817 schedulerURL.clear(); 1818 1819 // If no changes made, return. 1820 if (moduleState()->dirty() == false && !schedulerURL.isEmpty()) 1821 return; 1822 1823 if (schedulerURL.isEmpty()) 1824 { 1825 schedulerURL = 1826 QFileDialog::getSaveFileUrl(Ekos::Manager::Instance(), i18nc("@title:window", "Save Ekos Scheduler List"), dirPath, 1827 "Ekos Scheduler List (*.esl)"); 1828 // if user presses cancel 1829 if (schedulerURL.isEmpty()) 1830 { 1831 schedulerURL = backupCurrent; 1832 return; 1833 } 1834 1835 dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); 1836 1837 if (schedulerURL.toLocalFile().contains('.') == 0) 1838 schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); 1839 } 1840 1841 if (schedulerURL.isValid()) 1842 { 1843 if ((saveScheduler(schedulerURL)) == false) 1844 { 1845 KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save")); 1846 return; 1847 } 1848 1849 // update save button tool tip 1850 queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName()); 1851 } 1852 else 1853 { 1854 QString message = i18n("Invalid URL: %1", schedulerURL.url()); 1855 KSNotification::sorry(message, i18n("Invalid URL")); 1856 } 1857 } 1858 1859 void Scheduler::checkJobInputComplete() 1860 { 1861 // For object selection, all fields must be filled 1862 bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); 1863 1864 // For FITS selection, only the name and fits URL should be filled. 1865 bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); 1866 1867 // Sequence selection is required 1868 bool const seqSelectionOK = !sequenceEdit->text().isEmpty(); 1869 1870 // Finally, adding is allowed upon object/FITS and sequence selection 1871 bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK; 1872 1873 addToQueueB->setEnabled(addingOK); 1874 } 1875 1876 void Scheduler::setDirty() 1877 { 1878 // check if all fields are filled to allow adding a job 1879 checkJobInputComplete(); 1880 1881 // ignore changes that are a result of syncGUIToJob() or syncGUIToGeneralSettings() 1882 if (jobUnderEdit < 0) 1883 return; 1884 1885 moduleState()->setDirty(true); 1886 1887 if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) 1888 return; 1889 1890 // update state 1891 if (sender() == schedulerStartupScript) 1892 moduleState()->setStartupScriptURL(QUrl::fromUserInput(schedulerStartupScript->text())); 1893 else if (sender() == schedulerShutdownScript) 1894 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(schedulerShutdownScript->text())); 1895 } 1896 1897 void Scheduler::startJobEvaluation() 1898 { 1899 // Reset all jobs 1900 // other states too? 1901 if (SCHEDULER_RUNNING != moduleState()->schedulerState()) 1902 process()->resetJobs(); 1903 1904 // reset the iterations counter 1905 moduleState()->resetSequenceExecutionCounter(); 1906 1907 // And evaluate all pending jobs per the conditions set in each 1908 process()->evaluateJobs(true); 1909 } 1910 1911 void Scheduler::sortJobsPerAltitude() 1912 { 1913 // We require a first job to sort, so bail out if list is empty 1914 if (moduleState()->jobs().isEmpty()) 1915 return; 1916 1917 // Don't reset current job 1918 // setCurrentJob(nullptr); 1919 1920 // Don't reset scheduler jobs startup times before sorting - we need the first job startup time 1921 1922 // Sort by startup time, using the first job time as reference for altitude calculations 1923 using namespace std::placeholders; 1924 QList<SchedulerJob*> sortedJobs = moduleState()->jobs(); 1925 std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), 1926 std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, moduleState()->jobs().first()->getStartupTime())); 1927 1928 // If order changed, reset and re-evaluate 1929 if (reorderJobs(sortedJobs)) 1930 { 1931 for (SchedulerJob * job : moduleState()->jobs()) 1932 job->reset(); 1933 1934 process()->evaluateJobs(true); 1935 } 1936 } 1937 1938 void Scheduler::resumeCheckStatus() 1939 { 1940 disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); 1941 TEST_PRINT(stderr, "%d Setting %s\n", __LINE__, timerStr(RUN_SCHEDULER).toLatin1().data()); 1942 moduleState()->setupNextIteration(RUN_SCHEDULER); 1943 } 1944 1945 ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() 1946 { 1947 // The UI holds the state 1948 if (errorHandlingRestartQueueButton->isChecked()) 1949 return ERROR_RESTART_AFTER_TERMINATION; 1950 else if (errorHandlingRestartImmediatelyButton->isChecked()) 1951 return ERROR_RESTART_IMMEDIATELY; 1952 else 1953 return ERROR_DONT_RESTART; 1954 } 1955 1956 void Scheduler::setErrorHandlingStrategy(ErrorHandlingStrategy strategy) 1957 { 1958 errorHandlingStrategyDelay->setEnabled(strategy != ERROR_DONT_RESTART); 1959 1960 switch (strategy) 1961 { 1962 case ERROR_RESTART_AFTER_TERMINATION: 1963 errorHandlingRestartQueueButton->setChecked(true); 1964 break; 1965 case ERROR_RESTART_IMMEDIATELY: 1966 errorHandlingRestartImmediatelyButton->setChecked(true); 1967 break; 1968 default: 1969 errorHandlingDontRestartButton->setChecked(true); 1970 break; 1971 } 1972 } 1973 1974 // Can't use a SchedulerAlgorithm type for the arg here 1975 // as the compiler is unhappy connecting the signals currentIndexChanged(int) 1976 // or activated(int) to an enum. 1977 void Scheduler::setAlgorithm(int algIndex) 1978 { 1979 if (algIndex != ALGORITHM_GREEDY) 1980 { 1981 appendLogText(i18n("Warning: The Classic scheduler algorithm has been retired. Switching you to the Greedy algorithm.")); 1982 algIndex = ALGORITHM_GREEDY; 1983 } 1984 Options::setSchedulerAlgorithm(algIndex); 1985 1986 groupLabel->setDisabled(false); 1987 groupEdit->setDisabled(false); 1988 queueTable->model()->setHeaderData(START_TIME_COLUMN, Qt::Horizontal, tr("Next Start")); 1989 queueTable->model()->setHeaderData(END_TIME_COLUMN, Qt::Horizontal, tr("Next End")); 1990 queueTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 1991 } 1992 1993 void Scheduler::resetAllJobs() 1994 { 1995 if (moduleState()->schedulerState() == SCHEDULER_RUNNING) 1996 return; 1997 1998 // Reset capture count of all jobs before re-evaluating 1999 foreach (SchedulerJob *job, moduleState()->jobs()) 2000 job->setCompletedCount(0); 2001 2002 // Evaluate all jobs, this refreshes storage and resets job states 2003 startJobEvaluation(); 2004 } 2005 2006 void Scheduler::checkTwilightWarning(bool enabled) 2007 { 2008 if (enabled) 2009 return; 2010 2011 if (KMessageBox::warningContinueCancel( 2012 nullptr, 2013 i18n("Turning off astronomial twilight check may cause the observatory " 2014 "to run during daylight. This can cause irreversible damage to your equipment!"), 2015 i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), 2016 "astronomical_twilight_warning") == KMessageBox::Cancel) 2017 { 2018 schedulerTwilight->setChecked(true); 2019 } 2020 } 2021 2022 2023 void Scheduler::updateProfiles() 2024 { 2025 schedulerProfileCombo->blockSignals(true); 2026 schedulerProfileCombo->clear(); 2027 schedulerProfileCombo->addItems(moduleState()->profiles()); 2028 schedulerProfileCombo->setCurrentText(moduleState()->currentProfile()); 2029 schedulerProfileCombo->blockSignals(false); 2030 } 2031 2032 void Scheduler::updateJobStageUI(SchedulerJobStage stage) 2033 { 2034 /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */ 2035 /* 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? */ 2036 static QMap<SchedulerJobStage, QString> stageStrings; 2037 static QString stageStringUnknown; 2038 if (stageStrings.isEmpty()) 2039 { 2040 stageStrings[SCHEDSTAGE_IDLE] = i18n("Idle"); 2041 stageStrings[SCHEDSTAGE_SLEWING] = i18n("Slewing"); 2042 stageStrings[SCHEDSTAGE_SLEW_COMPLETE] = i18n("Slew complete"); 2043 stageStrings[SCHEDSTAGE_FOCUSING] = 2044 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING] = i18n("Focusing"); 2045 stageStrings[SCHEDSTAGE_FOCUS_COMPLETE] = 2046 stageStrings[SCHEDSTAGE_POSTALIGN_FOCUSING_COMPLETE ] = i18n("Focus complete"); 2047 stageStrings[SCHEDSTAGE_ALIGNING] = i18n("Aligning"); 2048 stageStrings[SCHEDSTAGE_ALIGN_COMPLETE] = i18n("Align complete"); 2049 stageStrings[SCHEDSTAGE_RESLEWING] = i18n("Repositioning"); 2050 stageStrings[SCHEDSTAGE_RESLEWING_COMPLETE] = i18n("Repositioning complete"); 2051 /*stageStrings[SCHEDSTAGE_CALIBRATING] = i18n("Calibrating");*/ 2052 stageStrings[SCHEDSTAGE_GUIDING] = i18n("Guiding"); 2053 stageStrings[SCHEDSTAGE_GUIDING_COMPLETE] = i18n("Guiding complete"); 2054 stageStrings[SCHEDSTAGE_CAPTURING] = i18n("Capturing"); 2055 stageStringUnknown = i18n("Unknown"); 2056 } 2057 2058 if (activeJob() == nullptr) 2059 jobStatus->setText(stageStrings[SCHEDSTAGE_IDLE]); 2060 else 2061 jobStatus->setText(QString("%1: %2").arg(activeJob()->getName(), 2062 stageStrings.value(stage, stageStringUnknown))); 2063 2064 } 2065 2066 2067 void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status) 2068 { 2069 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %d\n", __LINE__, "ekosInterface:indiStatusChanged", status); 2070 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status; 2071 2072 moduleState()->setIndiCommunicationStatus(status); 2073 } 2074 2075 void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status) 2076 { 2077 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %d\n", __LINE__, "ekosInterface:ekosStatusChanged", status); 2078 qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status; 2079 2080 moduleState()->setEkosCommunicationStatus(status); 2081 } 2082 2083 void Scheduler::simClockScaleChanged(float newScale) 2084 { 2085 if (moduleState()->currentlySleeping()) 2086 { 2087 QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround(static_cast<double> 2088 (moduleState()->iterationTimer().remainingTime()) 2089 * KStarsData::Instance()->clock()->scale() 2090 / newScale)); 2091 appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...", 2092 remainingTimeMs.toString("hh:mm:ss"))); 2093 moduleState()->iterationTimer().stop(); 2094 moduleState()->iterationTimer().start(remainingTimeMs.msecsSinceStartOfDay()); 2095 } 2096 } 2097 2098 void Scheduler::simClockTimeChanged() 2099 { 2100 moduleState()->calculateDawnDusk(); 2101 updateNightTime(); 2102 2103 // If the Scheduler is not running, reset all jobs and re-evaluate from a new current start point 2104 if (SCHEDULER_RUNNING != moduleState()->schedulerState()) 2105 { 2106 startJobEvaluation(); 2107 } 2108 } 2109 2110 void Scheduler::registerNewDevice(const QString &name, int interface) 2111 { 2112 Q_UNUSED(name) 2113 2114 if (interface & INDI::BaseDevice::DOME_INTERFACE) 2115 { 2116 QList<QVariant> dbusargs; 2117 dbusargs.append(INDI::BaseDevice::DOME_INTERFACE); 2118 QDBusReply<QStringList> paths = process()->indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths", 2119 dbusargs); 2120 if (paths.error().type() == QDBusError::NoError) 2121 { 2122 // Select last device in case a restarted caused multiple instances in the tree 2123 setDomePathString(paths.value().last()); 2124 delete process()->domeInterface(); 2125 process()->setDomeInterface(new QDBusInterface(kstarsInterfaceString, domePathString, domeInterfaceString, 2126 QDBusConnection::sessionBus(), this)); 2127 connect(process()->domeInterface(), SIGNAL(ready()), this, SLOT(syncProperties())); 2128 checkInterfaceReady(process()->domeInterface()); 2129 } 2130 } 2131 2132 if (interface & INDI::BaseDevice::WEATHER_INTERFACE) 2133 { 2134 QList<QVariant> dbusargs; 2135 dbusargs.append(INDI::BaseDevice::WEATHER_INTERFACE); 2136 QDBusReply<QStringList> paths = process()->indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths", 2137 dbusargs); 2138 if (paths.error().type() == QDBusError::NoError) 2139 { 2140 // Select last device in case a restarted caused multiple instances in the tree 2141 setWeatherPathString(paths.value().last()); 2142 delete process()->weatherInterface(); 2143 process()->setWeatherInterface(new QDBusInterface(kstarsInterfaceString, weatherPathString, weatherInterfaceString, 2144 QDBusConnection::sessionBus(), this)); 2145 connect(process()->weatherInterface(), SIGNAL(ready()), this, SLOT(syncProperties())); 2146 connect(process()->weatherInterface(), SIGNAL(newStatus(ISD::Weather::Status)), this, 2147 SLOT(setWeatherStatus(ISD::Weather::Status))); 2148 checkInterfaceReady(process()->weatherInterface()); 2149 } 2150 } 2151 2152 if (interface & INDI::BaseDevice::DUSTCAP_INTERFACE) 2153 { 2154 QList<QVariant> dbusargs; 2155 dbusargs.append(INDI::BaseDevice::DUSTCAP_INTERFACE); 2156 QDBusReply<QStringList> paths = process()->indiInterface()->callWithArgumentList(QDBus::AutoDetect, "getDevicesPaths", 2157 dbusargs); 2158 if (paths.error().type() == QDBusError::NoError) 2159 { 2160 // Select last device in case a restarted caused multiple instances in the tree 2161 setDustCapPathString(paths.value().last()); 2162 delete process()->capInterface(); 2163 process()->setCapInterface(new QDBusInterface(kstarsInterfaceString, dustCapPathString, dustCapInterfaceString, 2164 QDBusConnection::sessionBus(), this)); 2165 connect(process()->capInterface(), SIGNAL(ready()), this, SLOT(syncProperties())); 2166 checkInterfaceReady(process()->capInterface()); 2167 } 2168 } 2169 } 2170 2171 void Scheduler::registerNewModule(const QString &name) 2172 { 2173 qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")"; 2174 2175 if (name == "Focus") 2176 { 2177 delete process()->focusInterface(); 2178 process()->setFocusInterface(new QDBusInterface(kstarsInterfaceString, focusPathString, focusInterfaceString, 2179 QDBusConnection::sessionBus(), this)); 2180 connect(process()->focusInterface(), SIGNAL(newStatus(Ekos::FocusState)), this, 2181 SLOT(setFocusStatus(Ekos::FocusState)), Qt::UniqueConnection); 2182 } 2183 else if (name == "Capture") 2184 { 2185 delete process()->captureInterface(); 2186 process()->setCaptureInterface(new QDBusInterface(kstarsInterfaceString, capturePathString, captureInterfaceString, 2187 QDBusConnection::sessionBus(), this)); 2188 2189 connect(process()->captureInterface(), SIGNAL(ready()), this, SLOT(syncProperties())); 2190 connect(process()->captureInterface(), SIGNAL(newStatus(Ekos::CaptureState)), this, 2191 SLOT(setCaptureStatus(Ekos::CaptureState)), 2192 Qt::UniqueConnection); 2193 connect(process()->captureInterface(), SIGNAL(captureComplete(QVariantMap)), this, SLOT(setCaptureComplete(QVariantMap)), 2194 Qt::UniqueConnection); 2195 checkInterfaceReady(process()->captureInterface()); 2196 } 2197 else if (name == "Mount") 2198 { 2199 delete process()->mountInterface(); 2200 process()->setMountInterface(new QDBusInterface(kstarsInterfaceString, mountPathString, mountInterfaceString, 2201 QDBusConnection::sessionBus(), this)); 2202 2203 connect(process()->mountInterface(), SIGNAL(ready()), this, SLOT(syncProperties())); 2204 connect(process()->mountInterface(), SIGNAL(newStatus(ISD::Mount::Status)), this, SLOT(setMountStatus(ISD::Mount::Status)), 2205 Qt::UniqueConnection); 2206 2207 checkInterfaceReady(process()->mountInterface()); 2208 } 2209 else if (name == "Align") 2210 { 2211 delete process()->alignInterface(); 2212 process()->setAlignInterface(new QDBusInterface(kstarsInterfaceString, alignPathString, alignInterfaceString, 2213 QDBusConnection::sessionBus(), this)); 2214 connect(process()->alignInterface(), SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), 2215 Qt::UniqueConnection); 2216 } 2217 else if (name == "Guide") 2218 { 2219 delete process()->guideInterface(); 2220 process()->setGuideInterface(new QDBusInterface(kstarsInterfaceString, guidePathString, guideInterfaceString, 2221 QDBusConnection::sessionBus(), this)); 2222 connect(process()->guideInterface(), SIGNAL(newStatus(Ekos::GuideState)), this, 2223 SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection); 2224 } 2225 } 2226 2227 void Scheduler::syncProperties() 2228 { 2229 QDBusInterface *iface = qobject_cast<QDBusInterface*>(sender()); 2230 2231 if (iface == process()->mountInterface()) 2232 { 2233 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "mountInterface:property", "canPark"); 2234 QVariant canMountPark = process()->mountInterface()->property("canPark"); 2235 TEST_PRINT(stderr, " @@@dbus received %s\n", !canMountPark.isValid() ? "invalid" : (canMountPark.toBool() ? "T" : "F")); 2236 2237 schedulerUnparkMount->setEnabled(canMountPark.toBool()); 2238 schedulerParkMount->setEnabled(canMountPark.toBool()); 2239 moduleState()->setMountReady(true); 2240 } 2241 else if (iface == process()->capInterface()) 2242 { 2243 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "dustCapInterface:property", "canPark"); 2244 QVariant canCapPark = process()->capInterface()->property("canPark"); 2245 TEST_PRINT(stderr, " @@@dbus received %s\n", !canCapPark.isValid() ? "invalid" : (canCapPark.toBool() ? "T" : "F")); 2246 2247 if (canCapPark.isValid()) 2248 { 2249 schedulerCloseDustCover->setEnabled(canCapPark.toBool()); 2250 schedulerOpenDustCover->setEnabled(canCapPark.toBool()); 2251 moduleState()->setCapReady(true); 2252 } 2253 else 2254 { 2255 schedulerCloseDustCover->setEnabled(false); 2256 schedulerOpenDustCover->setEnabled(false); 2257 } 2258 } 2259 else if (iface == process()->domeInterface()) 2260 { 2261 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "domeInterface:property", "canPark"); 2262 QVariant canDomePark = process()->domeInterface()->property("canPark"); 2263 TEST_PRINT(stderr, " @@@dbus received %s\n", !canDomePark.isValid() ? "invalid" : (canDomePark.toBool() ? "T" : "F")); 2264 2265 if (canDomePark.isValid()) 2266 { 2267 schedulerParkDome->setEnabled(canDomePark.toBool()); 2268 schedulerUnparkDome->setEnabled(canDomePark.toBool()); 2269 moduleState()->setDomeReady(true); 2270 } 2271 else 2272 { 2273 schedulerParkDome->setEnabled(false); 2274 schedulerUnparkDome->setEnabled(false); 2275 } 2276 } 2277 else if (iface == process()->weatherInterface()) 2278 { 2279 QVariant status = process()->weatherInterface()->property("status"); 2280 if (status.isValid()) 2281 { 2282 schedulerWeather->setEnabled(true); 2283 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt())); 2284 } 2285 else 2286 schedulerWeather->setEnabled(false); 2287 } 2288 else if (iface == process()->captureInterface()) 2289 { 2290 TEST_PRINT(stderr, "sch%d @@@dbus(%s): %s\n", __LINE__, "captureInterface:property", "coolerControl"); 2291 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl"); 2292 TEST_PRINT(stderr, " @@@dbus received %s\n", 2293 !hasCoolerControl.isValid() ? "invalid" : (hasCoolerControl.toBool() ? "T" : "F")); 2294 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool()); 2295 moduleState()->setCaptureReady(true); 2296 } 2297 } 2298 2299 void Scheduler::checkInterfaceReady(QDBusInterface *iface) 2300 { 2301 if (iface == process()->mountInterface()) 2302 { 2303 QVariant canMountPark = process()->mountInterface()->property("canPark"); 2304 if (canMountPark.isValid()) 2305 { 2306 schedulerUnparkMount->setEnabled(canMountPark.toBool()); 2307 schedulerParkMount->setEnabled(canMountPark.toBool()); 2308 moduleState()->setMountReady(true); 2309 } 2310 } 2311 else if (iface == process()->capInterface()) 2312 { 2313 QVariant canCapPark = process()->capInterface()->property("canPark"); 2314 if (canCapPark.isValid()) 2315 { 2316 schedulerCloseDustCover->setEnabled(canCapPark.toBool()); 2317 schedulerOpenDustCover->setEnabled(canCapPark.toBool()); 2318 moduleState()->setCapReady(true); 2319 } 2320 else 2321 { 2322 schedulerCloseDustCover->setEnabled(false); 2323 schedulerOpenDustCover->setEnabled(false); 2324 } 2325 } 2326 else if (iface == process()->weatherInterface()) 2327 { 2328 QVariant status = process()->weatherInterface()->property("status"); 2329 if (status.isValid()) 2330 { 2331 schedulerWeather->setEnabled(true); 2332 setWeatherStatus(static_cast<ISD::Weather::Status>(status.toInt())); 2333 } 2334 else 2335 schedulerWeather->setEnabled(false); 2336 } 2337 else if (iface == process()->domeInterface()) 2338 { 2339 QVariant canDomePark = process()->domeInterface()->property("canPark"); 2340 if (canDomePark.isValid()) 2341 { 2342 schedulerUnparkDome->setEnabled(canDomePark.toBool()); 2343 schedulerParkDome->setEnabled(canDomePark.toBool()); 2344 moduleState()->setDomeReady(true); 2345 } 2346 } 2347 else if (iface == process()->captureInterface()) 2348 { 2349 QVariant hasCoolerControl = process()->captureInterface()->property("coolerControl"); 2350 if (hasCoolerControl.isValid()) 2351 { 2352 schedulerWarmCCD->setEnabled(hasCoolerControl.toBool()); 2353 moduleState()->setCaptureReady(true); 2354 } 2355 } 2356 } 2357 2358 void Scheduler::setAlignStatus(AlignState status) 2359 { 2360 process()->setAlignStatus(status); 2361 } 2362 2363 void Scheduler::setGuideStatus(GuideState status) 2364 { 2365 process()->setGuideStatus(status); 2366 } 2367 2368 void Scheduler::setCaptureStatus(Ekos::CaptureState status) 2369 { 2370 TEST_PRINT(stderr, "sch%d @@@setCaptureStatus(%d) %s\n", __LINE__, static_cast<int>(status), 2371 (activeJob() == nullptr) ? "IGNORED" : "OK"); 2372 if (activeJob() == nullptr) 2373 return; 2374 2375 qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status); 2376 2377 /* If current job is scheduled and has not started yet, wait */ 2378 if (SCHEDJOB_SCHEDULED == activeJob()->getState()) 2379 { 2380 QDateTime const now = SchedulerModuleState::getLocalTime(); 2381 if (now < activeJob()->getStartupTime()) 2382 return; 2383 } 2384 2385 if (activeJob()->getStage() == SCHEDSTAGE_CAPTURING) 2386 { 2387 if (status == Ekos::CAPTURE_PROGRESS && (activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN)) 2388 { 2389 // JM 2021.09.20 2390 // Re-set target coords in align module 2391 // When capture starts, alignment module automatically rests target coords to mount coords. 2392 // However, we want to keep align module target synced with the scheduler target and not 2393 // the mount coord 2394 const SkyPoint targetCoords = activeJob()->getTargetCoords(); 2395 QList<QVariant> targetArgs; 2396 targetArgs << targetCoords.ra0().Hours() << targetCoords.dec0().Degrees(); 2397 process()->alignInterface()->callWithArgumentList(QDBus::AutoDetect, "setTargetCoords", targetArgs); 2398 } 2399 else if (status == Ekos::CAPTURE_ABORTED) 2400 { 2401 appendLogText(i18n("Warning: job '%1' failed to capture target.", activeJob()->getName())); 2402 2403 if (moduleState()->increaseCaptureFailureCount()) 2404 { 2405 // If capture failed due to guiding error, let's try to restart that 2406 if (activeJob()->getStepPipeline() & SchedulerJob::USE_GUIDE) 2407 { 2408 // Check if it is guiding related. 2409 Ekos::GuideState gStatus = process()->getGuidingStatus(); 2410 if (gStatus == Ekos::GUIDE_ABORTED || 2411 gStatus == Ekos::GUIDE_CALIBRATION_ERROR || 2412 gStatus == GUIDE_DITHERING_ERROR) 2413 { 2414 appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", 2415 activeJob()->getName(), 2416 moduleState()->captureFailureCount(), moduleState()->maxFailureAttempts())); 2417 process()->startGuiding(true); 2418 return; 2419 } 2420 } 2421 2422 /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ 2423 appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", activeJob()->getName())); 2424 process()->startCapture(true); 2425 } 2426 else 2427 { 2428 /* FIXME: it's not clear whether this situation can be recovered at all */ 2429 appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", activeJob()->getName())); 2430 activeJob()->setState(SCHEDJOB_ABORTED); 2431 2432 process()->findNextJob(); 2433 } 2434 } 2435 else if (status == Ekos::CAPTURE_COMPLETE) 2436 { 2437 KSNotification::event(QLatin1String("EkosScheduledImagingFinished"), 2438 i18n("Ekos job (%1) - Capture finished", activeJob()->getName()), KSNotification::Scheduler); 2439 2440 activeJob()->setState(SCHEDJOB_COMPLETE); 2441 process()->findNextJob(); 2442 } 2443 else if (status == Ekos::CAPTURE_IMAGE_RECEIVED) 2444 { 2445 // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times. 2446 // FIXME: rework this once capture storage is reworked 2447 if (Options::rememberJobProgress()) 2448 { 2449 process()->updateCompletedJobsCount(true); 2450 2451 for (const auto &job : moduleState()->jobs()) 2452 SchedulerUtils::estimateJobTime(job, moduleState()->capturedFramesCount(), this); 2453 } 2454 // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks 2455 else 2456 activeJob()->setCompletedCount(activeJob()->getCompletedCount() + 1); 2457 2458 moduleState()->resetCaptureFailureCount(); 2459 } 2460 } 2461 } 2462 2463 void Scheduler::setFocusStatus(FocusState status) 2464 { 2465 process()->setFocusStatus(status); 2466 } 2467 2468 void Scheduler::setMountStatus(ISD::Mount::Status status) 2469 { 2470 process()->setMountStatus(status); 2471 } 2472 2473 void Scheduler::setWeatherStatus(ISD::Weather::Status status) 2474 { 2475 TEST_PRINT(stderr, "sch%d @@@setWeatherStatus(%d)\n", __LINE__, static_cast<int>(status)); 2476 ISD::Weather::Status newStatus = status; 2477 QString statusString; 2478 2479 switch (newStatus) 2480 { 2481 case ISD::Weather::WEATHER_OK: 2482 statusString = i18n("Weather conditions are OK."); 2483 break; 2484 2485 case ISD::Weather::WEATHER_WARNING: 2486 statusString = i18n("Warning: weather conditions are in the WARNING zone."); 2487 break; 2488 2489 case ISD::Weather::WEATHER_ALERT: 2490 statusString = i18n("Caution: weather conditions are in the DANGER zone!"); 2491 break; 2492 2493 default: 2494 break; 2495 } 2496 2497 if (newStatus != moduleState()->weatherStatus()) 2498 { 2499 moduleState()->setWeatherStatus(newStatus); 2500 2501 qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; 2502 2503 if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_OK) 2504 weatherLabel->setPixmap( 2505 QIcon::fromTheme("security-high") 2506 .pixmap(QSize(32, 32))); 2507 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_WARNING) 2508 { 2509 weatherLabel->setPixmap( 2510 QIcon::fromTheme("security-medium") 2511 .pixmap(QSize(32, 32))); 2512 KSNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone"), 2513 KSNotification::Scheduler, KSNotification::Warn); 2514 } 2515 else if (moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT) 2516 { 2517 weatherLabel->setPixmap( 2518 QIcon::fromTheme("security-low") 2519 .pixmap(QSize(32, 32))); 2520 KSNotification::event(QLatin1String("WeatherAlert"), 2521 i18n("Weather conditions are critical. Observatory shutdown is imminent"), KSNotification::Scheduler, 2522 KSNotification::Alert); 2523 } 2524 else 2525 weatherLabel->setPixmap(QIcon::fromTheme("chronometer") 2526 .pixmap(QSize(32, 32))); 2527 2528 weatherLabel->show(); 2529 weatherLabel->setToolTip(statusString); 2530 2531 appendLogText(statusString); 2532 2533 emit weatherChanged(moduleState()->weatherStatus()); 2534 } 2535 2536 // Shutdown scheduler if it was started and not already in shutdown 2537 // and if weather checkbox is checked. 2538 if (schedulerWeather->isChecked() && moduleState()->weatherStatus() == ISD::Weather::WEATHER_ALERT 2539 && moduleState()->schedulerState() != Ekos::SCHEDULER_IDLE 2540 && moduleState()->schedulerState() != Ekos::SCHEDULER_SHUTDOWN) 2541 { 2542 appendLogText(i18n("Starting shutdown procedure due to severe weather.")); 2543 if (activeJob()) 2544 { 2545 activeJob()->setState(SCHEDJOB_ABORTED); 2546 process()->stopCurrentJobAction(); 2547 } 2548 process()->checkShutdownState(); 2549 } 2550 } 2551 2552 void Scheduler::handleSchedulerSleeping(bool shutdown, bool sleep) 2553 { 2554 if (shutdown) 2555 { 2556 schedulerWeather->setEnabled(false); 2557 weatherLabel->hide(); 2558 } 2559 if (sleep) 2560 changeSleepLabel(i18n("Scheduler is in sleep mode")); 2561 } 2562 2563 void Scheduler::handleSchedulerStateChanged(SchedulerState newState) 2564 { 2565 switch (newState) 2566 { 2567 case SCHEDULER_RUNNING: 2568 /* Update UI to reflect startup */ 2569 pi->startAnimation(); 2570 sleepLabel->hide(); 2571 startB->setIcon(QIcon::fromTheme("media-playback-stop")); 2572 startB->setToolTip(i18n("Stop Scheduler")); 2573 pauseB->setEnabled(true); 2574 pauseB->setChecked(false); 2575 2576 /* Disable edit-related buttons */ 2577 queueLoadB->setEnabled(false); 2578 setJobManipulation(true, false); 2579 //mosaicB->setEnabled(false); 2580 evaluateOnlyB->setEnabled(false); 2581 startupB->setEnabled(false); 2582 shutdownB->setEnabled(false); 2583 break; 2584 2585 default: 2586 break; 2587 } 2588 // forward the state chqnge 2589 emit newStatus(newState); 2590 } 2591 2592 void Scheduler::handleSetPaused() 2593 { 2594 pauseB->setCheckable(true); 2595 pauseB->setChecked(true); 2596 } 2597 2598 void Scheduler::setCaptureComplete(const QVariantMap &metadata) 2599 { 2600 if (activeJob() && 2601 activeJob()->getStepPipeline() & SchedulerJob::USE_ALIGN && 2602 metadata["type"].toInt() == FRAME_LIGHT && 2603 Options::alignCheckFrequency() > 0 && 2604 ++m_SolverIteration >= Options::alignCheckFrequency()) 2605 { 2606 m_SolverIteration = 0; 2607 2608 auto filename = metadata["filename"].toString(); 2609 auto exposure = metadata["exposure"].toDouble(); 2610 2611 constexpr double minSolverSeconds = 5.0; 2612 double solverTimeout = std::max(exposure - 2, minSolverSeconds); 2613 if (solverTimeout >= minSolverSeconds) 2614 { 2615 auto profiles = getDefaultAlignOptionsProfiles(); 2616 auto parameters = profiles.at(Options::solveOptionsProfile()); 2617 // Double search radius 2618 parameters.search_radius = parameters.search_radius * 2; 2619 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater); 2620 connect(m_Solver.get(), &SolverUtils::done, this, &Ekos::Scheduler::solverDone, Qt::UniqueConnection); 2621 //connect(m_Solver.get(), &SolverUtils::newLog, this, &Ekos::Scheduler::appendLogText, Qt::UniqueConnection); 2622 2623 auto width = metadata["width"].toUInt(); 2624 auto height = metadata["height"].toUInt(); 2625 2626 auto lowScale = Options::astrometryImageScaleLow(); 2627 auto highScale = Options::astrometryImageScaleHigh(); 2628 2629 // solver utils uses arcsecs per pixel only 2630 if (Options::astrometryImageScaleUnits() == SSolver::DEG_WIDTH) 2631 { 2632 lowScale = (lowScale * 3600) / std::max(width, height); 2633 highScale = (highScale * 3600) / std::min(width, height); 2634 } 2635 else if (Options::astrometryImageScaleUnits() == SSolver::ARCMIN_WIDTH) 2636 { 2637 lowScale = (lowScale * 60) / std::max(width, height); 2638 highScale = (highScale * 60) / std::min(width, height); 2639 } 2640 2641 m_Solver->useScale(Options::astrometryUseImageScale(), lowScale, highScale); 2642 m_Solver->usePosition(Options::astrometryUsePosition(), activeJob()->getTargetCoords().ra().Degrees(), 2643 activeJob()->getTargetCoords().dec().Degrees()); 2644 m_Solver->setHealpix(moduleState()->indexToUse(), moduleState()->healpixToUse()); 2645 m_Solver->runSolver(filename); 2646 } 2647 } 2648 } 2649 2650 void Scheduler::handleJobsUpdated(QJsonArray jobsList) 2651 { 2652 syncGreedyParams(); 2653 updateJobTable(); 2654 2655 emit jobsUpdated(jobsList); 2656 } 2657 2658 void Scheduler::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds) 2659 { 2660 disconnect(m_Solver.get(), &SolverUtils::done, this, &Ekos::Scheduler::solverDone); 2661 2662 if (!activeJob()) 2663 return; 2664 2665 QString healpixString = ""; 2666 if (moduleState()->indexToUse() != -1 || moduleState()->healpixToUse() != -1) 2667 healpixString = QString("Healpix %1 Index %2").arg(moduleState()->healpixToUse()).arg(moduleState()->indexToUse()); 2668 2669 if (timedOut || !success) 2670 { 2671 // Don't use the previous index and healpix next time we solve. 2672 moduleState()->setIndexToUse(-1); 2673 moduleState()->setHealpixToUse(-1); 2674 } 2675 else 2676 { 2677 int index, healpix; 2678 // Get the index and healpix from the successful solve. 2679 m_Solver->getSolutionHealpix(&index, &healpix); 2680 moduleState()->setIndexToUse(index); 2681 moduleState()->setHealpixToUse(healpix); 2682 } 2683 2684 if (timedOut) 2685 appendLogText(i18n("Solver timed out: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString)); 2686 else if (!success) 2687 appendLogText(i18n("Solver failed: %1s %2", QString("%L1").arg(elapsedSeconds, 0, 'f', 1), healpixString)); 2688 else 2689 { 2690 const double ra = solution.ra; 2691 const double dec = solution.dec; 2692 2693 const auto target = activeJob()->getTargetCoords(); 2694 2695 SkyPoint alignCoord; 2696 alignCoord.setRA0(ra / 15.0); 2697 alignCoord.setDec0(dec); 2698 alignCoord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd()); 2699 alignCoord.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); 2700 const double diffRa = (alignCoord.ra().deltaAngle(target.ra())).Degrees() * 3600; 2701 const double diffDec = (alignCoord.dec().deltaAngle(target.dec())).Degrees() * 3600; 2702 2703 // This is an approximation, probably ok for small angles. 2704 const double diffTotal = hypot(diffRa, diffDec); 2705 2706 // Note--the RA output is in DMS. This is because we're looking at differences in arcseconds 2707 // and HMS coordinates are misleading (one HMS second is really 6 arc-seconds). 2708 qCDebug(KSTARS_EKOS_SCHEDULER) << 2709 QString("Target Distance: %1\" Target (RA: %2 DE: %3) Current (RA: %4 DE: %5) %6 solved in %7s") 2710 .arg(QString("%L1").arg(diffTotal, 0, 'f', 0), 2711 target.ra().toDMSString(), 2712 target.dec().toDMSString(), 2713 alignCoord.ra().toDMSString(), 2714 alignCoord.dec().toDMSString(), 2715 healpixString, 2716 QString("%L1").arg(elapsedSeconds, 0, 'f', 2)); 2717 emit targetDistance(diffTotal); 2718 2719 // If we exceed align check threshold, we abort and re-align. 2720 if (diffTotal / 60 > Options::alignCheckThreshold()) 2721 { 2722 appendLogText(i18n("Captured frame is %1 arcminutes away from target, re-aligning...", QString::number(diffTotal / 60.0, 2723 'f', 1))); 2724 process()->stopCurrentJobAction(); 2725 process()->startAstrometry(); 2726 } 2727 } 2728 } 2729 2730 2731 2732 bool Scheduler::createJobSequence(XMLEle * root, const QString &prefix, const QString &outputDir) 2733 { 2734 return process()->createJobSequence(root, prefix, outputDir); 2735 } 2736 2737 XMLEle *Scheduler::getSequenceJobRoot(const QString &filename) 2738 { 2739 return process()->getSequenceJobRoot(filename); 2740 } 2741 2742 bool Scheduler::importMosaic(const QJsonObject &payload) 2743 { 2744 QScopedPointer<FramingAssistantUI> assistant(new FramingAssistantUI()); 2745 return assistant->importMosaic(payload); 2746 } 2747 2748 void Scheduler::startupStateChanged(StartupState state) 2749 { 2750 jobStatus->setText(startupStateString(state)); 2751 2752 switch (moduleState()->startupState()) 2753 { 2754 case STARTUP_IDLE: 2755 startupB->setIcon(QIcon::fromTheme("media-playback-start")); 2756 break; 2757 case STARTUP_COMPLETE: 2758 startupB->setIcon(QIcon::fromTheme("media-playback-start")); 2759 appendLogText(i18n("Manual startup procedure completed successfully.")); 2760 break; 2761 case STARTUP_ERROR: 2762 startupB->setIcon(QIcon::fromTheme("media-playback-start")); 2763 appendLogText(i18n("Manual startup procedure terminated due to errors.")); 2764 break; 2765 default: 2766 // in all other cases startup is running 2767 startupB->setIcon(QIcon::fromTheme("media-playback-stop")); 2768 break; 2769 } 2770 } 2771 void Scheduler::shutdownStateChanged(ShutdownState state) 2772 { 2773 jobStatus->setText(shutdownStateString(state)); 2774 if (state == SHUTDOWN_COMPLETE || state == SHUTDOWN_IDLE 2775 || state == SHUTDOWN_ERROR) 2776 shutdownB->setIcon(QIcon::fromTheme("media-playback-start")); 2777 else 2778 shutdownB->setIcon(QIcon::fromTheme("media-playback-stop")); 2779 } 2780 void Scheduler::ekosStateChanged(EkosState state) 2781 { 2782 jobStatus->setText(ekosStateString(state)); 2783 } 2784 void Scheduler::indiStateChanged(INDIState state) 2785 { 2786 jobStatus->setText(indiStateString(state)); 2787 } 2788 void Scheduler::parkWaitStateChanged(ParkWaitState state) 2789 { 2790 jobStatus->setText(parkWaitStateString(state)); 2791 } 2792 2793 SchedulerJob *Scheduler::activeJob() 2794 { 2795 return moduleState()->activeJob(); 2796 } 2797 2798 Ekos::SchedulerState Scheduler::status() 2799 { 2800 return moduleState()->schedulerState(); 2801 } 2802 2803 bool Scheduler::saveScheduler(const QUrl &fileURL) 2804 { 2805 return process()->saveScheduler(fileURL); 2806 } 2807 2808 void Scheduler::loadGlobalSettings() 2809 { 2810 QString key; 2811 QVariant value; 2812 2813 QVariantMap settings; 2814 // All Combo Boxes 2815 for (auto &oneWidget : findChildren<QComboBox*>()) 2816 { 2817 key = oneWidget->objectName(); 2818 value = Options::self()->property(key.toLatin1()); 2819 if (value.isValid()) 2820 { 2821 oneWidget->setCurrentText(value.toString()); 2822 settings[key] = value; 2823 } 2824 else 2825 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!"; 2826 } 2827 2828 // All Double Spin Boxes 2829 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 2830 { 2831 key = oneWidget->objectName(); 2832 value = Options::self()->property(key.toLatin1()); 2833 if (value.isValid()) 2834 { 2835 oneWidget->setValue(value.toDouble()); 2836 settings[key] = value; 2837 } 2838 else 2839 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!"; 2840 } 2841 2842 // All Spin Boxes 2843 for (auto &oneWidget : findChildren<QSpinBox*>()) 2844 { 2845 key = oneWidget->objectName(); 2846 value = Options::self()->property(key.toLatin1()); 2847 if (value.isValid()) 2848 { 2849 oneWidget->setValue(value.toInt()); 2850 settings[key] = value; 2851 } 2852 else 2853 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!"; 2854 } 2855 2856 // All Checkboxes 2857 for (auto &oneWidget : findChildren<QCheckBox*>()) 2858 { 2859 key = oneWidget->objectName(); 2860 value = Options::self()->property(key.toLatin1()); 2861 if (value.isValid()) 2862 { 2863 oneWidget->setChecked(value.toBool()); 2864 settings[key] = value; 2865 } 2866 else 2867 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!"; 2868 } 2869 2870 // All Line Edits 2871 for (auto &oneWidget : findChildren<QLineEdit*>()) 2872 { 2873 key = oneWidget->objectName(); 2874 value = Options::self()->property(key.toLatin1()); 2875 if (value.isValid()) 2876 { 2877 oneWidget->setText(value.toString()); 2878 settings[key] = value; 2879 2880 if (key == "sequenceEdit") 2881 setSequence(value.toString()); 2882 else if (key == "schedulerStartupScript") 2883 moduleState()->setStartupScriptURL(QUrl::fromUserInput(value.toString())); 2884 else if (key == "schedulerShutdownScript") 2885 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(value.toString())); 2886 } 2887 else 2888 qCDebug(KSTARS_EKOS_FOCUS) << "Option" << key << "not found!"; 2889 } 2890 2891 // All Radio buttons 2892 for (auto &oneWidget : findChildren<QRadioButton*>()) 2893 { 2894 key = oneWidget->objectName(); 2895 value = Options::self()->property(key.toLatin1()); 2896 if (value.isValid()) 2897 { 2898 oneWidget->setChecked(value.toBool()); 2899 settings[key] = value; 2900 } 2901 } 2902 2903 // All QDateTime edits 2904 for (auto &oneWidget : findChildren<QDateTimeEdit*>()) 2905 { 2906 key = oneWidget->objectName(); 2907 value = Options::self()->property(key.toLatin1()); 2908 if (value.isValid()) 2909 { 2910 oneWidget->setDateTime(QDateTime::fromString(value.toString(), Qt::ISODate)); 2911 settings[key] = value; 2912 } 2913 } 2914 2915 setErrorHandlingStrategy(static_cast<ErrorHandlingStrategy>(Options::errorHandlingStrategy())); 2916 2917 m_GlobalSettings = m_Settings = settings; 2918 } 2919 2920 void Scheduler::syncSettings() 2921 { 2922 QDoubleSpinBox *dsb = nullptr; 2923 QSpinBox *sb = nullptr; 2924 QCheckBox *cb = nullptr; 2925 QRadioButton *rb = nullptr; 2926 QComboBox *cbox = nullptr; 2927 QLineEdit *lineedit = nullptr; 2928 QDateTimeEdit *datetimeedit = nullptr; 2929 2930 QString key; 2931 QVariant value; 2932 2933 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender()))) 2934 { 2935 key = dsb->objectName(); 2936 value = dsb->value(); 2937 2938 } 2939 else if ( (sb = qobject_cast<QSpinBox*>(sender()))) 2940 { 2941 key = sb->objectName(); 2942 value = sb->value(); 2943 } 2944 else if ( (cb = qobject_cast<QCheckBox*>(sender()))) 2945 { 2946 key = cb->objectName(); 2947 value = cb->isChecked(); 2948 } 2949 else if ( (rb = qobject_cast<QRadioButton*>(sender()))) 2950 { 2951 key = rb->objectName(); 2952 value = rb->isChecked(); 2953 } 2954 else if ( (cbox = qobject_cast<QComboBox*>(sender()))) 2955 { 2956 key = cbox->objectName(); 2957 value = cbox->currentText(); 2958 } 2959 else if ( (lineedit = qobject_cast<QLineEdit*>(sender()))) 2960 { 2961 key = lineedit->objectName(); 2962 value = lineedit->text(); 2963 } 2964 else if ( (datetimeedit = qobject_cast<QDateTimeEdit*>(sender()))) 2965 { 2966 key = datetimeedit->objectName(); 2967 value = datetimeedit->dateTime().toString(Qt::ISODate); 2968 } 2969 2970 // Save immediately 2971 Options::self()->setProperty(key.toLatin1(), value); 2972 2973 m_Settings[key] = value; 2974 m_GlobalSettings[key] = value; 2975 2976 emit settingsUpdated(getAllSettings()); 2977 } 2978 2979 /////////////////////////////////////////////////////////////////////////////////////////// 2980 /// 2981 /////////////////////////////////////////////////////////////////////////////////////////// 2982 QVariantMap Scheduler::getAllSettings() const 2983 { 2984 QVariantMap settings; 2985 2986 // All Combo Boxes 2987 for (auto &oneWidget : findChildren<QComboBox*>()) 2988 settings.insert(oneWidget->objectName(), oneWidget->currentText()); 2989 2990 // All Double Spin Boxes 2991 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 2992 settings.insert(oneWidget->objectName(), oneWidget->value()); 2993 2994 // All Spin Boxes 2995 for (auto &oneWidget : findChildren<QSpinBox*>()) 2996 settings.insert(oneWidget->objectName(), oneWidget->value()); 2997 2998 // All Checkboxes 2999 for (auto &oneWidget : findChildren<QCheckBox*>()) 3000 settings.insert(oneWidget->objectName(), oneWidget->isChecked()); 3001 3002 // All Line Edits 3003 for (auto &oneWidget : findChildren<QLineEdit*>()) 3004 { 3005 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those 3006 if (!oneWidget->objectName().startsWith("qt_")) 3007 settings.insert(oneWidget->objectName(), oneWidget->text()); 3008 } 3009 3010 // All Radio Buttons 3011 for (auto &oneWidget : findChildren<QRadioButton*>()) 3012 settings.insert(oneWidget->objectName(), oneWidget->isChecked()); 3013 3014 // All QDateTime 3015 for (auto &oneWidget : findChildren<QDateTimeEdit*>()) 3016 { 3017 settings.insert(oneWidget->objectName(), oneWidget->dateTime().toString(Qt::ISODate)); 3018 } 3019 3020 return settings; 3021 } 3022 3023 /////////////////////////////////////////////////////////////////////////////////////////// 3024 /// 3025 /////////////////////////////////////////////////////////////////////////////////////////// 3026 void Scheduler::setAllSettings(const QVariantMap &settings) 3027 { 3028 // Disconnect settings that we don't end up calling syncSettings while 3029 // performing the changes. 3030 disconnectSettings(); 3031 3032 for (auto &name : settings.keys()) 3033 { 3034 // Combo 3035 auto comboBox = findChild<QComboBox*>(name); 3036 if (comboBox) 3037 { 3038 syncControl(settings, name, comboBox); 3039 continue; 3040 } 3041 3042 // Double spinbox 3043 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name); 3044 if (doubleSpinBox) 3045 { 3046 syncControl(settings, name, doubleSpinBox); 3047 continue; 3048 } 3049 3050 // spinbox 3051 auto spinBox = findChild<QSpinBox*>(name); 3052 if (spinBox) 3053 { 3054 syncControl(settings, name, spinBox); 3055 continue; 3056 } 3057 3058 // checkbox 3059 auto checkbox = findChild<QCheckBox*>(name); 3060 if (checkbox) 3061 { 3062 syncControl(settings, name, checkbox); 3063 continue; 3064 } 3065 3066 // Line Edits 3067 auto lineedit = findChild<QLineEdit*>(name); 3068 if (lineedit) 3069 { 3070 syncControl(settings, name, lineedit); 3071 3072 if (name == "sequenceEdit") 3073 setSequence(lineedit->text()); 3074 else if (name == "fitsEdit") 3075 processFITSSelection(QUrl::fromLocalFile(lineedit->text())); 3076 else if (name == "schedulerStartupScript") 3077 moduleState()->setStartupScriptURL(QUrl::fromUserInput(lineedit->text())); 3078 else if (name == "schedulerShutdownScript") 3079 moduleState()->setShutdownScriptURL(QUrl::fromUserInput(lineedit->text())); 3080 3081 continue; 3082 } 3083 3084 // Radio button 3085 auto radioButton = findChild<QRadioButton*>(name); 3086 if (radioButton) 3087 { 3088 syncControl(settings, name, radioButton); 3089 continue; 3090 } 3091 3092 auto datetimeedit = findChild<QDateTimeEdit*>(name); 3093 if (datetimeedit) 3094 { 3095 syncControl(settings, name, datetimeedit); 3096 continue; 3097 } 3098 } 3099 3100 m_Settings = settings; 3101 3102 // Restablish connections 3103 connectSettings(); 3104 } 3105 3106 /////////////////////////////////////////////////////////////////////////////////////////// 3107 /// 3108 /////////////////////////////////////////////////////////////////////////////////////////// 3109 bool Scheduler::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget) 3110 { 3111 QSpinBox *pSB = nullptr; 3112 QDoubleSpinBox *pDSB = nullptr; 3113 QCheckBox *pCB = nullptr; 3114 QComboBox *pComboBox = nullptr; 3115 QLineEdit *pLineEdit = nullptr; 3116 QRadioButton *pRadioButton = nullptr; 3117 QDateTimeEdit *pDateTimeEdit = nullptr; 3118 bool ok = true; 3119 3120 if ((pSB = qobject_cast<QSpinBox *>(widget))) 3121 { 3122 const int value = settings[key].toInt(&ok); 3123 if (ok) 3124 { 3125 pSB->setValue(value); 3126 return true; 3127 } 3128 } 3129 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget))) 3130 { 3131 const double value = settings[key].toDouble(&ok); 3132 if (ok) 3133 { 3134 pDSB->setValue(value); 3135 return true; 3136 } 3137 } 3138 else if ((pCB = qobject_cast<QCheckBox *>(widget))) 3139 { 3140 const bool value = settings[key].toBool(); 3141 pCB->setChecked(value); 3142 return true; 3143 } 3144 // ONLY FOR STRINGS, not INDEX 3145 else if ((pComboBox = qobject_cast<QComboBox *>(widget))) 3146 { 3147 const QString value = settings[key].toString(); 3148 pComboBox->setCurrentText(value); 3149 return true; 3150 } 3151 else if ((pLineEdit = qobject_cast<QLineEdit *>(widget))) 3152 { 3153 const auto value = settings[key].toString(); 3154 pLineEdit->setText(value); 3155 return true; 3156 } 3157 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget))) 3158 { 3159 const bool value = settings[key].toBool(); 3160 pRadioButton->setChecked(value); 3161 return true; 3162 } 3163 else if ((pDateTimeEdit = qobject_cast<QDateTimeEdit *>(widget))) 3164 { 3165 const auto value = QDateTime::fromString(settings[key].toString(), Qt::ISODate); 3166 pDateTimeEdit->setDateTime(value); 3167 return true; 3168 } 3169 3170 return false; 3171 }; 3172 3173 void Scheduler::connectSettings() 3174 { 3175 // All Combo Boxes 3176 for (auto &oneWidget : findChildren<QComboBox*>()) 3177 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings); 3178 3179 // All Double Spin Boxes 3180 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 3181 connect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings); 3182 3183 // All Spin Boxes 3184 for (auto &oneWidget : findChildren<QSpinBox*>()) 3185 connect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings); 3186 3187 // All Checkboxes 3188 for (auto &oneWidget : findChildren<QCheckBox*>()) 3189 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings); 3190 3191 // All Radio Butgtons 3192 for (auto &oneWidget : findChildren<QRadioButton*>()) 3193 connect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings); 3194 3195 // All QLineEdits 3196 for (auto &oneWidget : findChildren<QLineEdit*>()) 3197 { 3198 // Many other widget types (e.g. spinboxes) apparently have QLineEdit inside them so we want to skip those 3199 if (!oneWidget->objectName().startsWith("qt_")) 3200 connect(oneWidget, &QLineEdit::textChanged, this, &Ekos::Scheduler::syncSettings); 3201 } 3202 3203 // All QDateTimeEdit 3204 for (auto &oneWidget : findChildren<QDateTimeEdit*>()) 3205 connect(oneWidget, &QDateTimeEdit::dateTimeChanged, this, &Ekos::Scheduler::syncSettings); 3206 } 3207 3208 void Scheduler::disconnectSettings() 3209 { 3210 // All Combo Boxes 3211 for (auto &oneWidget : findChildren<QComboBox*>()) 3212 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::Scheduler::syncSettings); 3213 3214 // All Double Spin Boxes 3215 for (auto &oneWidget : findChildren<QDoubleSpinBox*>()) 3216 disconnect(oneWidget, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings); 3217 3218 // All Spin Boxes 3219 for (auto &oneWidget : findChildren<QSpinBox*>()) 3220 disconnect(oneWidget, QOverload<int>::of(&QSpinBox::valueChanged), this, &Ekos::Scheduler::syncSettings); 3221 3222 // All Checkboxes 3223 for (auto &oneWidget : findChildren<QCheckBox*>()) 3224 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::Scheduler::syncSettings); 3225 3226 // All Radio Butgtons 3227 for (auto &oneWidget : findChildren<QRadioButton*>()) 3228 disconnect(oneWidget, &QRadioButton::toggled, this, &Ekos::Scheduler::syncSettings); 3229 3230 // All QLineEdits 3231 for (auto &oneWidget : findChildren<QLineEdit*>()) 3232 disconnect(oneWidget, &QLineEdit::editingFinished, this, &Ekos::Scheduler::syncSettings); 3233 3234 // All QDateTimeEdit 3235 for (auto &oneWidget : findChildren<QDateTimeEdit*>()) 3236 disconnect(oneWidget, &QDateTimeEdit::editingFinished, this, &Ekos::Scheduler::syncSettings); 3237 } 3238 3239 }