diff --git a/kstars/ekos/manager.ui b/kstars/ekos/manager.ui index 927d5e87c..ce20e2689 100644 --- a/kstars/ekos/manager.ui +++ b/kstars/ekos/manager.ui @@ -1,1672 +1,1697 @@ Manager 0 0 - 612 - 643 + 796 + 761 + + + 0 + 0 + + + + + 796 + 568 + + 16777215 16777215 0 0 Ekos 1 3 3 3 3 + + + 0 + 0 + + + + + 0 + 0 + + Qt::Vertical - + 0 0 0 0 0 3 3 3 3 3 1 1. Select Profile 1 3 3 3 3 Profile: 0 0 QComboBox::AdjustToContents 0 0 22 22 22 22 Add profile 22 22 0 0 22 22 22 22 Edit profile 22 22 0 0 22 22 22 22 Remove profile 22 22 22 22 22 22 Custom Drivers 22 22 22 22 22 22 Launch Ekos Profile Wizard 22 22 Qt::Horizontal QSizePolicy::Minimum 1 20 2. Start && Stop Ekos 1 3 3 3 3 1 true 32 32 22 22 true 32 32 Ekos Live .. 22 22 false 32 32 INDI Control Panel .. 22 22 false 32 32 Serial Port Assistant - + + .. 22 22 true 32 32 Ekos Options .. 22 22 32 32 Logs Logs 22 22 3. Connect && Disconnect Devices 1 3 3 3 3 1 false 32 32 Connect false 32 32 Disconnect true 0 0 0 0 Summary 1 3 3 3 3 false Capture 3 3 3 3 1 font-weight:bold; Status: font-weight:bold; Idle Qt::Horizontal 1 20 250 250 1 false 3 1 Image Qt::AlignCenter 1 Qt::Horizontal 13 20 false 100 100 100 100 Qt::Horizontal 13 20 --:--:-- Qt::AlignCenter 1 Sequence Qt::AlignCenter 1 Qt::Horizontal 13 20 false 100 100 100 100 Qt::Horizontal 13 20 --:--:-- Qt::AlignCenter 1 Overall Qt::AlignCenter 1 Qt::Horizontal 13 20 false 100 100 100 100 1 3 3 3 3 Qt::Horizontal 13 20 --:--:-- Qt::AlignCenter 1 false Mount && Alignment 1 3 3 3 3 1 font-weight:bold; Status: Qt::PlainText font-weight:bold; Idle Qt::PlainText Qt::Horizontal 40 20 font-weight:bold; Target: 50 20 1 Right Ascension RA: true Declination DE: true Azimuth AZ: true Altitude AL: true Qt::Vertical 10 10 false 0 0 Focus false 1 3 3 3 3 1 font-weight:bold; Status: font-weight:bold; Idle Qt::Horizontal 40 20 HFR: 0 0 20 20 16777215 20 1 QLayout::SetDefaultConstraint 0 0 150 70 0 0 ArrowCursor Focus Profile false background-color: qlineargradient(x1:0, y1:0, x2:0, y2:0.5, stop:0 darkgray, stop:1 black); QFrame::Panel true Qt::AlignCenter 0 0 70 70 Focus Star false background-color: qlineargradient(x1:0, y1:0, x2:0, y2:0.5, stop:0 darkgray, stop:1 black); QFrame::Panel false Qt::AlignCenter false Guide 1 3 3 3 3 1 font-weight:bold; Status: font-weight:bold; Idle Qt::Horizontal QSizePolicy::Minimum 1 20 σRA: 0 0 30 20 25 20 σDEC: 0 0 30 20 25 20 1 150 70 Guide Profile false background-color: qlineargradient(x1:0, y1:0, x2:0, y2:0.5, stop:0 darkgray, stop:1 black); QFrame::Panel true Qt::AlignCenter 70 70 ArrowCursor Guide Star false background-color: qlineargradient(x1:0, y1:0, x2:0, y2:0.5, stop:0 darkgray, stop:1 black); QFrame::Panel false Qt::AlignCenter 0 0 0 50 16777215 16777215 QAbstractScrollArea::AdjustToContents true Qt::Vertical 20 40 Advanced Ekos Options Options... Clear QRoundProgressBar QWidget
QRoundProgressBar.h
1
processINDIB connectB disconnectB profileCombo addProfileB editProfileB deleteProfileB customDriversB wizardProfileB ekosLiveB optionsB ekosOptionsB clearB ekosLogOut raOUT decOUT azOUT altOUT toolsWidget
diff --git a/kstars/ekos/opsekos.ui b/kstars/ekos/opsekos.ui index cf40e2f18..5573bafb9 100644 --- a/kstars/ekos/opsekos.ui +++ b/kstars/ekos/opsekos.ui @@ -1,825 +1,871 @@ OpsEkos 0 0 - 426 - 379 + 478 + 450 TabWidget - 0 + 1 General Configuration Load Device Configuration: Ne&ver true O&n connection - Always &load defaults + Alwa&ys load defaults 3 Icons Orientation: Ekos modules icons are placed on the top of pages Top true ekosIconsPositionGroup Ekos module icons are placed to the left of pages - Left + &Left ekosIconsPositionGroup Qt::Horizontal 40 20 Make Ekos window independent from KStars main window. Requires restart to take effect. Independent Window Qt::Vertical 20 40 Scheduler <html><head/><body><p>Lead time is the minimum time in minutes between jobs. The scheduler starts execution of a job before its scheduled startup time by this lead time. Early execution is useful as focusing, alignment, and guiding procedures may take prolonged periods to time to complete.</p></body></html> Lead Time: <html><head/><body><p>Do not permit jobs to be scheduled or executed past this many minutes before dawn.</p></body></html> Pre-dawn <html><head/><body><p>In case no scheduler job is scheduled for this many hours, perform a complete shutdown procedure and restart observatory operations once the next job is ready.</p></body></html> Pre-emptive shutdown <html><head/><body><p>Pre-emptive shutdown hours</p></body></html> <html><body><p>Do not permit jobs to be scheduled less than this many degrees before the altitude restriction. Actual execution proceeds until the altitude limit.</p></body></html> Setting Altitude Cutoff + + + + -24.000000000000000 + + + 24.000000000000000 + + + 0.500000000000000 + + + + + + + -24.000000000000000 + + + 24.000000000000000 + + + 0.500000000000000 + + + + + + + <html><body><p>Offset astronomical dusk by this many hours. This positive or negative value adjusts the twilight restriction.</p></body></html> + + + Dusk Offset + + + + + + + <html><body><p>Offset astronomical dawn by this many hours. This positive or negative value adjusts the twilight restriction.</p></body></html> + + + Dawn Offset + + + Qt::Horizontal 40 20 Reset Mount Model On Alignment Failure Reset Mount Model Before Starting Each Job <html><head/><body><p>When guiding is started, always clear calibration. Turning off this option can lead to unpredictable guiding failures.</p></body></html> Always Reset Guide Calibration <html><head/><body><p>If guiding calibration fails then restart alignment process before proceeding to guiding recalibration process again. This can help recenter the target object in the field of view if the calibration process strayed too far off.</p></body></html> Restart Alignment on Guiding Calibration Failure After shutdown procedure is successfully executed, stop INDI and Ekos. Stop Ekos After Shutdown <html><head/><body><p>If the shutdown script terminates INDI server, enable this option so that no disconnection errors are generated.</p></body></html> Shutdown Script Terminates INDI High priority and altitude jobs are executed first Sort jobs by Altitude and Priority <html><head/><body><p>When processing a scheduled job, resume the sequence starting from the last image present in storage.</p></body></html> Remember Job Progress false Qt::Vertical 20 40 Capture 3 3 3 3 3 Miscellaneous Seconds ° C Qt::Horizontal 163 13 Wait this many seconds after guiding is resumed before starting capture Maximum acceptable difference between requested and measured temperature set point. When the temperature threshold is below this value, the temperature set point request is deemed successful. Temperature Threshold: 0.100000000000000 5.000000000000000 0.100000000000000 0.100000000000000 Wait this many seconds after guiding is resumed to stabilize the guiding performance before capture. Guiding Settle: Cover or uncover telescope dialog timeout in seconds Dialog Timeout: 600 10 60 Seconds <html><head/><body><p>When starting to process a sequence list, reset all capture counts to zero. Scheduler overrides this option when Remember Job Progress is enabled.</p></body></html> Always Reset Sequence When Starting false Reset Mount Model After Meridian Flip <html><head/><body><p>If using a filter wheel and an absolute position focuser, then always remember the focus position of the autofocus process for light frames in each filter. Before a flat frame is captured, the focuser is moved to the same focus point as light frames.</p></body></html> Capture flat frames at the same focus as light frames Display received FITS in the Summary screen preview window. Summary Screen Preview Display received DSLR images in the Image Viewer DSLR Image Viewer Qt::Horizontal 40 20 Clear saved DSLR sizes Clear DSLR Info In-Sequence Focus 3 3 3 3 3 frames Qt::Horizontal 40 20 50.000000000000000 % <html><head/><body><p>Set HFR Threshold percentage gain. When an autofocus operation is completed, the autofocus HFR value is increased by this threshold percentage value and stored within the capture module. If In-Sequence-Focus is engaged, the autofocus module only performs autofocusing procedure if current HFR value exceeds the capture module HFR threshold. Increase value to permit more relaxed changes in HFR values without requiring a full autofocus run.</p></body></html> HFR Threshold Modifier: Run In-Sequence HFR check after this many frames. In-Sequence HFR Check: Run In-Sequence HFR check after this many frames. 1 30 <html><head/><body><p>Calculate median focus value after each autofocus operation is complete. If the autofocus results become progressively worse with time, the median value shall reflect this trend and prevent unnecessary autofocus operations when the seeing conditions deteriorate.</p></body></html> Use Median Focus <html><head/><body><p>In-Sequence HFR threshold value controls when the autofocus process is started. If the measured HFR value exceeds the HFR threshold, autofocus process is initiated. If the HFR threshold value is zero initially (default), then the autofocus process best HFR value is used to set the new HFR threshold, after applying the HFR threshold modifier percentage. This new HFR threshold is then used for subsequent In-Sequence focus checks. If this option is enabled, the HFR threshold value is constant and gets saved to the sequence file.</p></body></html> Save Sequence HFR Value to File Qt::Vertical 20 40 Dark Library Remove one row from the database and delete the associated dark frame from disk Clear Row Maximum acceptable difference between current and recorded dark frame temperature set point. When the difference exceeds this value, a new dark frame shall be captured for this set point. T. Threshold: Remove all dark frames data and files Clear All ° C Reuse dark frames from the dark library for this many days. If exceeded, a new dark frame shall be captured and stored for future use. Dark Validity Qt::Horizontal 40 20 0.100000000000000 30.000000000000000 0.100000000000000 0.100000000000000 Open folder where dark frames are stored Darks Folder 365 10 days Clear Expired Refresh Double click to load dark frame QAbstractItemView::AllEditTriggers true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows true true diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp index 5c401442a..c30fda354 100644 --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1,7323 +1,7334 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "scheduler.h" #include "ksalmanac.h" #include "ksnotification.h" #include "kstars.h" #include "kstarsdata.h" #include "ksutils.h" #include "mosaic.h" #include "Options.h" #include "scheduleradaptor.h" #include "schedulerjob.h" #include "skymapcomposite.h" #include "auxiliary/QProgressIndicator.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/capture/sequencejob.h" #include "skyobjects/starobject.h" #include +#include #include #include #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 #define RESTART_GUIDING_DELAY_MS 5000 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 #define DEFAULT_MIN_MOON_SEPARATION 0 namespace Ekos { Scheduler::Scheduler() { setupUi(this); qRegisterMetaType("Ekos::SchedulerState"); qDBusRegisterMetaType(); new SchedulerAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); dirPath = QUrl::fromLocalFile(QDir::homePath()); // Get current KStars time and set seconds to zero QDateTime currentDateTime = KStarsData::Instance()->lt(); QTime currentTime = currentDateTime.time(); currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); currentDateTime.setTime(currentTime); // Set initial time for startup and completion times startupTimeEdit->setDateTime(currentDateTime); completionTimeEdit->setDateTime(currentDateTime); // Set up DBus interfaces QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Example of connecting DBus signals //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); schedulerTimer.setInterval(UPDATE_PERIOD_MS); jobTimer.setInterval(UPDATE_PERIOD_MS); connect(&schedulerTimer, &QTimer::timeout, this, &Scheduler::checkStatus); connect(&jobTimer, &QTimer::timeout, this, &Scheduler::checkJobStage); restartGuidingTimer.setSingleShot(true); restartGuidingTimer.setInterval(RESTART_GUIDING_DELAY_MS); connect(&restartGuidingTimer, &QTimer::timeout, this, [this]() { startGuiding(true); }); pi = new QProgressIndicator(this); bottomLayout->addWidget(pi, 0, nullptr); geo = KStarsData::Instance()->geo(); raBox->setDegType(false); //RA box should be HMS-style /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */ queueTable->setToolTip(i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); /* Set first button mode to add observation job from left-hand fields */ setJobAddApply(true); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); removeFromQueueB->setToolTip(i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); sortJobsB->setToolTip(i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); loadSequenceB->setIcon(QIcon::fromTheme("document-open")); loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectShutdownScriptB->setIcon( QIcon::fromTheme("document-open")); selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSB->setIcon(QIcon::fromTheme("document-open")); selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); startupB->setIcon( QIcon::fromTheme("media-playback-start")); startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startupB, &QPushButton::clicked, this, &Scheduler::runStartupProcedure); connect(shutdownB, &QPushButton::clicked, this, &Scheduler::runShutdownProcedure); selectObjectB->setIcon(QIcon::fromTheme("edit-find")); connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject); connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS); connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence); connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript); connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Scheduler::queueTableSelectionChanged); connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setCheckable(false); connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler); connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause); connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs); connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save); connect(queueLoadB, &QPushButton::clicked, this, &Scheduler::load); connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); // restore default values for error handling strategy setErrorHandlingStrategy(static_cast(Options::errorHandlingStrategy())); errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay()); // save new default values for error handling strategy connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [this](bool checked) { Options::setRescheduleErrors(checked); }); connect(errorHandlingButtonGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton * button) { Q_UNUSED(button); Options::setErrorHandlingStrategy(getErrorHandlingStrategy()); }); connect(errorHandlingDelaySB, static_cast(&QSpinBox::valueChanged), [this](int value) { Options::setErrorHandlingStrategyDelay(value); }); + connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig); + + calculateDawnDusk(); loadProfiles(); watchJobChanges(true); } QString Scheduler::getCurrentJobName() { return (currentJob != nullptr ? currentJob->getName() : ""); } void Scheduler::watchJobChanges(bool enable) { /* Don't double watch, this will cause multiple signals to be connected */ if (enable == jobChangesAreWatched) return; /* These are the widgets we want to connect, per signal function, to listen for modifications */ QLineEdit * const lineEdits[] = { nameEdit, raBox, decBox, fitsEdit, sequenceEdit, startupScript, shutdownScript }; QDateTimeEdit * const dateEdits[] = { startupTimeEdit, completionTimeEdit }; QComboBox * const comboBoxes[] = { schedulerProfileCombo }; QButtonGroup * const buttonGroups[] = { stepsButtonGroup, errorHandlingButtonGroup, startupButtonGroup, constraintButtonGroup, completionButtonGroup, startupProcedureButtonGroup, shutdownProcedureGroup }; QAbstractButton * const buttons[] = { errorHandlingRescheduleErrorsCB }; QSpinBox * const spinBoxes[] = { culminationOffset, repeatsSpin, prioritySpin, errorHandlingDelaySB }; QDoubleSpinBox * const dspinBoxes[] = { minMoonSeparation, minAltitude }; if (enable) { /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'. * The main problem with this implementation compared to the macro method is that it is now possible to * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart. */ for (auto * const control : lineEdits) connect(control, &QLineEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : dateEdits) connect(control, &QDateTimeEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : comboBoxes) connect(control, static_cast(&QComboBox::currentIndexChanged), this, [this]() { setDirty(); }); for (auto * const control : buttonGroups) connect(control, static_cast(&QButtonGroup::buttonToggled), this, [this](int, bool) { setDirty(); }); for (auto * const control : buttons) connect(control, static_cast(&QAbstractButton::clicked), this, [this](bool) { setDirty(); }); for (auto * const control : spinBoxes) connect(control, static_cast(&QSpinBox::valueChanged), this, [this]() { setDirty(); }); for (auto * const control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double) { setDirty(); }); } else { /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets, * because we did not take care to keep the connection object when connecting. No problem in our case, we do not * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to * disconnect selectively. */ for (auto * const control : lineEdits) disconnect(control, &QLineEdit::editingFinished, this, nullptr); for (auto * const control : dateEdits) disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); for (auto * const control : comboBoxes) disconnect(control, static_cast(&QComboBox::currentIndexChanged), this, nullptr); for (auto * const control : buttons) disconnect(control, static_cast(&QAbstractButton::clicked), this, nullptr); for (auto * const control : buttonGroups) disconnect(control, static_cast(&QButtonGroup::buttonToggled), this, nullptr); for (auto * const control : spinBoxes) disconnect(control, static_cast(&QSpinBox::valueChanged), this, nullptr); for (auto * const control : dspinBoxes) disconnect(control, static_cast(&QDoubleSpinBox::valueChanged), this, nullptr); } jobChangesAreWatched = enable; } void Scheduler::appendLogText(const QString &text) { /* FIXME: user settings for log length */ int const max_log_count = 2000; if (m_LogText.size() > max_log_count) m_LogText.removeLast(); m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_SCHEDULER) << text; emit newLog(text); } void Scheduler::clearLog() { m_LogText.clear(); emit newLog(QString()); } +void Scheduler::applyConfig() +{ + calculateDawnDusk(); + + if (SCHEDULER_RUNNING != state) + { + jobEvaluationOnly = true; + evaluateJobs(); + } +} + void Scheduler::selectObject() { if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); addObject(object); } } void Scheduler::addObject(SkyObject *object) { if (object != nullptr) { QString finalObjectName(object->name()); if (object->name() == "star") { StarObject *s = dynamic_cast(object); if (s->getHDIndex() != 0) finalObjectName = QString("HD %1").arg(s->getHDIndex()); } nameEdit->setText(finalObjectName); raBox->showInHours(object->ra0()); decBox->showInDegrees(object->dec0()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); setDirty(); } } void Scheduler::selectFITS() { fitsURL = QFileDialog::getOpenFileUrl(this, i18n("Select FITS Image"), dirPath, "FITS (*.fits *.fit)"); if (fitsURL.isEmpty()) return; dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); fitsEdit->setText(fitsURL.toLocalFile()); if (nameEdit->text().isEmpty()) nameEdit->setText(fitsURL.fileName()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); processFITSSelection(); setDirty(); } void Scheduler::processFITSSelection() { const QString filename = fitsEdit->text(); int status = 0; double ra = 0, dec = 0; dms raDMS, deDMS; char comment[128], error_status[512]; fitsfile *fptr = nullptr; if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; char objectra_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); return; } raDMS.setD(ra); } else { raDMS = dms::fromString(objectra_str, false); } status = 0; char objectde_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); return; } deDMS.setD(dec); } else { deDMS = dms::fromString(objectde_str, true); } raBox->setDMS(raDMS.toHMSString()); decBox->setDMS(deDMS.toDMSString()); char object_str[256] = {0}; if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status)) { QFileInfo info(filename); nameEdit->setText(info.baseName()); } else { nameEdit->setText(object_str); } } void Scheduler::selectSequence() { sequenceURL = QFileDialog::getOpenFileUrl(this, i18n("Select Sequence Queue"), dirPath, i18n("Ekos Sequence Queue (*.esq)")); if (sequenceURL.isEmpty()) return; dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); sequenceEdit->setText(sequenceURL.toLocalFile()); // For object selection, all fields must be filled if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false) // For FITS selection, only the name and fits URL should be filled. || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false)) { addToQueueB->setEnabled(true); mosaicB->setEnabled(true); } setDirty(); } void Scheduler::selectStartupScript() { startupScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Startup Script"), dirPath, i18n("Script (*)")); if (startupScriptURL.isEmpty()) return; dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename)); mDirty = true; startupScript->setText(startupScriptURL.toLocalFile()); } void Scheduler::selectShutdownScript() { shutdownScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Shutdown Script"), dirPath, i18n("Script (*)")); if (shutdownScriptURL.isEmpty()) return; dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename)); mDirty = true; shutdownScript->setText(shutdownScriptURL.toLocalFile()); } void Scheduler::addJob() { if (0 <= jobUnderEdit) { /* If a job is being edited, reset edition mode as all fields are already transferred to the job */ resetJobEdit(); } else { /* If a job is being added, save fields into a new job */ saveJob(); /* There is now an evaluation for each change, so don't duplicate the evaluation now */ // jobEvaluationOnly = true; // evaluateJobs(); } } void Scheduler::saveJob() { if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: You cannot add or modify a job while the scheduler is running.")); return; } if (nameEdit->text().isEmpty()) { appendLogText(i18n("Warning: Target name is required.")); return; } if (sequenceEdit->text().isEmpty()) { appendLogText(i18n("Warning: Sequence file is required.")); return; } // Coordinates are required unless it is a FITS file if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) { appendLogText(i18n("Warning: Target coordinates are required.")); return; } bool raOk = false, decOk = false; dms /*const*/ ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms /*const*/ dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } watchJobChanges(false); /* Create or Update a scheduler job */ int currentRow = queueTable->currentRow(); SchedulerJob * job = nullptr; /* If no row is selected for insertion, append at end of list. */ if (currentRow < 0) currentRow = queueTable->rowCount(); /* Add job to queue only if it is new, else reuse current row. * Make sure job is added at the right index, now that queueTable may have a line selected without being edited. */ if (0 <= jobUnderEdit) { /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from queueTable->currentRow(). */ if (jobUnderEdit != currentRow) qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table."; /* Use the job in the row currently edited */ job = jobs.at(currentRow); } else { /* 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. */ job = new SchedulerJob(); jobs.insert(currentRow, job); queueTable->insertRow(currentRow); } /* Configure or reconfigure the observation job */ job->setName(nameEdit->text()); job->setPriority(prioritySpin->value()); job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); /* Consider sequence file is new, and clear captured frames map */ job->setCapturedFramesMap(SchedulerJob::CapturedFramesMap()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); job->setFITSFile(fitsURL); // #1 Startup conditions if (asapConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_ASAP); } else if (culminationConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_CULMINATION); job->setCulminationOffset(culminationOffset->value()); } else { job->setStartupCondition(SchedulerJob::START_AT); job->setStartupTime(startupTimeEdit->dateTime()); } /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); job->setFileStartupTime(job->getStartupTime()); // #2 Constraints // Do we have minimum altitude constraint? if (altConstraintCheck->isChecked()) job->setMinAltitude(minAltitude->value()); else job->setMinAltitude(-90); // Do we have minimum moon separation constraint? if (moonSeparationCheck->isChecked()) job->setMinMoonSeparation(minMoonSeparation->value()); else job->setMinMoonSeparation(-1); // Check enforce weather constraints job->setEnforceWeather(weatherCheck->isChecked()); // twilight constraints job->setEnforceTwilight(twilightCheck->isChecked()); /* Verifications */ /* FIXME: perhaps use a method more visible to the end-user */ if (SchedulerJob::START_AT == job->getFileStartupCondition()) { /* Warn if appending a job which startup time doesn't allow proper score */ if (calculateJobScore(job, job->getStartupTime()) < 0) appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); } // #3 Completion conditions if (sequenceCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); } else if (repeatCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_REPEAT); job->setRepeatsRequired(repeatsSpin->value()); job->setRepeatsRemaining(repeatsSpin->value()); } else if (loopCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_LOOP); } else { job->setCompletionCondition(SchedulerJob::FINISH_AT); job->setCompletionTime(completionTimeEdit->dateTime()); } // Job steps job->setStepPipeline(SchedulerJob::USE_NONE); if (trackStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_TRACK)); if (focusStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_FOCUS)); if (alignStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_ALIGN)); if (guideStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_GUIDE)); /* Reset job state to evaluate the changes */ job->reset(); // Warn user if a duplicated job is in the list - same target, same sequence // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! foreach (SchedulerJob *a_job, jobs) { if (a_job == job) { break; } else if (a_job->getName() == job->getName()) { int const a_job_row = a_job->getNameCell() ? a_job->getNameCell()->row() + 1 : 0; /* 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. */ appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, " "the scheduler may consider the same storage for captures.", job->getName(), currentRow, a_job_row)); /* Warn the user in case the two jobs are really identical */ if (a_job->getSequenceFile() == job->getSequenceFile()) { if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress()) appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count " "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')", job->getName(), currentRow, a_job_row, job->getRepeatsRequired())); if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " "as currently they will start in order of insertion in the table", job->getName(), currentRow)); } } } if (-1 == jobUnderEdit) { QTableWidgetItem *nameCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_NAME), nameCell); nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *statusCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STATUS), statusCell); statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *captureCount = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_CAPTURES), captureCount); captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *scoreValue = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_SCORE), scoreValue); scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *startupCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STARTTIME), startupCell); startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *altitudeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ALTITUDE), altitudeCell); altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *completionCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ENDTIME), completionCell); completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_DURATION), estimatedTimeCell); estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *leadTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_LEADTIME), leadTimeCell); leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } setJobStatusCells(currentRow); /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); startB->setEnabled(true); evaluateOnlyB->setEnabled(true); setJobManipulation(!Options::sortSchedulerJobs(), true); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1); watchJobChanges(true); if (SCHEDULER_LOADING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::syncGUIToJob(SchedulerJob *job) { nameEdit->setText(job->getName()); prioritySpin->setValue(job->getPriority()); raBox->showInHours(job->getTargetCoords().ra0()); decBox->showInDegrees(job->getTargetCoords().dec0()); if (job->getFITSFile().isEmpty() == false) fitsEdit->setText(job->getFITSFile().toLocalFile()); else fitsEdit->clear(); sequenceEdit->setText(job->getSequenceFile().toLocalFile()); trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); switch (job->getFileStartupCondition()) { case SchedulerJob::START_ASAP: asapConditionR->setChecked(true); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; case SchedulerJob::START_CULMINATION: culminationConditionR->setChecked(true); culminationOffset->setValue(job->getCulminationOffset()); break; case SchedulerJob::START_AT: startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(job->getStartupTime()); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; } if (-90 < job->getMinAltitude()) { altConstraintCheck->setChecked(true); minAltitude->setValue(job->getMinAltitude()); } else { altConstraintCheck->setChecked(false); minAltitude->setValue(DEFAULT_MIN_ALTITUDE); } if (job->getMinMoonSeparation() >= 0) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(job->getMinMoonSeparation()); } else { moonSeparationCheck->setChecked(false); minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION); } weatherCheck->setChecked(job->getEnforceWeather()); twilightCheck->blockSignals(true); twilightCheck->setChecked(job->getEnforceTwilight()); twilightCheck->blockSignals(false); switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: sequenceCompletionR->setChecked(true); break; case SchedulerJob::FINISH_REPEAT: repeatCompletionR->setChecked(true); repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_LOOP: loopCompletionR->setChecked(true); break; case SchedulerJob::FINISH_AT: timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(job->getCompletionTime()); break; } setJobManipulation(!Options::sortSchedulerJobs(), true); } void Scheduler::loadJob(QModelIndex i) { if (jobUnderEdit == i.row()) return; if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: you cannot add or modify a job while the scheduler is running.")); return; } SchedulerJob * const job = jobs.at(i.row()); if (job == nullptr) return; watchJobChanges(false); //job->setState(SchedulerJob::JOB_IDLE); //job->setStage(SchedulerJob::STAGE_IDLE); syncGUIToJob(job); if (job->getFITSFile().isEmpty() == false) fitsURL = job->getFITSFile(); else fitsURL = QUrl(); sequenceURL = job->getSequenceFile(); /* Turn the add button into an apply button */ setJobAddApply(false); /* Disable scheduler start/evaluate buttons */ startB->setEnabled(false); evaluateOnlyB->setEnabled(false); /* Don't let the end-user remove a job being edited */ setJobManipulation(false, false); jobUnderEdit = i.row(); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(jobUnderEdit + 1); watchJobChanges(true); } void Scheduler::queueTableSelectionChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous); // prevent selection when not idle if (state != SCHEDULER_IDLE) return; if (current.row() < 0 || (current.row() + 1) > jobs.size()) return; SchedulerJob * const job = jobs.at(current.row()); if (job == nullptr) return; resetJobEdit(); syncGUIToJob(job); } void Scheduler::clickQueueTable(QModelIndex index) { setJobManipulation(!Options::sortSchedulerJobs() && index.isValid(), index.isValid()); } void Scheduler::setJobAddApply(bool add_mode) { if (add_mode) { addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list.")); //addToQueueB->setStyleSheet(QString()); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); } else { addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); //addToQueueB->setStyleSheet("background-color:orange;}"); addToQueueB->setEnabled(true); } } void Scheduler::setJobManipulation(bool can_reorder, bool can_delete) { bool can_edit = (state == SCHEDULER_IDLE); if (can_reorder) { int const currentRow = queueTable->currentRow(); queueUpB->setEnabled(can_edit && 0 < currentRow); queueDownB->setEnabled(can_edit && currentRow < queueTable->rowCount() - 1); } else { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } sortJobsB->setEnabled(can_edit && can_reorder); removeFromQueueB->setEnabled(can_edit && can_delete); } bool Scheduler::reorderJobs(QList reordered_sublist) { /* Add jobs not reordered at the end of the list, in initial order */ foreach (SchedulerJob* job, jobs) if (!reordered_sublist.contains(job)) reordered_sublist.append(job); if (jobs != reordered_sublist) { /* Remember job currently selected */ int const selectedRow = queueTable->currentRow(); SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr; /* Reassign list */ jobs = reordered_sublist; /* Reassign status cells for all jobs, and reset them */ for (int row = 0; row < jobs.size(); row++) setJobStatusCells(row); /* Reselect previously selected job */ if (nullptr != selectedJob) queueTable->selectRow(jobs.indexOf(selectedJob)); return true; } else return false; } void Scheduler::moveJobUp() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow - 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) return; /* Swap jobs in the list */ jobs.swap(currentRow, destinationRow); /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = destinationRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::moveJobDown() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow + 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) return; /* Swap jobs in the list */ jobs.swap(currentRow, destinationRow); /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = currentRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::setJobStatusCells(int row) { if (row < 0 || jobs.size() <= row) return; SchedulerJob * const job = jobs.at(row); job->setNameCell(queueTable->item(row, static_cast(SCHEDCOL_NAME))); job->setStatusCell(queueTable->item(row, static_cast(SCHEDCOL_STATUS))); job->setCaptureCountCell(queueTable->item(row, static_cast(SCHEDCOL_CAPTURES))); job->setScoreCell(queueTable->item(row, static_cast(SCHEDCOL_SCORE))); job->setAltitudeCell(queueTable->item(row, static_cast(SCHEDCOL_ALTITUDE))); job->setStartupCell(queueTable->item(row, static_cast(SCHEDCOL_STARTTIME))); job->setCompletionCell(queueTable->item(row, static_cast(SCHEDCOL_ENDTIME))); job->setEstimatedTimeCell(queueTable->item(row, static_cast(SCHEDCOL_DURATION))); job->setLeadTimeCell(queueTable->item(row, static_cast(SCHEDCOL_LEADTIME))); job->updateJobCells(); } void Scheduler::resetJobEdit() { if (jobUnderEdit < 0) return; SchedulerJob * const job = jobs.at(jobUnderEdit); Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid"); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(jobUnderEdit + 1); jobUnderEdit = -1; watchJobChanges(false); /* Revert apply button to add */ setJobAddApply(true); /* Refresh state of job manipulation buttons */ setJobManipulation(!Options::sortSchedulerJobs(), true); /* Restore scheduler operation buttons */ evaluateOnlyB->setEnabled(true); startB->setEnabled(true); Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode"); } void Scheduler::removeJob() { int currentRow = queueTable->currentRow(); /* Don't remove a row that is not selected */ if (currentRow < 0) return; /* Grab the job currently selected */ SchedulerJob * const job = jobs.at(currentRow); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1); /* Remove the job from the table */ queueTable->removeRow(currentRow); /* If there are no job rows left, update UI buttons */ if (queueTable->rowCount() == 0) { setJobManipulation(false, false); evaluateOnlyB->setEnabled(false); queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); startB->setEnabled(false); pauseB->setEnabled(false); } /* Else update the selection */ else { if (currentRow > queueTable->rowCount()) currentRow = queueTable->rowCount() - 1; loadJob(queueTable->currentIndex()); queueTable->selectRow(currentRow); } /* If needed, reset edit mode to clean up UI */ if (jobUnderEdit >= 0) resetJobEdit(); /* And remove the job object */ jobs.removeOne(job); delete (job); mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::toggleScheduler() { if (state == SCHEDULER_RUNNING) { preemptiveShutdown = false; stop(); } else start(); } void Scheduler::stop() { if (state != SCHEDULER_RUNNING) return; qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping..."; // Stop running job and abort all others // in case of soft shutdown we skip this if (preemptiveShutdown == false) { bool wasAborted = false; foreach (SchedulerJob *job, jobs) { if (job == currentJob) { stopCurrentJobAction(); stopGuiding(); } if (job->getState() <= SchedulerJob::JOB_BUSY) { appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName())); job->setState(SchedulerJob::JOB_ABORTED); wasAborted = true; } } if (wasAborted) KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted.")); } schedulerTimer.stop(); jobTimer.stop(); restartGuidingTimer.stop(); state = SCHEDULER_IDLE; emit newStatus(state); ekosState = EKOS_IDLE; indiState = INDI_IDLE; parkWaitState = PARKWAIT_IDLE; // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete. // Or if we're doing a soft shutdown if (startupState != STARTUP_COMPLETE || preemptiveShutdown) { if (startupState == STARTUP_SCRIPT) { scriptProcess.disconnect(); scriptProcess.terminate(); } startupState = STARTUP_IDLE; } // Reset startup state to unparking phase (dome -> mount -> cap) // We do not want to run the startup script again but unparking should be checked // whenever the scheduler is running again. else if (startupState == STARTUP_COMPLETE) { if (unparkDomeCheck->isChecked()) startupState = STARTUP_UNPARK_DOME; else if (unparkMountCheck->isChecked()) startupState = STARTUP_UNPARK_MOUNT; else if (uncapCheck->isChecked()) startupState = STARTUP_UNPARK_CAP; } shutdownState = SHUTDOWN_IDLE; setCurrentJob(nullptr); captureBatch = 0; indiConnectFailureCount = 0; ekosConnectFailureCount = 0; focusFailureCount = 0; guideFailureCount = 0; alignFailureCount = 0; captureFailureCount = 0; jobEvaluationOnly = false; loadAndSlewProgress = false; autofocusCompleted = false; startupB->setEnabled(true); shutdownB->setEnabled(true); // If soft shutdown, we return for now if (preemptiveShutdown) { sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready")); sleepLabel->show(); return; } // Clear target name in capture interface upon stopping if (captureInterface.isNull() == false) captureInterface->setProperty("targetName", QString()); if (scriptProcess.state() == QProcess::Running) scriptProcess.terminate(); sleepTimer.stop(); //sleepTimer.disconnect(); sleepLabel->hide(); pi->stopAnimation(); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Scheduler")); pauseB->setEnabled(false); //startB->setText("Start Scheduler"); queueLoadB->setEnabled(true); addToQueueB->setEnabled(true); setJobManipulation(false, false); mosaicB->setEnabled(true); evaluateOnlyB->setEnabled(true); } void Scheduler::start() { switch (state) { case SCHEDULER_IDLE: /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */ startupScriptURL = QUrl::fromUserInput(startupScript->text()); if (!startupScript->text().isEmpty() && !startupScriptURL.isValid()) { appendLogText(i18n("Warning: startup script URL %1 is not valid.", startupScript->text())); return; } /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */ shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); if (!shutdownScript->text().isEmpty() && !shutdownScriptURL.isValid()) { appendLogText(i18n("Warning: shutdown script URL %1 is not valid.", shutdownScript->text())); return; } qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting..."; /* Update UI to reflect startup */ pi->startAnimation(); sleepLabel->hide(); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setChecked(false); /* Disable edit-related buttons */ queueLoadB->setEnabled(false); addToQueueB->setEnabled(false); setJobManipulation(false, false); mosaicB->setEnabled(false); evaluateOnlyB->setEnabled(false); startupB->setEnabled(false); shutdownB->setEnabled(false); /* Reset and re-evaluate all scheduler jobs, then start the Scheduler */ startJobEvaluation(); state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler started.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started."; break; case SCHEDULER_PAUSED: /* Update UI to reflect resume */ startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setCheckable(false); pauseB->setChecked(false); /* Edit-related buttons are still disabled */ /* The end-user cannot update the schedule, don't re-evaluate jobs. Timer schedulerTimer is already running. */ state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler resuming.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming."; break; default: break; } } void Scheduler::pause() { state = SCHEDULER_PAUSED; emit newStatus(state); appendLogText(i18n("Scheduler pause planned...")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Scheduler")); } void Scheduler::setPaused() { pauseB->setCheckable(true); pauseB->setChecked(true); schedulerTimer.stop(); appendLogText(i18n("Scheduler paused.")); } void Scheduler::setCurrentJob(SchedulerJob *job) { /* Reset job widgets */ if (currentJob) { currentJob->setStageLabel(nullptr); } /* Set current job */ currentJob = job; /* Reassign job widgets, or reset to defaults */ if (currentJob) { currentJob->setStageLabel(jobStatus); queueTable->selectRow(jobs.indexOf(currentJob)); } else { jobStatus->setText(i18n("No job running")); //queueTable->clearSelection(); } } void Scheduler::evaluateJobs() { /* Don't evaluate if list is empty */ if (jobs.isEmpty()) return; /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ QDateTime const now = KStarsData::Instance()->lt(); /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ if (Options::rememberJobProgress()) updateCompletedJobsCount(); /* Update dawn and dusk astronomical times - unconditionally in case date changed */ calculateDawnDusk(); /* First, filter out non-schedulable jobs */ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ QList sortedJobs = jobs; /* Then enumerate SchedulerJobs to consolidate imaging time */ foreach (SchedulerJob *job, sortedJobs) { /* Let aborted jobs be rescheduled later instead of forgetting them */ switch (job->getState()) { case SchedulerJob::JOB_SCHEDULED: /* If job is scheduled, keep it for evaluation against others */ break; case SchedulerJob::JOB_INVALID: case SchedulerJob::JOB_COMPLETE: /* If job is invalid or complete, bypass evaluation */ continue; case SchedulerJob::JOB_BUSY: /* If job is busy, edge case, bypass evaluation */ continue; case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */ if (state == SCHEDULER_RUNNING) continue; /* Fall through */ case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: default: /* If job is idle, re-evaluate completely */ job->setEstimatedTime(-1); break; } switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_AT: /* Job is complete if its fixed completion time is passed */ if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) { job->setState(SchedulerJob::JOB_COMPLETE); continue; } break; case SchedulerJob::FINISH_REPEAT: // In case of a repeating jobs, let's make sure we have more runs left to go // If we don't, re-estimate imaging time for the scheduler job before concluding if (job->getRepeatsRemaining() == 0) { appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); if (Options::rememberJobProgress()) { job->setEstimatedTime(-1); } else { job->setState(SchedulerJob::JOB_COMPLETE); job->setEstimatedTime(0); continue; } } break; default: break; } // -1 = Job is not estimated yet // -2 = Job is estimated but time is unknown // > 0 Job is estimated and time is known if (job->getEstimatedTime() == -1) { if (estimateJobTime(job) == false) { job->setState(SchedulerJob::JOB_INVALID); continue; } } if (job->getEstimatedTime() == 0) { job->setRepeatsRemaining(0); job->setState(SchedulerJob::JOB_COMPLETE); continue; } // In any other case, evaluate job->setState(SchedulerJob::JOB_EVALUATION); } /* * At this step, we prepare scheduling of jobs. * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. */ updatePreDawn(); /* This predicate matches jobs not being evaluated and not aborted */ auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; }; /* This predicate matches jobs neither being evaluated nor aborted nor in error state */ auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != s; }; /* This predicate matches jobs that aborted, or completed for whatever reason */ auto finished_or_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s; }; bool nea = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted); bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error); /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) { appendLogText(i18n("No jobs left in the scheduler queue.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those */ if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.", errorHandlingRescheduleErrorsCB->isChecked() ? "aborted or error" : "aborted")); // set aborted and error jobs to evaluation state for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const job = sortedJobs.at(index); if (SchedulerJob::JOB_ABORTED == job->getState() || (errorHandlingRescheduleErrorsCB->isChecked() && SchedulerJob::JOB_ERROR == job->getState())) job->setState(SchedulerJob::JOB_EVALUATION); } if (errorHandlingRestartAfterAllButton->isChecked()) { // interrupt regular status checks during the sleep time schedulerTimer.stop(); // but before we restart them, we wait for the given delay. appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); // we continue to determine which job should be running, when the delay is over } } /* If option says so, reorder by altitude and priority before sequencing */ /* FIXME: refactor so all sorts are using the same predicates */ /* FIXME: use std::stable_sort as qStableSort is deprecated */ /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); if (Options::sortSchedulerJobs()) { using namespace std::placeholders; std::stable_sort(sortedJobs.begin(), sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); } /* The first reordered job has no lead time - this could also be the delay from now to startup */ sortedJobs.first()->setLeadTime(0); /* The objective of the following block is to make sure jobs are sequential in the list filtered previously. * * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable. * If the completion time of the previous job overlaps the current job, we offset the startup of the current job. * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time. * The lead time from the Options registry is used as a buffer between jobs. * * Note about the situation where the current job overlaps the next job, and the next job is not movable: * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory. * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an * infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort. * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit. * Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled * at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the * schedule will be altered by external events during the execution. * * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs: * - Twilight constraint moves jobs to the next dark sky interval. * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time. * - Culmination constraint moves jobs to the next transit time, with arbitrary offset. * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past. * * Here are the constraints that have an effect on jobs following the job being examined: * - Repeats requirement increases the duration of the current job, pushing subsequent jobs. * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented). * - Fixed completion makes subsequent jobs start after that boundary time. * * However, we need a way to inform the end-user about failed schedules clearly in the UI. * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will * be processed when possible, probably not at the expected moment. */ // Make sure no two jobs have the same scheduled time or overlap with other jobs for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const currentJob = sortedJobs.at(index); // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order if (SchedulerJob::JOB_EVALUATION != currentJob->getState()) continue; // At this point, a job with no valid start date is a problem, so consider invalid startup time is now if (!currentJob->getStartupTime().isValid()) currentJob->setStartupTime(now); // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated SchedulerJob const * previousJob = nullptr; for (int i = index - 1; 0 <= i; i--) { SchedulerJob const * const a_job = sortedJobs.at(i); if (SchedulerJob::JOB_SCHEDULED == a_job->getState()) { previousJob = a_job; break; } } Q_ASSERT_X(nullptr == previousJob || previousJob != currentJob, __FUNCTION__, "Previous job considered for schedule is either undefined or not equal to current."); // Locate the next job - nothing special required except end of list check SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr; Q_ASSERT_X(nullptr == nextJob || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); // We're attempting to schedule the job 10 times before making it invalid for (int attempt = 1; attempt < 11; attempt++) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") .arg(attempt) .arg(static_cast(currentJob->getEstimatedTime())) .arg(currentJob->getName()) .arg(index + 1) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())); // ----- #1 Should we reject the current job because of its fixed startup time? // // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime. // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm. // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors. // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved. if (SchedulerJob::START_AT == currentJob->getFileStartupCondition()) { // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now if (currentJob->getStartupTime().addSecs(static_cast (ceil(Options::leadTime() * 60))) < now) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } // Check whether the current job has a positive dark sky score at the time of startup else if (true == currentJob->getEnforceTwilight() && getDarkSkyScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive altitude score at the time of startup else if (-90 < currentJob->getMinAltitude() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive Moon separation score at the time of startup else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", currentJob->getName())); break; } // Check whether a previous job overlaps the current job if (nullptr != previousJob && previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Make this job invalid if startup time is not achievable because a START_AT job is non-movable if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); } // This job is non-movable, we're done currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); break; } // ----- #2 Should we delay the current job because it overlaps the previous job? // // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job. // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap. // If there is a previous job, the current job is simply delayed to avoid an eventual overlap. // IF there is a previous job but it never finishes, the current job is rejected. // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable. if (nullptr != previousJob) { if (previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setStartupTime(previousCompletionTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())) .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat())); // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition()) if (false == estimateJobTime(currentJob)) currentJob->setState(SchedulerJob::JOB_INVALID); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.", currentJob->getName())); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); // Lead time can be zero, so completion may equal startup Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); } // ----- #3 Should we delay the current job because it overlaps daylight? // // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night. // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here. // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn. // However, completion time during daylight only causes a warning, as this case will be processed as the job runs. if (currentJob->getEnforceTwilight()) { // During that check, we don't verify the current job can actually complete before dawn. // If the job is interrupted while running, it will be aborted and rescheduled at a later time. // We wouldn't start observation 30 mins (default) before dawn. // FIXME: Refactor duplicated dawn/dusk calculations double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); // Compute dawn time for the startup date of the job // FIXME: Use KAlmanac to find the real dawn/dusk time for the day the job is supposed to be processed QDateTime const dawnDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(earlyDawn * 24 * 3600)); // Check if the job starts after dawn if (dawnDateTime < currentJob->getStartupTime()) { // Compute dusk time for the startup date of the job - no lead time on dusk QDateTime duskDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(Dusk * 24 * 3600)); // Near summer solstice, dusk may happen before dawn on the same day, shift dusk by one day in that case if (duskDateTime < dawnDateTime) duskDateTime = duskDateTime.addDays(1); // Check if the job starts before dusk if (currentJob->getStartupTime() < duskDateTime) { // Delay job to next dusk - we will check other requirements later on currentJob->setStartupTime(duskDateTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } // Compute dawn time for the day following the startup time, but disregard the pre-dawn offset as we'll consider completion // FIXME: Use KAlmanac to find the real dawn/dusk time for the day next to the day the job is supposed to be processed QDateTime const nextDawnDateTime(currentJob->getStartupTime().date().addDays(1), QTime(0, 0).addSecs(Dawn * 24 * 3600)); // Check if the completion date overlaps the next dawn, and issue a warning if so if (nextDawnDateTime < currentJob->getCompletionTime()) { appendLogText(i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", currentJob->getName())); } Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated startup time results in a positive dark sky score."); } // ----- #4 Should we delay the current job because of its target culmination? // // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time. // This restriction may be used to start a job at the least air mass, or after a meridian flip. // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time. // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid. if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) { // Consolidate the culmination time, with offset, of the current job QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime()); if (nextCulminationTime.isValid()) // Guaranteed { if (currentJob->getStartupTime() < nextCulminationTime) { currentJob->setStartupTime(nextCulminationTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(currentJob->getCulminationOffset()))); break; } // Don't test altitude here, because we will push the job during the next check step // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #5 Should we delay the current job because its altitude is incorrect? // // Altitude time ensures the job is assigned a startup time when its target is high enough. // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running. // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time. // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case. if (-90 < currentJob->getMinAltitude()) { // Consolidate a new altitude time from the startup time of the current job QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime()); if (nextAltitudeTime.isValid()) { if (currentJob->getStartupTime() < nextAltitudeTime) { currentJob->setStartupTime(nextAltitudeTime); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), 0.0 < currentJob->getMinMoonSeparation() ? QString("%L1").arg(static_cast(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : QString("-"))); break; } Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable? // // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account. // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation. if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition()) { // In the current implementation, it is not possible to abort a running job when the next job is supposed to start. // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers. // Calculate time we have between the end of the current job and the next job double const timeToNext = static_cast (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime())); // If that time is overlapping the next job, abort the current job if (timeToNext < Options::leadTime() * 60) { currentJob->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", currentJob->getName())); break; } Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__, "No overlap "); } // ----- #7 Should we reject the current job because it exceeded its fixed completion time? // // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time. // Its main objective is to catch wrong dates in the FINISH_AT configuration. if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition()) { if (currentJob->getCompletionTime() < currentJob->getStartupTime()) { appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_INVALID); break; } } // ----- #8 Should we reject the current job because of weather? // // That verification is left for runtime // // if (false == isWeatherOK(currentJob)) //{ // currentJob->setState(SchedulerJob::JOB_ABORTED); // // appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName())); //} // ----- #9 Update score for current time and mark evaluating jobs as scheduled currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") .arg(currentJob->getName()) .arg(index + 1) .arg(attempt) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getEstimatedTime()); break; } // Check if job was successfully scheduled, else reject it if (SchedulerJob::JOB_EVALUATION == currentJob->getState()) { currentJob->setState(SchedulerJob::JOB_INVALID); //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.", // currentJob->getName(), // index + 1)); #if 0 // Advices if (-90 < currentJob->getMinAltitude()) appendLogText(i18n("Job '%1' may require relaxing the current altitude requirement of %2 degrees.", currentJob->getName(), QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals))); if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition() && Options::leadTime() < 5) appendLogText(i18n("Job '%1' may require increasing the current lead time of %2 minutes to make transit time calculation stable.", currentJob->getName(), Options::leadTime())); #endif } } /* Apply sorting to queue table, and mark it for saving if it changes */ mDirty = reorderJobs(sortedJobs) | mDirty; if (jobEvaluationOnly || state != SCHEDULER_RUNNING) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; jobEvaluationOnly = false; return; } /* * At this step, we finished evaluating jobs. * We select the first job that has to be run, per schedule. */ /* This predicate matches jobs that are neither scheduled to run nor aborted */ auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; }; /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted)) { appendLogText(i18n("No jobs left in the scheduler queue after evaluating.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */ else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) { if (SchedulerJob::JOB_ABORTED == job->getState()) job->setState(SchedulerJob::JOB_EVALUATION); }); jobEvaluationOnly = false; return; } /* The job to run is the first scheduled, locate it in the list */ QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * const job) { return SchedulerJob::JOB_SCHEDULED == job->getState(); }); /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */ if (sortedJobs.end() == job_to_execute_iterator) { appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* Check if job can be processed right now */ SchedulerJob * const job_to_execute = *job_to_execute_iterator; if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) if( 0 <= calculateJobScore(job_to_execute, now)) job_to_execute->setStartupTime(now); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") .arg(job_to_execute->getName()) .arg(job_to_execute->getPriority()) .arg(job_to_execute->getScore()); // Set the current job, and let the status timer execute it when ready setCurrentJob(job_to_execute); } void Scheduler::wakeUpScheduler() { sleepLabel->hide(); sleepTimer.stop(); if (preemptiveShutdown) { preemptiveShutdown = false; appendLogText(i18n("Scheduler is awake.")); start(); } else { if (state == SCHEDULER_RUNNING) appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); else appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); schedulerTimer.start(); } } int16_t Scheduler::getWeatherScore() const { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return 0; if (weatherStatus == ISD::Weather::WEATHER_WARNING) return BAD_SCORE / 2; else if (weatherStatus == ISD::Weather::WEATHER_ALERT) return BAD_SCORE; return 0; } int16_t Scheduler::getDarkSkyScore(QDateTime const &when) const { double const secsPerDay = 24.0 * 3600.0; double const minsPerDay = 24.0 * 60.0; // Dark sky score is calculated based on distance to today's dawn and next dusk. // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. // - If observation is between dawn and dusk, score is BAD_SCORE. // // If observation time is invalid, the score is calculated for the current day time. // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. // FIXME: Dark sky score should consider the middle of the local night as best value. // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. int const earlyDawnSecs = static_cast ((Dawn - static_cast (Options::preDawnTime()) / minsPerDay) * secsPerDay); int const dawnSecs = static_cast (Dawn * secsPerDay); int const duskSecs = static_cast (Dusk * secsPerDay); int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay() / 1000; int16_t score = 0; if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) { score = BAD_SCORE / 50; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (obsSecs < dawnSecs) { score = static_cast ((dawnSecs - obsSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (duskSecs <= obsSecs) { score = static_cast ((obsSecs - duskSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else { score = BAD_SCORE; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } return score; } int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const { if (nullptr == job) return BAD_SCORE; /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ if (!job->getLightFramesRequired()) return 1000; int16_t total = 0; /* As soon as one score is negative, it's a no-go and other scores are unneeded */ if (job->getEnforceTwilight()) { int16_t const darkSkyScore = getDarkSkyScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", darkSkyScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += darkSkyScore; } /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. */ if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) { int16_t const altitudeScore = job->getAltitudeScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", altitudeScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += altitudeScore; } if (0 <= total) { int16_t const moonSeparationScore = job->getMoonSeparationScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", moonSeparationScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += moonSeparationScore; } qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") .arg(job->getName()) .arg(QString::asprintf("%+d", total)) .arg(when.toString(job->getDateTimeDisplayFormat())); return total; } void Scheduler::calculateDawnDusk() { KSAlmanac ksal; - Dawn = ksal.getDawnAstronomicalTwilight(); - Dusk = ksal.getDuskAstronomicalTwilight(); + Dawn = ksal.getDawnAstronomicalTwilight() + Options::dawnOffset() / 24.0; + Dusk = ksal.getDuskAstronomicalTwilight() + Options::duskOffset() / 24.0; - //QTime now = KStarsData::Instance()->lt().time(); - //QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); - QTime dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); + QTime const dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); + QTime const dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); duskDateTime.setDate(KStars::Instance()->data()->lt().date()); duskDateTime.setTime(dusk); - // FIXME: reduce spam by moving twilight time to a text label - //appendLogText(i18n("Astronomical twilight: dusk at %1, dawn at %2, and current time is %3", - // dusk.toString(), dawn.toString(), now.toString())); + nightTime->setText(i18n("%1 - %2", dusk.toString("hh:mm"), dawn.toString("hh:mm"))); } void Scheduler::executeJob(SchedulerJob *job) { // Some states have executeJob called after current job is cancelled - checkStatus does this if (job == nullptr) return; // Don't execute the current job if it is already busy if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState()) return; setCurrentJob(job); int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); QDateTime const now = KStarsData::Instance()->lt(); // If we already started, we check when the next object is scheduled at. // If it is more than 30 minutes in the future, we park the mount if that is supported // and we unpark when it is due to start. //int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If the time to wait is greater than the lead time (5 minutes by default) // then we sleep, otherwise we wait. It's the same thing, just different labels. if (shouldSchedulerSleep(currentJob)) return; // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt else if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) return; // From this point job can be executed now if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { captureInterface->setProperty("targetName", job->getName().replace(' ', "")); } updatePreDawn(); // Reset autofocus so that focus step is applied properly when checked // When the focus step is not checked, the capture module will eventually run focus periodically autofocusCompleted = false; qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName(); currentJob->setState(SchedulerJob::JOB_BUSY); KNotification::event(QLatin1String("EkosSchedulerJobStart"), i18n("Ekos job started (%1)", currentJob->getName())); // No need to continue evaluating jobs as we already have one. schedulerTimer.stop(); jobTimer.start(); } bool Scheduler::checkEkosState() { if (state == SCHEDULER_PAUSED) return false; switch (ekosState) { case EKOS_IDLE: { if (m_EkosCommunicationStatus == Ekos::Success) { ekosState = EKOS_READY; return true; } else { ekosInterface->call(QDBus::AutoDetect, "start"); ekosState = EKOS_STARTING; currentOperationTime.start(); qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << m_EkosCommunicationStatus << "Starting Ekos..."; return false; } } case EKOS_STARTING: { if (m_EkosCommunicationStatus == Ekos::Success) { appendLogText(i18n("Ekos started.")); ekosConnectFailureCount = 0; ekosState = EKOS_READY; return true; } else if (m_EkosCommunicationStatus == Ekos::Error) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos failed. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "start"); return false; } appendLogText(i18n("Starting Ekos failed.")); stop(); return false; } else if (m_EkosCommunicationStatus == Ekos::Idle) return false; // If a minute passed, give up else if (currentOperationTime.elapsed() > (60 * 1000)) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "stop"); QTimer::singleShot(1000, this, [&]() { ekosInterface->call(QDBus::AutoDetect, "start"); currentOperationTime.restart(); }); return false; } appendLogText(i18n("Starting Ekos timed out.")); stop(); return false; } } break; case EKOS_STOPPING: { if (m_EkosCommunicationStatus == Ekos::Idle) { appendLogText(i18n("Ekos stopped.")); ekosState = EKOS_IDLE; return true; } } break; case EKOS_READY: return true; } return false; } bool Scheduler::isINDIConnected() { return (m_INDICommunicationStatus == Ekos::Success); } bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) return false; //qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State" << indiState; switch (indiState) { case INDI_IDLE: { if (m_INDICommunicationStatus == Ekos::Success) { indiState = INDI_PROPERTY_CHECK; indiConnectFailureCount = 0; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties..."; } else { qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices..."; ekosInterface->call(QDBus::AutoDetect, "connectDevices"); indiState = INDI_CONNECTING; currentOperationTime.start(); } } break; case INDI_CONNECTING: { if (m_INDICommunicationStatus == Ekos::Success) { appendLogText(i18n("INDI devices connected.")); indiState = INDI_PROPERTY_CHECK; } else if (m_INDICommunicationStatus == Ekos::Error) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); } else { appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details.")); stop(); } } // If 30 seconds passed, we retry else if (currentOperationTime.elapsed() > (30 * 1000)) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); currentOperationTime.restart(); } else { appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details.")); stop(); } } } break; case INDI_DISCONNECTING: { if (m_INDICommunicationStatus == Ekos::Idle) { appendLogText(i18n("INDI devices disconnected.")); indiState = INDI_IDLE; return true; } } break; case INDI_PROPERTY_CHECK: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties."; // If dome unparking is required then we wait for dome interface if (unparkDomeCheck->isChecked() && m_DomeReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome unpark required but dome is not yet ready."; return false; } // If mount unparking is required then we wait for mount interface if (unparkMountCheck->isChecked() && m_MountReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready."; return false; } // If cap unparking is required then we wait for cap interface if (uncapCheck->isChecked() && m_CapReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready."; return false; } // capture interface is required at all times to proceed. if (captureInterface.isNull()) return false; if (m_CaptureReady == false) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); if (hasCoolerControl.isValid()) { warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } else qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet..."; } indiState = INDI_READY; indiConnectFailureCount = 0; return true; #if 0 // Check if mount and dome support parking or not. QDBusReply boolReply = mountInterface->call(QDBus::AutoDetect, "canPark"); unparkMountCheck->setEnabled(boolReply.value()); parkMountCheck->setEnabled(boolReply.value()); //qDebug() << "Mount can park " << boolReply.value(); boolReply = domeInterface->call(QDBus::AutoDetect, "canPark"); unparkDomeCheck->setEnabled(boolReply.value()); parkDomeCheck->setEnabled(boolReply.value()); boolReply = captureInterface->call(QDBus::AutoDetect, "hasCoolerControl"); warmCCDCheck->setEnabled(boolReply.value()); QDBusReply updateReply = weatherInterface->call(QDBus::AutoDetect, "getUpdatePeriod"); if (updateReply.error().type() == QDBusError::NoError) { weatherCheck->setEnabled(true); if (updateReply.value() > 0) { weatherTimer.setInterval(updateReply.value() * 1000); connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather); weatherTimer.start(); // Check weather initially checkWeather(); } } else weatherCheck->setEnabled(false); QDBusReply capReply = capInterface->call(QDBus::AutoDetect, "canPark"); if (capReply.error().type() == QDBusError::NoError) { capCheck->setEnabled(capReply.value()); uncapCheck->setEnabled(capReply.value()); } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } indiState = INDI_READY; return true; #endif } case INDI_READY: return true; } return false; } bool Scheduler::checkStartupState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(startupState); switch (startupState) { case STARTUP_IDLE: { KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process..."; // If Ekos is already started, we skip the script and move on to dome unpark step // unless we do not have light frames, then we skip all //QDBusReply isEkosStarted; //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); //if (isEkosStarted.value() == Ekos::Success) if (m_EkosCommunicationStatus == Ekos::Success) { if (startupScriptURL.isEmpty() == false) appendLogText(i18n("Ekos is already started, skipping startup script...")); if (currentJob->getLightFramesRequired()) startupState = STARTUP_UNPARK_DOME; else startupState = STARTUP_COMPLETE; return true; } if (schedulerProfileCombo->currentText() != i18n("Default")) { QList profile; profile.append(schedulerProfileCombo->currentText()); ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile); } if (startupScriptURL.isEmpty() == false) { startupState = STARTUP_SCRIPT; executeScript(startupScriptURL.toString(QUrl::PreferLocalFile)); return false; } startupState = STARTUP_UNPARK_DOME; return false; } case STARTUP_SCRIPT: return false; case STARTUP_UNPARK_DOME: // If there is no job in case of manual startup procedure, // or if the job requires light frames, let's proceed with // unparking the dome, otherwise startup process is complete. if (currentJob == nullptr || currentJob->getLightFramesRequired()) { if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked()) unParkDome(); else startupState = STARTUP_UNPARK_MOUNT; } else { startupState = STARTUP_COMPLETE; return true; } break; case STARTUP_UNPARKING_DOME: checkDomeParkingStatus(); break; case STARTUP_UNPARK_MOUNT: if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked()) unParkMount(); else startupState = STARTUP_UNPARK_CAP; break; case STARTUP_UNPARKING_MOUNT: checkMountParkingStatus(); break; case STARTUP_UNPARK_CAP: if (uncapCheck->isEnabled() && uncapCheck->isChecked()) unParkCap(); else startupState = STARTUP_COMPLETE; break; case STARTUP_UNPARKING_CAP: checkCapParkingStatus(); break; case STARTUP_COMPLETE: return true; case STARTUP_ERROR: stop(); return true; } return false; } bool Scheduler::checkShutdownState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state..."; switch (shutdownState) { case SHUTDOWN_IDLE: KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process")); qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process..."; // weatherTimer.stop(); // weatherTimer.disconnect(); weatherLabel->hide(); jobTimer.stop(); setCurrentJob(nullptr); if (state == SCHEDULER_RUNNING) schedulerTimer.start(); if (preemptiveShutdown == false) { sleepTimer.stop(); //sleepTimer.disconnect(); } if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked()) { appendLogText(i18n("Warming up CCD...")); // Turn it off //QVariant arg(false); //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg); captureInterface->setProperty("coolerControl", false); } // The following steps require a connection to the INDI server if (isINDIConnected()) { if (capCheck->isEnabled() && capCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_CAP; return false; } if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_MOUNT; return false; } if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_DOME; return false; } } else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection.")); if (shutdownScriptURL.isEmpty() == false) { shutdownState = SHUTDOWN_SCRIPT; return false; } shutdownState = SHUTDOWN_COMPLETE; return true; case SHUTDOWN_PARK_CAP: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (capCheck->isEnabled() && capCheck->isChecked()) parkCap(); else shutdownState = SHUTDOWN_PARK_MOUNT; break; case SHUTDOWN_PARKING_CAP: checkCapParkingStatus(); break; case SHUTDOWN_PARK_MOUNT: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) parkMount(); else shutdownState = SHUTDOWN_PARK_DOME; break; case SHUTDOWN_PARKING_MOUNT: checkMountParkingStatus(); break; case SHUTDOWN_PARK_DOME: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) parkDome(); else shutdownState = SHUTDOWN_SCRIPT; break; case SHUTDOWN_PARKING_DOME: checkDomeParkingStatus(); break; case SHUTDOWN_SCRIPT: if (shutdownScriptURL.isEmpty() == false) { // Need to stop Ekos now before executing script if it happens to stop INDI if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI()) { stopEkos(); return false; } shutdownState = SHUTDOWN_SCRIPT_RUNNING; executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile)); } else shutdownState = SHUTDOWN_COMPLETE; break; case SHUTDOWN_SCRIPT_RUNNING: return false; case SHUTDOWN_COMPLETE: return true; case SHUTDOWN_ERROR: stop(); return true; } return false; } bool Scheduler::checkParkWaitState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State..."; switch (parkWaitState) { case PARKWAIT_IDLE: return true; case PARKWAIT_PARK: parkMount(); break; case PARKWAIT_PARKING: checkMountParkingStatus(); break; case PARKWAIT_PARKED: return true; case PARKWAIT_UNPARK: unParkMount(); break; case PARKWAIT_UNPARKING: checkMountParkingStatus(); break; case PARKWAIT_UNPARKED: return true; case PARKWAIT_ERROR: appendLogText(i18n("park/unpark wait procedure failed, aborting...")); stop(); return true; } return false; } void Scheduler::executeScript(const QString &filename) { appendLogText(i18n("Executing script %1...", filename)); connect(&scriptProcess, &QProcess::readyReadStandardOutput, this, &Scheduler::readProcessOutput); connect(&scriptProcess, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus) { checkProcessExit(exitCode); }); scriptProcess.start(filename); } void Scheduler::readProcessOutput() { appendLogText(scriptProcess.readAllStandardOutput().simplified()); } void Scheduler::checkProcessExit(int exitCode) { scriptProcess.disconnect(); if (exitCode == 0) { if (startupState == STARTUP_SCRIPT) startupState = STARTUP_UNPARK_DOME; else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) shutdownState = SHUTDOWN_COMPLETE; return; } if (startupState == STARTUP_SCRIPT) { appendLogText(i18n("Startup script failed, aborting...")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) { appendLogText(i18n("Shutdown script failed, aborting...")); shutdownState = SHUTDOWN_ERROR; } } bool Scheduler::checkStatus() { if (state == SCHEDULER_PAUSED) { if (currentJob == nullptr) { setPaused(); return false; } switch (currentJob->getState()) { case SchedulerJob::JOB_BUSY: // do nothing break; case SchedulerJob::JOB_COMPLETE: // start finding next job before pausing break; default: // in all other cases pause setPaused(); break; } } // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) { // #2.1 If shutdown is already complete or in error, we need to stop if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR) { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) return false; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); return false; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); return false; } if (shutdownState == SHUTDOWN_COMPLETE) appendLogText(i18n("Shutdown complete.")); else appendLogText(i18n("Shutdown procedure failed, aborting...")); // Stop Scheduler stop(); return true; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; checkShutdownState(); return false; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) return false; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); // #2.5 If there is no current job after evaluation, shutdown if (nullptr == currentJob) { checkShutdownState(); return false; } } // JM 2018-12-07: Check if we need to sleep else if (shouldSchedulerSleep(currentJob) == false) { // #3 Check if startup procedure has failed. if (startupState == STARTUP_ERROR) { // Stop Scheduler stop(); return true; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) return false; // #5 Check if Ekos is started if (checkEkosState() == false) return false; // #6 Check if INDI devices are connected. if (checkINDIState() == false) return false; // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job if (checkParkWaitState() == false) return false; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) return false; // #8 Check it it already completed (should only happen starting a paused job) // Find the next job in this case, otherwise execute the current one if (currentJob->getState() == SchedulerJob::JOB_COMPLETE) findNextJob(); else executeJob(currentJob); } return true; } void Scheduler::checkJobStage() { Q_ASSERT_X(currentJob, __FUNCTION__, "Actual current job is required to check job stage"); if (!currentJob) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" << currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) << "state" << currentJob->getState(); QDateTime const now = KStarsData::Instance()->lt(); /* Refresh the score of the current job */ /* currentJob->setScore(calculateJobScore(currentJob, now)); */ /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (now < currentJob->getStartupTime()) return; // #1 Check if we need to stop at some point if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT && currentJob->getState() == SchedulerJob::JOB_BUSY) { // If the job reached it COMPLETION time, we stop it. if (now.secsTo(currentJob->getCompletionTime()) <= 0) { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } // #2 Check if altitude restriction still holds true if (-90 < currentJob->getMinAltitude()) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ if (p.alt().Degrees() < currentJob->getMinAltitude()) { // Only terminate job due to altitude limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " "marking aborted.", currentJob->getName(), QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()), QString("%L1").arg(currentJob->getMinAltitude(), 0, 'f', minAltitude->decimals()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } } // #3 Check if moon separation is still valid if (currentJob->getMinMoonSeparation() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); double moonSeparation = currentJob->getCurrentMoonSeparation(); if (moonSeparation < currentJob->getMinMoonSeparation()) { // Only terminate job due to moon separation limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 " "degrees), marking aborted.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } } // #4 Check if we're not at dawn if (currentJob->getEnforceTwilight() && now > KStarsDateTime(preDawnDateTime)) { // If either mount or dome are not parked, we shutdown if we approach dawn if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false)) { // Minute is a DOUBLE value, do not use i18np appendLogText(i18n( "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking aborted.", preDawnDateTime.toString(), Options::preDawnTime(), currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); stopGuiding(); findNextJob(); return; } } // #5 Check system status to improve robustness // This handles external events such as disconnections or end-user manipulating INDI panel if (!checkStatus()) return; // #6 Check each stage is processing properly // FIXME: Vanishing property should trigger a call to its event callback switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: getNextAction(); break; case SchedulerJob::STAGE_ALIGNING: // Let's make sure align module does not become unresponsive if (currentOperationTime.elapsed() > ALIGN_INACTIVITY_TIMEOUT) { QVariant const status = alignInterface->property("status"); Ekos::AlignState alignStatus = static_cast(status.toInt()); if (alignStatus == Ekos::ALIGN_IDLE) { if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request..."; startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_CAPTURING: // Let's make sure capture module does not become unresponsive if (currentOperationTime.elapsed() > CAPTURE_INACTIVITY_TIMEOUT) { QVariant const status = captureInterface->property("status"); Ekos::CaptureState captureStatus = static_cast(status.toInt()); if (captureStatus == Ekos::CAPTURE_IDLE) { if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request..."; startCapture(); } else { appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_FOCUSING: // Let's make sure focus module does not become unresponsive if (currentOperationTime.elapsed() > FOCUS_INACTIVITY_TIMEOUT) { QVariant const status = focusInterface->property("status"); Ekos::FocusState focusStatus = static_cast(status.toInt()); if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING) { if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request..."; startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_GUIDING: // Let's make sure guide module does not become unresponsive if (currentOperationTime.elapsed() > GUIDE_INACTIVITY_TIMEOUT) { GuideState guideStatus = getGuidingStatus(); if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED) { if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request..."; startGuiding(); } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_SLEWING: case SchedulerJob::STAGE_RESLEWING: // While slewing or re-slewing, check slew status can still be obtained { QVariant const slewStatus = mountInterface->property("status"); if (slewStatus.isValid()) { // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event // FIXME: in that case, filter TRACKING events only? ISD::Telescope::Status const status = static_cast(slewStatus.toInt()); setMountStatus(status); } else { appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; case SchedulerJob::STAGE_SLEW_COMPLETE: case SchedulerJob::STAGE_RESLEWING_COMPLETE: // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving if (m_DomeReady) { QVariant const isDomeMoving = domeInterface->property("isMoving"); if (!isDomeMoving.isValid()) { appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (!isDomeMoving.value()) getNextAction(); } else getNextAction(); break; #if 0 case SchedulerJob::STAGE_FOCUSING: { QDBusReply focusReply = focusInterface->call(QDBus::AutoDetect, "getStatus"); if (focusReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while focusing, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus stage..."; Ekos::FocusState focusStatus = static_cast(focusReply.value()); // Is focus complete? if (focusStatus == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (focusStatus == Ekos::FOCUS_FAILED || focusStatus == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; #endif /*case SchedulerJob::STAGE_POSTALIGN_FOCUSING: focusInterface->call(QDBus::AutoDetect,"resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); break;*/ #if 0 case SchedulerJob::STAGE_ALIGNING: { QDBusReply alignReply; qCDebug(KSTARS_EKOS_SCHEDULER) << "Alignment stage..."; alignReply = alignInterface->call(QDBus::AutoDetect, "getStatus"); if (alignReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while aligning, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } Ekos::AlignState alignStatus = static_cast(alignReply.value()); // Is solver complete? if (alignStatus == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (alignStatus == Ekos::ALIGN_FAILED || alignStatus == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, aborting job.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } break; #endif #if 0 case SchedulerJob::STAGE_GUIDING: { QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; if (guideReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while guiding, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } Ekos::GuideState guideStatus = static_cast(guideReply.value()); // If calibration stage complete? if (guideStatus == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } // JM 2018-07-30: GUIDE_IDLE is also a failure else if (guideStatus == Ekos::GUIDE_CALIBRATION_ERROR || guideStatus == Ekos::GUIDE_ABORTED) { if (guideStatus == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (guideStatus == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { appendLogText(i18n("Job '%1' is guiding, and is restarting its guiding procedure.", currentJob->getName())); startGuiding(true); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } } } break; #endif #if 0 case SchedulerJob::STAGE_CAPTURING: { QDBusReply captureReply = captureInterface->call(QDBus::AutoDetect, "getSequenceQueueStatus"); if (captureReply.error().type() == QDBusError::UnknownObject) { appendLogText(i18n("Warning: job '%1' lost connection to INDI server while capturing, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else if (captureReply.value().toStdString() == "Aborted" || captureReply.value().toStdString() == "Error") { appendLogText(i18n("Warning: job '%1' failed to capture target (%2).", currentJob->getName(), captureReply.value())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. QDBusReply guideReply = guideInterface->call(QDBus::AutoDetect, "getStatus"); if (guideReply.value() == Ekos::GUIDE_ABORTED || guideReply.value() == Ekos::GUIDE_CALIBRATION_ERROR || guideReply.value() == GUIDE_DITHERING_ERROR) // If guiding failed, let's restart it //if(guideReply.value() == false) { appendLogText(i18n("Job '%1' is capturing, and is restarting its guiding procedure.", currentJob->getName())); //currentJob->setStage(SchedulerJob::STAGE_GUIDING); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (captureReply.value().toStdString() == "Complete") { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); captureInterface->call(QDBus::AutoDetect, "clearSequenceQueue"); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else { captureFailureCount = 0; /* currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); */ } } break; #endif default: break; } } void Scheduler::getNextAction() { qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action..."; switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: if (currentJob->getLightFramesRequired()) { if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) startSlew(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) if (getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running, directly start capturing.")); startCapture(); } else startGuiding(); else startCapture(); } else { if (currentJob->getStepPipeline()) appendLogText( i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", currentJob->getName())); startCapture(); } break; case SchedulerJob::STAGE_SLEW_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_FOCUS_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_ALIGN_COMPLETE: currentJob->setStage(SchedulerJob::STAGE_RESLEWING); break; case SchedulerJob::STAGE_RESLEWING_COMPLETE: // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus // frame is ready for the capture module in-sequence-focus procedure. if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus()) // Post alignment re-focusing startFocusing(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_GUIDING_COMPLETE: startCapture(); break; default: break; } } void Scheduler::stopCurrentJobAction() { if (nullptr != currentJob) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: break; case SchedulerJob::STAGE_SLEWING: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_FOCUSING: focusInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_ALIGNING: alignInterface->call(QDBus::AutoDetect, "abort"); break; //case SchedulerJob::STAGE_CALIBRATING: // guideInterface->call(QDBus::AutoDetect,"stopCalibration"); // break; case SchedulerJob::STAGE_GUIDING: stopGuiding(); break; case SchedulerJob::STAGE_CAPTURING: captureInterface->call(QDBus::AutoDetect, "abort"); //stopGuiding(); break; default: break; } /* Reset interrupted job stage */ currentJob->setStage(SchedulerJob::STAGE_IDLE); } } bool Scheduler::manageConnectionLoss() { if (SCHEDULER_RUNNING != state) return false; // Don't manage loss if Ekos is actually down in the state machine switch (ekosState) { case EKOS_IDLE: case EKOS_STOPPING: return false; default: break; } // Don't manage loss if INDI is actually down in the state machine switch (indiState) { case INDI_IDLE: case INDI_DISCONNECTING: return false; default: break; } // If Ekos is assumed to be up, check its state //QDBusReply const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (m_EkosCommunicationStatus == Ekos::Success) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss."); // If INDI is assumed to be up, check its state if (isINDIConnected()) { // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed."); return false; } } // Stop actions of the current job stopCurrentJobAction(); // Stop guiding, in case we are using it stopGuiding(); // Acknowledge INDI and Ekos disconnections disconnectINDI(); stopEkos(); // Let the Scheduler attempt to connect INDI again return true; } void Scheduler::load() { QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18n("Open Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); /* Run a job idle evaluation after a successful load */ if (loadScheduler(fileURL.toLocalFile())) startJobEvaluation(); } bool Scheduler::loadScheduler(const QString &fileURL) { SchedulerState const old_state = state; state = SCHEDULER_LOADING; QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); state = old_state; return false; } if (jobUnderEdit >= 0) resetJobEdit(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; XMLEle *subEP = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *tag = tagXMLEle(ep); if (!strcmp(tag, "Job")) processJobInfo(ep); else if (!strcmp(tag, "Profile")) { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tag, "ErrorHandlingStrategy")) { setErrorHandlingStrategy(static_cast(cLocale.toInt(findXMLAttValu(ep, "value")))); subEP = findXMLEle(ep, "delay"); if (subEP) { errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } subEP = findXMLEle(ep, "RescheduleErrors"); errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr); } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; startupScript->clear(); unparkDomeCheck->setChecked(false); unparkMountCheck->setChecked(false); uncapCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "StartupScript")) { startupScript->setText(findXMLAttValu(procedure, "value")); startupScriptURL = QUrl::fromUserInput(startupScript->text()); } else if (!strcmp(proc, "UnparkDome")) unparkDomeCheck->setChecked(true); else if (!strcmp(proc, "UnparkMount")) unparkMountCheck->setChecked(true); else if (!strcmp(proc, "UnparkCap")) uncapCheck->setChecked(true); } } else if (!strcmp(tag, "ShutdownProcedure")) { XMLEle *procedure; shutdownScript->clear(); warmCCDCheck->setChecked(false); parkDomeCheck->setChecked(false); parkMountCheck->setChecked(false); capCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "ShutdownScript")) { shutdownScript->setText(findXMLAttValu(procedure, "value")); shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); } else if (!strcmp(proc, "ParkDome")) parkDomeCheck->setChecked(true); else if (!strcmp(proc, "ParkMount")) parkMountCheck->setChecked(true); else if (!strcmp(proc, "ParkCap")) capCheck->setChecked(true); else if (!strcmp(proc, "WarmCCD")) warmCCDCheck->setChecked(true); } } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); state = old_state; return false; } } schedulerURL = QUrl::fromLocalFile(fileURL); mosaicB->setEnabled(true); mDirty = false; delLilXML(xmlParser); state = old_state; return true; } bool Scheduler::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; altConstraintCheck->setChecked(false); moonSeparationCheck->setChecked(false); weatherCheck->setChecked(false); twilightCheck->blockSignals(true); twilightCheck->setChecked(false); twilightCheck->blockSignals(false); minAltitude->setValue(minAltitude->minimum()); minMoonSeparation->setValue(minMoonSeparation->minimum()); // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Name")) nameEdit->setText(pcdataXMLEle(ep)); else if (!strcmp(tagXMLEle(ep), "Priority")) prioritySpin->setValue(atoi(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Coordinates")) { subEP = findXMLEle(ep, "J2000RA"); if (subEP) { dms ra; ra.setH(cLocale.toDouble(pcdataXMLEle(subEP))); raBox->showInHours(ra); } subEP = findXMLEle(ep, "J2000DE"); if (subEP) { dms de; de.setD(cLocale.toDouble(pcdataXMLEle(subEP))); decBox->showInDegrees(de); } } else if (!strcmp(tagXMLEle(ep), "Sequence")) { sequenceEdit->setText(pcdataXMLEle(ep)); sequenceURL = QUrl::fromUserInput(sequenceEdit->text()); } else if (!strcmp(tagXMLEle(ep), "FITS")) { fitsEdit->setText(pcdataXMLEle(ep)); fitsURL.setPath(fitsEdit->text()); } else if (!strcmp(tagXMLEle(ep), "StartupCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("ASAP", pcdataXMLEle(subEP))) asapConditionR->setChecked(true); else if (!strcmp("Culmination", pcdataXMLEle(subEP))) { culminationConditionR->setChecked(true); culminationOffset->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("At", pcdataXMLEle(subEP))) { startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Constraints")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) { altConstraintCheck->setChecked(true); minAltitude->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) weatherCheck->setChecked(true); else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) twilightCheck->setChecked(true); } } else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("Sequence", pcdataXMLEle(subEP))) sequenceCompletionR->setChecked(true); else if (!strcmp("Repeat", pcdataXMLEle(subEP))) { repeatCompletionR->setChecked(true); repeatsSpin->setValue(cLocale.toInt(findXMLAttValu(subEP, "value"))); } else if (!strcmp("Loop", pcdataXMLEle(subEP))) loopCompletionR->setChecked(true); else if (!strcmp("At", pcdataXMLEle(subEP))) { timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Steps")) { XMLEle *module; trackStepCheck->setChecked(false); focusStepCheck->setChecked(false); alignStepCheck->setChecked(false); guideStepCheck->setChecked(false); for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(module); if (!strcmp(proc, "Track")) trackStepCheck->setChecked(true); else if (!strcmp(proc, "Focus")) focusStepCheck->setChecked(true); else if (!strcmp(proc, "Align")) alignStepCheck->setChecked(true); else if (!strcmp(proc, "Guide")) guideStepCheck->setChecked(true); } } } addToQueueB->setEnabled(true); saveJob(); return true; } void Scheduler::saveAs() { schedulerURL.clear(); save(); } void Scheduler::save() { QUrl backupCurrent = schedulerURL; if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) schedulerURL.clear(); // If no changes made, return. if (mDirty == false && !schedulerURL.isEmpty()) return; if (schedulerURL.isEmpty()) { schedulerURL = QFileDialog::getSaveFileUrl(this, i18n("Save Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); // if user presses cancel if (schedulerURL.isEmpty()) { schedulerURL = backupCurrent; return; } dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); if (schedulerURL.toLocalFile().contains('.') == 0) schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); } if (schedulerURL.isValid()) { if ((saveScheduler(schedulerURL)) == false) { KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save")); return; } mDirty = false; } else { QString message = i18n("Invalid URL: %1", schedulerURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Scheduler::saveScheduler(const QUrl &fileURL) { QFile file; file.setFileName(fileURL.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; outstream << "" << schedulerProfileCombo->currentText() << "" << endl; foreach (SchedulerJob *job, jobs) { outstream << "" << endl; outstream << "" << job->getName() << "" << endl; outstream << "" << job->getPriority() << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "" << endl; outstream << "" << endl; if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false) outstream << "" << job->getFITSFile().toLocalFile() << "" << endl; outstream << "" << job->getSequenceFile().toLocalFile() << "" << endl; outstream << "" << endl; if (job->getFileStartupCondition() == SchedulerJob::START_ASAP) outstream << "ASAP" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (-90 < job->getMinAltitude()) outstream << "MinimumAltitude" << endl; if (job->getMinMoonSeparation() > 0) outstream << "MoonSeparation" << endl; if (job->getEnforceWeather()) outstream << "EnforceWeather" << endl; if (job->getEnforceTwilight()) outstream << "EnforceTwilight" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) outstream << "Sequence" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) outstream << "Repeat" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP) outstream << "Loop" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getStepPipeline() & SchedulerJob::USE_TRACK) outstream << "Track" << endl; if (job->getStepPipeline() & SchedulerJob::USE_FOCUS) outstream << "Focus" << endl; if (job->getStepPipeline() & SchedulerJob::USE_ALIGN) outstream << "Align" << endl; if (job->getStepPipeline() & SchedulerJob::USE_GUIDE) outstream << "Guide" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; if (errorHandlingRescheduleErrorsCB->isChecked()) outstream << "" << endl; outstream << "" << errorHandlingDelaySB->value() << "" << endl; outstream << "" << endl; outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; if (unparkDomeCheck->isChecked()) outstream << "UnparkDome" << endl; if (unparkMountCheck->isChecked()) outstream << "UnparkMount" << endl; if (uncapCheck->isChecked()) outstream << "UnparkCap" << endl; outstream << "" << endl; outstream << "" << endl; if (warmCCDCheck->isChecked()) outstream << "WarmCCD" << endl; if (capCheck->isChecked()) outstream << "ParkCap" << endl; if (parkMountCheck->isChecked()) outstream << "ParkMount" << endl; if (parkDomeCheck->isChecked()) outstream << "ParkDome" << endl; if (shutdownScript->text().isEmpty() == false) outstream << "ShutdownScript" << endl; outstream << "" << endl; outstream << "" << endl; appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile())); file.close(); return true; } void Scheduler::startSlew() { Q_ASSERT(currentJob != nullptr); // If the mount was parked by a pause or the end-user, unpark if (isMountParked()) { parkWaitState = PARKWAIT_UNPARK; return; } if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); SkyPoint target = currentJob->getTargetCoords(); QList telescopeSlew; telescopeSlew.append(target.ra().Hours()); telescopeSlew.append(target.dec().Degrees()); QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); if (slewModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(slewModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else { currentJob->setStage(SchedulerJob::STAGE_SLEWING); appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName())); } } void Scheduler::startFocusing() { // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed // when first focus request is made in capture module if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); // Reset Focus frame so that next frame take a full-resolution capture first. focusInterface->call(QDBus::AutoDetect, "resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); return; } // Check if autofocus is supported QDBusReply focusModeReply; focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus"); if (focusModeReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(focusModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (focusModeReply.value() == false) { appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName())); currentJob->setStepPipeline( static_cast(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS)); currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); return; } // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); QDBusMessage reply; // We always need to reset frame first if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } // Set autostar if full field option is false if (Options::focusUseFullField() == false) { QList autoStar; autoStar.append(true); if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } // Start auto-focus if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING); appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName())); } else { currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Focusing %1 ...", currentJob->getName())); }*/ currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Job '%1' is focusing.", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::findNextJob() { if (state == SCHEDULER_PAUSED) { // everything finished, we can pause setPaused(); return; } Q_ASSERT_X(currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED || currentJob->getState() == SchedulerJob::JOB_COMPLETE, __FUNCTION__, "Finding next job requires current to be in error, aborted or complete"); jobTimer.stop(); // Reset failed count alignFailureCount = guideFailureCount = focusFailureCount = captureFailureCount = 0; /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); if (currentJob->getState() == SchedulerJob::JOB_ERROR) appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); else appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); // restart aborted jobs immediately, if error handling strategy is set to "restart immediately" if (errorHandlingRestartImmediatelyButton->isChecked() && (currentJob->getState() == SchedulerJob::JOB_ABORTED || (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked()))) { // reset the state so that it will be restarted currentJob->setState(SchedulerJob::JOB_SCHEDULED); appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(( errorHandlingDelaySB->value() * 1000)); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); return; } // otherwise start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } // Job is complete, so check completion criteria to optimize processing // In any case, we're done whether the job completed successfully or not. else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) { /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */ if (Options::rememberJobProgress()) { foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); } captureBatch = 0; // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { /* If the job is about to repeat, decrease its repeat count and reset its start time */ if (0 < currentJob->getRepeatsRemaining()) { currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); currentJob->setStartupTime(QDateTime()); } /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); /* Re-evaluate all jobs, without selecting a new job */ jobEvaluationOnly = true; evaluateJobs(); /* If current job is actually complete because of previous duplicates, prepare for next job */ if (currentJob == nullptr || currentJob->getRepeatsRemaining() == 0) { stopCurrentJobAction(); stopGuiding(); if (currentJob != nullptr) { appendLogText(i18np("Job '%1' is complete after #%2 batch.", "Job '%1' is complete after #%2 batches.", currentJob->getName(), currentJob->getRepeatsRequired())); setCurrentJob(nullptr); } schedulerTimer.start(); } /* If job requires more work, continue current observation */ else { /* FIXME: raise priority to allow other jobs to schedule in-between */ executeJob(currentJob); /* If we are guiding, continue capturing */ if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } /* If we are not guiding, but using alignment, realign */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { currentJob->setStage(SchedulerJob::STAGE_ALIGNING); startAstrometry(); } /* Else if we are neither guiding nor using alignment, slew back to target */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) { currentJob->setStage(SchedulerJob::STAGE_SLEWING); startSlew(); } /* Else just start capturing */ else { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.", "Job '%1' is repeating, #%2 batches remaining.", currentJob->getName(), currentJob->getRepeatsRemaining())); /* currentJob remains the same */ jobTimer.start(); } } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName())); /* currentJob remains the same */ jobTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); stopGuiding(); captureBatch = 0; appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.", "Job '%1' stopping, reached completion time with #%2 batches done.", currentJob->getName(), captureBatch + 1)); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.", "Job '%1' completed #%2 batches before completion time, restarted.", currentJob->getName(), captureBatch)); /* currentJob remains the same */ jobTimer.start(); } } else { /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */ qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << currentJob->getName() << "' timer elapsed, but no action to be taken."; // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } } void Scheduler::startAstrometry() { QDBusMessage reply; setSolverAction(Align::GOTO_SLEW); // Always turn update coords on //QVariant arg(true); //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg); // If FITS file is specified, then we use load and slew if (currentJob->getFITSFile().isEmpty() == false) { QList solveArgs; solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile)); if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } loadAndSlewProgress = true; appendLogText(i18n("Job '%1' is plate solving %2.", currentJob->getName(), currentJob->getFITSFile().fileName())); } else { if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName())); } /* FIXME: not supposed to modify the job */ currentJob->setStage(SchedulerJob::STAGE_ALIGNING); currentOperationTime.restart(); } void Scheduler::startGuiding(bool resetCalibration) { // avoid starting the guider twice if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running for %1 ...", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_GUIDING); currentOperationTime.restart(); return; } // Connect Guider guideInterface->call(QDBus::AutoDetect, "connectGuider"); // Set Auto Star to true QVariant arg(true); guideInterface->call(QDBus::AutoDetect, "setCalibrationAutoStar", arg); // Only reset calibration on trouble // and if we are allowed to reset calibration (true by default) if (resetCalibration && Options::resetGuideCalibration()) guideInterface->call(QDBus::AutoDetect, "clearCalibration"); guideInterface->call(QDBus::AutoDetect, "guide"); currentJob->setStage(SchedulerJob::STAGE_GUIDING); appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::startCapture(bool restart) { captureInterface->setProperty("targetName", currentJob->getName().replace(' ', "")); QString url = currentJob->getSequenceFile().toLocalFile(); if (restart == false) { QList dbusargs; dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); } switch (currentJob->getCompletionCondition()) { case SchedulerJob::FINISH_LOOP: case SchedulerJob::FINISH_AT: // In these cases, we leave the captured frames map empty // to ensure, that the capture sequence is executed in any case. break; default: // Scheduler always sets captured frame map when starting a sequence - count may be different, robustness, dynamic priority #if 0 // JM 2018-09-24: If job is looping, no need to set captured frame maps. if (currentJob->getCompletionCondition() != SchedulerJob::FINISH_SEQUENCE) break; #endif // hand over the map of captured frames so that the capture // process knows about existing frames SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap(); for (auto &e : fMap.keys()) { QList dbusargs; QDBusMessage reply; dbusargs.append(e); dbusargs.append(fMap.value(e)); if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() == QDBusMessage::ErrorMessage) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg(reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; } // Never ignore sequence history in the Capture module, it is unrelated to storage #if 0 // If sequence is a loop, ignore sequence history // FIXME: set, but never used. if (currentJob->getCompletionCondition() != SchedulerJob::FINISH_SEQUENCE) captureInterface->call(QDBus::AutoDetect, "ignoreSequenceHistory"); #endif // Start capture process captureInterface->call(QDBus::AutoDetect, "start"); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); KNotification::event(QLatin1String("EkosScheduledImagingStart"), i18n("Ekos job (%1) - Capture started", currentJob->getName())); if (captureBatch > 0) appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1)); else appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::stopGuiding() { if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)) { switch (currentJob->getStage()) { case SchedulerJob::STAGE_GUIDING_COMPLETE: case SchedulerJob::STAGE_CAPTURING: qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName()); guideInterface->call(QDBus::AutoDetect, "abort"); guideFailureCount = 0; break; default: break; } } } void Scheduler::setSolverAction(Align::GotoMode mode) { QVariant gotoMode(static_cast(mode)); alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode); } void Scheduler::disconnectINDI() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI..."; indiState = INDI_DISCONNECTING; ekosInterface->call(QDBus::AutoDetect, "disconnectDevices"); } void Scheduler::stopEkos() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos..."; ekosState = EKOS_STOPPING; ekosConnectFailureCount = 0; ekosInterface->call(QDBus::AutoDetect, "stop"); m_MountReady = m_CapReady = m_CaptureReady = m_DomeReady = false; } void Scheduler::setDirty() { mDirty = true; if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) return; if (0 <= jobUnderEdit && state != SCHEDULER_RUNNING && 0 <= queueTable->currentRow()) { // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation for (int row = jobUnderEdit; row < jobs.size(); row++) jobs.at(row)->reset(); saveJob(); } // For object selection, all fields must be filled bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); // For FITS selection, only the name and fits URL should be filled. bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); // Sequence selection is required bool const seqSelectionOK = !sequenceEdit->text().isEmpty(); // Finally, adding is allowed upon object/FITS and sequence selection bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK; addToQueueB->setEnabled(addingOK); mosaicB->setEnabled(addingOK); } void Scheduler::updateCompletedJobsCount(bool forced) { /* Use a temporary map in order to limit the number of file searches */ SchedulerJob::CapturedFramesMap newFramesCount; /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */ /* Check if one job is idle or requires evaluation - if so, force refresh */ forced |= std::any_of(jobs.begin(), jobs.end(), [](SchedulerJob * oneJob) -> bool { SchedulerJob::JOBStatus const state = oneJob->getState(); return state == SchedulerJob::JOB_IDLE || state == SchedulerJob::JOB_EVALUATION;}); /* If update is forced, clear the frame map */ if (forced) capturedFramesCount.clear(); /* Enumerate SchedulerJobs to count captures that are already stored */ for (SchedulerJob *oneJob : jobs) { QList seqjobs; bool hasAutoFocus = false; //oneJob->setLightFramesRequired(false); /* Look into the sequence requirements, bypass if invalid */ if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus) == false) { appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), oneJob->getSequenceFile().toLocalFile())); oneJob->setState(SchedulerJob::JOB_INVALID); continue; } /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */ for (SequenceJob *oneSeqJob : seqjobs) { /* Only consider captures stored on client (Ekos) side */ /* FIXME: ask the remote for the file count */ if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) continue; /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */ QString const signature = oneSeqJob->getSignature(); /* If signature was processed during this run, keep it */ if (newFramesCount.constEnd() != newFramesCount.constFind(signature)) continue; /* If signature was processed during an earlier run, use the earlier count */ QMap::const_iterator const earlierRunIterator = capturedFramesCount.constFind(signature); if (capturedFramesCount.constEnd() != earlierRunIterator) { newFramesCount[signature] = earlierRunIterator.value(); continue; } /* Else recount captures already stored */ newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); } // determine whether we need to continue capturing, depending on captured frames bool lightFramesRequired = false; switch (oneJob->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: case SchedulerJob::FINISH_REPEAT: for (SequenceJob *oneSeqJob : seqjobs) { QString const signature = oneSeqJob->getSignature(); /* If frame is LIGHT, how hany do we have left? */ if (oneSeqJob->getFrameType() == FRAME_LIGHT && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature]) lightFramesRequired = true; } break; default: // in all other cases it does not depend on the number of captured frames lightFramesRequired = true; } oneJob->setLightFramesRequired(lightFramesRequired); } capturedFramesCount = newFramesCount; //if (forced) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:"; QMap::const_iterator it = capturedFramesCount.constBegin(); for (; it != capturedFramesCount.constEnd(); it++) qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value(); } } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { /* updateCompletedJobsCount(); */ // Load the sequence job associated with the argument scheduler job. QList seqJobs; bool hasAutoFocus = false; if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus) == false) { qCWarning(KSTARS_EKOS_SCHEDULER) << QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(schedJob->getSequenceFile().toLocalFile()); return false; } // FIXME: setting in-sequence focus should be done in XML processing. schedJob->setInSequenceFocus(hasAutoFocus); if (hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)) appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", schedJob->getName())); /* This is the map of captured frames for this scheduler job, keyed per storage signature. * It will be forwarded to the Capture module in order to capture only what frames are required. * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. */ SchedulerJob::CapturedFramesMap capture_map; bool const rememberJobProgress = Options::rememberJobProgress(); int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; // Determine number of captures in the scheduler job int capturesPerRepeat = 0; foreach (SequenceJob *seqJob, seqJobs) capturesPerRepeat += seqJob->getCount(); // Loop through sequence jobs to calculate the number of required frames and estimate duration. foreach (SequenceJob *seqJob, seqJobs) { // FIXME: find a way to actually display the filter name. QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), seqJob->getFilterName()); if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); schedJob->setEstimatedTime(-2); qDeleteAll(seqJobs); return true; } // Note that looping jobs will have zero repeats required. int const captures_required = seqJob->getCount() * schedJob->getRepeatsRequired(); int captures_completed = 0; if (rememberJobProgress) { /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count. * * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage. * * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location. * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job. * * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are * missing, first because that's not how the sequence job is designed (completed count, not required count), and second * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence * jobs three times. * * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them. * * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages. * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0. * When that scheduler job executes, only B and L captures will be processed. * * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage. * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0. * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed. * * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets. * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage. * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. */ // Retrieve cached count of completed captures for the output folder of this seqJob QString const signature = seqJob->getSignature(); QString const signature_path = QFileInfo(signature).path(); captures_completed = capturedFramesCount[signature]; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(captures_completed).arg(signature_path); // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one foreach (SequenceJob *prevSeqJob, seqJobs) { // Enumerate seqJobs up to the current one if (seqJob == prevSeqJob) break; // If the previous sequence signature matches the current, reduce completion count to take duplicates into account if (!signature.compare(prevSeqJob->getLocalDir() + prevSeqJob->getDirectoryPostfix())) { // Note that looping jobs will have zero repeats required. int const previous_captures_required = prevSeqJob->getCount() * schedJob->getRepeatsRequired(); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has a previous duplicate sequence job requiring %2 captures.").arg(seqName).arg(previous_captures_required); captures_completed -= previous_captures_required; } // Now completed count can be needlessly negative for this job, so clamp to zero if (captures_completed < 0) captures_completed = 0; // And break if no captures remain, this job has to execute if (captures_completed == 0) break; } // Finally we're only interested in the number of captures required for this sequence item if (0 < captures_required && captures_required < captures_completed) captures_completed = captures_required; qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(seqName).arg(captures_completed).arg(captures_required).arg(signature_path); // Update the completion count for this signature in the frame map if we still have captures to take. // That frame map will be transferred to the Capture module, for which the sequence is a single batch of the scheduler job. // For instance, consider a scheduler job repeated 3 times and using a 3xLum sequence, so we want 9xLum in the end. // - If no captures are already processed, the frame map contains Lum=0 // - If 1xLum are already processed, the frame map contains Lum=0 when the batch executes, so that 3xLum may be taken. // - If 3xLum are already processed, the frame map contains Lum=0 when the batch executes, as we still need more than what the sequence provides. // - If 7xLum are already processed, the frame map contains Lum=1 when the batch executes, because we now only need 2xLum to finish the job. // Therefore we need to specify a number of existing captures only for the last batch of the scheduler job. // In the last batch, we only need the remainder of frames to get to the required total. if (captures_completed < captures_required) { if (captures_required - captures_completed < seqJob->getCount()) capture_map[signature] = captures_completed % seqJob->getCount(); else capture_map[signature] = 0; } else capture_map[signature] = captures_required; // From now on, 'captures_completed' is the number of frames completed for the *current* sequence job } // Else rely on the captures done during this session else captures_completed = schedJob->getCompletedCount() / capturesPerRepeat * seqJob->getCount(); // Check if we still need any light frames. Because light frames changes the flow of the observatory startup // Without light frames, there is no need to do focusing, alignment, guiding...etc // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required // OR if the completion condition is set to LOOP so it is never complete due to looping. // Note that looping jobs will have zero repeats required. // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job. bool const areJobCapturesComplete = !(captures_completed < captures_required || 0 == captures_required); if (seqJob->getFrameType() == FRAME_LIGHT) { if(areJobCapturesComplete) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(captures_required); } } else { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName); } totalSequenceCount += captures_required; totalCompletedCount += captures_completed; /* If captures are not complete, we have imaging time left */ if (!areJobCapturesComplete) { /* if looping, consider we always have one capture left */ unsigned int const captures_to_go = 0 < captures_required ? captures_required - captures_completed : 1; totalImagingTime += fabs((seqJob->getExposure() + seqJob->getDelay()) * captures_to_go); /* If we have light frames to process, add focus/dithering delay */ if (seqJob->getFrameType() == FRAME_LIGHT) { // If inSequenceFocus is true if (hasAutoFocus) { // Wild guess that each in sequence auto focus takes an average of 30 seconds. It can take any where from 2 seconds to 2+ minutes. // FIXME: estimating one focus per capture is probably not realistic. qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName); totalImagingTime += captures_to_go * 30; } // If we're dithering after each exposure, that's another 10-20 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName); totalImagingTime += (captures_to_go * 15) / Options::ditherFrames(); } } } } schedJob->setCapturedFramesMap(capture_map); schedJob->setSequenceCount(totalSequenceCount); // only in case we remember the job progress, we change the completion count if (rememberJobProgress) schedJob->setCompletedCount(totalCompletedCount); qDeleteAll(seqJobs); // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! // We can't estimate times that do not finish when sequence is done if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { // We can't know estimated time if it is looping indefinitely schedJob->setEstimatedTime(-2); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") .arg(schedJob->getName()); } // If we know startup and finish times, we can estimate time right away else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { // FIXME: SchedulerJob is probably doing this already qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // If we know finish time only, we can roughly estimate the time considering the job starts now else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null else if (totalImagingTime <= 0) { schedJob->setEstimatedTime(0); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); } // Else consolidate with step durations else { if (schedJob->getLightFramesRequired()) { /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ // Are we doing tracking? It takes about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) totalImagingTime += 30; // Are we doing initial focusing? That can take about 2 minutes if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) totalImagingTime += 120; // Are we doing astrometry? That can take about 60 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { totalImagingTime += 60; } // Are we doing guiding? if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Looping, finding guide star, settling takes 15 sec totalImagingTime += 15; // Add guiding settle time from dither setting (used by phd2::guide()) totalImagingTime += Options::ditherSettle(); // Add guiding settle time from ekos sccheduler setting totalImagingTime += Options::guidingSettle(); // If calibration always cleared // then calibration process can take about 2 mins if(Options::resetGuideCalibration()) totalImagingTime += 120; } } dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); schedJob->setEstimatedTime(totalImagingTime); qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); } return true; } void Scheduler::parkMount() { QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount already parked.")); break; case ISD::PARK_UNPARKING: //case Mount::UNPARKING_BUSY: /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::UNPARKING_OK: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_UNPARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "park"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through case ISD::PARK_PARKING: //case Mount::PARKING_BUSY: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARKING_MOUNT; parkWaitState = PARKWAIT_PARKING; appendLogText(i18n("Parking mount in progress...")); break; // All cases covered above so no need for default //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value()); } } void Scheduler::unParkMount() { if (mountInterface.isNull()) return; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARK_CAP; parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount already unparked.")); break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::PARKING_OK: // case Mount::PARKING_ERROR: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_PARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "unpark"); if (mountReply.error().type() != QDBusError::NoError) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARKING_MOUNT; parkWaitState = PARKWAIT_UNPARKING; qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value()); } } void Scheduler::checkMountParkingStatus() { if (mountInterface.isNull()) return; static int parkingFailureCount = 0; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::PARKING_OK: case ISD::PARK_PARKED: // If we are starting up, we will unpark the mount in checkParkWaitState soon // If we are shutting down and mount is parked, proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount parked.")); parkingFailureCount = 0; break; //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: // If we are starting up and mount is unparked, proceed to next step // If we are shutting down, we will park the mount in checkParkWaitState soon if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount unparked.")); parkingFailureCount = 0; break; // FIXME: Create an option for the parking/unparking timeout. //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); unParkMount(); } else { appendLogText(i18n("Warning: mount unpark operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); parkMount(); } else { appendLogText(i18n("Warning: mount park operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress..."; break; //case Mount::PARKING_ERROR: case ISD::PARK_ERROR: if (startupState == STARTUP_UNPARKING_MOUNT) { appendLogText(i18n("Mount unparking error.")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_PARKING_MOUNT) { appendLogText(i18n("Mount parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (parkWaitState == PARKWAIT_PARKING) { appendLogText(i18n("Mount parking error.")); parkWaitState = PARKWAIT_ERROR; } else if (parkWaitState == PARKWAIT_UNPARKING) { appendLogText(i18n("Mount unparking error.")); parkWaitState = PARKWAIT_ERROR; } parkingFailureCount = 0; break; //case Mount::PARKING_IDLE: // FIXME Does this work as intended? check! case ISD::PARK_UNKNOWN: // Last parking action did not result in an action, so proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Last unparking action did not result in an action, so proceed to next step if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; else if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; parkingFailureCount = 0; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value()); } } bool Scheduler::isMountParked() { if (mountInterface.isNull()) return false; // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear //QDBusReply const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark"); QVariant canPark = mountInterface->property("canPark"); if (canPark.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } else if (canPark.toBool() == true) { // If it is able to park, obtain its current status //QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(mountInterface->lastError().type()); manageConnectionLoss(); return false; } // Deduce state of mount - see getParkingStatus in mount.cpp switch (static_cast(parkingStatus.toInt())) { // case Mount::PARKING_OK: // INDI switch ok, and parked // case Mount::PARKING_IDLE: // INDI switch idle, and parked case ISD::PARK_PARKED: return true; // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked // case Mount::PARKING_ERROR: // INDI switch error // case Mount::PARKING_BUSY: // INDI switch busy // case Mount::UNPARKING_BUSY: // INDI switch busy default: return false; } } // If the mount is not able to park, consider it not parked return false; } void Scheduler::parkDome() { if (domeInterface.isNull()) return; //QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); //Dome::ParkingStatus status = static_cast(domeReply.value()); QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_DOME; domeInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already parked.")); shutdownState = SHUTDOWN_SCRIPT; } } void Scheduler::unParkDome() { if (domeInterface.isNull()) return; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } if (static_cast(parkingStatus.toInt()) != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_DOME; domeInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already unparked.")); startupState = STARTUP_UNPARK_MOUNT; } } void Scheduler::checkDomeParkingStatus() { if (domeInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parked.")); shutdownState = SHUTDOWN_SCRIPT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_DOME) { startupState = STARTUP_UNPARK_MOUNT; appendLogText(i18n("Dome unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (120 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkDome(); else unParkDome(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_DOME) { appendLogText(i18n("Dome unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } bool Scheduler::isDomeParked() { if (domeInterface.isNull()) return false; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); return status == ISD::PARK_PARKED; } void Scheduler::parkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_CAP; capInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking Cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } } void Scheduler::unParkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_CAP; capInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already unparked.")); startupState = STARTUP_COMPLETE; } } void Scheduler::checkCapParkingStatus() { if (capInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_CAP) { startupState = STARTUP_COMPLETE; appendLogText(i18n("Cap unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkCap(); else unParkCap(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_CAP) { appendLogText(i18n("Cap unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } void Scheduler::startJobEvaluation() { // Reset current job setCurrentJob(nullptr); // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept for (SchedulerJob * job : jobs) { job->reset(); job->setCompletedCount(0); } // Unconditionally update the capture storage updateCompletedJobsCount(true); // And evaluate all pending jobs per the conditions set in each jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::sortJobsPerAltitude() { // We require a first job to sort, so bail out if list is empty if (jobs.isEmpty()) return; // Don't reset current job // setCurrentJob(nullptr); // Don't reset scheduler jobs startup times before sorting - we need the first job startup time // Sort by startup time, using the first job time as reference for altitude calculations using namespace std::placeholders; QList sortedJobs = jobs; std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime())); // If order changed, reset and re-evaluate if (reorderJobs(sortedJobs)) { for (SchedulerJob * job : jobs) job->reset(); jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); int dayOffset = 0; QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); if (KStarsData::Instance()->lt().time() >= dawn) dayOffset = 1; preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); } bool Scheduler::isWeatherOK(SchedulerJob *job) { if (weatherStatus == ISD::Weather::WEATHER_OK || weatherCheck->isChecked() == false) return true; else if (weatherStatus == ISD::Weather::WEATHER_IDLE) { if (indiState == INDI_READY) appendLogText(i18n("Weather information is pending...")); return true; } // Temporary BUSY is ALSO accepted for now // TODO Figure out how to exactly handle this if (weatherStatus == ISD::Weather::WEATHER_WARNING) return true; if (weatherStatus == ISD::Weather::WEATHER_ALERT) { job->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName())); } /*else if (weatherStatus == IPS_BUSY) { appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); schedulerTimer.stop(); connect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); }*/ return false; } void Scheduler::resumeCheckStatus() { disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); schedulerTimer.start(); } Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() { // The UI holds the state if (errorHandlingRestartAfterAllButton->isChecked()) return ERROR_RESTART_AFTER_TERMINATION; else if (errorHandlingRestartImmediatelyButton->isChecked()) return ERROR_RESTART_IMMEDIATELY; else return ERROR_DONT_RESTART; } void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy) { - errorHandlingWaitLabel->setEnabled(strategy != ERROR_DONT_RESTART); errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART); switch (strategy) { case ERROR_RESTART_AFTER_TERMINATION: errorHandlingRestartAfterAllButton->setChecked(true); break; case ERROR_RESTART_IMMEDIATELY: errorHandlingRestartImmediatelyButton->setChecked(true); break; default: errorHandlingDontRestartButton->setChecked(true); break; } } void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } Mosaic mosaicTool; SkyPoint center; center.setRA0(ra); center.setDec0(dec); mosaicTool.setCenter(center); mosaicTool.calculateFOV(); mosaicTool.adjustSize(); if (mosaicTool.exec() == QDialog::Accepted) { // #1 Edit Sequence File ---> Not needed as of 2016-09-12 since Scheduler can send Target Name to Capture module it will append it to root dir // #1.1 Set prefix to Target-Part# // #1.2 Set directory to output/Target-Part# // #2 Save all sequence files in Jobs dir // #3 Set as current Sequence file // #4 Change Target name to Target-Part# // #5 Update J2000 coords // #6 Repeat and save Ekos Scheduler List in the output directory qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir " << mosaicTool.getJobsDir(); QString outputDir = mosaicTool.getJobsDir(); QString targetName = nameEdit->text().simplified().remove(' '); int batchCount = 1; XMLEle *root = getSequenceJobRoot(); if (root == nullptr) return; // Delete any prior jobs before saving if (!jobs.empty()) { if (KMessageBox::questionYesNo(nullptr, i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No) { qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); } } foreach (OneTile *oneJob, mosaicTool.getJobs()) { QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++); prefix.replace(' ', '-'); nameEdit->setText(prefix); if (createJobSequence(root, prefix, outputDir) == false) return; QString filename = QString("%1/%2.esq").arg(outputDir, prefix); sequenceEdit->setText(filename); sequenceURL = QUrl::fromLocalFile(filename); raBox->showInHours(oneJob->skyCenter.ra0()); decBox->showInDegrees(oneJob->skyCenter.dec0()); saveJob(); } delXMLEle(root); QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir, targetName))); if (saveScheduler(mosaicURL)) { appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile())); } else { appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile())); } } } XMLEle *Scheduler::getSequenceJobRoot() { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return nullptr; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) break; } delLilXML(xmlParser); sFile.close(); return root; } bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir) { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open sequence file %1", sFile.fileName()), i18n("Could Not Open File")); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Job")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp(tagXMLEle(subEP), "Prefix")) { XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix"); if (rawPrefix) { editXMLEle(rawPrefix, prefix.toLatin1().constData()); } } else if (!strcmp(tagXMLEle(subEP), "FITSDirectory")) { editXMLEle(subEP, QString("%1/%2").arg(outputDir, prefix).toLatin1().constData()); } } } } QDir().mkpath(outputDir); QString filename = QString("%1/%2.esq").arg(outputDir, prefix); FILE *outputFile = fopen(filename.toLatin1().constData(), "w"); if (outputFile == nullptr) { QString message = i18n("Unable to write to file %1", filename); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } fprintf(outputFile, ""); prXMLEle(outputFile, root, 0); fclose(outputFile); return true; } void Scheduler::resetAllJobs() { if (state == SCHEDULER_RUNNING) return; // Reset capture count of all jobs before re-evaluating foreach (SchedulerJob *job, jobs) job->setCompletedCount(0); // Evaluate all jobs, this refreshes storage and resets job states startJobEvaluation(); } void Scheduler::checkTwilightWarning(bool enabled) { if (enabled) return; if (KMessageBox::warningContinueCancel( nullptr, i18n("Turning off astronomial twilight check may cause the observatory " "to run during daylight. This can cause irreversible damage to your equipment!"), i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "astronomical_twilight_warning") == KMessageBox::Cancel) { twilightCheck->setChecked(true); } } void Scheduler::checkStartupProcedure() { if (checkStartupState() == false) QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); else { if (startupState == STARTUP_COMPLETE) appendLogText(i18n("Manual startup procedure completed successfully.")); else if (startupState == STARTUP_ERROR) appendLogText(i18n("Manual startup procedure terminated due to errors.")); startupB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runStartupProcedure() { if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE) { /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */ if (indiState == INDI_IDLE) { KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online.")); return; } if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing startup procedure manually...")); startupB->setIcon( QIcon::fromTheme("media-playback-stop")); startupState = STARTUP_IDLE; checkStartupState(); QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); } } else { switch (startupState) { case STARTUP_IDLE: break; case STARTUP_SCRIPT: scriptProcess.terminate(); break; case STARTUP_UNPARK_DOME: break; case STARTUP_UNPARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_MOUNT: break; case STARTUP_UNPARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_CAP: break; case STARTUP_UNPARKING_CAP: break; case STARTUP_COMPLETE: break; case STARTUP_ERROR: break; } startupState = STARTUP_IDLE; appendLogText(i18n("Startup procedure terminated.")); } } void Scheduler::checkShutdownProcedure() { // If shutdown procedure is not finished yet, let's check again in 1 second. if (checkShutdownState() == false) QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); else { if (shutdownState == SHUTDOWN_COMPLETE) { appendLogText(i18n("Manual shutdown procedure completed successfully.")); // Stop Ekos if (Options::stopEkosAfterShutdown()) stopEkos(); } else if (shutdownState == SHUTDOWN_ERROR) appendLogText(i18n("Manual shutdown procedure terminated due to errors.")); shutdownState = SHUTDOWN_IDLE; shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runShutdownProcedure() { if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE) { if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing shutdown procedure manually...")); shutdownB->setIcon( QIcon::fromTheme("media-playback-stop")); shutdownState = SHUTDOWN_IDLE; checkShutdownState(); QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); } } else { switch (shutdownState) { case SHUTDOWN_IDLE: break; case SHUTDOWN_SCRIPT: break; case SHUTDOWN_SCRIPT_RUNNING: scriptProcess.terminate(); break; case SHUTDOWN_PARK_DOME: break; case SHUTDOWN_PARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_MOUNT: break; case SHUTDOWN_PARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_CAP: break; case SHUTDOWN_PARKING_CAP: break; case SHUTDOWN_COMPLETE: break; case SHUTDOWN_ERROR: break; } shutdownState = SHUTDOWN_IDLE; appendLogText(i18n("Shutdown procedure terminated.")); } } void Scheduler::loadProfiles() { QString currentProfile = schedulerProfileCombo->currentText(); QDBusReply profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles"); if (profiles.error().type() == QDBusError::NoError) { schedulerProfileCombo->blockSignals(true); schedulerProfileCombo->clear(); schedulerProfileCombo->addItem(i18n("Default")); schedulerProfileCombo->addItems(profiles); schedulerProfileCombo->setCurrentText(currentProfile); schedulerProfileCombo->blockSignals(false); } } bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open sequence queue file '%1'", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Autofocus")) hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); else if (!strcmp(tagXMLEle(ep), "Job")) jobs.append(processJobInfo(ep, schedJob)); } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); qDeleteAll(jobs); return false; } } return true; } SequenceJob *Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob) { XMLEle *ep = nullptr; XMLEle *subEP = nullptr; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; SequenceJob *job = new SequenceJob(); QString rawPrefix, frameType, filterType; double exposure = 0; bool filterEnabled = false, expEnabled = false, tsEnabled = false; /* Reset light frame presence flag before enumerating */ // JM 2018-09-14: If last sequence job is not LIGHT // then scheduler job light frame is set to whatever last sequence job is // so if it was non-LIGHT, this value is set to false which is wrong. //if (nullptr != schedJob) // schedJob->setLightFramesRequired(false); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) { exposure = atof(pcdataXMLEle(ep)); job->setExposure(exposure); } else if (!strcmp(tagXMLEle(ep), "Filter")) { filterType = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameType = QString(pcdataXMLEle(ep)); /* Record frame type and mark presence of light frames for this sequence */ CCDFrameType const frameEnum = frameTypes[frameType]; job->setFrameType(frameEnum); if (FRAME_LIGHT == frameEnum && nullptr != schedJob) schedJob->setLightFramesRequired(true); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) rawPrefix = QString(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterEnabled = !strcmp("1", pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expEnabled = (!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) tsEnabled = (!strcmp("1", pcdataXMLEle(subEP))); job->setPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); } else if (!strcmp(tagXMLEle(ep), "Count")) { job->setCount(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { job->setDelay(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { job->setLocalDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { job->setRemoteDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { job->setUploadMode(static_cast(atoi(pcdataXMLEle(ep)))); } } // Make full prefix QString imagePrefix = rawPrefix; if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameType; if (filterEnabled && filterType.isEmpty() == false && (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += filterType; } if (expEnabled) { imagePrefix += '_'; imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs"); } job->setFullPrefix(imagePrefix); QString targetName = schedJob->getName().remove(' '); // Directory postfix QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in capture.cpp */ if (targetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameType; else directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameType; if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && filterType.isEmpty() == false) directoryPostfix += QLatin1String("/") + filterType; job->setDirectoryPostfix(directoryPostfix); return job; } int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix) { int seqFileCount = 0; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.baseName()); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file, seqPrefix); QDirIterator it(sig_dir, QDir::Files); /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */ while (it.hasNext()) { QString const fileName = QFileInfo(it.next()).baseName(); if (fileName.startsWith(seqPrefix)) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName); seqFileCount++; } } return seqFileCount; } void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status; m_INDICommunicationStatus = status; } void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status; m_EkosCommunicationStatus = status; } void Scheduler::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")"; if (name == "Focus") { delete focusInterface; focusInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Focus", "org.kde.kstars.Ekos.Focus", QDBusConnection::sessionBus(), this); connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)), Qt::UniqueConnection); } else if (name == "Capture") { delete captureInterface; captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); connect(captureInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)), Qt::UniqueConnection); } else if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); connect(mountInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)), Qt::UniqueConnection); } else if (name == "Align") { delete alignInterface; alignInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Align", "org.kde.kstars.Ekos.Align", QDBusConnection::sessionBus(), this); connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), Qt::UniqueConnection); } else if (name == "Guide") { delete guideInterface; guideInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Guide", "org.kde.kstars.Ekos.Guide", QDBusConnection::sessionBus(), this); connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection); } else if (name == "Dome") { delete domeInterface; domeInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Dome", "org.kde.kstars.Ekos.Dome", QDBusConnection::sessionBus(), this); connect(domeInterface, SIGNAL(ready()), this, SLOT(syncProperties())); } else if (name == "Weather") { delete weatherInterface; weatherInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Weather", "org.kde.kstars.Ekos.Weather", QDBusConnection::sessionBus(), this); connect(weatherInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(weatherInterface, SIGNAL(newStatus(ISD::Weather::Status)), this, SLOT(setWeatherStatus(ISD::Weather::Status))); } else if (name == "DustCap") { delete capInterface; capInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/DustCap", "org.kde.kstars.Ekos.DustCap", QDBusConnection::sessionBus(), this); connect(capInterface, SIGNAL(ready()), this, SLOT(syncProperties()), Qt::UniqueConnection); } } void Scheduler::syncProperties() { QDBusInterface *iface = qobject_cast(sender()); if (iface == mountInterface) { QVariant canMountPark = mountInterface->property("canPark"); unparkMountCheck->setEnabled(canMountPark.toBool()); parkMountCheck->setEnabled(canMountPark.toBool()); m_MountReady = true; } else if (iface == capInterface) { QVariant canCapPark = capInterface->property("canPark"); if (canCapPark.isValid()) { capCheck->setEnabled(canCapPark.toBool()); uncapCheck->setEnabled(canCapPark.toBool()); m_CapReady = true; } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } } else if (iface == weatherInterface) { QVariant updatePeriod = weatherInterface->property("updatePeriod"); if (updatePeriod.isValid()) { weatherCheck->setEnabled(true); QVariant status = weatherInterface->property("status"); setWeatherStatus(static_cast(status.toInt())); // if (updatePeriod.toInt() > 0) // { // weatherTimer.setInterval(updatePeriod.toInt() * 1000); // connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather, Qt::UniqueConnection); // weatherTimer.start(); // // Check weather initially // checkWeather(); // } } else weatherCheck->setEnabled(true); } else if (iface == domeInterface) { QVariant canDomePark = domeInterface->property("canPark"); unparkDomeCheck->setEnabled(canDomePark.toBool()); parkDomeCheck->setEnabled(canDomePark.toBool()); m_DomeReady = true; } else if (iface == captureInterface) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } } void Scheduler::setAlignStatus(Ekos::AlignState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_ALIGNING) { // Is solver complete? if (status == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setGuideStatus(Ekos::GuideState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_GUIDING) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; // If calibration stage complete? if (status == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; // if guiding recovered while we are waiting, abort the restart restartGuidingTimer.stop(); currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } else if (status == Ekos::GUIDE_CALIBRATION_ERROR || status == Ekos::GUIDE_ABORTED) { if (status == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); // if the timer for restarting the guiding is already running, we do nothing and // wait for the action triggered by the timer. This way we avoid that a small guiding problem // abort the scheduler job if (restartGuidingTimer.isActive()) return; if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (status == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(), (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000)); restartGuidingTimer.start(RESTART_GUIDING_DELAY_MS * guideFailureCount); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } GuideState Scheduler::getGuidingStatus() { QVariant guideStatus = guideInterface->property("status"); Ekos::GuideState gStatus = static_cast(guideStatus.toInt()); return gStatus; } void Scheduler::setCaptureStatus(Ekos::CaptureState status) { if (currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_CAPTURING) { if (status == Ekos::CAPTURE_ABORTED) { appendLogText(i18n("Warning: job '%1' failed to capture target.", currentJob->getName())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. Ekos::GuideState gStatus = getGuidingStatus(); if (gStatus == Ekos::GUIDE_ABORTED || gStatus == Ekos::GUIDE_CALIBRATION_ERROR || gStatus == GUIDE_DITHERING_ERROR) { appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(), captureFailureCount, MAX_FAILURE_ATTEMPTS)); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(true); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (status == Ekos::CAPTURE_COMPLETE) { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else if (status == Ekos::CAPTURE_IMAGE_RECEIVED) { // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times. // FIXME: rework this once capture storage is reworked if (Options::rememberJobProgress()) { updateCompletedJobsCount(true); for (SchedulerJob * job : jobs) estimateJobTime(job); } // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks else currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); captureFailureCount = 0; } } } void Scheduler::setFocusStatus(Ekos::FocusState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_FOCUSING) { // Is focus complete? if (status == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setMountStatus(ISD::Telescope::Status status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status; /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (static_cast(KStarsData::Instance()->lt()) < currentJob->getStartupTime()) return; switch (currentJob->getStage()) { case SchedulerJob::STAGE_SLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_RESLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; default: break; } } void Scheduler::setWeatherStatus(ISD::Weather::Status status) { ISD::Weather::Status newStatus = status; QString statusString; switch (newStatus) { case ISD::Weather::WEATHER_OK: statusString = i18n("Weather conditions are OK."); break; case ISD::Weather::WEATHER_WARNING: statusString = i18n("Warning: weather conditions are in the WARNING zone."); break; case ISD::Weather::WEATHER_ALERT: statusString = i18n("Caution: weather conditions are in the DANGER zone!"); break; default: break; } if (newStatus != weatherStatus) { weatherStatus = newStatus; qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; if (weatherStatus == ISD::Weather::WEATHER_OK) weatherLabel->setPixmap( QIcon::fromTheme("security-high") .pixmap(QSize(32, 32))); else if (weatherStatus == ISD::Weather::WEATHER_WARNING) { weatherLabel->setPixmap( QIcon::fromTheme("security-medium") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone")); } else if (weatherStatus == ISD::Weather::WEATHER_ALERT) { weatherLabel->setPixmap( QIcon::fromTheme("security-low") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherAlert"), i18n("Weather conditions are critical. Observatory shutdown is imminent")); } else weatherLabel->setPixmap(QIcon::fromTheme("chronometer") .pixmap(QSize(32, 32))); weatherLabel->show(); weatherLabel->setToolTip(statusString); appendLogText(statusString); emit weatherChanged(weatherStatus); } // Shutdown scheduler if it was started and not already in shutdown if (weatherStatus == ISD::Weather::WEATHER_ALERT && state != Ekos::SCHEDULER_IDLE && state != Ekos::SCHEDULER_SHUTDOWN) { appendLogText(i18n("Starting shutdown procedure due to severe weather.")); if (currentJob) { currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); stopGuiding(); jobTimer.stop(); } checkShutdownState(); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), &Scheduler::Qt::UniqueConnection); } } bool Scheduler::shouldSchedulerSleep(SchedulerJob *currentJob) { if (currentJob->getLightFramesRequired() == false) return false; QDateTime const now = KStarsData::Instance()->lt(); int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() && nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Observatory scheduled for shutdown until next job is ready.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); preemptiveShutdown = true; weatherCheck->setEnabled(false); weatherLabel->hide(); checkShutdownState(); //schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval( (nextObservationTime + 1) * 1000); sleepTimer.start(); return true; } // Otherwise, sleep until job is ready /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again. // This is also only performed if next job is due more than the default lead time (5 minutes). // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. else if (nextObservationTime > Options::leadTime() * 60 && startupState == STARTUP_COMPLETE && parkWaitState == PARKWAIT_IDLE && (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Parking the mount until the job is ready.", currentJob->getName(), currentJob->getStartupTime().toString())); parkWaitState = PARKWAIT_PARK; return false; } else if (nextObservationTime > Options::leadTime() * 60) { appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), now.addSecs(nextObservationTime + 1).toString())); sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); sleepLabel->show(); // Warn the user if the next job is really far away - 60/5 = 12 times the lead time if (nextObservationTime > Options::leadTime() * 60 * 12) { dms delay(static_cast(nextObservationTime * 15.0 / 3600.0)); appendLogText(i18n( "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.", currentJob->getName(), delay.toHMSString())); } /* FIXME: stop tracking now */ schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval(( (nextObservationTime + 1) * 1000)); sleepTimer.start(); return true; } return false; } } diff --git a/kstars/ekos/scheduler/scheduler.h b/kstars/ekos/scheduler/scheduler.h index 38b60fe30..77f9ba883 100644 --- a/kstars/ekos/scheduler/scheduler.h +++ b/kstars/ekos/scheduler/scheduler.h @@ -1,794 +1,796 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_scheduler.h" #include "ekos/align/align.h" #include "indi/indiweather.h" #include #include #include #include #include #include #include class QProgressIndicator; class GeoLocation; class SchedulerJob; class SkyObject; +class KConfigDialog; namespace Ekos { class SequenceJob; /** * @brief The Ekos scheduler is a simple scheduler class to orchestrate automated multi object observation jobs. * @author Jasem Mutlaq * @version 1.2 */ class Scheduler : public QWidget, public Ui::Scheduler { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Scheduler") Q_PROPERTY(Ekos::SchedulerState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString profile READ profile WRITE setProfile) public: typedef enum { EKOS_IDLE, EKOS_STARTING, EKOS_STOPPING, EKOS_READY } EkosState; typedef enum { INDI_IDLE, INDI_CONNECTING, INDI_DISCONNECTING, INDI_PROPERTY_CHECK, INDI_READY } INDIState; typedef enum { STARTUP_IDLE, STARTUP_SCRIPT, STARTUP_UNPARK_DOME, STARTUP_UNPARKING_DOME, STARTUP_UNPARK_MOUNT, STARTUP_UNPARKING_MOUNT, STARTUP_UNPARK_CAP, STARTUP_UNPARKING_CAP, STARTUP_ERROR, STARTUP_COMPLETE } StartupState; typedef enum { SHUTDOWN_IDLE, SHUTDOWN_PARK_CAP, SHUTDOWN_PARKING_CAP, SHUTDOWN_PARK_MOUNT, SHUTDOWN_PARKING_MOUNT, SHUTDOWN_PARK_DOME, SHUTDOWN_PARKING_DOME, SHUTDOWN_SCRIPT, SHUTDOWN_SCRIPT_RUNNING, SHUTDOWN_ERROR, SHUTDOWN_COMPLETE } ShutdownState; typedef enum { PARKWAIT_IDLE, PARKWAIT_PARK, PARKWAIT_PARKING, PARKWAIT_PARKED, PARKWAIT_UNPARK, PARKWAIT_UNPARKING, PARKWAIT_UNPARKED, PARKWAIT_ERROR } ParkWaitStatus; /** @brief options what should happen if an error or abort occurs */ typedef enum { ERROR_DONT_RESTART, ERROR_RESTART_AFTER_TERMINATION, ERROR_RESTART_IMMEDIATELY } ErrorHandlingStrategy; /** @brief Columns, in the same order as UI. */ typedef enum { SCHEDCOL_NAME = 0, SCHEDCOL_STATUS, SCHEDCOL_CAPTURES, SCHEDCOL_ALTITUDE, SCHEDCOL_SCORE, SCHEDCOL_STARTTIME, SCHEDCOL_ENDTIME, SCHEDCOL_DURATION, SCHEDCOL_LEADTIME, SCHEDCOL_COUNT } SchedulerColumns; Scheduler(); ~Scheduler() = default; QString getCurrentJobName(); void appendLogText(const QString &); QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } void clearLog(); + void applyConfig(); void addObject(SkyObject *object); /** * @brief startSlew DBus call for initiating slew */ void startSlew(); /** * @brief startFocusing DBus call for feeding ekos the specified settings and initiating focus operation */ void startFocusing(); /** * @brief startAstrometry initiation of the capture and solve operation. We change the job state * after solver is started */ void startAstrometry(); /** * @brief startGuiding After ekos is fed the calibration options, we start the guiging process * @param resetCalibration By default calibration is not reset until it is explicitly requested */ void startGuiding(bool resetCalibration = false); /** * @brief startCapture The current job file name is solved to an url which is fed to ekos. We then start the capture process * @param restart Set to true if the goal to restart an existing sequence. The only difference is that when a sequence is restarted, sequence file * is not loaded from disk again since that results in erasing all the history of the capture process. */ void startCapture(bool restart = false); /** * @brief getNextAction Checking for the next appropriate action regarding the current state of the scheduler and execute it */ void getNextAction(); /** * @brief disconnectINDI disconnect all INDI devices from server. */ void disconnectINDI(); /** * @brief stopEkos shutdown Ekos completely */ void stopEkos(); /** * @brief stopGuiding After guiding is done we need to stop the process */ void stopGuiding(); /** * @brief setSolverAction set the GOTO mode for the solver * @param mode 0 For Sync, 1 for SlewToTarget, 2 for Nothing */ void setSolverAction(Align::GotoMode mode); /** @defgroup SchedulerDBusInterface Ekos DBus Interface - Scheduler Module * Ekos::Align interface provides primary functions to run and stop the scheduler. */ /*@{*/ /** DBUS interface function. * @brief Start the scheduler main loop and evaluate jobs and execute them accordingly. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * @brief Stop the scheduler. */ Q_SCRIPTABLE Q_NOREPLY void stop(); /** DBUS interface function. * @brief Loads the Ekos Scheduler List (.esl) file. * @param fileURL path to a file * @return true if loading file is successful, false otherwise. */ Q_SCRIPTABLE bool loadScheduler(const QString &fileURL); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void resetAllJobs(); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void sortJobsPerAltitude(); Ekos::SchedulerState status() { return state; } void setProfile(const QString &profile) { schedulerProfileCombo->setCurrentText(profile); } QString profile() { return schedulerProfileCombo->currentText(); } /** * @brief retrieve the error handling strategy from the UI */ ErrorHandlingStrategy getErrorHandlingStrategy(); /** * @brief select the error handling strategy (no restart, restart after all terminated, restart immediately) */ void setErrorHandlingStrategy (ErrorHandlingStrategy strategy); /** @}*/ /** @{ */ private: /** @internal Safeguard flag to avoid registering signals from widgets multiple times. */ bool jobChangesAreWatched { false }; protected: /** @internal Enables signal watch on SchedulerJob form values in order to apply changes to current job. * @param enable is the toggle flag, true to watch for changes, false to ignore them. */ void watchJobChanges(bool enable); /** @internal Marks the currently selected SchedulerJob as modified change. * * This triggers job re-evaluation. * Next time save button is invoked, the complete content is written to disk. */ void setDirty(); /** @} */ protected: /** @internal Associate job table cells on a row to the corresponding SchedulerJob. * @param row is an integer indexing the row to associate cells from, and also the index of the job in the job list.. */ void setJobStatusCells(int row); protected slots: /** * @brief registerNewModule Register an Ekos module as it arrives via DBus * and create the appropriate DBus interface to communicate with it. * @param name of module */ void registerNewModule(const QString &name); /** * @brief syncProperties Sync startup properties from the various device to enable/disable features in the scheduler * like the ability to park/unpark..etc */ void syncProperties(); void setAlignStatus(Ekos::AlignState status); void setGuideStatus(Ekos::GuideState status); void setCaptureStatus(Ekos::CaptureState status); void setFocusStatus(Ekos::FocusState status); void setMountStatus(ISD::Telescope::Status status); void setWeatherStatus(ISD::Weather::Status status); /** * @brief select object from KStars's find dialog. */ void selectObject(); /** * @brief Selects FITS file for solving. */ void selectFITS(); /** * @brief Selects sequence queue. */ void selectSequence(); /** * @brief Selects sequence queue. */ void selectStartupScript(); /** * @brief Selects sequence queue. */ void selectShutdownScript(); /** * @brief addToQueue Construct a SchedulerJob and add it to the queue or save job settings from current form values. * jobUnderEdit determines whether to add or edit */ void saveJob(); /** * @brief addJob Add a new job from form values */ void addJob(); /** * @brief editJob Edit an observation job * @param i index model in queue table */ void loadJob(QModelIndex i); /** * @brief removeJob Remove a job from the currently selected row. If no row is selected, it remove the last job in the queue. */ void removeJob(); /** * @brief setJobAddApply Set first button state to add new job or apply changes. */ void setJobAddApply(bool add_mode); /** * @brief setJobManipulation Enable or disable job manipulation buttons. */ void setJobManipulation(bool can_reorder, bool can_delete); /** * @brief set all GUI fields to the values of the given scheduler job */ void syncGUIToJob(SchedulerJob *job); /** * @brief jobSelectionChanged Update UI state when the job list is clicked once. */ void clickQueueTable(QModelIndex index); /** * @brief Update scheduler parameters to the currently selected scheduler job * @param current table position * @param previous table position */ void queueTableSelectionChanged(QModelIndex current, QModelIndex previous); /** * @brief reorderJobs Change the order of jobs in the UI based on a subset of its jobs. */ bool reorderJobs(QList reordered_sublist); /** * @brief moveJobUp Move the selected job up in the job list. */ void moveJobUp(); /** * @brief moveJobDown Move the selected job down in the list. */ void moveJobDown(); /** * @brief shouldSchedulerSleep Check if the scheduler needs to sleep until the job is ready * @param currentJob Job to check * @return True if we set the scheduler to sleep mode. False, if not required and we need to execute now */ bool shouldSchedulerSleep(SchedulerJob *currentJob); void toggleScheduler(); void pause(); void setPaused(); void save(); void saveAs(); void load(); void resetJobEdit(); /** * @brief checkJobStatus Check the overall state of the scheduler, Ekos, and INDI. When all is OK, it calls evaluateJobs() when no job is current or executeJob() if a job is selected. * @return False if this function needs to be called again later, true if situation is stable and operations may continue. */ bool checkStatus(); /** * @brief checkJobStage Check the progress of the job states and make DBUS call to start the next stage until the job is complete. */ void checkJobStage(); /** * @brief findNextJob Check if the job met the completion criteria, and if it did, then it search for next job candidate. If no jobs are found, it starts the shutdown stage. */ void findNextJob(); /** * @brief stopCurrentJobAction Stop whatever action taking place in the current job (eg. capture, guiding...etc). */ void stopCurrentJobAction(); /** * @brief manageConnectionLoss Mitigate loss of connection with the INDI server. * @return true if connection to Ekos/INDI should be attempted again, false if not mitigation is available or needed. */ bool manageConnectionLoss(); /** * @brief readProcessOutput read running script process output and display it in Ekos */ void readProcessOutput(); /** * @brief checkProcessExit Check script process exist status. This is called when the process exists either normally or abnormally. * @param exitCode exit code from the script process. Depending on the exist code, the status of startup/shutdown procedure is set accordingly. */ void checkProcessExit(int exitCode); /** * @brief resumeCheckStatus If the scheduler primary loop was suspended due to weather or sleep event, resume it again. */ void resumeCheckStatus(); /** * @brief checkWeather Check weather status and act accordingly depending on the current status of the scheduler and running jobs. */ //void checkWeather(); /** * @brief wakeUpScheduler Wake up scheduler from sleep state */ void wakeUpScheduler(); /** * @brief startJobEvaluation Start job evaluation only without starting the scheduler process itself. Display the result to the user. */ void startJobEvaluation(); /** * @brief startMosaicTool Start Mosaic tool and create jobs if necessary. */ void startMosaicTool(); /** * @brief displayTwilightWarning Display twilight warning to user if it is unchecked. */ void checkTwilightWarning(bool enabled); void runStartupProcedure(); void checkStartupProcedure(); void runShutdownProcedure(); void checkShutdownProcedure(); void setINDICommunicationStatus(Ekos::CommunicationStatus status); void setEkosCommunicationStatus(Ekos::CommunicationStatus status); signals: void newLog(const QString &text); void newStatus(Ekos::SchedulerState state); void weatherChanged(ISD::Weather::Status state); void newTarget(const QString &); private: /** * @brief evaluateJobs evaluates the current state of each objects and gives each one a score based on the constraints. * Given that score, the scheduler will decide which is the best job that needs to be executed. */ void evaluateJobs(); /** * @brief executeJob After the best job is selected, we call this in order to start the process that will execute the job. * checkJobStatus slot will be connected in order to figure the exact state of the current job each second * @param value */ void executeJob(SchedulerJob *job); void executeScript(const QString &filename); /** * @brief getDarkSkyScore Get the dark sky score of a date and time. The further from dawn the better. * @param when date and time to check the dark sky score, now if omitted * @return Dark sky score. Daylight get bad score, as well as pre-dawn to dawn. */ int16_t getDarkSkyScore(QDateTime const &when = QDateTime()) const; /** * @brief calculateJobScore Calculate job dark sky score, altitude score, and moon separation scores and returns the sum. * @param job Target * @param when date and time to evaluate constraints, now if omitted. * @return Total score */ int16_t calculateJobScore(SchedulerJob const *job, QDateTime const &when = QDateTime()) const; /** * @brief getWeatherScore Get current weather condition score. * @return If weather condition OK, return score 0, else bad score. */ int16_t getWeatherScore() const; /** * @brief calculateDawnDusk Get dawn and dusk times for today */ void calculateDawnDusk(); /** * @brief checkEkosState Check ekos startup stages and take whatever action necessary to get Ekos up and running * @return True if Ekos is running, false if Ekos start up is in progress. */ bool checkEkosState(); /** * @brief isINDIConnected Determines the status of the INDI connection. * @return True if INDI connection is up and usable, else false. */ bool isINDIConnected(); /** * @brief checkINDIState Check INDI startup stages and take whatever action necessary to get INDI devices connected. * @return True if INDI devices are connected, false if it is under progress. */ bool checkINDIState(); /** * @brief checkStartupState Check startup procedure stages and make sure all stages are complete. * @return True if startup is complete, false otherwise. */ bool checkStartupState(); /** * @brief checkShutdownState Check shutdown procedure stages and make sure all stages are complete. * @return */ bool checkShutdownState(); /** * @brief checkParkWaitState Check park wait state. * @return If parking/unparking in progress, return false. If parking/unparking complete, return true. */ bool checkParkWaitState(); /** * @brief parkMount Park mount */ void parkMount(); /** * @brief unParkMount Unpark mount */ void unParkMount(); /** * @return True if mount is parked */ bool isMountParked(); /** * @brief parkDome Park dome */ void parkDome(); /** * @brief unParkDome Unpark dome */ void unParkDome(); /** * @return True if dome is parked */ bool isDomeParked(); /** * @brief parkCap Close dust cover */ void parkCap(); /** * @brief unCap Open dust cover */ void unParkCap(); /** * @brief checkMountParkingStatus check mount parking status and updating corresponding states accordingly. */ void checkMountParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkDomeParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkCapParkingStatus(); /** * @brief saveScheduler Save scheduler jobs to a file * @param path path of a file * @return true on success, false on failure. */ bool saveScheduler(const QUrl &fileURL); /** * @brief processJobInfo Process the job information from a scheduler file and populate jobs accordingly * @param root XML root element of JOB * @return true on success, false on failure. */ bool processJobInfo(XMLEle *root); /** * @brief updatePreDawn Update predawn time depending on current time and user offset */ void updatePreDawn(); /** * @brief estimateJobTime Estimates the time the job takes to complete based on the sequence file and what modules to utilize during the observation run. * @param job target job * @return Estimated time in seconds. */ bool estimateJobTime(SchedulerJob *schedJob); /** * @brief createJobSequence Creates a job sequence for the mosaic tool given the prefix and output dir. The currently selected sequence file is modified * and a new version given the supplied parameters are saved to the output directory * @param prefix Prefix to set for the job sequence * @param outputDir Output dir to set for the job sequence * @return True if new file is saved, false otherwise */ bool createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir); /** @internal Change the current job, updating associated widgets. * @param job is an existing SchedulerJob to set as current, or nullptr. */ void setCurrentJob(SchedulerJob *job); /** * @brief processFITSSelection When a FITS file is selected, open it and try to guess * the object name, and its J2000 RA/DE to fill the UI with such info automatically. */ void processFITSSelection(); void loadProfiles(); XMLEle *getSequenceJobRoot(); bool isWeatherOK(SchedulerJob *job); /** * @brief updateCompletedJobsCount For each scheduler job, examine sequence job storage and count captures. * @param forced forces recounting captures unconditionally if true, else only IDLE, EVALUATION or new jobs are examined. */ void updateCompletedJobsCount(bool forced = false); SequenceJob *processJobInfo(XMLEle *root, SchedulerJob *schedJob); bool loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus); int getCompletedFiles(const QString &path, const QString &seqPrefix); // retrieve the guiding status GuideState getGuidingStatus(); Ekos::Scheduler *ui { nullptr }; //DBus interfaces QPointer focusInterface { nullptr }; QPointer ekosInterface { nullptr }; QPointer captureInterface { nullptr }; QPointer mountInterface { nullptr }; QPointer alignInterface { nullptr }; QPointer guideInterface { nullptr }; QPointer domeInterface { nullptr }; QPointer weatherInterface { nullptr }; QPointer capInterface { nullptr }; // Scheduler and job state and stages SchedulerState state { SCHEDULER_IDLE }; EkosState ekosState { EKOS_IDLE }; INDIState indiState { INDI_IDLE }; StartupState startupState { STARTUP_IDLE }; ShutdownState shutdownState { SHUTDOWN_IDLE }; ParkWaitStatus parkWaitState { PARKWAIT_IDLE }; Ekos::CommunicationStatus m_EkosCommunicationStatus { Ekos::Idle }; Ekos::CommunicationStatus m_INDICommunicationStatus { Ekos::Idle }; /// List of all jobs as entered by the user or file QList jobs; /// Active job SchedulerJob *currentJob { nullptr }; /// URL to store the scheduler file QUrl schedulerURL; /// URL for Ekos Sequence QUrl sequenceURL; /// FITS URL to solve QUrl fitsURL; /// Startup script URL QUrl startupScriptURL; /// Shutdown script URL QUrl shutdownScriptURL; /// Store all log strings QStringList m_LogText; /// Busy indicator widget QProgressIndicator *pi { nullptr }; /// Are we editing a job right now? Job row index int jobUnderEdit { -1 }; /// Pointer to Geographic location GeoLocation *geo { nullptr }; /// How many repeated job batches did we complete thus far? uint16_t captureBatch { 0 }; /// Startup and Shutdown scripts process QProcess scriptProcess; /// Store day fraction of dawn to calculate dark skies range double Dawn { -1 }; /// Store day fraction of dusk to calculate dark skies range double Dusk { -1 }; /// Pre-dawn is where we stop all jobs, it is a user-configurable value before Dawn. QDateTime preDawnDateTime; /// Dusk date time QDateTime duskDateTime; /// Was job modified and needs saving? bool mDirty { false }; /// Keep watch of weather status ISD::Weather::Status weatherStatus { ISD::Weather::WEATHER_IDLE }; /// Keep track of how many times we didn't receive weather updates uint8_t noWeatherCounter { 0 }; /// Are we shutting down until later? bool preemptiveShutdown { false }; /// Only run job evaluation bool jobEvaluationOnly { false }; /// Keep track of Load & Slew operation bool loadAndSlewProgress { false }; /// Check if initial autofocus is completed and do not run autofocus until there is a change is telescope position/alignment. bool autofocusCompleted { false }; /// Keep track of INDI connection failures uint8_t indiConnectFailureCount { 0 }; /// Keep track of Ekos connection failures uint8_t ekosConnectFailureCount { 0 }; /// Keep track of Ekos focus module failures uint8_t focusFailureCount { 0 }; /// Keep track of Ekos guide module failures uint8_t guideFailureCount { 0 }; /// Keep track of Ekos align module failures uint8_t alignFailureCount { 0 }; /// Keep track of Ekos capture module failures uint8_t captureFailureCount { 0 }; /// Call checkWeather when weatherTimer time expires. It is equal to the UpdatePeriod time in INDI::Weather device. //QTimer weatherTimer; /// Timer to put the scheduler into sleep mode until a job is ready QTimer sleepTimer; /// To call checkStatus QTimer schedulerTimer; /// To call checkJobStage QTimer jobTimer; /// Delay for restarting the guider QTimer restartGuidingTimer; /// Generic time to track timeout of current operation in progress QTime currentOperationTime; QUrl dirPath; QMap capturedFramesCount; bool m_MountReady { false }; bool m_CaptureReady { false }; bool m_DomeReady { false }; bool m_CapReady { false }; // When a module is commanded to perform an action, wait this many milliseconds // before check its state again. If State is still IDLE, then it either didn't received the command // or there is another problem. static const uint32_t ALIGN_INACTIVITY_TIMEOUT = 120000; static const uint32_t FOCUS_INACTIVITY_TIMEOUT = 120000; static const uint32_t CAPTURE_INACTIVITY_TIMEOUT = 120000; static const uint16_t GUIDE_INACTIVITY_TIMEOUT = 60000; }; } diff --git a/kstars/ekos/scheduler/scheduler.ui b/kstars/ekos/scheduler/scheduler.ui index a4ccf57b4..1e1ad95e5 100644 --- a/kstars/ekos/scheduler/scheduler.ui +++ b/kstars/ekos/scheduler/scheduler.ui @@ -1,1868 +1,2167 @@ Scheduler 0 0 - 721 - 541 + 957 + 755 - + - 3 + 1 3 3 3 3 - + 0 0 Object && Sequence Selection - + 3 + + QLayout::SetDefaultConstraint + 3 3 3 3 - - - - - - Default - - - - - - - - 5 - - - - - true - - - true - - - - - - - - 22 - 22 - - - - Load the image sequence queue. - - - - - - - - - - - - Select which steps to execute before starting the capture process. - - - Steps: - - - - - - - color:red - - - * + + + + + Qt::Vertical - - - - - - Target coordinates in J2000 Epoch + + QSizePolicy::Expanding - - J2000: + + + 20 + 40 + - + - - - - Assigned priority to each job with 1 being the highest priority and 20 being the lowest priority - - - - - + + + 1 - - 20 - - - 10 + + 3 - - - - - - color:red - - - * - - - - - - - + + - Slew to the target and track it before proceeding to the next step. + Select optional FITS file to be used for alignment. When using FITS, specify the object or the estimated coordinates which are used to slew the mount. - Track - - - true + FITS File: - - stepsButtonGroup - - - - - Perform autofocusing before proceeding to the next step. + + + + color:red - Focus - - - true + * - - stepsButtonGroup - - - + + + + 1 + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + + + + 0 + 0 + + + + false + + + true + + + + + + + - Perform alignment using astrometry solver before proceeding to the next step. + Ekos Sequence File - Align - - - true + Sequence: - - stepsButtonGroup - - - + + + + + 0 + 0 + + - Perform calibration and autoguiding before proceeding to the next step. + Load the image sequence queue. - Guide - - - true + - - stepsButtonGroup - - - - - - - 5 - - - + + - RA + Priority: - - + + + + 1 + + + QLayout::SetDefaultConstraint + + + 0 + + + + + true + + + + 0 + 0 + + + + true + + + + + + + + + 6 + + + 6 + + + + + + 0 + 0 + + + + Slew to the target and track it before proceeding to the next step. + + + Qt::LeftToRight + + + Track + + + true + + + stepsButtonGroup + + + + + + + + 0 + 0 + + + + Perform alignment using astrometry solver before proceeding to the next step. + + + Align + + + true + + + stepsButtonGroup + + + + + + + + 0 + 0 + + + + Perform calibration and autoguiding before proceeding to the next step. + + + Guide + + + true + + + stepsButtonGroup + + + + + + + + 0 + 0 + + + + Perform autofocusing before proceeding to the next step. + + + Qt::LeftToRight + + + Focus + + + true + + + stepsButtonGroup + + + + + + + + + + 0 + 0 + + + Assigned priority to each job with 1 being the highest priority and 20 being the lowest priority + + + + 1 + + + 20 + + + 10 + - - + + - DEC + Target: - - - - + + + + + 0 + 0 + + + + Default + + - - - - - - Ekos Device Profile - - - Profile: - - - - - - - 5 - - - + + + + + 0 + 0 + + + + Select which steps to execute before starting the capture process. + + + Steps: + + - + - - - 22 - 22 - + + + 0 + 0 + - - - 22 - 22 - + + + + + + + + + 1 + + + QLayout::SetDefaultConstraint + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + - - - - - - Select optional FITS file to be used for alignment. When using FITS, specify the object or the estimated coordinates which are used to slew the mount. - - - FITS File: - - - - - - - Ekos Sequence File - - - Sequence: - - - - - - - Target: - - - - - - - Priority: - - - - - - - 5 - - - - - false + + + + color:red - - true + + * - - - - - 22 - 22 - + + + + Ekos Device Profile - + Profile: + + + + + + + Target coordinates in J2000 Epoch + + + J2000: + + + + + + + 5 + + + 0 + + + + + RA + + + + + + + + 0 + 0 + + + + + + + + + + + DEC + + + + + + + + 0 + 0 + + + + + + + Qt::LeftToRight + + + + + + + + + + + + + + 255 + 0 + 0 + + + + + + + 255 + 0 + 0 + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + 255 + 0 + 0 + + + + + + + 255 + 0 + 0 + + + + + + + + + 160 + 162 + 162 + + + + + + + 168 + 169 + 169 + + + + + + + 160 + 162 + 162 + + + + + + + + * - + Qt::Vertical + + QSizePolicy::Expanding + 20 40 1 + + QLayout::SetDefaultConstraint + 1 false 0 0 32 32 32 32 false 0 0 32 32 32 32 false 0 0 32 32 32 32 false 0 0 32 32 32 32 false 0 0 32 32 false 0 0 32 32 false 0 0 32 32 32 32 Create mosaic job. You must add a job before creating a mosaic job. Qt::Horizontal 40 20 true 0 0 32 32 32 32 false 0 0 32 32 false 0 0 32 32 32 32 QAbstractItemView::SingleSelection QAbstractItemView::SelectRows 0 true Name Status 50 false Captures Altitude Score Start Time End Time Est. Duration Lead time 1 false 0 0 32 32 32 32 Start Scheduler QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } false 0 0 32 32 32 32 Pause Scheduler QPushButton:checked { background-color: maroon; border: 1px outset; font-weight:bold; } true 32 32 Qt::Horizontal 40 20 Idle - - + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + 1 - - + + + + + 0 + 0 + + - Job Startup Conditions + Job Completion Conditions - + - 1 + 3 + + + QLayout::SetDefaultConstraint 3 3 3 3 - + - 1 + 3 - - - - Start the observation job as soon as all the constraints, if any, are met. The best candidate target shall be imaged first. + + + + + 0 + 0 + - ASAP + &Repeat for - - true + + completionButtonGroup + + + + + + + + 0 + 0 + + + + dd/MM/yy hh:mm + + + true + + + Qt::LocalTime + + + + + + + + 0 + 0 + + + + The observation job is completed when the sequence is complete. + + + Se&quence completion + + + true - startupButtonGroup + completionButtonGroup + + + + + + + + 0 + 0 + + + + Terminate the job on the given date and time. + + + Repeat &until + + + completionButtonGroup + + + + + + + + 0 + 0 + + + + Restart job until it is executed this many times. + + + runs + + + 1 + + + 1000 + + + 1 + + + + + + + + 0 + 0 + + + + Restart the sequence job indefinitely. + + + Repeat &until terminated + + + completionButtonGroup + + + + + + + + 0 + 0 + + + + Job Startup Conditions + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + - + - 1 + 3 - + + + + + 0 + 0 + + + + dd/MM hh:mm + + + true + + + + + + + 0 + 0 + + Start the observation job when the object reaches culmination adjusted for the offset value in minutes. By default, the observation job runs 60 minutes prior to culmination. - Cu&lmination Offset + Cul&mination Offset startupButtonGroup - + + + + + 0 + 0 + + + + start the job on the specified date and time + + + O&n + + + startupButtonGroup + + + + + + + 0 + 0 + + <html><head/><body><p>Offset in minutes to start imaging before or after culmination time.</p></body></html> + + min + -240 240 10 -60 - - - - Qt::Horizontal - - - - 40 - 20 - + + + + + 0 + 0 + - - - - - - - - 1 - - - - start the job on the specified date and time + Start the observation job as soon as all the constraints, if any, are met. The best candidate target shall be imaged first. - O&n + ASAP + + + true startupButtonGroup - - - - dd/MM/yy hh:mm - - - true + + + + - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 0 + 0 + + Job Constraints - 1 + 3 3 3 3 3 - + - 1 + 3 - - - - ° - - - - - - - ° - - - + + + 0 + 0 + + The moon separation must remain equal to or higher than the given value. Moon > constraintButtonGroup - - - - 180.000000000000000 + + + + + 0 + 0 + - - 10.000000000000000 + + The object's altitude must remain equal or higher than the given value. + + + Alt > + + true + + + constraintButtonGroup + + + + 0 + 0 + + + + ° + -15.000000000000000 89.900000000000006 - - - - The object's altitude must remain equal or higher than the given value. + + + + + 0 + 0 + - - Alt > + + <html><head/><body><p>Weather conditions must remain safe. When weather conditions become dangerous, shutdown procedure is initiated.</p></body></html> - - true + + Weather constraintButtonGroup - - - - - - 1 - - - - - Jobs are only executed during astronomical twilight darkness period. + + + + + 0 + 0 + - - Twilight + + ° - - true + + 180.000000000000000 + + + 10.000000000000000 - - constraintButtonGroup - - - + + + + + 0 + 0 + + - <html><head/><body><p>Weather conditions must remain safe. When weather conditions become dangerous, shutdown procedure is initiated.</p></body></html> + <html><body><p>The twilight restriction constrains jobs to execute in astronomical darkness. Use the dusk and dawn offsets in the Ekos Scheduler options to adjust the interval.</p></body></html> - Weather + Twilight + + + true constraintButtonGroup - + - - - 32 - 32 - - - - - 32 - 32 - - + + Qt::AlignCenter + - - - - Qt::Horizontal + + + + - - - 40 - 20 - + + Qt::AlignCenter - + - - + + + + + 0 + 0 + + + + <html><head/><body><p>One-time shutdown procedure to be executed after all scheduler jobs are completed. The script is executed <span style=" font-weight:600; text-decoration: underline;">after</span> the shutdown procedures (e.g. parking), if selected, are completed.</p></body></html> + - Job Completion Conditions + Observatory Shutdown Procedure - + - 3 + 1 3 3 3 3 - - - The observation job is completed when the sequence is complete. - - - Se&quence completion - - - true + + + 1 - - completionButtonGroup - - - - - - + + + true + + + + 0 + 0 + + + + Turn off CCD cooler. + - &Repeat for + Warm CCD - completionButtonGroup + shutdownProcedureGroup - - - Restart job until it is executed this many times. - - - 1 - - - 1000 + + + + 0 + 0 + - - 1 + + Close dust cover - - - - - runs + Cap + + shutdownProcedureGroup + - - - Qt::Horizontal + + + true - - - 40 - 20 - + + + 0 + 0 + - - - - - - - - Restart the sequence job indefinitely. - - - Repeat until ter&minated - - - completionButtonGroup - - - - - - - 1 - - - - Terminate the job on the given date and time. + Park telescope to home position. - Repeat until + Park Mount - completionButtonGroup + shutdownProcedureGroup - - - dd/MM/yy hh:mm - - + + true - - Qt::LocalTime + + + 0 + 0 + + + + Park dome to home position + + + Park Dome + + shutdownProcedureGroup + - + Qt::Horizontal 40 20 + + + + + 0 + 0 + + + + + + + - - - - - - - <html><head/><body><p>Define what should happen when a job steps into an error or aborts:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Don't re-schedule</span>: Don't restart the job in case of an error or an abort.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Re-schedule after all terminated</span>: If a job gets aborted, the scheduler will only re-schedule it if when all jobs are finished or aborted. If this is the case, the scheduler re-schedules all aborted jobs and sleeps for the given delay.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Re-schedule immediately</span>: As soon as a job gets aborted, the scheduler will re-schedule it and waits the given delay.</li></ul><p>If the option for re-scheduling errors is selected, errors are handled like aborts. Otherwise, jobs that step into an error are never re-scheduled.</p></body></html> - - - Handling of aborted jobs - - - - 18 - - - 0 - - - - - Do not re-schedule aborted jobs. - - - Don'&t re-schedule - - - errorHandlingButtonGroup - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Re-schedule aborted jobs as soon as all executable jobs are either completed or aborted. - - - Re-s&chedule after all finished - - - errorHandlingButtonGroup - - - - - - - Treat errors like aborts. - - - Re-schedule errors - - - - - - - Delay how long should be waited until an aborted job will be restarted. - - - wait (secs) - - - - - - - Re-schedule an aborted job immediately. - - - Re-s&chedule immediately - - - errorHandlingButtonGroup - - - - - - - Delay in seconds. - - - 10000 - - - 10 + + + + 1 - + + + + Script: + + + + + + + + 0 + 0 + + + + false + + + true + + + + + + + + 0 + 0 + + + + + + + + - - - - - - + + - <html><head/><body><p>One-time startup procedure to be executed before starting Ekos. The script is executed <span style=" font-weight:600; text-decoration: underline;">before</span> the startup procedures (e.g. unpark scope), if selected, are executed.</p></body></html> + <html><head/><body><p>Define what should happen when a job steps into an error or aborts:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Don't re-schedule</span>: Don't restart the job in case of an error or an abort.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Re-schedule after all terminated</span>: If a job gets aborted, the scheduler will only re-schedule it if when all jobs are finished or aborted. If this is the case, the scheduler re-schedules all aborted jobs and sleeps for the given delay.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Re-schedule immediately</span>: As soon as a job gets aborted, the scheduler will re-schedule it and waits the given delay.</li></ul><p>If the option for re-scheduling errors is selected, errors are handled like aborts. Otherwise, jobs that step into an error are never re-scheduled.</p></body></html> - Observatory Startup Procedure + Aborted Job Management - + 1 3 3 3 3 - + 1 - - - true + + + + 0 + 0 + - Park dome to home position + Do not re-schedule aborted jobs. - UnPark Dome + &None - startupProcedureButtonGroup + errorHandlingButtonGroup - - - true + + + + 0 + 0 + - Park telescope to home position. + Re-schedule aborted jobs as soon as all executable jobs are either completed or aborted. - UnPark Mount + &Queue - startupProcedureButtonGroup + errorHandlingButtonGroup - + + + + 0 + 0 + + - Open dust cover + Re-schedule an aborted job immediately. - UnCap + I&mmediate - startupProcedureButtonGroup + errorHandlingButtonGroup - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 32 - 32 - - - - - - - - + 1 - + + + + 0 + 0 + + + + Treat errors like aborts. + - Script: + Re-schedule errors - + - + 0 0 - - false + + Delay in seconds. - - true + + s wait - - - - - - - 32 - 32 - + + 10000 - - + + 10 - - - - - 0 - 0 - - + + - <html><head/><body><p>One-time shutdown procedure to be executed after all scheduler jobs are completed. The script is executed <span style=" font-weight:600; text-decoration: underline;">after</span> the shutdown procedures (e.g. parking), if selected, are completed.</p></body></html> + <html><head/><body><p>One-time startup procedure to be executed before starting Ekos. The script is executed <span style=" font-weight:600; text-decoration: underline;">before</span> the startup procedures (e.g. unpark scope), if selected, are executed.</p></body></html> - Observatory Shutdown Procedure + Observatory Startup Procedure - + 1 3 3 3 3 - + 1 - + true - - Turn off CCD cooler. - - - Warm CCD + + + 0 + 0 + - - shutdownProcedureGroup - - - - - - Close dust cover + Park dome to home position - Cap + UnPark Dome - shutdownProcedureGroup + startupProcedureButtonGroup - + true + + + 0 + 0 + + Park telescope to home position. - Park Mount + UnPark Mount - shutdownProcedureGroup + startupProcedureButtonGroup - - - true + + + + 0 + 0 + - Park dome to home position + Open dust cover - Park Dome + UnCap - shutdownProcedureGroup + startupProcedureButtonGroup - + Qt::Horizontal 40 20 - - - - 0 - 0 - - - - - 32 - 32 - + + + + 0 + 0 + - + 1 - + Script: - + - + 0 0 false true - - - - 32 - 32 - + + + + 0 + 0 + - - - - 3 - - - - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 20 - 1 - - - - dmsBox QLineEdit
widgets/dmsbox.h
1
nameEdit - selectObjectB raBox decBox - fitsEdit - selectFITSB - sequenceEdit - loadSequenceB trackStepCheck - focusStepCheck - alignStepCheck - guideStepCheck unparkDomeCheck unparkMountCheck uncapCheck startupB selectStartupScriptB warmCCDCheck capCheck parkMountCheck parkDomeCheck shutdownB shutdownScript selectShutdownScriptB addToQueueB removeFromQueueB evaluateOnlyB mosaicB queueLoadB queueSaveB queueSaveAsB queueTable startB pauseB - asapConditionR - culminationConditionR - culminationOffset - startupTimeConditionR - startupTimeEdit - altConstraintCheck - minAltitude - moonSeparationCheck - minMoonSeparation - twilightCheck - weatherCheck - sequenceCompletionR - loopCompletionR - timeCompletionR - completionTimeEdit false - - - + false - + false - - + + false + +
diff --git a/kstars/kstars.h b/kstars/kstars.h index 1385fdbe1..b751c2fa7 100644 --- a/kstars/kstars.h +++ b/kstars/kstars.h @@ -1,886 +1,890 @@ /** ************************************************************************* kstars.h - K Desktop Planetarium ------------------- begin : Mon Feb 5 01:11:45 PST 2001 copyright : (C) 2001 by Jason Harris email : jharris@30doradus.org ***************************************************************************/ /** ************************************************************************* * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #pragma once #include "config-kstars.h" #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) #include #else #include #endif #ifdef HAVE_CFITSIO #include #endif // forward declaration is enough. We only need pointers class QActionGroup; class QDockWidget; class QPalette; class KActionMenu; +class KConfigDialog; class KStarsData; class SkyPoint; class SkyMap; class GeoLocation; class FindDialog; class TimeStepBox; class ImageExporter; class AltVsTime; class WUTDialog; class WIView; class WILPSettings; class WIEquipSettings; class ObsConditions; class AstroCalc; class SkyCalendar; class ScriptBuilder; class PlanetViewer; //class JMoonTool; class MoonPhaseTool; class FlagManager; class Execute; class ExportImageDialog; class PrintingWizard; class HorizonManager; class EyepieceField; class AddDeepSkyObject; class OpsCatalog; class OpsGuides; class OpsSolarSystem; class OpsSatellites; class OpsSupernovae; class OpsColors; class OpsAdvanced; class OpsINDI; class OpsEkos; class OpsFITS; class OpsXplanet; namespace Ekos { class Manager; } #ifdef HAVE_CFITSIO class FITSViewer; #endif /** *@class KStars *@short This is the main window for KStars. *In addition to the GUI elements, the class contains the program clock, KStarsData, and SkyMap objects. It also contains functions for the \ref DBusInterface D-Bus interface. KStars is now a singleton class. Use KStars::createInstance() to create an instance and KStars::Instance() to get a pointer to the instance *@author Jason Harris, Jasem Mutlaq *@version 1.1 */ class KStars : public KXmlGuiWindow { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars") private: /** * @short Constructor. * @param doSplash should the splash panel be displayed during * initialization. * @param startClockRunning should the clock be running on startup? * @param startDateString date (in string representation) to start running from. * * @todo Refer to documentation on date format. */ explicit KStars(bool doSplash, bool startClockRunning = true, const QString &startDateString = QString()); public: /** * @short Create an instance of this class. Destroy any previous instance * @param doSplash * @param clockrunning * @param startDateString * @note See KStars::KStars for details on parameters * @return a pointer to the instance */ static KStars *createInstance(bool doSplash, bool clockrunning = true, const QString &startDateString = QString()); /** @return a pointer to the instance of this class */ inline static KStars *Instance() { return pinstance; } /** Destructor. */ ~KStars() override; /** Syncs config file. Deletes objects. */ void releaseResources(); /** @return pointer to KStarsData object which contains application data. */ inline KStarsData *data() const { return m_KStarsData; } /** @return pointer to SkyMap object which is the sky display widget. */ inline SkyMap *map() const { return m_SkyMap; } inline FlagManager *flagManager() const { return m_FlagManager; } inline PrintingWizard *printingWizard() const { return m_PrintingWizard; } #ifdef HAVE_CFITSIO QPointer genericFITSViewer(); void addFITSViewer(QPointer fv); void clearAllViewers(); #endif #ifdef HAVE_INDI Ekos::Manager *ekosManager(); #endif /** Add an item to the color-scheme action manu * @param name The name to use in the menu * @param actionName The internal name for the action (derived from filename) */ void addColorMenuItem(const QString &name, const QString &actionName); /** Remove an item from the color-scheme action manu * @param actionName The internal name of the action (derived from filename) */ void removeColorMenuItem(const QString &actionName); /** @short Apply config options throughout the program. * In most cases, options are set in the "Options" object directly, * but for some things we have to manually react to config changes. * @param doApplyFocus If true, then focus posiiton will be set * from config file */ void applyConfig(bool doApplyFocus = true); void showImgExportDialog(); void syncFOVActions(); void hideAllFovExceptFirst(); void selectNextFov(); void selectPreviousFov(); void showWISettingsUI(); void showWI(ObsConditions *obs); /** Load HIPS information and repopulate menu. */ void repopulateHIPS(); WIEquipSettings *getWIEquipSettings() { return m_WIEquipmentSettings; } public Q_SLOTS: /** @defgroup DBusInterface DBus Interface KStars provides powerful scripting functionality via DBus. The most common DBus functions can be constructed and executed within the ScriptBuilder tool. Any 3rd party language or tool with support for DBus can access several interfaces provided by KStars:
  • KStars: Provides functions to manipulate the skymap including zoom, pan, and motion to selected objects. Add and remove object trails and labels. Wait for user input before running further actions.
  • SimClock: Provides functions to start and stop time, set a different date and time, and to set the clock scale.
  • Ekos: Provides functions to start and stop Ekos Manager, set Ekos connection mode, and access to Ekos modules:
    • Capture: Provides functions to capture images, load sequence queues, control filter wheel, and obtain information on job progress.
    • Focus: Provides functions to focus control in manual and automated mode. Start and stop focusing procedures and set autofocus options.
    • Guide: Provides functions to start and stop calibration and autoguiding procedures. Set calibration and autoguide options.
    • Align: Provides functions to solve images use online or offline astrometry.net solver.
*/ /*@{*/ /** DBUS interface function. * Set focus to given Ra/Dec coordinates * @param ra the Right Ascension coordinate for the focus (in Hours) * @param dec the Declination coordinate for the focus (in Degrees) */ Q_SCRIPTABLE Q_NOREPLY void setRaDec(double ra, double dec); /** DBUS interface function. * Set focus to given Alt/Az coordinates. * @param alt the Altitude coordinate for the focus (in Degrees) * @param az the Azimuth coordinate for the focus (in Degrees) */ Q_SCRIPTABLE Q_NOREPLY void setAltAz(double alt, double az); /** DBUS interface function. * Point in the direction described by the string argument. * @param direction either an object name, a compass direction (e.g., "north"), or "zenith" */ Q_SCRIPTABLE Q_NOREPLY void lookTowards(const QString &direction); /** DBUS interface function. * Add a name label to the named object * @param name the name of the object to which the label will be attached */ Q_SCRIPTABLE Q_NOREPLY void addLabel(const QString &name); /** DBUS interface function. * Remove a name label from the named object * @param name the name of the object from which the label will be removed */ Q_SCRIPTABLE Q_NOREPLY void removeLabel(const QString &name); /** DBUS interface function. * Add a trail to the named solar system body * @param name the name of the body to which the trail will be attached */ Q_SCRIPTABLE Q_NOREPLY void addTrail(const QString &name); /** DBUS interface function. * Remove a trail from the named solar system body * @param name the name of the object from which the trail will be removed */ Q_SCRIPTABLE Q_NOREPLY void removeTrail(const QString &name); /** DBUS interface function. Zoom in one step. */ Q_SCRIPTABLE Q_NOREPLY void zoomIn(); /** DBUS interface function. Zoom out one step. */ Q_SCRIPTABLE Q_NOREPLY void zoomOut(); /** DBUS interface function. reset to the default zoom level. */ Q_SCRIPTABLE Q_NOREPLY void defaultZoom(); /** DBUS interface function. Set zoom level to specified value. * @param z the zoom level. Units are pixels per radian. */ Q_SCRIPTABLE Q_NOREPLY void zoom(double z); /** DBUS interface function. Set local time and date. * @param yr year of date * @param mth month of date * @param day day of date * @param hr hour of time * @param min minute of time * @param sec second of time */ Q_SCRIPTABLE Q_NOREPLY void setLocalTime(int yr, int mth, int day, int hr, int min, int sec); /** DBUS interface function. Set local time and date to present values acc. system clock * @note Just a proxy for slotSetTimeToNow(), but it is better to * keep the DBus interface separate from the internal methods. */ Q_SCRIPTABLE Q_NOREPLY void setTimeToNow(); /** DBUS interface function. Delay further execution of DBUS commands. * @param t number of seconds to delay */ Q_SCRIPTABLE Q_NOREPLY void waitFor(double t); /** DBUS interface function. Pause further DBUS execution until a key is pressed. * @param k the key which will resume DBUS execution */ Q_SCRIPTABLE Q_NOREPLY void waitForKey(const QString &k); /** DBUS interface function. Toggle tracking. * @param track engage tracking if true; else disengage tracking */ Q_SCRIPTABLE Q_NOREPLY void setTracking(bool track); /** DBUS interface function. modify a view option. * @param option the name of the option to be modified * @param value the option's new value */ Q_SCRIPTABLE Q_NOREPLY void changeViewOption(const QString &option, const QString &value); /** DBUS interface function. * @param name the name of the option to query * @return the current value of the named option */ Q_SCRIPTABLE QString getOption(const QString &name); /** DBUS interface function. Read config file. * This function is useful for restoring the user settings from the config file, * after having modified the settings in memory. * @sa writeConfig() */ Q_SCRIPTABLE Q_NOREPLY void readConfig(); /** DBUS interface function. Write current settings to config file. * This function is useful for storing user settings before modifying them with a DBUS * script. The original settings can be restored with readConfig(). * @sa readConfig() */ Q_SCRIPTABLE Q_NOREPLY void writeConfig(); /** DBUS interface function. Show text message in a popup window. * @note Not Yet Implemented * @param x x-coordinate for message window * @param y y-coordinate for message window * @param message the text to display in the message window */ Q_SCRIPTABLE Q_NOREPLY void popupMessage(int x, int y, const QString &message); /** DBUS interface function. Draw a line on the sky map. * @note Not Yet Implemented * @param x1 starting x-coordinate of line * @param y1 starting y-coordinate of line * @param x2 ending x-coordinate of line * @param y2 ending y-coordinate of line * @param speed speed at which line should appear from start to end points (in pixels per second) */ Q_SCRIPTABLE Q_NOREPLY void drawLine(int x1, int y1, int x2, int y2, int speed); /** DBUS interface function. Set the geographic location. * @param city the city name of the location * @param province the province name of the location * @param country the country name of the location * @return True if geographic location is found and set, false otherwise. */ Q_SCRIPTABLE bool setGeoLocation(const QString &city, const QString &province, const QString &country); /** * @brief location Returns a JSON Object (as string) that contains the following information: * name: String * province: String * country: String * longitude: Double (-180 to +180) * latitude: Double (-90 to +90) * tz0 (Time zone without DST): Double * tz (Time zone with DST): Double * @return Stringified JSON object as described above. */ Q_SCRIPTABLE QString location(); /** DBUS interface function. Set the GPS geographic location. * @param longitude longitude in degrees (-180 West to +180 East) * @param latitude latitude in degrees (-90 South to +90 North) * @param elevation site elevation in meters * @param tz0 Time zone offset WITHOUT daylight saving time. * @return True if geographic location is set, false otherwise. */ Q_SCRIPTABLE bool setGPSLocation(double longitude, double latitude, double elevation, double tz0); /** DBUS interface function. Modify a color. * @param colorName the name of the color to be modified (e.g., "SkyColor") * @param value the new color to use */ Q_SCRIPTABLE Q_NOREPLY void setColor(const QString &colorName, const QString &value); /** DBUS interface function. Load a color scheme. * @param name the name of the color scheme to load (e.g., "Moonless Night") */ Q_SCRIPTABLE Q_NOREPLY void loadColorScheme(const QString &name); /** DBUS interface function. Export the sky image to a file. * @param filename the filename for the exported image * @param width the width for the exported image. Map's width will be used if nothing or an invalid value is supplied. * @param height the height for the exported image. Map's height will be used if nothing or an invalid value is supplied. * @param includeLegend should we include a legend? */ Q_SCRIPTABLE Q_NOREPLY void exportImage(const QString &filename, int width = -1, int height = -1, bool includeLegend = false); /** DBUS interface function. Return a URL to retrieve Digitized Sky Survey image. * @param objectName name of the object. * @note If the object is note found, the string "ERROR" is returned. */ Q_SCRIPTABLE QString getDSSURL(const QString &objectName); /** DBUS interface function. Return a URL to retrieve Digitized Sky Survey image. * @param RA_J2000 J2000.0 RA * @param Dec_J2000 J2000.0 Declination * @param width width of the image, in arcminutes (default = 15) * @param height height of the image, in arcminutes (default = 15) */ Q_SCRIPTABLE QString getDSSURL(double RA_J2000, double Dec_J2000, float width = 15, float height = 15); /** DBUS interface function. Return XML containing information about a sky object * @param objectName name of the object. * @note If the object was not found, the XML is empty. */ Q_SCRIPTABLE QString getObjectDataXML(const QString &objectName); /** DBUS interface function. Return XML containing position info about a sky object * @param objectName name of the object. * @note If the object was not found, the XML is empty. */ Q_SCRIPTABLE QString getObjectPositionInfo(const QString &objectName); /** DBUS interface function. Render eyepiece view and save it in the file(s) specified * @note See EyepieceField::renderEyepieceView() for more info. This is a DBus proxy that calls that method, and then writes the resulting image(s) to file(s). * @note Important: If imagePath is empty, but overlay is true, or destPathImage is supplied, this method will make a blocking DSS download. */ Q_SCRIPTABLE Q_NOREPLY void renderEyepieceView(const QString &objectName, const QString &destPathChart, const double fovWidth = -1.0, const double fovHeight = -1.0, const double rotation = 0.0, const double scale = 1.0, const bool flip = false, const bool invert = false, QString imagePath = QString(), const QString &destPathImage = QString(), const bool overlay = false, const bool invertColors = false); /** DBUS interface function. Set the approx field-of-view * @param FOV_Degrees field of view in degrees */ Q_SCRIPTABLE Q_NOREPLY void setApproxFOV(double FOV_Degrees); /** DBUS interface function. Get the dimensions of the Sky Map. * @return a string containing widthxheight in pixels. */ Q_SCRIPTABLE QString getSkyMapDimensions(); /** DBUS interface function. Return a newline-separated list of objects in the observing wishlist. * @note Unfortunately, unnamed objects are troublesome. Hopefully, we don't have them on the observing list. */ Q_SCRIPTABLE QString getObservingWishListObjectNames(); /** DBUS interface function. Return a newline-separated list of objects in the observing session plan. * @note Unfortunately, unnamed objects are troublesome. Hopefully, we don't have them on the observing list. */ Q_SCRIPTABLE QString getObservingSessionPlanObjectNames(); /** DBUS interface function. Print the sky image. * @param usePrintDialog if true, the KDE print dialog will be shown; otherwise, default parameters will be used * @param useChartColors if true, the "Star Chart" color scheme will be used for the printout, which will save ink. */ Q_SCRIPTABLE Q_NOREPLY void printImage(bool usePrintDialog, bool useChartColors); /** DBUS interface function. Open FITS image. * @param imageUrl URL of FITS image to load. For a local file the prefix must be file:// For example * if the file is located at /home/john/m42.fits then the full URL is file:///home/john/m42.fits */ Q_SCRIPTABLE Q_NOREPLY void openFITS(const QUrl &imageUrl); /** @}*/ /** * Update time-dependent data and (possibly) repaint the sky map. * @param automaticDSTchange change DST status automatically? */ void updateTime(const bool automaticDSTchange = true); /** action slot: sync kstars clock to system time */ void slotSetTimeToNow(); /** Apply new settings and redraw skymap */ void slotApplyConfigChanges(); /** Apply new settings for WI */ void slotApplyWIConfigChanges(); /** Called when zoom level is changed. Enables/disables zoom * actions and updates status bar. */ void slotZoomChanged(); /** action slot: Allow user to specify a field-of-view angle for the display window in degrees, * and set the zoom level accordingly. */ void slotSetZoom(); /** action slot: Toggle whether kstars is tracking current position */ void slotTrack(); /** action slot: open dialog for selecting a new geographic location */ void slotGeoLocator(); /** * @brief slotSetTelescopeEnabled call when telescope comes online or goes offline. * @param enable True if telescope is online and connected, false otherwise. */ void slotSetTelescopeEnabled(bool enable); /** * @brief slotSetDomeEnabled call when dome comes online or goes offline. * @param enable True if dome is online and connected, false otherwise. */ void slotSetDomeEnabled(bool enable); /** Delete FindDialog because ObjNames list has changed in KStarsData due to * reloading star data. So list in FindDialog must be new filled with current data. */ void clearCachedFindDialog(); /** Remove all trails which may have been added to solar system bodies */ void slotClearAllTrails(); /** Display position in the status bar. */ void slotShowPositionBar(SkyPoint *); /** action slot: open Flag Manager */ void slotFlagManager(); /** Show the eyepiece view tool */ void slotEyepieceView(SkyPoint *sp, const QString &imagePath = QString()); /** Show the add deep-sky object dialog */ void slotAddDeepSkyObject(); /** action slot: open KStars startup wizard */ void slotWizard(); void updateLocationFromWizard(const GeoLocation &geo); WIView *wiView() { return m_WIView; } bool isWIVisible() { if (!m_WIView) return false; if (!m_wiDock) return false; return m_wiDock->isVisible(); } //FIXME Port to QML2 //#if 0 /** action slot: open What's Interesting settings window */ void slotWISettings(); /** action slot: toggle What's Interesting window */ void slotToggleWIView(); //#endif private slots: /** action slot: open a dialog for setting the time and date */ void slotSetTime(); /** action slot: toggle whether kstars clock is running or not */ void slotToggleTimer(); /** action slot: advance one step forward in time */ void slotStepForward(); /** action slot: advance one step backward in time */ void slotStepBackward(); /** action slot: open dialog for finding a named object */ void slotFind(); /** action slot: open KNewStuff window to download extra data. */ void slotDownload(); /** action slot: open KStars calculator to compute astronomical ephemeris */ void slotCalculator(); /** action slot: open Elevation vs. Time tool */ void slotAVT(); /** action slot: open What's up tonight dialog */ void slotWUT(); /** action slot: open Sky Calendar tool */ void slotCalendar(); /** action slot: open the glossary */ void slotGlossary(); /** action slot: open ScriptBuilder dialog */ void slotScriptBuilder(); /** action slot: open Solar system viewer */ void slotSolarSystem(); /** action slot: open Jupiter Moons tool */ // void slotJMoonTool(); /** action slot: open Moon Phase Calendar tool */ void slotMoonPhaseTool(); #if 0 /** action slot: open Telescope wizard */ void slotTelescopeWizard(); #endif /** action slot: open INDI driver panel */ void slotINDIDriver(); /** action slot: open INDI control panel */ void slotINDIPanel(); /** action slot: open Ekos panel */ void slotEkos(); /** action slot: Track with the telescope (INDI) */ void slotINDITelescopeTrack(); /** * Action slot: Slew with the telescope (INDI) * * @param focused_object Slew to the focused object or the mouse pointer if false. * */ void slotINDITelescopeSlew(bool focused_object = true); void slotINDITelescopeSlewMousePointer(); /** * Action slot: Sync the telescope (INDI) * * @param focused_object Sync the position of the focused object or the mouse pointer if false. * */ void slotINDITelescopeSync(bool focused_object = true); void slotINDITelescopeSyncMousePointer(); /** action slot: Abort any telescope motion (INDI) */ void slotINDITelescopeAbort(); /** action slot: Park the telescope (INDI) */ void slotINDITelescopePark(); /** action slot: Unpark the telescope (INDI) */ void slotINDITelescopeUnpark(); /** action slot: Park the dome (INDI) */ void slotINDIDomePark(); /** action slot: UnPark the dome (INDI) */ void slotINDIDomeUnpark(); /** action slot: open dialog for setting the view options */ void slotViewOps(); /** finish setting up after the kstarsData has finished */ void datainitFinished(); /** Open FITS image. */ void slotOpenFITS(); /** Action slot to save the sky image to a file.*/ void slotExportImage(); /** Action slot to select a DBUS script and run it.*/ void slotRunScript(); /** Action slot to print skymap. */ void slotPrint(); /** Action slot to start Printing Wizard. */ void slotPrintingWizard(); /** Action slot to show tip-of-the-day window. */ void slotTipOfDay(); /** Action slot to set focus coordinates manually (opens FocusDialog). */ void slotManualFocus(); /** Meta-slot to point the focus at special points (zenith, N, S, E, W). * Uses the name of the Action which sent the Signal to identify the * desired direction. */ void slotPointFocus(); /** Meta-slot to set the color scheme according to the name of the * Action which sent the activating signal. */ void slotColorScheme(); /** * @brief slotThemeChanged save theme name in options */ void slotThemeChanged(); /** Select the Target symbol (a.k.a. field-of-view indicator) */ void slotTargetSymbol(bool flag); /** Select the HIPS Source catalog. */ void slotHIPSSource(); /** Invoke the Field-of-View symbol editor window */ void slotFOVEdit(); /** Toggle between Equatorial and Ecliptic coordinate systems */ void slotCoordSys(); /** Set the map projection according to the menu selection */ void slotMapProjection(); /** Toggle display of the observing list tool*/ void slotObsList(); /** Meta-slot to handle display toggles for all of the viewtoolbar buttons. * uses the name of the sender to identify the item to change. */ void slotViewToolBar(); /** Meta-slot to handle display toggles for all of the INDItoolbar buttons. * uses the name of the sender to identify the item to change. */ void slotINDIToolBar(); /** Meta-slot to handle toggling display of GUI elements (toolbars and infoboxes) * uses name of the sender action to identify the widget to hide/show. */ void slotShowGUIItem(bool); /** Toggle to and from full screen mode */ void slotFullScreen(); /** Save data to config file before exiting.*/ void slotAboutToQuit(); void slotEquipmentWriter(); void slotObserverManager(); void slotHorizonManager(); void slotExecute(); void slotPolarisHourAngle(); /** Update comets orbital elements*/ void slotUpdateComets(bool isAutoUpdate = false); /** Update asteroids orbital elements*/ void slotUpdateAsteroids(bool isAutoUpdate = false); /** Update list of recent supernovae*/ void slotUpdateSupernovae(); /** Update satellites orbital elements*/ void slotUpdateSatellites(); /** Configure Notifications */ void slotConfigureNotifications(); private: /** Load FOV information and repopulate menu. */ void repopulateFOV(); /** * @brief populateThemes Populate application themes */ void populateThemes(); /** Initialize Menu bar, toolbars and all Actions. */ void initActions(); + /** Prepare options dialog. */ + KConfigDialog* prepareOps(); + /** Initialize Status bar. */ void initStatusBar(); /** Initialize focus position */ void initFocus(); /** Build the KStars main window */ void buildGUI(); void closeEvent(QCloseEvent *event) override; public: /** Check if the KStars main window is shown */ bool isGUIReady() { return m_SkyMap != nullptr; } /** Was KStars started with the clock running, or paused? */ bool isStartedWithClockRunning() { return StartClockRunning; } /// Set to true when the application is being closed static bool Closing; private: /// Pointer to an instance of KStars static KStars *pinstance; KActionMenu *colorActionMenu { nullptr }; KActionMenu *fovActionMenu { nullptr }; KActionMenu *hipsActionMenu { nullptr }; KStarsData *m_KStarsData { nullptr }; SkyMap *m_SkyMap { nullptr }; // Widgets TimeStepBox *m_TimeStepBox { nullptr }; // Dialogs & Tools // File Menu ExportImageDialog *m_ExportImageDialog { nullptr }; PrintingWizard *m_PrintingWizard { nullptr }; // Tool Menu AstroCalc *m_AstroCalc { nullptr }; AltVsTime *m_AltVsTime { nullptr }; SkyCalendar *m_SkyCalendar { nullptr }; ScriptBuilder *m_ScriptBuilder { nullptr }; PlanetViewer *m_PlanetViewer { nullptr }; WUTDialog *m_WUTDialog { nullptr }; // JMoonTool *m_JMoonTool { nullptr }; FlagManager *m_FlagManager { nullptr }; HorizonManager *m_HorizonManager { nullptr }; EyepieceField *m_EyepieceView { nullptr }; #ifdef HAVE_CFITSIO QPointer m_GenericFITSViewer; QList> m_FITSViewers; #endif #ifdef HAVE_INDI QPointer m_EkosManager; #endif AddDeepSkyObject *m_addDSODialog { nullptr }; // FIXME Port to QML2 //#if 0 WIView *m_WIView { nullptr }; WILPSettings *m_WISettings { nullptr }; WIEquipSettings *m_WIEquipmentSettings { nullptr }; ObsConditions *m_ObsConditions { nullptr }; QDockWidget *m_wiDock { nullptr }; //#endif QActionGroup *projectionGroup { nullptr }; QActionGroup *cschemeGroup { nullptr }; QActionGroup *hipsGroup { nullptr }; QActionGroup *telescopeGroup { nullptr }; QActionGroup *domeGroup { nullptr }; bool DialogIsObsolete { false }; bool StartClockRunning { false }; QString StartDateString; QLabel AltAzField, RADecField, J2000RADecField; //QPalette OriginalPalette, DarkPalette; OpsCatalog *opcatalog { nullptr }; OpsGuides *opguides { nullptr }; OpsSolarSystem *opsolsys { nullptr }; OpsSatellites *opssatellites { nullptr }; OpsSupernovae *opssupernovae { nullptr }; OpsColors *opcolors { nullptr }; OpsAdvanced *opadvanced { nullptr }; OpsINDI *opsindi { nullptr }; OpsEkos *opsekos { nullptr }; OpsFITS *opsfits { nullptr }; OpsXplanet *opsxplanet { nullptr }; }; diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg index 6e481a8f8..fdabddbf6 100644 --- a/kstars/kstars.kcfg +++ b/kstars/kstars.kcfg @@ -1,2435 +1,2443 @@ ksutils.h The screen coordinates of the Time InfoBox. QPoint(0,0) The screen coordinates of the Focus InfoBox. QPoint(600,0) The screen coordinates of the Geographic Location InfoBox. QPoint(0,600) If true, the Time InfoBox will show only its top line of data. true If true, the Focus InfoBox will show only its top line of data. true If true, the Geographic Location InfoBox will show only its top line of data. true Toggles display of all three InfoBoxes. true Toggles display of the Time InfoBox. true Toggles display of the Focus InfoBox. true Toggles display of the Geographic Location InfoBox. true Is the Time InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 0 0 3 Is the Focus InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 1 0 3 Is the Geographic Location InfoBox anchored to a window edge? 0 = not anchored; 1 = anchored to right edge; 2 = anchored to bottom edge; 3 = anchored to bottom and right edges. 2 0 3 Toggle display of the status bar. true Toggle display of the Horizontal coordinates of the mouse cursor in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the current epoch in the status bar. true Toggle display of the Equatorial coordinates of the mouse cursor at the standard epoch in the status bar. false true 1024 768 true Black Body List of the filenames of custom object catalogs. List of integers toggling display of each custom object catalog (any nonzero value indicates the objects in that catalog will be displayed). List of names for which custom catalogs are to be displayed. Names of objects entered into the find dialog are resolved using online services and stored in the database. This option also toggles the display of such resolved objects on the sky map. true 800 600 true true false Toggle display of crosshairs centered at telescope's pointed position in the KStars sky map. true Toggle display of INDI messages in the KStars statusbar. true false Show INDI messages as desktop notifications instead of dialogs. false true false false The default location of saved FITS files KSUtils::getDefaultPath("fitsDir") INDI server will attempt to bind with ports starting from this port 7624 INDI server will attempt to bind with ports ending with this port 9000 List of the aliases for filter wheel slots. PATH to indiserver binary KSUtils::getDefaultPath("indiServer") false PATH to indi drivers directory KSUtils::getDefaultPath("indiDriversDir") false 320 240 false false false false false false false false false false false The City name of the current geographic location. Greenwich The Province name of the current geographic location. This is the name of the state for locations in the U. S. The Country name of the current geographic location. United Kingdom The longitude of the current geographic location, in decimal degrees. 0.0 The latitude of the current geographic location, in decimal degrees. 51.468 -10.0 0.0 Two-letter code that determines the dates on which daylight savings time begins and ends (you can view the rules by pressing the "Explain DST Rules" button in the Geographic Location window). -- If true, focus changes will cause the sky to visibly spin to the new position. Otherwise, the display will "snap" instantly to the new position. true If true, clicking on the skymap will select the closest object and highlights it. false Type of cursor when exploring the sky map. 1 The names of the currently selected field-of-view indicators. The list of defined FOV indicator names is listed in the "Settings|FOV Symbols" menu. Telrad If true, trails attached to solar system bodies will fade into the background sky color. true The right ascension of the initial focus position of the sky map, in decimal hours. This value is volatile; it is reset whenever the program shuts down. 180.0 The declination of the initial focus position of the sky map, in decimal degrees. This value is volatile; it is reset whenever the program shuts down. 45.0 The name of the object that should be centered and tracked on startup. If no object should be centered, set to "nothing". This value is volatile; it is reset whenever the program shuts down. nothing True if the skymap should track on its initial position on startup. This value is volatile; it is reset whenever the program shuts down. false Toggle whether KStars should hide some objects while the display is moving, for smoother motion. true Toggle whether constellation boundaries are hidden while the display is in motion. true Toggle whether constellation lines are hidden while the display is in motion. false Choose sky culture. 11 Toggle whether constellation names are hidden while the display is in motion. false Toggle whether the coordinate grids are hidden while the display is in motion. true Toggle whether the Milky Way contour is hidden while the display is in motion. true Toggle whether IC objects are hidden while the display is in motion. true Toggle whether Messier objects are hidden while the display is in motion. true Toggle whether NGC objects are hidden while the display is in motion. true Toggle whether extra objects are hidden while the display is in motion. true Toggle whether solar system objects are hidden while the display is in motion. false Toggle whether faint stars are hidden while the display is in motion. true Toggle whether name labels are hidden while the display is in motion. true Toggle whether asteroids are drawn in the sky map. true Toggle whether asteroid name labels are drawn in the sky map. false true Toggle whether comets are drawn in the sky map. true Toggle whether comet comas are drawn in the sky map. true Toggle whether comet name labels are drawn in the sky map. false Toggle whether supernovae are drawn in the sky map. false Toggle whether supernova name labels are drawn in the sky map. false Set magnitude limit for supernovae to be shown on the skymap. 16 Toggle supernova alerts. true Set magnitude limit for supernovae to be alerted. 13 Toggle whether constellation boundaries are drawn in the sky map. false Toggle whether constellation boundary containing the central focus point is highlighted in the sky map. false Toggle whether constellation lines are drawn in the sky map. false Toggle whether constellation art drawn in the sky map. false Toggle whether constellation name labels are drawn in the sky map. false Toggle whether deep-sky objects are drawn in the sky map. true Toggle whether the ecliptic line is drawn in the sky map. false Toggle whether the equator line is drawn in the sky map. false Coordinate grids will automatically change according to active coordinate system. true Toggle whether the equatorial coordinate grid is drawn in the sky map. false Toggle whether the horizontal coordinate grid is drawn in the sky map. false Toggle whether the local meridian line is drawn in the sky map. false Toggle whether the region below the horizon is opaque. true Toggle whether the horizon line is drawn in the sky map. true Toggle whether flags are drawn in the sky map. true Toggle whether IC objects are drawn in the sky map. false Toggle whether NGC objects are drawn in the sky map. true Toggle whether Messier objects are drawn in the sky map. true Toggle whether Messier objects are rendered as images in the sky map. true Toggle whether extra objects are drawn in the sky map. true Toggle whether the Milky Way contour is drawn in the sky map. true Toggle whether the Milky Way contour is filled. When this option is false, the Milky Way is shown as an outline. true Meta-option to control whether all major planets (and the Sun and Moon) are drawn in the sky map. true Toggle whether major planets (and the Sun and Moon) are rendered as images in the sky map. true Toggle whether major planets (and the Sun and Moon) are labeled in the sky map. true Toggle whether the Sun is drawn in the sky map. true Toggle whether the Moon is drawn in the sky map. true Toggle whether Mercury is drawn in the sky map. true Toggle whether Venus is drawn in the sky map. true Toggle whether Mars is drawn in the sky map. true Toggle whether Jupiter is drawn in the sky map. true Toggle whether Saturn is drawn in the sky map. true Toggle whether Uranus is drawn in the sky map. true Toggle whether Neptune is drawn in the sky map. true Toggle whether Pluto is drawn in the sky map. true Toggle whether stars are drawn in the sky map. true Toggle whether star magnitude (brightness) labels are shown in the sky map. false Toggle whether star name labels are shown in the sky map. true Toggle whether deep-sky object magnitude (brightness) labels are shown in the sky map. false Toggle whether deep-sky object name labels are shown in the sky map. false The timescale above which slewing mode is forced on at all times. 60 The background fill mode for the on-screen information boxes: 0="no BG"; 1="semi-transparent BG"; 2="opaque BG" 1 Algorithm for the mapping projection. 0 Use official IAU abbreviations for constellation names. false Use Latin constellation names. false Use localized constellation names (if localized names are not available, default to Latin names). true Display the sky with horizontal coordinates (when false, equatorial coordinates will be used). true Toggle whether a centered object automatically gets a name label attached. true Toggle whether a centered solar system object automatically gets a trail attached, as long as it remains centered. true Toggle whether the object under the mouse cursor gets a transient name label. true Toggle whether object positions are corrected for the effects of atmospheric refraction (only applies when horizontal coordinates are used). true Toggle whether corrections due to bending of light around the sun are taken into account false Toggle whether the sky is rendered using antialiasing. Lines and shapes are smoother with antialiasing, but rendering the screen will take more time. true The zoom level, measured in pixels per radian. 250. 250. 5000000. When zooming in or out, change zoom speed factor by this multiplier. 0.2 0.01 1.0 The faint magnitude limit for drawing asteroids. 15.0 The maximum magnitude (visibility) to filter the asteroid data download from JPL. 12.000 Controls the relative number of asteroid name labels drawn in the map. 4.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed in. 16.0 The faint magnitude limit for drawing deep-sky objects, when fully zoomed out. 5.0 When enabled, objects whose magnitudes are unknown, or not available to KStars, are drawn irrespective of the faint limits set. true Sets the density of stars in the field of view 5 The faint magnitude limit for drawing stars, when the map is in motion (only applicable if faint stars are set to be hidden while the map is in motion). 5.0 The relative density for drawing star name and magnitude labels. 2.0 The relative density for drawing deep-sky object name and magnitude labels. 5.0 If true, long names (common names) for deep-sky objects are shown in the labels. false The maximum solar distance for drawing comets. 3.0 Use experimental OpenGL backend (deprecated). false The state of the clock (running or not) true Objects in the observing list will be highlighted with a symbol in the map. true Objects in the observing list will be highlighted with a colored name label in the map. false The observing list will prefer DSS imagery while downloading imagery. true The observing list will prefer SDSS imagery while downloading imagery. false Check this if you use a large Dobsonian telescope. Sorting by percentage current altitude is an easy way of determining what objects are well-placed for observation. However, when using a large Dobsonian telescope, objects close to the zenith are hard to observe. Since tracking there corresponds to a rotation in azimuth, it is both counterintuitive and requires the observer to frequently move the ladder. The region around the zenith where this is particularly frustrating is called the Dobsonian hole. This checkbox makes the observing list consider objects present in the hole as unfit for observation. false This specifies the angular radius of the Dobsonian hole, i.e. the region where a large Dobsonian telescope cannot be pointed easily. 15.00 40.00 The name of the color scheme moonless-night.colors The method for rendering stars: 0="realistic colors"; 1="solid red"; 2="solid black"; 3="solid white"; 4="solid real colors" 0 4 The color saturation level of stars (only applicable when using "realistic colors" mode). 6 10 The color for the angular-distance measurement ruler. #FFF The background color of the on-screen information boxes. #000 The text color for the on-screen information boxes, when activated by a mouse click. #F00 The normal text color of the on-screen information boxes. #FFF The color for the constellation boundary lines. #222 The color for the constellation boundary lines. #222 The color for the constellation figure lines. #555 The color for the constellation names. #AA7 The color for the cardinal compass point labels. #002 The color for the ecliptic line. #663 The color for the equator line. #FFF The color for the equatorial coordinate grid lines. #456 The color for the horizontal coordinate grid lines. #5A3 The color for objects which have extra URL links available. #A00 The color for the horizon line and opaque ground. #5A3 The color for the local meridian line. #0059b3 The color for Messier object symbols. #0F0 The color for NGC object symbols. #066 The color for IC object symbols. #439 The color for the Milky Way contour. #123 The color for star name labels. #7AA The color for deep-sky object name labels. #7AA The color for solar system object labels. #439 The color for solar system object trails. #963 The color for the sky background. #002 The color for the artificial horizon region. #C82828 The color for telescope target symbols. #8B8 Color of visible satellites. #00FF00 Color of invisible satellites. #FF0000 Color of satellites labels. #640000 Color of supernova #FFA500 The color for user-added object labels. #439 The color for RA Guide Error bar in Ekos guide module. #00FF00 The color for DEC Guide Error bar in Ekos guide module. #00A5FF The color for solver FOV box in Ekos alignment module. #FFFF00 false Xplanet binary path KSUtils::getDefaultPath("XplanetPath") Option to use a FIFO file instead of saving to the hard disk true How long to wait for XPlanet before giving up in milliseconds 1000 How long to pause between frames in the XPlanet Animation 100 Width of xplanet window 640 Height of xplanet window 480 If true, display a label in the upper right corner. false Show local time. true Show GMT instead of local time. false Specify the text of the first line of the label. By default, it says something like "Looking at Earth". Any instances of %t will be replaced by the target name, and any instances of %o will be replaced by the origin name. Specify the point size. 12 Set the color for the label. #F00 Specify the format for the date/time label. This format string is passed to strftime(3). The default is "%c %Z", which shows the date, time, and time zone in the locale’s appropriate date and time representation. %c %Z false true false false Draw a glare around the sun with a radius of the specified value larger than the Sun. The default value is 28. 28 Place the observer above a random latitude and longitude false Place the observer above the specified longitude and latitude true Render the target body as seen from above the specified latitude (in degrees). The default value is 0. 0 Place the observer above the specified longitude (in degrees). Longitude is positive going east, negative going west (for the earth and moon), so for example Los Angeles is at -118 or 242. The default value is 0. 0 The default is no projection. Multiple bodies will not be shown if this option is specified, although shadows will still be drawn. 0 Use a file as the background image, with the planet to be superimposed upon it. This option is only meaningful with the -projection option. A color may also be supplied. false Use a file as the background image. false The path of the background image. Use a color as the background. true The color of the background. #000 A star of the specified magnitude will have a pixel brightness of 1. The default value is 10. Stars will be drawn more brightly if this number is larger. 10 If checked, use an arc file to be plotted against the background stars. false Specify an arc file to be plotted against the background stars. If checked, use a config file. false Use the specified configuration file. If checked, use kstars's FOV. false If checked, use the specified marker file. false Specify a file containing user-defined marker data to display against the background stars. If checked, write coordinates of the bounding box for each marker in a file. false Write coordinates of the bounding box for each marker to this file. If checked, use star map file to draw the background stars. false Star map file path This option is only used when creating JPEG images. The quality can range from 0 to 100. The default value is 80. 80 Toggle whether satellite tracks are drawn in the sky map. false Toggle whether satellite tracks are drawn in the sky map. false If selected, satellites will be draw like stars, otherwise, draw satellites as small colored square. false Toggle whether satellite labels are drawn in the sky map. false List of selected satellites. Checking this option causes recomputation of current equatorial coordinates from catalog coordinates (i.e. application of precession, nutation and aberration corrections) for every redraw of the map. This makes processing slower when there are many stars to handle, but is more likely to be bug free. There are known bugs in the rendering of stars when this recomputation is avoided. false The default size for DSS images downloaded from the Internet. 15.0 To include parts of the star field, we add some extra padding around DSS images of deep-sky objects. This option configures the total (both sides) padding added to either dimension of the field. 10.0 Checking this option causes KStars to generate verbose debug information for diagnostic purposes. This may cause slowdown of KStars. false Checking this option causes KStars to generate regular debug information. true Checking this option causes KStars to stop generating ANY debug information. false Checking this option causes KStars log debug messages to the default output used by the platform (e.g. Standard Error). true Checking this option causes KStars log debug messages to a log file as specified. false Log FITS Data activity. false Log INDI devices activity. false Log Ekos Capture Module activity. false Log Ekos Focus Module activity. false Log Ekos Guide Module activity. false Log Ekos Alignment Module activity. false Log Ekos Mount Module activity. false Log Ekos Observatory Module activity. false true Display all captured FITS images in a single tab instead of multiple tabs per image. true Display all captured FITS images in a single FITS Viewer window. By default each camera create its own FITS Viewer instance false Display all opened FITS images in a single FITS Viewer window. true Bring the FITSViewer window to the foreground when receiving a new image. true false !KSUtils::isHardwareLimited() false !KSUtils::isHardwareLimited() !KSUtils::isHardwareLimited() KSUtils::isHardwareLimited() 4 false false 40.0 0 600 600 true false true true true false Simulators false true false true 1 Minimum telescope altitude limit. If the telescope is below this limit, it will be commanded to stop. 0 Maximum telescope altitude limit. If the telescope is above this limit, it will be commanded to stop. 90.0 false If the target hour angle exceeds this value, Ekos will command a meridian flip and if successful it will resume guiding and capture operations. false true false false false 3:00 AM false 1 0 If guide deviation exceeds this limit, the exposure will be automatically aborted and only resumed when the deviation is within this limit. 2 If HFR deviation exceeds this limit, the autofocus routine will be automatically started. 0.5 false false false Sets the time interval before forced autofocus attempts during a capture sequence. 60 false If set, Ekos will capture a few flat images to determine the optimal exposure time to achieve the desired ADU value. 0 Maximum difference between measured and target ADU values to deem the value as acceptable. 1000 0 0 0 0 0.1 0 false false 2.5 true false 1 30 true !KSUtils::isHardwareLimited() !KSUtils::isHardwareLimited() 0 KSUtils::getDefaultPath("fitsDir") 60 false false false Step size of the absolute focuser. The step size TICKS should be adjusted so that when the focuser moves TICKS steps, the difference in HFR is more than 0.1 pixels. Lower the value when you are close to optimal focus. 100 Wait for this many seconds after moving the focuser before capturing the next image during AutoFocus. 0 Wait for this many seconds after resuming guide. 0 The tolerance specifies the percentage difference between the current focusing position and the minimum obtained during the focusing run. Adjustment of this value is necessary to prevent the focusing algorithm from oscillating back and forth. 1 Set the maximum travel distance of an absolute focuser. 10000 Specifies gain value of CCD when performing focusing if supported by camera. 0 Set box size to select a focus star. 64 Set horizontal binning of CCD camera while in focus mode. 1 Set vertical binning of CCD camera while in focus mode. 1 true false During full field focusing, stars which are inside this percentage of the frame are filtered out of HFR calculation (default 0%). Detection algorithms may also have an inherent filter. 0.0 During full field focusing, stars which are outside this percentage of the frame are filtered out of HFR calculation (default 100%). Detection algorithms may also have an inherent filter. 100.0 false true false 0 150 0 0 1 Specifies exposure value of CCD in seconds when performing plate solving. 1 Set binning index of CCD camera while in alignment mode. Default values 0-3 corresponding to 1x1 to 4x4 binning. 4 is max binning. 4 Use rotator when performing load and slew. false Threshold between measured and FITS position angles in arcminutes to consider the load and slew operation successful. 30 0 0 1 true false false 30 0 false 1500 false true true true true 1 true 2 true true true 30 false Path to astrometry.net solver location. KSUtils::getDefaultPath("AstrometrySolverBinary") false Path to astrometry.net wcsinfo location. KSUtils::getDefaultPath("AstrometryWCSInfo") false Path to astrometry.net file location. KSUtils::getDefaultPath("AstrometryConfFile") true true Folder in which the desired python executable or link to be used for astrometry.net resides. /usr/local/opt/python/libexec/bin Key to access astrometry.net online web services. You must register with astrometry.net to obtain a key. iczikaqstszeptgs http://nova.astrometry.net true 180 -1 true 1.0 0 0 localhost 4400 localhost 5656 0 1000 2 false 1 false false false 3 60 10 true false false false 2 1 0 1 45 10 500 false false false 2 true true true true true true 133.33 133.33 0 0 0 0 5000 5000 100 100 0.5 2 true true false false Log Ekos Scheduler Module activity. false Sort scheduler jobs by priority and altitude. true true false false false false true false 2 true 5 30 3 + + + 0 + + + + 0 + 0 0 0 0 0 0 1 0 false 7624 8624 300 1000 None false false Toggle whether the HIPS sources are drawn in the sky map. false true true false false true 600 true true true 30 true true true diff --git a/kstars/kstarsactions.cpp b/kstars/kstarsactions.cpp index f0bbc2805..21c7febd9 100644 --- a/kstars/kstarsactions.cpp +++ b/kstars/kstarsactions.cpp @@ -1,1928 +1,1935 @@ /*************************************************************************** kstarsactions.cpp - K Desktop Planetarium ------------------- begin : Mon Feb 25 2002 copyright : (C) 2002 by Jason Harris email : jharris@30doradus.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ // This file contains function definitions for Actions declared in kstars.h #include "kstars.h" #include "imageexporter.h" #include "kstarsdata.h" #include "kstars_debug.h" #include "ksnotification.h" #include "kswizard.h" #include "Options.h" #include "skymap.h" #include "dialogs/exportimagedialog.h" #include "dialogs/finddialog.h" #include "dialogs/focusdialog.h" #include "dialogs/fovdialog.h" #include "dialogs/locationdialog.h" #include "dialogs/timedialog.h" #include "oal/execute.h" #include "oal/equipmentwriter.h" #include "oal/observeradd.h" #include "options/opsadvanced.h" #include "options/opscatalog.h" #include "options/opscolors.h" #include "options/opsguides.h" #include "options/opssatellites.h" #include "options/opssolarsystem.h" #include "options/opssupernovae.h" #include "printing/printingwizard.h" #include "projections/projector.h" #include "skycomponents/asteroidscomponent.h" #include "skycomponents/cometscomponent.h" #include "skycomponents/satellitescomponent.h" #include "skycomponents/skymapcomposite.h" #include "skycomponents/solarsystemcomposite.h" #include "skycomponents/supernovaecomponent.h" #include "tools/adddeepskyobject.h" #include "tools/altvstime.h" #include "tools/astrocalc.h" #include "tools/eyepiecefield.h" #include "tools/flagmanager.h" #include "tools/horizonmanager.h" #include "tools/observinglist.h" #include "tools/planetviewer.h" #include "tools/scriptbuilder.h" #include "tools/skycalendar.h" #include "tools/wutdialog.h" #include "tools/polarishourangle.h" #include "tools/whatsinteresting/wiequipsettings.h" #include "tools/whatsinteresting/wilpsettings.h" #include "tools/whatsinteresting/wiview.h" #include "hips/hipsmanager.h" #ifdef HAVE_INDI #include //#include "indi/telescopewizardprocess.h" #include "indi/opsindi.h" #include "indi/drivermanager.h" #include "indi/guimanager.h" #include "indi/indilistener.h" #endif #ifdef HAVE_CFITSIO #include "fitsviewer/fitsviewer.h" #include "fitsviewer/opsfits.h" #ifdef HAVE_INDI #include "ekos/manager.h" #include "ekos/opsekos.h" #endif #endif #include "xplanet/opsxplanet.h" #ifdef HAVE_NOTIFYCONFIG #include #endif #include #include #include #include #include #include #include #ifdef _WIN32 #include #undef interface #endif #include /** ViewToolBar Action. All of the viewToolBar buttons are connected to this slot. **/ void KStars::slotViewToolBar() { KToggleAction *a = (KToggleAction *)sender(); KConfigDialog *kcd = KConfigDialog::exists("settings"); if (a == actionCollection()->action("show_stars")) { Options::setShowStars(a->isChecked()); if (kcd) { opcatalog->kcfg_ShowStars->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_deepsky")) { Options::setShowDeepSky(a->isChecked()); if (kcd) { opcatalog->kcfg_ShowDeepSky->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_planets")) { Options::setShowSolarSystem(a->isChecked()); if (kcd) { opsolsys->kcfg_ShowSolarSystem->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_clines")) { Options::setShowCLines(a->isChecked()); if (kcd) { opguides->kcfg_ShowCLines->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_cnames")) { Options::setShowCNames(a->isChecked()); if (kcd) { opguides->kcfg_ShowCNames->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_cbounds")) { Options::setShowCBounds(a->isChecked()); if (kcd) { opguides->kcfg_ShowCBounds->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_constellationart")) { Options::setShowConstellationArt(a->isChecked()); if (kcd) { opguides->kcfg_ShowConstellationArt->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_mw")) { Options::setShowMilkyWay(a->isChecked()); if (kcd) { opguides->kcfg_ShowMilkyWay->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_equatorial_grid")) { // if autoSelectGrid is selected and the user clicked the // show_equatorial_grid button, he probably wants us to disable // the autoSelectGrid and display the equatorial grid. Options::setAutoSelectGrid(false); Options::setShowEquatorialGrid(a->isChecked()); if (kcd) { opguides->kcfg_ShowEquatorialGrid->setChecked(a->isChecked()); opguides->kcfg_AutoSelectGrid->setChecked(false); } } else if (a == actionCollection()->action("show_horizontal_grid")) { Options::setAutoSelectGrid(false); Options::setShowHorizontalGrid(a->isChecked()); if (kcd) { opguides->kcfg_ShowHorizontalGrid->setChecked(a->isChecked()); opguides->kcfg_AutoSelectGrid->setChecked(false); } } else if (a == actionCollection()->action("show_horizon")) { Options::setShowGround(a->isChecked()); if (!a->isChecked() && Options::useRefraction()) { QString caption = i18n("Refraction effects disabled"); QString message = i18n("When the horizon is switched off, refraction effects are temporarily disabled."); KMessageBox::information(this, message, caption, "dag_refract_hide_ground"); } if (kcd) { opguides->kcfg_ShowGround->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_flags")) { Options::setShowFlags(a->isChecked()); if (kcd) { opguides->kcfg_ShowFlags->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_satellites")) { Options::setShowSatellites(a->isChecked()); if (kcd) { opssatellites->kcfg_ShowSatellites->setChecked(a->isChecked()); } } else if (a == actionCollection()->action("show_supernovae")) { Options::setShowSupernovae(a->isChecked()); if (kcd) { opssupernovae->kcfg_ShowSupernovae->setChecked(a->isChecked()); } } // update time for all objects because they might be not initialized // it's needed when using horizontal coordinates data()->setFullTimeUpdate(); updateTime(); map()->forceUpdate(); } void KStars::slotINDIToolBar() { #ifdef HAVE_INDI KToggleAction *a = qobject_cast(sender()); if (a == actionCollection()->action("show_control_panel")) { if (a->isChecked()) { GUIManager::Instance()->raise(); GUIManager::Instance()->activateWindow(); GUIManager::Instance()->showNormal(); } else GUIManager::Instance()->hide(); } else if (a == actionCollection()->action("show_ekos")) { if (a->isChecked()) { ekosManager()->raise(); ekosManager()->activateWindow(); ekosManager()->showNormal(); } else ekosManager()->hide(); } else if (a == actionCollection()->action("lock_telescope")) { if (INDIListener::Instance()->size() == 0) { KSNotification::sorry(i18n("KStars did not find any active telescopes.")); return; } ISD::GDInterface *oneScope = nullptr; foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice *bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; if (bd->isConnected() == false) { KMessageBox::error( nullptr, i18n("Telescope %1 is offline. Please connect and retry again.", gd->getDeviceName())); return; } oneScope = gd; break; } if (oneScope == nullptr) { KSNotification::sorry(i18n("KStars did not find any active telescopes.")); return; } if (a->isChecked()) oneScope->runCommand(INDI_CENTER_LOCK); else oneScope->runCommand(INDI_CENTER_UNLOCK); } else if (a == actionCollection()->action("show_fits_viewer")) { if (m_FITSViewers.isEmpty()) { a->setEnabled(false); return; } if (a->isChecked()) { for (QPointer view : m_FITSViewers) { if (view->getTabs().empty() == false) { view->raise(); view->activateWindow(); view->showNormal(); } } } else { for (QPointer view : m_FITSViewers) { view->hide(); } } } else if (a == actionCollection()->action("show_mount_box")) { ekosManager()->mountModule()->toggleMountToolBox(); } else if (a == actionCollection()->action("show_sensor_fov")) { Options::setShowSensorFOV(a->isChecked()); } #endif } void KStars::slotSetTelescopeEnabled(bool enable) { telescopeGroup->setEnabled(enable); if (enable == false) { for(QAction *a : telescopeGroup->actions()) { a->setChecked(false); } } } void KStars::slotSetDomeEnabled(bool enable) { domeGroup->setEnabled(enable); if (enable == false) { for(QAction *a : domeGroup->actions()) { a->setChecked(false); } } } /** Major Dialog Window Actions **/ void KStars::slotCalculator() { if (!m_AstroCalc) m_AstroCalc = new AstroCalc(this); m_AstroCalc->show(); } void KStars::slotWizard() { QPointer wizard = new KSWizard(this); if (wizard->exec() == QDialog::Accepted) { Options::setRunStartupWizard(false); //don't run on startup next time updateLocationFromWizard(*(wizard->geo())); } delete wizard; } void KStars::updateLocationFromWizard(const GeoLocation &geo) { data()->setLocation(geo); // adjust local time to keep UT the same. // create new LT without DST offset KStarsDateTime ltime = data()->geo()->UTtoLT(data()->ut()); // reset timezonerule to compute next dst change data()->geo()->tzrule()->reset_with_ltime(ltime, data()->geo()->TZ0(), data()->isTimeRunningForward()); // reset next dst change time data()->setNextDSTChange(data()->geo()->tzrule()->nextDSTChange()); // reset local sideral time data()->syncLST(); // Make sure Numbers, Moon, planets, and sky objects are updated immediately data()->setFullTimeUpdate(); // If the sky is in Horizontal mode and not tracking, reset focus such that // Alt/Az remain constant. if (!Options::isTracking() && Options::useAltAz()) { map()->focus()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } // recalculate new times and objects data()->setSnapNextFocus(); updateTime(); } void KStars::slotDownload() { // 2017-07-04: Explicitly load kstars.knsrc from resources file QPointer dlg(new KNS3::DownloadDialog(":/kconfig/kstars.knsrc", this)); dlg->exec(); // Get the list of all the installed entries. KNS3::Entry::List installed_entries; KNS3::Entry::List changed_entries; if (dlg) { installed_entries = dlg->installedEntries(); changed_entries = dlg->changedEntries(); } delete dlg; foreach (const KNS3::Entry &entry, installed_entries) { foreach (const QString &name, entry.installedFiles()) { if (name.endsWith(QLatin1String(".cat"))) { data()->catalogdb()->AddCatalogContents(name); // To start displaying the custom catalog, add it to SkyMapComposite QString catalogName = data()->catalogdb()->GetCatalogName(name); Options::setShowCatalogNames(Options::showCatalogNames() << catalogName); Options::setCatalogFile(Options::catalogFile() << name); Options::setShowCatalog(Options::showCatalog() << 1); } } KStars::Instance()->data()->skyComposite()->reloadDeepSky(); // update time for all objects because they might be not initialized // it's needed when using horizontal coordinates KStars::Instance()->data()->setFullTimeUpdate(); KStars::Instance()->updateTime(); KStars::Instance()->map()->forceUpdate(); } foreach (const KNS3::Entry &entry, changed_entries) { foreach (const QString &name, entry.uninstalledFiles()) { if (name.endsWith(QLatin1String(".cat"))) { data()->catalogdb()->RemoveCatalog(name); // To start displaying the custom catalog, add it to SkyMapComposite QStringList catFile = Options::catalogFile(); catFile.removeOne(name); Options::setCatalogFile(catFile); } } } } void KStars::slotAVT() { if (!m_AltVsTime) m_AltVsTime = new AltVsTime(this); m_AltVsTime->show(); } void KStars::slotWUT() { if (!m_WUTDialog) m_WUTDialog = new WUTDialog(this); m_WUTDialog->show(); } //FIXME Port to QML2 //#if 0 void KStars::slotWISettings() { if (!m_WIView) slotToggleWIView(); if (m_WIView && !m_wiDock->isVisible()) slotToggleWIView(); if (KConfigDialog::showDialog("wisettings")) { m_WIEquipmentSettings->populateScopeListWidget(); return; } KConfigDialog *dialog = new KConfigDialog(this, "wisettings", Options::self()); connect(dialog, SIGNAL(settingsChanged(QString)), this, SLOT(slotApplyWIConfigChanges())); m_WISettings = new WILPSettings(this); m_WIEquipmentSettings = new WIEquipSettings(); dialog->addPage(m_WISettings, i18n("Light Pollution Settings")); dialog->addPage(m_WIEquipmentSettings, i18n("Equipment Settings - Equipment Type and Parameters")); dialog->exec(); if (m_WIEquipmentSettings) m_WIEquipmentSettings->setAperture(); //Something isn't working with this! } void KStars::slotToggleWIView() { if (KStars::Closing) return; if (!m_WIView) { m_WIView = new WIView(nullptr); m_wiDock = new QDockWidget(this); m_wiDock->setStyleSheet("QDockWidget::title{background-color:black;}"); m_wiDock->setObjectName("What's Interesting"); m_wiDock->setAllowedAreas(Qt::RightDockWidgetArea); QWidget *container = QWidget::createWindowContainer(m_WIView->getWIBaseView()); m_wiDock->setWidget(container); m_wiDock->setMinimumWidth(400); addDockWidget(Qt::RightDockWidgetArea, m_wiDock); connect(m_wiDock, SIGNAL(visibilityChanged(bool)), actionCollection()->action("show_whatsinteresting"), SLOT(setChecked(bool))); m_wiDock->setVisible(true); } else { m_wiDock->setVisible(!m_wiDock->isVisible()); } } void KStars::slotCalendar() { if (!m_SkyCalendar) m_SkyCalendar = new SkyCalendar(this); m_SkyCalendar->show(); } void KStars::slotGlossary() { // GlossaryDialog *dlg = new GlossaryDialog( this, true ); // QString glossaryfile =data()->stdDirs->findResource( "data", "kstars/glossary.xml" ); // QUrl u = glossaryfile; // Glossary *g = new Glossary( u ); // g->setName( i18n( "Knowledge" ) ); // dlg->addGlossary( g ); // dlg->show(); } void KStars::slotScriptBuilder() { if (!m_ScriptBuilder) m_ScriptBuilder = new ScriptBuilder(this); m_ScriptBuilder->show(); } void KStars::slotSolarSystem() { if (!m_PlanetViewer) m_PlanetViewer = new PlanetViewer(this); m_PlanetViewer->show(); } /* void KStars::slotJMoonTool() { if ( ! m_JMoonTool ) m_JMoonTool = new JMoonTool(this); m_JMoonTool->show(); } */ void KStars::slotMoonPhaseTool() { //FIXME Port to KF5 //if( ! mpt ) mpt = new MoonPhaseTool( this ); //mpt->show(); } void KStars::slotFlagManager() { if (!m_FlagManager) m_FlagManager = new FlagManager(this); m_FlagManager->show(); } #if 0 void KStars::slotTelescopeWizard() { #ifdef HAVE_INDI #ifndef Q_OS_WIN QString indiServerDir = Options::indiServer(); #ifdef Q_OS_OSX if (Options::indiServerIsInternal()) indiServerDir = QCoreApplication::applicationDirPath() + "/indi"; else indiServerDir = QFileInfo(Options::indiServer()).dir().path(); #endif QStringList paths; paths << "/usr/bin" << "/usr/local/bin" << indiServerDir; if (QStandardPaths::findExecutable("indiserver").isEmpty()) { if (QStandardPaths::findExecutable("indiserver", paths).isEmpty()) { KSNotification::error(i18n("Unable to find INDI server. Please make sure the package that provides " "the 'indiserver' binary is installed.")); return; } } #endif QPointer twiz = new telescopeWizardProcess(this); twiz->exec(); delete twiz; #endif } #endif void KStars::slotINDIPanel() { #ifdef HAVE_INDI #ifndef Q_OS_WIN QString indiServerDir = Options::indiServer(); #ifdef Q_OS_OSX if (Options::indiServerIsInternal()) indiServerDir = QCoreApplication::applicationDirPath() + "/indi"; else indiServerDir = QFileInfo(Options::indiServer()).dir().path(); #endif QStringList paths; paths << "/usr/bin" << "/usr/local/bin" << indiServerDir; if (QStandardPaths::findExecutable("indiserver").isEmpty()) { if (QStandardPaths::findExecutable("indiserver", paths).isEmpty()) { KSNotification::error(i18n("Unable to find INDI server. Please make sure the package that provides " "the 'indiserver' binary is installed.")); return; } } #endif GUIManager::Instance()->updateStatus(true); #endif } void KStars::slotINDIDriver() { #ifdef HAVE_INDI #ifndef Q_OS_WIN if (KMessageBox::warningContinueCancel( nullptr, i18n("INDI Device Manager should only be used by advanced technical users. It cannot be used with Ekos. Do you still want to open INDI device manager?"), i18n("INDI Device Manager"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "indi_device_manager_warning") == KMessageBox::Cancel) return; QString indiServerDir = Options::indiServer(); #ifdef Q_OS_OSX if (Options::indiServerIsInternal()) indiServerDir = QCoreApplication::applicationDirPath() + "/indi"; else indiServerDir = QFileInfo(Options::indiServer()).dir().path(); #endif QStringList paths; paths << "/usr/bin" << "/usr/local/bin" << indiServerDir; if (QStandardPaths::findExecutable("indiserver").isEmpty()) { if (QStandardPaths::findExecutable("indiserver", paths).isEmpty()) { KSNotification::error(i18n("Unable to find INDI server. Please make sure the package that provides " "the 'indiserver' binary is installed.")); return; } } #endif DriverManager::Instance()->raise(); DriverManager::Instance()->activateWindow(); DriverManager::Instance()->showNormal(); #endif } void KStars::slotEkos() { #ifdef HAVE_CFITSIO #ifdef HAVE_INDI #ifndef Q_OS_WIN QString indiServerDir = Options::indiServer(); #ifdef Q_OS_OSX if (Options::indiServerIsInternal()) indiServerDir = QCoreApplication::applicationDirPath() + "/indi"; else indiServerDir = QFileInfo(Options::indiServer()).dir().path(); #endif QStringList paths; paths << "/usr/bin" << "/usr/local/bin" << indiServerDir; if (QStandardPaths::findExecutable("indiserver").isEmpty()) { if (QStandardPaths::findExecutable("indiserver", paths).isEmpty()) { KSNotification::error(i18n("Unable to find INDI server. Please make sure the package that provides " "the 'indiserver' binary is installed.")); return; } } #endif if (ekosManager()->isVisible() && ekosManager()->isActiveWindow()) { ekosManager()->hide(); } else { ekosManager()->raise(); ekosManager()->activateWindow(); ekosManager()->showNormal(); } #endif #endif } void KStars::slotINDITelescopeTrack() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected()) { KToggleAction *a = qobject_cast(sender()); if (a != nullptr) { telescope->setTrackEnabled(a->isChecked()); return; } } } #endif } void KStars::slotINDITelescopeSlew(bool focused_object) { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected()) { if (focused_object) { if (m_SkyMap->focusObject() != nullptr) telescope->Slew(m_SkyMap->focusObject()); } else telescope->Slew(m_SkyMap->mousePoint()); return; } } #else Q_UNUSED(focused_object) #endif } void KStars::slotINDITelescopeSlewMousePointer() { #ifdef HAVE_INDI slotINDITelescopeSlew(false); #endif } void KStars::slotINDITelescopeSync(bool focused_object) { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected() && telescope->canSync()) { if (focused_object) { if (m_SkyMap->focusObject() != nullptr) telescope->Sync(m_SkyMap->focusObject()); } else telescope->Sync(m_SkyMap->mousePoint()); return; } } #else Q_UNUSED(focused_object) #endif } void KStars::slotINDITelescopeSyncMousePointer() { #ifdef HAVE_INDI slotINDITelescopeSync(false); #endif } void KStars::slotINDITelescopeAbort() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected()) { telescope->Abort(); return; } } #endif } void KStars::slotINDITelescopePark() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected() && telescope->canPark()) { telescope->Park(); return; } } #endif } void KStars::slotINDITelescopeUnpark() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Telescope* telescope = dynamic_cast(gd); if (telescope != nullptr && telescope->isConnected() && telescope->canPark()) { telescope->UnPark(); return; } } #endif } void KStars::slotINDIDomePark() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Dome* dome = dynamic_cast(gd); if (dome != nullptr && dome->isConnected() && dome->canPark()) { dome->Park(); return; } } #endif } void KStars::slotINDIDomeUnpark() { #ifdef HAVE_INDI if (m_KStarsData == nullptr || INDIListener::Instance() == nullptr) return; for (auto *gd : INDIListener::Instance()->getDevices()) { ISD::Dome* dome = dynamic_cast(gd); if (dome != nullptr && dome->isConnected() && dome->canPark()) { dome->UnPark(); return; } } #endif } void KStars::slotGeoLocator() { QPointer locationdialog = new LocationDialog(this); if (locationdialog->exec() == QDialog::Accepted) { GeoLocation *newLocation = locationdialog->selectedCity(); if (newLocation) { // set new location in options data()->setLocation(*newLocation); // adjust local time to keep UT the same. // create new LT without DST offset KStarsDateTime ltime = newLocation->UTtoLT(data()->ut()); // reset timezonerule to compute next dst change newLocation->tzrule()->reset_with_ltime(ltime, newLocation->TZ0(), data()->isTimeRunningForward()); // reset next dst change time data()->setNextDSTChange(newLocation->tzrule()->nextDSTChange()); // reset local sideral time data()->syncLST(); // Make sure Numbers, Moon, planets, and sky objects are updated immediately data()->setFullTimeUpdate(); // If the sky is in Horizontal mode and not tracking, reset focus such that // Alt/Az remain constant. if (!Options::isTracking() && Options::useAltAz()) { map()->focus()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } // recalculate new times and objects data()->setSnapNextFocus(); updateTime(); } } delete locationdialog; } void KStars::slotViewOps() { - //An instance of your dialog could be already created and could be cached, - //in which case you want to display the cached dialog instead of creating - //another one - if (KConfigDialog::showDialog("settings")) - return; + // An instance of your dialog could be already created and could be cached, + // in which case you want to display the cached dialog instead of creating + // another one + prepareOps()->show(); +} + +KConfigDialog* KStars::prepareOps() +{ + KConfigDialog *dialog = KConfigDialog::exists("settings"); + if (nullptr != dialog) + return dialog; + + // KConfigDialog didn't find an instance of this dialog, so lets create it : + dialog = new KConfigDialog(this, "settings", Options::self()); - //KConfigDialog didn't find an instance of this dialog, so lets create it : - KConfigDialog *dialog = new KConfigDialog(this, "settings", Options::self()); // For some reason the dialog does not resize to contents // so we set initial 'resonable' size here. Any better way to do this? dialog->resize(800, 600); #ifdef Q_OS_OSX dialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); #endif connect(dialog, SIGNAL(settingsChanged(QString)), this, SLOT(slotApplyConfigChanges())); opcatalog = new OpsCatalog(); opguides = new OpsGuides(); opsolsys = new OpsSolarSystem(); opssatellites = new OpsSatellites(); opssupernovae = new OpsSupernovae(); opcolors = new OpsColors(); opadvanced = new OpsAdvanced(); KPageWidgetItem *page; page = dialog->addPage(opcatalog, i18n("Catalogs"), "kstars_catalog"); page->setIcon(QIcon::fromTheme("kstars_catalog")); page = dialog->addPage(opsolsys, i18n("Solar System"), "kstars_solarsystem"); page->setIcon(QIcon::fromTheme("kstars_solarsystem")); page = dialog->addPage(opssatellites, i18n("Satellites"), "kstars_satellites"); page->setIcon(QIcon::fromTheme("kstars_satellites")); page = dialog->addPage(opssupernovae, i18n("Supernovae"), "kstars_supernovae"); page->setIcon(QIcon::fromTheme("kstars_supernovae")); page = dialog->addPage(opguides, i18n("Guides"), "kstars_guides"); page->setIcon(QIcon::fromTheme("kstars_guides")); page = dialog->addPage(opcolors, i18n("Colors"), "kstars_colors"); page->setIcon(QIcon::fromTheme("kstars_colors")); #ifdef HAVE_CFITSIO opsfits = new OpsFITS(); page = dialog->addPage(opsfits, i18n("FITS"), "kstars_fitsviewer"); page->setIcon(QIcon::fromTheme("kstars_fitsviewer")); #endif #ifdef HAVE_INDI opsindi = new OpsINDI(); page = dialog->addPage(opsindi, i18n("INDI"), "kstars_indi"); page->setIcon(QIcon::fromTheme("kstars_indi")); #ifdef HAVE_CFITSIO opsekos = new OpsEkos(); KPageWidgetItem *ekosOption = dialog->addPage(opsekos, i18n("Ekos"), "kstars_ekos"); ekosOption->setIcon(QIcon::fromTheme("kstars_ekos")); if (m_EkosManager) m_EkosManager->setOptionsWidget(ekosOption); #endif #endif opsxplanet = new OpsXplanet(this); page = dialog->addPage(opsxplanet, i18n("Xplanet"), "kstars_xplanet"); page->setIcon(QIcon::fromTheme("kstars_xplanet")); page = dialog->addPage(opadvanced, i18n("Advanced"), "kstars_advanced"); page->setIcon(QIcon::fromTheme("kstars_advanced")); - dialog->show(); + return dialog; } void KStars::slotApplyConfigChanges() { Options::self()->save(); applyConfig(); //data()->setFullTimeUpdate(); //map()->forceUpdate(); } void KStars::slotApplyWIConfigChanges() { Options::self()->save(); applyConfig(); m_WIView->updateObservingConditions(); m_WIView->onReloadIconClicked(); } void KStars::slotSetTime() { QPointer timedialog = new TimeDialog(data()->lt(), data()->geo(), this); if (timedialog->exec() == QDialog::Accepted) { data()->changeDateTime(data()->geo()->LTtoUT(timedialog->selectedDateTime())); if (Options::useAltAz()) { if (map()->focusObject()) { map()->focusObject()->EquatorialToHorizontal(data()->lst(), data()->geo()->lat()); map()->setFocus(map()->focusObject()); } else map()->focus()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } map()->forceUpdateNow(); //If focusObject has a Planet Trail, clear it and start anew. KSPlanetBase *planet = dynamic_cast(map()->focusObject()); if (planet && planet->hasTrail()) { planet->clearTrail(); planet->addToTrail(); } } delete timedialog; } //Set Time to CPU clock void KStars::slotSetTimeToNow() { data()->changeDateTime(KStarsDateTime::currentDateTimeUtc()); if (Options::useAltAz()) { if (map()->focusObject()) { map()->focusObject()->EquatorialToHorizontal(data()->lst(), data()->geo()->lat()); map()->setFocus(map()->focusObject()); } else map()->focus()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } map()->forceUpdateNow(); //If focusObject has a Planet Trail, clear it and start anew. KSPlanetBase *planet = dynamic_cast(map()->focusObject()); if (planet && planet->hasTrail()) { planet->clearTrail(); planet->addToTrail(); } } void KStars::slotFind() { //clearCachedFindDialog(); SkyObject *targetObject = nullptr; if (FindDialog::Instance()->exec() == QDialog::Accepted && (targetObject = FindDialog::Instance()->targetObject())) { map()->setClickedObject(targetObject); map()->setClickedPoint(map()->clickedObject()); map()->slotCenter(); } // check if data has changed while dialog was open //if (DialogIsObsolete) // clearCachedFindDialog(); } void KStars::slotOpenFITS() { #ifdef HAVE_CFITSIO static QUrl path = QUrl::fromLocalFile(QDir::homePath()); QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open FITS"), path, "FITS (*.fits *.fits.fz *.fit *.fts)"); if (fileURL.isEmpty()) return; // Remember last directory path.setUrl(fileURL.url(QUrl::RemoveFilename)); QPointer fv = new FITSViewer((Options::independentWindowFITS()) ? nullptr : this); connect(fv, &FITSViewer::loaded, [ &, fv]() { addFITSViewer(fv); fv->show(); }); fv->addFITS(fileURL, FITS_NORMAL, FITS_NONE, QString(), false); #endif } void KStars::slotExportImage() { //TODO Check this //For remote files, this returns //QFileInfo::absolutePath: QFileInfo::absolutePath: Constructed with empty filename //As of 2014-07-19 //QUrl fileURL = KFileDialog::getSaveUrl( QDir::homePath(), "image/png image/jpeg image/gif image/x-portable-pixmap image/bmp image/svg+xml" ); QUrl fileURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Export Image"), QUrl(), "Images (*.png *.jpeg *.gif *.bmp *.svg)"); //User cancelled file selection dialog - abort image export if (fileURL.isEmpty()) { return; } //Warn user if file exists! if (QFile::exists(fileURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel( parentWidget(), i18n("A file named \"%1\" already exists. Overwrite it?", fileURL.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite()); if (r == KMessageBox::Cancel) return; } // execute image export dialog // Note: We don't let ExportImageDialog create its own ImageExporter because we want legend settings etc to be remembered between UI use and DBus scripting interface use. //if ( !m_ImageExporter ) //m_ImageExporter = new ImageExporter( this ); if (!m_ExportImageDialog) { m_ExportImageDialog = new ExportImageDialog(fileURL.toLocalFile(), QSize(map()->width(), map()->height()), KStarsData::Instance()->imageExporter()); } else { m_ExportImageDialog->setOutputUrl(fileURL.toLocalFile()); m_ExportImageDialog->setOutputSize(QSize(map()->width(), map()->height())); } m_ExportImageDialog->show(); } void KStars::slotRunScript() { QUrl fileURL = QFileDialog::getOpenFileUrl( KStars::Instance(), QString(), QUrl(QDir::homePath()), "*.kstars|" + i18nc("Filter by file type: KStars Scripts.", "KStars Scripts (*.kstars)")); QFile f; //QString fname; if (fileURL.isValid()) { if (fileURL.isLocalFile() == false) { KSNotification::sorry(i18n("Executing remote scripts is not supported.")); return; } f.setFileName(fileURL.toLocalFile()); if (!f.open(QIODevice::ReadOnly)) { QString message = i18n("Could not open file %1", f.fileName()); KSNotification::sorry(message, i18n("Could Not Open File")); return; } QTextStream istream(&f); QString line; bool fileOK(true); while (!istream.atEnd()) { line = istream.readLine(); if (line.at(0) != '#' && line.left(9) != "dbus-send") { fileOK = false; break; } } if (!fileOK) { int answer; answer = KMessageBox::warningContinueCancel( nullptr, i18n("The selected script contains unrecognized elements, " "indicating that it was not created using the KStars script builder. " "This script may not function properly, and it may even contain malicious code. " "Would you like to execute it anyway?"), i18n("Script Validation Failed"), KGuiItem(i18n("Run Nevertheless")), KStandardGuiItem::cancel(), "daExecuteScript"); if (answer == KMessageBox::Cancel) return; } //Add statusbar message that script is running statusBar()->showMessage(i18n("Running script: %1", fileURL.fileName())); // 2017-09-19: Jasem // FIXME This is a hack and does not work on non-Linux systems // The Script Builder should generate files that can run cross-platform QProcess p; p.start(f.fileName()); if (!p.waitForStarted()) return; while (!p.waitForFinished(10)) { qApp->processEvents(); //otherwise tempfile may get deleted before script completes. if (p.state() != QProcess::Running) break; } statusBar()->showMessage(i18n("Script finished."), 0); } } void KStars::slotPrint() { bool switchColors(false); //Suggest Chart color scheme if (data()->colorScheme()->colorNamed("SkyColor") != QColor(255, 255, 255)) { QString message = i18n("You can save printer ink by using the \"Star Chart\" " "color scheme, which uses a white background. Would you like to " "temporarily switch to the Star Chart color scheme for printing?"); int answer = KMessageBox::questionYesNoCancel( nullptr, message, i18n("Switch to Star Chart Colors?"), KGuiItem(i18n("Switch Color Scheme")), KGuiItem(i18n("Do Not Switch")), KStandardGuiItem::cancel(), "askAgainPrintColors"); if (answer == KMessageBox::Cancel) return; if (answer == KMessageBox::Yes) switchColors = true; } printImage(true, switchColors); } void KStars::slotPrintingWizard() { if (m_PrintingWizard) { delete m_PrintingWizard; } m_PrintingWizard = new PrintingWizard(this); m_PrintingWizard->show(); } void KStars::slotToggleTimer() { if (data()->clock()->isActive()) { data()->clock()->stop(); updateTime(); } else { if (fabs(data()->clock()->scale()) > Options::slewTimeScale()) data()->clock()->setManualMode(true); data()->clock()->start(); if (data()->clock()->isManualMode()) map()->forceUpdate(); } // Update clock state in options Options::setRunClock(data()->clock()->isActive()); } void KStars::slotStepForward() { if (data()->clock()->isActive()) data()->clock()->stop(); data()->clock()->manualTick(true); map()->forceUpdate(); } void KStars::slotStepBackward() { if (data()->clock()->isActive()) data()->clock()->stop(); data()->clock()->manualTick(true, true); map()->forceUpdate(); } //Pointing void KStars::slotPointFocus() { // In the following cases, we set slewing=true in order to disengage tracking map()->stopTracking(); if (sender() == actionCollection()->action("zenith")) map()->setDestinationAltAz(dms(90.0), map()->focus()->az()); else if (sender() == actionCollection()->action("north")) map()->setDestinationAltAz(dms(15.0), dms(0.0001)); else if (sender() == actionCollection()->action("east")) map()->setDestinationAltAz(dms(15.0), dms(90.0)); else if (sender() == actionCollection()->action("south")) map()->setDestinationAltAz(dms(15.0), dms(180.0)); else if (sender() == actionCollection()->action("west")) map()->setDestinationAltAz(dms(15.0), dms(270.0)); } void KStars::slotTrack() { if (Options::isTracking()) { Options::setIsTracking(false); actionCollection()->action("track_object")->setText(i18n("Engage &Tracking")); actionCollection() ->action("track_object") ->setIcon(QIcon::fromTheme("document-decrypt")); KSPlanetBase *planet = dynamic_cast(map()->focusObject()); if (planet && data()->temporaryTrail) { planet->clearTrail(); data()->temporaryTrail = false; } map()->setClickedObject(nullptr); map()->setFocusObject(nullptr); //no longer tracking focusObject map()->setFocusPoint(nullptr); } else { map()->setClickedPoint(map()->focus()); map()->setClickedObject(nullptr); map()->setFocusObject(nullptr); //no longer tracking focusObject map()->setFocusPoint(map()->clickedPoint()); Options::setIsTracking(true); actionCollection()->action("track_object")->setText(i18n("Stop &Tracking")); actionCollection() ->action("track_object") ->setIcon(QIcon::fromTheme("document-encrypt")); } map()->forceUpdate(); } void KStars::slotManualFocus() { QPointer focusDialog = new FocusDialog(); // JM 2019-09-04: Should default to RA/DE always // if (Options::useAltAz()) // focusDialog->activateAzAltPage(); if (focusDialog->exec() == QDialog::Accepted) { //If the requested position is very near the pole, we need to point first //to an intermediate location just below the pole in order to get the longitudinal //position (RA/Az) right. double realAlt(focusDialog->point()->alt().Degrees()); double realDec(focusDialog->point()->dec().Degrees()); if (Options::useAltAz() && realAlt > 89.0) { focusDialog->point()->setAlt(89.0); focusDialog->point()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } if (!Options::useAltAz() && realDec > 89.0) { focusDialog->point()->setDec(89.0); focusDialog->point()->EquatorialToHorizontal(data()->lst(), data()->geo()->lat()); } map()->setClickedPoint(focusDialog->point()); if (Options::isTracking()) slotTrack(); map()->slotCenter(); //The slew takes some time to complete, and this often causes the final focus point to be slightly //offset from the user's requested coordinates (because EquatorialToHorizontal() is called //throughout the process, which depends on the sidereal time). So we now "polish" the final //position by resetting the final focus to the focusDialog point. // //Also, if the requested position was within 1 degree of the coordinate pole, this will //automatically correct the final pointing from the intermediate offset position to the final position data()->setSnapNextFocus(); if (Options::useAltAz()) { map()->setDestinationAltAz(focusDialog->point()->alt(), focusDialog->point()->az()); } else { map()->setDestination(focusDialog->point()->ra(), focusDialog->point()->dec()); } //Now, if the requested point was near a pole, we need to reset the Alt/Dec of the focus. if (Options::useAltAz() && realAlt > 89.0) map()->focus()->setAlt(realAlt); if (!Options::useAltAz() && realDec > 89.0) map()->focus()->setDec(realAlt); //Don't track if we set Alt/Az coordinates. This way, Alt/Az remain constant. if (focusDialog->usedAltAz()) map()->stopTracking(); } delete focusDialog; } void KStars::slotZoomChanged() { // Enable/disable actions actionCollection()->action("zoom_out")->setEnabled(Options::zoomFactor() > MINZOOM); actionCollection()->action("zoom_in")->setEnabled(Options::zoomFactor() < MAXZOOM); // Update status bar map()->setupProjector(); // this needs to be run redundantly, so that the FOV returned below is up-to-date. float fov = map()->projector()->fov(); KLocalizedString fovi18nstring = ki18nc("approximate field of view", "Approximate FOV: %1 degrees"); if (fov < 1.0) { fov = fov * 60.0; fovi18nstring = ki18nc("approximate field of view", "Approximate FOV: %1 arcminutes"); } if (fov < 1.0) { fov = fov * 60.0; fovi18nstring = ki18nc("approximate field of view", "Approximate FOV: %1 arcseconds"); } QString fovstring = fovi18nstring.subs(QString::number(fov, 'f', 1)).toString(); statusBar()->showMessage(fovstring, 0); } void KStars::slotSetZoom() { bool ok; double currentAngle = map()->width() / (Options::zoomFactor() * dms::DegToRad); double minAngle = map()->width() / (MAXZOOM * dms::DegToRad); double maxAngle = map()->width() / (MINZOOM * dms::DegToRad); double angSize = QInputDialog::getDouble( nullptr, i18nc("The user should enter an angle for the field-of-view of the display", "Enter Desired Field-of-View Angle"), i18n("Enter a field-of-view angle in degrees: "), currentAngle, minAngle, maxAngle, 1, &ok); if (ok) { map()->setZoomFactor(map()->width() / (angSize * dms::DegToRad)); } } void KStars::slotCoordSys() { if (Options::useAltAz()) { Options::setUseAltAz(false); if (Options::useRefraction()) { if (map()->focusObject()) //simply update focus to focusObject's position map()->setFocus(map()->focusObject()); else //need to recompute focus for unrefracted position { map()->setFocusAltAz(SkyPoint::unrefract(map()->focus()->alt()), map()->focus()->az()); map()->focus()->HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); } } actionCollection()->action("coordsys")->setText(i18n("Switch to horizonal view (Horizontal &Coordinates)")); } else { Options::setUseAltAz(true); if (Options::useRefraction()) { map()->setFocusAltAz(map()->focus()->altRefracted(), map()->focus()->az()); } actionCollection()->action("coordsys")->setText(i18n("Switch to star globe view (Equatorial &Coordinates)")); } map()->forceUpdate(); } void KStars::slotMapProjection() { if (sender() == actionCollection()->action("project_lambert")) Options::setProjection(Projector::Lambert); if (sender() == actionCollection()->action("project_azequidistant")) Options::setProjection(Projector::AzimuthalEquidistant); if (sender() == actionCollection()->action("project_orthographic")) Options::setProjection(Projector::Orthographic); if (sender() == actionCollection()->action("project_equirectangular")) Options::setProjection(Projector::Equirectangular); if (sender() == actionCollection()->action("project_stereographic")) Options::setProjection(Projector::Stereographic); if (sender() == actionCollection()->action("project_gnomonic")) Options::setProjection(Projector::Gnomonic); //DEBUG qCDebug(KSTARS) << "Projection system: " << Options::projection(); m_SkyMap->forceUpdate(); } //Settings Menu: void KStars::slotColorScheme() { //use mid(3) to exclude the leading "cs_" prefix from the action name QString filename = QString(sender()->objectName()).mid(3) + ".colors"; loadColorScheme(filename); } void KStars::slotTargetSymbol(bool flag) { qDebug() << QString("slotTargetSymbol: %1 %2").arg(sender()->objectName()).arg(flag); QStringList names = Options::fOVNames(); if (flag) { // Add FOV to list names.append(sender()->objectName()); } else { // Remove FOV from list int ix = names.indexOf(sender()->objectName()); if (ix >= 0) names.removeAt(ix); } Options::setFOVNames(names); // Sync visibleFOVs with fovNames data()->syncFOV(); map()->forceUpdate(); } void KStars::slotHIPSSource() { QAction *selectedAction = qobject_cast(sender()); Q_ASSERT(selectedAction != nullptr); QString selectedSource = selectedAction->text().remove('&'); // selectedSource could be translated, while we need to send only Latin "None" // to Hips manager. if (selectedSource == i18n("None")) HIPSManager::Instance()->setCurrentSource("None"); else HIPSManager::Instance()->setCurrentSource(selectedSource); map()->forceUpdate(); } void KStars::slotFOVEdit() { QPointer fovdlg = new FOVDialog(this); if (fovdlg->exec() == QDialog::Accepted) { FOVManager::save(); repopulateFOV(); } delete fovdlg; } void KStars::slotObsList() { m_KStarsData->observingList()->show(); } void KStars::slotEquipmentWriter() { QPointer equipmentdlg = new EquipmentWriter(); equipmentdlg->loadEquipment(); equipmentdlg->exec(); delete equipmentdlg; } void KStars::slotObserverManager() { QPointer m_observerAdd = new ObserverAdd(); m_observerAdd->exec(); delete m_observerAdd; } void KStars::slotHorizonManager() { if (!m_HorizonManager) { m_HorizonManager = new HorizonManager(this); connect(m_SkyMap, SIGNAL(positionClicked(SkyPoint*)), m_HorizonManager, SLOT(addSkyPoint(SkyPoint*))); } m_HorizonManager->show(); } void KStars::slotEyepieceView(SkyPoint *sp, const QString &imagePath) { if (!m_EyepieceView) m_EyepieceView = new EyepieceField(this); // FIXME: Move FOV choice into the Eyepiece View tool itself. bool ok = true; const FOV *fov = nullptr; if (!data()->getAvailableFOVs().isEmpty()) { // Ask the user to choose from a list of available FOVs. //int index; const FOV *f; QMap nameToFovMap; foreach (f, data()->getAvailableFOVs()) { nameToFovMap.insert(f->name(), f); } nameToFovMap.insert(i18n("Attempt to determine from image"), nullptr); fov = nameToFovMap[QInputDialog::getItem(this, i18n("Eyepiece View: Choose a field-of-view"), i18n("FOV to render eyepiece view for:"), nameToFovMap.uniqueKeys(), 0, false, &ok)]; } if (ok) m_EyepieceView->showEyepieceField(sp, fov, imagePath); } void KStars::slotExecute() { KStarsData::Instance()->executeSession()->init(); KStarsData::Instance()->executeSession()->show(); } void KStars::slotPolarisHourAngle() { QPointer pHourAngle = new PolarisHourAngle(this); pHourAngle->exec(); } //Help Menu void KStars::slotTipOfDay() { KTipDialog::showTip(this, "kstars/tips", true); } // Toggle to and from full screen mode void KStars::slotFullScreen() { if (topLevelWidget()->isFullScreen()) { topLevelWidget()->setWindowState(topLevelWidget()->windowState() & ~Qt::WindowFullScreen); // reset } else { topLevelWidget()->setWindowState(topLevelWidget()->windowState() | Qt::WindowFullScreen); // set } } void KStars::slotClearAllTrails() { //Exclude object with temporary trail SkyObject *exOb(nullptr); if (map()->focusObject() && map()->focusObject()->isSolarSystem() && data()->temporaryTrail) { exOb = map()->focusObject(); } TrailObject::clearTrailsExcept(exOb); map()->forceUpdate(); } //toggle display of GUI Items on/off void KStars::slotShowGUIItem(bool show) { //Toolbars if (sender() == actionCollection()->action("show_statusBar")) { Options::setShowStatusBar(show); statusBar()->setVisible(show); } if (sender() == actionCollection()->action("show_sbAzAlt")) { Options::setShowAltAzField(show); if (!show) AltAzField.hide(); else AltAzField.show(); } if (sender() == actionCollection()->action("show_sbRADec")) { Options::setShowRADecField(show); if (!show) RADecField.hide(); else RADecField.show(); } if (sender() == actionCollection()->action("show_sbJ2000RADec")) { Options::setShowJ2000RADecField(show); if (!show) J2000RADecField.hide(); else J2000RADecField.show(); } } void KStars::addColorMenuItem(const QString &name, const QString &actionName) { KToggleAction *kta = actionCollection()->add(actionName); kta->setText(name); kta->setObjectName(actionName); kta->setActionGroup(cschemeGroup); colorActionMenu->addAction(kta); KConfigGroup cg = KSharedConfig::openConfig()->group("Colors"); if (actionName.mid(3) == cg.readEntry("ColorSchemeFile", "moonless-night.colors").remove(".colors")) { kta->setChecked(true); } connect(kta, SIGNAL(toggled(bool)), this, SLOT(slotColorScheme())); } void KStars::removeColorMenuItem(const QString &actionName) { qCDebug(KSTARS) << "removing " << actionName; colorActionMenu->removeAction(actionCollection()->action(actionName)); } void KStars::slotAboutToQuit() { // Delete skymap. This required to run destructors and save // current state in the option. delete m_SkyMap; //Store Window geometry in Options object Options::setWindowWidth(width()); Options::setWindowHeight(height()); //explicitly save the colorscheme data to the config file data()->colorScheme()->saveToConfig(); //synch the config file with the Config object writeConfig(); //Terminate Child Processes if on OS X #ifdef Q_OS_OSX QProcess *quit = new QProcess(this); quit->start("killall kdeinit5"); quit->waitForFinished(1000); quit->start("killall klauncher"); quit->waitForFinished(1000); quit->start("killall kioslave"); quit->waitForFinished(1000); quit->start("killall kio_http_cache_cleaner"); quit->waitForFinished(1000); delete quit; #endif } void KStars::slotShowPositionBar(SkyPoint *p) { if (Options::showAltAzField()) { dms a = p->alt(); if (Options::useAltAz()) a = p->altRefracted(); QString s = QString("%1, %2").arg(p->az().toDMSString(true), //true: force +/- symbol a.toDMSString(true)); //true: force +/- symbol //statusBar()->changeItem( s, 1 ); AltAzField.setText(s); } if (Options::showRADecField()) { KStarsDateTime lastUpdate; lastUpdate.setDJD(KStarsData::Instance()->updateNum()->getJD()); QString sEpoch = QString::number(lastUpdate.epoch(), 'f', 1); QString s = QString("%1, %2 (J%3)") .arg(p->ra().toHMSString(), p->dec().toDMSString(true), sEpoch); //true: force +/- symbol //statusBar()->changeItem( s, 2 ); RADecField.setText(s); } if (Options::showJ2000RADecField()) { SkyPoint p0; p0 = p->deprecess(KStarsData::Instance()->updateNum()); // deprecess to update RA0/Dec0 from RA/Dec QString s = QString("%1, %2 (J2000)") .arg(p0.ra().toHMSString(), p0.dec().toDMSString(true)); //true: force +/- symbol //statusBar()->changeItem( s, 2 ); J2000RADecField.setText(s); } } void KStars::slotUpdateComets(bool isAutoUpdate) { data()->skyComposite()->solarSystemComposite()->cometsComponent()->updateDataFile(isAutoUpdate); } void KStars::slotUpdateAsteroids(bool isAutoUpdate) { data()->skyComposite()->solarSystemComposite()->asteroidsComponent()->updateDataFile(isAutoUpdate); } void KStars::slotUpdateSupernovae() { data()->skyComposite()->supernovaeComponent()->slotTriggerDataFileUpdate(); } void KStars::slotUpdateSatellites() { data()->skyComposite()->satellites()->updateTLEs(); } void KStars::slotAddDeepSkyObject() { if (!m_addDSODialog) { Q_ASSERT(data() && data()->skyComposite() && data()->skyComposite()->manualAdditionsComponent()); m_addDSODialog = new AddDeepSkyObject(this, data()->skyComposite()->manualAdditionsComponent()); } m_addDSODialog->show(); } void KStars::slotConfigureNotifications() { #ifdef HAVE_NOTIFYCONFIG KNotifyConfigWidget::configure(this); #endif } diff --git a/kstars/kstarsinit.cpp b/kstars/kstarsinit.cpp index 19efc8c28..8c31682d3 100644 --- a/kstars/kstarsinit.cpp +++ b/kstars/kstarsinit.cpp @@ -1,955 +1,958 @@ /*************************************************************************** kstarsinit.cpp - K Desktop Planetarium ------------------- begin : Mon Feb 25 2002 copyright : (C) 2002 by Jason Harris email : jharris@30doradus.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kstars.h" #include "fov.h" #include "kspaths.h" #include "kstarsdata.h" #include "kstars_debug.h" #include "Options.h" #include "skymap.h" #include "texturemanager.h" #include "projections/projector.h" #include "skycomponents/skymapcomposite.h" #include "skyobjects/ksplanetbase.h" #include "widgets/timespinbox.h" #include "widgets/timestepbox.h" #include "hips/hipsmanager.h" #include "auxiliary/thememanager.h" #ifdef HAVE_INDI #include "indi/drivermanager.h" #include "indi/guimanager.h" #include "ekos/manager.h" #endif #include #include #include #include #include #include #include #include //This file contains functions that kstars calls at startup (except constructors). //These functions are declared in kstars.h namespace { // A lot of QAction is defined there. In order to decrease amount // of boilerplate code a trick with << operator overloading is used. // This makes code more concise and readable. // // When data type could not used directly. Either because of // overloading rules or because one data type have different // semantics its wrapped into struct. // // Downside is unfamiliar syntax and really unhelpful error // messages due to general abuse of << overloading // Set QAction text QAction *operator<<(QAction *ka, QString text) { ka->setText(text); return ka; } // Set icon for QAction QAction *operator<<(QAction *ka, const QIcon &icon) { ka->setIcon(icon); return ka; } // Set keyboard shortcut QAction *operator<<(QAction *ka, const QKeySequence sh) { KStars::Instance()->actionCollection()->setDefaultShortcut(ka, sh); //ka->setShortcut(sh); return ka; } // Add action to group. AddToGroup struct acts as newtype wrapper // in order to allow overloading. struct AddToGroup { QActionGroup *grp; AddToGroup(QActionGroup *g) : grp(g) {} }; QAction *operator<<(QAction *ka, AddToGroup g) { g.grp->addAction(ka); return ka; } // Set checked property. Checked is newtype wrapper. struct Checked { bool flag; Checked(bool f) : flag(f) {} }; QAction *operator<<(QAction *ka, Checked chk) { ka->setCheckable(true); ka->setChecked(chk.flag); return ka; } // Set tool tip. ToolTip is used as newtype wrapper. struct ToolTip { QString tip; ToolTip(QString msg) : tip(msg) {} }; QAction *operator<<(QAction *ka, const ToolTip &tool) { ka->setToolTip(tool.tip); return ka; } // Create new KToggleAction and connect slot to toggled(bool) signal QAction *newToggleAction(KActionCollection *col, QString name, QString text, QObject *receiver, const char *member) { QAction *ka = col->add(name) << text; QObject::connect(ka, SIGNAL(toggled(bool)), receiver, member); return ka; } } void KStars::initActions() { // Check if we have this specific Breeze icon. If not, try to set the theme search path and if appropriate, the icon theme rcc file // in each OS if (!QIcon::hasThemeIcon(QLatin1String("kstars_flag"))) KSTheme::Manager::instance()->setIconTheme(KSTheme::Manager::BREEZE_DARK_THEME); QAction *ka; // ==== File menu ================ ka = KNS3::standardAction(i18n("Download New Data..."), this, SLOT(slotDownload()), actionCollection(), "get_data") << QKeySequence(Qt::CTRL + Qt::Key_N); ka->setIcon(QIcon::fromTheme("favorites")); ka->setWhatsThis(i18n("Downloads new data")); ka->setToolTip(ka->whatsThis()); ka->setStatusTip(ka->whatsThis()); #ifdef HAVE_CFITSIO actionCollection()->addAction("open_file", this, SLOT(slotOpenFITS())) << i18n("Open FITS...") << QIcon::fromTheme("document-open") << QKeySequence(Qt::CTRL + Qt::Key_O); #endif actionCollection()->addAction("export_image", this, SLOT(slotExportImage())) << i18n("&Save Sky Image...") << QIcon::fromTheme("document-export-image"); // 2017-09-17 Jasem: FIXME! Scripting does not work properly under non UNIX systems. // It must be updated to use DBus session bus from Qt (like scheduler) #ifndef Q_OS_WIN actionCollection()->addAction("run_script", this, SLOT(slotRunScript())) << i18n("&Run Script...") << QIcon::fromTheme("system-run") << QKeySequence(Qt::CTRL + Qt::Key_R); #endif actionCollection()->addAction("printing_wizard", this, SLOT(slotPrintingWizard())) << i18nc("start Printing Wizard", "Printing &Wizard"); ka = actionCollection()->addAction(KStandardAction::Print, "print", this, SLOT(slotPrint())); ka->setIcon(QIcon::fromTheme("document-print")); //actionCollection()->addAction( KStandardAction::Quit, "quit", this, SLOT(close) ); ka = actionCollection()->addAction(KStandardAction::Quit, "quit", qApp, SLOT(closeAllWindows())); ka->setIcon(QIcon::fromTheme("application-exit")); // ==== Time Menu ================ actionCollection()->addAction("time_to_now", this, SLOT(slotSetTimeToNow())) << i18n("Set Time to &Now") << QKeySequence(Qt::CTRL + Qt::Key_E) << QIcon::fromTheme("clock"); actionCollection()->addAction("time_dialog", this, SLOT(slotSetTime())) << i18nc("set Clock to New Time", "&Set Time...") << QKeySequence(Qt::CTRL + Qt::Key_S) << QIcon::fromTheme("clock"); ka = actionCollection()->add("clock_startstop") << i18n("Stop &Clock") << QIcon::fromTheme("media-playback-pause"); if (!StartClockRunning) ka->toggle(); QObject::connect(ka, SIGNAL(triggered()), this, SLOT(slotToggleTimer())); // If we are started in --paused state make sure the icon reflects that now if (StartClockRunning == false) { QAction *a = actionCollection()->action("clock_startstop"); if (a) a->setIcon(QIcon::fromTheme("run-build-install-root")); } QObject::connect(data()->clock(), &SimClock::clockToggled, [ = ](bool toggled) { QAction *a = actionCollection()->action("clock_startstop"); if (a) { a->setChecked(toggled); // Many users forget to unpause KStars, so we are using run-build-install-root icon which is red // and stands out from the rest of the icons so users are aware when KStars is paused visually a->setIcon(toggled ? QIcon::fromTheme("run-build-install-root") : QIcon::fromTheme("media-playback-pause")); a->setToolTip(toggled ? i18n("Resume Clock") : i18n("Stop Clock")); } }); //UpdateTime() if clock is stopped (so hidden objects get drawn) QObject::connect(data()->clock(), SIGNAL(clockToggled(bool)), this, SLOT(updateTime())); actionCollection()->addAction("time_step_forward", this, SLOT(slotStepForward())) << i18n("Advance one step forward in time") << QIcon::fromTheme("media-skip-forward") << QKeySequence(Qt::Key_Greater); actionCollection()->addAction("time_step_backward", this, SLOT(slotStepBackward())) << i18n("Advance one step backward in time") << QIcon::fromTheme("media-skip-backward") << QKeySequence(Qt::Key_Less); // ==== Pointing Menu ================ actionCollection()->addAction("zenith", this, SLOT(slotPointFocus())) << i18n("&Zenith") << QKeySequence("Z"); actionCollection()->addAction("north", this, SLOT(slotPointFocus())) << i18n("&North") << QKeySequence("N"); actionCollection()->addAction("east", this, SLOT(slotPointFocus())) << i18n("&East") << QKeySequence("E"); actionCollection()->addAction("south", this, SLOT(slotPointFocus())) << i18n("&South") << QKeySequence("S"); actionCollection()->addAction("west", this, SLOT(slotPointFocus())) << i18n("&West") << QKeySequence("W"); actionCollection()->addAction("find_object", this, SLOT(slotFind())) << i18n("&Find Object...") << QIcon::fromTheme("edit-find") << QKeySequence(Qt::CTRL + Qt::Key_F); actionCollection()->addAction("track_object", this, SLOT(slotTrack())) << i18n("Engage &Tracking") << QIcon::fromTheme("object-locked") << QKeySequence(Qt::CTRL + Qt::Key_T); actionCollection()->addAction("manual_focus", this, SLOT(slotManualFocus())) << i18n("Set Coordinates &Manually...") << QKeySequence(Qt::CTRL + Qt::Key_M); QAction *action; // ==== View Menu ================ action = actionCollection()->addAction(KStandardAction::ZoomIn, "zoom_in", map(), SLOT(slotZoomIn())); action->setIcon(QIcon::fromTheme("zoom-in")); action = actionCollection()->addAction(KStandardAction::ZoomOut, "zoom_out", map(), SLOT(slotZoomOut())); action->setIcon(QIcon::fromTheme("zoom-out")); actionCollection()->addAction("zoom_default", map(), SLOT(slotZoomDefault())) << i18n("&Default Zoom") << QIcon::fromTheme("zoom-fit-best") << QKeySequence(Qt::CTRL + Qt::Key_Z); actionCollection()->addAction("zoom_set", this, SLOT(slotSetZoom())) << i18n("&Zoom to Angular Size...") << QIcon::fromTheme("zoom-original") << QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z); action = actionCollection()->addAction(KStandardAction::FullScreen, this, SLOT(slotFullScreen())); action->setIcon(QIcon::fromTheme("view-fullscreen")); actionCollection()->addAction("coordsys", this, SLOT(slotCoordSys())) << (Options::useAltAz() ? i18n("Switch to star globe view (Equatorial &Coordinates)") : i18n("Switch to horizonal view (Horizontal &Coordinates)")) << QKeySequence("Space"); actionCollection()->addAction("project_lambert", this, SLOT(slotMapProjection())) << i18n("&Lambert Azimuthal Equal-area") << QKeySequence("F5") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::Lambert); actionCollection()->addAction("project_azequidistant", this, SLOT(slotMapProjection())) << i18n("&Azimuthal Equidistant") << QKeySequence("F6") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::AzimuthalEquidistant); actionCollection()->addAction("project_orthographic", this, SLOT(slotMapProjection())) << i18n("&Orthographic") << QKeySequence("F7") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::Orthographic); actionCollection()->addAction("project_equirectangular", this, SLOT(slotMapProjection())) << i18n("&Equirectangular") << QKeySequence("F8") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::Equirectangular); actionCollection()->addAction("project_stereographic", this, SLOT(slotMapProjection())) << i18n("&Stereographic") << QKeySequence("F9") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::Stereographic); actionCollection()->addAction("project_gnomonic", this, SLOT(slotMapProjection())) << i18n("&Gnomonic") << QKeySequence("F10") << AddToGroup(projectionGroup) << Checked(Options::projection() == Projector::Gnomonic); //Settings Menu: //Info Boxes option actions QAction *kaBoxes = actionCollection()->add("show_boxes") << i18nc("Show the information boxes", "Show &Info Boxes") << Checked(Options::showInfoBoxes()); connect(kaBoxes, SIGNAL(toggled(bool)), map(), SLOT(slotToggleInfoboxes(bool))); kaBoxes->setChecked(Options::showInfoBoxes()); ka = actionCollection()->add("show_time_box") << i18nc("Show time-related info box", "Show &Time Box"); connect(kaBoxes, SIGNAL(toggled(bool)), ka, SLOT(setEnabled(bool))); connect(ka, SIGNAL(toggled(bool)), map(), SLOT(slotToggleTimeBox(bool))); ka->setChecked(Options::showTimeBox()); ka->setEnabled(Options::showInfoBoxes()); ka = actionCollection()->add("show_focus_box") << i18nc("Show focus-related info box", "Show &Focus Box"); connect(kaBoxes, SIGNAL(toggled(bool)), ka, SLOT(setEnabled(bool))); connect(ka, SIGNAL(toggled(bool)), map(), SLOT(slotToggleFocusBox(bool))); ka->setChecked(Options::showFocusBox()); ka->setEnabled(Options::showInfoBoxes()); ka = actionCollection()->add("show_location_box") << i18nc("Show location-related info box", "Show &Location Box"); connect(kaBoxes, SIGNAL(toggled(bool)), ka, SLOT(setEnabled(bool))); connect(ka, SIGNAL(toggled(bool)), map(), SLOT(slotToggleGeoBox(bool))); ka->setChecked(Options::showGeoBox()); ka->setEnabled(Options::showInfoBoxes()); //Toolbar options newToggleAction(actionCollection(), "show_mainToolBar", i18n("Show Main Toolbar"), toolBar("kstarsToolBar"), SLOT(setVisible(bool))); newToggleAction(actionCollection(), "show_viewToolBar", i18n("Show View Toolbar"), toolBar("viewToolBar"), SLOT(setVisible(bool))); //Statusbar view options newToggleAction(actionCollection(), "show_statusBar", i18n("Show Statusbar"), this, SLOT(slotShowGUIItem(bool))); newToggleAction(actionCollection(), "show_sbAzAlt", i18n("Show Az/Alt Field"), this, SLOT(slotShowGUIItem(bool))); newToggleAction(actionCollection(), "show_sbRADec", i18n("Show RA/Dec Field"), this, SLOT(slotShowGUIItem(bool))); newToggleAction(actionCollection(), "show_sbJ2000RADec", i18n("Show J2000.0 RA/Dec Field"), this, SLOT(slotShowGUIItem(bool))); populateThemes(); //Color scheme actions. These are added to the "colorschemes" KActionMenu. colorActionMenu = actionCollection()->add("colorschemes"); colorActionMenu->setText(i18n("C&olor Schemes")); addColorMenuItem(i18n("&Classic"), "cs_classic"); addColorMenuItem(i18n("&Star Chart"), "cs_chart"); addColorMenuItem(i18n("&Night Vision"), "cs_night"); addColorMenuItem(i18n("&Moonless Night"), "cs_moonless-night"); //Add any user-defined color schemes: QFile file(KSPaths::locate(QStandardPaths::GenericDataLocation, "colors.dat")); //determine filename in local user KDE directory tree. if (file.exists() && file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); while (!stream.atEnd()) { QString line = stream.readLine(); QString schemeName = line.left(line.indexOf(':')); QString actionname = "cs_" + line.mid(line.indexOf(':') + 1, line.indexOf('.') - line.indexOf(':') - 1); addColorMenuItem(i18n(schemeName.toLocal8Bit()), actionname.toLocal8Bit()); } file.close(); } //Add FOV Symbol actions fovActionMenu = actionCollection()->add("fovsymbols"); fovActionMenu->setText(i18n("&FOV Symbols")); fovActionMenu->setDelayed(false); fovActionMenu->setIcon(QIcon::fromTheme("crosshairs")); FOVManager::readFOVs(); repopulateFOV(); //Add HIPS Sources actions hipsActionMenu = actionCollection()->add("hipssources"); hipsActionMenu->setText(i18n("HiPS All Sky Overlay")); hipsActionMenu->setDelayed(false); hipsActionMenu->setIcon(QIcon::fromTheme("view-preview")); HIPSManager::Instance()->readSources(); repopulateHIPS(); actionCollection()->addAction("geolocation", this, SLOT(slotGeoLocator())) << i18nc("Location on Earth", "&Geographic...") << QIcon::fromTheme("kstars_xplanet") << QKeySequence(Qt::CTRL + Qt::Key_G); // Configure Notifications #ifdef HAVE_NOTIFYCONFIG KStandardAction::configureNotifications(this, SLOT(slotConfigureNotifications()), actionCollection()); #endif + // Prepare the options dialog early for modules to connect signals + prepareOps(); + ka = actionCollection()->addAction(KStandardAction::Preferences, "configure", this, SLOT(slotViewOps())); //I am not sure what icon preferences is supposed to be. //ka->setIcon( QIcon::fromTheme("")); actionCollection()->addAction("startwizard", this, SLOT(slotWizard())) << i18n("Startup Wizard...") << QIcon::fromTheme("tools-wizard"); // Manual data entry actionCollection()->addAction("manual_add_dso", this, SLOT(slotAddDeepSkyObject())) << i18n("Manually add a deep-sky object"); // Updates actions actionCollection()->addAction("update_comets", this, SLOT(slotUpdateComets())) << i18n("Update comets orbital elements"); actionCollection()->addAction("update_asteroids", this, SLOT(slotUpdateAsteroids())) << i18n("Update asteroids orbital elements"); actionCollection()->addAction("update_supernovae", this, SLOT(slotUpdateSupernovae())) << i18n("Update Recent Supernovae data"); actionCollection()->addAction("update_satellites", this, SLOT(slotUpdateSatellites())) << i18n("Update satellites orbital elements"); //Tools Menu: actionCollection()->addAction("astrocalculator", this, SLOT(slotCalculator())) << i18n("Calculator") << QIcon::fromTheme("accessories-calculator") << QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_C); /* FIXME Enable once port to KF5 is complete for moonphasetool actionCollection()->addAction("moonphasetool", this, SLOT(slotMoonPhaseTool()) ) << i18n("Moon Phase Calendar"); */ actionCollection()->addAction("obslist", this, SLOT(slotObsList())) << i18n("Observation Planner") << QKeySequence(Qt::CTRL + Qt::Key_L); actionCollection()->addAction("altitude_vs_time", this, SLOT(slotAVT())) << i18n("Altitude vs. Time") << QKeySequence(Qt::CTRL + Qt::Key_A); actionCollection()->addAction("whats_up_tonight", this, SLOT(slotWUT())) << i18n("What's up Tonight") << QKeySequence(Qt::CTRL + Qt::Key_U); //FIXME Port to QML2 //#if 0 actionCollection()->addAction("whats_interesting", this, SLOT(slotToggleWIView())) << i18n("What's Interesting...") << QKeySequence(Qt::CTRL + Qt::Key_W); //#endif actionCollection()->addAction("XPlanet", map(), SLOT(slotStartXplanetViewer())) << i18n("XPlanet Solar System Simulator") << QKeySequence(Qt::CTRL + Qt::Key_X); actionCollection()->addAction("skycalendar", this, SLOT(slotCalendar())) << i18n("Sky Calendar"); #ifdef HAVE_INDI ka = actionCollection()->addAction("ekos", this, SLOT(slotEkos())) << i18n("Ekos") << QKeySequence(Qt::CTRL + Qt::Key_K); ka->setShortcutContext(Qt::ApplicationShortcut); #endif //FIXME: implement glossary // ka = actionCollection()->addAction("glossary"); // ka->setText( i18n("Glossary...") ); // ka->setShortcuts( QKeySequence(Qt::CTRL+Qt::Key_K ) ); // connect( ka, SIGNAL(triggered()), this, SLOT(slotGlossary()) ); // 2017-09-17 Jasem: FIXME! Scripting does not work properly under non UNIX systems. // It must be updated to use DBus session bus from Qt (like scheduler) #ifndef Q_OS_WIN actionCollection()->addAction("scriptbuilder", this, SLOT(slotScriptBuilder())) << i18n("Script Builder") << QKeySequence(Qt::CTRL + Qt::Key_B); #endif actionCollection()->addAction("solarsystem", this, SLOT(slotSolarSystem())) << i18n("Solar System") << QKeySequence(Qt::CTRL + Qt::Key_Y); // Disabled until fixed later /*actionCollection()->addAction("jmoontool", this, SLOT(slotJMoonTool()) ) << i18n("Jupiter's Moons") << QKeySequence(Qt::CTRL+Qt::Key_J );*/ actionCollection()->addAction("flagmanager", this, SLOT(slotFlagManager())) << i18n("Flags"); actionCollection()->addAction("equipmentwriter", this, SLOT(slotEquipmentWriter())) << i18n("List your &Equipment...") << QIcon::fromTheme("kstars") << QKeySequence(Qt::CTRL + Qt::Key_0); actionCollection()->addAction("manageobserver", this, SLOT(slotObserverManager())) << i18n("Manage Observer...") << QIcon::fromTheme("im-user") << QKeySequence(Qt::CTRL + Qt::Key_1); //TODO only enable it when finished actionCollection()->addAction("artificialhorizon", this, SLOT(slotHorizonManager())) << i18n("Artificial Horizon..."); // ==== observation menu - execute ================ actionCollection()->addAction("execute", this, SLOT(slotExecute())) << i18n("Execute the session Plan...") << QKeySequence(Qt::CTRL + Qt::Key_2); // ==== observation menu - polaris hour angle ================ actionCollection()->addAction("polaris_hour_angle", this, SLOT(slotPolarisHourAngle())) << i18n("Polaris Hour Angle..."); // ==== devices Menu ================ #ifdef HAVE_INDI #ifndef Q_OS_WIN #if 0 actionCollection()->addAction("telescope_wizard", this, SLOT(slotTelescopeWizard())) << i18n("Telescope Wizard...") << QIcon::fromTheme("tools-wizard"); #endif #endif actionCollection()->addAction("device_manager", this, SLOT(slotINDIDriver())) << i18n("Device Manager...") << QIcon::fromTheme("network-server") << QKeySequence(Qt::SHIFT + Qt::META + Qt::Key_D); actionCollection()->addAction("custom_drivers", DriverManager::Instance(), SLOT(showCustomDrivers())) << i18n("Custom Drivers...") << QIcon::fromTheme("address-book-new"); ka = actionCollection()->addAction("indi_cpl", this, SLOT(slotINDIPanel())) << i18n("INDI Control Panel...") << QKeySequence(Qt::CTRL + Qt::Key_I); ka->setShortcutContext(Qt::ApplicationShortcut); ka->setEnabled(false); #else //FIXME need to disable/hide devices submenu in the tools menu. It is created from the kstarsui.rc file //but I don't know how to hide/disable it yet. menuBar()->findChildren() does not return any children that I can //iterate over. Anyway to resolve this? #endif //Help Menu: ka = actionCollection()->addAction(KStandardAction::TipofDay, "help_tipofday", this, SLOT(slotTipOfDay())); ka->setWhatsThis(i18n("Displays the Tip of the Day")); ka->setIcon(QIcon::fromTheme("help-hint")); // KStandardAction::help(this, SLOT(appHelpActivated()), actionCollection(), "help_contents" ); //Add timestep widget for toolbar m_TimeStepBox = new TimeStepBox(toolBar("kstarsToolBar")); // Add a tool tip to TimeStep describing the weird nature of time steps QString TSBToolTip = i18nc("Tooltip describing the nature of the time step control", "Use this to set the rate at which time in the simulation flows.\nFor time step \'X\' " "up to 10 minutes, time passes at the rate of \'X\' per second.\nFor time steps larger " "than 10 minutes, frames are displayed at an interval of \'X\'."); m_TimeStepBox->setToolTip(TSBToolTip); m_TimeStepBox->tsbox()->setToolTip(TSBToolTip); QWidgetAction *wa = new QWidgetAction(this); wa->setDefaultWidget(m_TimeStepBox); actionCollection()->addAction("timestep_control", wa) << i18n("Time step control"); // ==== viewToolBar actions ================ actionCollection()->add("show_stars", this, SLOT(slotViewToolBar())) << i18nc("Toggle Stars in the display", "Stars") << QIcon::fromTheme("kstars_stars") << ToolTip(i18n("Toggle stars")); actionCollection()->add("show_deepsky", this, SLOT(slotViewToolBar())) << i18nc("Toggle Deep Sky Objects in the display", "Deep Sky") << QIcon::fromTheme("kstars_deepsky") << ToolTip(i18n("Toggle deep sky objects")); actionCollection()->add("show_planets", this, SLOT(slotViewToolBar())) << i18nc("Toggle Solar System objects in the display", "Solar System") << QIcon::fromTheme("kstars_planets") << ToolTip(i18n("Toggle Solar system objects")); actionCollection()->add("show_clines", this, SLOT(slotViewToolBar())) << i18nc("Toggle Constellation Lines in the display", "Const. Lines") << QIcon::fromTheme("kstars_clines") << ToolTip(i18n("Toggle constellation lines")); actionCollection()->add("show_cnames", this, SLOT(slotViewToolBar())) << i18nc("Toggle Constellation Names in the display", "Const. Names") << QIcon::fromTheme("kstars_cnames") << ToolTip(i18n("Toggle constellation names")); actionCollection()->add("show_cbounds", this, SLOT(slotViewToolBar())) << i18nc("Toggle Constellation Boundaries in the display", "C. Boundaries") << QIcon::fromTheme("kstars_cbound") << ToolTip(i18n("Toggle constellation boundaries")); actionCollection()->add("show_constellationart", this, SLOT(slotViewToolBar())) << xi18nc("Toggle Constellation Art in the display", "C. Art (BETA)") << QIcon::fromTheme("kstars_constellationart") << ToolTip(xi18n("Toggle constellation art (BETA)")); actionCollection()->add("show_mw", this, SLOT(slotViewToolBar())) << i18nc("Toggle Milky Way in the display", "Milky Way") << QIcon::fromTheme("kstars_mw") << ToolTip(i18n("Toggle milky way")); actionCollection()->add("show_equatorial_grid", this, SLOT(slotViewToolBar())) << i18nc("Toggle Equatorial Coordinate Grid in the display", "Equatorial coord. grid") << QIcon::fromTheme("kstars_grid") << ToolTip(i18n("Toggle equatorial coordinate grid")); actionCollection()->add("show_horizontal_grid", this, SLOT(slotViewToolBar())) << i18nc("Toggle Horizontal Coordinate Grid in the display", "Horizontal coord. grid") << QIcon::fromTheme("kstars_hgrid") << ToolTip(i18n("Toggle horizontal coordinate grid")); actionCollection()->add("show_horizon", this, SLOT(slotViewToolBar())) << i18nc("Toggle the opaque fill of the ground polygon in the display", "Ground") << QIcon::fromTheme("kstars_horizon") << ToolTip(i18n("Toggle opaque ground")); actionCollection()->add("show_flags", this, SLOT(slotViewToolBar())) << i18nc("Toggle flags in the display", "Flags") << QIcon::fromTheme("kstars_flag") << ToolTip(i18n("Toggle flags")); actionCollection()->add("show_satellites", this, SLOT(slotViewToolBar())) << i18nc("Toggle satellites in the display", "Satellites") << QIcon::fromTheme("kstars_satellites") << ToolTip(i18n("Toggle satellites")); actionCollection()->add("show_supernovae", this, SLOT(slotViewToolBar())) << i18nc("Toggle supernovae in the display", "Supernovae") << QIcon::fromTheme("kstars_supernovae") << ToolTip(i18n("Toggle supernovae")); actionCollection()->add("show_whatsinteresting", this, SLOT(slotToggleWIView())) << i18nc("Toggle What's Interesting", "What's Interesting") << QIcon::fromTheme("view-list-details") << ToolTip(i18n("Toggle What's Interesting")); #ifdef HAVE_INDI // ==== INDIToolBar actions ================ actionCollection()->add("show_ekos", this, SLOT(slotINDIToolBar())) << i18nc("Toggle Ekos in the display", "Ekos") << QIcon::fromTheme("kstars_ekos") << ToolTip(i18n("Toggle Ekos")); ka = actionCollection()->add("show_control_panel", this, SLOT(slotINDIToolBar())) << i18nc("Toggle the INDI Control Panel in the display", "INDI Control Panel") << QIcon::fromTheme("kstars_indi") << ToolTip(i18n("Toggle INDI Control Panel")); ka->setEnabled(false); ka = actionCollection()->add("show_fits_viewer", this, SLOT(slotINDIToolBar())) << i18nc("Toggle the FITS Viewer in the display", "FITS Viewer") << QIcon::fromTheme("kstars_fitsviewer") << ToolTip(i18n("Toggle FITS Viewer")); ka->setEnabled(false); ka = actionCollection()->add("show_sensor_fov", this, SLOT(slotINDIToolBar())) << i18nc("Toggle the sensor Field of View", "Sensor FOV") << QIcon::fromTheme("archive-extract") << ToolTip(i18n("Toggle Sensor FOV")); ka->setEnabled(false); ka->setChecked(Options::showSensorFOV()); ka = actionCollection()->add("show_mount_box", this, SLOT(slotINDIToolBar())) << i18nc("Toggle the Mount Control Panel", "Mount Control") << QIcon::fromTheme("draw-text") << ToolTip(i18n("Toggle Mount Control Panel")); telescopeGroup->addAction(ka); ka = actionCollection()->add("lock_telescope", this, SLOT(slotINDIToolBar())) << i18nc("Toggle the telescope center lock in display", "Center Telescope") << QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")) << ToolTip(i18n("Toggle Lock Telescope Center")); telescopeGroup->addAction(ka); ka = actionCollection()->add("telescope_track", this, SLOT(slotINDITelescopeTrack())) << i18n("Toggle Telescope Tracking") << QIcon::fromTheme("object-locked"); telescopeGroup->addAction(ka); ka = actionCollection()->addAction("telescope_slew", this, SLOT(slotINDITelescopeSlew())) << i18n("Slew telescope to the focused object") << QIcon::fromTheme("object-rotate-right"); telescopeGroup->addAction(ka); ka = actionCollection()->addAction("telescope_sync", this, SLOT(slotINDITelescopeSync())) << i18n("Sync telescope to the focused object") << QIcon::fromTheme("media-record"); telescopeGroup->addAction(ka); ka = actionCollection()->addAction("telescope_abort", this, SLOT(slotINDITelescopeAbort())) << i18n("Abort telescope motions") << QIcon::fromTheme("process-stop"); ka->setShortcutContext(Qt::ApplicationShortcut); telescopeGroup->addAction(ka); ka = actionCollection()->addAction("telescope_park", this, SLOT(slotINDITelescopePark())) << i18n("Park telescope") << QIcon::fromTheme("flag-red"); telescopeGroup->addAction(ka); ka = actionCollection()->addAction("telescope_unpark", this, SLOT(slotINDITelescopeUnpark())) << i18n("Unpark telescope") << QIcon::fromTheme("flag-green"); ka->setShortcutContext(Qt::ApplicationShortcut); telescopeGroup->addAction(ka); actionCollection()->addAction("telescope_slew_mouse", this, SLOT(slotINDITelescopeSlewMousePointer())) << i18n("Slew the telescope to the mouse pointer position"); actionCollection()->addAction("telescope_sync_mouse", this, SLOT(slotINDITelescopeSyncMousePointer())) << i18n("Sync the telescope to the mouse pointer position"); // Disable all telescope actions by default telescopeGroup->setEnabled(false); // Dome Actions ka = actionCollection()->addAction("dome_park", this, SLOT(slotINDIDomePark())) << i18n("Park dome") << QIcon::fromTheme("dome-park", QIcon(":/icons/dome-park.svg")); domeGroup->addAction(ka); ka = actionCollection()->addAction("dome_unpark", this, SLOT(slotINDIDomeUnpark())) << i18n("Unpark dome") << QIcon::fromTheme("dome-unpark", QIcon(":/icons/dome-unpark.svg")); ka->setShortcutContext(Qt::ApplicationShortcut); domeGroup->addAction(ka); domeGroup->setEnabled(false); #endif } void KStars::repopulateFOV() { // Read list of all FOVs //qDeleteAll( data()->availFOVs ); data()->availFOVs = FOVManager::getFOVs(); data()->syncFOV(); // Iterate through FOVs fovActionMenu->menu()->clear(); foreach (FOV *fov, data()->availFOVs) { KToggleAction *kta = actionCollection()->add(fov->name()); kta->setText(fov->name()); if (Options::fOVNames().contains(fov->name())) { kta->setChecked(true); } fovActionMenu->addAction(kta); connect(kta, SIGNAL(toggled(bool)), this, SLOT(slotTargetSymbol(bool))); } // Add menu bottom QAction *ka = actionCollection()->addAction("edit_fov", this, SLOT(slotFOVEdit())) << i18n("Edit FOV Symbols..."); fovActionMenu->addSeparator(); fovActionMenu->addAction(ka); } void KStars::repopulateHIPS() { // Iterate through actions hipsActionMenu->menu()->clear(); // Remove all actions QList actions = hipsGroup->actions(); for (auto &action : actions) hipsGroup->removeAction(action); QAction *ka = actionCollection()->addAction(i18n("None"), this, SLOT(slotHIPSSource())) << i18n("None") << AddToGroup(hipsGroup) << Checked(Options::hIPSSource() == "None"); hipsActionMenu->addAction(ka); hipsActionMenu->addSeparator(); for (QMap source : HIPSManager::Instance()->getHIPSSources()) { QString title = source.value("obs_title"); QAction *ka = actionCollection()->addAction(title, this, SLOT(slotHIPSSource())) << title << AddToGroup(hipsGroup) << Checked(Options::hIPSSource() == title); hipsActionMenu->addAction(ka); } // Hips settings ka = actionCollection()->addAction("hipssettings", HIPSManager::Instance(), SLOT(showSettings())) << i18n("HiPS Settings..."); hipsActionMenu->addSeparator(); hipsActionMenu->addAction(ka); } void KStars::initStatusBar() { statusBar()->showMessage(i18n(" Welcome to KStars ")); QString s = "000d 00m 00s, +00d 00\' 00\""; //only need this to set the width if (Options::showAltAzField()) { AltAzField.setText(s); statusBar()->insertPermanentWidget(0, &AltAzField); } if (Options::showRADecField()) { RADecField.setText(s); statusBar()->insertPermanentWidget(1, &RADecField); } if (Options::showJ2000RADecField()) { J2000RADecField.setText(s); statusBar()->insertPermanentWidget(1, &J2000RADecField); } if (!Options::showStatusBar()) statusBar()->hide(); } void KStars::datainitFinished() { //Time-related connections connect(data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(updateTime())); connect(data()->clock(), SIGNAL(timeChanged()), this, SLOT(updateTime())); //Add GUI elements to main window buildGUI(); connect(data()->clock(), SIGNAL(scaleChanged(float)), map(), SLOT(slotClockSlewing())); connect(data(), SIGNAL(skyUpdate(bool)), map(), SLOT(forceUpdateNow())); connect(m_TimeStepBox, SIGNAL(scaleChanged(float)), data(), SLOT(setTimeDirection(float))); connect(m_TimeStepBox, SIGNAL(scaleChanged(float)), data()->clock(), SLOT(setClockScale(float))); connect(m_TimeStepBox, SIGNAL(scaleChanged(float)), map(), SLOT(setFocus())); //m_equipmentWriter = new EquipmentWriter(); //m_observerAdd = new ObserverAdd; //Do not start the clock if "--paused" specified on the cmd line if (StartClockRunning) { // The initial time is set when KStars is first executed // but until all data is loaded, some time elapsed already so we need to synchronize if no Start Date string // was supplied to KStars if (StartDateString.isEmpty()) data()->changeDateTime(KStarsDateTime::currentDateTimeUtc()); data()->clock()->start(); } // Connect cache function for Find dialog connect(data(), SIGNAL(clearCache()), this, SLOT(clearCachedFindDialog())); //Propagate config settings applyConfig(false); //show the window. must be before kswizard and messageboxes show(); //Initialize focus initFocus(); data()->setFullTimeUpdate(); updateTime(); // Initial State qCDebug(KSTARS) << "Date/Time is:" << data()->clock()->utc().toString(); qCDebug(KSTARS) << "Location:" << data()->geo()->fullName(); qCDebug(KSTARS) << "TZ0:" << data()->geo()->TZ0() << "TZ:" << data()->geo()->TZ(); KSTheme::Manager::instance()->setCurrentTheme(Options::currentTheme()); //If this is the first startup, show the wizard if (Options::runStartupWizard()) { slotWizard(); } //Show TotD KTipDialog::showTip(this, "kstars/tips"); // Update comets and asteroids if enabled. if (Options::orbitalElementsAutoUpdate()) { slotUpdateComets(true); slotUpdateAsteroids(true); } } void KStars::initFocus() { //Case 1: tracking on an object if (Options::isTracking() && Options::focusObject() != i18n("nothing")) { SkyObject *oFocus; if (Options::focusObject() == i18n("star")) { SkyPoint p(Options::focusRA(), Options::focusDec()); double maxrad = 1.0; oFocus = data()->skyComposite()->starNearest(&p, maxrad); } else { oFocus = data()->objectNamed(Options::focusObject()); } if (oFocus) { map()->setFocusObject(oFocus); map()->setClickedObject(oFocus); map()->setFocusPoint(oFocus); } else { qWarning() << "Cannot center on " << Options::focusObject() << ": no object found." << endl; } //Case 2: not tracking, and using Alt/Az coords. Set focus point using //FocusRA as the Azimuth, and FocusDec as the Altitude } else if (!Options::isTracking() && Options::useAltAz()) { SkyPoint pFocus; pFocus.setAz(Options::focusRA()); pFocus.setAlt(Options::focusDec()); pFocus.HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); map()->setFocusPoint(&pFocus); //Default: set focus point using FocusRA as the RA and //FocusDec as the Dec } else { SkyPoint pFocus(Options::focusRA(), Options::focusDec()); pFocus.EquatorialToHorizontal(data()->lst(), data()->geo()->lat()); map()->setFocusPoint(&pFocus); } data()->setSnapNextFocus(); map()->setDestination(*map()->focusPoint()); map()->setFocus(map()->destination()); map()->showFocusCoords(); //Check whether initial position is below the horizon. if (Options::useAltAz() && Options::showGround() && map()->focus()->alt().Degrees() < -1.0) { QString caption = i18n("Initial Position is Below Horizon"); QString message = i18n("The initial position is below the horizon.\nWould you like to reset to the default position?"); if (KMessageBox::warningYesNo(this, message, caption, KGuiItem(i18n("Reset Position")), KGuiItem(i18n("Do Not Reset")), "dag_start_below_horiz") == KMessageBox::Yes) { map()->setClickedObject(nullptr); map()->setFocusObject(nullptr); Options::setIsTracking(false); data()->setSnapNextFocus(true); SkyPoint DefaultFocus; DefaultFocus.setAz(180.0); DefaultFocus.setAlt(45.0); DefaultFocus.HorizontalToEquatorial(data()->lst(), data()->geo()->lat()); map()->setDestination(DefaultFocus); } } //If there is a focusObject() and it is a SS body, add a temporary Trail if (map()->focusObject() && map()->focusObject()->isSolarSystem() && Options::useAutoTrail()) { ((KSPlanetBase *)map()->focusObject())->addToTrail(); data()->temporaryTrail = true; } } void KStars::buildGUI() { //create the texture manager TextureManager::Create(); //create the skymap m_SkyMap = SkyMap::Create(); connect(m_SkyMap, SIGNAL(mousePointChanged(SkyPoint*)), SLOT(slotShowPositionBar(SkyPoint*))); connect(m_SkyMap, SIGNAL(zoomChanged()), SLOT(slotZoomChanged())); setCentralWidget(m_SkyMap); //Initialize menus, toolbars, and statusbars initStatusBar(); initActions(); setupGUI(StandardWindowOptions(Default & ~Create)); createGUI("kstarsui.rc"); //get focus of keyboard and mouse actions (for example zoom in with +) map()->QWidget::setFocus(); resize(Options::windowWidth(), Options::windowHeight()); // check zoom in/out buttons if (Options::zoomFactor() >= MAXZOOM) actionCollection()->action("zoom_in")->setEnabled(false); if (Options::zoomFactor() <= MINZOOM) actionCollection()->action("zoom_out")->setEnabled(false); } void KStars::populateThemes() { KSTheme::Manager::instance()->setThemeMenuAction(new QMenu(i18n("&Themes"), this)); KSTheme::Manager::instance()->registerThemeActions(this); connect(KSTheme::Manager::instance(), SIGNAL(signalThemeChanged()), this, SLOT(slotThemeChanged())); } void KStars::slotThemeChanged() { Options::setCurrentTheme(KSTheme::Manager::instance()->currentThemeName()); }